Compare commits
@@ -1,5 +1,5 @@
|
||||
freebsd_instance:
|
||||
image_family: freebsd-14-2
|
||||
image_family: freebsd-14-3
|
||||
|
||||
build_task:
|
||||
# Don't change this name without adjusting .github/workflows/build.yaml
|
||||
@@ -35,7 +35,7 @@ build_task:
|
||||
- find main src -type f -iname '*.hs' -exec md5sum "{}" +
|
||||
|
||||
build_script: |
|
||||
stack build -j 1 --local-bin-path . --copy-bins --stack-yaml stack-21.7.yaml
|
||||
stack build -j 1 --local-bin-path . --copy-bins
|
||||
strip postgrest
|
||||
|
||||
bin_artifacts:
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
@@ -1,17 +0,0 @@
|
||||
<!--
|
||||
Before reporting a bug:
|
||||
If your database schema has changed while the PostgREST server is running,
|
||||
send the server a SIGUSR1 signal or restart it(http://postgrest.org/en/stable/admin.html#schema-reloading)
|
||||
to ensure the schema cache is not stale. This sometimes fixes apparent bugs.
|
||||
-->
|
||||
### Environment
|
||||
|
||||
* PostgreSQL version: (if using docker, specify the image)
|
||||
* PostgREST version: (if using docker, specify the image)
|
||||
* Operating system:
|
||||
|
||||
### Description of issue
|
||||
|
||||
(Expected behavior vs actual behavior)
|
||||
|
||||
(Steps to reproduce: Include a minimal SQL definition plus how you make the request to PostgREST and the response body)
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report to help us improve
|
||||
type: Bug
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Before reporting a bug:
|
||||
If your database schema has changed while the PostgREST server is running,
|
||||
send the server a SIGUSR1 signal or restart it (http://postgrest.org/en/stable/admin.html#schema-reloading) to ensure the schema cache is not stale. This sometimes fixes apparent bugs.
|
||||
-->
|
||||
### Environment
|
||||
|
||||
* PostgreSQL version: (if using docker, specify the image)
|
||||
* PostgREST version: (if using docker, specify the image)
|
||||
* Operating system:
|
||||
|
||||
### Description of issue
|
||||
|
||||
Describe the behavior you expected vs the actual behavior. Include:
|
||||
|
||||
- A minimal SQL definition.
|
||||
- How you make the request to PostgREST (curl command preferred).
|
||||
- The PostgREST response.
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an enhancement for this project
|
||||
type: Feature
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
A clear and concise description of what the problem is.
|
||||
|
||||
## Solution
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
@@ -19,14 +19,14 @@ inputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ inputs.prefix }}-
|
||||
- uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
- uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
|
||||
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
|
||||
@@ -11,7 +11,7 @@ inputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: nixbuild/nix-quick-install-action@5bb6a3b3abe66fd09bbf250dce8ada94f856a703 # v30
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
with:
|
||||
nix_conf: |-
|
||||
always-allow-substitutes = true
|
||||
|
||||
@@ -8,18 +8,33 @@
|
||||
"/^v[0-9]+/"
|
||||
],
|
||||
"rebaseWhen": "conflicted",
|
||||
"pip_requirements": {
|
||||
"enabled": false
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchBaseBranches": [ "/^v[0-9]+/" ],
|
||||
"matchManagers": ["haskell-cabal"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchBaseBranches": [ "/^v[0-9]+/" ],
|
||||
"groupName": "all dependencies"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["docutils"],
|
||||
"allowedVersions": "<0.21.0"
|
||||
"matchManagers": ["haskell-cabal"],
|
||||
"matchPackageNames": ["base", "bytestring", "containers", "directory", "mtl", "parsec", "process", "text"],
|
||||
"groupName": "GHC dependencies"
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["macos"],
|
||||
"allowedVersions": "<13"
|
||||
"matchManagers": ["haskell-cabal"],
|
||||
"matchPackageNames": ["hasql", "hasql-dynamic-statements", "hasql-notifications", "hasql-transaction", "hasql-pool"],
|
||||
"groupName": "hasql"
|
||||
},
|
||||
{
|
||||
"matchManagers": ["haskell-cabal"],
|
||||
"matchPackageNames": ["fuzzyset"],
|
||||
"allowedVersions": "<0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
name: Nix - Linux x86-64 static
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -60,9 +60,9 @@ jobs:
|
||||
|
||||
macos:
|
||||
name: Nix - MacOS
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
artifact: postgrest-ubuntu-aarch64
|
||||
deps: sudo apt-get update && sudo apt-get install libpq-dev
|
||||
|
||||
- name: MacOS
|
||||
- name: MacOS aarch64
|
||||
runs-on: macos-14
|
||||
cache: |
|
||||
~/.stack/pantry
|
||||
@@ -105,6 +105,15 @@ jobs:
|
||||
artifact: postgrest-macos-aarch64
|
||||
deps: brew link --force libpq
|
||||
|
||||
- name: MacOS x86-64
|
||||
runs-on: macos-13
|
||||
cache: |
|
||||
~/.stack/pantry
|
||||
~/.stack/snapshots
|
||||
~/.stack/stack.sqlite3
|
||||
artifact: postgrest-macos-x86-64
|
||||
deps: brew link --force libpq
|
||||
|
||||
- name: Windows
|
||||
runs-on: windows-2022
|
||||
cache: |
|
||||
@@ -117,11 +126,11 @@ jobs:
|
||||
name: Stack - ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: haskell-actions/setup@d9b5b3fcf7ca56b8fe585c9b77d3b0ce466affd2 # v2.7.10
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: haskell-actions/setup@82e8b5066385702e477d9dc98f287070493e8abd # v2.8.2
|
||||
with:
|
||||
# This must match the version in stack.yaml's resolver
|
||||
ghc-version: 9.6.5
|
||||
ghc-version: 9.6.7
|
||||
enable-stack: true
|
||||
stack-no-global: true
|
||||
stack-setup-ghc: true
|
||||
@@ -159,7 +168,7 @@ jobs:
|
||||
name: Stack - FreeBSD from CirrusCI
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: ./.github/actions/artifact-from-cirrus
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
@@ -171,13 +180,13 @@ jobs:
|
||||
cabal:
|
||||
strategy:
|
||||
matrix:
|
||||
ghc: ['9.6.4', '9.8.2']
|
||||
ghc: ['9.6.7', '9.8.4']
|
||||
fail-fast: false
|
||||
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: haskell-actions/setup@d9b5b3fcf7ca56b8fe585c9b77d3b0ce466affd2 # v2.7.10
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: haskell-actions/setup@82e8b5066385702e477d9dc98f287070493e8abd # v2.8.2
|
||||
with:
|
||||
ghc-version: ${{ matrix.ghc }}
|
||||
- name: Cache .cabal
|
||||
@@ -186,8 +195,8 @@ jobs:
|
||||
path: |
|
||||
~/.cabal/packages
|
||||
~/.cabal/store
|
||||
prefix: cabal-${{ matrix.ghc }}
|
||||
suffix: ${{ hashFiles('postgrest.cabal', 'cabal.project', 'cabal.project.freeze') }}
|
||||
prefix: cabal-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}
|
||||
suffix: ${{ hashFiles('postgrest.cabal', 'cabal.project') }}
|
||||
- name: Cache dist-newstyle
|
||||
uses: ./.github/actions/cache-on-main
|
||||
with:
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
name: Lint & Style
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
- test
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
|
||||
- name: Tag latest commit
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
name: Build
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
name: Spellcheck
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -51,17 +51,3 @@ jobs:
|
||||
run: postgrest-docs-spellcheck
|
||||
- name: Run dictcheck
|
||||
run: postgrest-docs-dictcheck
|
||||
|
||||
|
||||
linkcheck:
|
||||
name: Linkcheck
|
||||
if: github.base_ref == 'main'
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
tools: docs.linkcheck.bin
|
||||
- run: postgrest-docs-linkcheck
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
name: Linkcheck
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '1 2 * * 3'
|
||||
|
||||
jobs:
|
||||
linkcheck:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
tools: docs.linkcheck.bin
|
||||
- run: postgrest-docs-linkcheck
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Check the version to be released
|
||||
run: |
|
||||
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||
@@ -64,9 +64,9 @@ jobs:
|
||||
needs:
|
||||
- prepare
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Create release bundle with archives for all builds
|
||||
@@ -81,6 +81,9 @@ jobs:
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-aarch64.tar.xz" \
|
||||
-C artifacts/postgrest-macos-aarch64 postgrest
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-x86-64.tar.xz" \
|
||||
-C artifacts/postgrest-macos-x86-64 postgrest
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \
|
||||
-C artifacts/postgrest-freebsd-x86-64 postgrest
|
||||
|
||||
@@ -135,17 +138,17 @@ jobs:
|
||||
env:
|
||||
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Download x86-64 Docker image
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: postgrest-docker-x86-64
|
||||
- name: Download aarch64 binary
|
||||
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
||||
with:
|
||||
name: postgrest-ubuntu-aarch64
|
||||
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
with:
|
||||
username: ${{ vars.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
@@ -191,8 +194,8 @@ jobs:
|
||||
vars.DOCKER_REPO && vars.DOCKER_USER &&
|
||||
github.ref == 'refs/tags/devel'
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
|
||||
with:
|
||||
username: ${{ vars.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
||||
run: postgrest-coverage
|
||||
- name: Upload coverage to codecov
|
||||
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
with:
|
||||
files: ./coverage/codecov.json
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pgVersion: ["9_6", 10, 11, 12, 13, 14, 15, 16]
|
||||
pgVersion: [12, 13, 14, 15, 16, 17]
|
||||
name: PG ${{ matrix.pgVersion }}
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -100,21 +100,24 @@ jobs:
|
||||
name: Memory
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
tools: memory.test.bin
|
||||
tools: tests.testMemory.bin
|
||||
- name: Run memory tests
|
||||
run: postgrest-test-memory
|
||||
|
||||
|
||||
loadtest:
|
||||
strategy:
|
||||
matrix:
|
||||
kind: ['mixed', 'jwt']
|
||||
name: Loadtest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nix Environment
|
||||
@@ -122,11 +125,37 @@ jobs:
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
tools: loadtest.loadtestAgainst.bin loadtest.report.bin
|
||||
- uses: WyriHaximus/github-action-get-previous-tag@04e8485ecb6487243907e330d522ff60f02283ce # v1.4.0
|
||||
id: get-latest-tag
|
||||
with:
|
||||
prefix: v
|
||||
- name: Run loadtest
|
||||
env:
|
||||
TARGET_BRANCH: ${{ github.base_ref || github.ref_name }}
|
||||
run: |
|
||||
postgrest-loadtest-against main ${{ steps.get-latest-tag.outputs.tag }}
|
||||
if [ "$TARGET_BRANCH" = "main" ]; then
|
||||
latest_tag=$(git tag --sort=-creatordate --list "v*" | head -n1)
|
||||
else
|
||||
latest_tag=$(git tag --merged HEAD --sort=-creatordate "v*" | head -n1)
|
||||
fi
|
||||
postgrest-loadtest-against -k ${{ matrix.kind }} "$TARGET_BRANCH" "$latest_tag"
|
||||
postgrest-loadtest-report >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
flake:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runs-on:
|
||||
- macos-13 # x86_64-darwin
|
||||
- macos-14 # aarch64-darwin
|
||||
- ubuntu-24.04 # x86_64-linux
|
||||
- ubuntu-24.04-arm # aarch64-linux
|
||||
name: Flake Check
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
- name: Run flake check
|
||||
run: |
|
||||
nix flake check
|
||||
|
||||
@@ -14,7 +14,7 @@ site
|
||||
.#*
|
||||
*.swp
|
||||
result*
|
||||
dist-newstyle
|
||||
dist-*
|
||||
postgrest.hp
|
||||
postgrest.prof
|
||||
__pycache__
|
||||
@@ -24,3 +24,4 @@ coverage
|
||||
loadtest
|
||||
.history
|
||||
.docs-build
|
||||
gen_targets.http
|
||||
|
||||
@@ -5,6 +5,6 @@ python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
os: ubuntu-24.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
|
||||
@@ -13,21 +13,21 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/gnuhost.png">
|
||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/neon.jpg">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/neon.jpg">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/tembo.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/code-build.png">
|
||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||
<img width="296px" src="static/euronodes.svg">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
@@ -35,11 +35,6 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
<img width="296px" src="static/supabase.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/tembo.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -93,6 +88,11 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
<img width="222px" src="static/oblivious.jpg">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="222px" src="static/code-build.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -5,6 +5,126 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## Unreleased
|
||||
|
||||
## [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 #4048
|
||||
- Fix OpenAPI specification incorrectly exposing GET methods for volatile functions by @joelonsql in #4174
|
||||
- Fix empty spread embeddings return unexpected SQL error by @taimoorzaeem in #3887
|
||||
- Fix `/metrics` endpoint not responding with `Content-Type` header by @taimoorzaeem in #4271
|
||||
|
||||
## [13.0.4] - 2025-06-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix regression that makes full-text search not work on domain types based on `tsvector` by @laurenceisla in #4135
|
||||
- Fix `jwt-aud` config not failing when set to an invalid URI by @taimoorzaeem in #4132
|
||||
|
||||
## [13.0.3] - 2025-06-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- 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
|
||||
|
||||
## [13.0.2] - 2025-06-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix regression that makes `ORDER BY` with nulls-order not work alongside limits by @laurenceisla in #4109
|
||||
|
||||
## [13.0.1] - 2025-06-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix jwt error returning HTTP status `400` for invalid role by @taimoorzaeem in #3601
|
||||
- Fix `db-extra-search-path` cannot be set to nothing by @taimoorzaeem in #4074
|
||||
+ It can now be disabled by setting it to empty string.
|
||||
+ Schema Cache load error is now logged including `db-schemas` and `db-extra-search-path` config values.
|
||||
|
||||
## [13.0.0] - 2025-05-08
|
||||
|
||||
### Added
|
||||
|
||||
- #3558, Add the `admin-server-host` config to set the host for the admin server - @develop7
|
||||
- #3607, Log to stderr when the JWT secret is less than 32 characters long - @laurenceisla
|
||||
- #2858, Performance improvements when calling RPCs via GET using indexes in more cases - @wolfgangwalther
|
||||
- #3560, Log resolved host in "Listening on ..." messages - @develop7
|
||||
- #3727, Log maximum pool size - @steve-chavez
|
||||
- #1536, Add string comparison feature for jwt-role-claim-key - @taimoorzaeem
|
||||
- #3747, Allow `not_null` value for the `is` operator - @taimoorzaeem
|
||||
- #2255, Apply `to_tsvector()` explicitly to the full-text search filtered column (excluding `tsvector` types) - @laurenceisla
|
||||
- #1578, Log the main SQL query to stderr at the current `log-level` when `log-query=main-query` - @laurenceisla
|
||||
- #3903, Log connection pool borrows on `log-level=debug` - @taimoorzaeem
|
||||
- #3041, Allow spreading one-to-many and many-to-many embedded resources - @laurenceisla
|
||||
+ The selected columns in the embedded resources are aggregated into arrays
|
||||
+ Aggregates are not supported
|
||||
- #2967, Add `Proxy-Status` header for better error response - @taimoorzaeem
|
||||
- #4016, Add `Content-Length` response header - @laurenceisla
|
||||
|
||||
### Fixed
|
||||
|
||||
- #3693, Prevent spread embedding to allow aggregates when they are disabled - @laurenceisla
|
||||
- #3693, A nested spread embedding now correctly groups by the fields of its top parent relationship - @laurenceisla
|
||||
- #3693, Fix spread embedding errors when using the `count()` aggregate without a field - @laurenceisla
|
||||
+ Fixed `"column reference <col> is ambiguous"` error when selecting `?select=...table(col,count())`
|
||||
+ Fixed `"column <json_aggregate>.<alias> does not exist"` error when selecting `?select=...table(aias:count())`
|
||||
- #3727, Clarify "listening" logs - @steve-chavez
|
||||
- #3795, Clarify `Accept: vnd.pgrst.object` error message - @steve-chavez
|
||||
- #3697, #3602, Handle queries on non-existing table gracefully - @taimoorzaeem
|
||||
- #3600, #3926, Improve JWT errors - @taimoorzaeem
|
||||
- #3013, Fix `order=` with POST, PATCH, PUT and DELETE requests - @taimoorzaeem
|
||||
- #3965, Fix filter on unselected columns in a table-valued function - @taimoorzaeem
|
||||
- #4052, Fix schema cache load duplicate objects with different object type but same oid - @taimoorzaeem
|
||||
|
||||
### Changed
|
||||
|
||||
- #2052, Dropped support for PostgreSQL 9.6 - @wolfgangwalther
|
||||
- #2052, Dropped support for PostgreSQL 10 - @wolfgangwalther
|
||||
- #2052, Dropped support for PostgreSQL 11 - @wolfgangwalther
|
||||
- #3508, PostgREST now fails to start when `server-port` and `admin-server-port` config options are the same - @develop7
|
||||
- #3607, PostgREST now fails to start when the JWT secret is less than 32 characters long - @laurenceisla
|
||||
- #3644, Fail schema cache lookup with invalid `db-schemas` or `db-extra-search-path` config - @wolfgangwalther
|
||||
- Previously, this would silently return 200 - OK on the root endpoint, but don't provide any usable endpoints.
|
||||
- Note: This also applies when deleting the `public` schema - both config options default to that.
|
||||
- #3757, Remove support for `Prefer: params=single-object` - @joelonsql
|
||||
+ This preference was deprecated in favor of Functions with an array of JSON objects
|
||||
- #3013, Drop support for Limited updates/deletes
|
||||
+ The feature was complicated and largely unused.
|
||||
- #3956, Drop `/config` endpoint of admin server - @steve-chavez
|
||||
+ The endpoint was at risk of being left unprotected when exposing it.
|
||||
+ The accompanying `admin-server-config-enabled` config was also dropped.
|
||||
- #3697, #3602, Querying non-existent table now returns `PGRST205` error instead of empty json - @taimoorzaeem
|
||||
- #3600, #3926, Improve JWT errors - @taimoorzaeem
|
||||
+ Return `PGRST301` error when `Bearer` in auth header is sent empty
|
||||
+ Diagnostic error messages instead of exposed internals
|
||||
+ Return new `PGRST303` error when jwt claims decoding fails
|
||||
- #3906, Return `PGRST125` and `PGRST126` errors instead of empty json - @taimoorzaeem
|
||||
|
||||
## [12.2.12] - 2025-05-01
|
||||
|
||||
### Fixed
|
||||
@@ -12,11 +132,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- #3956, Fix exposing admin server `/config` by default - @steve-chavez
|
||||
+ The above endpoint is now disabled unless the `admin-server-config-enabled` config is set to `true`
|
||||
|
||||
## [12.2.11] - 2025-04-21
|
||||
## [12.2.11] - 2025-04-22
|
||||
|
||||
### Fixed
|
||||
|
||||
- #4030, Fix regression with parameter `charset=utf-8` in mediatype - @taimoorzaeem
|
||||
- #4030, Fix regression with parameter `charset=utf-8` in mediatype - @taimoorzaeem
|
||||
|
||||
## [12.2.10] - 2025-04-18
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
This repository follows the same contribution guidelines as the main PostgREST repository contribution guidelines:
|
||||
|
||||
https://github.com/PostgREST/postgrest/blob/main/.github/CONTRIBUTING.md
|
||||
@@ -2,7 +2,7 @@
|
||||
# The x86-64 is a single-static-binary image built via Nix, see:
|
||||
# nix/tools/docker/README.md
|
||||
|
||||
FROM ubuntu:noble@sha256:72297848456d5d37d1262630108ab308d3e9ec7ed1c3286a32fe09856619a782 AS postgrest
|
||||
FROM ubuntu:noble@sha256:66460d557b25769b102175144d538d88219c077c678a49af4afca6fbfc1b5252 AS postgrest
|
||||
|
||||
RUN apt-get update -y \
|
||||
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
|
||||
|
||||
@@ -22,21 +22,21 @@ API than you are likely to write from scratch.
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/gnuhost.png">
|
||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/neon.jpg">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/neon.jpg">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/tembo.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/code-build.png">
|
||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||
<img width="296px" src="static/euronodes.svg">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
@@ -44,11 +44,6 @@ API than you are likely to write from scratch.
|
||||
<img width="296px" src="static/supabase.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/tembo.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1 +1 @@
|
||||
index-state: hackage.haskell.org 2025-02-01T14:59:33Z
|
||||
index-state: hackage.haskell.org 2025-10-13T04:53:27Z
|
||||
|
||||
@@ -3,7 +3,15 @@
|
||||
, compiler ? "ghc948"
|
||||
|
||||
, # Commit of the Nixpkgs repository that we want to use.
|
||||
nixpkgsVersion ? import nix/nixpkgs-version.nix
|
||||
# It defaults to reading the inputs from flake.lock, which serves
|
||||
# as a compatibility layer for non-flake builds / default.nix / shell.nix.
|
||||
nixpkgsVersion ? let
|
||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
||||
in
|
||||
{
|
||||
inherit (lock.nodes.nixpkgs.locked) owner repo rev;
|
||||
tarballHash = lock.nodes.nixpkgs.locked.narHash;
|
||||
}
|
||||
|
||||
, # Nix files that describe the Nixpkgs repository. We evaluate the expression
|
||||
# using `import` below.
|
||||
@@ -19,7 +27,7 @@ let
|
||||
"postgrest";
|
||||
|
||||
# PostgREST source files, filtered based on the rules in the .gitignore files
|
||||
# and file extensions. We want to include as litte as possible, as the files
|
||||
# and file extensions. We want to include as little as possible, as the files
|
||||
# added here will increase the space used in the Nix store and trigger the
|
||||
# build of new Nix derivations when changed.
|
||||
src =
|
||||
@@ -36,9 +44,6 @@ let
|
||||
allOverlays.checked-shell-script
|
||||
allOverlays.gitignore
|
||||
allOverlays.postgresql-libpq
|
||||
allOverlays.postgresql-legacy
|
||||
allOverlays.postgresql-future
|
||||
allOverlays.postgis
|
||||
(allOverlays.haskell-packages { inherit compiler; })
|
||||
allOverlays.slocat
|
||||
];
|
||||
@@ -49,34 +54,28 @@ let
|
||||
|
||||
postgresqlVersions =
|
||||
[
|
||||
{ name = "postgresql-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 = "postgresql-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 = "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 ]); }
|
||||
{ name = "postgresql-11"; postgresql = pkgs.postgresql_11.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "postgresql-10"; postgresql = pkgs.postgresql_10.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "postgresql-9_6"; postgresql = pkgs.postgresql_9_6.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
];
|
||||
|
||||
# Dynamic derivation for PostgREST
|
||||
postgrest =
|
||||
pkgs.haskell.packages."${compiler}".callCabal2nix name src { };
|
||||
postgrest = pkgs.lib.pipe (pkgs.haskell.packages."${compiler}".callCabal2nix name src { }) [
|
||||
# To allow ghc-datasize to be used.
|
||||
lib.disableLibraryProfiling
|
||||
# We are never going to use dynamic haskell libraries anyway. "Dynamic" refers to how
|
||||
# non-haskell deps are linked. All haskell dependencies are always statically linked.
|
||||
lib.disableSharedLibraries
|
||||
];
|
||||
|
||||
staticHaskellPackage = import nix/static.nix { inherit compiler name pkgs src; };
|
||||
|
||||
# Options passed to cabal in dev tools and tests
|
||||
devCabalOptions =
|
||||
"-f dev --test-show-detail=direct";
|
||||
|
||||
profiledHaskellPackages =
|
||||
pkgs.haskell.packages."${compiler}".extend (_: super:
|
||||
{
|
||||
mkDerivation =
|
||||
args:
|
||||
super.mkDerivation (args // { enableLibraryProfiling = true; });
|
||||
}
|
||||
);
|
||||
"-f dev --test-show-detail=direct --disable-shared";
|
||||
|
||||
inherit (pkgs.haskell) lib;
|
||||
in
|
||||
@@ -90,12 +89,11 @@ rec {
|
||||
lib.dontCheck postgrest;
|
||||
|
||||
# Profiled dynamic executable.
|
||||
postgrestProfiled =
|
||||
lib.enableExecutableProfiling (
|
||||
lib.dontHaddock (
|
||||
lib.dontCheck (profiledHaskellPackages.callCabal2nix name src { })
|
||||
)
|
||||
);
|
||||
postgrestProfiled = pkgs.lib.pipe postgrestPackage [
|
||||
lib.enableExecutableProfiling
|
||||
lib.enableLibraryProfiling
|
||||
lib.dontHaddock
|
||||
];
|
||||
|
||||
inherit (postgrest) env;
|
||||
|
||||
@@ -125,17 +123,9 @@ rec {
|
||||
loadtest =
|
||||
pkgs.callPackage nix/tools/loadtest.nix { inherit withTools; };
|
||||
|
||||
# Script for running memory tests.
|
||||
memory =
|
||||
pkgs.callPackage nix/tools/memory.nix { inherit postgrestProfiled withTools; };
|
||||
|
||||
# Utility for updating the pinned version of Nixpkgs.
|
||||
nixpkgsTools =
|
||||
pkgs.callPackage nix/tools/nixpkgsTools.nix { };
|
||||
|
||||
# Scripts for publishing new releases.
|
||||
release =
|
||||
pkgs.callPackage nix/tools/release { };
|
||||
pkgs.callPackage nix/tools/release.nix { };
|
||||
|
||||
# Linting and styling tools.
|
||||
style =
|
||||
|
||||
@@ -19,21 +19,21 @@ write from scratch.
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/gnuhost.png">
|
||||
<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/neon.jpg">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<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/neon.jpg">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/tembo.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/code-build.png">
|
||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||
<img width="296px" src="static/euronodes.svg">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
@@ -41,11 +41,6 @@ write from scratch.
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/tembo.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -64,7 +59,7 @@ The image is built from scratch using
|
||||
[Nix](https://nixos.org/nixpkgs/manual/#sec-pkgs-dockerTools) instead of a
|
||||
`Dockerfile`, which yields a highly secure and optimized image. This is also why
|
||||
no commands are listed in the image history. See the [PostgREST
|
||||
respository](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.
|
||||
|
||||
This does not apply to the arm64 variant, which is based on Ubuntu.
|
||||
|
||||
@@ -60,6 +60,10 @@ note bottom of ApiRequest
|
||||
Parses the URL syntax
|
||||
end note
|
||||
|
||||
note bottom of Plan
|
||||
Generates internal AST
|
||||
end note
|
||||
|
||||
note bottom of Query
|
||||
Generates the SQL
|
||||
end note
|
||||
@@ -71,12 +75,15 @@ end note
|
||||
url of Admin is [[../references/admin_server.html#admin-server]]
|
||||
url of API is [[../explanations/schema_isolation.html]]
|
||||
url of Auth is [[../references/auth.html#authn]]
|
||||
url of ApiRequest is [[../explanations/architecture.html#api-request]]
|
||||
url of Plan is [[../explanations/architecture.html#plan]]
|
||||
url of Query is [[../explanations/architecture.html#query]]
|
||||
url of Authorization is [[../explanations/db_authz.html]]
|
||||
url of CLI is [[../references/cli.html#cli]]
|
||||
url of "Connection Pool" is [[../references/connection_pool.html]]
|
||||
url of Config is [[../references/configuration.html#configuration]]
|
||||
url of HTTPADMIN is [[https://aosabook.org/en/posa/warp.html]]
|
||||
url of HTTPAPI is [[https://aosabook.org/en/posa/warp.html]]
|
||||
url of HTTPADMIN is [[../explanations/architecture.html#http]]
|
||||
url of HTTPAPI is [[../explanations/architecture.html#http]]
|
||||
url of Listener is [[../references/listener.html#listener]]
|
||||
url of Proxy is [[../explanations/nginx.html]]
|
||||
url of "Schema Cache" is [[../references/schema_cache.html#schema-cache]]
|
||||
|
||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 468 B After Width: | Height: | Size: 156 B |
@@ -1 +1 @@
|
||||
<?xml version="1.0" encoding="us-ascii" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="411px" preserveAspectRatio="none" style="width:315px;height:411px;" version="1.1" viewBox="0 0 315 411" width="315px" zoomAndPan="magnify"><defs/><g><!--cluster PostgreSQL--><g id="cluster_PostgreSQL"><path d="M6,16 C6,6 152,6 152,6 C152,6 298,6 298,16 L298,309.14 C298,319.14 152,319.14 152,319.14 C152,319.14 6,319.14 6,309.14 L6,16 " fill="none" style="stroke:#E7E7E7;stroke-width:1.0;"/><path d="M6,16 C6,26 152,26 152,26 C152,26 298,26 298,16 " fill="none" style="stroke:#E7E7E7;stroke-width:1.0;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="81.375" x="111.3125" y="42.9659">PostgreSQL</text></g><!--cluster public--><g id="cluster_public"><polygon fill="none" points="30,77,40,67,150,67,150,155.07,140,165.07,30,165.07,30,77" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="140" x2="150" y1="77" y2="67"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="30" x2="140" y1="77" y2="77"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="140" x2="140" y1="77" y2="165.07"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="44.625" x="63.6875" y="94.9659">public</text></g><!--cluster API--><g id="cluster_API"><polygon fill="none" points="68,199.07,78,189.07,236,189.07,236,285.14,226,295.14,68,295.14,68,199.07" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="226" x2="236" y1="199.07" y2="189.07"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="68" x2="226" y1="199.07" y2="199.07"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="226" x2="226" y1="199.07" y2="295.14"/><text fill="#FFFFFF" font-family="sans-serif" font-size="20" font-weight="bold" lengthAdjust="spacing" textLength="29.875" x="133.0625" y="223.45">api</text></g><!--entity tables_public--><g id="elem_tables_public"><rect fill="#313139" height="39.0679" rx="2.5" ry="2.5" style="stroke:#E7E7E7;stroke-width:0.5;" width="61" x="69.5" y="110"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="41" x="79.5" y="134.9659">tables</text></g><!--entity extensions--><g id="elem_extensions"><polygon fill="none" points="165.81,115,175.81,105,282.185,105,282.185,144.0679,272.185,154.0679,165.81,154.0679,165.81,115" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="272.185" x2="282.185" y1="115" y2="105"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="165.81" x2="272.185" y1="115" y2="115"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="272.185" x2="272.185" y1="115" y2="154.0679"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="76.375" x="180.81" y="139.9659">extensions</text></g><!--entity vf_api--><g id="elem_vf_api"><rect fill="#313139" height="39.0679" rx="2.5" ry="2.5" style="stroke:#E7E7E7;stroke-width:0.5;" width="136" x="84" y="240.07"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="116" x="94" y="265.0359">views + functions</text></g><!--entity PostgREST--><g id="elem_PostgREST"><ellipse cx="152" cy="371.14" fill="#313139" rx="8" ry="8" style="stroke:#E7E7E7;stroke-width:0.5;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="70" x="117" y="403.1059">PostgREST</text></g><!--reverse link tables_public to vf_api--><g id="link_tables_public_vf_api"><path d="M107.25,155.29 C107.25,179.74 107.25,215.13 107.25,239.67 " fill="none" id="tables_public-backto-vf_api" style="stroke:#E7E7E7;stroke-width:1.0;"/><polygon fill="#E7E7E7" points="107.25,149.29,103.25,158.29,107.25,154.29,111.25,158.29,107.25,149.29" style="stroke:#E7E7E7;stroke-width:1.0;"/></g><!--reverse link extensions to vf_api--><g id="link_extensions_vf_api"><path d="M192.91,160.31 C192.91,185.07 192.91,216.68 192.91,239.61 " fill="none" id="extensions-backto-vf_api" style="stroke:#E7E7E7;stroke-width:1.0;"/><polygon fill="#E7E7E7" points="192.91,154.31,188.91,163.31,192.91,159.31,196.91,163.31,192.91,154.31" style="stroke:#E7E7E7;stroke-width:1.0;"/></g><!--link vf_api to PostgREST--><g id="link_vf_api_PostgREST"><path d="M152,285.55 C152,309.98 152,338.34 152,356.18 " fill="none" id="vf_api-PostgREST" style="stroke:#E7E7E7;stroke-width:3.0;"/><polygon fill="#E7E7E7" points="152,279.55,148,288.55,152,284.55,156,288.55,152,279.55" style="stroke:#E7E7E7;stroke-width:3.0;"/><polygon fill="#E7E7E7" points="152,362.18,156,353.18,152,357.18,148,353.18,152,362.18" style="stroke:#E7E7E7;stroke-width:3.0;"/></g><!--SRC=[KypCIyufJKbLqDFJBqxEqCqipjShpSq10000]--></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="391px" preserveAspectRatio="none" style="width:328px;height:391px;" version="1.1" viewBox="0 0 328 391" width="328px" zoomAndPan="magnify"><defs/><g><!--cluster PostgreSQL--><g id="cluster_PostgreSQL"><path d="M6,16 C6,6 158.5,6 158.5,6 C158.5,6 311,6 311,16 L311,293.59 C311,303.59 158.5,303.59 158.5,303.59 C158.5,303.59 6,303.59 6,293.59 L6,16 " fill="none" style="stroke:#E7E7E7;stroke-width:1.0;"/><path d="M6,16 C6,26 158.5,26 158.5,26 C158.5,26 311,26 311,16 " fill="none" style="stroke:#E7E7E7;stroke-width:1.0;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="92.2305" x="112.3848" y="40.9951">PostgreSQL</text></g><!--cluster public--><g id="cluster_public"><polygon fill="none" points="30,74,40,64,153,64,153,146.29,143,156.29,30,156.29,30,74" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="143" x2="153" y1="74" y2="64"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="30" x2="143" y1="74" y2="74"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="143" x2="143" y1="74" y2="156.29"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="47.9063" x="63.5469" y="89.9951">public</text></g><!--cluster API--><g id="cluster_API"><polygon fill="none" points="70,190.29,80,180.29,246,180.29,246,269.59,236,279.59,70,279.59,70,190.29" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="236" x2="246" y1="190.29" y2="180.29"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="70" x2="236" y1="190.29" y2="190.29"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="236" x2="236" y1="190.29" y2="279.59"/><text fill="#FFFFFF" font-family="sans-serif" font-size="20" font-weight="bold" lengthAdjust="spacing" textLength="34.668" x="136.666" y="211.8545">api</text></g><!--entity tables_public--><g id="elem_tables_public"><rect fill="#313139" height="36.2969" rx="2.5" ry="2.5" style="stroke:#E7E7E7;stroke-width:0.5;" width="62.752" x="71.62" y="104"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="42.752" x="81.62" y="126.9951">tables</text></g><!--entity extensions--><g id="elem_extensions"><polygon fill="none" points="169.14,109,179.14,99,294.8695,99,294.8695,135.2969,284.8695,145.2969,169.14,145.2969,169.14,109" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="284.8695" x2="294.8695" y1="109" y2="99"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="169.14" x2="284.8695" y1="109" y2="109"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="284.8695" x2="284.8695" y1="109" y2="145.2969"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="85.7295" x="184.14" y="131.9951">extensions</text></g><!--entity vf_api--><g id="elem_vf_api"><rect fill="#313139" height="36.2969" rx="2.5" ry="2.5" style="stroke:#E7E7E7;stroke-width:0.5;" width="144.6465" x="85.68" y="227.29"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="124.6465" x="95.68" y="250.2851">views + functions</text></g><!--entity PostgREST--><g id="elem_PostgREST"><ellipse cx="158" cy="352.59" fill="#313139" rx="8" ry="8" style="stroke:#E7E7E7;stroke-width:0.5;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="74.6895" x="120.6553" y="382.5851">PostgREST</text></g><!--reverse link tables_public to vf_api--><g id="link_tables_public_vf_api"><path d="M110.03,146.6 C110.03,169.85 110.03,203.55 110.03,226.86 " fill="none" id="tables_public-backto-vf_api" style="stroke:#E7E7E7;stroke-width:1.0;"/><polygon fill="#E7E7E7" points="110.03,140.6,106.03,149.6,110.03,145.6,114.03,149.6,110.03,140.6" style="stroke:#E7E7E7;stroke-width:1.0;"/></g><!--reverse link extensions to vf_api--><g id="link_extensions_vf_api"><path d="M199.73,151.63 C199.73,175.24 199.73,205.18 199.73,226.88 " fill="none" id="extensions-backto-vf_api" style="stroke:#E7E7E7;stroke-width:1.0;"/><polygon fill="#E7E7E7" points="199.73,145.63,195.73,154.63,199.73,150.63,203.73,154.63,199.73,145.63" style="stroke:#E7E7E7;stroke-width:1.0;"/></g><!--link vf_api to PostgREST--><g id="link_vf_api_PostgREST"><path d="M158,269.62 C158,292.9 158,320.34 158,337.81 " fill="none" id="vf_api-PostgREST" style="stroke:#E7E7E7;stroke-width:3.0;"/><polygon fill="#E7E7E7" points="158,263.62,154,272.62,158,268.62,162,272.62,158,263.62" style="stroke:#E7E7E7;stroke-width:3.0;"/><polygon fill="#E7E7E7" points="158,343.81,162,334.81,158,338.81,154,334.81,158,343.81" style="stroke:#E7E7E7;stroke-width:3.0;"/></g><!--SRC=[KypCIyufJKbLqDFJBqxEqCqipjShpSq10000]--></g></svg>
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
@@ -50,7 +50,7 @@ source_suffix = ".rst"
|
||||
master_doc = "index"
|
||||
|
||||
# This is overriden by readthedocs with the version tag anyway
|
||||
version = "12.2"
|
||||
version = "13.0"
|
||||
# To avoid repetition in <title> we set this to an empty string.
|
||||
release = ""
|
||||
|
||||
@@ -114,7 +114,7 @@ html_theme = "sphinx_rtd_theme"
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
html_theme_options = {"display_version": False}
|
||||
html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
@@ -297,6 +297,9 @@ user_agent = (
|
||||
)
|
||||
|
||||
linkcheck_ignore = [
|
||||
# 403 only in CI / GitHub Actions
|
||||
r"https://www.patreon.com/postgrest",
|
||||
r"https://blog.frankel.ch/poor-man-api",
|
||||
# Odd SSL error
|
||||
r"https://www.dripdepot.com",
|
||||
# New GitHub UI delays comment load, so anchor fails
|
||||
|
||||
@@ -34,7 +34,7 @@ Templates
|
||||
Example Apps
|
||||
------------
|
||||
|
||||
* `archtika <https://github.com/archtika/archtika>`_ - self‑hosted CMS
|
||||
* `archtika <https://github.com/thiloho/archtika>`_ - self‑hosted CMS
|
||||
* `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
|
||||
* `general <https://github.com/PierreRochard/general>`_ - example auth back-end
|
||||
|
||||
@@ -58,7 +58,7 @@ A request might be rejected at this level if it's invalid. For example when prov
|
||||
Plan
|
||||
----
|
||||
|
||||
Using the Schema Cache, `Plan.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Plan.hs>`_ fills in out-of-band SQL details (like an ``ON CONFLICT (pk)`` clause) required to complete the user request.
|
||||
Using the Schema Cache, `Plan.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Plan.hs>`_ generates an internal AST, filling out-of-band SQL details (like an ``ON CONFLICT (pk)`` clause) required to complete the user request.
|
||||
|
||||
A request might be rejected at this level if it's invalid. For example when doing resource embedding on a nonexistent resource.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.. _external_jwt:
|
||||
.. _external_auth:
|
||||
|
||||
External JWT Generation
|
||||
External Authentication
|
||||
-----------------------
|
||||
|
||||
JWT from Auth0
|
||||
@@ -16,16 +16,11 @@ Supported PostgreSQL versions
|
||||
=============================
|
||||
|
||||
=============== =================================
|
||||
**Supported** PostgreSQL >= 9.6
|
||||
**Supported** PostgreSQL >= 12
|
||||
=============== =================================
|
||||
|
||||
PostgREST works with all PostgreSQL versions starting from 9.6.
|
||||
PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_.
|
||||
|
||||
.. note::
|
||||
|
||||
Support for PostgreSQL versions 9.6, 10 and 11 is deprecated. From this on version onwards, PostgREST will only support non-end-of-life PostgreSQL versions.
|
||||
|
||||
See https://www.postgresql.org/support/versioning/.
|
||||
|
||||
Running PostgREST
|
||||
=================
|
||||
|
||||
@@ -38,30 +38,30 @@ Sponsors
|
||||
.. image:: ../static/cybertec.svg
|
||||
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
|
||||
|
||||
.. image:: ../static/gnuhost.png
|
||||
:target: https://euronodes.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
|
||||
.. container:: img-dark
|
||||
|
||||
.. image:: ../static/neon-dark.jpg
|
||||
:target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
|
||||
.. container:: img-light
|
||||
|
||||
.. image:: ../static/neon.jpg
|
||||
:target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
|
||||
.. image:: ../static/tembo.png
|
||||
:target: https://www.tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
||||
|
||||
|
|
||||
|
||||
.. container:: img-dark
|
||||
|
||||
.. image:: ../static/code-build-dark.png
|
||||
:target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
|
||||
.. image:: ../static/euronodes.svg
|
||||
:target: https://www.euronodes.com/postgrest
|
||||
|
||||
.. container:: img-light
|
||||
|
||||
.. image:: ../static/code-build.png
|
||||
:target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
|
||||
.. image:: ../static/euronodes.svg
|
||||
:target: https://www.euronodes.com/postgrest
|
||||
|
||||
.. container:: img-dark
|
||||
|
||||
@@ -73,14 +73,11 @@ Sponsors
|
||||
.. image:: ../static/supabase.png
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
|
||||
.. image:: ../static/tembo.png
|
||||
:target: https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
||||
|
||||
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
|
||||
to create space and center the logos. It's not easy to layout with restructuredText.
|
||||
|
||||
.. .. image:: _static/empty.png
|
||||
:target: #sponsors
|
||||
.. image:: _static/empty.png
|
||||
:target: #sponsors
|
||||
|
||||
|
|
||||
|
||||
@@ -212,17 +209,12 @@ Here are some companies that use PostgREST in production.
|
||||
* `Datrium <https://www.datrium.com>`_
|
||||
* `Drip Depot <https://www.dripdepot.com>`_
|
||||
* `Image-charts <https://www.image-charts.com>`_
|
||||
* `Moat <https://www.oracle.com/advertising/>`_
|
||||
* `Netwo <https://www.netwo.io>`_
|
||||
* `Nimbus <https://www.nimbusfacility.com/sg/home>`_
|
||||
- See how Nimbus uses PostgREST in `Paul Copplestone's blog post <https://paul.copplest.one/blog/nimbus-tech-2019-04.html>`_.
|
||||
* `OpenBooking <https://openbooking.ch>`_
|
||||
* `Supabase <https://supabase.com>`_
|
||||
|
||||
.. Failing links
|
||||
* `eGull <http://www.egull.co>`_
|
||||
* `MotionDynamic - Fast highly dynamic video generation at scale <https://motiondynamic.tech>`_
|
||||
|
||||
Testimonials
|
||||
------------
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
Greenplum
|
||||
#########
|
||||
|
||||
`Greenplum <https://blogs.vmware.com/tanzu/tanzu-greenplum/>`_ has been reported to work by adding ``LOGIN`` to the :ref:`anonymous and user roles <roles>`.
|
||||
|
||||
For more details, see https://github.com/PostgREST/postgrest/issues/2021.
|
||||
@@ -3,6 +3,7 @@ api
|
||||
API's
|
||||
APIs
|
||||
APISIX
|
||||
AST
|
||||
aud
|
||||
Auth
|
||||
auth
|
||||
@@ -27,10 +28,12 @@ CSV
|
||||
durations
|
||||
DDL
|
||||
DOM
|
||||
DSL
|
||||
DevOps
|
||||
dockerize
|
||||
enum
|
||||
Enums
|
||||
Entra
|
||||
eq
|
||||
ETH
|
||||
Ethereum
|
||||
@@ -43,7 +46,6 @@ Github
|
||||
Google
|
||||
grantor
|
||||
GraphQL
|
||||
Greenplum
|
||||
gte
|
||||
GUC
|
||||
Haskell
|
||||
@@ -68,9 +70,11 @@ isdistinct
|
||||
JS
|
||||
js
|
||||
JSON
|
||||
JSPath
|
||||
JWK
|
||||
JWT
|
||||
jwt
|
||||
Keycloak
|
||||
Kubernetes
|
||||
localhost
|
||||
login
|
||||
@@ -94,10 +98,11 @@ npm
|
||||
nxl
|
||||
nxr
|
||||
OAuth
|
||||
ORM
|
||||
Observability
|
||||
Okta
|
||||
OpenAPI
|
||||
openapi
|
||||
ORM
|
||||
ov
|
||||
parametrized
|
||||
passphrase
|
||||
@@ -169,6 +174,7 @@ unikernel
|
||||
unix
|
||||
updatable
|
||||
unfulfillable
|
||||
unselected
|
||||
Untyped
|
||||
UPSERT
|
||||
Upsert
|
||||
|
||||
@@ -12,7 +12,7 @@ Health Check
|
||||
|
||||
You can enable a health check to verify if PostgREST is available for client requests. Also to check the status of its internal state.
|
||||
|
||||
Two endpoints ``live`` and ``ready`` will then be available.
|
||||
Two endpoints ``live`` and ``ready`` will then be available. Both these endpoints reply with a status code and empty response body.
|
||||
|
||||
.. important::
|
||||
|
||||
@@ -55,27 +55,6 @@ Metrics
|
||||
|
||||
Provides :ref:`metrics`.
|
||||
|
||||
.. _runtime_config:
|
||||
|
||||
Runtime Configuration
|
||||
=====================
|
||||
|
||||
Provides a ``config`` endpoint that returns the runtime :ref:`configuration`. This requires setting :ref:`admin-server-config-enabled`.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3001/config"
|
||||
|
||||
.. code-block::
|
||||
|
||||
db-aggregates-enabled = false
|
||||
db-anon-role = "web_anon"
|
||||
db-channel = "pgrst"
|
||||
db-channel-enabled = false
|
||||
...
|
||||
|
||||
.. _runtime_schema_cache:
|
||||
|
||||
Runtime Schema Cache
|
||||
====================
|
||||
|
||||
|
||||
@@ -225,10 +225,10 @@ If we also want to get the total ``amount`` grouped by the ``order_date`` of the
|
||||
Note that the aggregate is done within the embedded resource ``orders``.
|
||||
It is not affected by any of the columns from the top-level relationship ``customers``.
|
||||
|
||||
Using Aggregates in Spreads
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Aggregates in To-One Spreads
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
All the aggregates inside a :ref:`spread embedded resource <spread_embed>` will be hoisted to the top-level relationship.
|
||||
All the aggregates inside a :ref:`one-to-one or many-to-one spread embedded resource <spread_to_one_embed>` will be hoisted to the top-level relationship.
|
||||
In other words, it will behave as if the aggregate was done in the top-level relationship itself. For example:
|
||||
|
||||
.. code-block:: bash
|
||||
@@ -251,3 +251,7 @@ This will take the ``max`` and ``min`` subscription date of every customer and g
|
||||
"min": "2016-02-11"
|
||||
}
|
||||
]
|
||||
|
||||
.. note::
|
||||
|
||||
Aggregates inside to-many spreads are not supported
|
||||
|
||||
@@ -131,10 +131,6 @@ For this the ``Content-Type: application/json`` header must be included in the r
|
||||
|
||||
If an overloaded function has a single ``json`` or ``jsonb`` unnamed parameter, PostgREST will call this function as a fallback provided that no other overloaded function is found with the parameters sent in the POST request.
|
||||
|
||||
.. warning::
|
||||
|
||||
Sending the JSON request body as a single argument is also possible with :ref:`Prefer: params=single-object <prefer_params>` but this method is **deprecated**.
|
||||
|
||||
.. _function_single_unnamed:
|
||||
|
||||
Functions with a single unnamed parameter
|
||||
@@ -298,6 +294,23 @@ Let's get its :ref:`explain_plan` when calling it with filters applied:
|
||||
|
||||
Notice there's no "Function Scan" node in the plan, which tells us it has been inlined.
|
||||
|
||||
Horizontal Filtering
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Table-valued functions support horizontal filtering on selected and unselected columns.
|
||||
|
||||
For example, the following RPC with filter on unselected column returns:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/getallprojects?select=id,client_id&name=like.OSX"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{ "id": 4, "client_id": 2 }
|
||||
]
|
||||
|
||||
.. _scalar_functions:
|
||||
|
||||
Scalar functions
|
||||
|
||||
@@ -19,6 +19,9 @@ For extra customization, the OpenAPI output contains a "description" field for e
|
||||
COMMENT ON TABLE monotremes IS
|
||||
'Freakish mammals lay the best eggs for breakfast';
|
||||
|
||||
COMMENT ON VIEW monotremes_v IS
|
||||
'Only the platypus is publicly visible';
|
||||
|
||||
COMMENT ON COLUMN monotremes.has_venomous_claw IS
|
||||
'Sometimes breakfast is not worth it';
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ The following preferences are supported.
|
||||
- ``Prefer: missing``. See :ref:`prefer_missing`.
|
||||
- ``Prefer: max-affected``, See :ref:`prefer_max_affected`.
|
||||
- ``Prefer: tx``. See :ref:`prefer_tx`.
|
||||
- ``Prefer: params``. See :ref:`prefer_params`.
|
||||
|
||||
.. _prefer_handling:
|
||||
|
||||
@@ -243,7 +242,7 @@ Will result in:
|
||||
Max Affected
|
||||
============
|
||||
|
||||
You can set a limit to the amount of resources affected in a request by sending ``max-affected`` preference. This feature works in combination with ``handling=strict`` preference. ``max-affected`` would be ignored with lenient handling. The "affected resources" are the number of rows returned by ``DELETE`` and ``PATCH`` requests. This is also supported through ``RPC`` calls.
|
||||
You can set a limit to the amount of resources affected in a request by sending ``max-affected`` preference. This feature works in combination with ``handling=strict`` preference. ``max-affected`` would be ignored with lenient handling. The "affected resources" are the number of rows returned by ``DELETE`` and ``PATCH`` requests.
|
||||
|
||||
To illustrate the use of this preference, consider the following scenario where the ``items`` table contains 14 rows.
|
||||
|
||||
@@ -266,30 +265,34 @@ To illustrate the use of this preference, consider the following scenario where
|
||||
"hint": null
|
||||
}
|
||||
|
||||
.. _prefer_params:
|
||||
|
||||
Single JSON object as Function Parameter
|
||||
----------------------------------------
|
||||
|
||||
.. warning::
|
||||
|
||||
Using this preference is **deprecated** in favor of :ref:`function_single_json`.
|
||||
|
||||
:code:`Prefer: params=single-object` allows sending the JSON request body as the single argument of a :ref:`function <functions>`.
|
||||
With :ref:`RPC <functions>`, the preference is honored completely on the basis of the number of rows returned in the result set of the function. This can be useful for complex mutation queries using `data-modifying statements <https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-MODIFYING>`_. A simple example:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE FUNCTION mult_them(param json) RETURNS int AS $$
|
||||
SELECT (param->>'x')::int * (param->>'y')::int
|
||||
CREATE FUNCTION test.delete_items()
|
||||
RETURNS SETOF items AS $$
|
||||
DELETE FROM items WHERE id < 15 RETURNING *;
|
||||
$$ LANGUAGE SQL;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/mult_them" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-H "Prefer: params=single-object" \
|
||||
-d '{ "x": 4, "y": 2 }'
|
||||
curl -i "http://localhost:3000/rpc/delete_items" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: handling=strict, max-affected=10"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
8
|
||||
{
|
||||
"code": "PGRST124",
|
||||
"message": "Query result exceeds max-affected preference constraint",
|
||||
"details": "The query affects 14 rows",
|
||||
"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.
|
||||
|
||||
@@ -209,18 +209,25 @@ The join table is also detected if the composite key has additional columns.
|
||||
One-to-one relationships
|
||||
------------------------
|
||||
|
||||
One-to-one relationships are detected in two ways.
|
||||
One-to-one relationships are detected in two ways. (We'll use the ``films`` and ``technical_specs`` tables from the :ref:`sample film database <erd_film>` as an example).
|
||||
|
||||
- When the foreign key is a primary key as specified in the :ref:`sample film database <erd_film>`.
|
||||
- When the foreign key has a unique constraint.
|
||||
- When the foreign key is also a primary key.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create table technical_specs(
|
||||
film_id int references films(id) unique,
|
||||
runtime time,
|
||||
camera text,
|
||||
sound text
|
||||
film_id int references films(id) primary key
|
||||
-- ...
|
||||
);
|
||||
|
||||
- Or when the foreign key has a unique constraint.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create table technical_specs(
|
||||
id int primary key
|
||||
, film_id int references films(id) unique
|
||||
-- ...
|
||||
);
|
||||
|
||||
.. code-block:: bash
|
||||
@@ -927,7 +934,12 @@ Filters can also be applied on nested embedded resources:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/films?select=*,roles(*,actors(*))&roles.actors.order=last_name&roles.actors.first_name=like.*Tom*"
|
||||
# curl "http://localhost:3000/films?select=*,roles(*,actors(*))&roles.actors.order=last_name&roles.actors.first_name=like.*Tom*"
|
||||
|
||||
curl --get "http://localhost:3000/films" \
|
||||
-d "select=*,roles(*,actors(*))" \
|
||||
-d "roles.actors.order=last_name" \
|
||||
-d "roles.actors.first_name=like.*Tom*"
|
||||
|
||||
The result will show the nested actors named Tom and order them by last name. Aliases can also be used instead of the resource names to filter the nested tables.
|
||||
|
||||
@@ -1138,14 +1150,19 @@ For example, to arrange the films in descending order using the director's last
|
||||
Spread embedded resource
|
||||
========================
|
||||
|
||||
On many-to-one and one-to-one relationships, you can "spread" the embedded resource. That is, remove the surrounding JSON object for the embedded resource columns.
|
||||
You can modify the shape of the embedded resources by using the spread syntax (``...``).
|
||||
|
||||
.. _spread_to_one_embed:
|
||||
|
||||
Spread To-One relationships
|
||||
---------------------------
|
||||
|
||||
Spread on resources forming :ref:`one-to-one <one-to-one>` and :ref:`many-to-one <many-to-one>` relationships, will lift the embedded columns to the top object.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# curl "http://localhost:3000/films?select=title,...directors(director_last_name:last_name)&title=like.*Workers*"
|
||||
|
||||
curl --get "http://localhost:3000/films" \
|
||||
-d "select=title,...directors(director_last_name:last_name)" \
|
||||
-d "select=title,...directors(director_first_name:first_name, director_last_name:last_name)" \
|
||||
-d "title=like.*Workers*"
|
||||
|
||||
.. code-block:: json
|
||||
@@ -1153,17 +1170,148 @@ On many-to-one and one-to-one relationships, you can "spread" the embedded resou
|
||||
[
|
||||
{
|
||||
"title": "Workers Leaving The Lumière Factory In Lyon",
|
||||
"director_first_name": "Louis",
|
||||
"director_last_name": "Lumière"
|
||||
}
|
||||
]
|
||||
|
||||
Note that there is no ``"directors"`` object. Also the embed columns can be aliased normally.
|
||||
Note that there is no wrapping ``"directors"`` object, unlike regularly embedding :ref:`many-to-one <many-to-one>` relationships. Also note that embedded columns can be aliased normally.
|
||||
|
||||
You can use this to get the columns of a join table in a many-to-many relationship. For instance, to get films and its actors, but including the ``character`` column from the roles table:
|
||||
.. _spread_to_many_embed:
|
||||
|
||||
Spread To-Many relationships
|
||||
----------------------------
|
||||
|
||||
Spread on resources forming :ref:`one-to-many <one-to-many>` and :ref:`many-to-many <many-to-many>` relationships, will convert the embedded columns into correlated arrays.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# curl "http://localhost:3000/films?select=title,actors:roles(character,...actors(first_name,last_name))&title=like.*Lighthouse*"
|
||||
curl --get "http://localhost:3000/directors" \
|
||||
-d "select=first_name,...films(film_titles:title,film_years:year)" \
|
||||
-d "first_name=like.Quentin*"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"first_name": "Quentin",
|
||||
"film_titles": [
|
||||
"Pulp Fiction",
|
||||
"Reservoir Dogs"
|
||||
],
|
||||
"film_years": [
|
||||
1994,
|
||||
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 correlated—in the above result, we can say that "Pulp Fiction" premiered in 1994 and "Reservoir Dogs" in 1992.
|
||||
|
||||
Order in spread to-many
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In the above example, the order of the values inside the correlated arrays is unspecified, but all the values are guaranteed to be in the same unspecified order.
|
||||
|
||||
You can order the correlated arrays explicitly. For example, to order by the film year:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl --get "http://localhost:3000/directors" \
|
||||
-d "select=first_name,...films(film_titles:title,film_years:year)" \
|
||||
-d "first_name=like.Quentin*" \
|
||||
-d "films.order=year"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"first_name": "Quentin",
|
||||
"film_titles": [
|
||||
"Reservoir Dogs",
|
||||
"Pulp Fiction"
|
||||
],
|
||||
"film_years": [
|
||||
1992,
|
||||
1994
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
.. warning::
|
||||
|
||||
Aliasing spreaded columns is recommended since JSON allows duplicate keys. Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl --get "localhost:3000/projects" \
|
||||
-d "select=id,name,...clients(id,name)"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[{"id":1,"name":"Windows 7","id":1,"name":"Microsoft"},
|
||||
{"id":2,"name":"Windows 10","id":1,"name":"Microsoft"},
|
||||
{"id":3,"name":"IOS","id":2,"name":"Apple"},
|
||||
{"id":4,"name":"OSX","id":2,"name":"Apple"},
|
||||
{"id":5,"name":"Orphan","id":null,"name":null}]
|
||||
|
||||
This can be a problem in Javascript objects, since only the last duplicated key will be considered. To solve it do:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl --get "localhost:3000/projects" \
|
||||
-d "select=id,name,...clients(client_id:id,client_name:name)"
|
||||
|
||||
|
||||
Multiple Spreads
|
||||
----------------
|
||||
|
||||
You can use multiple spreads at any level. For example, let's spread ``technical_specs`` and ``roles`` into ``films`` and then spread ``films`` into ``directors``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl --get "http://localhost:3000/directors" \
|
||||
-d "select=first_name,...films(film_titles:title,film_years:year,...technical_specs(film_runtimes:runtime),...roles(film_characters:character))" \
|
||||
-d "first_name=like.Quentin*" \
|
||||
-d "films.order=year" \
|
||||
-d "films.roles.order=character"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"first_name": "Quentin",
|
||||
"film_titles": [
|
||||
"Reservoir Dogs",
|
||||
"Pulp Fiction"
|
||||
],
|
||||
"film_years": [
|
||||
1992,
|
||||
1994
|
||||
],
|
||||
"film_runtimes": [
|
||||
"01:39:00",
|
||||
"02:29:00"
|
||||
]
|
||||
"film_characters": [
|
||||
[ "Mr. Pink", "Mr. White" ],
|
||||
[ "Mia Wallace", "Vincent Vega" ]
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
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" ]``.
|
||||
- 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``).
|
||||
|
||||
Spread a join table
|
||||
-------------------
|
||||
|
||||
Spread can be used to move the columns of a join table in a :ref:`many-to-many <many-to-many>` to the top object. For instance, to get the ``character`` column of the ``roles`` join table into ``actors``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl --get "http://localhost:3000/films" \
|
||||
-d "select=title,actors:roles(character,...actors(first_name,last_name))" \
|
||||
@@ -1184,6 +1332,4 @@ You can use this to get the columns of a join table in a many-to-many relationsh
|
||||
}
|
||||
]
|
||||
|
||||
.. note::
|
||||
|
||||
The spread operator ``...`` is borrowed from the Javascript `spread syntax <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax>`_.
|
||||
|
||||
@@ -100,17 +100,15 @@ This returns
|
||||
|
||||
{ "id": 1 }
|
||||
|
||||
with a :code:`Content-Type: application/vnd.pgrst.object+json`.
|
||||
|
||||
When a singular response is requested but no entries are found, the server responds with an error message and 406 Not Acceptable status code rather than the usual empty array and 200 status:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"message": "JSON object requested, multiple (or no) rows returned",
|
||||
"details": "Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row",
|
||||
"hint": null,
|
||||
"code": "PGRST505"
|
||||
"code": "PGRST116",
|
||||
"message": "Cannot coerce the result to a single JSON object",
|
||||
"details": "The result contains 0 rows",
|
||||
"hint": null
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -72,7 +72,7 @@ imatch :code:`~*` ~* operator, see :ref:`pattern_matching`
|
||||
in :code:`IN` one of a list of values, e.g. :code:`?a=in.(1,2,3)`
|
||||
– also supports commas in quoted strings like
|
||||
:code:`?a=in.("hi,there","yes,you")`
|
||||
is :code:`IS` checking for exact equality (null,true,false,unknown)
|
||||
is :code:`IS` checking for exact equality (null,not_null,true,false,unknown)
|
||||
isdistinct :code:`IS DISTINCT FROM` not equal, treating :code:`NULL` as a comparable value
|
||||
fts :code:`@@` :ref:`fts` using to_tsquery
|
||||
plfts :code:`@@` :ref:`fts` using plainto_tsquery
|
||||
@@ -175,25 +175,47 @@ To ensure best performance on larger data sets, an `appropriate index <https://w
|
||||
Full-Text Search
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The :code:`fts` filter mentioned above has a number of options to support flexible textual queries, namely the choice of plain vs phrase search and the language used for stemming. Suppose that :code:`tsearch` is a table with column :code:`my_tsv`, of type `tsvector <https://www.postgresql.org/docs/current/datatype-textsearch.html>`_. The following examples illustrate the possibilities.
|
||||
The :code:`fts` operator has a number of options to support flexible textual queries, namely the choice of plain vs phrase search and the language used for stemming.
|
||||
|
||||
The following examples illustrate the possibilities, assuming column :code:`my_tsv` is of type `tsvector <https://www.postgresql.org/docs/current/datatype-textsearch.html>`_.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_tsv=fts(french).amusant"
|
||||
curl --get "http://localhost:3000/people" \
|
||||
-d "my_tsv=fts(french).amusant"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_tsv=plfts.The%20Fat%20Cats"
|
||||
curl --get "http://localhost:3000/people" \
|
||||
-d "my_tsv=plfts.The%20Fat%20Cats"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats"
|
||||
curl --get "http://localhost:3000/people" \
|
||||
-d "my_tsv=not.phfts(english).The%20Fat%20Cats"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_tsv=not.wfts(french).amusant"
|
||||
curl --get "http://localhost:3000/people" \
|
||||
-d "my_tsv=not.wfts(french).amusant"
|
||||
|
||||
Using `websearch_to_tsquery` requires PostgreSQL of version at least 11.0 and will raise an error in earlier versions of the database.
|
||||
.. _fts_to_tsvector:
|
||||
|
||||
Automatic ``tsvector`` conversion
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If the filtered column is not of type ``tsvector``, then it will be automatically converted using `to_tsvector() <https://www.postgresql.org/docs/current/functions-textsearch.html#TEXTSEARCH-FUNCTIONS-TABLE>`_.
|
||||
This allows using the ``fts`` operator on ``text`` and ``json`` types out of the box.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl --get "http://localhost:3000/people" \
|
||||
-d "my_text_column=fts(french).amusant"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl --get "http://localhost:3000/people" \
|
||||
-d "my_json_column=not.phfts(english).The%20Fat%20Cats"
|
||||
|
||||
.. _v_filter:
|
||||
|
||||
@@ -683,31 +705,6 @@ Deletions also support :ref:`prefer_return`, :ref:`resource_embedding` and :ref:
|
||||
|
||||
Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`.
|
||||
|
||||
.. _limited_update_delete:
|
||||
|
||||
Limited Update/Delete
|
||||
=====================
|
||||
|
||||
You can limit the amount of affected rows by :ref:`update` or :ref:`delete` with the ``limit`` query parameter. For this, you must add an explicit ``order`` on a unique column(s).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -X PATCH "/users?limit=10&order=id&last_login=lt.2020-01-01" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "status": "inactive" }'
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -X DELETE "http://localhost:3000/users?limit=10&order=id&status=eq.inactive"
|
||||
|
||||
If your table has no unique columns, you can use the `ctid <https://www.postgresql.org/docs/current/ddl-system-columns.html>`_ system column.
|
||||
|
||||
Using ``offset`` to target a different subset of rows is also possible.
|
||||
|
||||
.. note::
|
||||
|
||||
There is no native ``UPDATE...LIMIT`` or ``DELETE...LIMIT`` support in PostgreSQL; the generated query simulates that behavior and is based on `this Crunchy Data blog post <https://www.crunchydata.com/blog/simulating-update-or-delete-with-limit-in-postgres-ctes-to-the-rescue>`_.
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript">
|
||||
|
||||
@@ -77,7 +77,7 @@ If the client included no JWT (or one without a role claim) then PostgREST switc
|
||||
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_jwt`).
|
||||
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:
|
||||
|
||||
@@ -156,6 +156,21 @@ You can specify the literal value as we saw earlier, or reference a filename to
|
||||
|
||||
jwt-secret = "@rsa.jwk.pub"
|
||||
|
||||
JWK ``kid`` validation
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
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:
|
||||
|
||||
- If the JWT contains a ``kid`` parameter, then PostgREST will look for the JWK in the :ref:`jwt-secret`.
|
||||
|
||||
+ 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 JWK matches the ``kid`` value then it will validate the token against that JWK accordingly.
|
||||
|
||||
- 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
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -164,10 +179,67 @@ PostgREST honors the following `JWT claims <https://datatracker.ietf.org/doc/htm
|
||||
- ``exp`` Expiration Time
|
||||
- ``iat`` Issued At
|
||||
- ``nbf`` Not Before
|
||||
- ``aud`` Audience, see :ref:`jwt-aud`
|
||||
- ``aud`` :ref:`Audience <jwt_aud_validation>`
|
||||
|
||||
.. note::
|
||||
PostgREST allows for a 30-second clock skew when validating the ``exp`` and ``iat`` claims. In other words, it gives an extra 30 seconds before the token is rejected if there is a slight discrepancy in the timestamps.
|
||||
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_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>`_.
|
||||
It works this way:
|
||||
|
||||
- If :ref:`jwt-aud` is not set (the default), PostgREST identifies with all audiences and allows the JWT for any ``aud`` claim.
|
||||
- If :ref:`jwt-aud` is set to a specific audience, PostgREST will check if this audience is present in the ``aud`` claim:
|
||||
|
||||
+ If the ``aud`` value is a JSON string, it will match it to the :ref:`jwt-aud`.
|
||||
+ If the ``aud`` value is a JSON array of strings, it will search every element for a match.
|
||||
+ 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.
|
||||
|
||||
.. _jwt_role_extract:
|
||||
|
||||
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.
|
||||
|
||||
The DSL follows the `JSONPath <https://goessner.net/articles/JsonPath/>`_ expression grammar with extended string comparison operators. Supported operators are:
|
||||
|
||||
- ``==`` selects the first array element that exactly matches the right operand
|
||||
- ``!=`` selects the first array element that does not match the right operand
|
||||
- ``^==`` selects the first array element that starts 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
|
||||
|
||||
Usage examples:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
# {"postgrest":{"roles": ["other", "author"]}}
|
||||
# the DSL accepts characters that are alphanumerical or one of "_$@" as keys
|
||||
jwt-role-claim-key = ".postgrest.roles[1]"
|
||||
|
||||
# {"https://www.example.com/role": { "key": "author" }}
|
||||
# non-alphanumerical characters can go inside quotes(escaped in the config value)
|
||||
jwt-role-claim-key = ".\"https://www.example.com/role\".key"
|
||||
|
||||
# {"postgrest":{"roles": ["other", "author"]}}
|
||||
# `@` represents the current element in the array
|
||||
# all the these match the string "author"
|
||||
jwt-role-claim-key = ".postgrest.roles[?(@ == \"author\")]"
|
||||
jwt-role-claim-key = ".postgrest.roles[?(@ != \"other\")]"
|
||||
jwt-role-claim-key = ".postgrest.roles[?(@ ^== \"aut\")]"
|
||||
jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]"
|
||||
jwt-role-claim-key = ".postgrest.roles[?(@ *== \"utho\")]"
|
||||
|
||||
.. 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>`_.
|
||||
|
||||
JWT Security
|
||||
~~~~~~~~~~~~
|
||||
|
||||
@@ -146,6 +146,21 @@ To reload the configuration from within the database, you can use the ``NOTIFY``
|
||||
List of parameters
|
||||
==================
|
||||
|
||||
.. _admin-server-host:
|
||||
|
||||
admin-server-host
|
||||
-----------------
|
||||
|
||||
=============== =======================
|
||||
**Type** String
|
||||
**Default** `server-host` value
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_ADMIN_SERVER_HOST
|
||||
**In-Database** `n/a`
|
||||
=============== =======================
|
||||
|
||||
Specifies the host for the :ref:`admin_server`. Defaults to :ref:`server-host` value.
|
||||
|
||||
.. _admin-server-port:
|
||||
|
||||
admin-server-port
|
||||
@@ -159,31 +174,7 @@ admin-server-port
|
||||
**In-Database** `n/a`
|
||||
=============== =======================
|
||||
|
||||
Specifies the port for the :ref:`admin_server`.
|
||||
|
||||
.. _admin-server-config-enabled:
|
||||
|
||||
admin-server-config-enabled
|
||||
---------------------------
|
||||
|
||||
.. danger::
|
||||
|
||||
The ``/config`` endpoint contains sensitive information, don't enable this if you're exposing the Admin Server publicly.
|
||||
|
||||
To safely enable this you can use a proxy like :ref:`nginx` to:
|
||||
|
||||
- Ensure ``/config`` are only available to local networks.
|
||||
- Only expose ``/live`` and ``/ready`` to public networks.
|
||||
|
||||
=============== =================================
|
||||
**Type** Boolean
|
||||
**Default** False
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_ADMIN_SERVER_CONFIG_ENABLED
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
Enables the admin server :ref:`runtime_config` and :ref:`runtime_schema_cache` endpoints.
|
||||
Specifies the port for the :ref:`admin_server`. Cannot be equal to :ref:`server-port`.
|
||||
|
||||
.. _app.settings.*:
|
||||
|
||||
@@ -200,6 +191,10 @@ app.settings.*
|
||||
|
||||
Arbitrary settings that can be used to pass in secret keys directly as strings, or via OS environment variables. For instance: :code:`app.settings.jwt_secret = "$(MYAPP_JWT_SECRET)"` will take :code:`MYAPP_JWT_SECRET` from the environment and make it available to PostgreSQL functions as :code:`current_setting('app.settings.jwt_secret')`.
|
||||
|
||||
When using the environment variable `PGRST_APP_SETTINGS_*` form, the remainder of the variable is used as the new name. Case is not important : :code:`PGRST_APP_SETTINGS_MY_ENV_VARIABLE=some_value` can be accessed in postgres as :code:`current_setting('app.settings.my_env_variable')`.
|
||||
|
||||
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.
|
||||
|
||||
.. _db-aggregates-enabled:
|
||||
|
||||
db-aggregates-enabled
|
||||
@@ -320,6 +315,10 @@ db-extra-search-path
|
||||
|
||||
Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``.
|
||||
|
||||
.. 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 ``""``.
|
||||
|
||||
.. _db-hoisted-tx-settings:
|
||||
|
||||
db-hoisted-tx-settings
|
||||
@@ -604,11 +603,7 @@ jwt-aud
|
||||
**In-Database** pgrst.jwt_aud
|
||||
=============== =================================
|
||||
|
||||
Specifies the `JWT audience claim <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3>`_. If this claim is present in the client provided JWT then you must set this to the same value as in the JWT, otherwise verifying the JWT will fail.
|
||||
|
||||
.. warning::
|
||||
|
||||
Using this setting will only reject tokens with a different audience claim. Tokens **without** audience claim will still be accepted.
|
||||
Specifies an audience for the JWT ``aud`` claim. See :ref:`jwt_aud_validation`.
|
||||
|
||||
.. _jwt-role-claim-key:
|
||||
|
||||
@@ -625,17 +620,7 @@ jwt-role-claim-key
|
||||
|
||||
*For backwards compatibility, this config parameter is also available without prefix as "role-claim-key".*
|
||||
|
||||
A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. This can be used to consume a JWT provided by a third party service like Auth0, Okta or Keycloak. Usage examples:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
# {"postgrest":{"roles": ["other", "author"]}}
|
||||
# the DSL accepts characters that are alphanumerical or one of "_$@" as keys
|
||||
jwt-role-claim-key = ".postgrest.roles[1]"
|
||||
|
||||
# {"https://www.example.com/role": { "key": "author }}
|
||||
# non-alphanumerical characters can go inside quotes(escaped in the config value)
|
||||
jwt-role-claim-key = ".\"https://www.example.com/role\".key"
|
||||
See :ref:`jwt_role_extract` on how to specify key paths and usage examples.
|
||||
|
||||
.. _jwt-secret:
|
||||
|
||||
@@ -723,6 +708,30 @@ log-level
|
||||
|
||||
Because currently there's no buffering for logging, the levels with minimal logging(``crit/error``) will increase throughput.
|
||||
|
||||
.. _log-query:
|
||||
|
||||
log-query
|
||||
---------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** "disabled"
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_LOG_QUERY
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
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
|
||||
@@ -836,6 +845,12 @@ server-host
|
||||
* :code:`*6` - any IPv4 or IPv6 hostname, IPv6 preferred
|
||||
* :code:`!6` - any IPv6 hostname
|
||||
|
||||
Examples:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
server-host = "127.0.0.1"
|
||||
|
||||
.. _server-port:
|
||||
|
||||
server-port
|
||||
@@ -879,7 +894,7 @@ server-timing-enabled
|
||||
**In-Database** pgrst.server_timing_enabled
|
||||
=============== =================================
|
||||
|
||||
Enables the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>`_ header.
|
||||
Enables the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing>`_ header.
|
||||
See :ref:`server-timing_header`.
|
||||
|
||||
.. _server-unix-socket:
|
||||
|
||||
@@ -5,6 +5,8 @@ Errors
|
||||
|
||||
PostgREST error messages follow the PostgreSQL error structure. It includes ``MESSAGE``, ``DETAIL``, ``HINT``, ``ERRCODE`` and will add an HTTP status code to the response.
|
||||
|
||||
.. _postgresql_errors:
|
||||
|
||||
Errors from PostgreSQL
|
||||
======================
|
||||
|
||||
@@ -205,14 +207,6 @@ Related to the HTTP request elements.
|
||||
| | | specified in the ``select`` part of the query string. |
|
||||
| PGRST108 | | See :ref:`embed_filters`. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst109: | 400 | Restricting a Deletion or an Update using limits must |
|
||||
| | | include the ordering of a unique column. |
|
||||
| PGRST109 | | See :ref:`limited_update_delete`. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst110: | 400 | When restricting a Deletion or an Update using limits |
|
||||
| | | modifies more rows than the maximum specified in the limit. |
|
||||
| PGRST110 | | See :ref:`limited_update_delete`. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst111: | 500 | An invalid ``response.headers`` was set. |
|
||||
| | | See :ref:`guc_resp_hdrs`. |
|
||||
| PGRST111 | | |
|
||||
@@ -241,10 +235,6 @@ Related to the HTTP request elements.
|
||||
| | | there is no many-to-one or one-to-one relationship between |
|
||||
| PGRST118 | | them. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst119: | 400 | Could not use the spread operator on the related table |
|
||||
| | | because there is no many-to-one or one-to-one relationship |
|
||||
| PGRST119 | | between them. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst120: | 400 | An embedded resource can only be filtered using the |
|
||||
| | | ``is.null`` or ``not.is.null`` :ref:`operators <operators>`.|
|
||||
| PGRST120 | | |
|
||||
@@ -257,6 +247,31 @@ Related to the HTTP request elements.
|
||||
| | | ``Prefer: handling=strict``. See :ref:`prefer_handling`. |
|
||||
| PGRST122 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst123: | 400 | Aggregate functions are disabled. |
|
||||
| | | See :ref:`db-aggregates-enabled`. |
|
||||
| PGRST123 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst124: | 400 | ``max-affected`` preference is violated. |
|
||||
| | | See :ref:`prefer_max_affected`. |
|
||||
| PGRST124 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst125: | 404 | Invalid path is specified in request URL. |
|
||||
| | | |
|
||||
| PGRST125 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst126: | 404 | Open API config is disabled but API root path is |
|
||||
| | | accessed. See :ref:`openapi-mode`. |
|
||||
| PGRST126 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst127: | 400 | The feature specified in the ``details`` field is not |
|
||||
| | | implemented. |
|
||||
| PGRST127 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst128: | 400 | ``max-affected`` preference is violated with ``RPC`` call. |
|
||||
| | | See :ref:`prefer_max_affected`. |
|
||||
| PGRST128 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
|
||||
|
||||
.. _pgrst2**:
|
||||
|
||||
@@ -290,6 +305,10 @@ Related to a :ref:`schema_cache`. Most of the time, these errors are solved by :
|
||||
| | | in the ``columns`` query parameter is not found. |
|
||||
| PGRST204 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst205: | 404 | Caused when the :ref:`table specified <tables_views>` in |
|
||||
| | | the URI is not found. |
|
||||
| PGRST205 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
|
||||
.. _pgrst3**:
|
||||
|
||||
@@ -305,14 +324,18 @@ Related to the authentication process using JWT. You can follow the :ref:`tut1`
|
||||
| | | configuration. |
|
||||
| PGRST300 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst301: | 401 | Any error related to the verification of the JWT, |
|
||||
| | | which means that the JWT provided is invalid in some way. |
|
||||
| .. _pgrst301: | 401 | Provided JWT couldn't be decoded or it is invalid. |
|
||||
| | | |
|
||||
| PGRST301 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst302: | 401 | Attempted to do a request without |
|
||||
| | | :ref:`authentication <client_auth>` when the anonymous role |
|
||||
| PGRST302 | | is disabled by not setting it in :ref:`db-anon-role`. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst303: | 401 | :ref:`JWT claims validation <jwt_claims_validation>` |
|
||||
| | | or parsing failed. |
|
||||
| PGRST303 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
|
||||
.. The Internal Errors Group X** is always at the end
|
||||
|
||||
@@ -331,6 +354,8 @@ Internal errors. If you encounter any of these, you may have stumbled on a Postg
|
||||
| PGRSTX00 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
|
||||
.. _custom_errors:
|
||||
|
||||
Custom Errors
|
||||
=============
|
||||
|
||||
@@ -427,3 +452,24 @@ For non standard HTTP status, you can optionally add ``status_text`` to describe
|
||||
detail = '{"status":419,"status_text":"Page Expired","headers":{"X-Powered-By":"Nerd Rage"}}';
|
||||
|
||||
If PostgREST can't parse the JSON objects ``message`` and ``detail``, it will throw a ``PGRST121`` error. See :ref:`Errors from PostgREST<pgrst1**>`.
|
||||
|
||||
.. _proxy-status_header:
|
||||
|
||||
Proxy-Status Header
|
||||
===================
|
||||
|
||||
For error cases, the standard `Proxy-Status <https://www.rfc-editor.org/rfc/rfc9209.html#name-the-proxy-status-http-field>`_ header is returned with the error code. The error code comes from either :ref:`PostgREST <pgrst_errors>`, :ref:`PostgreSQL <postgresql_errors>` or :ref:`Custom <custom_errors>` errors. This is useful when doing ``HEAD`` requests where the HTTP status is not descriptive enough.
|
||||
|
||||
For example, doing a request on a table with high count (say 30_000_000), we get:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HEAD /table HTTP/1.1
|
||||
Prefer: count=exact
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 500 Internal Server Error
|
||||
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.
|
||||
|
||||
@@ -15,12 +15,14 @@ Observability allows measuring a system's current state based on the data it gen
|
||||
Logs
|
||||
====
|
||||
|
||||
PostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, and HTTP response status.
|
||||
PostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, the HTTP response status and the response body size in bytes if available.
|
||||
|
||||
With :ref:`log-level` set to ``info``, we get:
|
||||
|
||||
.. code::
|
||||
|
||||
127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 - "" "curl/7.64.0"
|
||||
127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 - "" "curl/7.64.0"
|
||||
127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 56 "" "curl/7.64.0"
|
||||
127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 162 "" "curl/7.64.0"
|
||||
|
||||
For diagnostic information about the server itself, PostgREST logs to ``stderr``:
|
||||
|
||||
@@ -31,22 +33,52 @@ For diagnostic information about the server itself, PostgREST logs to ``stderr``
|
||||
.. code::
|
||||
|
||||
06/May/2024:08:16:11 -0500: Starting PostgREST 12.1...
|
||||
06/May/2024:08:16:11 -0500: Attempting to connect to the database...
|
||||
06/May/2024:08:16:11 -0500: Successfully connected to PostgreSQL 14.10 (Ubuntu 14.10-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit
|
||||
06/May/2024:08:16:11 -0500: Listening on port 3000
|
||||
06/May/2024:08:16:11 -0500: Listening for notifications on the "pgrst" channel
|
||||
06/May/2024:08:16:11 -0500: Connection Pool initialized with a maximum size of 10 connections
|
||||
06/May/2024:08:16:11 -0500: API server listening on port 3000
|
||||
06/May/2024:08:16:11 -0500: Listening for database notifications on the "pgrst" channel
|
||||
06/May/2024:08:16:11 -0500: Config reloaded
|
||||
06/May/2024:08:16:11 -0500: Schema cache queried in 3.8 milliseconds
|
||||
06/May/2024:08:16:11 -0500: Schema cache loaded 15 Relations, 8 Relationships, 8 Functions, 0 Domain Representations, 4 Media Type Handlers
|
||||
06/May/2024:14:11:27 -0500: Received a config reload message on the "pgrst" channel
|
||||
06/May/2024:14:11:27 -0500: Config reloaded
|
||||
|
||||
.. note::
|
||||
|
||||
Logs are based on the ``log-level`` setting. See :ref:`log-level`.
|
||||
|
||||
.. _sql_query_logs:
|
||||
|
||||
SQL Query Logs
|
||||
--------------
|
||||
|
||||
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
|
||||
|
||||
log-level = "warn"
|
||||
log-query = "main-query"
|
||||
|
||||
The SQL queries will only be logged on ``400`` HTTP errors and up.
|
||||
So, if the user requests a resource without sufficient privileges:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "localhost:3000/protected_table"
|
||||
|
||||
This will be logged by PostgREST:
|
||||
|
||||
.. code::
|
||||
|
||||
17/Feb/2025:17:28:15 -0500: WITH pgrst_source AS ( SELECT "public"."protected_table".* FROM "public"."protected_table" ) SELECT null::bigint AS total_result_set, pg_catalog.count(_postgrest_t) AS page_total, coalesce(json_agg(_postgrest_t), '[]') AS body, nullif(current_setting('response.headers', true), '') AS response_headers, nullif(current_setting('response.status', true), '') AS response_status, '' AS response_inserted FROM ( SELECT * FROM pgrst_source ) _postgrest_t
|
||||
127.0.0.1 - web_anon [17/Feb/2025:17:28:15 -0500] "GET /protected_table HTTP/1.1" 401 99 "" "curl/8.7.1"
|
||||
|
||||
Database Logs
|
||||
-------------
|
||||
|
||||
Currently PostgREST doesn't log the SQL commands executed against the underlying database.
|
||||
|
||||
To find the SQL operations, you can watch the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below.
|
||||
Additionally, to find all the SQL operations, you can watch the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below.
|
||||
|
||||
Find :code:`postgresql.conf` inside your PostgreSQL data directory (to find that, issue the command :code:`show data_directory;`). Either find the settings scattered throughout the file and change them to the following values, or append this block of code to the end of the configuration file.
|
||||
|
||||
@@ -96,6 +128,11 @@ The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in
|
||||
|
||||
curl "http://localhost:3001/metrics"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
# HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load
|
||||
# TYPE pgrst_schema_cache_query_time_seconds gauge
|
||||
pgrst_schema_cache_query_time_seconds 1.5937927e-2
|
||||
@@ -204,12 +241,17 @@ You can enable tracing HTTP requests by setting :ref:`server-trace-header`. Spec
|
||||
HTTP/1.1 200 OK
|
||||
X-Request-Id: 123
|
||||
|
||||
Proxy-Status Header
|
||||
-------------------
|
||||
|
||||
See :ref:`proxy-status_header`.
|
||||
|
||||
.. _server-timing_header:
|
||||
|
||||
Server-Timing Header
|
||||
--------------------
|
||||
|
||||
You can enable the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>`_ header by setting :ref:`server-timing-enabled` on.
|
||||
You can enable the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing>`_ header by setting :ref:`server-timing-enabled` on.
|
||||
This header communicates metrics of the different phases in the request-response cycle.
|
||||
|
||||
.. code-block:: bash
|
||||
@@ -233,6 +275,27 @@ This header communicates metrics of the different phases in the request-response
|
||||
|
||||
We're working on lowering the duration of the ``parse`` and ``plan`` stages on https://github.com/PostgREST/postgrest/issues/2816.
|
||||
|
||||
.. _content-length_header:
|
||||
|
||||
Content-Length Header
|
||||
---------------------
|
||||
|
||||
You can verify the response body size in bytes in the `Content-Length header <https://httpwg.org/specs/rfc9110.html#field.content-length>`_.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i 'localhost:3000/users'
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Length: 104
|
||||
|
||||
Note that this header won't be returned on ``HEAD`` requests for optimization purposes (see :ref:`head_req`).
|
||||
This is in line with `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.content-length>`_.
|
||||
|
||||
The body size is also present in the :ref:`PostgREST logs <pgrst_logging>`.
|
||||
|
||||
.. _explain_plan:
|
||||
|
||||
Execution plan
|
||||
|
||||
@@ -284,11 +284,10 @@ However, starting from PostgreSQL 15, you can grant privileges for these setting
|
||||
|
||||
GRANT SET ON PARAMETER <setting> TO <authenticator>;
|
||||
|
||||
Function Settings
|
||||
~~~~~~~~~~~~~~~~~
|
||||
Hoisted Function Settings
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In addition to :ref:`impersonated_settings`, PostgREST will also apply function settings as transaction-scoped settings. This allows functions settings to override
|
||||
the impersonated and connection role settings.
|
||||
PostgREST can "hoist" function settings to transaction-scoped settings. This allows functions settings to override the impersonated and connection role settings.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
@@ -303,7 +302,7 @@ When calling the above function (see :ref:`functions`), the statement timeout wi
|
||||
|
||||
.. note::
|
||||
|
||||
Only the transactions that are hoisted by config :ref:`db-hoisted-tx-settings` will be applied.
|
||||
Only the settings in :ref:`db-hoisted-tx-settings` will be hoisted.
|
||||
|
||||
.. _main_query:
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
docutils==0.20.1
|
||||
sphinx-copybutton
|
||||
sphinx-intl
|
||||
sphinx-rtd-theme>=0.5.1
|
||||
sphinx-rtd-dark-mode>=1.3.0
|
||||
sphinx-tabs>=3.2.0
|
||||
sphinx>=5.0.2
|
||||
# This file is auto-generated by postgrest-nixpkgs-upgrade
|
||||
sphinx==7.4.7
|
||||
sphinx-copybutton==0.5.2
|
||||
sphinx-rtd-dark-mode==1.3.0
|
||||
sphinx-rtd-theme==3.0.2
|
||||
sphinx-tabs==3.4.7
|
||||
sphinxext-opengraph==0.9.1
|
||||
urllib3==2.2.3
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
pacman -S postgrest
|
||||
|
||||
.. tab:: Nix
|
||||
.. tab:: Nix via nixpkgs
|
||||
|
||||
You can install PostgREST from nixpkgs.
|
||||
|
||||
@@ -36,6 +36,17 @@
|
||||
|
||||
nix-env -i postgrest
|
||||
|
||||
.. tab:: Nix via flake
|
||||
|
||||
You can install PostgREST via flake.
|
||||
|
||||
.. code:: nix
|
||||
|
||||
{
|
||||
inputs.postgrest.url = "github:postgrest/postgrest";
|
||||
# ...
|
||||
}
|
||||
|
||||
.. group-tab:: Windows
|
||||
|
||||
You can install PostgREST using `Chocolatey <https://community.chocolatey.org/packages/postgrest>`_ or `Scoop <https://github.com/ScoopInstaller/Scoop>`_.
|
||||
|
||||
@@ -213,12 +213,8 @@ You should see something similar to:
|
||||
.. code-block:: text
|
||||
|
||||
Starting PostgREST 12.0.2...
|
||||
Attempting to connect to the database...
|
||||
Connection successful
|
||||
Listening on port 3000
|
||||
Config reloaded
|
||||
Listening for notifications on the pgrst channel
|
||||
Schema cache loaded
|
||||
Successfully connected to PostgreSQL 14.10 (Ubuntu 14.10-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit
|
||||
API server listening on port 3000
|
||||
|
||||
It's now ready to serve web requests. There are many nice graphical API exploration tools you can use, but for this tutorial we'll use :code:`curl` because it's likely to be installed on your system already. Open a new terminal (leaving the one open that PostgREST is running inside). Try doing an HTTP request for the todos.
|
||||
|
||||
|
||||
@@ -52,17 +52,31 @@ Check that the :code:`tutorial.conf` (created in the previous tutorial) has the
|
||||
|
||||
If the PostgREST server is still running from the previous tutorial, restart it to load the updated configuration file.
|
||||
|
||||
.. _tut1_step3:
|
||||
|
||||
Step 3. Sign a Token
|
||||
--------------------
|
||||
|
||||
Ordinarily your own code in the database or in another server will create and sign authentication tokens, but for this tutorial we will make one "by hand." Go to `jwt.io <https://jwt.io/#debugger-io>`_ and fill in the fields like this:
|
||||
Ordinarily your own code in the database or in another server will create and sign authentication tokens, but for this tutorial we will make one "by hand" using ``bash`` and ``openssl``.
|
||||
|
||||
.. figure:: ../_static/tuts/tut1-jwt-io.png
|
||||
:alt: jwt.io interface
|
||||
.. code:: bash
|
||||
|
||||
How to create a token at https://jwt.io
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
**Remember to fill in the secret you generated rather than the word "secret".** After you have filled in the secret and payload, the encoded data on the left will update. Copy the encoded token.
|
||||
JWT_SECRET='test_secret_that_is_at_least_32_characters_long'
|
||||
|
||||
_base64 () { openssl base64 -e -A | tr '+/' '-_' | tr -d '='; }
|
||||
|
||||
header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | _base64)
|
||||
|
||||
payload=$(echo -n "{\"role\":\"todo_user\"}" | _base64)
|
||||
|
||||
signature=$(echo -n "$header.$payload" | openssl dgst -sha256 -hmac "$JWT_SECRET" -binary | _base64)
|
||||
|
||||
echo -n "$header.$payload.$signature"
|
||||
|
||||
**Remember to fill in the secret you generated rather than keeping the "test_secret_that_is_at_least_32_characters_long".** After you have filled in the secret and payload, the encoded data on the left will update. Copy the encoded token.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -145,14 +159,22 @@ To observe expiration in action, we'll add an :code:`exp` claim of five minutes
|
||||
|
||||
select extract(epoch from now() + '5 minutes'::interval) :: integer;
|
||||
|
||||
Go back to jwt.io and change the payload to
|
||||
Or in ``bash``:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"role": "todo_user",
|
||||
"exp": 123456789
|
||||
}
|
||||
.. code-block:: bash
|
||||
|
||||
exp=$(( EPOCHSECONDS + 5*60 )) # five minutes
|
||||
|
||||
echo $exp
|
||||
|
||||
Go back to :ref:`tut1_step3` and change the payload to
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
payload=$(echo -n "{\"role\":\"todo_user\",\"exp\":\"123456789\"}" | _base64)
|
||||
|
||||
echo -n "$header.$payload.$signature"
|
||||
|
||||
**NOTE**: Don't forget to change the dummy epoch value :code:`123456789` in the snippet above to the epoch value returned by the :code:`psql` command.
|
||||
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1731165248,
|
||||
"narHash": "sha256-DiHFKIdBmMx5/DUARhVqaxvEIiy4EE6Eqs9Qs4oxme8=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a90280100f41a10914edfe729a4053e60c92b8e3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
description = "REST API for any Postgres database";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||
};
|
||||
|
||||
nixConfig = {
|
||||
extra-substituters = "https://postgrest.cachix.org";
|
||||
extra-trusted-public-keys = "postgrest.cachix.org-1:icgW4R15fz1+LqvhPjt4EnX/r19AaqxiVV+1olwlZtI=";
|
||||
};
|
||||
|
||||
outputs = { nixpkgs, ... }:
|
||||
let
|
||||
systems = [
|
||||
"aarch64-darwin"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"x86_64-linux"
|
||||
];
|
||||
|
||||
pgrstFor = system: import ./default.nix {
|
||||
inherit system;
|
||||
nixpkgsVersion = {
|
||||
owner = "nixos";
|
||||
repo = "nixpkgs";
|
||||
inherit (nixpkgs) rev;
|
||||
tarballHash = nixpkgs.narHash;
|
||||
};
|
||||
};
|
||||
|
||||
genSystems = f: nixpkgs.lib.genAttrs systems (system: f (pgrstFor system));
|
||||
in
|
||||
{
|
||||
packages = genSystems (attrs: {
|
||||
default = attrs.postgrestPackage;
|
||||
profiled = attrs.postgrestProfiled;
|
||||
} // nixpkgs.lib.optionalAttrs (attrs ? postgrestStatic) {
|
||||
static = attrs.postgrestStatic;
|
||||
});
|
||||
|
||||
apps = genSystems (attrs: {
|
||||
default = {
|
||||
type = "app";
|
||||
program = "${attrs.postgrestStatic or attrs.postgrestPackage}/bin/postgrest";
|
||||
meta.description = "REST API for any Postgres database";
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -72,45 +72,53 @@ The PostgREST utilities available in `nix-shell` all have names that begin with
|
||||
```bash
|
||||
# Note: The utilities listed here might not be up to date.
|
||||
[nix-shell]$ postgrest-<tab>
|
||||
postgrest-build postgrest-test-spec
|
||||
postgrest-check postgrest-watch
|
||||
postgrest-clean postgrest-with-all
|
||||
postgrest-coverage postgrest-with-postgresql-10
|
||||
postgrest-lint postgrest-with-postgresql-11
|
||||
postgrest-run postgrest-with-postgresql-12
|
||||
postgrest-style postgrest-with-postgresql-13
|
||||
postgrest-style-check postgrest-with-postgresql-9.6
|
||||
postgrest-test-io
|
||||
postgrest-build postgrest-profiled-run
|
||||
postgrest-check postgrest-push-cachix
|
||||
postgrest-clean postgrest-release
|
||||
postgrest-coverage postgrest-repl
|
||||
postgrest-coverage-draft-overlay postgrest-run
|
||||
postgrest-docs-build postgrest-style
|
||||
postgrest-docs-check postgrest-style-check
|
||||
postgrest-docs-dictcheck postgrest-test-big-schema
|
||||
postgrest-docs-linkcheck postgrest-test-doctests
|
||||
postgrest-docs-render postgrest-test-io
|
||||
postgrest-docs-serve postgrest-test-memory
|
||||
postgrest-docs-spellcheck postgrest-test-replica
|
||||
postgrest-dump-minimal-imports postgrest-test-spec
|
||||
postgrest-dump-schema postgrest-test-spec-idempotence
|
||||
postgrest-gen-ctags postgrest-watch
|
||||
postgrest-gen-jwt postgrest-with-all
|
||||
postgrest-gen-secret postgrest-with-git
|
||||
postgrest-git-hooks postgrest-with-pgrst
|
||||
postgrest-hsie-graph-modules postgrest-with-postgresql-12
|
||||
postgrest-hsie-graph-symbols postgrest-with-postgresql-13
|
||||
postgrest-hsie-minimal-imports postgrest-with-postgresql-14
|
||||
postgrest-lint postgrest-with-postgresql-15
|
||||
postgrest-loadtest postgrest-with-postgresql-16
|
||||
postgrest-loadtest-against postgrest-with-postgresql-17
|
||||
postgrest-loadtest-report postgrest-with-slow-pg
|
||||
postgrest-nixpkgs-upgrade postgrest-with-slow-postgrest
|
||||
postgrest-parallel-curl
|
||||
...
|
||||
|
||||
[nix-shell]$
|
||||
|
||||
```
|
||||
|
||||
Some additional modules like `memory`, `docker` and `release`
|
||||
have large dependencies that would need to be built before the shell becomes
|
||||
The `docker` module has large dependencies to be build before the shell becomes
|
||||
available, which could take an especially long time if the cachix binary cache
|
||||
is not used. You can activate those by passing a flag to `nix-shell` with
|
||||
`nix-shell --arg <module> true`. This will make the respective utilities available:
|
||||
is not used. You can activate it by passing a flag to `nix-shell` with
|
||||
`nix-shell --arg docker true`. This will make the respective utilities available:
|
||||
|
||||
```bash
|
||||
$ nix-shell --arg memory true
|
||||
[nix-shell]$ postgrest-<tab>
|
||||
postgrest-build postgrest-test-spec
|
||||
postgrest-check postgrest-watch
|
||||
postgrest-clean postgrest-with-all
|
||||
postgrest-coverage postgrest-with-postgresql-10
|
||||
postgrest-lint postgrest-with-postgresql-11
|
||||
postgrest-run postgrest-with-postgresql-12
|
||||
postgrest-style postgrest-with-postgresql-13
|
||||
postgrest-style-check postgrest-with-postgresql-9.6
|
||||
postgrest-test-io
|
||||
postgrest-test-memory
|
||||
$ nix-shell --arg docker true
|
||||
[nix-shell]$ postgrest-docker-<tab>
|
||||
postgrest-docker-load
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
Note that `postgrest-test-memory` is now also available.
|
||||
Note that `postgrest-docker-load` is now also available.
|
||||
|
||||
To run one-off commands, you can also use `nix-shell --run <command>`, which
|
||||
will launch the Nix shell, run that one command and exit. Note that the tab
|
||||
@@ -371,10 +379,8 @@ that).
|
||||
|
||||
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
|
||||
where or when you use it. The pinned version can be upgraded with the small
|
||||
`nixpkgs-upgrade` utility. Running `nixpkgs-upgrade > nix/nixpkgs-version.nix`
|
||||
in `nix-shell` will upgrade the pinned version to the latest `nixpkgs-unstable`
|
||||
version.
|
||||
where or when you use it. The pinned version is taken from `flake.lock` and
|
||||
can be updated with `nix flake update`.
|
||||
|
||||
### `shell.nix`
|
||||
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
{ buildPythonPackage
|
||||
, fetchFromGitHub
|
||||
, lib
|
||||
, nose
|
||||
, sphinx
|
||||
, sphinx-rtd-theme
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "sphinx-rtd-dark-mode";
|
||||
version = "1.3.0";
|
||||
format = "setuptools";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "MrDogeBro";
|
||||
repo = "sphinx_rtd_dark_mode";
|
||||
rev = "refs/tags/v${version}";
|
||||
hash = "sha256-N5KG2Wqn9wfGNY3VH4FnBce1aZUbnvVmwD10Loe0Qn4=";
|
||||
};
|
||||
|
||||
propagatedBuildInputs = [
|
||||
sphinx-rtd-theme
|
||||
];
|
||||
|
||||
nativeCheckInputs = [
|
||||
nose
|
||||
sphinx
|
||||
];
|
||||
|
||||
checkPhase = ''
|
||||
runHook preCheck
|
||||
nosetests tests
|
||||
runHook postCheck
|
||||
'';
|
||||
|
||||
pythonImportsCheck = [
|
||||
"sphinx_rtd_dark_mode"
|
||||
];
|
||||
|
||||
meta = with lib; {
|
||||
description = "Adds a toggleable dark mode to the Read the Docs theme for Sphinx.";
|
||||
homepage = "https://github.com/MrDogeBro/sphinx_rtd_dark_mode";
|
||||
changelog = "https://github.com/MrDogeBro/sphinx_rtd_dark_mode/releases/tag/v${version}";
|
||||
license = licenses.mit;
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,10 @@ 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"
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
# Pinned version of Nixpkgs, generated with postgrest-nixpkgs-upgrade.
|
||||
{
|
||||
owner = "NixOS";
|
||||
repo = "nixpkgs";
|
||||
date = "2024-05-29";
|
||||
rev = "a15e8d1b3d9e6496c4a3214e2104f6d28dfa7df7";
|
||||
tarballHash = "sha256:0r4a4165f4n1zlpnyjrdrwrg86n1b6g3axsdh3j5iizpmjrlxmd7";
|
||||
}
|
||||
@@ -3,9 +3,6 @@
|
||||
checked-shell-script = import ./checked-shell-script;
|
||||
gitignore = import ./gitignore.nix;
|
||||
haskell-packages = import ./haskell-packages.nix;
|
||||
postgis = import ./postgis.nix;
|
||||
postgresql-libpq = import ./postgresql-libpq.nix;
|
||||
postgresql-legacy = import ./postgresql-legacy.nix;
|
||||
postgresql-future = import ./postgresql-future.nix;
|
||||
slocat = import ./slocat.nix;
|
||||
}
|
||||
|
||||
@@ -48,14 +48,17 @@ let
|
||||
|
||||
# Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329
|
||||
# jailbreak, because hspec limit for tests
|
||||
fuzzyset = lib.doJailbreak
|
||||
(prev.callHackageDirect
|
||||
fuzzyset = prev.fuzzyset_0_2_4;
|
||||
|
||||
# TODO: Remove once available in nixpkgs haskellPackages
|
||||
configurator-pg =
|
||||
prev.callHackageDirect
|
||||
{
|
||||
pkg = "fuzzyset";
|
||||
ver = "0.2.4";
|
||||
sha256 = "sha256-lpkrTFcR0B4rT/P6x7ui31Twgq7BBj6KIvjKyqXKdpc=";
|
||||
pkg = "configurator-pg";
|
||||
ver = "0.2.11";
|
||||
sha256 = "sha256-mtGtNawDJgz2ZIEVca+IYXVu4oNw9xsfJiYWAqAbbgc=";
|
||||
}
|
||||
{ });
|
||||
{ };
|
||||
|
||||
hasql-pool = lib.dontCheck (prev.callHackageDirect
|
||||
{
|
||||
@@ -63,13 +66,7 @@ let
|
||||
ver = "1.0.1";
|
||||
sha256 = "sha256-Hf1f7lX0LWkjrb25SDBovCYPRdmUP1H6pAxzi7kT4Gg=";
|
||||
}
|
||||
{ }
|
||||
);
|
||||
|
||||
postgresql-libpq = lib.dontCheck
|
||||
(prev.postgresql-libpq.override {
|
||||
postgresql = super.libpq;
|
||||
});
|
||||
{ });
|
||||
|
||||
hasql-notifications = lib.dontCheck (prev.callHackageDirect
|
||||
{
|
||||
@@ -77,9 +74,20 @@ let
|
||||
ver = "0.2.2.2";
|
||||
sha256 = "sha256-myKwlug7OgTa/qP6mHfCD+5Q8IhM17JvpJBfSo+M01k=";
|
||||
}
|
||||
{ }
|
||||
);
|
||||
{ });
|
||||
|
||||
# newer nixpkgs already has 0.10., so we fallback to default for forward compat
|
||||
jose-jwt = prev.jose-jwt_0_10_0 or prev.jose-jwt;
|
||||
|
||||
postgresql-libpq = lib.dontCheck (prev.callHackageDirect
|
||||
{
|
||||
pkg = "postgresql-libpq";
|
||||
ver = "0.10.1.0";
|
||||
sha256 = "sha256-tXOMqCO8opMilI9rx0D+njqjIjbZsH168Bzb8Aq8Ff4=";
|
||||
}
|
||||
{
|
||||
postgresql = super.libpq;
|
||||
});
|
||||
};
|
||||
in
|
||||
{
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
final: prev:
|
||||
let
|
||||
postgis_3_2_3 = rec {
|
||||
version = "3.2.3";
|
||||
src = final.fetchurl {
|
||||
url = "https://download.osgeo.org/postgis/source/postgis-${version}.tar.gz";
|
||||
sha256 = "sha256-G02LXHVuWrpZ77wYM7Iu/k1lYneO7KVvpJf+susTZow=";
|
||||
};
|
||||
meta.broken = false;
|
||||
};
|
||||
in
|
||||
{
|
||||
postgresql_11 = prev.postgresql_11.override { this = final.postgresql_11; } // {
|
||||
pkgs = prev.postgresql_11.pkgs // {
|
||||
postgis = prev.postgresql_11.pkgs.postgis.overrideAttrs (_: postgis_3_2_3);
|
||||
};
|
||||
};
|
||||
postgresql_10 = prev.postgresql_10.override { this = final.postgresql_11; } // {
|
||||
pkgs = prev.postgresql_10.pkgs // {
|
||||
postgis = prev.postgresql_10.pkgs.postgis.overrideAttrs (_: postgis_3_2_3);
|
||||
};
|
||||
};
|
||||
postgresql_9_6 = prev.postgresql_9_6.override { this = final.postgresql_11; } // {
|
||||
pkgs = prev.postgresql_9_6.pkgs // {
|
||||
postgis = prev.postgresql_9_6.pkgs.postgis.overrideAttrs (_: postgis_3_2_3);
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
_: _:
|
||||
# Overlay that adds future versions of PostgreSQL that are supported by
|
||||
# PostgREST.
|
||||
{
|
||||
## Example for including a postgresql version from a specific nixpks commit:
|
||||
##
|
||||
# postgresql_16 =
|
||||
# let
|
||||
# rev = "5148520bfab61f99fd25fb9ff7bfbb50dad3c9db";
|
||||
# tarballHash = "1dfjmz65h8z4lk845724vypzmf3dbgsdndjpj8ydlhx6c7rpcq3p";
|
||||
#
|
||||
# pinnedPkgs =
|
||||
# builtins.fetchTarball {
|
||||
# url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||
# sha256 = tarballHash;
|
||||
# };
|
||||
# in
|
||||
# (import pinnedPkgs { }).pkgs.postgresql_16;
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
_: _:
|
||||
# Overlay that adds legacy versions of PostgreSQL that are supported by
|
||||
# PostgREST.
|
||||
{
|
||||
# PostgreSQL 9.6 was removed from Nixpkgs with
|
||||
# https://github.com/NixOS/nixpkgs/commit/757dd008b2f2926fc0f7688fa8189f930ea47521
|
||||
# We pin its parent commit to get the last version that was available.
|
||||
postgresql_9_6 =
|
||||
let
|
||||
rev = "571cbf3d1db477058303cef8754fb85a14e90eb7";
|
||||
tarballHash = "0q74wn418i1bn5sssacmw8ykpmqvzr0s93sj6pbs3rf6bf134fkz";
|
||||
pinnedPkgs =
|
||||
builtins.fetchTarball {
|
||||
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||
sha256 = tarballHash;
|
||||
};
|
||||
in
|
||||
(import pinnedPkgs { }).pkgs.postgresql_9_6;
|
||||
|
||||
# PostgreSQL 10 was removed from Nixpkgs with
|
||||
# https://github.com/NixOS/nixpkgs/commit/aa1483114bb329fee7e1266100b8d8921ed4723f
|
||||
# We pin its parent commit to get the last version that was available.
|
||||
postgresql_10 =
|
||||
let
|
||||
rev = "79661ba7e2fb96ebefbb537458a5bbae9dc5bd1a";
|
||||
tarballHash = "0rn796pfn4sg90ai9fdnwmr10a2s835p1arazzgz46h6s5cxvq97";
|
||||
pinnedPkgs =
|
||||
builtins.fetchTarball {
|
||||
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||
sha256 = tarballHash;
|
||||
};
|
||||
in
|
||||
(import pinnedPkgs { }).pkgs.postgresql_10;
|
||||
|
||||
# PostgreSQL 11 was removed from Nixpkgs with
|
||||
# https://github.com/NixOS/nixpkgs/commit/1220a4d4dd1a4590780a5e1c18d1333a121be366
|
||||
# We pin its parent commit to get the last version that was available.
|
||||
postgresql_11 =
|
||||
let
|
||||
rev = "f5458516e42cc5cb4123cc2d93f45c240548aa18";
|
||||
tarballHash = "1h03621sxfhw4z6ya74k6c2lyx3z7pvf2jcg4vs7i01yz2m6w3cv";
|
||||
pinnedPkgs =
|
||||
builtins.fetchTarball {
|
||||
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||
sha256 = tarballHash;
|
||||
};
|
||||
in
|
||||
(import pinnedPkgs { }).pkgs.postgresql_11;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
_: super:
|
||||
{
|
||||
libpq = super.callPackage ../libpq.nix {
|
||||
# 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;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,12 +4,8 @@
|
||||
, src
|
||||
}:
|
||||
let
|
||||
# This builds a static PostgREST exectuable based on pkgsStatic.
|
||||
# pkgsStatic is based on musl, so is a kind of cross-compilation.
|
||||
# We still make this explicit here via pkgsCross, because we need
|
||||
# to get postgresql/libpq for musl, too.
|
||||
pkgsCross = pkgs.pkgsCross.musl64;
|
||||
inherit (pkgsCross) pkgsStatic;
|
||||
# This builds a static PostgREST executable based on pkgsStatic.
|
||||
inherit (pkgs) pkgsStatic;
|
||||
inherit (pkgsStatic.haskell) lib;
|
||||
|
||||
packagesStatic =
|
||||
@@ -25,9 +21,16 @@ let
|
||||
|
||||
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 = [ pkgsCross.libpq ];
|
||||
libraryPkgconfigDepends = [ pkgsStatic.libpq ];
|
||||
librarySystemDepends = [ ];
|
||||
}).overrideAttrs (_: prevAttrs: {
|
||||
buildInputs = prevAttrs.buildInputs ++ [ pkgsStatic.openssl ];
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ let
|
||||
withEnv = postgrest.env;
|
||||
}
|
||||
''
|
||||
${cabal-install}/bin/cabal v2-update
|
||||
exec ${cabal-install}/bin/cabal v2-build ${devCabalOptions} "''${_arg_leftovers[@]}"
|
||||
'';
|
||||
|
||||
@@ -59,6 +60,35 @@ let
|
||||
postgrest "''${_arg_leftovers[@]}"
|
||||
'';
|
||||
|
||||
|
||||
runProfiled =
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-profiled-run";
|
||||
docs = "Run a profiled build of postgREST. This will generate a postgrest.prof file that can be used to do optimization.";
|
||||
args =
|
||||
[
|
||||
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [PostgREST anonymous role])"
|
||||
"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_JWT_SECRET], [reallyreallyreallyreallyverysafe], [PostgREST JWT secret])"
|
||||
"ARG_LEFTOVERS([PostgREST arguments])"
|
||||
];
|
||||
workingDir = "/";
|
||||
withEnv = postgrest.env;
|
||||
}
|
||||
''
|
||||
export PGRST_DB_ANON_ROLE
|
||||
export PGRST_DB_POOL
|
||||
export PGRST_DB_POOL_ACQUISITION_TIMEOUT
|
||||
export PGRST_JWT_SECRET
|
||||
|
||||
${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-run -- \
|
||||
postgrest +RTS -p -h -RTS "''${_arg_leftovers[@]}"
|
||||
'';
|
||||
|
||||
repl =
|
||||
checkedShellScript
|
||||
{
|
||||
@@ -80,6 +110,7 @@ buildToolbox
|
||||
build
|
||||
clean
|
||||
run
|
||||
runProfiled
|
||||
repl;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -246,6 +246,7 @@ let
|
||||
}
|
||||
''
|
||||
mkdir -p "$_arg_dumpdir"
|
||||
${cabal-install}/bin/cabal v2-update
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} \
|
||||
--builddir="$tmpdir" \
|
||||
--ghc-option=-ddump-minimal-imports \
|
||||
@@ -333,20 +334,29 @@ let
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-gen-jwt";
|
||||
docs = "Generate a JWT";
|
||||
docs = ''
|
||||
Generate a JWT. Example: postgrest-gen-jwt --exp 10 postgrest_test_author
|
||||
|
||||
# This can be used to quickly prove a JWT expiry
|
||||
$ curl localhost:3000/authors_only -H "Authorization: Bearer \$(postgrest-gen-jwt --exp -31 postgrest_test_author)"
|
||||
'';
|
||||
args = [
|
||||
"ARG_POSITIONAL_SINGLE([role], [role for the jwt payload])"
|
||||
"ARG_OPTIONAL_SINGLE([secret],, [secret used to sign the JWT], [reallyreallyreallyreallyverysafe])"
|
||||
"ARG_OPTIONAL_SINGLE([exp],, [seconds for JWT expiry, it accepts negative values], [3600])"
|
||||
];
|
||||
}
|
||||
''
|
||||
# From https://stackoverflow.com/questions/59002949/how-to-create-a-json-web-token-jwt-using-openssl-shell-commands
|
||||
# Based on https://stackoverflow.com/questions/59002949/how-to-create-a-json-web-token-jwt-using-openssl-shell-commands
|
||||
|
||||
# Construct the header
|
||||
jwt_header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | base64 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//)
|
||||
|
||||
# Construct the exp value
|
||||
expiry=$((EPOCHSECONDS + _arg_exp))
|
||||
|
||||
# Construct the payload
|
||||
payload=$(echo -n "{\"role\":\"$_arg_role\"}" | base64 | sed s/\+/-/g |sed 's/\//_/g' | sed -E s/=+$//)
|
||||
payload=$(echo -n "{\"role\": \"$_arg_role\", \"exp\": $expiry}" | base64 | sed s/\+/-/g |sed 's/\//_/g' | sed -E s/=+$//)
|
||||
|
||||
# Convert secret to hex
|
||||
hexsecret=$(echo -n "$_arg_secret" | xxd -p | paste -sd "")
|
||||
|
||||
@@ -2,23 +2,40 @@
|
||||
, aspellDicts
|
||||
, buildToolbox
|
||||
, checkedShellScript
|
||||
, fetchPypi
|
||||
, lib
|
||||
, plantuml
|
||||
, python3
|
||||
, python3Packages
|
||||
, writeTextFile
|
||||
, writers
|
||||
, plantuml
|
||||
}:
|
||||
let
|
||||
selectPythonPackages = ps: [
|
||||
ps.sphinx
|
||||
ps.sphinx_rtd_theme
|
||||
ps.livereload
|
||||
ps.sphinx-tabs
|
||||
ps.sphinx-copybutton
|
||||
ps.sphinxext-opengraph
|
||||
(ps.callPackage ../docs-extensions/sphinx-rtd-dark-mode.nix { })
|
||||
# TODO: Remove override once new sphinx-intl version (> 2.1.0) is released and available in nixpkgs
|
||||
(ps.sphinx-intl.overrideAttrs (drv: { nativeBuildInputs = drv.nativeBuildInputs ++ [ ps.six ]; }))
|
||||
];
|
||||
selectPythonPackages = ps:
|
||||
let
|
||||
# TODO: Remove with next nixpkgs update
|
||||
sphinx-rtd-theme = assert ps.sphinx-rtd-theme.version == "2.0.0"; ps.sphinx-rtd-theme.overrideAttrs rec {
|
||||
version = "3.0.2";
|
||||
src = fetchPypi {
|
||||
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 {
|
||||
name = "requirements.txt";
|
||||
text = lib.concatMapStringsSep "\n" (pkg: "${pkg.pname}==${pkg.version}") (selectPythonPackages python3Packages);
|
||||
};
|
||||
|
||||
python = python3.withPackages selectPythonPackages;
|
||||
|
||||
@@ -73,7 +90,7 @@ let
|
||||
server =
|
||||
writers.writePython3
|
||||
"postgrest-docs-server"
|
||||
{ libraries = selectPythonPackages python3Packages; }
|
||||
{ libraries = selectPythonPackages python3Packages ++ [ python3Packages.livereload ]; }
|
||||
''
|
||||
import sys
|
||||
from livereload import Server, shell
|
||||
@@ -183,4 +200,5 @@ buildToolbox
|
||||
serve
|
||||
spellcheck;
|
||||
};
|
||||
extra = { inherit requirements; };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# generates a file to be used by the vegeta load testing tool
|
||||
import time
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import json
|
||||
import argparse
|
||||
import sys
|
||||
import random
|
||||
|
||||
SECRET = b"reallyreallyreallyreallyverysafe"
|
||||
URL = "http://postgrest"
|
||||
JWT_DURATION = 120
|
||||
TOTAL_TARGETS = 50000 # tuned by hand to reduce result variance
|
||||
|
||||
|
||||
def base64url_encode(data: bytes) -> str:
|
||||
"""URL-safe Base64 encode without padding."""
|
||||
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
||||
|
||||
|
||||
def generate_jwt(exp_inc: int) -> str:
|
||||
"""Generate an HS256 JWT"""
|
||||
# Header & payload
|
||||
header = {"alg": "HS256", "typ": "JWT"}
|
||||
now = int(time.time())
|
||||
payload = {
|
||||
"sub": f"user_{random.getrandbits(32)}",
|
||||
"iat": now,
|
||||
"exp": now + exp_inc,
|
||||
"role": "postgrest_test_author",
|
||||
}
|
||||
|
||||
# Encode to JSON and then to Base64URL
|
||||
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)
|
||||
|
||||
# Sign (HMAC‑SHA256) the "<header>.<payload>" string
|
||||
signing_input = f"{header_b64}.{payload_b64}".encode()
|
||||
signature = hmac.new(SECRET, signing_input, hashlib.sha256).digest()
|
||||
signature_b64 = base64url_encode(signature)
|
||||
|
||||
return f"{header_b64}.{payload_b64}.{signature_b64}"
|
||||
|
||||
|
||||
# We want to ensure 401 Unauthorized responses don't happen during
|
||||
# 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 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
|
||||
|
||||
return build_run_postgrest_time + file_generation_time + dynamic_exp_inc
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate Vegeta targets with unique JWTs"
|
||||
)
|
||||
parser.add_argument(
|
||||
"output",
|
||||
help="Path to write the generated targets file",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
lines = []
|
||||
start_time = time.time()
|
||||
|
||||
for i in range(TOTAL_TARGETS):
|
||||
token = generate_jwt(estimate_adequate_jwt_exp_increase(i))
|
||||
lines.append(f"OPTIONS {URL}/authors_only")
|
||||
lines.append(f"Authorization: Bearer {token}")
|
||||
lines.append("") # blank line to separate requests
|
||||
|
||||
try:
|
||||
with open(args.output, "w") as f:
|
||||
f.write("\n".join(lines))
|
||||
except IOError as e:
|
||||
print(f"Error writing to {args.output}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"Created {TOTAL_TARGETS} targets in {args.output} ({elapsed:.2f}s)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -41,6 +41,8 @@ let
|
||||
args = [
|
||||
"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([kind], [k], [Kind of loadtest (mixed: repeat mixed requests, jwt: run once over many requests with unique jwts)], [mixed])"
|
||||
"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,jwt])"
|
||||
"ARG_LEFTOVERS([additional vegeta arguments])"
|
||||
];
|
||||
workingDir = "/";
|
||||
@@ -55,17 +57,36 @@ let
|
||||
export PGRST_DB_POOL="1"
|
||||
export PGRST_DB_TX_END="rollback-allow-override"
|
||||
export PGRST_LOG_LEVEL="crit"
|
||||
export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe"
|
||||
export PGRST_JWT_CACHE_MAX_LIFETIME="86400"
|
||||
|
||||
mkdir -p "$(dirname "$_arg_output")"
|
||||
abs_output="$(realpath "$_arg_output")"
|
||||
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withSlowPg} \
|
||||
${withTools.withPgrst} \
|
||||
${withTools.withSlowPgrst} \
|
||||
sh -c "cd \"$_arg_testdir\" && ${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
||||
case "$_arg_kind" in
|
||||
jwt)
|
||||
|
||||
${genTargets} "$_arg_testdir"/gen_targets.http
|
||||
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} \
|
||||
sh -c "cd \"$_arg_testdir\" && ${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
||||
;;
|
||||
|
||||
*)
|
||||
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withSlowPg} \
|
||||
${withTools.withPgrst} \
|
||||
${withTools.withSlowPgrst} \
|
||||
sh -c "cd \"$_arg_testdir\" && ${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
||||
;;
|
||||
|
||||
esac
|
||||
'';
|
||||
|
||||
loadtestAgainst =
|
||||
@@ -83,6 +104,7 @@ let
|
||||
'';
|
||||
args = [
|
||||
"ARG_POSITIONAL_INF([target], [Commit-ish reference to compare with], 1)"
|
||||
"ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest], [mixed])"
|
||||
];
|
||||
positionalCompletion =
|
||||
''
|
||||
@@ -106,7 +128,7 @@ let
|
||||
# Save the results in the current working tree, too,
|
||||
# otherwise they'd be lost in the temporary working tree
|
||||
# created by withTools.withGit.
|
||||
${withTools.withGit} "$tgt" ${loadtest} --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
|
||||
|
||||
@@ -122,7 +144,7 @@ let
|
||||
|
||||
EOF
|
||||
|
||||
${loadtest} --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load"
|
||||
${loadtest} -k "$_arg_kind" --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load"
|
||||
|
||||
cat << EOF
|
||||
|
||||
@@ -178,6 +200,7 @@ let
|
||||
| ${toMarkdown}
|
||||
'';
|
||||
|
||||
genTargets = writers.writePython3 "postgrest-gen-loadtest-targets" { } (builtins.readFile ./generate_targets.py);
|
||||
in
|
||||
buildToolbox {
|
||||
name = "postgrest-loadtest";
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
# The memory tests have large dependencies (a profiled build of PostgREST)
|
||||
# and are run less often than the spec tests, so we don't include them in
|
||||
# the default test environment. We make them available through a separate module.
|
||||
# TODO both of these require reentering the nix-shell if you make a change to the code
|
||||
{ buildToolbox
|
||||
, checkedShellScript
|
||||
, curl
|
||||
, postgrestProfiled
|
||||
, withTools
|
||||
}:
|
||||
let
|
||||
test =
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-test-memory";
|
||||
docs = "Run the memory tests.";
|
||||
workingDir = "/";
|
||||
withPath = [ postgrestProfiled curl ];
|
||||
}
|
||||
''
|
||||
${withTools.withPg} -f test/spec/fixtures/load.sql test/memory/memory-tests.sh
|
||||
'';
|
||||
|
||||
runProfiled =
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-profiled-run";
|
||||
docs = "Run a profiled build of postgREST. This will generate a postgrest.prof file that can be used to do optimization. Note: if you make a change to the code, you must reenter the nix-shell for an updated profiled build.";
|
||||
args =
|
||||
[
|
||||
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [PostgREST anonymous role])"
|
||||
"ARG_USE_ENV([PGRST_DB_POOL], [1], [PostgREST pool size])"
|
||||
"ARG_USE_ENV([PGRST_DB_POOL_ACQUISITION_TIMEOUT], [1], [PostgREST pool timeout])"
|
||||
"ARG_LEFTOVERS([PostgREST arguments])"
|
||||
];
|
||||
workingDir = "/";
|
||||
withPath = [ postgrestProfiled ];
|
||||
}
|
||||
''
|
||||
export PGRST_DB_ANON_ROLE
|
||||
export PGRST_DB_POOL
|
||||
export PGRST_DB_POOL_ACQUISITION_TIMEOUT
|
||||
|
||||
postgrest +RTS -p -h -RTS "''${_arg_leftovers[@]}"
|
||||
'';
|
||||
in
|
||||
buildToolbox
|
||||
{
|
||||
name = "postgrest-memory";
|
||||
tools = { inherit test runProfiled; };
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
{ buildToolbox
|
||||
, checkedShellScript
|
||||
, coreutils
|
||||
, curl
|
||||
, jq
|
||||
, nix
|
||||
}:
|
||||
# Utility script for pinning the latest stable version of Nixpkgs.
|
||||
|
||||
# Instead of pinning Nixpkgs based on the huge Git repository, we reference a
|
||||
# specific tarball that only contains the source of the revision that we want
|
||||
# to pin.
|
||||
let
|
||||
name =
|
||||
"postgrest-nixpkgs-upgrade";
|
||||
|
||||
refUrl =
|
||||
"https://api.github.com/repos/nixos/nixpkgs/git/matching-refs/heads/nixpkgs-";
|
||||
|
||||
githubV3Header =
|
||||
"Accept: application/vnd.github.v3+json";
|
||||
|
||||
tarballUrlBase =
|
||||
"https://github.com/nixos/nixpkgs/archive/";
|
||||
|
||||
upgrade =
|
||||
checkedShellScript
|
||||
{
|
||||
inherit name;
|
||||
docs = "Pin the newest stable version of Nixpkgs.";
|
||||
workingDir = "/";
|
||||
}
|
||||
''
|
||||
# The list of refs is sorted. The first result will be nixpkgs-unstable, the second the latest stable branch.
|
||||
commitHash="$(${curl}/bin/curl "${refUrl}" -H "${githubV3Header}" | ${jq}/bin/jq -r 'sort_by(.ref) | reverse | .[1].object.sha')"
|
||||
tarballUrl="${tarballUrlBase}$commitHash.tar.gz"
|
||||
tarballHash="$(${nix}/bin/nix-prefetch-url --unpack "$tarballUrl")"
|
||||
currentDate="$(${coreutils}/bin/date --iso)"
|
||||
|
||||
cat > nix/nixpkgs-version.nix << EOF
|
||||
# Pinned version of Nixpkgs, generated with ${name}.
|
||||
{
|
||||
owner = "NixOS";
|
||||
repo = "nixpkgs";
|
||||
date = "$currentDate";
|
||||
rev = "$commitHash";
|
||||
tarballHash = "$tarballHash";
|
||||
}
|
||||
EOF
|
||||
'';
|
||||
|
||||
in
|
||||
buildToolbox
|
||||
{
|
||||
name = "postgrest-nixpkgs";
|
||||
tools = { inherit upgrade; };
|
||||
}
|
||||
@@ -29,22 +29,28 @@ let
|
||||
case "$1" in
|
||||
major)
|
||||
new_version="$((major+1)).0.0"
|
||||
new_docs_version="$((major+1)).0"
|
||||
;;
|
||||
minor)
|
||||
new_version="$major.$((minor+1)).0"
|
||||
new_docs_version="$major.$((minor+1))"
|
||||
;;
|
||||
patch)
|
||||
new_version="$major.$minor.$((patch+1))"
|
||||
new_docs_version="$major.$minor"
|
||||
;;
|
||||
devel)
|
||||
new_version="$major.$((minor+1))"
|
||||
new_docs_version="devel"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Updating postgrest.cabal ..."
|
||||
sed -i -E "s/^(version:\s+).*$/\1$new_version/" postgrest.cabal > /dev/null
|
||||
echo "Updating docs/conf.py ..."
|
||||
sed -i -E "s/^(version = ).*$/\1\"$new_docs_version\"/" docs/conf.py > /dev/null
|
||||
|
||||
git add postgrest.cabal > /dev/null
|
||||
git add postgrest.cabal docs/conf.py > /dev/null
|
||||
}
|
||||
|
||||
today_date_for_changelog="$(date '+%Y-%m-%d')"
|
||||
@@ -1,6 +1,7 @@
|
||||
{ buildToolbox
|
||||
, cabal-install
|
||||
, checkedShellScript
|
||||
, curl
|
||||
, devCabalOptions
|
||||
, ghc
|
||||
, glibcLocales ? null
|
||||
@@ -27,6 +28,7 @@ let
|
||||
withEnv = postgrest.env;
|
||||
}
|
||||
''
|
||||
${cabal-install}/bin/cabal v2-update
|
||||
${withTools.withPg} -f test/spec/fixtures/load.sql \
|
||||
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec -- "''${_arg_leftovers[@]}"
|
||||
'';
|
||||
@@ -40,6 +42,7 @@ let
|
||||
withEnv = postgrest.env;
|
||||
}
|
||||
''
|
||||
${cabal-install}/bin/cabal v2-update
|
||||
# This makes nix-env -iA tests.doctests.bin work.
|
||||
export NIX_GHC=${postgrest.env.NIX_GHC}
|
||||
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:doctests
|
||||
@@ -54,6 +57,7 @@ let
|
||||
withEnv = postgrest.env;
|
||||
}
|
||||
''
|
||||
${cabal-install}/bin/cabal v2-update
|
||||
${withTools.withPg} -f test/spec/fixtures/load.sql \
|
||||
${runtimeShell} -c " \
|
||||
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec && \
|
||||
@@ -81,7 +85,8 @@ let
|
||||
withEnv = postgrest.env;
|
||||
}
|
||||
''
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions}
|
||||
${cabal-install}/bin/cabal v2-update
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
||||
${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[@]}"
|
||||
'';
|
||||
@@ -96,7 +101,8 @@ let
|
||||
withEnv = postgrest.env;
|
||||
}
|
||||
''
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions}
|
||||
${cabal-install}/bin/cabal v2-update
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
||||
${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[@]}"
|
||||
'';
|
||||
@@ -111,7 +117,8 @@ let
|
||||
withEnv = postgrest.env;
|
||||
}
|
||||
''
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions}
|
||||
${cabal-install}/bin/cabal v2-update
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
||||
${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[@]}"
|
||||
'';
|
||||
@@ -126,6 +133,7 @@ let
|
||||
withPath = [ jq ];
|
||||
}
|
||||
''
|
||||
${cabal-install}/bin/cabal v2-update
|
||||
${withTools.withPg} -f test/spec/fixtures/load.sql \
|
||||
${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
|
||||
postgrest --dump-schema
|
||||
@@ -154,6 +162,7 @@ let
|
||||
rm -rf coverage/*
|
||||
|
||||
# build once before running all the tests
|
||||
${cabal-install}/bin/cabal v2-update
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec
|
||||
|
||||
(
|
||||
@@ -227,6 +236,22 @@ let
|
||||
sed -i 's|^module \(.*\):|module \1/|g' test/coverage.overlay
|
||||
'';
|
||||
|
||||
testMemory =
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-test-memory";
|
||||
docs = "Run the memory tests.";
|
||||
workingDir = "/";
|
||||
withEnv = postgrest.env;
|
||||
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-exec -- ${withTools.withPg} -f test/spec/fixtures/load.sql \
|
||||
test/memory/memory-tests.sh
|
||||
'';
|
||||
|
||||
in
|
||||
buildToolbox
|
||||
{
|
||||
@@ -241,6 +266,7 @@ buildToolbox
|
||||
testReplica
|
||||
dumpSchema
|
||||
coverage
|
||||
coverageDraftOverlay;
|
||||
coverageDraftOverlay
|
||||
testMemory;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: postgrest
|
||||
version: 12.2.12
|
||||
version: 13.0.8
|
||||
synopsis: REST API for any Postgres database
|
||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||
for tables, views, and functions, supporting all HTTP methods that security
|
||||
@@ -16,14 +16,13 @@ extra-source-files: CHANGELOG.md
|
||||
cabal-version: >= 1.10
|
||||
|
||||
tested-with:
|
||||
-- nix
|
||||
GHC == 9.4.8
|
||||
-- cabal on Ubuntu
|
||||
-- stack on FreeBSD, MacOS, Ubuntu, Windows
|
||||
GHC == 9.4.5
|
||||
-- nix, cabal on Ubuntu (arm)
|
||||
, GHC == 9.4.8
|
||||
, GHC == 9.6.7
|
||||
-- cabal on Ubuntu
|
||||
, GHC == 9.6.4
|
||||
-- cabal on Ubuntu
|
||||
, GHC == 9.8.2
|
||||
, GHC == 9.8.4
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
@@ -48,6 +47,8 @@ library
|
||||
PostgREST.App
|
||||
PostgREST.AppState
|
||||
PostgREST.Auth
|
||||
PostgREST.Auth.JwtCache
|
||||
PostgREST.Auth.Types
|
||||
PostgREST.CLI
|
||||
PostgREST.Config
|
||||
PostgREST.Config.Database
|
||||
@@ -66,6 +67,7 @@ library
|
||||
PostgREST.Logger
|
||||
PostgREST.MediaType
|
||||
PostgREST.Metrics
|
||||
PostgREST.Network
|
||||
PostgREST.Observation
|
||||
PostgREST.Query
|
||||
PostgREST.Query.QueryBuilder
|
||||
@@ -87,44 +89,43 @@ library
|
||||
PostgREST.Response.GucHeader
|
||||
PostgREST.Response.Performance
|
||||
PostgREST.Version
|
||||
other-modules: Paths_postgrest
|
||||
build-depends: base >= 4.9 && < 4.20
|
||||
, HTTP >= 4000.3.7 && < 4000.5
|
||||
, Ranged-sets >= 0.3 && < 0.5
|
||||
, aeson >= 2.0.3 && < 2.3
|
||||
, auto-update >= 0.1.4 && < 0.2
|
||||
, auto-update >= 0.1.4 && < 0.3
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
, bytestring >= 0.10.8 && < 0.13
|
||||
, cache >= 0.1.3 && < 0.2.0
|
||||
, case-insensitive >= 1.2 && < 1.3
|
||||
, cassava >= 0.4.5 && < 0.6
|
||||
, clock >= 0.8.3 && < 0.9.0
|
||||
, configurator-pg >= 0.2 && < 0.3
|
||||
, configurator-pg >= 0.2.11 && < 0.3
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
, contravariant-extras >= 0.3.3 && < 0.4
|
||||
, cookie >= 0.4.2 && < 0.5
|
||||
, cookie >= 0.4.2 && < 0.6
|
||||
, directory >= 1.2.6 && < 1.4
|
||||
, either >= 4.4.1 && < 5.1
|
||||
, extra >= 1.7.0 && < 2.0
|
||||
, fuzzyset >= 0.2.4 && < 0.3
|
||||
, gitrev >= 1.2 && < 1.4
|
||||
, hasql >= 1.6.1.1 && < 1.7
|
||||
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||
, hasql-notifications >= 0.2.2.2 && < 0.2.3
|
||||
, hasql-pool >= 1.0.1 && < 1.1
|
||||
, hasql-transaction >= 1.0.1 && < 1.1
|
||||
, hasql-transaction >= 1.0.1 && < 1.2
|
||||
, heredoc >= 0.2 && < 0.3
|
||||
, http-types >= 0.12.2 && < 0.13
|
||||
, insert-ordered-containers >= 0.2.2 && < 0.3
|
||||
, interpolatedstring-perl6 >= 1 && < 1.1
|
||||
, jose >= 0.8.5.1 && < 0.12
|
||||
, lens >= 4.14 && < 5.3
|
||||
, iproute >= 1.7.0 && < 1.8
|
||||
, jose-jwt >= 0.9.6 && < 0.11
|
||||
, lens >= 4.14 && < 5.4
|
||||
, lens-aeson >= 1.0.1 && < 1.3
|
||||
, mtl >= 2.2.2 && < 2.4
|
||||
, network >= 2.6 && < 3.2
|
||||
, neat-interpolation >= 0.5 && < 0.6
|
||||
, network >= 2.6 && < 3.3
|
||||
, network-uri >= 2.6.1 && < 2.8
|
||||
, optparse-applicative >= 0.13 && < 0.19
|
||||
, parsec >= 3.1.11 && < 3.2
|
||||
-- Technically unused, can be removed after updating to hasql >= 1.7
|
||||
, postgresql-libpq >= 0.10
|
||||
, prometheus-client >= 1.1.1 && < 1.2.0
|
||||
, protolude >= 0.3.1 && < 0.4
|
||||
@@ -148,7 +149,7 @@ library
|
||||
-- for unix sockets; this is tested in test/io/test_io.py. See
|
||||
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
|
||||
, wai-logger >= 2.4.0
|
||||
, warp >= 3.3.19 && < 3.4
|
||||
, warp >= 3.3.19 && < 3.5
|
||||
-- -fno-spec-constr may help keep compile time memory use in check,
|
||||
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
||||
-- -optP-Wno-nonportable-include-path
|
||||
@@ -204,7 +205,7 @@ test-suite spec
|
||||
Feature.Auth.AuthSpec
|
||||
Feature.Auth.BinaryJwtSecretSpec
|
||||
Feature.Auth.NoAnonSpec
|
||||
Feature.Auth.NoJwtSpec
|
||||
Feature.Auth.NoJwtSecretSpec
|
||||
Feature.ConcurrentSpec
|
||||
Feature.CorsSpec
|
||||
Feature.ExtraSearchPathSpec
|
||||
@@ -227,7 +228,6 @@ test-suite spec
|
||||
Feature.Query.ErrorSpec
|
||||
Feature.Query.InsertSpec
|
||||
Feature.Query.JsonOperatorSpec
|
||||
Feature.Query.LimitedMutationSpec
|
||||
Feature.Query.MultipleSchemaSpec
|
||||
Feature.Query.NullsStripSpec
|
||||
Feature.Query.PgSafeUpdateSpec
|
||||
@@ -253,19 +253,19 @@ test-suite spec
|
||||
, aeson >= 2.0.3 && < 2.3
|
||||
, aeson-qq >= 0.8.1 && < 0.9
|
||||
, async >= 2.1.1 && < 2.3
|
||||
, auto-update >= 0.1.4 && < 0.2
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
, bytestring >= 0.10.8 && < 0.13
|
||||
, case-insensitive >= 1.2 && < 1.3
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
, hasql-pool >= 1.0.1 && < 1.1
|
||||
, hasql-transaction >= 1.0.1 && < 1.1
|
||||
, hasql-transaction >= 1.0.1 && < 1.2
|
||||
, heredoc >= 0.2 && < 0.3
|
||||
, hspec >= 2.3 && < 2.12
|
||||
, hspec-wai >= 0.10 && < 0.12
|
||||
, hspec-wai-json >= 0.10 && < 0.12
|
||||
, http-types >= 0.12.3 && < 0.13
|
||||
, lens >= 4.14 && < 5.3
|
||||
, jose-jwt >= 0.9.6 && < 0.11
|
||||
, lens >= 4.14 && < 5.4
|
||||
, lens-aeson >= 1.0.1 && < 1.3
|
||||
, monad-control >= 1.0.1 && < 1.1
|
||||
, postgrest
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
# We highly recommend that use the PostgREST binary cache by installing cachix
|
||||
# (https://app.cachix.org/) and running `cachix use postgrest`.
|
||||
{ docker ? false
|
||||
, memory ? false
|
||||
}:
|
||||
let
|
||||
postgrest =
|
||||
@@ -23,14 +22,12 @@ let
|
||||
postgrest.devTools
|
||||
postgrest.docs
|
||||
postgrest.loadtest
|
||||
postgrest.nixpkgsTools
|
||||
postgrest.release
|
||||
postgrest.style
|
||||
postgrest.tests
|
||||
postgrest.withTools
|
||||
postgrest.release
|
||||
]
|
||||
++ lib.optional docker postgrest.docker
|
||||
++ lib.optional memory postgrest.memory;
|
||||
++ lib.optional docker postgrest.docker;
|
||||
|
||||
in
|
||||
lib.overrideDerivation postgrest.env (
|
||||
|
||||
@@ -11,19 +11,17 @@ import qualified Network.Wai.Handler.Warp as Warp
|
||||
|
||||
import Control.Monad.Extra (whenJust)
|
||||
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
|
||||
import Network.Socket
|
||||
import Network.Socket.ByteString
|
||||
|
||||
import PostgREST.AppState (AppState)
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.MediaType (MediaType (..), toContentType)
|
||||
import PostgREST.Metrics (metricsToText)
|
||||
import PostgREST.Network (resolveHost)
|
||||
import PostgREST.Observation (Observation (..))
|
||||
|
||||
import qualified PostgREST.AppState as AppState
|
||||
import qualified PostgREST.Config as Config
|
||||
|
||||
|
||||
import Protolude
|
||||
|
||||
@@ -31,7 +29,8 @@ runAdmin :: AppState -> Warp.Settings -> IO ()
|
||||
runAdmin appState settings = do
|
||||
AppConfig{configAdminServerPort} <- AppState.getConfig appState
|
||||
whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do
|
||||
observer $ AdminStartObs configAdminServerPort
|
||||
host <- resolveHost adminSocket
|
||||
observer $ AdminStartObs host configAdminServerPort
|
||||
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
|
||||
where
|
||||
adminApp = admin appState
|
||||
@@ -55,18 +54,12 @@ admin appState req respond = do
|
||||
| otherwise = HTTP.status500
|
||||
in
|
||||
respond $ Wai.responseLBS status [] mempty
|
||||
["config"] -> do
|
||||
config@Config.AppConfig{configAdminServerConfigEnabled} <- AppState.getConfig appState
|
||||
if configAdminServerConfigEnabled then
|
||||
respond $ Wai.responseLBS HTTP.status200 [] (LBS.fromStrict $ encodeUtf8 $ Config.toText config)
|
||||
else
|
||||
respond $ Wai.responseLBS HTTP.status404 [] mempty
|
||||
["schema_cache"] -> do
|
||||
sCache <- AppState.getSchemaCache appState
|
||||
respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache)
|
||||
["metrics"] -> do
|
||||
mets <- metricsToText
|
||||
respond $ Wai.responseLBS HTTP.status200 [] mets
|
||||
respond $ Wai.responseLBS HTTP.status200 [toContentType MTTextPlain] mets -- Content-Type is required for prometheus compliance
|
||||
_ ->
|
||||
respond $ Wai.responseLBS HTTP.status404 [] mempty
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ Description : PostgREST functions to translate HTTP request to a domain type cal
|
||||
-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
-- TODO: This module shouldn't depend on SchemaCache
|
||||
module PostgREST.ApiRequest
|
||||
( ApiRequest(..)
|
||||
, InvokeMethod(..)
|
||||
@@ -14,6 +13,7 @@ module PostgREST.ApiRequest
|
||||
, DbAction(..)
|
||||
, Payload(..)
|
||||
, userApiRequest
|
||||
, userPreferences
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
@@ -44,16 +44,16 @@ import Network.Wai.Parse (parseHttpAccept)
|
||||
import Web.Cookie (parseCookies)
|
||||
|
||||
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
|
||||
import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
||||
RangeError (..))
|
||||
import PostgREST.Config (AppConfig (..),
|
||||
OpenAPIMode (..))
|
||||
import PostgREST.Config.Database (TimezoneNames)
|
||||
import PostgREST.Error (ApiRequestError (..),
|
||||
RangeError (..))
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
convertToLimitZeroRange,
|
||||
hasLimitZero,
|
||||
rangeRequested)
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier (..),
|
||||
Schema)
|
||||
@@ -128,8 +128,8 @@ data ApiRequest = ApiRequest {
|
||||
}
|
||||
|
||||
-- | Examines HTTP request and translates it into user intent.
|
||||
userApiRequest :: AppConfig -> Request -> RequestBody -> SchemaCache -> Either ApiRequestError ApiRequest
|
||||
userApiRequest conf req reqBody sCache = do
|
||||
userApiRequest :: AppConfig -> Preferences.Preferences -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
||||
userApiRequest conf prefs req reqBody = do
|
||||
resource <- getResource conf $ pathInfo req
|
||||
(schema, negotiatedByProfile) <- getSchema conf hdrs method
|
||||
act <- getAction resource schema method
|
||||
@@ -141,7 +141,7 @@ userApiRequest conf req reqBody sCache = do
|
||||
, iRange = ranges
|
||||
, iTopLevelRange = topLevelRange
|
||||
, iPayload = payload
|
||||
, iPreferences = Preferences.fromHeaders (configDbTxAllowOverride conf) (dbTimezones sCache) hdrs
|
||||
, iPreferences = prefs
|
||||
, iQueryParams = qPrms
|
||||
, iColumns = columns
|
||||
, iHeaders = iHdrs
|
||||
@@ -162,15 +162,21 @@ userApiRequest conf req reqBody sCache = do
|
||||
contentMediaType = maybe MTApplicationJSON MediaType.decodeMediaType $ lookupHeader "content-type"
|
||||
actIsInvokeSafe x = case x of {ActDb (ActRoutine _ (InvRead _)) -> True; _ -> False}
|
||||
|
||||
-- | Parses the Prefer header
|
||||
userPreferences :: AppConfig -> Request -> TimezoneNames -> Preferences.Preferences
|
||||
userPreferences conf req timezones = Preferences.fromHeaders (configDbTxAllowOverride conf) timezones $ requestHeaders req
|
||||
|
||||
getResource :: AppConfig -> [Text] -> Either ApiRequestError Resource
|
||||
getResource AppConfig{configOpenApiMode, configDbRootSpec} = \case
|
||||
[] -> case configDbRootSpec of
|
||||
Just (QualifiedIdentifier _ pathName) -> Right $ ResourceRoutine pathName
|
||||
Nothing | configOpenApiMode == OADisabled -> Left NotFound
|
||||
| otherwise -> Right ResourceSchema
|
||||
[] ->
|
||||
case (configOpenApiMode,configDbRootSpec) of
|
||||
(OADisabled,_) -> Left OpenAPIDisabled
|
||||
(_, Just qi) -> Right $ ResourceRoutine (qiName qi)
|
||||
(_, Nothing) -> Right ResourceSchema
|
||||
|
||||
[table] -> Right $ ResourceRelation table
|
||||
["rpc", pName] -> Right $ ResourceRoutine pName
|
||||
_ -> Left NotFound
|
||||
_ -> Left InvalidResourcePath
|
||||
|
||||
getAction :: Resource -> Schema -> ByteString -> Either ApiRequestError Action
|
||||
getAction resource schema method =
|
||||
@@ -201,7 +207,7 @@ getAction resource schema method =
|
||||
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
|
||||
getSchema AppConfig{configDbSchemas} hdrs method = do
|
||||
case profile of
|
||||
Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema $ toList configDbSchemas
|
||||
Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema p $ toList configDbSchemas
|
||||
| otherwise -> Right (p, True)
|
||||
Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated
|
||||
where
|
||||
@@ -218,9 +224,8 @@ getSchema AppConfig{configDbSchemas} hdrs method = do
|
||||
lookupHeader = flip lookup hdrs
|
||||
|
||||
getRanges :: ByteString -> QueryParams -> RequestHeaders -> Either ApiRequestError (NonnegRange, HM.HashMap Text NonnegRange)
|
||||
getRanges method QueryParams{qsOrder,qsRanges} hdrs
|
||||
getRanges method QueryParams{qsRanges} hdrs
|
||||
| isInvalidRange = Left $ InvalidRange (if rangeIsEmpty headerRange then LowerGTUpper else NegativeLimit)
|
||||
| method `elem` ["PATCH", "DELETE"] && not (null qsRanges) && null qsOrder = Left LimitNoOrderError
|
||||
| method == "PUT" && topLevelRange /= allRange = Left PutLimitNotAllowedError
|
||||
| otherwise = Right (topLevelRange, ranges)
|
||||
where
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
-- Module: PostgREST.ApiRequest.Preferences
|
||||
-- Description: Track client preferences to be employed when processing requests
|
||||
--
|
||||
-- Track client prefences set in HTTP 'Prefer' headers according to RFC7240[1].
|
||||
-- Track client preferences set in HTTP 'Prefer' headers according to RFC7240[1].
|
||||
--
|
||||
-- [1] https://datatracker.ietf.org/doc/html/rfc7240
|
||||
--
|
||||
@@ -12,7 +12,6 @@ module PostgREST.ApiRequest.Preferences
|
||||
, PreferCount(..)
|
||||
, PreferHandling(..)
|
||||
, PreferMissing(..)
|
||||
, PreferParameters(..)
|
||||
, PreferRepresentation(..)
|
||||
, PreferResolution(..)
|
||||
, PreferTransaction(..)
|
||||
@@ -37,7 +36,6 @@ import Protolude
|
||||
-- >>> import Text.Pretty.Simple (pPrint)
|
||||
-- >>> deriving instance Show PreferResolution
|
||||
-- >>> deriving instance Show PreferRepresentation
|
||||
-- >>> deriving instance Show PreferParameters
|
||||
-- >>> deriving instance Show PreferCount
|
||||
-- >>> deriving instance Show PreferTransaction
|
||||
-- >>> deriving instance Show PreferMissing
|
||||
@@ -51,7 +49,6 @@ data Preferences
|
||||
= Preferences
|
||||
{ preferResolution :: Maybe PreferResolution
|
||||
, preferRepresentation :: Maybe PreferRepresentation
|
||||
, preferParameters :: Maybe PreferParameters
|
||||
, preferCount :: Maybe PreferCount
|
||||
, preferTransaction :: Maybe PreferTransaction
|
||||
, preferMissing :: Maybe PreferMissing
|
||||
@@ -71,7 +68,6 @@ data Preferences
|
||||
-- Preferences
|
||||
-- { preferResolution = Just IgnoreDuplicates
|
||||
-- , preferRepresentation = Nothing
|
||||
-- , preferParameters = Nothing
|
||||
-- , preferCount = Just ExactCount
|
||||
-- , preferTransaction = Nothing
|
||||
-- , preferMissing = Nothing
|
||||
@@ -89,7 +85,6 @@ data Preferences
|
||||
-- Preferences
|
||||
-- { preferResolution = Just IgnoreDuplicates
|
||||
-- , preferRepresentation = Nothing
|
||||
-- , preferParameters = Nothing
|
||||
-- , preferCount = Just ExactCount
|
||||
-- , preferTransaction = Nothing
|
||||
-- , preferMissing = Just ApplyNulls
|
||||
@@ -122,7 +117,6 @@ data Preferences
|
||||
-- Preferences
|
||||
-- { preferResolution = Nothing
|
||||
-- , preferRepresentation = Just Full
|
||||
-- , preferParameters = Nothing
|
||||
-- , preferCount = Just ExactCount
|
||||
-- , preferTransaction = Just Commit
|
||||
-- , preferMissing = Just ApplyDefaults
|
||||
@@ -137,7 +131,6 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
|
||||
Preferences
|
||||
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
|
||||
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
|
||||
, preferParameters = parsePrefs [SingleObject]
|
||||
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
||||
, preferTransaction = if allowTxDbOverride then parsePrefs [Commit, Rollback] else Nothing
|
||||
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
|
||||
@@ -151,7 +144,6 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
|
||||
mapToHeadVal = map toHeaderValue
|
||||
acceptedPrefs = mapToHeadVal [MergeDuplicates, IgnoreDuplicates] ++
|
||||
mapToHeadVal [Full, None, HeadersOnly] ++
|
||||
mapToHeadVal [SingleObject] ++
|
||||
mapToHeadVal [ExactCount, PlannedCount, EstimatedCount] ++
|
||||
mapToHeadVal [Commit, Rollback] ++
|
||||
mapToHeadVal [ApplyDefaults, ApplyNulls] ++
|
||||
@@ -179,7 +171,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
|
||||
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
|
||||
|
||||
prefAppliedHeader :: Preferences -> Maybe HTTP.Header
|
||||
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone, preferMaxAffected } =
|
||||
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone, preferMaxAffected } =
|
||||
if null prefsVals
|
||||
then Nothing
|
||||
else Just (HTTP.hPreferenceApplied, combined)
|
||||
@@ -189,7 +181,6 @@ prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferPar
|
||||
toHeaderValue <$> preferResolution
|
||||
, toHeaderValue <$> preferMissing
|
||||
, toHeaderValue <$> preferRepresentation
|
||||
, toHeaderValue <$> preferParameters
|
||||
, toHeaderValue <$> preferCount
|
||||
, toHeaderValue <$> preferTransaction
|
||||
, toHeaderValue <$> preferHandling
|
||||
@@ -231,15 +222,6 @@ instance ToHeaderValue PreferRepresentation where
|
||||
toHeaderValue None = "return=minimal"
|
||||
toHeaderValue HeadersOnly = "return=headers-only"
|
||||
|
||||
-- | How to pass parameters to stored procedures.
|
||||
-- TODO: deprecated. Remove on next major version.
|
||||
data PreferParameters
|
||||
= SingleObject -- ^ Pass all parameters as a single json object to a stored procedure.
|
||||
deriving Eq
|
||||
|
||||
instance ToHeaderValue PreferParameters where
|
||||
toHeaderValue SingleObject = "params=single-object"
|
||||
|
||||
-- | How to determine the count of (expected) results
|
||||
data PreferCount
|
||||
= ExactCount -- ^ Exact count (slower).
|
||||
|
||||
@@ -46,7 +46,7 @@ import PostgREST.SchemaCache.Identifiers (FieldName)
|
||||
import PostgREST.ApiRequest.Types (AggregateFunction (..),
|
||||
EmbedParam (..), EmbedPath, Field,
|
||||
Filter (..), FtsOperator (..),
|
||||
Hint, JoinType (..),
|
||||
Hint, IsVal (..), JoinType (..),
|
||||
JsonOperand (..),
|
||||
JsonOperation (..), JsonPath,
|
||||
ListVal, LogicOperator (..),
|
||||
@@ -54,10 +54,11 @@ import PostgREST.ApiRequest.Types (AggregateFunction (..),
|
||||
OpQuantifier (..), Operation (..),
|
||||
OrderDirection (..),
|
||||
OrderNulls (..), OrderTerm (..),
|
||||
QPError (..), QuantOperator (..),
|
||||
QuantOperator (..),
|
||||
SelectItem (..),
|
||||
SimpleOperator (..), SingleVal,
|
||||
TrileanVal (..))
|
||||
SimpleOperator (..), SingleVal)
|
||||
|
||||
import PostgREST.Error (QPError (..))
|
||||
|
||||
import Protolude hiding (Sum, try)
|
||||
|
||||
@@ -640,7 +641,7 @@ pOpExpr pSVal = do
|
||||
pOperation = pIn <|> pIs <|> pIsDist <|> try pFts <|> try pSimpleOp <|> try pQuantOp <?> "operator (eq, gt, ...)"
|
||||
|
||||
pIn = In <$> (try (string "in" *> pDelimiter) *> pListVal)
|
||||
pIs = Is <$> (try (string "is" *> pDelimiter) *> pTriVal)
|
||||
pIs = Is <$> (try (string "is" *> pDelimiter) *> pIsVal)
|
||||
|
||||
pIsDist = IsDistinctFrom <$> (try (string "isdistinct" *> pDelimiter) *> pSVal)
|
||||
|
||||
@@ -653,11 +654,12 @@ pOpExpr pSVal = do
|
||||
quant <- optionMaybe $ try (between (char '(') (char ')') (try (string "any" $> QuantAny) <|> string "all" $> QuantAll))
|
||||
pDelimiter *> (OpQuant op quant <$> pSVal)
|
||||
|
||||
pTriVal = try (ciString "null" $> TriNull)
|
||||
<|> try (ciString "unknown" $> TriUnknown)
|
||||
<|> try (ciString "true" $> TriTrue)
|
||||
<|> try (ciString "false" $> TriFalse)
|
||||
<?> "null or trilean value (unknown, true, false)"
|
||||
pIsVal = try (ciString "null" $> IsNull)
|
||||
<|> try (ciString "not_null" $> IsNotNull)
|
||||
<|> try (ciString "true" $> IsTriTrue)
|
||||
<|> try (ciString "false" $> IsTriFalse)
|
||||
<|> try (ciString "unknown" $> IsTriUnknown)
|
||||
<?> "isVal: (null, not_null, true, false, unknown)"
|
||||
|
||||
pFts = do
|
||||
op <- try (string "fts" $> FilterFts)
|
||||
|
||||
@@ -5,7 +5,6 @@ module PostgREST.ApiRequest.Types
|
||||
, Cast
|
||||
, Depth
|
||||
, EmbedParam(..)
|
||||
, ApiRequestError(..)
|
||||
, EmbedPath
|
||||
, Field
|
||||
, Filter(..)
|
||||
@@ -14,6 +13,7 @@ module PostgREST.ApiRequest.Types
|
||||
, JsonOperand(..)
|
||||
, JsonOperation(..)
|
||||
, JsonPath
|
||||
, Language
|
||||
, ListVal
|
||||
, LogicOperator(..)
|
||||
, LogicTree(..)
|
||||
@@ -24,23 +24,15 @@ module PostgREST.ApiRequest.Types
|
||||
, OrderDirection(..)
|
||||
, OrderNulls(..)
|
||||
, OrderTerm(..)
|
||||
, QPError(..)
|
||||
, RaiseError(..)
|
||||
, RangeError(..)
|
||||
, SingleVal
|
||||
, TrileanVal(..)
|
||||
, IsVal(..)
|
||||
, SimpleOperator(..)
|
||||
, QuantOperator(..)
|
||||
, FtsOperator(..)
|
||||
, SelectItem(..)
|
||||
) where
|
||||
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier)
|
||||
import PostgREST.SchemaCache.Relationship (Relationship,
|
||||
RelationshipsMap)
|
||||
import PostgREST.SchemaCache.Routine (Routine (..))
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName)
|
||||
|
||||
import Protolude
|
||||
|
||||
@@ -68,51 +60,6 @@ data SelectItem
|
||||
}
|
||||
deriving (Eq, Show)
|
||||
|
||||
data ApiRequestError
|
||||
= AggregatesNotAllowed
|
||||
| AmbiguousRelBetween Text Text [Relationship]
|
||||
| AmbiguousRpc [Routine]
|
||||
| MediaTypeError [ByteString]
|
||||
| InvalidBody ByteString
|
||||
| InvalidFilters
|
||||
| InvalidPreferences [ByteString]
|
||||
| InvalidRange RangeError
|
||||
| InvalidRpcMethod ByteString
|
||||
| LimitNoOrderError
|
||||
| NotFound
|
||||
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||
| NoRpc Text Text [Text] Bool MediaType Bool [QualifiedIdentifier] [Routine]
|
||||
| NotEmbedded Text
|
||||
| PutLimitNotAllowedError
|
||||
| QueryParamError QPError
|
||||
| RelatedOrderNotToOne Text Text
|
||||
| SpreadNotToOne Text Text
|
||||
| UnacceptableFilter Text
|
||||
| UnacceptableSchema [Text]
|
||||
| UnsupportedMethod ByteString
|
||||
| ColumnNotFound Text Text
|
||||
| GucHeadersError
|
||||
| GucStatusError
|
||||
| OffLimitsChangesError Int64 Integer
|
||||
| PutMatchingPkError
|
||||
| SingularityError Integer
|
||||
| PGRSTParseError RaiseError
|
||||
| MaxAffectedViolationError Integer
|
||||
deriving Show
|
||||
|
||||
data QPError = QPError Text Text
|
||||
deriving Show
|
||||
data RaiseError
|
||||
= MsgParseError ByteString
|
||||
| DetParseError ByteString
|
||||
| NoDetail
|
||||
deriving Show
|
||||
data RangeError
|
||||
= NegativeLimit
|
||||
| LowerGTUpper
|
||||
| OutOfBounds Text Text
|
||||
deriving Show
|
||||
|
||||
type NodeName = Text
|
||||
type Depth = Integer
|
||||
|
||||
@@ -175,7 +122,7 @@ data JsonOperation
|
||||
deriving (Eq, Show, Ord)
|
||||
|
||||
-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text
|
||||
-- because we reuse our escaping functons and let pg do the casting with
|
||||
-- because we reuse our escaping functions and let pg do the casting with
|
||||
-- '1'::int
|
||||
data JsonOperand
|
||||
= JKey { jVal :: Text }
|
||||
@@ -218,7 +165,7 @@ data Operation
|
||||
= Op SimpleOperator SingleVal
|
||||
| OpQuant QuantOperator (Maybe OpQuantifier) SingleVal
|
||||
| In ListVal
|
||||
| Is TrileanVal
|
||||
| Is IsVal
|
||||
| IsDistinctFrom SingleVal
|
||||
| Fts FtsOperator (Maybe Language) SingleVal
|
||||
deriving (Eq, Show)
|
||||
@@ -231,12 +178,13 @@ type SingleVal = Text
|
||||
-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)
|
||||
type ListVal = [Text]
|
||||
|
||||
-- | Three-valued logic values
|
||||
data TrileanVal
|
||||
= TriTrue
|
||||
| TriFalse
|
||||
| TriNull
|
||||
| TriUnknown
|
||||
data IsVal
|
||||
= IsNull
|
||||
| IsNotNull
|
||||
-- Trilean values
|
||||
| IsTriTrue
|
||||
| IsTriFalse
|
||||
| IsTriUnknown
|
||||
deriving (Eq, Show)
|
||||
|
||||
-- Operators that are quantifiable, i.e. they can be used with the any/all modifiers
|
||||
|
||||
@@ -17,7 +17,7 @@ module PostgREST.App
|
||||
|
||||
|
||||
import Control.Monad.Except (liftEither)
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
import Data.Either.Combinators (mapLeft, whenLeft)
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.String (IsString (..))
|
||||
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
||||
@@ -42,10 +42,12 @@ import qualified PostgREST.Unix as Unix (installSignalHandlers)
|
||||
|
||||
import PostgREST.ApiRequest (ApiRequest (..))
|
||||
import PostgREST.AppState (AppState)
|
||||
import PostgREST.Auth (AuthResult (..))
|
||||
import PostgREST.Config (AppConfig (..), LogLevel (..))
|
||||
import PostgREST.Auth.Types (AuthResult (..))
|
||||
import PostgREST.Config (AppConfig (..), LogLevel (..),
|
||||
LogQuery (..))
|
||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||
import PostgREST.Error (Error)
|
||||
import PostgREST.Network (resolveHost)
|
||||
import PostgREST.Observation (Observation (..))
|
||||
import PostgREST.Response.Performance (ServerTiming (..),
|
||||
serverTimingHeader)
|
||||
@@ -66,8 +68,6 @@ run appState = do
|
||||
let observer = AppState.getObserver appState
|
||||
conf@AppConfig{..} <- AppState.getConfig appState
|
||||
|
||||
observer $ AppStartObs prettyVersion
|
||||
|
||||
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
|
||||
Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
|
||||
|
||||
@@ -82,7 +82,8 @@ run appState = do
|
||||
observer $ AppServerUnixObs path
|
||||
Nothing -> do
|
||||
port <- NS.socketPort $ AppState.getSocketREST appState
|
||||
observer $ AppServerPortObs port
|
||||
host <- resolveHost $ AppState.getSocketREST appState
|
||||
observer $ AppServerPortObs (fromJust host) port
|
||||
|
||||
Warp.runSettingsSocket (serverSettings conf) (AppState.getSocketREST appState) app
|
||||
|
||||
@@ -143,11 +144,28 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@
|
||||
body <- lift $ Wai.strictRequestBody req
|
||||
|
||||
let jwtTime = if configServerTimingEnabled then Auth.getJwtDur req else Nothing
|
||||
timezones = dbTimezones sCache
|
||||
prefs = ApiRequest.userPreferences conf req timezones
|
||||
|
||||
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestError $ ApiRequest.userApiRequest conf req body sCache
|
||||
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestError $ ApiRequest.userApiRequest conf prefs req body
|
||||
(planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
|
||||
(queryTime, queryResult) <- withTiming $ Query.runQuery appState conf authResult apiReq plan sCache pgVer (Just authRole /= configDbAnonRole)
|
||||
(respTime, resp) <- withTiming $ liftEither $ Response.actionResponse queryResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
|
||||
|
||||
let query = Query.query conf authResult apiReq plan sCache pgVer
|
||||
logSQL = lift . AppState.getObserver appState . DBQuery (Query.getSQLQuery query)
|
||||
|
||||
(queryTime, queryResult) <- withTiming $ do
|
||||
case query of
|
||||
Query.NoDbQuery r -> pure r
|
||||
Query.DbQuery{..} -> do
|
||||
dbRes <- lift $ AppState.usePool appState (dqTransaction dqIsoLevel dqTxMode $ runExceptT dqDbHandler)
|
||||
let eitherResp = mapLeft Error.PgErr . mapLeft (Error.PgError (Just authRole /= configDbAnonRole)) $ dbRes
|
||||
when (configLogQuery /= LogQueryDisabled) $ whenLeft eitherResp $ logSQL . Error.status
|
||||
liftEither eitherResp >>= liftEither
|
||||
|
||||
(respTime, resp) <- withTiming $ do
|
||||
let response = Response.actionResponse queryResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
|
||||
when (configLogQuery /= LogQueryDisabled) $ logSQL $ either Error.status Response.pgrstStatus response
|
||||
liftEither response
|
||||
|
||||
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime queryTime respTime) resp
|
||||
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
module PostgREST.AppState
|
||||
( AppState
|
||||
, AuthResult(..)
|
||||
, JwtCacheState(..)
|
||||
, destroy
|
||||
, getConfig
|
||||
, getSchemaCache
|
||||
@@ -32,10 +30,7 @@ module PostgREST.AppState
|
||||
, isPending
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.Cache as C
|
||||
import Data.Either.Combinators (whenLeft)
|
||||
import qualified Data.Text as T (unpack)
|
||||
import qualified Hasql.Pool as SQL
|
||||
@@ -44,6 +39,7 @@ import qualified Hasql.Session as SQL
|
||||
import qualified Hasql.Transaction.Sessions as SQL
|
||||
import qualified Network.HTTP.Types.Status as HTTP
|
||||
import qualified Network.Socket as NS
|
||||
import qualified PostgREST.Auth.JwtCache as JwtCache
|
||||
import qualified PostgREST.Error as Error
|
||||
import qualified PostgREST.Logger as Logger
|
||||
import qualified PostgREST.Metrics as Metrics
|
||||
@@ -61,6 +57,7 @@ import Data.IORef (IORef, atomicWriteIORef, newIORef,
|
||||
readIORef)
|
||||
import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||
|
||||
import PostgREST.Auth.JwtCache (JwtCacheState)
|
||||
import PostgREST.Config (AppConfig (..),
|
||||
addFallbackAppName,
|
||||
readAppConfig)
|
||||
@@ -79,17 +76,6 @@ import Data.Streaming.Network (bindPortTCP, bindRandomPortTCP)
|
||||
import Data.String (IsString (..))
|
||||
import Protolude
|
||||
|
||||
data AuthResult = AuthResult
|
||||
{ authClaims :: KM.KeyMap JSON.Value
|
||||
, authRole :: BS.ByteString
|
||||
}
|
||||
|
||||
-- | JWT Cache and IO action that triggers purging old entries from the cache
|
||||
data JwtCacheState = JwtCacheState
|
||||
{ jwtCache :: C.Cache ByteString AuthResult
|
||||
, purgeCache :: IO ()
|
||||
}
|
||||
|
||||
data AppState = AppState
|
||||
-- | Database connection pool
|
||||
{ statePool :: SQL.Pool
|
||||
@@ -113,14 +99,14 @@ data AppState = AppState
|
||||
, stateNextDelay :: IORef Int
|
||||
-- | Keeps track of the next delay for the listener
|
||||
, stateNextListenerDelay :: IORef Int
|
||||
-- | JWT Cache
|
||||
, jwtCacheState :: JwtCacheState
|
||||
-- | Network socket for REST API
|
||||
, stateSocketREST :: NS.Socket
|
||||
-- | Network socket for the admin UI
|
||||
, stateSocketAdmin :: Maybe NS.Socket
|
||||
-- | Observation handler
|
||||
, stateObserver :: ObservationHandler
|
||||
-- | JWT Cache
|
||||
, stateJwtCache :: JwtCache.JwtCacheState
|
||||
, stateLogger :: Logger.LoggerState
|
||||
, stateMetrics :: Metrics.MetricsState
|
||||
}
|
||||
@@ -139,23 +125,16 @@ init conf@AppConfig{configLogLevel, configDbPoolSize} = do
|
||||
metricsState <- Metrics.init configDbPoolSize
|
||||
let observer = liftA2 (>>) (Logger.observationLogger loggerState configLogLevel) (Metrics.observationMetrics metricsState)
|
||||
|
||||
observer $ AppStartObs prettyVersion
|
||||
|
||||
jwtCacheState <- JwtCache.init
|
||||
pool <- initPool conf observer
|
||||
(sock, adminSock) <- initSockets conf
|
||||
state' <- initWithPool (sock, adminSock) pool conf loggerState metricsState observer
|
||||
state' <- initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState observer
|
||||
pure state' { stateSocketREST = sock, stateSocketAdmin = adminSock}
|
||||
|
||||
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
|
||||
initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
|
||||
cache <- C.newCache Nothing
|
||||
-- purgeExpired has O(n^2) complexity
|
||||
-- so we wrap it in debounce to make sure it:
|
||||
-- 1) is executed asynchronously
|
||||
-- 2) only a single purge operation is running at a time
|
||||
debounce <- mkDebounce defaultDebounceSettings
|
||||
-- debounceFreq is set to default 1 second
|
||||
{ debounceAction = C.purgeExpired cache
|
||||
, debounceEdge = leadingEdge
|
||||
}
|
||||
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> JwtCache.JwtCacheState -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
|
||||
initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState observer = do
|
||||
|
||||
appState <- AppState pool
|
||||
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
||||
@@ -168,10 +147,10 @@ initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
|
||||
<*> myThreadId
|
||||
<*> newIORef 0
|
||||
<*> newIORef 1
|
||||
<*> pure (JwtCacheState cache debounce)
|
||||
<*> pure sock
|
||||
<*> pure adminSock
|
||||
<*> pure observer
|
||||
<*> pure jwtCacheState
|
||||
<*> pure loggerState
|
||||
<*> pure metricsState
|
||||
|
||||
@@ -195,7 +174,8 @@ initSockets AppConfig{..} = do
|
||||
cfg'uspm = configServerUnixSocketMode
|
||||
cfg'host = configServerHost
|
||||
cfg'port = configServerPort
|
||||
cfg'adminport = configAdminServerPort
|
||||
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,
|
||||
@@ -213,16 +193,16 @@ initSockets AppConfig{..} = do
|
||||
pure (num, sock)
|
||||
pure sock
|
||||
|
||||
adminSock <- case cfg'adminport of
|
||||
adminSock <- case cfg'adminPort of
|
||||
Just adminPort -> do
|
||||
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'host)
|
||||
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{..} observer =
|
||||
initPool AppConfig{..} observer = do
|
||||
SQL.acquire $ SQL.settings
|
||||
[ SQL.size configDbPoolSize
|
||||
, SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout
|
||||
@@ -332,7 +312,7 @@ getTime :: AppState -> IO UTCTime
|
||||
getTime = stateGetTime
|
||||
|
||||
getJwtCacheState :: AppState -> JwtCacheState
|
||||
getJwtCacheState = jwtCacheState
|
||||
getJwtCacheState = stateJwtCache
|
||||
|
||||
getSocketREST :: AppState -> NS.Socket
|
||||
getSocketREST = stateSocketREST
|
||||
@@ -408,6 +388,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion
|
||||
killThread mainThreadId
|
||||
observer $ DBConnectedObs $ pgvFullName actualPgVersion
|
||||
observer $ PoolInit configDbPoolSize
|
||||
putPgVersion appState actualPgVersion
|
||||
return $ Just actualPgVersion
|
||||
|
||||
@@ -426,7 +407,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
Left e -> do
|
||||
putSCacheStatus appState SCPending
|
||||
putSchemaCache appState Nothing
|
||||
observer $ SchemaCacheErrorObs e
|
||||
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
|
||||
return Nothing
|
||||
|
||||
Right sCache -> do
|
||||
@@ -456,7 +437,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
-- | 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.
|
||||
readInDbConfig :: Bool -> AppState -> IO ()
|
||||
readInDbConfig startingUp appState@AppState{stateObserver=observer, jwtCacheState=JwtCacheState{jwtCache}} = do
|
||||
readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
|
||||
conf <- getConfig appState
|
||||
pgVer <- getPgVersion appState
|
||||
dbSettings <-
|
||||
@@ -493,7 +474,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer, jwtCacheStat
|
||||
if configJwtSecret conf == configJwtSecret newConf then
|
||||
pass
|
||||
else
|
||||
C.purge jwtCache -- atomic O(1) operation
|
||||
JwtCache.emptyCache (getJwtCacheState appState) -- atomic O(1) operation
|
||||
|
||||
if startingUp then
|
||||
pass
|
||||
|
||||
@@ -10,76 +10,124 @@ 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
|
||||
very simple authentication system inside the PostgreSQL database.
|
||||
-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module PostgREST.Auth
|
||||
( AuthResult (..)
|
||||
, getResult
|
||||
( getResult
|
||||
, getJwtDur
|
||||
, getRole
|
||||
, middleware
|
||||
) where
|
||||
|
||||
import qualified Crypto.JWT as JWT
|
||||
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.Internal as BS
|
||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||
import qualified Data.Cache as C
|
||||
import qualified Data.Scientific as Sci
|
||||
import qualified Data.Text as T
|
||||
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.Wai as Wai
|
||||
import qualified Network.Wai.Middleware.HttpAuth as Wai
|
||||
|
||||
import Control.Lens (set)
|
||||
import Control.Monad.Except (liftEither)
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
import Data.List (lookup)
|
||||
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
|
||||
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
|
||||
import System.Clock (TimeSpec (..))
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
import System.TimeIt (timeItT)
|
||||
|
||||
import PostgREST.AppState (AppState, AuthResult (..),
|
||||
JwtCacheState (..), getConfig,
|
||||
getJwtCacheState, getTime)
|
||||
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
|
||||
import PostgREST.Error (Error (..))
|
||||
import PostgREST.AppState (AppState, getConfig, getJwtCacheState,
|
||||
getTime)
|
||||
import PostgREST.Auth.JwtCache (lookupJwtCache)
|
||||
import PostgREST.Auth.Types (AuthResult (..))
|
||||
import PostgREST.Config (AppConfig (..), FilterExp (..),
|
||||
JSPath, JSPathExp (..))
|
||||
import PostgREST.Error (Error (..), JwtClaimsError (..),
|
||||
JwtDecodeError (..), JwtError (..))
|
||||
|
||||
import Protolude
|
||||
|
||||
|
||||
-- | Receives the JWT secret and audience (from config) and a JWT and returns a
|
||||
-- JSON object of JWT claims.
|
||||
parseToken :: Monad m =>
|
||||
AppConfig -> LByteString -> UTCTime -> ExceptT Error m JSON.Value
|
||||
parseToken _ "" _ = return JSON.emptyObject
|
||||
parseToken AppConfig{..} token time = do
|
||||
secret <- liftEither . maybeToRight JwtTokenMissing $ configJWKS
|
||||
eitherClaims <-
|
||||
lift . runExceptT $
|
||||
JWT.verifyClaimsAt validation secret time =<< JWT.decodeCompact token
|
||||
liftEither . mapLeft jwtClaimsError $ JSON.toJSON <$> eitherClaims
|
||||
parseToken :: AppConfig -> Maybe ByteString -> UTCTime -> ExceptT Error IO JSON.Value
|
||||
parseToken _ Nothing _ = return JSON.emptyObject
|
||||
parseToken _ (Just "") _ = throwE . JwtErr $ JwtDecodeErr EmptyAuthHeader
|
||||
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
|
||||
validation =
|
||||
JWT.defaultJWTValidationSettings audienceCheck & set JWT.allowedSkew 30
|
||||
hasThreeParts :: ByteString -> Either Error ByteString
|
||||
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
|
||||
3 -> Right token
|
||||
n -> Left $ JwtErr $ JwtDecodeErr $ UnexpectedParts n
|
||||
jwtDecodeError :: JWT.JwtError -> JwtError
|
||||
-- The only errors we can get from JWT.decode function are:
|
||||
-- BadAlgorithm
|
||||
-- KeyError
|
||||
-- BadCrypto
|
||||
jwtDecodeError (JWT.KeyError 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
|
||||
|
||||
audienceCheck :: JWT.StringOrURI -> Bool
|
||||
audienceCheck = maybe (const True) (==) configJwtAudience
|
||||
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 JWTExpired >>
|
||||
verifyClaim mclaims "nbf" isValidNbfClaim JWTNotYetValid >>
|
||||
verifyClaim mclaims "iat" isValidIatClaim JWTIssuedAtFuture >>
|
||||
verifyClaim mclaims "aud" isValidAudClaim JWTNotInAudience >>
|
||||
return jclaims
|
||||
_ -> Left $ JwtClaimsErr ParsingClaimsFailed
|
||||
-- TODO: We could enable JWE support here (encrypted tokens)
|
||||
verifyClaims _ = Left $ JwtDecodeErr UnsupportedTokenType
|
||||
|
||||
jwtClaimsError :: JWT.JWTError -> Error
|
||||
jwtClaimsError JWT.JWTExpired = JwtTokenInvalid "JWT expired"
|
||||
jwtClaimsError e = JwtTokenInvalid $ show e
|
||||
verifyClaim mclaims claim func err = do
|
||||
isValid <- maybe (Right True) func (KM.lookup claim mclaims)
|
||||
unless isValid $ Left $ JwtClaimsErr 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 $ JwtClaimsErr ExpClaimNotNumber
|
||||
|
||||
isValidNbfClaim :: JSON.Value -> Either JwtError Bool
|
||||
isValidNbfClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
||||
isValidNbfClaim _ = Left $ JwtClaimsErr NbfClaimNotNumber
|
||||
|
||||
isValidIatClaim :: JSON.Value -> Either JwtError Bool
|
||||
isValidIatClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
||||
isValidIatClaim _ = Left $ JwtClaimsErr IatClaimNotNumber
|
||||
|
||||
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 $ JwtClaimsErr AudClaimNotStringOrArray
|
||||
|
||||
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 JwtTokenRequired $
|
||||
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
|
||||
unquoted <$> walkJSPath (Just jclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
||||
return AuthResult
|
||||
{ authClaims = mclaims & KM.insert "role" (JSON.toJSON $ decodeUtf8 role)
|
||||
@@ -90,8 +138,20 @@ parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
|
||||
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
|
||||
@@ -105,10 +165,11 @@ middleware appState app req respond = do
|
||||
conf <- getConfig appState
|
||||
time <- getTime appState
|
||||
|
||||
let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
|
||||
parseJwt = runExceptT $ parseToken conf (LBS.fromStrict token) time >>= parseClaims conf
|
||||
let token = Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
|
||||
parseJwt = runExceptT $ parseToken conf token time >>= parseClaims conf
|
||||
jwtCacheState = getJwtCacheState appState
|
||||
|
||||
-- If DbPlanEnabled -> calculate JWT validation time
|
||||
-- If ServerTimingEnabled -> calculate JWT validation time
|
||||
-- If JwtCacheMaxLifetime -> cache JWT validation result
|
||||
req' <- case (configServerTimingEnabled conf, configJwtCacheMaxLifetime conf) of
|
||||
(True, 0) -> do
|
||||
@@ -116,7 +177,9 @@ middleware appState app req respond = do
|
||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
|
||||
|
||||
(True, maxLifetime) -> do
|
||||
(dur, authResult) <- timeItT $ getJWTFromCache appState token maxLifetime parseJwt time
|
||||
(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
|
||||
@@ -124,57 +187,13 @@ middleware appState app req respond = do
|
||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
||||
|
||||
(False, maxLifetime) -> do
|
||||
authResult <- getJWTFromCache appState token maxLifetime parseJwt time
|
||||
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
|
||||
|
||||
-- | Used to retrieve and insert JWT to JWT Cache
|
||||
getJWTFromCache :: AppState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
|
||||
getJWTFromCache appState token maxLifetime parseJwt utc = do
|
||||
let JwtCacheState{..} = getJwtCacheState appState
|
||||
checkCache <- C.lookup jwtCache token
|
||||
authResult <- maybe parseJwt (pure . Right) checkCache
|
||||
|
||||
case (authResult,checkCache) of
|
||||
-- From comment:
|
||||
-- https://github.com/PostgREST/postgrest/pull/3801#discussion_r1857987914
|
||||
--
|
||||
-- We purge expired cache entries on a cache miss
|
||||
-- The reasoning is that:
|
||||
--
|
||||
-- 1. We expect it to be rare (otherwise there is no point of the cache)
|
||||
-- 2. It makes sure the cache is not growing (as inserting new entries
|
||||
-- does garbage collection)
|
||||
-- 3. Since this is time expiration based cache there is no real risk of
|
||||
-- starvation - sooner or later we are going to have a cache miss.
|
||||
|
||||
(Right res, Nothing) -> do -- cache miss
|
||||
|
||||
let timeSpec = getTimeSpec res maxLifetime utc
|
||||
|
||||
-- insert new cache entry
|
||||
C.insert' jwtCache timeSpec token res
|
||||
|
||||
-- Execute IO action to purge the cache
|
||||
-- It is assumed this action returns immidiately
|
||||
-- so that request processing is not blocked.
|
||||
purgeCache
|
||||
|
||||
_ -> pure ()
|
||||
|
||||
return authResult
|
||||
|
||||
-- Used to extract JWT exp claim and add to JWT Cache
|
||||
getTimeSpec :: AuthResult -> Int -> UTCTime -> Maybe TimeSpec
|
||||
getTimeSpec res maxLifetime utc = do
|
||||
let expireJSON = KM.lookup "exp" (authClaims res)
|
||||
utcToSecs = floor . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds
|
||||
sciToInt = fromMaybe 0 . Sci.toBoundedInteger
|
||||
case expireJSON of
|
||||
Just (JSON.Number seconds) -> Just $ TimeSpec (sciToInt seconds - utcToSecs utc) 0
|
||||
_ -> Just $ TimeSpec (fromIntegral maxLifetime :: Int64) 0
|
||||
|
||||
authResultKey :: Vault.Key (Either Error AuthResult)
|
||||
authResultKey = unsafePerformIO Vault.newKey
|
||||
{-# NOINLINE authResultKey #-}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
{-|
|
||||
Module : PostgREST.Auth.JwtCache
|
||||
Description : PostgREST Jwt Authentication Result Cache.
|
||||
|
||||
This module provides functions to deal with the JWT cache
|
||||
-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
module PostgREST.Auth.JwtCache
|
||||
( init
|
||||
, JwtCacheState
|
||||
, lookupJwtCache
|
||||
, emptyCache
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.Cache as C
|
||||
import qualified Data.Scientific as Sci
|
||||
|
||||
import Control.Debounce
|
||||
|
||||
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
|
||||
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
|
||||
import System.Clock (TimeSpec (..))
|
||||
|
||||
import PostgREST.Auth.Types (AuthResult (..))
|
||||
import PostgREST.Error (Error (..))
|
||||
|
||||
import Protolude
|
||||
|
||||
-- | JWT Cache and IO action that triggers purging old entries from the cache
|
||||
data JwtCacheState = JwtCacheState
|
||||
{ jwtCache :: C.Cache ByteString AuthResult
|
||||
, purgeCache :: IO ()
|
||||
}
|
||||
|
||||
-- | Initialize JwtCacheState
|
||||
init :: IO JwtCacheState
|
||||
init = do
|
||||
cache <- C.newCache Nothing -- no default expiration
|
||||
-- purgeExpired has O(n^2) complexity
|
||||
-- so we wrap it in debounce to make sure it:
|
||||
-- 1) is executed asynchronously
|
||||
-- 2) only a single purge operation is running at a time
|
||||
debounce <- mkDebounce defaultDebounceSettings
|
||||
-- debounceFreq is set to default 1 second
|
||||
{ debounceAction = C.purgeExpired cache
|
||||
, debounceEdge = leadingEdge
|
||||
}
|
||||
pure $ JwtCacheState cache debounce
|
||||
|
||||
-- | Used to retrieve and insert JWT to JWT Cache
|
||||
lookupJwtCache :: JwtCacheState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
|
||||
lookupJwtCache JwtCacheState{jwtCache, purgeCache} token maxLifetime parseJwt utc = do
|
||||
checkCache <- C.lookup jwtCache token
|
||||
authResult <- maybe parseJwt (pure . Right) checkCache
|
||||
|
||||
case (authResult,checkCache) of
|
||||
-- From comment:
|
||||
-- https://github.com/PostgREST/postgrest/pull/3801#discussion_r1857987914
|
||||
--
|
||||
-- We purge expired cache entries on a cache miss
|
||||
-- The reasoning is that:
|
||||
--
|
||||
-- 1. We expect it to be rare (otherwise there is no point of the cache)
|
||||
-- 2. It makes sure the cache is not growing (as inserting new entries
|
||||
-- does garbage collection)
|
||||
-- 3. Since this is time expiration based cache there is no real risk of
|
||||
-- starvation - sooner or later we are going to have a cache miss.
|
||||
|
||||
(Right res, Nothing) -> do -- cache miss
|
||||
|
||||
let timeSpec = getTimeSpec res maxLifetime utc
|
||||
|
||||
-- insert new cache entry
|
||||
C.insert' jwtCache (Just timeSpec) token res
|
||||
|
||||
-- Execute IO action to purge the cache
|
||||
-- It is assumed this action returns immidiately
|
||||
-- so that request processing is not blocked.
|
||||
purgeCache
|
||||
|
||||
_ -> pure ()
|
||||
|
||||
return authResult
|
||||
|
||||
-- Used to extract JWT exp claim and add to JWT Cache
|
||||
getTimeSpec :: AuthResult -> Int -> UTCTime -> TimeSpec
|
||||
getTimeSpec res maxLifetime utc = do
|
||||
let expireJSON = KM.lookup "exp" (authClaims res)
|
||||
utcToSecs = floor . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds
|
||||
sciToInt = fromMaybe 0 . Sci.toBoundedInteger
|
||||
case expireJSON of
|
||||
Just (JSON.Number seconds) -> TimeSpec (sciToInt seconds - utcToSecs utc) 0
|
||||
_ -> TimeSpec (fromIntegral maxLifetime :: Int64) 0
|
||||
|
||||
-- | Empty the cache (done when the config is reloaded)
|
||||
emptyCache :: JwtCacheState -> IO ()
|
||||
emptyCache JwtCacheState{jwtCache} = C.purge jwtCache
|
||||
@@ -0,0 +1,13 @@
|
||||
module PostgREST.Auth.Types
|
||||
( AuthResult (..) )
|
||||
where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.ByteString as BS
|
||||
|
||||
-- | Parse result for JWT Claims
|
||||
data AuthResult = AuthResult
|
||||
{ authClaims :: KM.KeyMap JSON.Value
|
||||
, authRole :: BS.ByteString
|
||||
}
|
||||
@@ -60,7 +60,7 @@ dumpSchema appState = do
|
||||
case result of
|
||||
Left e -> do
|
||||
let observer = AppState.getObserver appState
|
||||
observer $ SchemaCacheErrorObs e
|
||||
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
|
||||
exitFailure
|
||||
Right sCache -> return $ JSON.encode sCache
|
||||
|
||||
@@ -128,9 +128,6 @@ exampleConfigFile =
|
||||
[str|## Admin server used for checks. It's disabled by default unless a port is specified.
|
||||
|# admin-server-port = 3001
|
||||
|
|
||||
|## Whether to enable the /config endpoint of the admin server
|
||||
|# admin-server-config-enabled = false
|
||||
|
|
||||
|## The database role to use when no client authentication is provided
|
||||
|# db-anon-role = "anon"
|
||||
|
|
||||
@@ -212,6 +209,9 @@ exampleConfigFile =
|
||||
|## Logging level, the admitted values are: crit, error, warn, info and debug.
|
||||
|log-level = "error"
|
||||
|
|
||||
|## Log the requested SQL query at the current log-level.
|
||||
|log-query = "disabled"
|
||||
|
|
||||
|## Determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely.
|
||||
|## Admitted values: follow-privileges, ignore-privileges, disabled
|
||||
|openapi-mode = "follow-privileges"
|
||||
|
||||
@@ -15,7 +15,9 @@ module PostgREST.Config
|
||||
, Environment
|
||||
, JSPath
|
||||
, JSPathExp(..)
|
||||
, FilterExp(..)
|
||||
, LogLevel(..)
|
||||
, LogQuery(..)
|
||||
, OpenAPIMode(..)
|
||||
, Proxy(..)
|
||||
, toText
|
||||
@@ -28,28 +30,25 @@ module PostgREST.Config
|
||||
, addTargetSessionAttrs
|
||||
) where
|
||||
|
||||
import qualified Crypto.JOSE.Types as JOSE
|
||||
import qualified Crypto.JWT as JWT
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.Configurator as C
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Jose.Jwa as JWT
|
||||
import qualified Jose.Jwk as JWT
|
||||
|
||||
import Control.Lens (preview)
|
||||
import Control.Monad (fail)
|
||||
import Crypto.JWT (JWK, JWKSet, StringOrURI, stringOrUri)
|
||||
import Data.Aeson (toJSON)
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
import Data.List (lookup)
|
||||
import Data.List.NonEmpty (fromList, toList)
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.Scientific (floatingOrInteger)
|
||||
import Network.URI (escapeURIString,
|
||||
import Jose.Jwk (Jwk, JwkSet)
|
||||
import Network.URI (escapeURIString, isURI,
|
||||
isUnescapedInURIComponent)
|
||||
import Numeric (readOct, showOct)
|
||||
import System.Environment (getEnvironment)
|
||||
@@ -57,8 +56,9 @@ import System.Posix.Types (FileMode)
|
||||
|
||||
import PostgREST.Config.Database (RoleIsolationLvl,
|
||||
RoleSettings)
|
||||
import PostgREST.Config.JSPath (JSPath, JSPathExp (..),
|
||||
dumpJSPath, pRoleClaimKey)
|
||||
import PostgREST.Config.JSPath (FilterExp (..), JSPath,
|
||||
JSPathExp (..), dumpJSPath,
|
||||
pRoleClaimKey)
|
||||
import PostgREST.Config.Proxy (Proxy (..),
|
||||
isMalformedProxyUri, toURI)
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi,
|
||||
@@ -92,13 +92,14 @@ data AppConfig = AppConfig
|
||||
, configDbTxRollbackAll :: Bool
|
||||
, configDbUri :: Text
|
||||
, configFilePath :: Maybe FilePath
|
||||
, configJWKS :: Maybe JWKSet
|
||||
, configJwtAudience :: Maybe StringOrURI
|
||||
, configJWKS :: Maybe JwkSet
|
||||
, configJwtAudience :: Maybe Text
|
||||
, configJwtRoleClaimKey :: JSPath
|
||||
, configJwtSecret :: Maybe BS.ByteString
|
||||
, configJwtSecretIsBase64 :: Bool
|
||||
, configJwtCacheMaxLifetime :: Int
|
||||
, configLogLevel :: LogLevel
|
||||
, configLogQuery :: LogQuery
|
||||
, configOpenApiMode :: OpenAPIMode
|
||||
, configOpenApiSecurityActive :: Bool
|
||||
, configOpenApiServerProxyUri :: Maybe Text
|
||||
@@ -109,8 +110,8 @@ data AppConfig = AppConfig
|
||||
, configServerTimingEnabled :: Bool
|
||||
, configServerUnixSocket :: Maybe FilePath
|
||||
, configServerUnixSocketMode :: FileMode
|
||||
, configAdminServerHost :: Text
|
||||
, configAdminServerPort :: Maybe Int
|
||||
, configAdminServerConfigEnabled :: Bool
|
||||
, configRoleSettings :: RoleSettings
|
||||
, configRoleIsoLvl :: RoleIsolationLvl
|
||||
, configInternalSCSleep :: Maybe Int32
|
||||
@@ -127,6 +128,14 @@ dumpLogLevel = \case
|
||||
LogInfo -> "info"
|
||||
LogDebug -> "debug"
|
||||
|
||||
data LogQuery = LogQueryMain | LogQueryDisabled
|
||||
deriving (Eq)
|
||||
|
||||
dumpLogQuery :: LogQuery -> Text
|
||||
dumpLogQuery = \case
|
||||
LogQueryMain -> "main-query"
|
||||
LogQueryDisabled -> "disabled"
|
||||
|
||||
data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
|
||||
deriving Eq
|
||||
|
||||
@@ -164,12 +173,13 @@ toText conf =
|
||||
,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig)
|
||||
,("db-tx-end", q . showTxEnd)
|
||||
,("db-uri", q . configDbUri)
|
||||
,("jwt-aud", T.decodeUtf8 . LBS.toStrict . JSON.encode . maybe "" toJSON . configJwtAudience)
|
||||
,("jwt-aud", q . fromMaybe mempty . configJwtAudience)
|
||||
,("jwt-role-claim-key", q . T.intercalate mempty . fmap dumpJSPath . configJwtRoleClaimKey)
|
||||
,("jwt-secret", q . T.decodeUtf8 . showJwtSecret)
|
||||
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
|
||||
,("jwt-cache-max-lifetime", show . configJwtCacheMaxLifetime)
|
||||
,("log-level", q . dumpLogLevel . configLogLevel)
|
||||
,("log-query", q . dumpLogQuery . configLogQuery)
|
||||
,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode)
|
||||
,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive)
|
||||
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
|
||||
@@ -180,8 +190,8 @@ toText conf =
|
||||
,("server-timing-enabled", T.toLower . show . configServerTimingEnabled)
|
||||
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
|
||||
,("server-unix-socket-mode", q . T.pack . showSocketMode)
|
||||
,("admin-server-host", q . configAdminServerHost)
|
||||
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
|
||||
,("admin-server-config-enabled", T.toLower . show . configAdminServerConfigEnabled)
|
||||
]
|
||||
|
||||
-- quote all app.settings
|
||||
@@ -225,16 +235,18 @@ readAppConfig dbSettings optPath prevDbUri roleSettings roleIsolationLvl = do
|
||||
Left err ->
|
||||
return . Left $ "Error in config " <> err
|
||||
Right parsedConfig ->
|
||||
Right <$> decodeLoadFiles parsedConfig
|
||||
mapLeft show <$> decodeLoadFiles parsedConfig
|
||||
where
|
||||
-- Both C.ParseError and IOError are shown here
|
||||
loadConfig :: FilePath -> IO (Either SomeException C.Config)
|
||||
loadConfig = try . C.load
|
||||
|
||||
decodeLoadFiles :: AppConfig -> IO AppConfig
|
||||
decodeLoadFiles parsedConfig =
|
||||
decodeJWKS <$>
|
||||
(decodeSecret =<< readSecretFile =<< readDbUriFile prevDbUri parsedConfig)
|
||||
decodeLoadFiles :: AppConfig -> IO (Either IOException AppConfig)
|
||||
decodeLoadFiles parsedConfig = try $
|
||||
decodeJWKS =<<
|
||||
decodeSecret =<<
|
||||
readSecretFile =<<
|
||||
readDbUriFile prevDbUri parsedConfig
|
||||
|
||||
parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> RoleIsolationLvl -> C.Parser C.Config AppConfig
|
||||
parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
@@ -244,8 +256,8 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
|
||||
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
||||
<*> (fromMaybe True <$> optBool "db-channel-enabled")
|
||||
<*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
|
||||
<*> (maybe defaultHoistedAllowList splitOnCommas <$> optValue "db-hoisted-tx-settings")
|
||||
<*> (maybe ["public"] splitOnCommasEmptyable <$> optStringEmptyable "db-extra-search-path")
|
||||
<*> (maybe defaultHoistedAllowList splitOnCommas <$> optString "db-hoisted-tx-settings")
|
||||
<*> optWithAlias (optInt "db-max-rows")
|
||||
(optInt "max-rows")
|
||||
<*> (fromMaybe False <$> optBool "db-plan-enabled")
|
||||
@@ -260,8 +272,8 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
||||
<*> (fmap toQi <$> optWithAlias (optString "db-root-spec")
|
||||
(optString "root-spec"))
|
||||
<*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optValue "db-schemas")
|
||||
(optValue "db-schema"))
|
||||
<*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optString "db-schemas")
|
||||
(optString "db-schema"))
|
||||
<*> (fromMaybe True <$> optBool "db-config")
|
||||
<*> (fmap toQi <$> optString "db-pre-config")
|
||||
<*> parseTxEnd "db-tx-end" snd
|
||||
@@ -269,7 +281,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
|
||||
<*> pure optPath
|
||||
<*> pure Nothing
|
||||
<*> parseJwtAudience "jwt-aud"
|
||||
<*> optStringOrURI "jwt-aud"
|
||||
<*> parseRoleClaimKey "jwt-role-claim-key" "role-claim-key"
|
||||
<*> (fmap encodeUtf8 <$> optString "jwt-secret")
|
||||
<*> (fromMaybe False <$> optWithAlias
|
||||
@@ -277,18 +289,20 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
(optBool "secret-is-base64"))
|
||||
<*> (fromMaybe 0 <$> optInt "jwt-cache-max-lifetime")
|
||||
<*> parseLogLevel "log-level"
|
||||
<*> parseLogQuery "log-query"
|
||||
<*> parseOpenAPIMode "openapi-mode"
|
||||
<*> (fromMaybe False <$> optBool "openapi-security-active")
|
||||
<*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
|
||||
<*> parseCORSAllowedOrigins "server-cors-allowed-origins"
|
||||
<*> (fromMaybe "!4" <$> optString "server-host")
|
||||
<*> (fromMaybe 3000 <$> optInt "server-port")
|
||||
<*> (defaultServerHost <$> optString "server-host")
|
||||
<*> parseServerPort "server-port"
|
||||
<*> (fmap (CI.mk . encodeUtf8) <$> optString "server-trace-header")
|
||||
<*> (fromMaybe False <$> optBool "server-timing-enabled")
|
||||
<*> (fmap T.unpack <$> optString "server-unix-socket")
|
||||
<*> parseSocketFileMode "server-unix-socket-mode"
|
||||
<*> optInt "admin-server-port"
|
||||
<*> (fromMaybe False <$> optBool "admin-server-config-enabled")
|
||||
<*> (defaultServerHost <$> optWithAlias (optString "admin-server-host")
|
||||
(optString "server-host"))
|
||||
<*> parseAdminServerPort "admin-server-port"
|
||||
<*> pure roleSettings
|
||||
<*> pure roleIsolationLvl
|
||||
<*> optInt "internal-schema-cache-sleep"
|
||||
@@ -300,6 +314,17 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
fromEnv = M.mapKeys fromJust $ M.filterWithKey (\k _ -> isJust k) $ M.mapKeys normalize env
|
||||
normalize k = ("app.settings." <>) <$> T.stripPrefix "PGRST_APP_SETTINGS_" (toS k)
|
||||
|
||||
parseServerPort :: C.Key -> C.Parser C.Config Int
|
||||
parseServerPort k = fromMaybe 3000 <$> optInt k
|
||||
|
||||
parseAdminServerPort :: C.Key -> C.Parser C.Config (Maybe Int)
|
||||
parseAdminServerPort k = do
|
||||
serverPort <- parseServerPort "server-port"
|
||||
optInt k >>= \case
|
||||
Nothing -> pure Nothing
|
||||
Just asp | asp == serverPort -> fail "admin-server-port cannot be the same as server-port"
|
||||
| otherwise -> pure $ Just asp
|
||||
|
||||
parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
|
||||
parseSocketFileMode k =
|
||||
optString k >>= \case
|
||||
@@ -329,14 +354,6 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
Just val | isMalformedProxyUri val -> fail "Malformed proxy uri, a correct example: https://example.com:8443/basePath"
|
||||
| otherwise -> pure $ Just val
|
||||
|
||||
parseJwtAudience :: C.Key -> C.Parser C.Config (Maybe StringOrURI)
|
||||
parseJwtAudience k =
|
||||
optString k >>= \case
|
||||
Nothing -> pure Nothing -- no audience in config file
|
||||
Just aud -> case preview stringOrUri (T.unpack aud) of
|
||||
Nothing -> fail "Invalid Jwt audience. Check your configuration."
|
||||
aud' -> pure aud'
|
||||
|
||||
parseLogLevel :: C.Key -> C.Parser C.Config LogLevel
|
||||
parseLogLevel k =
|
||||
optString k >>= \case
|
||||
@@ -348,6 +365,14 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
Just "debug" -> pure LogDebug
|
||||
Just _ -> fail "Invalid logging level. Check your configuration."
|
||||
|
||||
parseLogQuery :: C.Key -> C.Parser C.Config LogQuery
|
||||
parseLogQuery k =
|
||||
optString k >>= \case
|
||||
Nothing -> pure LogQueryDisabled
|
||||
Just "disabled" -> pure LogQueryDisabled
|
||||
Just "main-query" -> pure LogQueryMain
|
||||
Just _ -> fail "Invalid SQL logging value. Check your configuration."
|
||||
|
||||
parseTxEnd :: C.Key -> ((Bool, Bool) -> Bool) -> C.Parser C.Config Bool
|
||||
parseTxEnd k f =
|
||||
optString k >>= \case
|
||||
@@ -379,8 +404,22 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
optString :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||
optString k = mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
||||
|
||||
optValue :: C.Key -> C.Parser C.Config (Maybe C.Value)
|
||||
optValue k = overrideFromDbOrEnvironment C.optional k identity
|
||||
optStringEmptyable :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||
optStringEmptyable k = overrideFromDbOrEnvironment C.optional k coerceText
|
||||
|
||||
optStringOrURI :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||
optStringOrURI k = do
|
||||
stringOrURI <- mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
||||
-- If the string contains ':' then it should
|
||||
-- be a valid URI according to RFC 3986
|
||||
case stringOrURI of
|
||||
Just s -> if T.isInfixOf ":" s then validateURI s else return (Just s)
|
||||
Nothing -> return Nothing
|
||||
where
|
||||
validateURI :: Text -> C.Parser C.Config (Maybe Text)
|
||||
validateURI s = if isURI (T.unpack s)
|
||||
then return $ Just s
|
||||
else fail "jwt-aud should be a string or a valid URI"
|
||||
|
||||
optInt :: (Read i, Integral i) => C.Key -> C.Parser C.Config (Maybe i)
|
||||
optInt k = join <$> overrideFromDbOrEnvironment C.optional k coerceInt
|
||||
@@ -420,12 +459,18 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
Nothing -> (> 0) <$> (readMaybe s :: Maybe Integer)
|
||||
coerceBool _ = Nothing
|
||||
|
||||
splitOnCommas :: C.Value -> [Text]
|
||||
splitOnCommas (C.String s) = T.strip <$> T.splitOn "," s
|
||||
splitOnCommas _ = []
|
||||
splitOnCommas :: Text -> [Text]
|
||||
splitOnCommas s = T.strip <$> T.splitOn "," s
|
||||
|
||||
splitOnCommasEmptyable :: Text -> [Text]
|
||||
splitOnCommasEmptyable "" = []
|
||||
splitOnCommasEmptyable s = T.strip <$> T.splitOn "," s
|
||||
|
||||
defaultHoistedAllowList = ["statement_timeout","plan_filter.statement_cost_limit","default_transaction_isolation"]
|
||||
|
||||
defaultServerHost :: Maybe Text -> Text
|
||||
defaultServerHost = fromMaybe "!4"
|
||||
|
||||
-- | Read the JWT secret from a file if configJwtSecret is actually a
|
||||
-- filepath(has @ as its prefix). To check if the JWT secret is provided is
|
||||
-- in fact a file path, it must be decoded as 'Text' to be processed.
|
||||
@@ -450,24 +495,30 @@ decodeSecret conf@AppConfig{..} =
|
||||
decodeB64 = B64.decode . encodeUtf8 . T.strip . replaceUrlChars . decodeUtf8
|
||||
replaceUrlChars = T.replace "_" "/" . T.replace "-" "+" . T.replace "." "="
|
||||
|
||||
-- | Parse `jwt-secret` configuration option and turn into a JWKSet.
|
||||
-- | Parse `jwt-secret` configuration option and turn into a JWKS.
|
||||
--
|
||||
-- There are three ways to specify `jwt-secret`: text secret, JSON Web Key
|
||||
-- (JWK), or JSON Web Key Set (JWKS). The first two are converted into a JWKSet
|
||||
-- (JWK), or JSON Web Key Set (JWKS). The first two are converted into a JwkSet
|
||||
-- with one key and the last is converted as is.
|
||||
decodeJWKS :: AppConfig -> AppConfig
|
||||
decodeJWKS conf =
|
||||
conf { configJWKS = parseSecret <$> configJwtSecret conf }
|
||||
decodeJWKS :: AppConfig -> IO AppConfig
|
||||
decodeJWKS conf = do
|
||||
jwks <- case configJwtSecret conf of
|
||||
Just s -> either fail (pure . Just) $ parseSecret s
|
||||
Nothing -> pure Nothing
|
||||
return $ conf { configJWKS = jwks }
|
||||
|
||||
parseSecret :: ByteString -> JWKSet
|
||||
parseSecret :: ByteString -> Either [Char] JwkSet
|
||||
parseSecret bytes =
|
||||
fromMaybe (maybe secret (\jwk' -> JWT.JWKSet [jwk']) maybeJWK)
|
||||
maybeJWKSet
|
||||
case maybeJWKSet of
|
||||
Just jwk -> Right jwk
|
||||
Nothing -> maybe validateSecret (\jwk' -> Right $ JWT.JwkSet [jwk']) maybeJWK
|
||||
where
|
||||
maybeJWKSet = JSON.decodeStrict bytes :: Maybe JWKSet
|
||||
maybeJWK = JSON.decodeStrict bytes :: Maybe JWK
|
||||
secret = JWT.JWKSet [JWT.fromKeyMaterial keyMaterial]
|
||||
keyMaterial = JWT.OctKeyMaterial . JWT.OctKeyParameters $ JOSE.Base64Octets bytes
|
||||
maybeJWKSet = JSON.decodeStrict bytes :: Maybe JwkSet
|
||||
maybeJWK = JSON.decodeStrict bytes :: Maybe Jwk
|
||||
secret = JWT.JwkSet [JWT.SymmetricJwk bytes Nothing (Just JWT.Sig) (Just $ JWT.Signed JWT.HS256)]
|
||||
validateSecret
|
||||
| BS.length bytes < 32 = Left "The JWT secret must be at least 32 characters long."
|
||||
| otherwise = Right secret
|
||||
|
||||
-- | Read database uri from a separate file if `db-uri` is a filepath.
|
||||
readDbUriFile :: Maybe Text -> AppConfig -> IO AppConfig
|
||||
|
||||
@@ -24,7 +24,7 @@ import qualified Hasql.Statement as SQL
|
||||
import qualified Hasql.Transaction as SQL
|
||||
import qualified Hasql.Transaction.Sessions as SQL
|
||||
|
||||
import Text.InterpolatedString.Perl6 (q, qc)
|
||||
import NeatInterpolation (trimming)
|
||||
|
||||
import Protolude
|
||||
|
||||
@@ -95,7 +95,7 @@ queryDbSettings preConfFunc prepared =
|
||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
||||
transaction SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings prepared
|
||||
where
|
||||
sql = [qc|
|
||||
sql = encodeUtf8 [trimming|
|
||||
WITH
|
||||
role_setting AS (
|
||||
SELECT setdatabase as database,
|
||||
@@ -109,25 +109,25 @@ queryDbSettings preConfFunc prepared =
|
||||
substr(setting, 1, strpos(setting, '=') - 1) as k,
|
||||
substr(setting, strpos(setting, '=') + 1) as v
|
||||
FROM role_setting
|
||||
{preConfigF}
|
||||
${preConfigF}
|
||||
)
|
||||
SELECT DISTINCT ON (key)
|
||||
replace(k, '{prefix}', '') AS key,
|
||||
replace(k, '${prefix}', '') AS key,
|
||||
v AS value
|
||||
FROM kv_settings
|
||||
WHERE k = ANY($1) AND v IS NOT NULL
|
||||
WHERE k = ANY($$1) AND v IS NOT NULL
|
||||
ORDER BY key, database DESC NULLS LAST;
|
||||
|]
|
||||
preConfigF = case preConfFunc of
|
||||
Nothing -> mempty
|
||||
Just func -> [qc|
|
||||
Just func -> [trimming|
|
||||
UNION
|
||||
SELECT
|
||||
null as database,
|
||||
x as k,
|
||||
current_setting(x, true) as v
|
||||
FROM unnest($1) x
|
||||
JOIN {func}() _ ON TRUE
|
||||
FROM unnest($$1) x
|
||||
JOIN ${func}() _ ON TRUE
|
||||
|]::Text
|
||||
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
||||
|
||||
@@ -136,7 +136,7 @@ queryRoleSettings pgVer prepared =
|
||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
||||
transaction SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) prepared
|
||||
where
|
||||
sql = [q|
|
||||
sql = encodeUtf8 [trimming|
|
||||
with
|
||||
role_setting as (
|
||||
select r.rolname, unnest(r.rolconfig) as setting
|
||||
@@ -161,14 +161,15 @@ queryRoleSettings pgVer prepared =
|
||||
i.value as iso_lvl,
|
||||
coalesce(array_agg(row(kv.key, kv.value)) filter (where key <> 'default_transaction_isolation'), '{}') as role_settings
|
||||
from kv_settings kv
|
||||
join pg_settings ps on ps.name = kv.key |] <>
|
||||
(if pgVer >= pgVersion150
|
||||
then "and (ps.context = 'user' or has_parameter_privilege(current_user::regrole::oid, ps.name, 'set')) "
|
||||
else "and ps.context = 'user' ") <> [q|
|
||||
join pg_settings ps on ps.name = kv.key and (ps.context = 'user' ${hasParameterPrivilege})
|
||||
left join iso_setting i on i.rolname = kv.rolname
|
||||
group by kv.rolname, i.value;
|
||||
|]
|
||||
|
||||
hasParameterPrivilege
|
||||
| pgVer >= pgVersion150 = "or has_parameter_privilege(current_user::regrole::oid, ps.name, 'set')"
|
||||
| otherwise = ""
|
||||
|
||||
processRows :: [(Text, Maybe Text, [(Text, Text)])] -> (RoleSettings, RoleIsolationLvl)
|
||||
processRows rs =
|
||||
let
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
|
||||
module PostgREST.Config.JSPath
|
||||
( JSPath
|
||||
, JSPathExp(..)
|
||||
, FilterExp(..)
|
||||
, dumpJSPath
|
||||
, pRoleClaimKey
|
||||
) where
|
||||
@@ -14,18 +16,37 @@ import Text.Read (read)
|
||||
import Protolude
|
||||
|
||||
|
||||
-- | full jspath, e.g. .property[0].attr.detail
|
||||
-- | full jspath, e.g. .property[0].attr.detail[?(@ == "role1")]
|
||||
type JSPath = [JSPathExp]
|
||||
|
||||
-- | jspath expression, e.g. .property, .property[0] or ."property-dash"
|
||||
-- NOTE: We only accept one JSPFilter expr (at the end of input)
|
||||
-- | jspath expression
|
||||
data JSPathExp
|
||||
= JSPKey Text
|
||||
| JSPIdx Int
|
||||
= JSPKey Text -- .property or ."property-dash"
|
||||
| JSPIdx Int -- [0]
|
||||
| JSPFilter FilterExp -- [?(@ == "match")]
|
||||
|
||||
data FilterExp
|
||||
= EqualsCond Text
|
||||
| NotEqualsCond Text
|
||||
| StartsWithCond Text
|
||||
| EndsWithCond Text
|
||||
| ContainsCond Text
|
||||
|
||||
dumpJSPath :: JSPathExp -> Text
|
||||
-- TODO: this needs to be quoted properly for special chars
|
||||
dumpJSPath (JSPKey k) = "." <> show k
|
||||
dumpJSPath (JSPIdx i) = "[" <> show i <> "]"
|
||||
dumpJSPath (JSPFilter cond) = "[?(@" <> expr <> ")]"
|
||||
where
|
||||
expr =
|
||||
case cond of
|
||||
EqualsCond text -> " == " <> show text
|
||||
NotEqualsCond text -> " != " <> show text
|
||||
StartsWithCond text -> " ^== " <> show text
|
||||
EndsWithCond text -> " ==^ " <> show text
|
||||
ContainsCond text -> " *== " <> show text
|
||||
|
||||
|
||||
-- Used for the config value "role-claim-key"
|
||||
pRoleClaimKey :: Text -> Either Text JSPath
|
||||
@@ -33,19 +54,47 @@ pRoleClaimKey selStr =
|
||||
mapLeft show $ P.parse pJSPath ("failed to parse role-claim-key value (" <> toS selStr <> ")") (toS selStr)
|
||||
|
||||
pJSPath :: P.Parser JSPath
|
||||
pJSPath = toJSPath <$> (period *> pPath `P.sepBy` period <* P.eof)
|
||||
where
|
||||
toJSPath :: [(Text, Maybe Int)] -> JSPath
|
||||
toJSPath = concatMap (\(key, idx) -> JSPKey key : maybeToList (JSPIdx <$> idx))
|
||||
period = P.char '.' <?> "period (.)"
|
||||
pPath :: P.Parser (Text, Maybe Int)
|
||||
pPath = (,) <$> pJSPKey <*> P.optionMaybe pJSPIdx
|
||||
pJSPath = P.many1 pJSPathExp <* P.eof
|
||||
|
||||
pJSPKey :: P.Parser Text
|
||||
pJSPKey = toS <$> P.many1 (P.alphaNum <|> P.oneOf "_$@") <|> pQuotedValue <?> "attribute name [a..z0..9_$@])"
|
||||
pJSPathExp :: P.Parser JSPathExp
|
||||
pJSPathExp = pJSPKey <|> pJSPFilter <|> pJSPIdx
|
||||
|
||||
pJSPIdx :: P.Parser Int
|
||||
pJSPIdx = P.char '[' *> (read <$> P.many1 P.digit) <* P.char ']' <?> "array index [0..n]"
|
||||
pJSPKey :: P.Parser JSPathExp
|
||||
pJSPKey = do
|
||||
P.char '.'
|
||||
val <- toS <$> P.many1 (P.alphaNum <|> P.oneOf "_$@") <|> pQuotedValue
|
||||
return (JSPKey val) <?> "pJSPKey: JSPath attribute key"
|
||||
|
||||
pJSPIdx :: P.Parser JSPathExp
|
||||
pJSPIdx = do
|
||||
P.char '['
|
||||
num <- read <$> P.many1 P.digit
|
||||
P.char ']'
|
||||
return (JSPIdx num) <?> "pJSPIdx: JSPath array index"
|
||||
|
||||
pJSPFilter :: P.Parser JSPathExp
|
||||
pJSPFilter = do
|
||||
P.try $ P.string "[?("
|
||||
condition <- pFilterConditionParser
|
||||
P.char ')'
|
||||
P.char ']'
|
||||
P.eof -- this should be the last jspath expression
|
||||
return (JSPFilter condition) <?> "pJSPFilter: JSPath filter exp"
|
||||
|
||||
pFilterConditionParser :: P.Parser FilterExp
|
||||
pFilterConditionParser = do
|
||||
P.char '@'
|
||||
P.spaces
|
||||
filt <- matchOperator
|
||||
P.spaces
|
||||
filt <$> pQuotedValue
|
||||
where
|
||||
matchOperator =
|
||||
P.try (P.string "==^" $> EndsWithCond)
|
||||
<|> P.try (P.string "==" $> EqualsCond)
|
||||
<|> P.try (P.string "!=" $> NotEqualsCond)
|
||||
<|> P.try (P.string "^==" $> StartsWithCond)
|
||||
<|> P.try (P.string "*==" $> ContainsCond)
|
||||
|
||||
pQuotedValue :: P.Parser Text
|
||||
pQuotedValue = toS <$> (P.char '"' *> P.many (P.noneOf "\"") <* P.char '"')
|
||||
|
||||
@@ -3,17 +3,10 @@
|
||||
module PostgREST.Config.PgVersion
|
||||
( PgVersion(..)
|
||||
, minimumPgVersion
|
||||
, pgVersion96
|
||||
, pgVersion100
|
||||
, pgVersion109
|
||||
, pgVersion110
|
||||
, pgVersion112
|
||||
, pgVersion114
|
||||
, pgVersion120
|
||||
, pgVersion121
|
||||
, pgVersion130
|
||||
, pgVersion140
|
||||
, pgVersion150
|
||||
, pgVersion170
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
@@ -33,28 +26,7 @@ instance Ord PgVersion where
|
||||
|
||||
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
||||
minimumPgVersion :: PgVersion
|
||||
minimumPgVersion = pgVersion96
|
||||
|
||||
pgVersion96 :: PgVersion
|
||||
pgVersion96 = PgVersion 90600 "9.6" "9.6"
|
||||
|
||||
pgVersion100 :: PgVersion
|
||||
pgVersion100 = PgVersion 100000 "10" "10"
|
||||
|
||||
pgVersion109 :: PgVersion
|
||||
pgVersion109 = PgVersion 100009 "10.9" "10.9"
|
||||
|
||||
pgVersion110 :: PgVersion
|
||||
pgVersion110 = PgVersion 110000 "11.0" "11.0"
|
||||
|
||||
pgVersion112 :: PgVersion
|
||||
pgVersion112 = PgVersion 110002 "11.2" "11.2"
|
||||
|
||||
pgVersion114 :: PgVersion
|
||||
pgVersion114 = PgVersion 110004 "11.4" "11.4"
|
||||
|
||||
pgVersion120 :: PgVersion
|
||||
pgVersion120 = PgVersion 120000 "12.0" "12.0"
|
||||
minimumPgVersion = pgVersion121
|
||||
|
||||
pgVersion121 :: PgVersion
|
||||
pgVersion121 = PgVersion 120001 "12.1" "12.1"
|
||||
@@ -67,3 +39,6 @@ pgVersion140 = PgVersion 140000 "14.0" "14.0"
|
||||
|
||||
pgVersion150 :: PgVersion
|
||||
pgVersion150 = PgVersion 150000 "15.0" "15.0"
|
||||
|
||||
pgVersion170 :: PgVersion
|
||||
pgVersion170 = PgVersion 170000 "17.0" "17.0"
|
||||
|
||||
@@ -8,21 +8,27 @@ Description : PostgREST error HTTP responses
|
||||
module PostgREST.Error
|
||||
( errorResponseFor
|
||||
, ApiRequestError(..)
|
||||
, QPError(..)
|
||||
, RangeError(..)
|
||||
, SchemaCacheError(..)
|
||||
, PgError(..)
|
||||
, Error(..)
|
||||
, JwtError (..)
|
||||
, JwtDecodeError(..)
|
||||
, JwtClaimsError(..)
|
||||
, errorPayload
|
||||
, status
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.FuzzySet as Fuzzy
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.Map.Internal as M
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Data.Text.Encoding.Error as T
|
||||
import qualified Hasql.Pool as SQL
|
||||
import qualified Hasql.Session as SQL
|
||||
import qualified Network.HTTP.Types.Status as HTTP
|
||||
@@ -32,12 +38,8 @@ import Network.Wai (Response, responseLBS)
|
||||
|
||||
import Network.HTTP.Types.Header (Header)
|
||||
|
||||
import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
||||
QPError (..),
|
||||
RaiseError (..),
|
||||
RangeError (..))
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import qualified PostgREST.MediaType as MediaType
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import qualified PostgREST.MediaType as MediaType
|
||||
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||
Schema)
|
||||
@@ -47,10 +49,11 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
RelationshipsMap)
|
||||
import PostgREST.SchemaCache.Routine (Routine (..),
|
||||
RoutineParam (..))
|
||||
import PostgREST.SchemaCache.Table (Table (..))
|
||||
import Protolude
|
||||
|
||||
|
||||
class (JSON.ToJSON a) => PgrstError a where
|
||||
class (ErrorBody a, JSON.ToJSON a) => PgrstError a where
|
||||
status :: a -> HTTP.Status
|
||||
headers :: a -> [Header]
|
||||
|
||||
@@ -59,200 +62,272 @@ class (JSON.ToJSON a) => PgrstError a where
|
||||
|
||||
errorResponseFor :: a -> Response
|
||||
errorResponseFor err =
|
||||
let baseHeader = MediaType.toContentType MTApplicationJSON in
|
||||
responseLBS (status err) (baseHeader : headers err) $ errorPayload err
|
||||
let
|
||||
baseHeader = MediaType.toContentType MTApplicationJSON
|
||||
cLHeader body = (,) "Content-Length" (show $ LBS.length body) :: Header
|
||||
in
|
||||
responseLBS (status err) (baseHeader : cLHeader (errorPayload err) : headers err) $ errorPayload err
|
||||
|
||||
class ErrorBody a where
|
||||
code :: a -> Text
|
||||
message :: a -> Text
|
||||
details :: a -> Maybe JSON.Value
|
||||
hint :: a -> Maybe JSON.Value
|
||||
|
||||
data ApiRequestError
|
||||
= AggregatesNotAllowed
|
||||
| MediaTypeError [ByteString]
|
||||
| InvalidBody ByteString
|
||||
| InvalidFilters
|
||||
| InvalidPreferences [ByteString]
|
||||
| InvalidRange RangeError
|
||||
| InvalidRpcMethod ByteString
|
||||
| NotEmbedded Text
|
||||
| NotImplemented Text
|
||||
| PutLimitNotAllowedError
|
||||
| QueryParamError QPError
|
||||
| RelatedOrderNotToOne Text Text
|
||||
| UnacceptableFilter Text
|
||||
| UnacceptableSchema Text [Text]
|
||||
| UnsupportedMethod ByteString
|
||||
| GucHeadersError
|
||||
| GucStatusError
|
||||
| PutMatchingPkError
|
||||
| SingularityError Integer
|
||||
| PGRSTParseError RaiseError
|
||||
| MaxAffectedViolationError Integer
|
||||
| InvalidResourcePath
|
||||
| OpenAPIDisabled
|
||||
| MaxAffectedRpcViolation
|
||||
deriving Show
|
||||
|
||||
data QPError = QPError Text Text
|
||||
deriving Show
|
||||
|
||||
data RaiseError
|
||||
= MsgParseError ByteString
|
||||
| DetParseError ByteString
|
||||
| NoDetail
|
||||
deriving Show
|
||||
|
||||
data RangeError
|
||||
= NegativeLimit
|
||||
| LowerGTUpper
|
||||
| OutOfBounds Text Text
|
||||
deriving Show
|
||||
|
||||
instance PgrstError ApiRequestError where
|
||||
status AggregatesNotAllowed{} = HTTP.status400
|
||||
status AmbiguousRelBetween{} = HTTP.status300
|
||||
status AmbiguousRpc{} = HTTP.status300
|
||||
status MediaTypeError{} = HTTP.status406
|
||||
status InvalidBody{} = HTTP.status400
|
||||
status InvalidFilters = HTTP.status405
|
||||
status InvalidPreferences{} = HTTP.status400
|
||||
status InvalidRpcMethod{} = HTTP.status405
|
||||
status InvalidRange{} = HTTP.status416
|
||||
status NotFound = HTTP.status404
|
||||
|
||||
status NoRelBetween{} = HTTP.status400
|
||||
status NoRpc{} = HTTP.status404
|
||||
status NotEmbedded{} = HTTP.status400
|
||||
status NotImplemented{} = HTTP.status400
|
||||
status PutLimitNotAllowedError = HTTP.status400
|
||||
status QueryParamError{} = HTTP.status400
|
||||
status RelatedOrderNotToOne{} = HTTP.status400
|
||||
status SpreadNotToOne{} = HTTP.status400
|
||||
status UnacceptableFilter{} = HTTP.status400
|
||||
status UnacceptableSchema{} = HTTP.status406
|
||||
status UnsupportedMethod{} = HTTP.status405
|
||||
status LimitNoOrderError = HTTP.status400
|
||||
status ColumnNotFound{} = HTTP.status400
|
||||
status GucHeadersError = HTTP.status500
|
||||
status GucStatusError = HTTP.status500
|
||||
status OffLimitsChangesError{} = HTTP.status400
|
||||
status PutMatchingPkError = HTTP.status400
|
||||
status SingularityError{} = HTTP.status406
|
||||
status PGRSTParseError{} = HTTP.status500
|
||||
status MaxAffectedViolationError{} = HTTP.status400
|
||||
status InvalidResourcePath = HTTP.status404
|
||||
status OpenAPIDisabled = HTTP.status404
|
||||
status MaxAffectedRpcViolation = HTTP.status400
|
||||
|
||||
headers _ = mempty
|
||||
|
||||
toJsonPgrstError :: ErrorCode -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value
|
||||
toJsonPgrstError code msg details hint = JSON.object [
|
||||
"code" .= code
|
||||
, "message" .= msg
|
||||
, "details" .= details
|
||||
, "hint" .= hint
|
||||
]
|
||||
-- Error codes:
|
||||
--
|
||||
-- Error codes are grouped by common modules or characteristics
|
||||
-- New group of errors will be added at the end of all the groups and will have the next prefix in the sequence
|
||||
-- Keep the "PGRST" prefix in every code for an easier search/grep
|
||||
-- They are grouped as following:
|
||||
--
|
||||
-- PGRST0xx -> Connection Error
|
||||
-- PGRST1xx -> ApiRequest Error
|
||||
-- PGRST2xx -> SchemaCache Error
|
||||
-- PGRST3xx -> JWT authentication Error
|
||||
-- PGRSTXxx -> Internal Hasql Error
|
||||
|
||||
instance JSON.ToJSON ApiRequestError where
|
||||
toJSON (QueryParamError (QPError message details)) = toJsonPgrstError
|
||||
ApiRequestErrorCode00 message (Just (JSON.String details)) Nothing
|
||||
instance ErrorBody ApiRequestError where
|
||||
-- CODE: Text
|
||||
code QueryParamError{} = "PGRST100"
|
||||
code InvalidRpcMethod{} = "PGRST101"
|
||||
code InvalidBody{} = "PGRST102"
|
||||
code InvalidRange{} = "PGRST103"
|
||||
-- code ParseRequestError = "PGRST104" -- no longer used
|
||||
code InvalidFilters = "PGRST105"
|
||||
code UnacceptableSchema{} = "PGRST106"
|
||||
code MediaTypeError{} = "PGRST107"
|
||||
code NotEmbedded{} = "PGRST108"
|
||||
-- code LimitNoOrderError = "PGRST109" -- no longer used
|
||||
-- code OffLimitsChangesError = "PGRST110" -- no longer used
|
||||
code GucHeadersError = "PGRST111"
|
||||
code GucStatusError = "PGRST112"
|
||||
-- code BinaryFieldError = "PGRST113" -- no longer used
|
||||
code PutLimitNotAllowedError = "PGRST114"
|
||||
code PutMatchingPkError = "PGRST115"
|
||||
code SingularityError{} = "PGRST116"
|
||||
code UnsupportedMethod{} = "PGRST117"
|
||||
code RelatedOrderNotToOne{} = "PGRST118"
|
||||
-- code SpreadNotToOne = "PGRST109" -- no longer used
|
||||
code UnacceptableFilter{} = "PGRST120"
|
||||
code PGRSTParseError{} = "PGRST121"
|
||||
code InvalidPreferences{} = "PGRST122"
|
||||
code AggregatesNotAllowed = "PGRST123"
|
||||
code MaxAffectedViolationError{} = "PGRST124"
|
||||
code InvalidResourcePath = "PGRST125"
|
||||
code OpenAPIDisabled = "PGRST126"
|
||||
code NotImplemented{} = "PGRST127"
|
||||
code MaxAffectedRpcViolation = "PGRST128"
|
||||
|
||||
toJSON (InvalidRpcMethod method) = toJsonPgrstError
|
||||
ApiRequestErrorCode01 ("Cannot use the " <> T.decodeUtf8 method <> " method on RPC") Nothing Nothing
|
||||
-- MESSAGE: Text
|
||||
message (QueryParamError (QPError msg _)) = msg
|
||||
message (InvalidRpcMethod method) = "Cannot use the " <> T.decodeUtf8 method <> " method on RPC"
|
||||
message (InvalidBody errorMessage) = T.decodeUtf8 errorMessage
|
||||
message (InvalidRange _) = "Requested range not satisfiable"
|
||||
message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators"
|
||||
message (UnacceptableSchema sch _) = "Invalid schema: " <> sch
|
||||
message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)
|
||||
message (NotEmbedded resource) = "'" <> resource <> "' is not an embedded resource in this request"
|
||||
message GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value"
|
||||
message GucStatusError = "response.status guc must be a valid status code"
|
||||
message PutLimitNotAllowedError = "limit/offset querystring parameters are not allowed for PUT"
|
||||
message PutMatchingPkError = "Payload values do not match URL in primary key column(s)"
|
||||
message (SingularityError _) = "Cannot coerce the result to a single JSON object"
|
||||
message (UnsupportedMethod method) = "Unsupported HTTP method: " <> T.decodeUtf8 method
|
||||
message (RelatedOrderNotToOne _ target) = "A related order on '" <> target <> "' is not possible"
|
||||
message (UnacceptableFilter target) = "Bad operator on the '" <> target <> "' embedded resource"
|
||||
message (PGRSTParseError _) = "Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error"
|
||||
message (InvalidPreferences _) = "Invalid preferences given with handling=strict"
|
||||
message AggregatesNotAllowed = "Use of aggregate functions is not allowed"
|
||||
message (MaxAffectedViolationError _) = "Query result exceeds max-affected preference constraint"
|
||||
message InvalidResourcePath = "Invalid path specified in request URL"
|
||||
message OpenAPIDisabled = "Root endpoint metadata is disabled"
|
||||
message (NotImplemented _) = "Feature not implemented"
|
||||
message MaxAffectedRpcViolation = "Function must return SETOF or TABLE when max-affected preference is used with handling=strict"
|
||||
|
||||
toJSON (InvalidBody errorMessage) = toJsonPgrstError
|
||||
ApiRequestErrorCode02 (T.decodeUtf8 errorMessage) Nothing Nothing
|
||||
|
||||
toJSON (InvalidRange rangeError) = toJsonPgrstError
|
||||
ApiRequestErrorCode03
|
||||
"Requested range not satisfiable"
|
||||
(Just $ case rangeError of
|
||||
-- DETAILS: Maybe JSON.Value
|
||||
details (QueryParamError (QPError _ dets)) = Just $ JSON.String dets
|
||||
details (InvalidRange rangeError) = Just $
|
||||
case rangeError of
|
||||
NegativeLimit -> "Limit should be greater than or equal to zero."
|
||||
LowerGTUpper -> "The lower boundary must be lower than or equal to the upper boundary in the Range header."
|
||||
OutOfBounds lower total -> JSON.String $ "An offset of " <> lower <> " was requested, but there are only " <> total <> " rows.")
|
||||
Nothing
|
||||
OutOfBounds lower total -> JSON.String $ "An offset of " <> lower <> " was requested, but there are only " <> total <> " rows."
|
||||
details (SingularityError n) = Just $ JSON.String $ T.unwords ["The result contains", show n, "rows"]
|
||||
details (RelatedOrderNotToOne origin target) = Just $ JSON.String $ "'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship"
|
||||
details (UnacceptableFilter _) = Just "Only is null or not is null filters are allowed on embedded resources"
|
||||
details (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorDetails raiseErr
|
||||
details (InvalidPreferences prefs) = Just $ JSON.String $ T.decodeUtf8 ("Invalid preferences: " <> BS.intercalate ", " prefs)
|
||||
details (MaxAffectedViolationError n) = Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"]
|
||||
details (NotImplemented details') = Just $ JSON.String details'
|
||||
|
||||
toJSON InvalidFilters = toJsonPgrstError
|
||||
ApiRequestErrorCode05 "Filters must include all and only primary key columns with 'eq' operators" Nothing Nothing
|
||||
details _ = Nothing
|
||||
|
||||
toJSON (UnacceptableSchema schemas) = toJsonPgrstError
|
||||
ApiRequestErrorCode06 ("The schema must be one of the following: " <> T.intercalate ", " schemas) Nothing Nothing
|
||||
-- HINT: Maybe JSON.Value
|
||||
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
|
||||
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
|
||||
hint (UnacceptableSchema _ schemas) = Just $ JSON.String $ "Only the following schemas are exposed: " <> T.intercalate ", " schemas
|
||||
|
||||
toJSON (MediaTypeError cts) = toJsonPgrstError
|
||||
ApiRequestErrorCode07 ("None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)) Nothing Nothing
|
||||
hint _ = Nothing
|
||||
|
||||
toJSON NotFound = JSON.object []
|
||||
instance JSON.ToJSON ApiRequestError where
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
toJSON (NotEmbedded resource) = toJsonPgrstError
|
||||
ApiRequestErrorCode08
|
||||
("'" <> resource <> "' is not an embedded resource in this request")
|
||||
Nothing
|
||||
(Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter.")
|
||||
data SchemaCacheError
|
||||
= AmbiguousRelBetween Text Text [Relationship]
|
||||
| AmbiguousRpc [Routine]
|
||||
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
|
||||
| ColumnNotFound Text Text
|
||||
| TableNotFound Text Text [Table]
|
||||
deriving Show
|
||||
|
||||
toJSON LimitNoOrderError = toJsonPgrstError
|
||||
ApiRequestErrorCode09 "A 'limit' was applied without an explicit 'order'" Nothing (Just "Apply an 'order' using unique column(s)")
|
||||
instance PgrstError SchemaCacheError where
|
||||
status AmbiguousRelBetween{} = HTTP.status300
|
||||
status AmbiguousRpc{} = HTTP.status300
|
||||
status NoRelBetween{} = HTTP.status400
|
||||
status NoRpc{} = HTTP.status404
|
||||
status ColumnNotFound{} = HTTP.status400
|
||||
status TableNotFound{} = HTTP.status404
|
||||
|
||||
toJSON (OffLimitsChangesError n maxs) = toJsonPgrstError
|
||||
ApiRequestErrorCode10
|
||||
"The maximum number of rows allowed to change was surpassed"
|
||||
(Just $ JSON.String $ T.unwords ["Results contain", show n, "rows changed but the maximum number allowed is", show maxs])
|
||||
Nothing
|
||||
headers _ = mempty
|
||||
|
||||
toJSON GucHeadersError = toJsonPgrstError
|
||||
ApiRequestErrorCode11 "response.headers guc must be a JSON array composed of objects with a single key and a string value" Nothing Nothing
|
||||
instance ErrorBody SchemaCacheError where
|
||||
code NoRelBetween{} = "PGRST200"
|
||||
code AmbiguousRelBetween{} = "PGRST201"
|
||||
code NoRpc{} = "PGRST202"
|
||||
code AmbiguousRpc{} = "PGRST203"
|
||||
code ColumnNotFound{} = "PGRST204"
|
||||
code TableNotFound{} = "PGRST205"
|
||||
|
||||
toJSON GucStatusError = toJsonPgrstError
|
||||
ApiRequestErrorCode12 "response.status guc must be a valid status code" Nothing Nothing
|
||||
|
||||
toJSON PutLimitNotAllowedError = toJsonPgrstError
|
||||
ApiRequestErrorCode14 "limit/offset querystring parameters are not allowed for PUT" Nothing Nothing
|
||||
|
||||
toJSON PutMatchingPkError = toJsonPgrstError
|
||||
ApiRequestErrorCode15 "Payload values do not match URL in primary key column(s)" Nothing Nothing
|
||||
|
||||
toJSON (SingularityError n) = toJsonPgrstError
|
||||
ApiRequestErrorCode16
|
||||
"JSON object requested, multiple (or no) rows returned"
|
||||
(Just $ JSON.String $ T.unwords ["The result contains", show n, "rows"])
|
||||
Nothing
|
||||
|
||||
toJSON (UnsupportedMethod method) = toJsonPgrstError
|
||||
ApiRequestErrorCode17 ("Unsupported HTTP method: " <> T.decodeUtf8 method) Nothing Nothing
|
||||
|
||||
toJSON (RelatedOrderNotToOne origin target) = toJsonPgrstError
|
||||
ApiRequestErrorCode18
|
||||
("A related order on '" <> target <> "' is not possible")
|
||||
(Just $ JSON.String $ "'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship")
|
||||
Nothing
|
||||
|
||||
toJSON (SpreadNotToOne origin target) = toJsonPgrstError
|
||||
ApiRequestErrorCode19
|
||||
("A spread operation on '" <> target <> "' is not possible")
|
||||
(Just $ JSON.String $ "'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship")
|
||||
Nothing
|
||||
|
||||
toJSON (UnacceptableFilter target) = toJsonPgrstError
|
||||
ApiRequestErrorCode20
|
||||
("Bad operator on the '" <> target <> "' embedded resource")
|
||||
(Just "Only is null or not is null filters are allowed on embedded resources")
|
||||
Nothing
|
||||
|
||||
toJSON (PGRSTParseError raiseErr) = toJsonPgrstError
|
||||
ApiRequestErrorCode21
|
||||
"Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error"
|
||||
(Just $ JSON.String $ pgrstParseErrorDetails raiseErr)
|
||||
(Just $ JSON.String $ pgrstParseErrorHint raiseErr)
|
||||
|
||||
toJSON (InvalidPreferences prefs) = toJsonPgrstError
|
||||
ApiRequestErrorCode22
|
||||
"Invalid preferences given with handling=strict"
|
||||
(Just $ JSON.String $ T.decodeUtf8 ("Invalid preferences: " <> BS.intercalate ", " prefs))
|
||||
Nothing
|
||||
|
||||
toJSON AggregatesNotAllowed = toJsonPgrstError
|
||||
ApiRequestErrorCode23 "Use of aggregate functions is not allowed" Nothing Nothing
|
||||
|
||||
toJSON (MaxAffectedViolationError n) = toJsonPgrstError
|
||||
ApiRequestErrorCode24
|
||||
"Query result exceeds max-affected preference constraint"
|
||||
(Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"])
|
||||
Nothing
|
||||
|
||||
toJSON (NoRelBetween parent child embedHint schema allRels) = toJsonPgrstError
|
||||
SchemaCacheErrorCode00
|
||||
("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache")
|
||||
(Just $ JSON.String $ "Searched for a foreign key relationship between '" <> parent <> "' and '" <> child <> maybe mempty ("' using the hint '" <>) embedHint <> "' in the schema '" <> schema <> "', but no matches were found.")
|
||||
(JSON.String <$> noRelBetweenHint parent child schema allRels)
|
||||
|
||||
toJSON (AmbiguousRelBetween parent child rels) = toJsonPgrstError
|
||||
SchemaCacheErrorCode01
|
||||
("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'")
|
||||
(Just $ JSON.toJSONList (compressedRel <$> rels))
|
||||
(Just $ JSON.String $ "Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key.")
|
||||
|
||||
toJSON (NoRpc schema procName argumentKeys hasPreferSingleObject contentType isInvPost allProcs overloadedProcs) =
|
||||
let func = schema <> "." <> procName
|
||||
message (NoRelBetween parent child _ _ _) = "Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache"
|
||||
message (AmbiguousRelBetween parent child _) = "Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'"
|
||||
message (NoRpc schema procName argumentKeys contentType isInvPost _ _) = "Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache"
|
||||
where
|
||||
onlySingleParams = isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream]
|
||||
func = schema <> "." <> procName
|
||||
prms = T.intercalate ", " argumentKeys
|
||||
prmsMsg = "(" <> prms <> ")"
|
||||
fmtPrms p = if null argumentKeys then " without parameters" else p
|
||||
message (AmbiguousRpc procs) = "Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs]
|
||||
message (ColumnNotFound rel col) = "Could not find the '" <> col <> "' column of '" <> rel <> "' in the schema cache"
|
||||
message (TableNotFound schemaName relName _) = "Could not find the table '" <> schemaName <> "." <> relName <> "' in the schema cache"
|
||||
|
||||
details (NoRelBetween parent child embedHint schema _) = Just $ JSON.String $ "Searched for a foreign key relationship between '" <> parent <> "' and '" <> child <> maybe mempty ("' using the hint '" <>) embedHint <> "' in the schema '" <> schema <> "', but no matches were found."
|
||||
details (AmbiguousRelBetween _ _ rels) = Just $ JSON.toJSONList (compressedRel <$> rels)
|
||||
details (NoRpc schema procName argumentKeys contentType isInvPost _ _) =
|
||||
Just $ JSON.String $ "Searched for the function " <> func <>
|
||||
(case (isInvPost, contentType) of
|
||||
(True, MTTextPlain) -> " with a single unnamed text parameter"
|
||||
(True, MTTextXML) -> " with a single unnamed xml parameter"
|
||||
(True, MTOctetStream) -> " with a single unnamed bytea parameter"
|
||||
(True, MTApplicationJSON) -> fmtPrms prmsDet <> " or with a single unnamed json/jsonb parameter"
|
||||
_ -> fmtPrms prmsDet
|
||||
) <> ", but no matches were found in the schema cache."
|
||||
where
|
||||
func = schema <> "." <> procName
|
||||
prms = T.intercalate ", " argumentKeys
|
||||
prmsDet = " with parameter" <> (if length argumentKeys > 1 then "s " else " ") <> prms
|
||||
fmtPrms p = if null argumentKeys then " without parameters" else p
|
||||
onlySingleParams = hasPreferSingleObject || (isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream])
|
||||
in toJsonPgrstError
|
||||
SchemaCacheErrorCode02
|
||||
("Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache")
|
||||
(Just $ JSON.String $ "Searched for the function " <> func <>
|
||||
(case (hasPreferSingleObject, isInvPost, contentType) of
|
||||
(True, _, _) -> " with a single json/jsonb parameter"
|
||||
(_, True, MTTextPlain) -> " with a single unnamed text parameter"
|
||||
(_, True, MTTextXML) -> " with a single unnamed xml parameter"
|
||||
(_, True, MTOctetStream) -> " with a single unnamed bytea parameter"
|
||||
(_, True, MTApplicationJSON) -> fmtPrms prmsDet <> " or with a single unnamed json/jsonb parameter"
|
||||
_ -> fmtPrms prmsDet) <>
|
||||
", but no matches were found in the schema cache.")
|
||||
-- The hint will be null in the case of single unnamed parameter functions
|
||||
(if onlySingleParams
|
||||
|
||||
details _ = Nothing
|
||||
|
||||
hint (NoRelBetween parent child _ schema allRels) = JSON.String <$> noRelBetweenHint parent child schema allRels
|
||||
hint (AmbiguousRelBetween _ child rels) = Just $ JSON.String $ "Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key."
|
||||
-- The hint will be null in the case of single unnamed parameter functions
|
||||
hint (NoRpc schema procName argumentKeys contentType isInvPost allProcs overloadedProcs) =
|
||||
if onlySingleParams
|
||||
then Nothing
|
||||
else JSON.String <$> noRpcHint schema procName argumentKeys allProcs overloadedProcs)
|
||||
else JSON.String <$> noRpcHint schema procName argumentKeys allProcs overloadedProcs
|
||||
where
|
||||
onlySingleParams = isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream]
|
||||
hint (AmbiguousRpc _) = Just "Try renaming the parameters or the function itself in the database so function overloading can be resolved"
|
||||
hint (TableNotFound schemaName relName tbls) = JSON.String <$> tableNotFoundHint schemaName relName tbls
|
||||
|
||||
toJSON (AmbiguousRpc procs) = toJsonPgrstError
|
||||
SchemaCacheErrorCode03
|
||||
("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs])
|
||||
Nothing
|
||||
(Just "Try renaming the parameters or the function itself in the database so function overloading can be resolved")
|
||||
hint _ = Nothing
|
||||
|
||||
toJSON (ColumnNotFound relName colName) = toJsonPgrstError
|
||||
SchemaCacheErrorCode04 ("Could not find the '" <> colName <> "' column of '" <> relName <> "' in the schema cache") Nothing Nothing
|
||||
instance JSON.ToJSON SchemaCacheError where
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
toJsonPgrstError :: Text -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value
|
||||
toJsonPgrstError code' message' details' hint' = JSON.object [
|
||||
"code" .= code'
|
||||
, "message" .= message'
|
||||
, "details" .= details'
|
||||
, "hint" .= hint'
|
||||
]
|
||||
|
||||
-- |
|
||||
-- If no relationship is found then:
|
||||
@@ -351,6 +426,16 @@ noRpcHint schema procName params allProcs overloadedProcs =
|
||||
| null overloadedProcs = Fuzzy.getOne fuzzySetOfProcs procName
|
||||
| otherwise = (procName <>) <$> Fuzzy.getOne fuzzySetOfParams (listToText params)
|
||||
|
||||
-- |
|
||||
-- Do a fuzzy search in all tables in the same schema and return closest result
|
||||
tableNotFoundHint :: Text -> Text -> [Table] -> Maybe Text
|
||||
tableNotFoundHint schema tblName tblList
|
||||
= fmap (\tbl -> "Perhaps you meant the table '" <> schema <> "." <> tbl <> "'") perhapsTable
|
||||
where
|
||||
perhapsTable = Fuzzy.getOne fuzzyTableSet tblName
|
||||
fuzzyTableSet = Fuzzy.fromList [ tableName tbl | tbl <- tblList, tableSchema tbl == schema]
|
||||
|
||||
|
||||
compressedRel :: Relationship -> JSON.Value
|
||||
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
|
||||
compressedRel ComputedRelationship{} = JSON.object mempty
|
||||
@@ -403,6 +488,8 @@ pgrstParseErrorHint err = case err of
|
||||
_ -> "DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'."
|
||||
|
||||
data PgError = PgError Authenticated SQL.UsageError
|
||||
deriving Show
|
||||
|
||||
type Authenticated = Bool
|
||||
|
||||
instance PgrstError PgError where
|
||||
@@ -420,48 +507,81 @@ instance PgrstError PgError where
|
||||
then [("WWW-Authenticate", "Bearer") :: Header]
|
||||
else mempty
|
||||
|
||||
proxyStatusHeader :: Text -> Header
|
||||
proxyStatusHeader code' = ("Proxy-Status", "PostgREST; error=" <> T.encodeUtf8 code')
|
||||
|
||||
instance JSON.ToJSON PgError where
|
||||
toJSON (PgError _ usageError) = JSON.toJSON usageError
|
||||
toJSON (PgError _ usageError) = toJsonPgrstError
|
||||
(code usageError) (message usageError) (details usageError) (hint usageError)
|
||||
|
||||
instance ErrorBody PgError where
|
||||
code (PgError _ usageError) = code usageError
|
||||
message (PgError _ usageError) = message usageError
|
||||
details (PgError _ usageError) = details usageError
|
||||
hint (PgError _ usageError) = hint usageError
|
||||
|
||||
instance JSON.ToJSON SQL.UsageError where
|
||||
toJSON (SQL.ConnectionUsageError e) = toJsonPgrstError
|
||||
ConnectionErrorCode00
|
||||
"Database connection error. Retrying the connection."
|
||||
(Just $ JSON.String $ T.decodeUtf8With T.lenientDecode $ fromMaybe "" e)
|
||||
Nothing
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
toJSON (SQL.SessionUsageError e) = JSON.toJSON e -- SQL.Error
|
||||
instance ErrorBody SQL.UsageError where
|
||||
code (SQL.ConnectionUsageError _) = "PGRST000"
|
||||
code (SQL.SessionUsageError (SQL.QueryError _ _ e)) = code e
|
||||
code SQL.AcquisitionTimeoutUsageError = "PGRST003"
|
||||
|
||||
toJSON SQL.AcquisitionTimeoutUsageError = toJsonPgrstError
|
||||
ConnectionErrorCode03 "Timed out acquiring connection from connection pool." Nothing Nothing
|
||||
message (SQL.ConnectionUsageError _) = "Database connection error. Retrying the connection."
|
||||
message (SQL.SessionUsageError (SQL.QueryError _ _ e)) = message e
|
||||
message SQL.AcquisitionTimeoutUsageError = "Timed out acquiring connection from connection pool."
|
||||
|
||||
instance JSON.ToJSON SQL.QueryError where
|
||||
toJSON (SQL.QueryError _ _ e) = JSON.toJSON e
|
||||
details (SQL.ConnectionUsageError e) = JSON.String . T.decodeUtf8 <$> e
|
||||
details (SQL.SessionUsageError (SQL.QueryError _ _ e)) = details e
|
||||
details SQL.AcquisitionTimeoutUsageError = Nothing
|
||||
|
||||
hint (SQL.ConnectionUsageError _) = Nothing
|
||||
hint (SQL.SessionUsageError (SQL.QueryError _ _ e)) = hint e
|
||||
hint SQL.AcquisitionTimeoutUsageError = Nothing
|
||||
|
||||
instance JSON.ToJSON SQL.CommandError where
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
instance ErrorBody SQL.CommandError where
|
||||
-- Special error raised with code PGRST, to allow full response control
|
||||
toJSON (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p)) =
|
||||
code (SQL.ResultError (SQL.ServerError "PGRST" m d _ _)) =
|
||||
case parseRaisePGRST m d of
|
||||
Right (r, _) -> JSON.object [
|
||||
"code" .= getCode r,
|
||||
"message" .= getMessage r,
|
||||
"details" .= checkMaybe (getDetails r),
|
||||
"hint" .= checkMaybe (getHint r)]
|
||||
Left e -> JSON.toJSON e
|
||||
where
|
||||
checkMaybe = maybe JSON.Null JSON.String
|
||||
Right (r, _) -> getCode r
|
||||
Left e -> code e
|
||||
code (SQL.ResultError (SQL.ServerError c _ _ _ _)) = T.decodeUtf8 c
|
||||
|
||||
toJSON (SQL.ResultError (SQL.ServerError c m d h _p)) = JSON.object [
|
||||
"code" .= (T.decodeUtf8 c :: Text),
|
||||
"message" .= (T.decodeUtf8 m :: Text),
|
||||
"details" .= (fmap T.decodeUtf8 d :: Maybe Text),
|
||||
"hint" .= (fmap T.decodeUtf8 h :: Maybe Text)]
|
||||
code (SQL.ResultError _) = "PGRSTX00" -- Internal Error
|
||||
|
||||
toJSON (SQL.ResultError resultError) = toJsonPgrstError
|
||||
InternalErrorCode00 (show resultError) Nothing Nothing
|
||||
code (SQL.ClientError _) = "PGRST001"
|
||||
|
||||
message (SQL.ResultError (SQL.ServerError "PGRST" m d _ _)) =
|
||||
case parseRaisePGRST m d of
|
||||
Right (r, _) -> getMessage r
|
||||
Left e -> message e
|
||||
message (SQL.ResultError (SQL.ServerError _ m _ _ _)) = T.decodeUtf8 m
|
||||
message (SQL.ResultError resultError) = show resultError -- We never really return this error, because we kill pgrst thread early in App.hs
|
||||
message (SQL.ClientError _) = "Database client error. Retrying the connection."
|
||||
|
||||
details (SQL.ResultError (SQL.ServerError "PGRST" m d _ _)) =
|
||||
case parseRaisePGRST m d of
|
||||
Right (r, _) -> JSON.String <$> getDetails r
|
||||
Left e -> details e
|
||||
details (SQL.ResultError (SQL.ServerError _ _ d _ _)) = JSON.String . T.decodeUtf8 <$> d
|
||||
details (SQL.ClientError d) = JSON.String . T.decodeUtf8 <$> d
|
||||
|
||||
details _ = Nothing
|
||||
|
||||
hint (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p)) =
|
||||
case parseRaisePGRST m d of
|
||||
Right (r, _) -> JSON.String <$> getHint r
|
||||
Left e -> hint e
|
||||
hint (SQL.ResultError (SQL.ServerError _ _ _ h _)) = JSON.String . T.decodeUtf8 <$> h
|
||||
|
||||
hint _ = Nothing
|
||||
|
||||
toJSON (SQL.ClientError d) = toJsonPgrstError
|
||||
ConnectionErrorCode01 "Database client error. Retrying the connection." (JSON.String <$> fmap T.decodeUtf8 d) Nothing
|
||||
|
||||
pgErrorStatus :: Bool -> SQL.UsageError -> HTTP.Status
|
||||
pgErrorStatus _ (SQL.ConnectionUsageError _) = HTTP.status503
|
||||
@@ -482,6 +602,10 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
|
||||
if BS.isSuffixOf "requires a WHERE clause" m
|
||||
then HTTP.status400 -- special case for pg-safeupdate, which we consider as client error
|
||||
else HTTP.status500 -- generic function or view server error, e.g. "more than one row returned by a subquery used as an expression"
|
||||
"22023" -> -- invalid_parameter_value. Catch nonexistent role error, see https://github.com/PostgREST/postgrest/issues/3601
|
||||
if BS.isPrefixOf "role" m && BS.isSuffixOf "does not exist" m
|
||||
then HTTP.status401 -- role in jwt does not exist
|
||||
else HTTP.status400
|
||||
'2':'5':_ -> HTTP.status500 -- invalid tx state
|
||||
'2':'8':_ -> HTTP.status403 -- invalid auth specification
|
||||
'2':'D':_ -> HTTP.status500 -- invalid tx termination
|
||||
@@ -519,41 +643,132 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
|
||||
|
||||
data Error
|
||||
= ApiRequestError ApiRequestError
|
||||
| JwtTokenInvalid Text
|
||||
| JwtTokenMissing
|
||||
| JwtTokenRequired
|
||||
| SchemaCacheErr SchemaCacheError
|
||||
| JwtErr JwtError
|
||||
| NoSchemaCacheError
|
||||
| PgErr PgError
|
||||
deriving Show
|
||||
|
||||
data JwtError
|
||||
= JwtDecodeErr JwtDecodeError
|
||||
| JwtSecretMissing
|
||||
| JwtTokenRequired
|
||||
| JwtClaimsErr JwtClaimsError
|
||||
deriving Show
|
||||
|
||||
data JwtDecodeError
|
||||
= EmptyAuthHeader
|
||||
| UnexpectedParts Int
|
||||
| KeyError Text
|
||||
| BadAlgorithm Text
|
||||
| BadCrypto
|
||||
| UnsupportedTokenType
|
||||
| UnreachableDecodeError
|
||||
deriving Show
|
||||
|
||||
data JwtClaimsError
|
||||
= JWTExpired
|
||||
| JWTNotYetValid
|
||||
| JWTIssuedAtFuture
|
||||
| JWTNotInAudience
|
||||
| ParsingClaimsFailed
|
||||
| ExpClaimNotNumber
|
||||
| NbfClaimNotNumber
|
||||
| IatClaimNotNumber
|
||||
| AudClaimNotStringOrArray
|
||||
deriving Show
|
||||
|
||||
instance PgrstError Error where
|
||||
status (ApiRequestError err) = status err
|
||||
status JwtTokenInvalid{} = HTTP.unauthorized401
|
||||
status JwtTokenMissing = HTTP.status500
|
||||
status JwtTokenRequired = HTTP.unauthorized401
|
||||
status (SchemaCacheErr err) = status err
|
||||
status (JwtErr err) = status err
|
||||
status NoSchemaCacheError = HTTP.status503
|
||||
status (PgErr err) = status err
|
||||
|
||||
headers (ApiRequestError err) = headers err
|
||||
headers (JwtTokenInvalid m) = [invalidTokenHeader m]
|
||||
headers JwtTokenRequired = [requiredTokenHeader]
|
||||
headers (PgErr err) = headers err
|
||||
headers _ = mempty
|
||||
headers (ApiRequestError err) = proxyStatusHeader (code err) : headers err
|
||||
headers (SchemaCacheErr err) = proxyStatusHeader (code err) : headers err
|
||||
headers (JwtErr err) = proxyStatusHeader (code err) : headers err
|
||||
headers (PgErr err) = proxyStatusHeader (code err) : headers err
|
||||
headers err@NoSchemaCacheError = proxyStatusHeader (code err) : mempty
|
||||
|
||||
instance JSON.ToJSON Error where
|
||||
toJSON NoSchemaCacheError = toJsonPgrstError
|
||||
ConnectionErrorCode02 "Could not query the database for the schema cache. Retrying." Nothing Nothing
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
toJSON JwtTokenMissing = toJsonPgrstError
|
||||
JWTErrorCode00 "Server lacks JWT secret" Nothing Nothing
|
||||
instance ErrorBody Error where
|
||||
code (ApiRequestError err) = code err
|
||||
code (SchemaCacheErr err) = code err
|
||||
code (JwtErr err) = code err
|
||||
code NoSchemaCacheError = "PGRST002"
|
||||
code (PgErr err) = code err
|
||||
|
||||
toJSON (JwtTokenInvalid message) = toJsonPgrstError
|
||||
JWTErrorCode01 message Nothing Nothing
|
||||
message (ApiRequestError err) = message err
|
||||
message (SchemaCacheErr err) = message err
|
||||
message (JwtErr err) = message err
|
||||
message NoSchemaCacheError = "Could not query the database for the schema cache. Retrying."
|
||||
message (PgErr err) = message err
|
||||
|
||||
toJSON JwtTokenRequired = toJsonPgrstError
|
||||
JWTErrorCode02 "Anonymous access is disabled" Nothing Nothing
|
||||
details (ApiRequestError err) = details err
|
||||
details (SchemaCacheErr err) = details err
|
||||
details (JwtErr err) = details err
|
||||
details NoSchemaCacheError = Nothing
|
||||
details (PgErr err) = details err
|
||||
|
||||
toJSON (PgErr err) = JSON.toJSON err
|
||||
toJSON (ApiRequestError err) = JSON.toJSON err
|
||||
hint (ApiRequestError err) = hint err
|
||||
hint (SchemaCacheErr err) = hint err
|
||||
hint (JwtErr err) = hint err
|
||||
hint NoSchemaCacheError = Nothing
|
||||
hint (PgErr err) = hint err
|
||||
|
||||
instance PgrstError JwtError where
|
||||
status JwtDecodeErr{} = HTTP.unauthorized401
|
||||
status JwtSecretMissing = HTTP.status500
|
||||
status JwtTokenRequired = HTTP.unauthorized401
|
||||
status JwtClaimsErr{} = HTTP.unauthorized401
|
||||
|
||||
headers e@(JwtDecodeErr _) = [invalidTokenHeader $ message e]
|
||||
headers JwtTokenRequired = [requiredTokenHeader]
|
||||
headers e@(JwtClaimsErr _) = [invalidTokenHeader $ message e]
|
||||
headers _ = mempty
|
||||
|
||||
instance JSON.ToJSON JwtError where
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
instance ErrorBody JwtError where
|
||||
code JwtSecretMissing = "PGRST300"
|
||||
code (JwtDecodeErr _) = "PGRST301"
|
||||
code JwtTokenRequired = "PGRST302"
|
||||
code (JwtClaimsErr _) = "PGRST303"
|
||||
|
||||
message JwtSecretMissing = "Server lacks JWT secret"
|
||||
message (JwtDecodeErr e) = case e of
|
||||
EmptyAuthHeader -> "Empty JWT is sent in Authorization header"
|
||||
UnexpectedParts n -> "Expected 3 parts in JWT; got " <> show n
|
||||
KeyError _ -> "No suitable key or wrong key type"
|
||||
BadAlgorithm _ -> "Wrong or unsupported encoding algorithm"
|
||||
BadCrypto -> "JWT cryptographic operation failed"
|
||||
UnsupportedTokenType -> "Unsupported token type"
|
||||
UnreachableDecodeError -> "JWT couldn't be decoded"
|
||||
message JwtTokenRequired = "Anonymous access is disabled"
|
||||
message (JwtClaimsErr e) = case e of
|
||||
JWTExpired -> "JWT expired"
|
||||
JWTNotYetValid -> "JWT not yet valid"
|
||||
JWTIssuedAtFuture -> "JWT issued at future"
|
||||
JWTNotInAudience -> "JWT not in audience"
|
||||
ParsingClaimsFailed -> "Parsing claims failed"
|
||||
ExpClaimNotNumber -> "The JWT 'exp' claim must be a number"
|
||||
NbfClaimNotNumber -> "The JWT 'nbf' claim must be a number"
|
||||
IatClaimNotNumber -> "The JWT 'iat' claim must be a number"
|
||||
AudClaimNotStringOrArray -> "The JWT 'aud' claim must be a string or an array of strings"
|
||||
|
||||
details (JwtDecodeErr jde) = case jde of
|
||||
KeyError dets -> Just $ JSON.String dets
|
||||
BadAlgorithm dets -> Just $ JSON.String dets
|
||||
_ -> Nothing
|
||||
details _ = Nothing
|
||||
|
||||
hint _ = Nothing
|
||||
|
||||
invalidTokenHeader :: Text -> Header
|
||||
invalidTokenHeader m =
|
||||
@@ -601,98 +816,3 @@ parseRaisePGRST m d = do
|
||||
det <- maybeToRight (PGRSTParseError NoDetail) d
|
||||
detJson <- maybeToRight (PGRSTParseError $ DetParseError det) (JSON.decodeStrict det)
|
||||
return (msgJson, detJson)
|
||||
|
||||
-- Error codes are grouped by common modules or characteristics
|
||||
data ErrorCode
|
||||
-- PostgreSQL connection errors
|
||||
= ConnectionErrorCode00
|
||||
| ConnectionErrorCode01
|
||||
| ConnectionErrorCode02
|
||||
| ConnectionErrorCode03
|
||||
-- API Request errors
|
||||
| ApiRequestErrorCode00
|
||||
| ApiRequestErrorCode01
|
||||
| ApiRequestErrorCode02
|
||||
| ApiRequestErrorCode03
|
||||
-- | ApiRequestErrorCode04 -- no longer used (used to be mapped to ParseRequestError)
|
||||
| ApiRequestErrorCode05
|
||||
| ApiRequestErrorCode06
|
||||
| ApiRequestErrorCode07
|
||||
| ApiRequestErrorCode08
|
||||
| ApiRequestErrorCode09
|
||||
| ApiRequestErrorCode10
|
||||
| ApiRequestErrorCode11
|
||||
-- | ApiRequestErrorCode13 -- no longer used (used to be mapped to BinaryFieldError)
|
||||
| ApiRequestErrorCode12
|
||||
| ApiRequestErrorCode14
|
||||
| ApiRequestErrorCode15
|
||||
| ApiRequestErrorCode16
|
||||
| ApiRequestErrorCode17
|
||||
| ApiRequestErrorCode18
|
||||
| ApiRequestErrorCode19
|
||||
| ApiRequestErrorCode20
|
||||
| ApiRequestErrorCode21
|
||||
| ApiRequestErrorCode22
|
||||
| ApiRequestErrorCode23
|
||||
| ApiRequestErrorCode24
|
||||
-- Schema Cache errors
|
||||
| SchemaCacheErrorCode00
|
||||
| SchemaCacheErrorCode01
|
||||
| SchemaCacheErrorCode02
|
||||
| SchemaCacheErrorCode03
|
||||
| SchemaCacheErrorCode04
|
||||
-- JWT authentication errors
|
||||
| JWTErrorCode00
|
||||
| JWTErrorCode01
|
||||
| JWTErrorCode02
|
||||
-- Internal errors related to the Hasql library
|
||||
| InternalErrorCode00
|
||||
|
||||
instance JSON.ToJSON ErrorCode where
|
||||
toJSON e = JSON.toJSON (buildErrorCode e)
|
||||
|
||||
-- New group of errors will be added at the end of all the groups and will have the next prefix in the sequence
|
||||
-- New errors are added at the end of the group they belong to and will have the next code in the sequence
|
||||
buildErrorCode :: ErrorCode -> Text
|
||||
buildErrorCode code = case code of
|
||||
-- Keep the "PGRST" prefix in every code for an easier search/grep
|
||||
ConnectionErrorCode00 -> "PGRST000"
|
||||
ConnectionErrorCode01 -> "PGRST001"
|
||||
ConnectionErrorCode02 -> "PGRST002"
|
||||
ConnectionErrorCode03 -> "PGRST003"
|
||||
|
||||
ApiRequestErrorCode00 -> "PGRST100"
|
||||
ApiRequestErrorCode01 -> "PGRST101"
|
||||
ApiRequestErrorCode02 -> "PGRST102"
|
||||
ApiRequestErrorCode03 -> "PGRST103"
|
||||
ApiRequestErrorCode05 -> "PGRST105"
|
||||
ApiRequestErrorCode06 -> "PGRST106"
|
||||
ApiRequestErrorCode07 -> "PGRST107"
|
||||
ApiRequestErrorCode08 -> "PGRST108"
|
||||
ApiRequestErrorCode09 -> "PGRST109"
|
||||
ApiRequestErrorCode10 -> "PGRST110"
|
||||
ApiRequestErrorCode11 -> "PGRST111"
|
||||
ApiRequestErrorCode12 -> "PGRST112"
|
||||
ApiRequestErrorCode14 -> "PGRST114"
|
||||
ApiRequestErrorCode15 -> "PGRST115"
|
||||
ApiRequestErrorCode16 -> "PGRST116"
|
||||
ApiRequestErrorCode17 -> "PGRST117"
|
||||
ApiRequestErrorCode18 -> "PGRST118"
|
||||
ApiRequestErrorCode19 -> "PGRST119"
|
||||
ApiRequestErrorCode20 -> "PGRST120"
|
||||
ApiRequestErrorCode21 -> "PGRST121"
|
||||
ApiRequestErrorCode22 -> "PGRST122"
|
||||
ApiRequestErrorCode23 -> "PGRST123"
|
||||
ApiRequestErrorCode24 -> "PGRST124"
|
||||
|
||||
SchemaCacheErrorCode00 -> "PGRST200"
|
||||
SchemaCacheErrorCode01 -> "PGRST201"
|
||||
SchemaCacheErrorCode02 -> "PGRST202"
|
||||
SchemaCacheErrorCode03 -> "PGRST203"
|
||||
SchemaCacheErrorCode04 -> "PGRST204"
|
||||
|
||||
JWTErrorCode00 -> "PGRST300"
|
||||
JWTErrorCode01 -> "PGRST301"
|
||||
JWTErrorCode02 -> "PGRST302"
|
||||
|
||||
InternalErrorCode00 -> "PGRSTX00"
|
||||
|
||||
@@ -21,7 +21,7 @@ import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
|
||||
import qualified Network.Wai as Wai
|
||||
import qualified Network.Wai.Middleware.RequestLogger as Wai
|
||||
|
||||
import Network.HTTP.Types.Status (status400, status500)
|
||||
import Network.HTTP.Types.Status (Status, status400, status500)
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
|
||||
import PostgREST.Config (LogLevel (..))
|
||||
@@ -57,24 +57,27 @@ logWithDebounce loggerState action = do
|
||||
|
||||
-- TODO stop using this middleware to reuse the same "observer" pattern for all our logs
|
||||
middleware :: LogLevel -> (Wai.Request -> Maybe BS.ByteString) -> Wai.Middleware
|
||||
middleware logLevel getAuthRole = case logLevel of
|
||||
LogCrit -> requestLogger (const False)
|
||||
LogError -> requestLogger (>= status500)
|
||||
LogWarn -> requestLogger (>= status400)
|
||||
LogInfo -> requestLogger (const True)
|
||||
LogDebug -> requestLogger (const True)
|
||||
where
|
||||
requestLogger filterStatus = unsafePerformIO $
|
||||
middleware logLevel getAuthRole =
|
||||
unsafePerformIO $
|
||||
Wai.mkRequestLogger Wai.defaultRequestLoggerSettings
|
||||
{ Wai.outputFormat =
|
||||
Wai.ApacheWithSettings $
|
||||
Wai.defaultApacheSettings &
|
||||
Wai.setApacheRequestFilter (\_ res -> filterStatus $ Wai.responseStatus res) &
|
||||
Wai.setApacheRequestFilter (\_ res -> shouldLogResponse logLevel $ Wai.responseStatus res) &
|
||||
Wai.setApacheUserGetter getAuthRole
|
||||
, Wai.autoFlush = True
|
||||
, Wai.destination = Wai.Handle stdout
|
||||
}
|
||||
|
||||
shouldLogResponse :: LogLevel -> Status -> Bool
|
||||
shouldLogResponse logLevel = case logLevel of
|
||||
LogCrit -> const False
|
||||
LogError -> (>= status500)
|
||||
LogWarn -> (>= status400)
|
||||
LogInfo -> const True
|
||||
LogDebug -> const True
|
||||
|
||||
-- All observations are logged except some that depend on the log-level
|
||||
observationLogger :: LoggerState -> LogLevel -> ObservationHandler
|
||||
observationLogger loggerState logLevel obs = case obs of
|
||||
o@(PoolAcqTimeoutObs _) -> do
|
||||
@@ -87,10 +90,16 @@ observationLogger loggerState logLevel obs = case obs of
|
||||
o@(HasqlPoolObs _) -> do
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
PoolRequest ->
|
||||
pure ()
|
||||
PoolRequestFullfilled ->
|
||||
pure ()
|
||||
o@(DBQuery sql status) -> do
|
||||
-- Does not log SQL when it's empty (for OPTIONS requests or for the default OpenAPI output)
|
||||
when (sql /= mempty && shouldLogResponse logLevel status) $ do
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
o@PoolRequest ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
o@PoolRequestFullfilled ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
o ->
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
{-|
|
||||
Module : PostgREST.Logger
|
||||
Description : Metrics based on the Observation module. See Observation.hs.
|
||||
-}
|
||||
module PostgREST.Metrics
|
||||
( init
|
||||
, MetricsState (..)
|
||||
@@ -28,6 +32,7 @@ init configDbPoolSize = do
|
||||
setGauge poolMaxSize (fromIntegral configDbPoolSize)
|
||||
pure $ MetricsState poolTimeouts poolAvailable poolWaiting poolMaxSize schemaCacheLoads schemaCacheQueryTime
|
||||
|
||||
-- Only some observations are used as metrics
|
||||
observationMetrics :: MetricsState -> ObservationHandler
|
||||
observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schemaCacheLoads schemaCacheQueryTime) obs = case obs of
|
||||
(PoolAcqTimeoutObs _) -> do
|
||||
@@ -47,7 +52,7 @@ observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schema
|
||||
SchemaCacheLoadedObs resTime -> do
|
||||
withLabel schemaCacheLoads "SUCCESS" incCounter
|
||||
setGauge schemaCacheQueryTime resTime
|
||||
SchemaCacheErrorObs _ -> do
|
||||
SchemaCacheErrorObs{} -> do
|
||||
withLabel schemaCacheLoads "FAIL" incCounter
|
||||
_ ->
|
||||
pure ()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
module PostgREST.Network
|
||||
( resolveHost
|
||||
) where
|
||||
|
||||
import Data.IP (fromHostAddress, fromHostAddress6)
|
||||
import Data.String (IsString (..))
|
||||
import qualified Network.Socket as NS
|
||||
|
||||
import Protolude
|
||||
|
||||
resolveHost :: NS.Socket -> IO (Maybe Text)
|
||||
resolveHost sock = do
|
||||
sn <- NS.getSocketName sock
|
||||
case sn of
|
||||
NS.SockAddrInet _ hostAddr -> pure $ Just $ fromString $ show $ fromHostAddress hostAddr
|
||||
-- The IPv6 addresses are wrapped in [] brackets. This is done in accordance
|
||||
-- to RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).
|
||||
-- In short, we did this to have a clear separation between the port and host
|
||||
-- because the components of an IPv6 are separated with the ':' character.
|
||||
NS.SockAddrInet6 _ _ hostAddr6 _ -> pure $ Just $ fromString $ "[" ++ show (fromHostAddress6 hostAddr6) ++ "]"
|
||||
_ -> pure Nothing
|
||||