Compare commits
176
Commits
-42
@@ -1,42 +0,0 @@
|
||||
freebsd_instance:
|
||||
image_family: freebsd-14-3
|
||||
|
||||
build_task:
|
||||
# Don't change this name without adjusting .github/workflows/build.yaml
|
||||
name: Build FreeBSD (Stack)
|
||||
install_script: pkg install -y postgresql16-client hs-stack git
|
||||
|
||||
only_if: |
|
||||
$CIRRUS_TAG != '' || $CIRRUS_BRANCH == 'main' || $CIRRUS_BRANCH =~ 'v*' ||
|
||||
changesInclude(
|
||||
'.github/workflows/build.yaml',
|
||||
'.github/actions/artifact-from-cirrus/**',
|
||||
'.cirrus.yml',
|
||||
'postgrest.cabal',
|
||||
'stack.yaml*',
|
||||
'**.hs'
|
||||
)
|
||||
|
||||
stack_cache:
|
||||
folders: /.stack
|
||||
fingerprint_script:
|
||||
- echo $CIRRUS_OS
|
||||
- stack --version
|
||||
- md5sum postgrest.cabal
|
||||
- md5sum stack.yaml.lock
|
||||
|
||||
stack_work_cache:
|
||||
folders: .stack-work
|
||||
fingerprint_script:
|
||||
- echo $CIRRUS_OS
|
||||
- stack --version
|
||||
- md5sum postgrest.cabal
|
||||
- md5sum stack.yaml.lock
|
||||
- find main src -type f -iname '*.hs' -exec md5sum "{}" +
|
||||
|
||||
build_script: |
|
||||
stack build -j 1 --local-bin-path . --copy-bins
|
||||
strip postgrest
|
||||
|
||||
bin_artifacts:
|
||||
path: postgrest
|
||||
@@ -1,2 +0,0 @@
|
||||
# Ignore blame for commit that moved protolude files under src/protolude
|
||||
d4949c633e8172d0e4dd8f5c991eaaae6b48fbb0
|
||||
@@ -0,0 +1,8 @@
|
||||
# TODO: Remove this once a new actionlint release has been cut
|
||||
# and made its way to us through nixpkgs.
|
||||
self-hosted-runner:
|
||||
labels:
|
||||
- macos-15-intel
|
||||
- macos-26
|
||||
- ubuntu-24.04-arm
|
||||
- ubuntu-slim
|
||||
@@ -1,119 +0,0 @@
|
||||
name: Artifact from Cirrus
|
||||
|
||||
description: Waits for a specific Cirrus CI run to complete, then downloads the artifact and uploads it to the current workflow. This will silently succeed if Cirrus CI did not schedule a task within 2 minutes.
|
||||
|
||||
inputs:
|
||||
download:
|
||||
description: Name of Artifact to download from Cirrus CI
|
||||
required: true
|
||||
task:
|
||||
description: Name of Cirrus Task
|
||||
required: true
|
||||
token:
|
||||
description: GitHub Token
|
||||
required: true
|
||||
upload:
|
||||
description: Name of Artifact to upload on GitHub Actions
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- shell: bash
|
||||
run: echo "GH_TOKEN=${{ inputs.token }}" >> "$GITHUB_ENV"
|
||||
- name: Wait for Check Suite to be created
|
||||
id: check-suite
|
||||
env:
|
||||
# GITHUB_SHA does weird things for pull request, so we roll our own:
|
||||
COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
shell: bash
|
||||
run: |
|
||||
get_check_runs_url() {
|
||||
gh api "repos/{owner}/{repo}/commits/${COMMIT}/check-suites" \
|
||||
| jq -r '.check_suites[] | select(.app.slug == "cirrus-ci") | .check_runs_url'
|
||||
}
|
||||
for _ in $(seq 1 12); do
|
||||
check_runs_url="$(get_check_runs_url)"
|
||||
if [ -z "$check_runs_url" ]; then
|
||||
echo "Cirrus CI task has not started, yet. Waiting..."
|
||||
sleep 10
|
||||
else
|
||||
echo "check_runs_url=$check_runs_url" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
>&2 echo "Cirrus CI check suite not found. Is Cirrus CI enabled for this repo?"
|
||||
- name: Find task by name
|
||||
id: find-task
|
||||
if: steps.check-suite.outputs.check_runs_url
|
||||
shell: bash
|
||||
run: |
|
||||
get_number_of_tasks() {
|
||||
gh api "${{ steps.check-suite.outputs.check_runs_url }}" \
|
||||
| jq -r '.check_runs | map(select(.name == "${{ inputs.task }}")) | length'
|
||||
}
|
||||
tasks="$(get_number_of_tasks)"
|
||||
case "$tasks" in
|
||||
0)
|
||||
echo "Task not found, assuming it's skipped intentionally..."
|
||||
exit 0
|
||||
;;
|
||||
1)
|
||||
echo "task_found=1" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
>&2 echo "More than 1 task with the same name found. Don't know what to do..."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
- name: Wait for Cirrus CI to complete task
|
||||
if: steps.find-task.outputs.task_found
|
||||
shell: bash
|
||||
run: |
|
||||
get_conclusion() {
|
||||
gh api "${{ steps.check-suite.outputs.check_runs_url }}" \
|
||||
| jq -r '.check_runs[] | select(.name == "${{ inputs.task }}" and .status == "completed") | .conclusion'
|
||||
}
|
||||
while true; do
|
||||
conclusion="$(get_conclusion)"
|
||||
if [ -z "$conclusion" ]; then
|
||||
echo "Cirrus CI task has not completed, yet. Waiting..."
|
||||
sleep 30
|
||||
else
|
||||
if [ "$conclusion" == "success" ]; then
|
||||
break
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
- name: Download artifact from Cirrus CI
|
||||
if: steps.find-task.outputs.task_found
|
||||
id: download
|
||||
shell: bash
|
||||
run: |
|
||||
get_external_id() {
|
||||
gh api "${{ steps.check-suite.outputs.check_runs_url }}" \
|
||||
| jq -er '.check_runs[] | select(.name == "${{ inputs.task }}") | .external_id'
|
||||
}
|
||||
archive="$(mktemp)"
|
||||
artifacts="$(mktemp -d)"
|
||||
until curl --no-progress-meter --fail -o "${archive}" \
|
||||
"https://api.cirrus-ci.com/v1/artifact/task/$(get_external_id)/${{ inputs.download }}.zip"
|
||||
do
|
||||
# This happens when a tag is pushed on the same commit. In this case the
|
||||
# job is immediately marked as "completed" for us, so we end up here after a few
|
||||
# seconds - but the actual Cirrus CI task is still running and didn't produce its artifact, yet.
|
||||
echo "Artifact not found on Cirrus CI, yet. Waiting..."
|
||||
sleep 30
|
||||
done
|
||||
unzip "${archive}" -d "${artifacts}"
|
||||
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
|
||||
- name: Save artifact to GitHub Actions
|
||||
if: steps.find-task.outputs.task_found
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ${{ inputs.upload }}
|
||||
path: ${{ steps.download.outputs.artifacts }}
|
||||
if-no-files-found: error
|
||||
@@ -8,6 +8,7 @@ inputs:
|
||||
required: true
|
||||
save-prs:
|
||||
description: Whether to additionally store the cache in a pull request, too. Should only be used for very small caches.
|
||||
type: boolean
|
||||
prefix:
|
||||
description: Cache key prefix to be used in both primary key and restore-keys.
|
||||
required: true
|
||||
@@ -18,17 +19,17 @@ inputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-
|
||||
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
${{ runner.os }}-${{ inputs.prefix }}-
|
||||
- uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-
|
||||
${{ runner.os }}-${{ inputs.prefix }}-
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
name: Run anywhere
|
||||
|
||||
description: Runs the same code either in a VM or on the bare machine
|
||||
|
||||
inputs:
|
||||
vm:
|
||||
description: Which VM to run on.
|
||||
envs:
|
||||
description: List of relevant environment variables, which might need to be copied into the VM.
|
||||
prepare:
|
||||
description: Code to run in a prepare step, e.g. installing dependencies.
|
||||
run:
|
||||
description: Code to run as the main action.
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- if: ${{ inputs.vm == 'freebsd' }}
|
||||
uses: vmactions/freebsd-vm@5a72679103d223925653750faa878a143340fbd0 # v1.5.0
|
||||
with:
|
||||
envs: ${{ inputs.envs }}
|
||||
prepare: ${{ inputs.prepare }}
|
||||
# Work around https://github.com/vmactions/freebsd-vm/issues/59
|
||||
run: |
|
||||
pw user add -n action -m
|
||||
su action -c '${{ inputs.run }}'
|
||||
- if: ${{ inputs.vm == '' }}
|
||||
name: Prepare
|
||||
shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
|
||||
run: ${{ inputs.prepare }}
|
||||
- if: ${{ inputs.vm == '' }}
|
||||
name: Run
|
||||
shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
|
||||
run: ${{ inputs.run }}
|
||||
@@ -11,12 +11,12 @@ inputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
|
||||
- uses: nixbuild/nix-quick-install-action@9f63be77f412a248c9d9a65a4c82cf066cdf8f0c # v35
|
||||
with:
|
||||
nix_conf: |-
|
||||
always-allow-substitutes = true
|
||||
max-jobs = auto
|
||||
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
with:
|
||||
name: postgrest
|
||||
authToken: ${{ inputs.authToken }}
|
||||
|
||||
@@ -4,9 +4,6 @@ codecov:
|
||||
|
||||
comment: false
|
||||
|
||||
github_checks:
|
||||
annotations: true
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"matchBaseBranches": [
|
||||
"/^v[0-9]+/"
|
||||
],
|
||||
"matchManagers": [
|
||||
"haskell-cabal"
|
||||
],
|
||||
@@ -23,6 +26,44 @@
|
||||
"/^v[0-9]+/"
|
||||
],
|
||||
"groupName": "all dependencies"
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"haskell-cabal"
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"base",
|
||||
"bytestring",
|
||||
"containers",
|
||||
"directory",
|
||||
"mtl",
|
||||
"parsec",
|
||||
"process",
|
||||
"text"
|
||||
],
|
||||
"groupName": "GHC dependencies"
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"haskell-cabal"
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"hasql",
|
||||
"hasql-dynamic-statements",
|
||||
"hasql-notifications",
|
||||
"hasql-transaction",
|
||||
"hasql-pool"
|
||||
],
|
||||
"groupName": "hasql"
|
||||
},
|
||||
{
|
||||
"matchManagers": [
|
||||
"haskell-cabal"
|
||||
],
|
||||
"matchPackageNames": [
|
||||
"fuzzyset"
|
||||
],
|
||||
"allowedVersions": "<0.3"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ on:
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-slim
|
||||
# It triggers only when PR is already merged on either:
|
||||
#
|
||||
# - The merge event itself (action != labeled) or
|
||||
@@ -28,9 +28,9 @@ jobs:
|
||||
# This actions creates the github token using the postgrest app secrets
|
||||
- name: Create Github App Token
|
||||
id: app-token
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
app-id: ${{ vars.POSTGREST_CI_APP_ID }}
|
||||
client-id: ${{ vars.POSTGREST_CI_APP_ID }}
|
||||
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
|
||||
permission-contents: write
|
||||
permission-pull-requests: write
|
||||
@@ -38,14 +38,14 @@ jobs:
|
||||
|
||||
# This is required for backport action to cherry-pick the PR
|
||||
- name: Fetch PR ref
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
|
||||
# Backport action that creates the PR with given settings
|
||||
- name: Create backport PR
|
||||
uses: korthout/backport-action@7c3f6cd5843cac11bc59a04a1b7699af93261670 # v4.5
|
||||
uses: korthout/backport-action@66065406958f46e82238fd59546f5a99e69e22aa # v4.5
|
||||
with:
|
||||
github_token: ${{ steps.app-token.outputs.token }}
|
||||
pull_description: 'Backport for #${pull_number}.'
|
||||
|
||||
@@ -31,20 +31,10 @@ concurrency:
|
||||
|
||||
jobs:
|
||||
static:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux aarch64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
artifact: aarch64
|
||||
- name: Linux x86-64
|
||||
runs-on: ubuntu-24.04
|
||||
artifact: x86-64
|
||||
name: Nix - ${{ matrix.name }} static
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
name: Nix - Linux x86-64 static
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -55,25 +45,25 @@ jobs:
|
||||
- name: Save built executable as artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: postgrest-linux-static-${{ matrix.artifact }}
|
||||
name: postgrest-linux-static-x86-64
|
||||
path: result/bin/postgrest
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Build Docker image
|
||||
run: nix-build -A docker.image --out-link postgrest-docker-${{ matrix.artifact }}.tar.gz
|
||||
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
|
||||
- name: Save built Docker image as artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: postgrest-docker-${{ matrix.artifact }}
|
||||
path: postgrest-docker-${{ matrix.artifact }}.tar.gz
|
||||
name: postgrest-docker-x86-64
|
||||
path: postgrest-docker.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
macos:
|
||||
name: Nix - MacOS
|
||||
runs-on: macos-15
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -93,64 +83,69 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: FreeBSD x86-64
|
||||
runs-on: ubuntu-24.04
|
||||
vm: freebsd
|
||||
artifact: postgrest-freebsd-x86-64
|
||||
deps: pkg install -y postgresql16-client hs-stack
|
||||
|
||||
- name: Linux aarch64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
artifact: postgrest-ubuntu-aarch64
|
||||
deps: sudo apt-get update && sudo apt-get install libpq-dev
|
||||
|
||||
- name: MacOS aarch64
|
||||
runs-on: macos-14
|
||||
cache: |
|
||||
~/.stack/pantry
|
||||
~/.stack/snapshots
|
||||
~/.stack/stack.sqlite3
|
||||
artifact: postgrest-macos-aarch64
|
||||
deps: brew link --force libpq
|
||||
|
||||
- name: MacOS x86-64
|
||||
runs-on: macos-15-intel
|
||||
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: |
|
||||
C:\sr\pantry
|
||||
C:\sr\snapshots
|
||||
C:\sr\stack.sqlite3
|
||||
deps: Add-Content $env:GITHUB_PATH $env:PGBIN
|
||||
artifact: postgrest-windows-x86-64
|
||||
|
||||
name: Stack - ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
env:
|
||||
# Putting .stack in the working directory helps with moving this in and out of the FreeBSD VM.
|
||||
STACK_ROOT: ${{ github.workspace }}/.stack
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- if: ${{ !matrix.vm }}
|
||||
uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
|
||||
with:
|
||||
# This must match the version in stack.yaml's resolver
|
||||
ghc-version: 9.10.3
|
||||
ghc-version: 9.6.7
|
||||
enable-stack: true
|
||||
stack-no-global: true
|
||||
stack-setup-ghc: true
|
||||
- name: Cache ~/.stack
|
||||
- name: Cache .stack
|
||||
uses: ./.github/actions/cache-on-main
|
||||
with:
|
||||
path: ${{ matrix.cache }}
|
||||
prefix: stack
|
||||
path: .stack
|
||||
prefix: ${{ matrix.vm }}${{ matrix.vm && '-' }}stack
|
||||
suffix: ${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
|
||||
- name: Cache .stack-work
|
||||
uses: ./.github/actions/cache-on-main
|
||||
with:
|
||||
path: .stack-work
|
||||
save-prs: true
|
||||
prefix: stack-work-${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
|
||||
prefix: ${{ matrix.vm }}${{ matrix.vm && '-' }}stack-work-${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
|
||||
suffix: ${{ hashFiles('main/**/*.hs', 'src/**/*.hs') }}
|
||||
- name: Install dependencies
|
||||
if: matrix.deps
|
||||
run: ${{ matrix.deps }}
|
||||
- name: Build with Stack
|
||||
run: stack build --lock-file error-on-write --local-bin-path result --copy-bins
|
||||
- name: Strip Executable
|
||||
run: strip result/postgrest*
|
||||
uses: ./.github/actions/run-anywhere
|
||||
with:
|
||||
vm: ${{ matrix.vm }}
|
||||
envs: STACK_ROOT
|
||||
prepare: ${{ matrix.deps }}
|
||||
run: |
|
||||
stack build --lock-file error-on-write --local-bin-path result --copy-bins
|
||||
strip result/postgrest*
|
||||
- name: Save built executable as artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
@@ -161,28 +156,15 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
freebsd:
|
||||
name: Stack - FreeBSD from CirrusCI
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: ./.github/actions/artifact-from-cirrus
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
task: Build FreeBSD (Stack)
|
||||
download: bin
|
||||
upload: postgrest-freebsd-x86-64
|
||||
|
||||
|
||||
cabal:
|
||||
strategy:
|
||||
matrix:
|
||||
ghc: ['9.10.3', '9.12.3']
|
||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
|
||||
with:
|
||||
ghc-version: ${{ matrix.ghc }}
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
name: Lint & Style
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
name: Commit
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 100 # fetch history (last 100 commits) instead of default shallow clone history, this is deemed enough for a PR history
|
||||
- name: Setup Nix Environment
|
||||
|
||||
@@ -41,16 +41,15 @@ jobs:
|
||||
concurrency:
|
||||
# Never tag outdated commits on the main branch by skipping superseded commits
|
||||
group: ci-tag-${{ (github.ref == 'refs/heads/main' && github.ref) || github.run_id }}
|
||||
# TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved
|
||||
cancel-in-progress: false
|
||||
cancel-in-progress: true
|
||||
if: vars.RELEASE_ENABLED
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-slim
|
||||
needs:
|
||||
- docs
|
||||
- test
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
|
||||
- name: Tag latest commit
|
||||
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
name: Build
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
name: Spellcheck
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
|
||||
@@ -7,12 +7,37 @@ on:
|
||||
|
||||
jobs:
|
||||
linkcheck:
|
||||
name: Linkcheck
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.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
|
||||
|
||||
- name: Run Linkcheck
|
||||
id: linkcheck
|
||||
run: postgrest-docs-linkcheck
|
||||
|
||||
# This actions creates the github token using the postgrest app secrets
|
||||
- name: Create Github App Token (Runs only on linkcheck failure)
|
||||
id: app-token
|
||||
if: ${{ failure() && steps.linkcheck.outcome == 'failure' }} # only create the token on linkcheck failure
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.POSTGREST_CI_APP_ID }}
|
||||
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
|
||||
permission-issues: write # required for commenting on issues
|
||||
|
||||
- name: Notify on linkcheck failure by commenting on GH Issue 4106
|
||||
if: ${{ failure() && steps.linkcheck.outcome == 'failure' }}
|
||||
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||
with:
|
||||
token: ${{ steps.app-token.outputs.token }}
|
||||
issue-number: 4106
|
||||
body: |
|
||||
**Linkcheck Job Failed!**
|
||||
|
||||
A broken link was detected in the docs. Please check the [failed run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
|
||||
|
||||
@@ -9,8 +9,7 @@ on:
|
||||
concurrency:
|
||||
# Terminate all previous runs of the same workflow for the same tag.
|
||||
group: release-${{ github.ref }}
|
||||
# TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved
|
||||
cancel-in-progress: false
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -20,13 +19,15 @@ jobs:
|
||||
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
|
||||
|
||||
prepare:
|
||||
name: Prepare
|
||||
runs-on: ubuntu-24.04
|
||||
github:
|
||||
name: GitHub
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-slim
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Check the version to be released
|
||||
run: |
|
||||
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||
@@ -48,23 +49,7 @@ jobs:
|
||||
|
||||
echo "Relevant extract from CHANGELOG.md:"
|
||||
cat CHANGES.md
|
||||
- name: Save CHANGES.md as artifact
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: release-changes
|
||||
path: CHANGES.md
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
github:
|
||||
name: GitHub
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- prepare
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
@@ -75,9 +60,6 @@ jobs:
|
||||
|
||||
mkdir -p release-bundle
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-aarch64.tar.xz" \
|
||||
-C artifacts/postgrest-linux-static-aarch64 postgrest
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-x86-64.tar.xz" \
|
||||
-C artifacts/postgrest-linux-static-x86-64 postgrest
|
||||
|
||||
@@ -90,6 +72,9 @@ jobs:
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \
|
||||
-C artifacts/postgrest-freebsd-x86-64 postgrest
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-ubuntu-aarch64.tar.xz" \
|
||||
-C artifacts/postgrest-ubuntu-aarch64 postgrest
|
||||
|
||||
zip --junk-paths "release-bundle/postgrest-${GITHUB_REF_NAME}-windows-x86-64.zip" \
|
||||
artifacts/postgrest-windows-x86-64/postgrest.exe
|
||||
|
||||
@@ -116,14 +101,14 @@ jobs:
|
||||
gh release edit devel \
|
||||
-t devel \
|
||||
--verify-tag \
|
||||
-F artifacts/release-changes/CHANGES.md \
|
||||
-F CHANGES.md \
|
||||
--prerelease
|
||||
gh release upload --clobber devel release-bundle/*
|
||||
else
|
||||
gh release create "${GITHUB_REF_NAME}" \
|
||||
-t "${GITHUB_REF_NAME}" \
|
||||
--verify-tag \
|
||||
-F artifacts/release-changes/CHANGES.md \
|
||||
-F CHANGES.md \
|
||||
release-bundle/*
|
||||
fi
|
||||
|
||||
@@ -132,62 +117,63 @@ jobs:
|
||||
name: Docker Hub
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs:
|
||||
- prepare
|
||||
- github
|
||||
if: |
|
||||
vars.DOCKER_REPO && vars.DOCKER_USER
|
||||
env:
|
||||
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- name: Download aarch64 Docker image
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: postgrest-docker-aarch64
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Download x86-64 Docker image
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: postgrest-docker-x86-64
|
||||
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
- name: Download aarch64 binary
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: postgrest-ubuntu-aarch64
|
||||
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
username: ${{ vars.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
- name: Build aarch64 Docker image
|
||||
run: |
|
||||
# This only pushes the image via digest, not a tag. This will not appear
|
||||
# in the image list on Docker Hub, yet. It will be later added to the main
|
||||
# tag's manifest.
|
||||
docker buildx build \
|
||||
-t "$DOCKER_REPO/postgrest" \
|
||||
--platform linux/arm64 \
|
||||
--output push-by-digest=true,type=image,push=true \
|
||||
--metadata-file metadata.json \
|
||||
.
|
||||
echo "SHA256_ARM=$(jq -r '."containerimage.digest"' metadata.json)" >> "$GITHUB_ENV"
|
||||
- name: Publish images on Docker Hub
|
||||
run: |
|
||||
docker load -i postgrest-docker-aarch64.tar.gz
|
||||
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64"
|
||||
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64"
|
||||
docker load -i postgrest-docker.tar.gz
|
||||
|
||||
docker load -i postgrest-docker-x86-64.tar.gz
|
||||
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64"
|
||||
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64"
|
||||
|
||||
docker manifest create "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}" \
|
||||
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64" \
|
||||
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64"
|
||||
docker manifest push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
|
||||
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
|
||||
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
|
||||
docker buildx imagetools create --append \
|
||||
-t "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}" \
|
||||
"$DOCKER_REPO/postgrest@$SHA256_ARM"
|
||||
|
||||
# Only tag 'latest' for full releases
|
||||
if [ "${GITHUB_REF_NAME}" != "devel" ]; then
|
||||
echo "Pushing to 'latest' tag for full release of ${GITHUB_REF_NAME} ..."
|
||||
docker manifest create "$DOCKER_REPO/postgrest:latest" \
|
||||
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64" \
|
||||
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64"
|
||||
docker manifest push "$DOCKER_REPO/postgrest:latest"
|
||||
docker tag postgrest:latest "$DOCKER_REPO"/postgrest:latest
|
||||
docker push "$DOCKER_REPO"/postgrest:latest
|
||||
docker buildx imagetools create --append \
|
||||
-t "$DOCKER_REPO/postgrest:latest" \
|
||||
"$DOCKER_REPO/postgrest@$SHA256_ARM"
|
||||
else
|
||||
echo "Skipping push to 'latest' tag for pre-release..."
|
||||
fi
|
||||
|
||||
|
||||
docker-description:
|
||||
name: Docker Hub Description
|
||||
runs-on: ubuntu-24.04
|
||||
if: |
|
||||
vars.DOCKER_REPO && vars.DOCKER_USER &&
|
||||
github.ref == 'refs/tags/devel'
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
|
||||
if: github.ref == 'refs/tags/devel'
|
||||
name: Docker Hub Description
|
||||
with:
|
||||
username: ${{ vars.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -49,10 +49,10 @@ jobs:
|
||||
|
||||
- run: postgrest-cabal-update
|
||||
|
||||
- name: Run coverage (IO tests and Spec tests against latest supported PostgreSQL)
|
||||
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
||||
run: postgrest-coverage
|
||||
- name: Upload coverage to codecov
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
with:
|
||||
files: ./coverage/codecov.json
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -70,7 +70,8 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pgVersion: [14, 15, 16, 17, 18]
|
||||
# Latest version is tested via `coverage` above.
|
||||
pgVersion: [13, 14, 15, 16]
|
||||
name: PG ${{ matrix.pgVersion }}
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
@@ -79,7 +80,7 @@ jobs:
|
||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -109,7 +110,7 @@ jobs:
|
||||
name: Memory
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -124,12 +125,13 @@ jobs:
|
||||
|
||||
loadtest:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
kind: ['mixed', 'errors', 'jwt-hs', 'jwt-hs-cache', 'jwt-hs-cache-worst', 'jwt-rsa', 'jwt-rsa-cache', 'jwt-rsa-cache-worst']
|
||||
kind: ['mixed', 'jwt-hs', 'jwt-hs-cache', 'jwt-hs-cache-worst', 'jwt-rsa', 'jwt-rsa-cache', 'jwt-rsa-cache-worst']
|
||||
name: Loadtest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nix Environment
|
||||
@@ -164,7 +166,7 @@ jobs:
|
||||
name: Flake Check
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nix Environment
|
||||
|
||||
@@ -26,4 +26,3 @@ loadtest
|
||||
.docs-build
|
||||
gen_targets.http
|
||||
gen_jwk.json
|
||||
gen_private.json
|
||||
|
||||
+1
-1
@@ -7,4 +7,4 @@ python:
|
||||
build:
|
||||
os: ubuntu-24.04
|
||||
tools:
|
||||
python: "3.12"
|
||||
python: "3.11"
|
||||
|
||||
+27
-27
@@ -4,34 +4,34 @@ All notable changes to this project will be documented in this file. From versio
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
|
||||
- Add string slicing operator for `jwt-role-claim-key` by @taimoorzaeem in #4599
|
||||
- Optimize requests with `Prefer: count=exact` that do not use ranges or `db-max-rows` by @laurenceisla in #3957
|
||||
+ Removed unnecessary double count when building the `Content-Range`.
|
||||
- Add config `client-error-verbosity` to customize error verbosity by @taimoorzaeem in #4088, #3980, #3824
|
||||
- Add `Vary` header to responses by @develop7 in #4609
|
||||
- Add config `db-timezone-enabled` for optional querying of timezones by @taimoorzaeem in #4751
|
||||
- Log schema cache queries timings on `log-level=debug` by @steve-chavez in #4805
|
||||
## [14.14] - 2026-06-29
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix admin server not logging cause of failure by @taimoorzaeem in #5012
|
||||
|
||||
## [14.13] - 2026-06-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix connection retrying message in `PGRST000` error by @netqo in #4980
|
||||
+ Remove redundant "Retrying the connection." from message because it is logged separately
|
||||
- Fix request failures when `work_mem` is set on a role by @laurenceisla in #4955
|
||||
|
||||
## [14.12] - 2026-05-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix race condition in pool_available metric causing negative values during network instability by @mkleczek in #4622
|
||||
|
||||
## [14.11] - 2026-05-04
|
||||
|
||||
### Fixed
|
||||
|
||||
- Shutdown should wait for in flight requests by @mkleczek in #4702
|
||||
- Fix login with uppercase and mixed case role names by @taimoorzaeem in #4678
|
||||
- Remove automatic transaction retries on `40001 (serialization_failure)` errors to prevent replication lag by @laurenceisla in #3673
|
||||
- Fix unexpected results when embedding and filtering the same table more than once by @laurenceisla in #4075
|
||||
|
||||
### Changed
|
||||
|
||||
- Drop support for PostgreSQL EOL version 13 by @wolfgangwalther in #4193
|
||||
- All responses now include a `Vary` header by @develop7 in #4609
|
||||
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
|
||||
+ Now fails at startup. Prior to this, it failed with `PGRST205` on requests related to these schemas.
|
||||
- Build a static executable for aarch64-linux by @wolfgangwalther in #4193
|
||||
- Build the minimal docker image for aarch64-linux by @wolfgangwalther in #4193
|
||||
- The name of an embedded table can no longer be used in filters if it has an alias by @laurenceisla in #4075
|
||||
+ e.g. `?select=alias:table(*)&table.id=eq.1` is not possible anymore, use `?select=alias:table(*)&alias.id=eq.1` instead.
|
||||
- Restore Listener query shape so it can be found in `pg_stat_activity` by @mkleczek in #4857 #4859
|
||||
- The LISTEN channel now automatically recovers when it stops working due to a PostgreSQL bug @laurenceisla in #3147
|
||||
- Fix misleading "Functions" name on schema cache summary in startup logs by @taimoorzaeem in #4821
|
||||
|
||||
## [14.10] - 2026-04-16
|
||||
|
||||
@@ -87,7 +87,7 @@ All notable changes to this project will be documented in this file. From versio
|
||||
|
||||
- Ensure Listener connections are released by @mkleczek in #4614
|
||||
- Fix incorrectly filtering the returned representation for PATCH requests when using `or/and` filters by @laurenceisla in #3707
|
||||
- Fix listener running with exception masked after first failure by @mkleczek in #4615
|
||||
- Fix listener running with exception masked after first failure by @mkleczek #4615
|
||||
|
||||
## [14.3] - 2026-01-03
|
||||
|
||||
@@ -691,7 +691,7 @@ All notable changes to this project will be documented in this file. From versio
|
||||
### Added
|
||||
|
||||
- #1933, #2109, Add a minimal health check endpoint - @steve-chavez
|
||||
+ For enabling this, the `admin-server-port` config must be set explicitly
|
||||
+ For enabling this, the `admin-server-port` config must be set explictly
|
||||
+ A `<host>:<admin_server_port>/live` endpoint is available for checking if postgrest is running on its port/socket. 200 OK = alive, 503 = dead.
|
||||
+ A `<host>:<admin_server_port>/ready` endpoint is available for checking a correct internal state(the database connection plus the schema cache). 200 OK = ready, 503 = not ready.
|
||||
- #1988, Add the current user to the request log on stdout - @DavidLindbom, @wolfgangwalther
|
||||
@@ -1174,7 +1174,7 @@ All notable changes to this project will be documented in this file. From versio
|
||||
- Customize content negotiation per route - @begriffs
|
||||
- Allow using nulls order without explicit order direction - @steve-chavez
|
||||
- Fatal error on postgres unsupported version, format supported version in error message - @steve-chavez
|
||||
- Prevent database memory consumption by prepared statements caches - @ruslantalpa
|
||||
- Prevent database memory cosumption by prepared statements caches - @ruslantalpa
|
||||
- Use specific columns in the RETURNING section - @ruslantalpa
|
||||
- Fix columns alias for RETURNING - @steve-chavez
|
||||
|
||||
|
||||
+14
-21
@@ -1,12 +1,17 @@
|
||||
# Contributing to PostgREST
|
||||
|
||||
## AI Policy
|
||||
**First:** if you're unsure or afraid of _anything_, just ask or
|
||||
submit the issue or pull request anyways. You won't be yelled at
|
||||
for giving your best effort. The worst that can happen is that
|
||||
you'll be politely asked to change something. We appreciate any
|
||||
sort of contributions, and don't want a wall of rules to get in the
|
||||
way of that.
|
||||
|
||||
We adhere to [Gentoo's AI policy](https://wiki.gentoo.org/wiki/Project:Council/AI_policy):
|
||||
|
||||
> It is expressly forbidden to contribute [...] any content that has been created with the assistance of Natural Language Processing artificial intelligence tools. This motion can be revisited, should a case been made over such a tool that does not pose copyright, ethical and quality concerns.
|
||||
|
||||
You can find more about its rationale [here](https://wiki.gentoo.org/wiki/Project:Council/AI_policy#Rationale).
|
||||
However, for those individuals who want a bit more guidance on the
|
||||
best way to contribute to the project, read on. This document will
|
||||
cover what we're looking for. By addressing all the points we're
|
||||
looking for, it raises the chances we can quickly merge or address
|
||||
your contributions.
|
||||
|
||||
## Issues
|
||||
|
||||
@@ -35,12 +40,12 @@ For questions on how to use PostgREST, please use
|
||||
We have a fully nix-based development environment with many tools for a smooth development workflow available.
|
||||
Check the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md) on how to set it up and use it.
|
||||
|
||||
### Haskell Conventions
|
||||
|
||||
* All contributions must pass the tests before being merged. When
|
||||
you create a pull request your code will automatically be tested.
|
||||
|
||||
* All fixes or features must have a test proving the improvement.
|
||||
|
||||
* All code must also pass a [linter](http://community.haskell.org/~ndm/hlint/) and [styler](https://github.com/jaspervdj/stylish-haskell)
|
||||
* All code must also pass [hlint](http://community.haskell.org/~ndm/hlint/) and [stylish-haskell](https://github.com/jaspervdj/stylish-haskell)
|
||||
with no warnings. This helps enforce a uniform style for all committers. Continuous integration will check this as well on every
|
||||
pull request. There are useful tools in the nix-shell that help with checking this locally. You can run `postgrest-check` to do this manually but
|
||||
we recommend adding it to `.git/hooks/pre-commit` as `nix-shell --run postgrest-check` to automatically check this before doing a commit.
|
||||
@@ -48,15 +53,3 @@ Check the [development docs](https://github.com/PostgREST/postgrest/blob/main/ni
|
||||
### Running Tests
|
||||
|
||||
For instructions on running tests, see the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md#testing).
|
||||
|
||||
### Structuring commits in pull requests
|
||||
|
||||
To simplify reviews, make it easy to split pull requests if deemed necessary, and to maintain clean and meaningful history of changes, you will be asked to update your PR if it does not follow the below rules:
|
||||
|
||||
* It must be possible to merge the PR branch into target using `git merge --ff-only`, ie. the source branch must be rebased on top of target.
|
||||
* No merge commits in the source branch.
|
||||
* All commits in the source branch must be self contained, meaning: it should be possible to treat each commit as a separate PR.
|
||||
* Commits in the source branch must contain only related changes (related means the changes target a single problem/goal). For example, any refactorings should be isolated from the actual change implementation into separate commits.
|
||||
* Tests, documentation, and changelog updates should be contained in the same commits as the actual code changes they relate to. An exception to this rule is when test or documentation changes are made in separate PR.
|
||||
* Commit messages must be prefixed with one of the prefixes defined in [the list used by commit verification scripts](https://github.com/PostgREST/postgrest/blob/main/nix/tools/gitTools.nix#L11).
|
||||
* Commit messages should contain a longer description of the purpose of the changes contained in the commit and, for non-trivial changes, a description of the changes themselves.
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
# PostgREST Docker Hub image for aarch64.
|
||||
# The x86-64 is a single-static-binary image built via Nix, see:
|
||||
# nix/tools/docker/README.md
|
||||
|
||||
FROM ubuntu:resolute@sha256:53958ec7b67c2c9355df922dd08dbf0360611f8c3cdb656875e81873db9ffdba AS postgrest
|
||||
|
||||
RUN apt-get update -y \
|
||||
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY postgrest /usr/bin/postgrest
|
||||
RUN chmod +x /usr/bin/postgrest
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
USER 1000
|
||||
|
||||
# Use the array form to avoid running the command using bash, which does not handle `SIGTERM` properly.
|
||||
# See https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop
|
||||
CMD ["postgrest"]
|
||||
@@ -1,4 +1,5 @@
|
||||
Copyright (c) 2014-2026 The PostgREST contributors
|
||||
Copyright (c) 2014 Joe Nelson
|
||||
Copyright (c) 2019 Steve Chavez
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
|
||||
@@ -125,7 +125,7 @@ and limited with - range headers. More about
|
||||
## Data Integrity
|
||||
|
||||
Rather than relying on an Object Relational Mapper and custom
|
||||
imperative coding, this system requires you to put declarative constraints
|
||||
imperative coding, this system requires you put declarative constraints
|
||||
directly into your database. Hence no application can corrupt your
|
||||
data (including your API server).
|
||||
|
||||
|
||||
@@ -1,4 +1,2 @@
|
||||
packages: postgrest.cabal
|
||||
tests: true
|
||||
allow-newer:
|
||||
hasql:postgresql-libpq
|
||||
|
||||
@@ -1 +1 @@
|
||||
index-state: hackage.haskell.org 2026-04-18T18:42:36Z
|
||||
index-state: hackage.haskell.org 2025-10-29T04:02:18Z
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
{ system ? builtins.currentSystem
|
||||
|
||||
, compiler ? "ghc9123"
|
||||
, compiler ? "ghc948"
|
||||
|
||||
, # Commit of the Nixpkgs repository that we want to use.
|
||||
# It defaults to reading the inputs from flake.lock, which serves
|
||||
@@ -44,6 +44,7 @@ let
|
||||
allOverlays.checked-shell-script
|
||||
allOverlays.gitignore
|
||||
(allOverlays.haskell-packages { inherit compiler; })
|
||||
allOverlays.slocat
|
||||
];
|
||||
|
||||
# Evaluated expression of the Nixpkgs repository.
|
||||
@@ -52,11 +53,11 @@ let
|
||||
|
||||
postgresqlVersions =
|
||||
[
|
||||
{ name = "pg-18"; postgresql = pkgs.postgresql_18.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "pg-17"; postgresql = pkgs.postgresql_17.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "pg-16"; postgresql = pkgs.postgresql_16.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "pg-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "pg-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
{ name = "pg-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||
];
|
||||
|
||||
haskellPackages = pkgs.haskell.packages."${compiler}";
|
||||
|
||||
@@ -61,3 +61,5 @@ The image is built from scratch using
|
||||
no commands are listed in the image history. See the [PostgREST
|
||||
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.
|
||||
|
||||
+3
-3
@@ -48,14 +48,14 @@ source_suffix = ".rst"
|
||||
# The master toctree document.
|
||||
master_doc = "index"
|
||||
|
||||
# This is overridden by readthedocs with the version tag anyway
|
||||
version = "devel"
|
||||
# This is overriden by readthedocs with the version tag anyway
|
||||
version = "14"
|
||||
# To avoid repetition in <title> we set this to an empty string.
|
||||
release = ""
|
||||
|
||||
# General information about the project.
|
||||
project = "PostgREST " + version
|
||||
author = "The PostgREST contributors"
|
||||
author = "Joe Nelson, Steve Chavez"
|
||||
copyright = "2017, " + author
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
|
||||
@@ -37,7 +37,6 @@ Example Apps
|
||||
* `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
|
||||
* `fullstack template <https://github.com/jenstroeger/fullstack-webapp-template>`_ - a complete fullstack webapp template using PG as db and message queue, Python and Dramatiq to implement async jobs, db migrations, test runners, and more.
|
||||
* `general <https://github.com/PierreRochard/general>`_ - example auth back-end
|
||||
* `guild-operators <https://github.com/cardano-community/koios-artifacts/tree/main/files/grest>`_ - example queries and functions that the Cardano Community uses for their Guild Operators' Repository
|
||||
* `PostGUI <https://github.com/priyank-purohit/PostGUI>`_ - React Material UI admin panel
|
||||
|
||||
@@ -163,7 +163,7 @@ Another option is to define the function with the :code:`SECURITY DEFINER` optio
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- login as a user which has privileges on the private schemas
|
||||
-- login as a user wich has privileges on the private schemas
|
||||
|
||||
-- create a sample function
|
||||
create or replace function login(email text, pass text, out token text) as $$
|
||||
|
||||
@@ -16,7 +16,7 @@ Supported PostgreSQL versions
|
||||
=============================
|
||||
|
||||
=============== =================================
|
||||
**Supported** PostgreSQL >= 14
|
||||
**Supported** PostgreSQL >= 13
|
||||
=============== =================================
|
||||
|
||||
PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_.
|
||||
@@ -181,23 +181,6 @@ If you want to have a visual overview of your API in your browser you can add sw
|
||||
|
||||
With this you can see the swagger-ui in your browser on port 8080.
|
||||
|
||||
.. _docker_cpu_contraint:
|
||||
|
||||
Docker Resource Constraints
|
||||
---------------------------
|
||||
|
||||
PostgREST does not support ``--cpus`` `constraint option <https://docs.docker.com/engine/containers/resource_constraints/#configure-the-default-cfs-scheduler>`_.
|
||||
|
||||
As a workaround, you may use the `GHC RTS <https://ghc.gitlab.haskell.org/ghc/doc/users_guide/runtime_control.html#runtime-system-rts-options>`_ ``-N`` option. For instance, to limit it to 2 CPU cores, do:
|
||||
|
||||
.. code::
|
||||
|
||||
# Set environment variable GHCRTS set to "-N2"
|
||||
docker run --rm -p 3000:3000 \
|
||||
-e PGRST_DB_URI="postgres://app_user:password@10.0.0.10/postgres" \
|
||||
-e GHCRTS="-N2"
|
||||
postgrest/postgrest
|
||||
|
||||
.. _build_source:
|
||||
|
||||
Building from Source
|
||||
|
||||
@@ -318,144 +318,6 @@ You can insert a new product using a JSON object for the ``extra_info`` column:
|
||||
|
||||
To query and filter the data see :ref:`json_columns` for a complete reference.
|
||||
|
||||
.. _ww_postgis:
|
||||
|
||||
PostGIS
|
||||
-------
|
||||
|
||||
You can use the string representation for `PostGIS <https://postgis.net/>`_ data types such as ``geometry`` or ``geography`` (you need to `install PostGIS <https://postgis.net/documentation/getting_started/>`_ first).
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- Activate the postgis module in the current database
|
||||
create extension if not exists postgis;
|
||||
|
||||
create table coverage (
|
||||
id int primary key,
|
||||
name text unique,
|
||||
area geometry
|
||||
);
|
||||
|
||||
To add areas in polygon format, you can use string representation:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/coverage" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
[
|
||||
{ "id": 1, "name": "small", "area": "SRID=4326;POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))" },
|
||||
{ "id": 2, "name": "big", "area": "SRID=4326;POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))" }
|
||||
]
|
||||
EOF
|
||||
|
||||
Now, when you request the information, PostgREST will automatically cast the ``area`` column into a ``Polygon`` geometry type. Although this is useful, you may need the whole output to be in `GeoJSON <https://geojson.org/>`_ format out of the box, which can be done by including the ``Accept: application/geo+json`` in the request. This will work for PostGIS versions 3.0.0 and up and will return the output as a `FeatureCollection Object <https://www.rfc-editor.org/rfc/rfc7946#section-3.3>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/coverage" \
|
||||
-H "Accept: application/geo+json"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"id": 1,
|
||||
"name": "small"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"id": 2,
|
||||
"name": "big"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
If you need to add an extra property, like the area in square units by using ``st_area(area)``, you could add a generated column to the table and it will appear in the ``properties`` key of each ``Feature``.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
alter table coverage
|
||||
add square_units double precision generated always as ( st_area(area) ) stored;
|
||||
|
||||
In the case that you are using older PostGIS versions, then creating a function is your best option:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function coverage_geo_collection() returns json as $$
|
||||
select
|
||||
json_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'features', json_agg(
|
||||
json_build_object(
|
||||
'type', 'Feature',
|
||||
'geometry', st_AsGeoJSON(c.area)::json,
|
||||
'properties', json_build_object('id', c.id, 'name', c.name)
|
||||
)
|
||||
)
|
||||
)
|
||||
from coverage c;
|
||||
$$ language sql;
|
||||
|
||||
Now this query will return the same results:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/coverage_geo_collection"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"id": 1,
|
||||
"name": "small"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"id": 2,
|
||||
"name": "big"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Ranges
|
||||
------
|
||||
|
||||
@@ -609,3 +471,20 @@ You can use other comparative filters and also all the `PostgreSQL special date/
|
||||
"due_date": "2022-02-27T06:00:00-05:00"
|
||||
}
|
||||
]
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript">
|
||||
let hash = window.location.hash;
|
||||
|
||||
const redirects = {
|
||||
// PostGIS
|
||||
'#postgis': '../integrations/postgis.html#postgis',
|
||||
};
|
||||
|
||||
let willRedirectTo = redirects[hash];
|
||||
|
||||
if (willRedirectTo) {
|
||||
window.location.href = willRedirectTo;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
.. _ww_postgis:
|
||||
|
||||
PostGIS
|
||||
=======
|
||||
|
||||
To work with `PostGIS <https://postgis.net/>`_ data types such as ``geometry`` or ``geography``, you'll need to `install PostGIS <https://postgis.net/documentation/getting_started/>`_ first.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- Activate the postgis module in the current database
|
||||
create extension if not exists postgis;
|
||||
|
||||
create table coverage (
|
||||
id int primary key,
|
||||
name text unique,
|
||||
area geometry
|
||||
);
|
||||
|
||||
insert into coverage (id, name, area) values
|
||||
(1, 'small', ST_GeomFromText('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))',4326)),
|
||||
(2, 'big', ST_GeomFromText('POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))', 4326);
|
||||
|
||||
.. _application/geo+json:
|
||||
|
||||
``application/geo+json``
|
||||
------------------------
|
||||
|
||||
PostgREST supports the `standard <https://www.iana.org/assignments/media-types/application/geo+json>`_ ``application/geo+json`` media type which can be used to get the output in `GeoJSON <https://geojson.org/>`_ format. This will work for PostGIS versions 3.0.0 and up and will return the output as a `FeatureCollection Object <https://www.rfc-editor.org/rfc/rfc7946#section-3.3>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/coverage" \
|
||||
-H "Accept: application/geo+json"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"id": 1,
|
||||
"name": "small"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"id": 2,
|
||||
"name": "big"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Using generated columns
|
||||
-----------------------
|
||||
|
||||
If you need to add an extra property, like the area in square units by using ``st_area(area)``, you could add a generated column to the table and it will appear in the ``properties`` key of each ``Feature``.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
alter table coverage
|
||||
add square_units double precision generated always as ( st_area(area) ) stored;
|
||||
|
||||
In the case that you are using older PostGIS versions, then creating a function is your best option:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function coverage_geo_collection() returns json as $$
|
||||
select
|
||||
json_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'features', json_agg(
|
||||
json_build_object(
|
||||
'type', 'Feature',
|
||||
'geometry', st_AsGeoJSON(c.area)::json,
|
||||
'properties', json_build_object('id', c.id, 'name', c.name)
|
||||
)
|
||||
)
|
||||
)
|
||||
from coverage c;
|
||||
$$ language sql;
|
||||
|
||||
Now this query will return the same results:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/coverage_geo_collection"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"id": 1,
|
||||
"name": "small"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"id": 2,
|
||||
"name": "big"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Using string representation
|
||||
---------------------------
|
||||
|
||||
To insert areas in polygon format, you can use string representation:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/coverage" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
[
|
||||
{ "id": 3, "name": "strip", "area": "SRID=4326;POLYGON((0 0, 50 0, 50 2, 0 2, 0 0))" },
|
||||
{ "id": 4, "name": "diamond", "area": "SRID=4326;POLYGON((5 0, 10 5, 5 10, 0 5, 5 0))" }
|
||||
]
|
||||
EOF
|
||||
|
||||
PostgREST will automatically cast the ``area`` column into a ``Polygon`` geometry type.
|
||||
@@ -4,7 +4,6 @@ API's
|
||||
APIs
|
||||
APISIX
|
||||
AST
|
||||
async
|
||||
aud
|
||||
Auth
|
||||
auth
|
||||
@@ -15,7 +14,6 @@ BOM
|
||||
Bytea
|
||||
Cardano
|
||||
cd
|
||||
CDNs
|
||||
centric
|
||||
CLI
|
||||
CMS
|
||||
@@ -32,7 +30,6 @@ DDL
|
||||
DOM
|
||||
DSL
|
||||
DevOps
|
||||
Dramatiq
|
||||
dockerize
|
||||
enum
|
||||
Enums
|
||||
@@ -44,7 +41,6 @@ EveryLayout
|
||||
filename
|
||||
FreeBSD
|
||||
fts
|
||||
fullstack
|
||||
GeoJSON
|
||||
Github
|
||||
Google
|
||||
@@ -192,7 +188,6 @@ verifier
|
||||
versioning
|
||||
Vondra
|
||||
Vue
|
||||
webapp
|
||||
webhooks
|
||||
websearch
|
||||
Websockets
|
||||
|
||||
@@ -21,7 +21,6 @@ PostgREST exposes three database objects of a schema as resources: tables, views
|
||||
api/aggregate_functions.rst
|
||||
api/openapi.rst
|
||||
api/preferences.rst
|
||||
api/vary_header.rst
|
||||
api/*
|
||||
|
||||
.. raw:: html
|
||||
|
||||
@@ -69,26 +69,6 @@ If the function doesn't modify the database, it will also run under the GET meth
|
||||
|
||||
The function parameter names match the JSON object keys in the POST case, for the GET case they match the query parameters ``?a=1&b=2``.
|
||||
|
||||
If the function is defined to have default values for the parameters then arguments for these parameters can be omitted in the request. For instance:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE FUNCTION greet_user(username TEXT DEFAULT 'guest')
|
||||
RETURNS TEXT AS $$
|
||||
SELECT 'Hello ' || username || '!';
|
||||
$$ LANGUAGE SQL IMMUTABLE;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/rpc/greet_user"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Context-Type: application/json; charset=utf-8
|
||||
|
||||
"Hello guest!"
|
||||
|
||||
.. _function_single_json:
|
||||
|
||||
Functions with an array of JSON objects
|
||||
|
||||
@@ -15,7 +15,7 @@ Using these domains, :ref:`functions <functions>` can become handlers and `user-
|
||||
|
||||
.. important::
|
||||
|
||||
- PostgREST vendor media types (``application/vnd.pgrst.plan``, ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``) cannot be overridden.
|
||||
- PostgREST vendor media types (``application/vnd.pgrst.plan``, ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``) cannot be overriden.
|
||||
- Long media types like ``application/vnd.openxmlformats-officedocument.wordprocessingml.document`` cannot be expressed as domains since they surpass `PostgreSQL identifier length <https://www.postgresql.org/docs/current/limits.html#LIMITS-TABLE>`_.
|
||||
For these you can use the :ref:`any_handler`.
|
||||
|
||||
|
||||
@@ -117,10 +117,6 @@ However, with ``handling=strict``, an invalid time zone preference will throw an
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
|
||||
.. note::
|
||||
|
||||
This feature requires querying `pg_timezone_names <https://www.postgresql.org/docs/current/view-pg-timezone-names.html>`_ during :ref:`schema_cache` load. If this is not desired, you can disable the feature with :ref:`db-timezone-enabled`.
|
||||
|
||||
.. _prefer_return:
|
||||
|
||||
Return Representation
|
||||
|
||||
@@ -1244,7 +1244,7 @@ You can order the correlated arrays explicitly. For example, to order by the fil
|
||||
|
||||
.. warning::
|
||||
|
||||
Aliasing spread columns is recommended since JSON allows duplicate keys. Example:
|
||||
Aliasing spreaded columns is recommended since JSON allows duplicate keys. Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ Builtin handlers are offered for common standard media types.
|
||||
|
||||
* ``text/csv`` and ``application/json``, for all API endpoints. See :ref:`tables_views` and :ref:`functions`.
|
||||
* ``application/openapi+json``, for the root endpoint. See :ref:`open-api`.
|
||||
* ``application/geo+json``, see :ref:`ww_postgis`.
|
||||
* ``application/geo+json``, see :ref:`application/geo+json`.
|
||||
* ``*/*``, resolves to ``application/json`` for API endpoints and to ``application/openapi+json`` for the root endpoint.
|
||||
|
||||
The following vendor media types handlers are also supported.
|
||||
|
||||
@@ -5,10 +5,6 @@ Schemas
|
||||
|
||||
PostgREST can expose a single or multiple schema's tables, views and functions. The :ref:`active database role <roles>` must have the usage privilege on the schemas to access them.
|
||||
|
||||
.. important::
|
||||
|
||||
``pg_catalog`` and ``information_schema`` are not allowed in :ref:`db-schemas`. This is done to prevent leaking sensitive information and hence they cannot be accessed directly. If you wish to expose objects of these schemas, expose another schema that contains wrapper views or functions over ``pg_catalog`` or ``information_schema`` objects.
|
||||
|
||||
Single schema
|
||||
-------------
|
||||
|
||||
|
||||
@@ -639,7 +639,7 @@ However, it can work with surrogate primary keys (e.g. ``id serial primary key``
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/employees?columns=id,name,salary" \
|
||||
curl "http://localhost:3000/employees?colums=id,name,salary" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-H "Prefer: resolution=merge-duplicates, missing=default" \
|
||||
-d @- << EOF
|
||||
|
||||
@@ -51,7 +51,7 @@ You can request table/columns with spaces in them by percent encoding the spaces
|
||||
Reserved characters
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If filters include PostgREST reserved characters(``,``, ``.``, ``:``, ``*``, ``(``, ``)``) you'll have to surround them in percent encoded double quotes ``%22`` for correct processing.
|
||||
If filters include PostgREST reserved characters(``,``, ``.``, ``:``, ``()``) you'll have to surround them in percent encoded double quotes ``%22`` for correct processing.
|
||||
|
||||
Here ``Hebdon,John`` and ``Williams,Mary`` are values.
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
.. _vary_header:
|
||||
|
||||
Vary Header
|
||||
===========
|
||||
|
||||
In order to assist caching proxies and CDNs, PostgREST includes a ``Vary`` header of value
|
||||
``Accept, Prefer, Range`` in its responses which should fit most of the bills. As any other
|
||||
response header, it's available for override
|
||||
by updating ``response.headers`` GUC variable accordingly, for example:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- Override the Vary header to include Accept, Prefer and X-Test-Vary headers
|
||||
perform set_config('response.headers', '[{"Vary": "Accept, Prefer, X-Test-Vary"}]', true);
|
||||
|
||||
In this case PostgREST will use provided value verbatim.
|
||||
@@ -217,7 +217,7 @@ It's recommended to leave the JWT cache enabled as our load tests indicate ~20%
|
||||
|
||||
- If the ``jwt-secret`` is changed and the config is reloaded, the JWT cache will reset.
|
||||
- JWTs that pass :ref:`jwt_signature` are cached, regardless if they pass :ref:`jwt_claims_validation`. We do this to ensure responses stays fast under common failure cases (such as expired JWTs).
|
||||
- You can use the :ref:`server-timing_header` to see the performance benefit of JWT caching.
|
||||
- You can use the :ref:`server-timing_header` to see the peformance benefit of JWT caching.
|
||||
|
||||
.. _jwt_role_extract:
|
||||
|
||||
@@ -234,17 +234,6 @@ The DSL follows the `JSONPath <https://goessner.net/articles/JsonPath/>`_ expres
|
||||
- ``==^`` selects the first array element that ends with the right operand
|
||||
- ``*==`` selects the first array element that contains the right operand
|
||||
|
||||
The selected role value can also be sliced using the slice operator ``[a:b]``. It is similar to `slice operator in python <https://docs.python.org/3/library/functions.html#slice>`_. Negative index values are also supported. The syntax is as:
|
||||
|
||||
- ``[a:b]`` take slice from index ``a`` up to ``b``
|
||||
- ``[a:]`` take slice from index ``a`` to end
|
||||
- ``[:b]`` take slice from start to index ``b``
|
||||
- ``[:]`` select everything, no slicing
|
||||
|
||||
.. important::
|
||||
|
||||
Make sure that you are not taking a slice where the start index comes after the end index like ``[11:2]``. The result of this would be empty string and so no role would get selected.
|
||||
|
||||
Usage examples:
|
||||
|
||||
.. code:: bash
|
||||
@@ -266,11 +255,6 @@ Usage examples:
|
||||
jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]"
|
||||
jwt-role-claim-key = ".postgrest.roles[?(@ *== \"utho\")]"
|
||||
|
||||
# {"postgrest":{"wlcg": ["/groupa", "/groupb/"]}}
|
||||
# skip the "/" character using slice operator
|
||||
jwt-role-claim-key = ".postgrest.wlcg[0][1:]"
|
||||
jwt-role-claim-key = ".postgrest.wlcg[1][1:-1]"
|
||||
|
||||
.. note::
|
||||
|
||||
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>`_.
|
||||
|
||||
@@ -195,33 +195,6 @@ app.settings.*
|
||||
|
||||
The :code:`current_setting` function has `an optional boolean second <https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET>`_ argument to avoid it from raising an error if the value was not defined. Default values to :code:`app.settings` can then be given by combining this argument with :code:`coalesce` and :code:`nullif` : :code:`coalesce(nullif(current_setting('app.settings.my_custom_variable', true), ''), 'default value')`. The use of :code:`nullif` is necessary because if set in a transaction, the setting is sometimes not "rolled back" to :code:`null`. See also :ref:`this section <guc_req_headers_cookies_claims>` for more information on this behaviour.
|
||||
|
||||
.. _client-error-verbosity:
|
||||
|
||||
client-error-verbosity
|
||||
----------------------
|
||||
|
||||
=============== =======================
|
||||
**Type** String
|
||||
**Default** verbose
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_CLIENT_ERROR_VERBOSITY
|
||||
**In-Database** pgrst.client_error_verbosity
|
||||
=============== =======================
|
||||
|
||||
Specifies the verbosity of PostgREST errors. See :ref:`client_error_verbosity`.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
# Return error "code", "message", "details" and "hint"
|
||||
client-error-verbosity = "verbose"
|
||||
|
||||
# Return only "code" and "message"
|
||||
client-error-verbosity = "minimal"
|
||||
|
||||
.. note::
|
||||
|
||||
This setting only affects client side error messages. Server side logs are not affected by this setting.
|
||||
|
||||
.. _db-aggregates-enabled:
|
||||
|
||||
db-aggregates-enabled
|
||||
@@ -291,7 +264,7 @@ db-channel-enabled
|
||||
|
||||
When this is set to :code:`true`, the notification channel specified in :ref:`db-channel` is enabled.
|
||||
|
||||
You should set this to ``false`` when using PostgreSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
||||
You should set this to ``false`` when using PostgresSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
||||
|
||||
.. _db-config:
|
||||
|
||||
@@ -506,7 +479,7 @@ db-prepared-statements
|
||||
|
||||
When disabled, the generated queries will be parameterized (invulnerable to SQL injection) but they will not be prepared (cached in the database session). Not using prepared statements will noticeably decrease performance, so it's recommended to always have this setting enabled.
|
||||
|
||||
You should only set this to ``false`` when using PostgreSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
||||
You should only set this to ``false`` when using PostgresSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
||||
|
||||
.. _db-root-spec:
|
||||
|
||||
@@ -540,21 +513,6 @@ db-schemas
|
||||
|
||||
The list of database schemas to expose to clients. See :ref:`schemas`.
|
||||
|
||||
.. _db-timezone-enabled:
|
||||
|
||||
db-timezone-enabled
|
||||
-------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Boolean
|
||||
**Default** True
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_TIMEZONE_ENABLED
|
||||
**In-Database** pgrst.db_timezone_enabled
|
||||
=============== =================================
|
||||
|
||||
Enables the use of :ref:`prefer_timezone` preference header. Disabled when set to ``false``.
|
||||
|
||||
.. _db-tx-end:
|
||||
|
||||
db-tx-end
|
||||
@@ -568,7 +526,7 @@ db-tx-end
|
||||
**In-Database** pgrst.db_tx_end
|
||||
=============== =================================
|
||||
|
||||
Specifies how to terminate the database transactions. See :ref:`prefer_tx`.
|
||||
Specifies how to terminate the database transactions.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
|
||||
@@ -473,38 +473,3 @@ For example, doing a request on a table with high count (say 30_000_000), we get
|
||||
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.
|
||||
|
||||
.. _client_error_verbosity:
|
||||
|
||||
Client Error Verbosity
|
||||
======================
|
||||
|
||||
For HTTP clients, the error verbosity can be set via :ref:`client-error-verbosity` config.
|
||||
|
||||
With ``verbose``, it returns ``code``, ``message``, ``details`` and ``hint``.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl "localhost:3000/itemsxx"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"code": "PGRST205",
|
||||
"message": "Could not find the table 'public.itemsxx' in the schema cache",
|
||||
"details": "Perhaps you meant the table 'public.items'",
|
||||
"hint": null
|
||||
}
|
||||
|
||||
With ``minimal``, just ``code`` and ``message`` is returned.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
curl "localhost:3000/itemsxx"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"code": "PGRST205",
|
||||
"message": "Could not find the table 'public.itemsxx' in the schema cache"
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ For diagnostic information about the server itself, PostgREST logs to ``stderr``
|
||||
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:08:16:11 -0500: Schema cache loaded 15 Relations, 8 Relationships, 8 RPCs, 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
|
||||
|
||||
|
||||
@@ -3,16 +3,10 @@
|
||||
Schema Cache
|
||||
============
|
||||
|
||||
PostgREST requires metadata from the database to provide a REST API that abstracts SQL details. One example of this is the interface for :ref:`resource_embedding`.
|
||||
PostgREST requires metadata from the database schema to provide a REST API that abstracts SQL details. One example of this is the interface for :ref:`resource_embedding`.
|
||||
|
||||
Getting this metadata requires expensive queries. To avoid repeating this work, PostgREST uses a schema cache.
|
||||
|
||||
.. note::
|
||||
|
||||
- Schema cache queries have been optimized over time to stay fast, even on complex databases. You can see a summary of their execution time in :ref:`pgrst_logging` and :ref:`metrics`.
|
||||
- If the schema cache queries are slow, the most likely cause is *system catalog bloat*, see `issue#3212 <https://github.com/PostgREST/postgrest/issues/3212>`_ for more details.
|
||||
- You can turn the :ref:`log-level` to ``debug`` to see the time of each schema cache query.
|
||||
|
||||
.. _schema_reloading:
|
||||
|
||||
Schema Cache Reloading
|
||||
|
||||
@@ -221,7 +221,7 @@ Notice that the ``response.headers`` should be set to an *array* of single-key o
|
||||
|
||||
.. note::
|
||||
|
||||
PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overridden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you use :ref:`custom_media`.
|
||||
PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overriden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you use :ref:`custom_media`.
|
||||
|
||||
.. _guc_resp_status:
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# This file is auto-generated by postgrest-nixpkgs-upgrade
|
||||
sphinx==9.1.0
|
||||
sphinx==8.2.3
|
||||
sphinx-copybutton==0.5.2
|
||||
sphinx-rtd-dark-mode==1.3.0
|
||||
sphinx-rtd-theme==3.1.0
|
||||
sphinx-tabs==3.5.0
|
||||
sphinxext-opengraph==0.13.0
|
||||
sphinx-rtd-theme==3.0.2
|
||||
sphinx-tabs==3.4.7
|
||||
sphinxext-opengraph==0.9.1
|
||||
@@ -22,7 +22,7 @@ Step 1. Install PostgreSQL
|
||||
|
||||
If you're already familiar with using PostgreSQL and have it installed on your system you can use the existing installation (see :ref:`pg-dependency` for minimum requirements). For this tutorial we'll describe how to use the database in Docker because database configuration is otherwise too complicated for a simple tutorial.
|
||||
|
||||
If Docker is not installed, you can get it `here <https://www.docker.com/get-started>`_. Make sure that Docker service is `started <https://docs.docker.com/engine/daemon/start/#start-the-daemon-using-operating-system-utilities>`_. Next, let's pull and start the database image:
|
||||
If Docker is not installed, you can get it `here <https://www.docker.com/get-started>`_. Next, let's pull and start the database image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
|
||||
Generated
+4
-4
@@ -2,16 +2,16 @@
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1776949667,
|
||||
"narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=",
|
||||
"lastModified": 1752006229,
|
||||
"narHash": "sha256-BeuAPwNM2RBc5bvUTb0j4GRs2yBkDeRCw/8Y3v9Xesc=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30",
|
||||
"rev": "c80edd02003fe3d8af527215a3ac069be9cfd47f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"ref": "nixpkgs-25.05-darwin",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "REST API for any Postgres database";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-25.05-darwin";
|
||||
};
|
||||
|
||||
nixConfig = {
|
||||
@@ -46,9 +46,5 @@
|
||||
meta.description = "REST API for any Postgres database";
|
||||
};
|
||||
});
|
||||
|
||||
devShells = genSystems (postgrest: {
|
||||
default = import ./shell.nix { inherit postgrest; };
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
+6
-6
@@ -91,11 +91,11 @@ 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-pg-14
|
||||
postgrest-hsie-graph-symbols postgrest-with-pg-15
|
||||
postgrest-hsie-minimal-imports postgrest-with-pg-16
|
||||
postgrest-lint postgrest-with-pg-17
|
||||
postgrest-loadtest postgrest-with-pg-18
|
||||
postgrest-hsie-graph-modules postgrest-with-pg-13
|
||||
postgrest-hsie-graph-symbols postgrest-with-pg-14
|
||||
postgrest-hsie-minimal-imports postgrest-with-pg-15
|
||||
postgrest-lint postgrest-with-pg-16
|
||||
postgrest-loadtest postgrest-with-pg-17
|
||||
postgrest-loadtest-against postgrest-with-slow-pg
|
||||
postgrest-loadtest-report postgrest-with-slow-postgrest
|
||||
postgrest-nixpkgs-upgrade
|
||||
@@ -174,7 +174,7 @@ $ nix-shell --run "postgrest-with-all postgrest-test-spec"
|
||||
|
||||
# Run the tests against a specific version of PostgreSQL (use tab-completion in
|
||||
# nix-shell to see all available versions):
|
||||
$ nix-shell --run "postgrest-with-pg-17 postgrest-test-spec"
|
||||
$ nix-shell --run "postgrest-with-pg-13 postgrest-test-spec"
|
||||
|
||||
```
|
||||
|
||||
|
||||
+24
-6
@@ -16,8 +16,11 @@ The following checklist guides you through the complete process in more detail.
|
||||
## Upgrade the pinned version of `nixpkgs`
|
||||
|
||||
The pinned version of [`nixpkgs`](https://github.com/NixOS/nixpkgs) is defined
|
||||
in [`flake.nix`](../flake.nix). To upgrade it, you can use a small utility
|
||||
script defined in [`nix/tools/nixpkgsTools.nix`](tools/nixpkgsTools.nix):
|
||||
in [`nix/nixpkgs-version.nix`](nixpkgs-version.nix). The pin refers directly to
|
||||
a GitHub tarball for the given revision, which is more efficient than pulling
|
||||
the complete Git repository. To upgrade it to the current `main` of
|
||||
`nixpkgs`, you can use a small utility script defined in
|
||||
[`nix/nixpkgs-update.nix`](nixpkgs-update.nix):
|
||||
|
||||
```bash
|
||||
# From the root of the repository, enter nix-shell
|
||||
@@ -27,12 +30,21 @@ nix-shell
|
||||
postgrest-nixpkgs-upgrade
|
||||
|
||||
# Exit the nix-shell with Ctrl-d
|
||||
|
||||
```
|
||||
|
||||
## Review overlays
|
||||
|
||||
Check whether the individual [overlays](overlays) are still required.
|
||||
|
||||
## Check if patches are still required and update them as needed
|
||||
|
||||
We track a number of PostgREST-specific patches in [`nix/patches`](patches).
|
||||
Check whether the pull-requests/issues linked in the
|
||||
[`default.nix`](patches/default.nix) have progressed and remove/modify the
|
||||
patches if they did. If conflicting changes occurred, you might have to rebase
|
||||
the respective patches.
|
||||
|
||||
## Build everything
|
||||
|
||||
Using the PostgREST binary Nix cache is recommended. Install
|
||||
@@ -46,19 +58,25 @@ errors, this is probably due to one of our patches. Try to fix them and re-run
|
||||
|
||||
## Update the PostgREST binary cache
|
||||
|
||||
If you have access to the PostgREST cachix project, you can push the
|
||||
If you have access to the PostgREST cachix signing key, you can push the
|
||||
artifacts that you built locally to the binary cache. This will accelerate the
|
||||
CI builds and tests, sometimes dramatically. This might sometimes even be
|
||||
required to avoid build timeouts in CI.
|
||||
|
||||
You'll need to login with your token with `cachix authtoken <token>`.
|
||||
You'll need to set the `CACHIX_SIGNING_KEY` before proceeding, e.g. by creating
|
||||
a file containing `export CACHIX_SIGNING_KEY=...` and sourcing that file, which
|
||||
avoids having the secret in your shell history.
|
||||
|
||||
To push all new artifacts to Cachix, run:
|
||||
|
||||
```
|
||||
nix-store -qR --include-outputs $$(nix-instantiate) | cachix push postgrest
|
||||
|
||||
# Or, equivalently
|
||||
nix-shell --run postgrest-push-cachix
|
||||
|
||||
```
|
||||
|
||||
The `postgrest-push-cachix` command will query the nix-store to list all
|
||||
dependencies and build artifacts of PostgREST. It will then push
|
||||
The `nix-store` command will query the nix-store to list all dependencies and
|
||||
build artifacts of PostgREST. The `cachix` command will efficiently push
|
||||
everything that is not yet cached to the binary cache.
|
||||
|
||||
+7
-20
@@ -4,7 +4,6 @@
|
||||
{-# LANGUAGE OverloadedStrings #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
{-# LANGUAGE TypeApplications #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
|
||||
-- | Haskell Imports and Exports tool
|
||||
@@ -34,16 +33,13 @@ import Data.Function ((&))
|
||||
import Data.List (intercalate)
|
||||
import Data.Maybe (catMaybes, mapMaybe)
|
||||
import Data.Text (Text)
|
||||
import GHC.Driver.Errors.Types (GhcMessage)
|
||||
import GHC.Generics (Generic)
|
||||
import GHC.Hs.Extension (GhcPs)
|
||||
import GHC.Types.Error (Messages, defaultDiagnosticOpts,
|
||||
getMessages)
|
||||
import GHC.Types.Error (getMessages)
|
||||
import GHC.Types.Name.Occurrence (occNameString)
|
||||
import GHC.Types.Name.Reader (rdrNameOcc)
|
||||
import GHC.Unit.Module (moduleNameString)
|
||||
import GHC.Unit.Module.Name (moduleNameString)
|
||||
import GHC.Utils.Error (pprMsgEnvelopeBagWithLoc)
|
||||
import GHC.Utils.Outputable (showSDocUnsafe)
|
||||
import System.Directory.Recursive (getFilesRecursive)
|
||||
import System.Exit (exitFailure)
|
||||
|
||||
@@ -202,7 +198,7 @@ sourceSymbols source = do
|
||||
return $ concatMap (importSymbols source filepath . GHC.unLoc) hsmodImports
|
||||
|
||||
-- | Parse a Haskell module
|
||||
parseModule :: FilePath -> IO (GHC.HsModule GhcPs)
|
||||
parseModule :: FilePath -> IO GHC.HsModule
|
||||
parseModule filepath = do
|
||||
result <- ExactPrint.parseModule GHC.Paths.libdir filepath
|
||||
case result of
|
||||
@@ -210,13 +206,7 @@ parseModule filepath = do
|
||||
return $ GHC.unLoc hsmod
|
||||
Left errs ->
|
||||
fail $ "Errors with " <> show filepath <> ":\n "
|
||||
<> formatParseErrors errs
|
||||
|
||||
formatParseErrors :: Messages GhcMessage -> String
|
||||
formatParseErrors errs =
|
||||
intercalate "\n "
|
||||
. fmap showSDocUnsafe
|
||||
$ pprMsgEnvelopeBagWithLoc (defaultDiagnosticOpts @GhcMessage) (getMessages errs)
|
||||
<> show (pprMsgEnvelopeBagWithLoc $ getMessages errs)
|
||||
|
||||
-- | Symbols imported in an import declaration.
|
||||
--
|
||||
@@ -224,12 +214,9 @@ formatParseErrors errs =
|
||||
-- only one item is returned.
|
||||
importSymbols :: FilePath -> FilePath -> GHC.ImportDecl GhcPs -> [ImportedSymbol]
|
||||
importSymbols source filepath GHC.ImportDecl{..} =
|
||||
case ideclImportList of
|
||||
Just (importListInterpretation, syms) ->
|
||||
symbol (if importListInterpretation == GHC.EverythingBut then Hiding else Explicit)
|
||||
. Just
|
||||
. GHC.unLoc
|
||||
<$> GHC.unLoc syms
|
||||
case ideclHiding of
|
||||
Just (hiding, syms) ->
|
||||
symbol (if hiding then Hiding else Explicit) . Just . GHC.unLoc <$> GHC.unLoc syms
|
||||
Nothing ->
|
||||
[ symbol Wildcard Nothing ]
|
||||
where
|
||||
|
||||
@@ -104,7 +104,7 @@ let
|
||||
''
|
||||
|
||||
+ lib.optionalString withTmpDir ''
|
||||
tmpdir="$(${coreutils}/bin/mktemp -d --tmpdir=/tmp ${name}-XXX)"
|
||||
tmpdir="$(${coreutils}/bin/mktemp -d --tmpdir ${name}-XXX)"
|
||||
|
||||
# we keep the tmpdir when an error occurs for debugging
|
||||
trap 'echo Temporary directory kept at: $tmpdir' ERR
|
||||
|
||||
@@ -3,4 +3,5 @@
|
||||
checked-shell-script = import ./checked-shell-script;
|
||||
gitignore = import ./gitignore.nix;
|
||||
haskell-packages = import ./haskell-packages.nix;
|
||||
slocat = import ./slocat.nix;
|
||||
}
|
||||
|
||||
@@ -47,43 +47,37 @@ let
|
||||
# - To modify and try packages locally, see "Working with locally modified Haskell packages" in the Nix README.
|
||||
|
||||
# Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329
|
||||
# jailbreak, because hspec limit for tests
|
||||
fuzzyset = prev.fuzzyset_0_2_4;
|
||||
|
||||
http2 =
|
||||
# TODO: Remove once available in nixpkgs haskellPackages
|
||||
configurator-pg =
|
||||
prev.callHackageDirect
|
||||
{
|
||||
pkg = "http2";
|
||||
ver = "5.4.0";
|
||||
sha256 = "sha256-PeEWVd61bQ8G7LvfLeXklzXqNJFaAjE2ecRMWJZESPE=";
|
||||
pkg = "configurator-pg";
|
||||
ver = "0.2.11";
|
||||
sha256 = "sha256-mtGtNawDJgz2ZIEVca+IYXVu4oNw9xsfJiYWAqAbbgc=";
|
||||
}
|
||||
{ };
|
||||
|
||||
http-semantics =
|
||||
# TODO: Remove once available in nixpkgs haskellPackages
|
||||
streaming-commons =
|
||||
prev.callHackageDirect
|
||||
{
|
||||
pkg = "http-semantics";
|
||||
ver = "0.4.0";
|
||||
sha256 = "sha256-rh0z51EKvsu5rQd5n2z3fSRjjEObouNZSBPO9NFYOF0=";
|
||||
pkg = "streaming-commons";
|
||||
ver = "0.2.3.1";
|
||||
sha256 = "sha256-Gl2eaJcWe1sxmcE/octWlH9uSnERguf+5H66K4fV87s=";
|
||||
}
|
||||
{ };
|
||||
|
||||
network-run =
|
||||
prev.callHackageDirect
|
||||
{
|
||||
pkg = "network-run";
|
||||
ver = "0.5.0";
|
||||
sha256 = "sha256-vbXh+CzxDsGApjqHxCYf/ijpZtUCApFbkcF5gyN0THU=";
|
||||
}
|
||||
{ };
|
||||
|
||||
warp =
|
||||
lib.dontCheck (prev.callHackageDirect
|
||||
{
|
||||
pkg = "warp";
|
||||
ver = "3.4.13";
|
||||
sha256 = "sha256-jmr8kpeSPDkOhT0i9PhozZapX4nUs92cOX7POAGb7/M=";
|
||||
}
|
||||
{ });
|
||||
# Downgrade hasql and related packages while we are still on GHC 9.4 for the static build.
|
||||
hasql = lib.dontCheck (lib.doJailbreak prev.hasql_1_6_4_4);
|
||||
hasql-dynamic-statements = lib.dontCheck prev.hasql-dynamic-statements_0_3_1_5;
|
||||
hasql-implicits = lib.dontCheck prev.hasql-implicits_0_1_1_3;
|
||||
hasql-notifications = lib.dontCheck prev.hasql-notifications_0_2_2_2;
|
||||
hasql-pool = lib.dontCheck prev.hasql-pool_1_0_1;
|
||||
hasql-transaction = lib.dontCheck prev.hasql-transaction_1_1_0_1;
|
||||
postgresql-binary = lib.dontCheck (lib.doJailbreak prev.postgresql-binary_0_13_1_3);
|
||||
};
|
||||
in
|
||||
{
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
_: prev:
|
||||
{
|
||||
slocat = prev.buildGoModule {
|
||||
name = "slocat";
|
||||
src = prev.fetchFromGitHub {
|
||||
owner = "robx";
|
||||
repo = "slocat";
|
||||
rev = "52e7512c6029fd00483e41ccce260a3b4b9b3b64";
|
||||
sha256 = "sha256-qn6luuh5wqREu3s8RfuMCP5PKdS2WdwPrujRYTpfzQ8=";
|
||||
};
|
||||
vendorHash = null;
|
||||
};
|
||||
}
|
||||
@@ -172,7 +172,7 @@ let
|
||||
# The following unsets all GIT_ variables.
|
||||
unset "''${!GIT_@}"
|
||||
|
||||
# shellcheck disable=SC2329
|
||||
# shellcheck disable=SC2317
|
||||
function restore () {
|
||||
ref="$(git stash list --format=format:%gD --grep "$1" -n1)"
|
||||
# this will avoid merge conflicts when applying the stash
|
||||
@@ -205,7 +205,7 @@ let
|
||||
${git}/bin/git add .
|
||||
;;
|
||||
pre-push)
|
||||
# Create a clean working tree without any uncommitted changes.
|
||||
# Create a clean working tree without any uncomitted changes.
|
||||
${withTools.withGit} HEAD ${style}/bin/postgrest-lint
|
||||
;;
|
||||
esac
|
||||
@@ -232,7 +232,7 @@ let
|
||||
${style}/bin/postgrest-lint
|
||||
;;
|
||||
pre-push)
|
||||
# Create a clean working tree without any uncommitted changes.
|
||||
# Create a clean working tree without any uncomitted changes.
|
||||
${withTools.withGit} HEAD ${check}
|
||||
;;
|
||||
esac
|
||||
|
||||
+6
-2
@@ -43,7 +43,7 @@ let
|
||||
}
|
||||
|
||||
if [ "$_arg_language" == "" ]; then
|
||||
# clean previous build, otherwise some errors might be suppressed
|
||||
# clean previous build, otherwise some errors might be supressed
|
||||
rm -rf "../.docs-build/html/default"
|
||||
|
||||
if [ -d languages ]; then
|
||||
@@ -54,7 +54,7 @@ let
|
||||
|
||||
build html "../.docs-build/html/default"
|
||||
else
|
||||
# clean previous build, otherwise some errors might be suppressed
|
||||
# clean previous build, otherwise some errors might be supressed
|
||||
rm -rf "../.docs-build/html/$_arg_language"
|
||||
|
||||
# update and build specific locale, can be used to create new locale
|
||||
@@ -122,6 +122,8 @@ let
|
||||
workingDir = "/docs";
|
||||
}
|
||||
''
|
||||
echo "Checking spelling mistakes..."
|
||||
|
||||
export LC_ALL=C
|
||||
|
||||
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
|
||||
@@ -144,6 +146,8 @@ let
|
||||
workingDir = "/docs";
|
||||
}
|
||||
''
|
||||
echo "Detecting obsolete dictionary entries..."
|
||||
|
||||
export LC_ALL=C
|
||||
|
||||
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# Generate RSA JWK/public material for loadtests.
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import jwcrypto.jwk as jwk
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate RSA JWK/private key pair for loadtests"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rsa",
|
||||
dest="jwk_path",
|
||||
metavar="JWK_PATH",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Path to write the RSA JWK file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--private-key",
|
||||
dest="private_key_path",
|
||||
metavar="PRIVATE_KEY_PATH",
|
||||
type=Path,
|
||||
required=True,
|
||||
help="Path to write the RSA private key file",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
key = jwk.JWK.generate(kty="RSA", size=4096)
|
||||
private_jwk, public_jwk = key.export_private(), key.export_public()
|
||||
|
||||
try:
|
||||
args.jwk_path.write_text(public_jwk)
|
||||
print(f"Created RSA JWK on {args.jwk_path}")
|
||||
except OSError as e:
|
||||
print(f"Error writing to {args.jwk_path}:{e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
args.private_key_path.write_text(private_jwk)
|
||||
print(f"Created private key on {args.private_key_path}")
|
||||
except OSError as e:
|
||||
print(f"Error writing to {args.private_key_path}:{e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+25
-106
@@ -12,24 +12,23 @@
|
||||
# from an array
|
||||
import time
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
import random
|
||||
import jwt
|
||||
import jwcrypto.jwk as jwk
|
||||
from typing import Optional
|
||||
from pathlib import Path
|
||||
from enum import Enum
|
||||
|
||||
URL = "http://postgrest"
|
||||
|
||||
secret_key = b"reallyreallyreallyreallyverysafe"
|
||||
|
||||
key = jwk.JWK.generate(kty="RSA", size=4096)
|
||||
private_key = jwt.algorithms.RSAAlgorithm.from_jwk(key.export_private())
|
||||
public_key = key.export_public()
|
||||
|
||||
def generate_jwt(
|
||||
now: int,
|
||||
exp_inc: Optional[int],
|
||||
rsa_private_key: Optional[jwt.algorithms.RSAAlgorithm],
|
||||
) -> str:
|
||||
|
||||
def generate_jwt(now: int, exp_inc: Optional[int], is_hs: bool) -> str:
|
||||
"""Generate an HS256 or RS256 JWT"""
|
||||
payload = {
|
||||
"sub": f"user_{random.getrandbits(32)}",
|
||||
@@ -40,72 +39,25 @@ def generate_jwt(
|
||||
if exp_inc is not None:
|
||||
payload["exp"] = now + exp_inc
|
||||
|
||||
if rsa_private_key is None:
|
||||
key = secret_key
|
||||
alg = "HS256"
|
||||
else:
|
||||
key = rsa_private_key
|
||||
alg = "RS256"
|
||||
return jwt.encode(payload, key, alg)
|
||||
k = secret_key if is_hs else private_key
|
||||
alg = "HS256" if is_hs else "RS256"
|
||||
return jwt.encode(payload, k, alg)
|
||||
|
||||
|
||||
HTTP_METHODS = (
|
||||
"GET",
|
||||
"OPTIONS",
|
||||
)
|
||||
|
||||
HttpMethod = Enum(
|
||||
"HttpMethod",
|
||||
{method: method for method in HTTP_METHODS},
|
||||
type=str,
|
||||
module=__name__,
|
||||
)
|
||||
|
||||
|
||||
def append_targets(lines: list[str], token: str, http_method: HttpMethod):
|
||||
lines.append(f"{http_method.value} {URL}/authors_only")
|
||||
def append_targets(lines: list[str], token: str):
|
||||
lines.append(f"OPTIONS {URL}/authors_only")
|
||||
lines.append(f"Authorization: Bearer {token}")
|
||||
lines.append("") # blank line to separate requests
|
||||
|
||||
|
||||
# we use this to chain commands on loadtest.nix
|
||||
def run_command(command: list[str]):
|
||||
if not command:
|
||||
return
|
||||
|
||||
if command[0] == "--":
|
||||
command = command[1:]
|
||||
|
||||
if not command:
|
||||
return
|
||||
|
||||
try:
|
||||
subprocess.run(command, check=True)
|
||||
except subprocess.CalledProcessError as exc:
|
||||
print(
|
||||
f"Error executing command {' '.join(command)}: {exc}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(exc.returncode)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate Vegeta targets with unique JWTs"
|
||||
)
|
||||
parser.add_argument(
|
||||
"targets_path",
|
||||
metavar="TARGETS_PATH",
|
||||
"output",
|
||||
help="Path to write the generated targets file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--private-key",
|
||||
dest="private_key_path",
|
||||
metavar="PRIVATE_KEY_PATH",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Path to the RSA private key file (required when --rsa is used)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--worst",
|
||||
dest="worst",
|
||||
@@ -119,31 +71,14 @@ def main():
|
||||
metavar="JWK_PATH",
|
||||
type=Path,
|
||||
default=None,
|
||||
help="Path to an existing RSA JWK file used for signing tokens",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--method",
|
||||
dest="http_method",
|
||||
choices=list(HTTP_METHODS),
|
||||
required=True,
|
||||
help="HTTP method for the vegeta targets",
|
||||
)
|
||||
parser.add_argument(
|
||||
"command",
|
||||
nargs=argparse.REMAINDER,
|
||||
help="Command (and arguments) to run after generating the targets",
|
||||
help="Path for generating a RSA JWK file to sign tokens with",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
rsa_private_key: Optional[jwt.algorithms.RSAAlgorithm] = None
|
||||
|
||||
is_hs = args.jwk_path is None
|
||||
|
||||
http_method = HttpMethod(args.http_method)
|
||||
|
||||
nsamples = 1000
|
||||
|
||||
if is_hs:
|
||||
ntargets = 200000
|
||||
else:
|
||||
@@ -151,25 +86,12 @@ def main():
|
||||
ntargets = 50000
|
||||
|
||||
if not is_hs:
|
||||
if args.private_key_path is None:
|
||||
parser.error("--rsa requires the --private-key option")
|
||||
try:
|
||||
private_key_data = args.private_key_path.read_text()
|
||||
except OSError as e:
|
||||
err = (
|
||||
f"Error reading RSA private key from {args.private_key_path}: "
|
||||
f"{e}. Generate RSA materials first with gen_rsa_materials.py."
|
||||
)
|
||||
print(err, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
rsa_private_key = jwt.algorithms.RSAAlgorithm.from_jwk(private_key_data)
|
||||
except Exception as exc: # broad exception to capture parsing errors
|
||||
err = (
|
||||
f"Error loading RSA private key from {args.private_key_path}: " f"{exc}"
|
||||
)
|
||||
print(err, file=sys.stderr)
|
||||
with open(args.jwk_path, "w") as jwk:
|
||||
jwk.write(public_key)
|
||||
print(f"Created {args.jwk_path} file containing the RSA JWK")
|
||||
except IOError as e:
|
||||
print(f"Error writing to {args.jwk_path}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(f"Generating {ntargets} targets...")
|
||||
@@ -188,7 +110,6 @@ def main():
|
||||
if args.worst:
|
||||
# estimated time takes to build and run postgrest itself
|
||||
build_run_postgrest_time = 2
|
||||
|
||||
# estimated time it takes to generate the targets file
|
||||
# the division numbers are tuned by hand
|
||||
if is_hs: # hs generation is much faster
|
||||
@@ -200,27 +121,25 @@ def main():
|
||||
inc = build_run_postgrest_time + gen_time
|
||||
|
||||
for i in range(ntargets):
|
||||
token = generate_jwt(now, inc + i // 1000, rsa_private_key)
|
||||
append_targets(lines, token, http_method)
|
||||
token = generate_jwt(now, inc + i // 1000, is_hs)
|
||||
append_targets(lines, token)
|
||||
|
||||
else:
|
||||
tokens = [generate_jwt(now, None, rsa_private_key) for _ in range(nsamples)]
|
||||
tokens = [generate_jwt(now, None, is_hs) for _ in range(nsamples)]
|
||||
for i in range(ntargets):
|
||||
token = random.choice(tokens)
|
||||
append_targets(lines, token, http_method)
|
||||
append_targets(lines, token)
|
||||
|
||||
try:
|
||||
with open(args.targets_path, "w") as f:
|
||||
with open(args.output, "w") as f:
|
||||
f.write("\n".join(lines))
|
||||
except IOError as e:
|
||||
print(f"Error writing to {args.targets_path}: {e}", file=sys.stderr)
|
||||
print(f"Error writing to {args.output}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
print(f"Created {ntargets} targets", end=" ")
|
||||
print(f"in {args.targets_path} ({elapsed:.2f}s)")
|
||||
|
||||
run_command(args.command)
|
||||
print(f"in {args.output} ({elapsed:.2f}s)")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+32
-105
@@ -18,8 +18,6 @@ let
|
||||
];
|
||||
}
|
||||
''
|
||||
echo "Starting vegeta loadtest..."
|
||||
|
||||
# ARG_USE_ENV only adds defaults or docs for environment variables
|
||||
# We manually implement a required check here
|
||||
# See also: https://github.com/matejak/argbash/issues/80
|
||||
@@ -44,9 +42,7 @@ let
|
||||
"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])"
|
||||
"ARG_OPTIONAL_SINGLE([method],, [HTTP method used for the jwt loadtests], [OPTIONS])"
|
||||
"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,errors,jwt-hs,jwt-hs-cache,jwt-hs-cache-worst,jwt-rsa,jwt-rsa-cache,jwt-rsa-cache-worst])"
|
||||
"ARG_TYPE_GROUP_SET([METHOD], [METHOD], [method], [OPTIONS,GET])"
|
||||
"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,jwt-hs,jwt-hs-cache,jwt-hs-cache-worst,jwt-rsa,jwt-rsa-cache,jwt-rsa-cache-worst])"
|
||||
"ARG_OPTIONAL_SINGLE([monitor], [m], [Monitoring file], [./loadtest/result.csv])"
|
||||
"ARG_LEFTOVERS([additional vegeta arguments])"
|
||||
];
|
||||
@@ -63,127 +59,67 @@ let
|
||||
export PGRST_DB_TX_END="rollback-allow-override"
|
||||
export PGRST_LOG_LEVEL="crit"
|
||||
export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe"
|
||||
# set previous PGRST_JWT_CACHE_MAX_LIFETIME configuration so that
|
||||
# load test works across branches
|
||||
# TODO clean once PGRST_JWT_CACHE_MAX_ENTRIES merged and released
|
||||
export PGRST_JWT_CACHE_MAX_LIFETIME="86400"
|
||||
|
||||
mkdir -p "$(dirname "$_arg_output")"
|
||||
abs_output="$(realpath "$_arg_output")"
|
||||
|
||||
case "$_arg_kind" in
|
||||
jwt-hs)
|
||||
${genTargetsHS} "$_arg_testdir"/gen_targets.http
|
||||
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
|
||||
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withGenTargets} --method "$_arg_method" "$_arg_testdir"/gen_targets.http \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
export PGRST_JWT_CACHE_MAX_LIFETIME="0"
|
||||
;;
|
||||
|
||||
jwt-hs-cache)
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withGenTargets} --method "$_arg_method" "$_arg_testdir"/gen_targets.http \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
${genTargetsHS} "$_arg_testdir"/gen_targets.http
|
||||
;;
|
||||
|
||||
jwt-hs-cache-worst)
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withGenTargets} --method "$_arg_method" --worst "$_arg_testdir"/gen_targets.http \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
${genTargetsHS} --worst "$_arg_testdir"/gen_targets.http
|
||||
;;
|
||||
|
||||
jwt-rsa)
|
||||
${genTargetsHS} --rsa="$_arg_testdir"/gen_jwk.json "$_arg_testdir"/gen_targets.http
|
||||
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
|
||||
|
||||
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
|
||||
export PGRST_JWT_CACHE_MAX_LIFETIME="0"
|
||||
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
||||
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withGenTargets} --method "$_arg_method" --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
;;
|
||||
|
||||
jwt-rsa-cache)
|
||||
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
|
||||
${genTargetsHS} --rsa="$_arg_testdir"/gen_jwk.json "$_arg_testdir"/gen_targets.http
|
||||
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
||||
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withGenTargets} --method "$_arg_method" --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
;;
|
||||
|
||||
jwt-rsa-cache-worst)
|
||||
${genTargetsHS} --worst --rsa="$_arg_testdir"/gen_jwk.json "$_arg_testdir"/gen_targets.http
|
||||
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
||||
|
||||
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
|
||||
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
||||
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withGenTargets} --method "$_arg_method" --worst --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
;;
|
||||
|
||||
mixed)
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
;;
|
||||
|
||||
# here we sleep purposefully to check how much memory does the schema cache consume in the final report
|
||||
errors)
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/errors.sql \
|
||||
${withTools.withPgrst} --timeout 2 --sleep 5 -m "$_arg_monitor" \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -targets errors.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
|
||||
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
||||
|
||||
if [ "$_arg_kind" != "errors" ]; then
|
||||
# fail in case 401 happened on jwt loadtests
|
||||
unauthorized_count="$(${vegeta}/bin/vegeta report -type=json "$_arg_output" \
|
||||
| ${jq}/bin/jq -r '.status_codes["401"] // 0')"
|
||||
|
||||
if [ "$unauthorized_count" -gt 0 ]; then
|
||||
last_unauthorized_body="$(${vegeta}/bin/vegeta encode "$_arg_output" \
|
||||
| ${jq}/bin/jq -rn '
|
||||
reduce inputs as $item (null;
|
||||
if $item.code == 401 then $item else . end
|
||||
)
|
||||
| if . == null then
|
||||
empty
|
||||
else
|
||||
(.body | @base64d)
|
||||
end
|
||||
')"
|
||||
|
||||
echo "loadtest failed: found $unauthorized_count 401 Unauthorized responses" >&2
|
||||
if [ -n "$last_unauthorized_body" ]; then
|
||||
printf '%s\n' "Last 401 response body:" >&2
|
||||
printf '%s\n' "$last_unauthorized_body" >&2
|
||||
fi
|
||||
|
||||
exit 1
|
||||
fi
|
||||
if [ "$_arg_kind" == "mixed" ]; then
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withSlowPg} \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
${withTools.withSlowPgrst} \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
else
|
||||
# shellcheck disable=SC2145
|
||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||
sh -c "cd \"$_arg_testdir\" && \
|
||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||
fi
|
||||
|
||||
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
||||
'';
|
||||
|
||||
loadtestAgainst =
|
||||
@@ -314,22 +250,13 @@ let
|
||||
| ${mergeMonitorResults}
|
||||
'';
|
||||
|
||||
withGenTargets =
|
||||
writers.writePython3 "postgrest-with-gen-loadtest-targets"
|
||||
genTargetsHS =
|
||||
writers.writePython3 "postgrest-gen-loadtest-targets-hs"
|
||||
{
|
||||
libraries = [ python3Packages.pyjwt python3Packages.jwcrypto ];
|
||||
doCheck = false; # postgrest-style conflicts with this
|
||||
}
|
||||
(builtins.readFile ./generate_targets.py);
|
||||
|
||||
genRsaMaterials =
|
||||
writers.writePython3 "postgrest-gen-rsa-materials"
|
||||
{
|
||||
libraries = [ python3Packages.jwcrypto ];
|
||||
doCheck = false; # postgrest-style conflicts with this
|
||||
}
|
||||
(builtins.readFile ./gen_rsa_materials.py);
|
||||
|
||||
mergeMonitorResults =
|
||||
writers.writePython3 "postgrest-merge-monitor-results"
|
||||
{
|
||||
|
||||
@@ -62,19 +62,19 @@ let
|
||||
git add CHANGELOG.md > /dev/null
|
||||
|
||||
echo "Committing ..."
|
||||
git commit -m "bump version to $new_version" > /dev/null
|
||||
git commit -m "chore: bump version to $new_version" > /dev/null
|
||||
|
||||
if [[ "$current_branch" == "main" ]]; then
|
||||
bump devel
|
||||
|
||||
# The order of operations is important here:
|
||||
# - bump devel is run and $A is updated to the new version
|
||||
# - bump devel is run and $A is upated to the new version
|
||||
# - the branch is created with the new A, but the commit before the devel bump
|
||||
# - the devel bump is committed
|
||||
git branch "v$A"
|
||||
|
||||
echo "Committing (devel bump)..."
|
||||
git commit -m "bump version to $new_version" > /dev/null
|
||||
git commit -m "chore: bump version to $new_version" > /dev/null
|
||||
fi
|
||||
|
||||
trap "echo Remote not found. Please push manually ..." ERR
|
||||
|
||||
+6
-8
@@ -29,20 +29,19 @@ let
|
||||
|
||||
# Format Haskell files
|
||||
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
|
||||
# TODO: fix style issues in src/protolude and include it
|
||||
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' --ignore-dir=src/protolude . \
|
||||
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \
|
||||
| xargs ${stylish-haskell}/bin/stylish-haskell -i
|
||||
|
||||
# Format Python files
|
||||
${black}/bin/black . 2> /dev/null
|
||||
'';
|
||||
|
||||
# Script to check whether any uncommitted changes result from postgrest-style
|
||||
# Script to check whether any uncommited changes result from postgrest-style
|
||||
styleCheck =
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-style-check";
|
||||
docs = "Check whether postgrest-style results in any uncommitted changes.";
|
||||
docs = "Check whether postgrest-style results in any uncommited changes.";
|
||||
workingDir = "/";
|
||||
}
|
||||
''
|
||||
@@ -84,18 +83,17 @@ let
|
||||
# ruff has gaps in scanning for unused code, so we use vulture
|
||||
echo "Scanning python files for unused code..."
|
||||
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?py$' . \
|
||||
| xargs ${python3Packages.vulture}/bin/vulture --exclude docs/conf.py --min-confidence 80
|
||||
| xargs ${python3Packages.vulture}/bin/vulture --exclude docs/conf.py
|
||||
|
||||
echo "Linting python files..."
|
||||
${ruff}/bin/ruff check .
|
||||
|
||||
echo "Checking consistency of import aliases in Haskell code..."
|
||||
${hsie} check-aliases main src/PostgREST
|
||||
${hsie} check-aliases main src
|
||||
|
||||
echo "Linting Haskell files..."
|
||||
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
|
||||
# TODO: fix lint issues in src/protolude and include it
|
||||
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' --ignore-dir=src/protolude . \
|
||||
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \
|
||||
| xargs ${hlint}/bin/hlint --hint=${hlintConfig}
|
||||
'';
|
||||
|
||||
|
||||
+5
-1
@@ -7,8 +7,10 @@
|
||||
, glibcLocales ? null
|
||||
, gnugrep
|
||||
, hpc-codecov
|
||||
, hostPlatform
|
||||
, jq
|
||||
, lib
|
||||
, nginx
|
||||
, postgrest
|
||||
, python3
|
||||
, runtimeShell
|
||||
@@ -93,6 +95,7 @@ let
|
||||
args = [ "ARG_LEFTOVERS([pytest arguments])" ];
|
||||
workingDir = "/";
|
||||
withEnv = postgrest.env;
|
||||
withPath = [ nginx ];
|
||||
}
|
||||
''
|
||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
||||
@@ -155,10 +158,11 @@ let
|
||||
redirectTixFiles = false;
|
||||
withEnv = postgrest.env;
|
||||
withTmpDir = true;
|
||||
withPath = [ nginx ];
|
||||
}
|
||||
(
|
||||
# required for `hpc markup` in CI; glibcLocales is not available e.g. on Darwin
|
||||
lib.optionalString (stdenv.isLinux && stdenv.hostPlatform.libc == "glibc") ''
|
||||
lib.optionalString (stdenv.isLinux && hostPlatform.libc == "glibc") ''
|
||||
export LOCALE_ARCHIVE="${glibcLocales}/lib/locale/locale-archive"
|
||||
'' +
|
||||
|
||||
|
||||
+103
-46
@@ -6,6 +6,7 @@
|
||||
, postgresqlVersions
|
||||
, postgrest
|
||||
, python3Packages
|
||||
, slocat
|
||||
, writeText
|
||||
, writers
|
||||
}:
|
||||
@@ -105,8 +106,7 @@ let
|
||||
|
||||
log "Starting replica on $replica_host"
|
||||
|
||||
# We set a low max_standby_streaming_delay to make the replication conflict fail faster in tests (otherwise it waits for the default 30s)
|
||||
pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" -c max_standby_streaming_delay=\"3s\" " \
|
||||
pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" " \
|
||||
>> "$setuplog"
|
||||
|
||||
>&2 echo "${commandName}: Replica enabled. You can connect to it with: psql 'postgres:///$PGDATABASE?host=$replica_host' -U postgres"
|
||||
@@ -117,7 +117,7 @@ let
|
||||
export PGRST_DB_URI="postgres:///$PGDATABASE?host=$PGREPLICAHOST,$PGHOST"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2329
|
||||
# shellcheck disable=SC2317
|
||||
stop () {
|
||||
log "Stopping the database cluster..."
|
||||
pg_ctl stop --mode=immediate >> "$setuplog"
|
||||
@@ -132,11 +132,9 @@ let
|
||||
fi
|
||||
|
||||
if test "$_arg_fixtures"; then
|
||||
load_start=$SECONDS
|
||||
>&2 printf "${commandName}: Loading fixtures under the postgres role..."
|
||||
log "Loading fixtures under the postgres role..."
|
||||
psql -U postgres -v PGUSER="$PGUSER" -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog"
|
||||
load_end=$((SECONDS - load_start))
|
||||
>&2 printf " done in %ss. Running command...\n" "$load_end"
|
||||
log "Done. Running command..."
|
||||
fi
|
||||
|
||||
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||
@@ -185,6 +183,81 @@ let
|
||||
|
||||
withPg = withTmpDb (builtins.head postgresqlVersions);
|
||||
|
||||
withSlowPg =
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-with-slow-pg";
|
||||
docs = "Run the given command with simulated high latency postgresql";
|
||||
args =
|
||||
[
|
||||
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||
"ARG_LEFTOVERS([command arguments])"
|
||||
"ARG_USE_ENV([PGHOST], [], [PG host (socket name)])"
|
||||
"ARG_USE_ENV([PGDELAY], [0ms], [extra PG latency (duration)])"
|
||||
];
|
||||
positionalCompletion = "_command";
|
||||
workingDir = "/";
|
||||
redirectTixFiles = false;
|
||||
withTmpDir = true;
|
||||
}
|
||||
''
|
||||
delay="''${PGDELAY:-0ms}"
|
||||
echo "delaying data to/from postgres by $delay"
|
||||
|
||||
REALPGHOST="$PGHOST"
|
||||
export PGHOST="$tmpdir/socket"
|
||||
mkdir -p "$PGHOST"
|
||||
|
||||
${slocat}/bin/slocat -delay "$delay" -src "$PGHOST/.s.PGSQL.5432" -dst "$REALPGHOST/.s.PGSQL.5432" &
|
||||
SLOCAT_PID=$!
|
||||
# shellcheck disable=SC2317
|
||||
stop_slocat() {
|
||||
kill "$SLOCAT_PID" || true
|
||||
wait "$SLOCAT_PID" || true
|
||||
}
|
||||
trap stop_slocat EXIT
|
||||
sleep 1 # should wait for socket file to appear instead
|
||||
|
||||
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||
'';
|
||||
|
||||
withSlowPgrst =
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-with-slow-postgrest";
|
||||
docs = "Run the given command with simulated high latency postgrest";
|
||||
args =
|
||||
[
|
||||
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||
"ARG_LEFTOVERS([command arguments])"
|
||||
"ARG_USE_ENV([PGRST_SERVER_UNIX_SOCKET], [], [PostgREST host (socket name)])"
|
||||
"ARG_USE_ENV([PGRST_DELAY], [0ms], [extra PostgREST latency (duration)])"
|
||||
];
|
||||
positionalCompletion = "_command";
|
||||
workingDir = "/";
|
||||
redirectTixFiles = false;
|
||||
withTmpDir = true;
|
||||
}
|
||||
''
|
||||
delay="''${PGRST_DELAY:-0ms}"
|
||||
echo "delaying data to/from PostgREST by $delay"
|
||||
|
||||
REAL_PGRST_SERVER_UNIX_SOCKET="$PGRST_SERVER_UNIX_SOCKET"
|
||||
export PGRST_SERVER_UNIX_SOCKET="$tmpdir/postgrest.socket"
|
||||
|
||||
${slocat}/bin/slocat -delay "$delay" -src "$PGRST_SERVER_UNIX_SOCKET" -dst "$REAL_PGRST_SERVER_UNIX_SOCKET" &
|
||||
SLOCAT_PID=$!
|
||||
# shellcheck disable=SC2317
|
||||
stop_slocat() {
|
||||
kill "$SLOCAT_PID" || true
|
||||
wait "$SLOCAT_PID" || true
|
||||
}
|
||||
trap stop_slocat EXIT
|
||||
sleep 1 # should wait for socket file to appear instead
|
||||
|
||||
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||
'';
|
||||
|
||||
withGit =
|
||||
let
|
||||
name = "postgrest-with-git";
|
||||
@@ -279,21 +352,15 @@ let
|
||||
'';
|
||||
|
||||
withPgrst =
|
||||
let
|
||||
commandName = "postgrest-with-pgrst";
|
||||
in
|
||||
checkedShellScript
|
||||
{
|
||||
name = commandName;
|
||||
name = "postgrest-with-pgrst";
|
||||
docs = "Build and run PostgREST and run <command> with PGRST_SERVER_UNIX_SOCKET set.";
|
||||
args =
|
||||
[
|
||||
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||
"ARG_LEFTOVERS([command arguments])"
|
||||
"ARG_OPTIONAL_SINGLE([monitor], [m], [Enable CPU and memory monitoring of the PostgREST process and output to the designated file as markdown])"
|
||||
"ARG_OPTIONAL_SINGLE([timeout], [t], [Maximum time to wait for PostgREST to be ready], [5])"
|
||||
"ARG_OPTIONAL_SINGLE([sleep], [s], [Sleep time after PostgREST is ready, this is useful for monitoring])"
|
||||
"ARG_USE_ENV([PGRST_CMD], [], [PostgREST executable to run])"
|
||||
];
|
||||
positionalCompletion = "_command";
|
||||
workingDir = "/";
|
||||
@@ -303,34 +370,30 @@ let
|
||||
''
|
||||
export PGRST_SERVER_UNIX_SOCKET="$tmpdir"/postgrest.socket
|
||||
|
||||
if [ -z "''${PGRST_CMD:-}" ]; then
|
||||
rm -f result
|
||||
build_start=$SECONDS
|
||||
if [ -z "''${PGRST_BUILD_CABAL:-}" ]; then
|
||||
echo -n "${commandName}: Building postgrest (nix)... "
|
||||
# Using lib.getBin to also make this work with older checkouts, where .bin was not a thing, yet.
|
||||
nix-build -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage' > "$tmpdir"/build.log 2>&1 || {
|
||||
echo "failed, output:"
|
||||
cat "$tmpdir"/build.log
|
||||
exit 1
|
||||
}
|
||||
PGRST_CMD=$(echo ./result*/bin/postgrest)
|
||||
else
|
||||
echo -n "${commandName}: Building postgrest (cabal)... "
|
||||
postgrest-build
|
||||
PGRST_CMD=postgrest-run
|
||||
fi
|
||||
build_end=$((SECONDS - build_start))
|
||||
printf "done in %ss.\n" "$build_end"
|
||||
rm -f result
|
||||
if [ -z "''${PGRST_BUILD_CABAL:-}" ]; then
|
||||
echo -n "Building postgrest (nix)... "
|
||||
# Using lib.getBin to also make this work with older checkouts, where .bin was not a thing, yet.
|
||||
nix-build -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage' > "$tmpdir"/build.log 2>&1 || {
|
||||
echo "failed, output:"
|
||||
cat "$tmpdir"/build.log
|
||||
exit 1
|
||||
}
|
||||
PGRST_CMD=$(echo ./result*/bin/postgrest)
|
||||
else
|
||||
echo -n "Building postgrest (cabal)... "
|
||||
postgrest-build
|
||||
PGRST_CMD=postgrest-run
|
||||
fi
|
||||
echo "done."
|
||||
|
||||
ver=$($PGRST_CMD ${legacyConfig} --version)
|
||||
|
||||
echo -n "${commandName}: Starting $ver... "
|
||||
echo -n "Starting $ver... "
|
||||
|
||||
$PGRST_CMD ${legacyConfig} > "$tmpdir"/run.log 2>&1 &
|
||||
pid=$!
|
||||
# shellcheck disable=SC2329
|
||||
# shellcheck disable=SC2317
|
||||
cleanup() {
|
||||
# Send INT to all postgrest processes.
|
||||
# Workaround to trigger dumping postgrest.prof for postgrest-profiled-run
|
||||
@@ -344,25 +407,17 @@ let
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
wait_start=$SECONDS
|
||||
timeout -s TERM "$_arg_timeout" ${waitForPgrstReady} || {
|
||||
timeout -s TERM 5 ${waitForPgrstReady} || {
|
||||
echo "timed out, output:"
|
||||
cat "$tmpdir"/run.log
|
||||
exit 1
|
||||
}
|
||||
wait_duration=$((SECONDS - wait_start))
|
||||
printf "done in %ss.\n" "$wait_duration"
|
||||
|
||||
echo "${commandName}: You can tail the server logs with: tail -f $tmpdir/run.log"
|
||||
echo "done."
|
||||
|
||||
if [[ -n "$_arg_monitor" ]]; then
|
||||
${monitorPid} "$pid" > "$_arg_monitor" &
|
||||
fi
|
||||
|
||||
if [[ -n "$_arg_sleep" ]]; then
|
||||
sleep "$_arg_sleep"
|
||||
fi
|
||||
|
||||
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||
'';
|
||||
|
||||
@@ -380,7 +435,9 @@ buildToolbox
|
||||
inherit
|
||||
withGit
|
||||
withPgAll
|
||||
withPgrst;
|
||||
withPgrst
|
||||
withSlowPg
|
||||
withSlowPgrst;
|
||||
} // builtins.listToAttrs (
|
||||
# Create a `postgrest-with-pg-` for each PostgreSQL version
|
||||
builtins.map (pg: { inherit (pg) name; value = withTmpDb pg; }) postgresqlVersions
|
||||
|
||||
+35
-88
@@ -1,27 +1,28 @@
|
||||
cabal-version: 3.0
|
||||
name: postgrest
|
||||
version: 15
|
||||
version: 14.14
|
||||
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
|
||||
permits.
|
||||
license: MIT
|
||||
license-file: LICENSE
|
||||
author: Joe Nelson, Adam Baker, Steve Chavez, Wolfgang Walther
|
||||
author: Joe Nelson, Adam Baker, Steve Chavez
|
||||
maintainer: Steve Chavez <stevechavezast@gmail.com>
|
||||
category: Executable, PostgreSQL, Network APIs
|
||||
homepage: https://postgrest.org
|
||||
bug-reports: https://github.com/PostgREST/postgrest/issues
|
||||
build-type: Simple
|
||||
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.10.3
|
||||
, GHC == 9.6.7
|
||||
-- cabal on Ubuntu
|
||||
-- nix
|
||||
, GHC == 9.12.3
|
||||
, GHC == 9.8.4
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
@@ -54,7 +55,6 @@ library
|
||||
PostgREST.Client
|
||||
PostgREST.Config
|
||||
PostgREST.Config.Database
|
||||
PostgREST.Debounce
|
||||
PostgREST.Config.JSPath
|
||||
PostgREST.Config.PgVersion
|
||||
PostgREST.Config.Proxy
|
||||
@@ -66,7 +66,6 @@ library
|
||||
PostgREST.SchemaCache.Representations
|
||||
PostgREST.SchemaCache.Table
|
||||
PostgREST.Error
|
||||
PostgREST.Error.Types
|
||||
PostgREST.Listener
|
||||
PostgREST.Logger
|
||||
PostgREST.MainTx
|
||||
@@ -82,7 +81,6 @@ library
|
||||
PostgREST.Plan
|
||||
PostgREST.Plan.CallPlan
|
||||
PostgREST.Plan.MutatePlan
|
||||
PostgREST.Plan.Negotiate
|
||||
PostgREST.Plan.ReadPlan
|
||||
PostgREST.Plan.Types
|
||||
PostgREST.RangeQuery
|
||||
@@ -98,9 +96,9 @@ library
|
||||
PostgREST.Response.Performance
|
||||
PostgREST.TimeIt
|
||||
PostgREST.Version
|
||||
build-depends: base >= 4.9 && < 4.22
|
||||
build-depends: base >= 4.9 && < 4.20
|
||||
, HTTP >= 4000.3.7 && < 4000.5
|
||||
, Ranged-sets >= 0.3 && < 0.6
|
||||
, Ranged-sets >= 0.3 && < 0.5
|
||||
, aeson >= 2.0.3 && < 2.3
|
||||
, auto-update >= 0.1.4 && < 0.3
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
@@ -108,20 +106,17 @@ library
|
||||
, case-insensitive >= 1.2 && < 1.3
|
||||
, cassava >= 0.4.5 && < 0.6
|
||||
, configurator-pg >= 0.2.11 && < 0.3
|
||||
, containers >= 0.5.7 && < 0.8
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
, cookie >= 0.4.2 && < 0.6
|
||||
-- crypton 1.1.0 moved from `memory` to `ram`, which jose-jwt fails to build with right now.
|
||||
-- should be possible to remove this once jose-jwt had a new release.
|
||||
, crypton < 1.1.0
|
||||
, directory >= 1.2.6 && < 1.4
|
||||
, either >= 4.4.1 && < 5.1
|
||||
, extra >= 1.7.0 && < 2.0
|
||||
, fuzzyset >= 0.2.4 && < 0.3
|
||||
, hasql >= 1.9 && <= 1.9.3.1
|
||||
, hasql-dynamic-statements >= 0.3.1 && <= 0.3.1.8
|
||||
, hasql-notifications >= 0.2.4.0 && < 0.3
|
||||
, hasql-pool >= 1.1 && <= 1.3.0.4
|
||||
, hasql-transaction >= 1.0.1 && <= 1.2.1
|
||||
, 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.2
|
||||
, http-client >= 0.7.19 && < 0.8
|
||||
, http-types >= 0.12.2 && < 0.13
|
||||
, insert-ordered-containers >= 0.2.2 && < 0.3
|
||||
@@ -134,16 +129,17 @@ library
|
||||
, 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
|
||||
, protolude >= 0.3.1 && < 0.4
|
||||
, regex-tdfa >= 1.2.2 && < 1.4
|
||||
, retry >= 0.7.4 && < 0.10
|
||||
, scientific >= 0.3.4 && < 0.4
|
||||
, streaming-commons >= 0.2.3.1 && < 0.3
|
||||
, swagger2 >= 2.4 && < 2.9
|
||||
, text >= 1.2.2 && < 2.2
|
||||
, time >= 1.6 && < 1.15
|
||||
, time >= 1.6 && < 1.13
|
||||
, unordered-containers >= 0.2.8 && < 0.3
|
||||
, unix-compat >= 0.5.4 && < 0.8
|
||||
, vault >= 0.3.1.5 && < 0.4
|
||||
@@ -156,11 +152,12 @@ 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.4.13 && < 3.5
|
||||
, warp >= 3.3.19 && < 3.5
|
||||
, stm >= 2.5 && < 3
|
||||
, stm-hamt >= 1.2 && < 2
|
||||
, focus >= 1.0 && < 2
|
||||
, some >= 1.0.4.1 && < 2
|
||||
, uuid >= 1.3 && < 2
|
||||
-- -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
|
||||
@@ -180,64 +177,16 @@ library
|
||||
build-depends:
|
||||
unix
|
||||
|
||||
library protolude
|
||||
visibility: private
|
||||
default-language: Haskell2010
|
||||
default-extensions: NoImplicitPrelude
|
||||
FlexibleContexts
|
||||
MultiParamTypeClasses
|
||||
OverloadedStrings
|
||||
hs-source-dirs: src/protolude
|
||||
exposed-modules: Protolude
|
||||
Protolude.Applicative
|
||||
Protolude.Base
|
||||
Protolude.Bifunctor
|
||||
Protolude.Bool
|
||||
Protolude.CallStack
|
||||
Protolude.Conv
|
||||
Protolude.ConvertText
|
||||
Protolude.Debug
|
||||
Protolude.Either
|
||||
Protolude.Error
|
||||
Protolude.Exceptions
|
||||
Protolude.Functor
|
||||
Protolude.List
|
||||
Protolude.Monad
|
||||
Protolude.Panic
|
||||
Protolude.Partial
|
||||
Protolude.Safe
|
||||
Protolude.Semiring
|
||||
Protolude.Show
|
||||
Protolude.Unsafe
|
||||
build-depends: array >= 0.4 && < 0.6
|
||||
, async >= 2.0 && < 2.3
|
||||
, base >= 4.6 && < 4.22
|
||||
, bytestring >= 0.10.8 && < 0.13
|
||||
, containers >= 0.5.7 && < 0.8
|
||||
, deepseq >= 1.3 && < 1.6
|
||||
, ghc-prim >= 0.3 && < 0.14
|
||||
, hashable >= 1.2 && < 1.6
|
||||
, mtl >= 2.1 && < 2.4
|
||||
, mtl-compat >= 0.2 && < 0.3
|
||||
, stm >= 2.5 && < 3
|
||||
, text >= 1.2.2 && < 2.2
|
||||
, transformers >= 0.2 && < 0.7
|
||||
, transformers-compat >= 0.4 && < 0.8
|
||||
-- Protolude has some partial functions, so
|
||||
-- it is fine to disable that specific warning
|
||||
ghc-options: -Werror -Wall -fwarn-identities -Wno-x-partial
|
||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||
|
||||
executable postgrest
|
||||
default-language: Haskell2010
|
||||
default-extensions: OverloadedStrings
|
||||
NoImplicitPrelude
|
||||
hs-source-dirs: main
|
||||
main-is: Main.hs
|
||||
build-depends: base >= 4.9 && < 4.22
|
||||
, containers >= 0.5.7 && < 0.8
|
||||
build-depends: base >= 4.9 && < 4.20
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
, postgrest
|
||||
, protolude
|
||||
, protolude >= 0.3.1 && < 0.4
|
||||
ghc-options: -threaded -rtsopts "-with-rtsopts=-N -I0 -qg"
|
||||
-O2 -Werror -Wall -fwarn-identities
|
||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||
@@ -292,9 +241,7 @@ test-suite spec
|
||||
Feature.Query.PgSafeUpdateSpec
|
||||
Feature.Query.PlanSpec
|
||||
Feature.Query.PostGISSpec
|
||||
Feature.Query.Preferences.HandlingSpec
|
||||
Feature.Query.Preferences.MaxAffectedSpec
|
||||
Feature.Query.Preferences.TimezoneSpec
|
||||
Feature.Query.PreferencesSpec
|
||||
Feature.Query.QueryLimitedSpec
|
||||
Feature.Query.QuerySpec
|
||||
Feature.Query.RangeSpec
|
||||
@@ -310,16 +257,16 @@ test-suite spec
|
||||
Feature.RollbackSpec
|
||||
Feature.RpcPreRequestGucsSpec
|
||||
SpecHelper
|
||||
build-depends: base >= 4.9 && < 4.22
|
||||
build-depends: base >= 4.9 && < 4.20
|
||||
, aeson >= 2.0.3 && < 2.3
|
||||
, aeson-qq >= 0.8.1 && < 0.9
|
||||
, async >= 2.1.1 && < 2.3
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
, bytestring >= 0.10.8 && < 0.13
|
||||
, case-insensitive >= 1.2 && < 1.3
|
||||
, containers >= 0.5.7 && < 0.8
|
||||
, hasql-pool >= 1.0.1 && <= 1.3.0.4
|
||||
, hasql-transaction >= 1.0.1 && <= 1.2.1
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
, hasql-pool >= 1.0.1 && < 1.1
|
||||
, hasql-transaction >= 1.0.1 && < 1.2
|
||||
, heredoc >= 0.2 && < 0.3
|
||||
, hspec >= 2.3 && < 2.12
|
||||
, hspec-expectations >= 0.8.4 && < 0.9
|
||||
@@ -333,7 +280,7 @@ test-suite spec
|
||||
, postgrest
|
||||
, process >= 1.4.2 && < 1.7
|
||||
, prometheus-client >= 1.1.1 && < 1.2.0
|
||||
, protolude
|
||||
, protolude >= 0.3.1 && < 0.4
|
||||
, regex-tdfa >= 1.2.2 && < 1.4
|
||||
, scientific >= 0.3.4 && < 0.4
|
||||
, text >= 1.2.2 && < 2.2
|
||||
@@ -359,11 +306,11 @@ test-suite observability
|
||||
Observation.JwtCache
|
||||
Observation.MetricsSpec
|
||||
Observation.SchemaCacheSpec
|
||||
build-depends: base >= 4.9 && < 4.22
|
||||
build-depends: base >= 4.9 && < 4.20
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
, bytestring >= 0.10.8 && < 0.13
|
||||
, hasql-pool >= 1.0.1 && <= 1.3.0.4
|
||||
, hasql-transaction >= 1.0.1 && <= 1.2.1
|
||||
, hasql-pool >= 1.0.1 && < 1.1
|
||||
, hasql-transaction >= 1.0.1 && < 1.2
|
||||
, hspec >= 2.3 && < 2.12
|
||||
, hspec-expectations >= 0.8.4 && < 0.9
|
||||
, hspec-wai >= 0.10 && < 0.12
|
||||
@@ -372,7 +319,7 @@ test-suite observability
|
||||
, jose-jwt >= 0.9.6 && < 0.11
|
||||
, postgrest
|
||||
, prometheus-client >= 1.1.1 && < 1.2.0
|
||||
, protolude
|
||||
, protolude >= 0.3.1 && < 0.4
|
||||
, text >= 1.2.2 && < 2.2
|
||||
, wai >= 3.2.1 && < 3.3
|
||||
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
|
||||
@@ -388,10 +335,10 @@ test-suite doctests
|
||||
NoImplicitPrelude
|
||||
hs-source-dirs: test/doc
|
||||
main-is: Main.hs
|
||||
build-depends: base >= 4.9 && < 4.22
|
||||
build-depends: base >= 4.9 && < 4.20
|
||||
, doctest >= 0.8
|
||||
, postgrest
|
||||
, pretty-simple
|
||||
, protolude
|
||||
, protolude >= 0.3.1 && < 0.4
|
||||
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
|
||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
# We highly recommend that use the PostgREST binary cache by installing cachix
|
||||
# (https://app.cachix.org/) and running `cachix use postgrest`.
|
||||
{ docker ? false
|
||||
, postgrest ? import ./default.nix { }
|
||||
}:
|
||||
let
|
||||
postgrest =
|
||||
import ./default.nix { };
|
||||
|
||||
inherit (postgrest) pkgs;
|
||||
|
||||
inherit (pkgs) lib;
|
||||
@@ -35,7 +37,10 @@ lib.overrideDerivation postgrest.env (
|
||||
buildInputs =
|
||||
base.buildInputs ++ [
|
||||
pkgs.cabal-install
|
||||
pkgs.cabal2nix
|
||||
pkgs.git
|
||||
pkgs.postgresql
|
||||
pkgs.update-nix-fetchgit
|
||||
postgrest.hsie.bin
|
||||
]
|
||||
++ toolboxes;
|
||||
@@ -44,10 +49,6 @@ lib.overrideDerivation postgrest.env (
|
||||
''
|
||||
export HISTFILE=.history
|
||||
|
||||
# Bypass proxy for all hosts, it prevents HTTP client failures used in test
|
||||
# suites. See: https://github.com/PostgREST/postgrest/issues/4633 for more info
|
||||
export NO_PROXY=*
|
||||
|
||||
source ${pkgs.bash-completion}/etc/profile.d/bash_completion.sh
|
||||
source ${pkgs.git}/share/git/contrib/completion/git-completion.bash
|
||||
source ${postgrest.hsie.bash-completion}
|
||||
|
||||
+13
-3
@@ -11,7 +11,8 @@ import Control.Monad.Extra (whenJust)
|
||||
import Network.Socket hiding (addrFamily)
|
||||
import Network.Socket.ByteString
|
||||
|
||||
import PostgREST.AppState (AppState)
|
||||
import PostgREST.AppState (AppState, getConfig)
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.MediaType (MediaType (..), toContentType)
|
||||
import PostgREST.Metrics (metricsToText)
|
||||
import PostgREST.Network (resolveSocketToAddress)
|
||||
@@ -24,13 +25,22 @@ import Protolude
|
||||
|
||||
runAdmin :: AppState -> Maybe NS.Socket -> NS.Socket -> Warp.Settings -> IO ()
|
||||
runAdmin appState maybeAdminSocket socketREST settings = do
|
||||
conf <- getConfig appState
|
||||
whenJust maybeAdminSocket $ \adminSocket -> do
|
||||
address <- resolveSocketToAddress adminSocket
|
||||
observer $ AdminStartObs address
|
||||
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
|
||||
void . forkIO $ handle (onError adminSocket) $
|
||||
Warp.runSettingsSocket (adminServerSettings conf address) adminSocket adminApp
|
||||
where
|
||||
adminApp = admin appState socketREST
|
||||
observer = AppState.getObserver appState
|
||||
adminServerSettings config addr =
|
||||
settings
|
||||
& Warp.setBeforeMainLoop (observer $ AdminStartObs addr)
|
||||
& maybe identity Warp.setPort (configAdminServerPort config)
|
||||
|
||||
onError adminSock ex = do
|
||||
observer $ AdminServerCrashedObs ex
|
||||
NS.close adminSock -- we close the socket so request doesn't hang
|
||||
|
||||
-- | PostgREST admin application
|
||||
admin :: AppState.AppState -> NS.Socket -> Wai.Application
|
||||
|
||||
@@ -64,7 +64,7 @@ data ApiRequest = ApiRequest {
|
||||
, iPayload :: Maybe Payload -- ^ Data sent by client and used for mutation actions
|
||||
, iPreferences :: Preferences.Preferences -- ^ Prefer header values
|
||||
, iQueryParams :: QueryParams.QueryParams
|
||||
, iColumns :: S.Set FieldName -- ^ parsed columns from &columns parameter and payload
|
||||
, iColumns :: S.Set FieldName -- ^ parsed colums from &columns parameter and payload
|
||||
, iHeaders :: [(ByteString, ByteString)] -- ^ HTTP request headers
|
||||
, iCookies :: [(ByteString, ByteString)] -- ^ Request Cookies
|
||||
, iPath :: ByteString -- ^ Raw request path
|
||||
|
||||
+37
-51
@@ -22,7 +22,6 @@ import GHC.IO.Exception (IOErrorType (..))
|
||||
import System.IO.Error (ioeGetErrorType)
|
||||
|
||||
import Control.Monad.Except (liftEither)
|
||||
import Control.Monad.Extra (whenJust)
|
||||
import Data.Either.Combinators (mapLeft, whenLeft)
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.String (IsString (..))
|
||||
@@ -61,16 +60,15 @@ import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.TimeIt (timeItT)
|
||||
import PostgREST.Version (docsVersion, prettyVersion)
|
||||
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.List as L
|
||||
import Data.Streaming.Network (bindPortTCP,
|
||||
bindRandomPortTCP)
|
||||
import qualified Data.Text as T
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.HTTP.Types.Header as HTTP (hVary)
|
||||
import qualified Network.Socket as NS
|
||||
import PostgREST.Unix (createAndBindDomainSocket)
|
||||
import Protolude hiding (Handler)
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.List as L
|
||||
import Data.Streaming.Network (bindPortTCP,
|
||||
bindRandomPortTCP)
|
||||
import qualified Data.Text as T
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.Socket as NS
|
||||
import PostgREST.Unix (createAndBindDomainSocket)
|
||||
import Protolude hiding (Handler)
|
||||
|
||||
type Handler = ExceptT Error
|
||||
|
||||
@@ -80,10 +78,8 @@ run appState = do
|
||||
|
||||
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
|
||||
(mainSocket, adminSocket) <- initSockets conf
|
||||
let closeSockets = do
|
||||
whenJust adminSocket NS.close
|
||||
NS.close mainSocket
|
||||
Unix.installSignalHandlers observer closeSockets (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
|
||||
|
||||
Unix.installSignalHandlers observer (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
|
||||
|
||||
Listener.runListener appState
|
||||
|
||||
@@ -130,31 +126,30 @@ postgrest logLevel appState connWorker =
|
||||
Logger.middleware logLevel Auth.getRole $
|
||||
-- fromJust can be used, because the auth middleware will **always** add
|
||||
-- some AuthResult to the vault.
|
||||
\req respond -> do
|
||||
appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload
|
||||
case fromJust $ Auth.getResult req of
|
||||
Left err -> respond $ Error.errorResponseFor configClientErrorVerbosity err
|
||||
Right authResult -> do
|
||||
maybeSchemaCache <- AppState.getSchemaCache appState
|
||||
\req respond -> case fromJust $ Auth.getResult req of
|
||||
Left err -> respond $ Error.errorResponseFor err
|
||||
Right authResult -> do
|
||||
appConf <- AppState.getConfig appState -- the config must be read again because it can reload
|
||||
maybeSchemaCache <- AppState.getSchemaCache appState
|
||||
|
||||
let
|
||||
eitherResponse :: IO (Either Error Wai.Response)
|
||||
eitherResponse =
|
||||
runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req
|
||||
let
|
||||
eitherResponse :: IO (Either Error Wai.Response)
|
||||
eitherResponse =
|
||||
runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req
|
||||
|
||||
response <- either (Error.errorResponseFor configClientErrorVerbosity) identity <$> eitherResponse
|
||||
-- Launch the connWorker when the connection is down. The postgrest
|
||||
-- function can respond successfully (with a stale schema cache) before
|
||||
-- the connWorker is done. However, when there's an empty schema cache
|
||||
-- postgrest responds with the error `PGRST002`; this means that the schema
|
||||
-- cache is still loading, so we don't launch the connWorker here because
|
||||
-- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704
|
||||
-- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done
|
||||
when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker
|
||||
resp <- do
|
||||
delay <- AppState.getNextDelay appState
|
||||
return $ addRetryHint delay response
|
||||
respond resp
|
||||
response <- either Error.errorResponseFor identity <$> eitherResponse
|
||||
-- Launch the connWorker when the connection is down. The postgrest
|
||||
-- function can respond successfully (with a stale schema cache) before
|
||||
-- the connWorker is done. However, when there's an empty schema cache
|
||||
-- postgrest responds with the error `PGRST002`; this means that the schema
|
||||
-- cache is still loading, so we don't launch the connWorker here because
|
||||
-- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704
|
||||
-- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done
|
||||
when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker
|
||||
resp <- do
|
||||
delay <- AppState.getNextDelay appState
|
||||
return $ addRetryHint delay response
|
||||
respond resp
|
||||
|
||||
postgrestResponse
|
||||
:: AppState.AppState
|
||||
@@ -180,7 +175,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthRe
|
||||
timezones = dbTimezones sCache
|
||||
prefs = ApiRequest.userPreferences conf req timezones
|
||||
|
||||
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
|
||||
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestError $ ApiRequest.userApiRequest conf prefs req body
|
||||
(planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
|
||||
|
||||
let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest
|
||||
@@ -201,7 +196,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthRe
|
||||
liftEither eitherResp
|
||||
|
||||
(respTime, resp) <- withTiming $ do
|
||||
let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache
|
||||
let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
|
||||
status' = either Error.status Response.pgrstStatus response
|
||||
|
||||
-- TODO: see above obsQuery, only this obsQuery should remain after refactoring (because the QueryObs depends on the status)
|
||||
@@ -212,17 +207,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthRe
|
||||
|
||||
where
|
||||
toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response
|
||||
toWaiResponse timing (Response.PgrstResponse st hdrs bod) =
|
||||
Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod
|
||||
|
||||
serverTimingHeaders :: ServerTiming -> [HTTP.Header]
|
||||
serverTimingHeaders timing = [serverTimingHeader timing | configServerTimingEnabled]
|
||||
|
||||
varyHeader :: HTTP.Header
|
||||
varyHeader = (HTTP.hVary, "Accept, Prefer, Range")
|
||||
|
||||
varyHeaderPresent :: [HTTP.Header] -> Bool
|
||||
varyHeaderPresent = any (\(h, _v) -> h == HTTP.hVary)
|
||||
toWaiResponse timing (Response.PgrstResponse st hdrs bod) = Wai.responseLBS st (hdrs ++ ([serverTimingHeader timing | configServerTimingEnabled])) bod
|
||||
|
||||
withTiming :: Handler IO a -> Handler IO (Maybe Double, a)
|
||||
withTiming f = if configServerTimingEnabled
|
||||
@@ -286,3 +271,4 @@ initSockets AppConfig{..} = do
|
||||
Nothing -> pure Nothing
|
||||
|
||||
pure (sock, adminSock)
|
||||
|
||||
|
||||
+62
-69
@@ -1,7 +1,6 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE RecursiveDo #-}
|
||||
|
||||
module PostgREST.AppState
|
||||
( AppState
|
||||
@@ -46,6 +45,7 @@ import PostgREST.Version (prettyVersion)
|
||||
|
||||
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
||||
updateAction)
|
||||
import Control.Debounce
|
||||
import Control.Retry (RetryPolicy, RetryStatus (..), capDelay,
|
||||
exponentialBackoff, retrying,
|
||||
rsPreviousDelay)
|
||||
@@ -55,14 +55,13 @@ import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||
|
||||
import PostgREST.Auth.JwtCache (JwtCacheState, update)
|
||||
import PostgREST.Config (AppConfig (..),
|
||||
readAppConfig,
|
||||
toConnectionSettings)
|
||||
addFallbackAppName,
|
||||
readAppConfig)
|
||||
import PostgREST.Config.Database (queryDbSettings,
|
||||
queryPgVersion,
|
||||
queryRoleSettings)
|
||||
import PostgREST.Config.PgVersion (PgVersion (..),
|
||||
minimumPgVersion)
|
||||
import PostgREST.Debounce (makeDebouncer)
|
||||
import PostgREST.SchemaCache (SchemaCache (..),
|
||||
querySchemaCache,
|
||||
showSummary)
|
||||
@@ -78,7 +77,7 @@ data AppState = AppState
|
||||
-- | Schema cache
|
||||
, stateSchemaCache :: IORef (Maybe SchemaCache)
|
||||
-- | The schema cache status
|
||||
, stateSCacheStatus :: SchemaCacheStatus
|
||||
, stateSCacheStatus :: IORef SchemaCacheStatus
|
||||
-- | State of the LISTEN channel
|
||||
, stateIsListenerOn :: IORef Bool
|
||||
-- | starts the connection worker with a debounce
|
||||
@@ -101,11 +100,11 @@ data AppState = AppState
|
||||
, stateMetrics :: Metrics.MetricsState
|
||||
}
|
||||
|
||||
-- | Schema cache status.
|
||||
-- Empty means pending and full means loaded.
|
||||
newtype SchemaCacheStatus = SchemaCacheStatus
|
||||
{ getSCStatusMVar :: MVar ()
|
||||
}
|
||||
-- | Schema cache status
|
||||
data SchemaCacheStatus
|
||||
= SCLoaded
|
||||
| SCPending
|
||||
deriving Eq
|
||||
|
||||
init :: AppConfig -> IO AppState
|
||||
init conf@AppConfig{configLogLevel, configDbPoolSize} = do
|
||||
@@ -116,17 +115,17 @@ init conf@AppConfig{configLogLevel, configDbPoolSize} = do
|
||||
observer $ AppStartObs prettyVersion
|
||||
|
||||
pool <- initPool conf observer
|
||||
initWithPool pool conf loggerState metricsState observer
|
||||
initWithPool pool conf loggerState metricsState observer --{ stateSocketREST = sock, stateSocketAdmin = adminSock}
|
||||
|
||||
initWithPool :: SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
|
||||
initWithPool pool conf loggerState metricsState observer = mdo
|
||||
initWithPool pool conf 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
|
||||
<*> newIORef Nothing
|
||||
<*> newSchemaCacheStatus
|
||||
<*> newIORef SCPending
|
||||
<*> newIORef False
|
||||
<*> makeDebouncer (retryingSchemaCacheLoad appState *> threadDelay 100000) -- 100ms cooldown
|
||||
<*> pure (pure ())
|
||||
<*> newIORef conf
|
||||
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
|
||||
<*> myThreadId
|
||||
@@ -137,53 +136,48 @@ initWithPool pool conf loggerState metricsState observer = mdo
|
||||
<*> pure loggerState
|
||||
<*> pure metricsState
|
||||
|
||||
return appState
|
||||
deb <-
|
||||
let decisecond = 100000 in
|
||||
mkDebounce defaultDebounceSettings
|
||||
{ debounceAction = retryingSchemaCacheLoad appState
|
||||
, debounceFreq = decisecond
|
||||
, debounceEdge = leadingEdge -- runs the worker at the start and the end
|
||||
}
|
||||
|
||||
return appState { debouncedSCacheLoader = deb}
|
||||
|
||||
destroy :: AppState -> IO ()
|
||||
destroy = destroyPool
|
||||
|
||||
initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool
|
||||
initPool cfg@AppConfig{..} observer = do
|
||||
initPool AppConfig{..} observer = do
|
||||
SQL.acquire $ SQL.settings
|
||||
[ SQL.size configDbPoolSize
|
||||
, SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout
|
||||
, SQL.agingTimeout $ fromIntegral configDbPoolMaxLifetime
|
||||
, SQL.idlenessTimeout $ fromIntegral configDbPoolMaxIdletime
|
||||
, SQL.staticConnectionSettings $ toConnectionSettings identity cfg
|
||||
, SQL.staticConnectionSettings (toUtf8 $ addFallbackAppName prettyVersion configDbUri)
|
||||
, SQL.observationHandler $ observer . HasqlPoolObs
|
||||
]
|
||||
|
||||
-- | Run an action with a database connection.
|
||||
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
||||
usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} sess = do
|
||||
observer PoolRequest
|
||||
observer PoolRequest
|
||||
|
||||
res <- SQL.use statePool sess
|
||||
res <- SQL.use statePool sess
|
||||
|
||||
observer PoolRequestFullfilled
|
||||
observer PoolRequestFullfilled
|
||||
|
||||
whenLeft res (\case
|
||||
SQL.AcquisitionTimeoutUsageError ->
|
||||
observer PoolAcqTimeoutObs
|
||||
err@(SQL.ConnectionUsageError e) ->
|
||||
let failureMessage = BS.unpack $ fromMaybe mempty e in
|
||||
when (("FATAL: password authentication failed" `isInfixOf` failureMessage) || ("no password supplied" `isInfixOf` failureMessage)) $ do
|
||||
observer $ ExitDBFatalError ServerAuthError err
|
||||
killThread mainThreadId
|
||||
err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) ->
|
||||
handleResultError err tpl resultErr
|
||||
err@(SQL.SessionUsageError (SQL.PipelineError (SQL.ResultError resultErr))) ->
|
||||
-- Passing the empty template will not work for schema cache queries, see TODO further below.
|
||||
handleResultError err mempty resultErr
|
||||
err@(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) ->
|
||||
-- An error on the client-side, usually indicates problems with connection
|
||||
observer $ QueryErrorCodeHighObs err
|
||||
SQL.SessionUsageError (SQL.PipelineError (SQL.ClientError _)) -> pure ()
|
||||
)
|
||||
|
||||
return res
|
||||
where
|
||||
handleResultError err tpl resultErr = do
|
||||
whenLeft res (\case
|
||||
SQL.AcquisitionTimeoutUsageError ->
|
||||
observer $ PoolAcqTimeoutObs SQL.AcquisitionTimeoutUsageError
|
||||
err@(SQL.ConnectionUsageError e) ->
|
||||
let failureMessage = BS.unpack $ fromMaybe mempty e in
|
||||
when (("FATAL: password authentication failed" `isInfixOf` failureMessage) || ("no password supplied" `isInfixOf` failureMessage)) $ do
|
||||
observer $ ExitDBFatalError ServerAuthError err
|
||||
killThread mainThreadId
|
||||
err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) -> do
|
||||
case resultErr of
|
||||
SQL.UnexpectedResult{} -> do
|
||||
observer $ ExitDBFatalError ServerPgrstBug err
|
||||
@@ -216,6 +210,12 @@ usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} ses
|
||||
SQL.ServerError{} ->
|
||||
when (Error.status (Error.PgError False err) >= HTTP.status500) $
|
||||
observer $ QueryErrorCodeHighObs err
|
||||
err@(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) ->
|
||||
-- An error on the client-side, usually indicates problems wth connection
|
||||
observer $ QueryErrorCodeHighObs err
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
-- | Flush the connection pool so that any future use of the pool will
|
||||
-- use connections freshly established after this call.
|
||||
@@ -282,15 +282,18 @@ putIsListenerOn = atomicWriteIORef . stateIsListenerOn
|
||||
|
||||
isLoaded :: AppState -> IO Bool
|
||||
isLoaded x = do
|
||||
scacheLoaded <- isSchemaCacheLoaded x
|
||||
scacheStatus <- readIORef $ stateSCacheStatus x
|
||||
connEstablished <- isConnEstablished x
|
||||
return $ scacheLoaded && connEstablished
|
||||
return $ scacheStatus == SCLoaded && connEstablished
|
||||
|
||||
isPending :: AppState -> IO Bool
|
||||
isPending x = do
|
||||
scacheLoaded <- isSchemaCacheLoaded x
|
||||
scacheStatus <- readIORef $ stateSCacheStatus x
|
||||
connEstablished <- isConnEstablished x
|
||||
return $ not scacheLoaded || not connEstablished
|
||||
return $ scacheStatus == SCPending || not connEstablished
|
||||
|
||||
putSCacheStatus :: AppState -> SchemaCacheStatus -> IO ()
|
||||
putSCacheStatus = atomicWriteIORef . stateSCacheStatus
|
||||
|
||||
getObserver :: AppState -> ObservationHandler
|
||||
getObserver = stateObserver
|
||||
@@ -308,6 +311,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
when (rsIterNumber > 0) $ do
|
||||
let delay = fromMaybe 0 rsPreviousDelay `div` oneSecondInUs
|
||||
observer $ ConnectionRetryObs delay
|
||||
putNextListenerDelay appState delay
|
||||
|
||||
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
|
||||
)
|
||||
@@ -315,7 +319,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
qPgVersion :: IO (Maybe PgVersion)
|
||||
qPgVersion = do
|
||||
AppConfig{..} <- getConfig appState
|
||||
pgVersion <- usePool appState queryPgVersion
|
||||
pgVersion <- usePool appState (queryPgVersion False) -- No need to prepare the query here, as the connection might not be established
|
||||
case pgVersion of
|
||||
Left e -> do
|
||||
observer $ QueryPgVersionError e
|
||||
@@ -343,27 +347,28 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
qSchemaCache = do
|
||||
conf@AppConfig{..} <- getConfig appState
|
||||
(resultTime, result) <-
|
||||
timeItT $ usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
||||
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
||||
timeItT $ usePool appState (transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
||||
case result of
|
||||
Left e -> do
|
||||
markSchemaCachePending appState
|
||||
putSCacheStatus appState SCPending
|
||||
putSchemaCache appState Nothing
|
||||
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
|
||||
return Nothing
|
||||
|
||||
Right sCache -> do
|
||||
-- IMPORTANT: While the pending schema cache state starts from running the above querySchemaCache, only at this stage we block API requests due to the usage of an
|
||||
-- IORef on putSchemaCache. This is why schema cache status is marked as pending here to signal the Admin server (using isPending) that we're on a recovery state.
|
||||
markSchemaCachePending appState
|
||||
-- IORef on putSchemaCache. This is why SCacheStatus is put at SCPending here to signal the Admin server (using isPending) that we're on a recovery state.
|
||||
putSCacheStatus appState SCPending
|
||||
putSchemaCache appState $ Just sCache
|
||||
(loadTime, summary) <- timeItT (evaluate $ showSummary sCache)
|
||||
-- Flush the pool after loading the schema cache to reset any stale session cache entries
|
||||
-- We do it after successfully querying the schema cache (because this can fail and during retries we would flush the pool repeatedly unnecessarily)
|
||||
-- and after marking sCacheStatus as pending,
|
||||
flushPool appState
|
||||
observer $ SchemaCacheQueriedObs resultTime $ dbQueryTimings sCache
|
||||
observer $ SchemaCacheLoadedObs loadTime summary
|
||||
markSchemaCacheLoaded appState
|
||||
observer $ SchemaCacheQueriedObs resultTime
|
||||
(t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache
|
||||
observer $ SchemaCacheLoadedObs t
|
||||
putSCacheStatus appState SCLoaded
|
||||
return $ Just sCache
|
||||
|
||||
shouldRetry :: RetryStatus -> (Maybe PgVersion, Maybe SchemaCache) -> IO Bool
|
||||
@@ -379,18 +384,6 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
|
||||
oneSecondInUs = 1000000 -- one second in microseconds
|
||||
|
||||
newSchemaCacheStatus :: IO SchemaCacheStatus
|
||||
newSchemaCacheStatus = SchemaCacheStatus <$> newEmptyMVar
|
||||
|
||||
markSchemaCachePending :: AppState -> IO ()
|
||||
markSchemaCachePending = void . tryTakeMVar . getSCStatusMVar . stateSCacheStatus
|
||||
|
||||
markSchemaCacheLoaded :: AppState -> IO ()
|
||||
markSchemaCacheLoaded = void . (`tryPutMVar` ()) . getSCStatusMVar . stateSCacheStatus
|
||||
|
||||
isSchemaCacheLoaded :: AppState -> IO Bool
|
||||
isSchemaCacheLoaded = fmap not . isEmptyMVar . getSCStatusMVar . stateSCacheStatus
|
||||
|
||||
-- | Reads the in-db config and reads the config file again
|
||||
-- | 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 ()
|
||||
@@ -399,7 +392,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
|
||||
pgVer <- getPgVersion appState
|
||||
dbSettings <-
|
||||
if configDbConfig conf then do
|
||||
qDbSettings <- usePool appState (queryDbSettings (quoteQi <$> configDbPreConfig conf))
|
||||
qDbSettings <- usePool appState (queryDbSettings (quoteQi <$> configDbPreConfig conf) (configDbPreparedStatements conf))
|
||||
case qDbSettings of
|
||||
Left e -> do
|
||||
observer $ ConfigReadErrorObs e
|
||||
@@ -409,7 +402,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
|
||||
pure mempty
|
||||
(roleSettings, roleIsolationLvl) <-
|
||||
if configDbConfig conf then do
|
||||
rSettings <- usePool appState (queryRoleSettings pgVer)
|
||||
rSettings <- usePool appState (queryRoleSettings pgVer (configDbPreparedStatements conf))
|
||||
case rSettings of
|
||||
Left e -> do
|
||||
observer $ QueryRoleSettingsErrorObs e
|
||||
|
||||
@@ -16,10 +16,14 @@ module PostgREST.Auth.Jwt
|
||||
, parseClaims) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.Key as K
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Internal as BS
|
||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||
import qualified Data.Scientific as Sci
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Vector as V
|
||||
import qualified Jose.Jwk as JWT
|
||||
import qualified Jose.Jwt as JWT
|
||||
|
||||
@@ -29,11 +33,12 @@ import Data.Text ()
|
||||
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
|
||||
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
|
||||
|
||||
import PostgREST.Auth.Types (AuthResult (..))
|
||||
import PostgREST.Config (AppConfig (..), audMatchesCfg)
|
||||
import PostgREST.Config.JSPath (walkJSPath)
|
||||
import PostgREST.Error (Error (..), JwtClaimsError (..),
|
||||
JwtDecodeError (..), JwtError (..))
|
||||
import PostgREST.Auth.Types (AuthResult (..))
|
||||
import PostgREST.Config (AppConfig (..), FilterExp (..), JSPath,
|
||||
JSPathExp (..), audMatchesCfg)
|
||||
import PostgREST.Error (Error (..),
|
||||
JwtClaimsError (AudClaimNotStringOrArray, ExpClaimNotNumber, IatClaimNotNumber, JWTExpired, JWTIssuedAtFuture, JWTNotInAudience, JWTNotYetValid, NbfClaimNotNumber, ParsingClaimsFailed),
|
||||
JwtDecodeError (..), JwtError (..))
|
||||
|
||||
import Data.Aeson ((.:?))
|
||||
import Data.Aeson.Types (parseMaybe)
|
||||
@@ -90,10 +95,13 @@ checkForErrors time audMatches = mconcat
|
||||
parseToken :: (MonadError Error m, MonadIO m) => JwkSet -> ByteString -> m JWT.JwtContent
|
||||
parseToken _ "" = throwError $ JwtErr $ JwtDecodeErr EmptyAuthHeader
|
||||
parseToken secret tkn = do
|
||||
-- secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
|
||||
tknWith3Parts <- hasThreeParts tkn
|
||||
eitherContent <- liftIO $ JWT.decode (JWT.keys secret) Nothing tknWith3Parts
|
||||
liftEither . mapLeft (JwtErr . jwtDecodeError) $ eitherContent
|
||||
--liftEither $ mapLeft JwtErr $ verifyClaims content
|
||||
where
|
||||
--hasThreeParts :: ByteString -> Either Error ByteString
|
||||
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
|
||||
3 -> pure token
|
||||
n -> throwError $ JwtErr $ JwtDecodeErr $ UnexpectedParts n
|
||||
@@ -116,10 +124,28 @@ parseClaims cfg@AppConfig{configJwtRoleClaimKey, configDbAnonRole} time mclaims
|
||||
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
|
||||
unquoted <$> walkJSPath (Just $ JSON.Object mclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
||||
pure AuthResult
|
||||
{ authClaims = mclaims
|
||||
{ authClaims = mclaims & KM.insert "role" (JSON.toJSON $ decodeUtf8 role)
|
||||
, authRole = role
|
||||
}
|
||||
where
|
||||
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
|
||||
walkJSPath x [] = x
|
||||
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest
|
||||
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
|
||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (EqualsCond txt)] = findFirstMatch (==) txt ar
|
||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (NotEqualsCond txt)] = findFirstMatch (/=) txt ar
|
||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (StartsWithCond txt)] = findFirstMatch T.isPrefixOf txt ar
|
||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (EndsWithCond txt)] = findFirstMatch T.isSuffixOf txt ar
|
||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (ContainsCond txt)] = findFirstMatch T.isInfixOf txt ar
|
||||
walkJSPath _ _ = Nothing
|
||||
|
||||
findFirstMatch matchWith pattern = foldr checkMatch Nothing
|
||||
where
|
||||
checkMatch (JSON.String txt) acc
|
||||
| pattern `matchWith` txt = Just $ JSON.String txt
|
||||
| otherwise = acc
|
||||
checkMatch _ acc = acc
|
||||
|
||||
unquoted :: JSON.Value -> BS.ByteString
|
||||
unquoted (JSON.String t) = encodeUtf8 t
|
||||
unquoted v = LBS.toStrict $ JSON.encode v
|
||||
|
||||
@@ -6,9 +6,7 @@ import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.ByteString as BS
|
||||
|
||||
-- |
|
||||
-- Parse and store result for JWT Claims. Can be accessed in
|
||||
-- db through GUCs (for RLS etc)
|
||||
-- | Parse result for JWT Claims
|
||||
data AuthResult = AuthResult
|
||||
{ authClaims :: KM.KeyMap JSON.Value
|
||||
, authRole :: BS.ByteString
|
||||
|
||||
@@ -62,7 +62,9 @@ dumpSchema :: AppState -> IO LBS.ByteString
|
||||
dumpSchema appState = do
|
||||
conf@AppConfig{..} <- AppState.getConfig appState
|
||||
result <-
|
||||
AppState.usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
||||
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
||||
AppState.usePool appState
|
||||
(transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
||||
case result of
|
||||
Left e -> do
|
||||
let observer = AppState.getObserver appState
|
||||
|
||||
+22
-82
@@ -9,7 +9,6 @@ Description : Manages PostgREST configuration type and parser.
|
||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module PostgREST.Config
|
||||
( AppConfig (..)
|
||||
@@ -28,25 +27,21 @@ module PostgREST.Config
|
||||
, parseSecret
|
||||
, addFallbackAppName
|
||||
, addTargetSessionAttrs
|
||||
, toConnectionSettings
|
||||
, exampleConfigFile
|
||||
, audMatchesCfg
|
||||
, Verbosity (..)
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.Configurator as C
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.String as S
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Hasql.Connection.Setting as SQL
|
||||
import qualified Hasql.Connection.Setting.Connection as SQL
|
||||
import qualified Jose.Jwa as JWT
|
||||
import qualified Jose.Jwk as JWT
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.Configurator as C
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.String as S
|
||||
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.Monad (fail)
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
@@ -68,18 +63,16 @@ import PostgREST.Config.JSPath (FilterExp (..), JSPath,
|
||||
pRoleClaimKey)
|
||||
import PostgREST.Config.Proxy (Proxy (..),
|
||||
isMalformedProxyUri, toURI)
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi,
|
||||
toQi)
|
||||
|
||||
import PostgREST.Version (prettyVersion)
|
||||
import Protolude hiding (Proxy, toList)
|
||||
import Protolude hiding (Proxy, toList)
|
||||
|
||||
audMatchesCfg :: AppConfig -> Text -> Bool
|
||||
audMatchesCfg = maybe (const True) (==) . configJwtAudience
|
||||
|
||||
data AppConfig = AppConfig
|
||||
{ configAppSettings :: [(Text, Text)]
|
||||
, configClientErrorVerbosity :: Verbosity
|
||||
, configDbAggregates :: Bool
|
||||
, configDbAnonRole :: Maybe BS.ByteString
|
||||
, configDbChannel :: Text
|
||||
@@ -99,7 +92,6 @@ data AppConfig = AppConfig
|
||||
, configDbSchemas :: NonEmpty Text
|
||||
, configDbConfig :: Bool
|
||||
, configDbPreConfig :: Maybe QualifiedIdentifier
|
||||
, configDbTimezoneEnabled :: Bool
|
||||
, configDbTxAllowOverride :: Bool
|
||||
, configDbTxRollbackAll :: Bool
|
||||
, configDbUri :: Text
|
||||
@@ -127,8 +119,6 @@ data AppConfig = AppConfig
|
||||
, configRoleSettings :: RoleSettings
|
||||
, configRoleIsoLvl :: RoleIsolationLvl
|
||||
, configInternalSCQuerySleep :: Maybe Int32
|
||||
, configInternalSCLoadSleep :: Maybe Int32
|
||||
, configInternalSCRelLoadSleep :: Maybe Int32
|
||||
}
|
||||
|
||||
data LogLevel = LogCrit | LogError | LogWarn | LogInfo | LogDebug
|
||||
@@ -142,15 +132,6 @@ dumpLogLevel = \case
|
||||
LogInfo -> "info"
|
||||
LogDebug -> "debug"
|
||||
|
||||
data Verbosity
|
||||
= Minimal
|
||||
| Verbose
|
||||
|
||||
dumpClientErrorVerbosity :: Verbosity -> Text
|
||||
dumpClientErrorVerbosity = \case
|
||||
Minimal -> "minimal"
|
||||
Verbose -> "verbose"
|
||||
|
||||
data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
|
||||
deriving Eq
|
||||
|
||||
@@ -167,8 +148,7 @@ toText conf =
|
||||
where
|
||||
-- apply conf to all pgrst settings
|
||||
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
||||
[("client-error-verbosity", q . dumpClientErrorVerbosity . configClientErrorVerbosity)
|
||||
,("db-aggregates-enabled", T.toLower . show . configDbAggregates)
|
||||
[("db-aggregates-enabled", T.toLower . show . configDbAggregates)
|
||||
,("db-anon-role", q . T.decodeUtf8 . fromMaybe "" . configDbAnonRole)
|
||||
,("db-channel", q . configDbChannel)
|
||||
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
|
||||
@@ -187,7 +167,6 @@ toText conf =
|
||||
,("db-schemas", q . T.intercalate "," . toList . configDbSchemas)
|
||||
,("db-config", T.toLower . show . configDbConfig)
|
||||
,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig)
|
||||
,("db-timezone-enabled", T.toLower . show . configDbTimezoneEnabled)
|
||||
,("db-tx-end", q . showTxEnd)
|
||||
,("db-uri", q . configDbUri)
|
||||
,("jwt-aud", q . fromMaybe mempty . configJwtAudience)
|
||||
@@ -217,10 +196,6 @@ toText conf =
|
||||
-- quote strings and replace " with \"
|
||||
q s = "\"" <> T.replace "\"" "\\\"" s <> "\""
|
||||
|
||||
dumpQi :: QualifiedIdentifier -> Text
|
||||
dumpQi (QualifiedIdentifier s i) =
|
||||
(if T.null s then mempty else s <> ".") <> i
|
||||
|
||||
showTxEnd c = case (configDbTxRollbackAll c, configDbTxAllowOverride c) of
|
||||
( False, False ) -> "commit"
|
||||
( False, True ) -> "commit-allow-override"
|
||||
@@ -273,7 +248,6 @@ parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> Rol
|
||||
parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
AppConfig
|
||||
<$> parseAppSettings "app.settings"
|
||||
<*> parseErrorVerbosity "client-error-verbosity"
|
||||
<*> (fromMaybe False <$> optBool "db-aggregates-enabled")
|
||||
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
|
||||
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
||||
@@ -294,10 +268,10 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
||||
<*> (fmap toQi <$> optWithAlias (optString "db-root-spec")
|
||||
(optString "root-spec"))
|
||||
<*> parseDbSchemas "db-schemas" "db-schema"
|
||||
<*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optString "db-schemas")
|
||||
(optString "db-schema"))
|
||||
<*> (fromMaybe True <$> optBool "db-config")
|
||||
<*> (fmap toQi <$> optString "db-pre-config")
|
||||
<*> (fromMaybe True <$> optBool "db-timezone-enabled")
|
||||
<*> parseTxEnd "db-tx-end" snd
|
||||
<*> parseTxEnd "db-tx-end" fst
|
||||
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
|
||||
@@ -328,17 +302,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> pure roleSettings
|
||||
<*> pure roleIsolationLvl
|
||||
<*> optInt "internal-schema-cache-query-sleep"
|
||||
<*> optInt "internal-schema-cache-load-sleep"
|
||||
<*> optInt "internal-schema-cache-relationship-load-sleep"
|
||||
where
|
||||
parseErrorVerbosity :: C.Key -> C.Parser C.Config Verbosity
|
||||
parseErrorVerbosity k =
|
||||
optString k >>= \case
|
||||
Nothing -> pure Verbose -- default
|
||||
Just "minimal" -> pure Minimal
|
||||
Just "verbose" -> pure Verbose
|
||||
Just _ -> fail "Invalid client-error-verbosity. Check your configuration."
|
||||
|
||||
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
|
||||
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
|
||||
where
|
||||
@@ -357,18 +321,6 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
Just asp | asp == serverPort -> fail "admin-server-port cannot be the same as server-port"
|
||||
| otherwise -> pure $ Just asp
|
||||
|
||||
parseDbSchemas :: C.Key -> C.Key -> C.Parser C.Config (NonEmpty Text)
|
||||
parseDbSchemas k al =
|
||||
optWithAlias (optString k) (optString al) >>= \case
|
||||
Nothing -> pure $ fromList ["public"]
|
||||
Just s
|
||||
| "pg_catalog" `elem` schemas -> fail (errMsg "pg_catalog")
|
||||
| "information_schema" `elem` schemas -> fail (errMsg "information_schema")
|
||||
| otherwise -> pure $ fromList schemas
|
||||
where
|
||||
schemas = splitOnCommas s
|
||||
errMsg x = "db-schemas does not allow schema: '" <> x <> "'"
|
||||
|
||||
parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
|
||||
parseSocketFileMode k =
|
||||
optString k >>= \case
|
||||
@@ -621,10 +573,10 @@ pgConnString conn | uriDesignator `T.isPrefixOf` conn || shortUriDesignator `T.i
|
||||
-- >>> addFallbackAppName ver "postgres://admin2:?pass?special?@localhost:5432/postgres"
|
||||
-- "postgres://admin2:?pass?special?@localhost:5432/postgres?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||
--
|
||||
-- >>> addFallbackAppName ver "postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass"
|
||||
-- addFallbackAppName ver "postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass"
|
||||
-- "postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||
--
|
||||
-- >>> addFallbackAppName ver "postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass"
|
||||
-- addFallbackAppName ver "postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass"
|
||||
-- "postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||
addFallbackAppName :: ByteString -> Text -> Text
|
||||
addFallbackAppName version dbUri = addConnStringOption dbUri "fallback_application_name" pgrstVer
|
||||
@@ -651,12 +603,6 @@ addFallbackAppName version dbUri = addConnStringOption dbUri "fallback_applicati
|
||||
addTargetSessionAttrs :: Text -> Text
|
||||
addTargetSessionAttrs dbUri = addConnStringOption dbUri "target_session_attrs" "read-write"
|
||||
|
||||
toConnectionSettings :: (Text -> Text) -> AppConfig -> [SQL.Setting]
|
||||
toConnectionSettings transformUri AppConfig{configDbUri, configDbPreparedStatements} =
|
||||
[ SQL.connection $ SQL.string $ transformUri . addFallbackAppName prettyVersion $ configDbUri
|
||||
, SQL.usePreparedStatements configDbPreparedStatements
|
||||
]
|
||||
|
||||
addConnStringOption :: Text -> Text -> Text -> Text
|
||||
addConnStringOption dbUri key val = dbUri <>
|
||||
case pgConnString dbUri of
|
||||
@@ -677,9 +623,6 @@ exampleConfigFile = S.unlines
|
||||
[ "## Admin server used for checks. It's disabled by default unless a port is specified."
|
||||
, "# admin-server-port = 3001"
|
||||
, ""
|
||||
, "# PostgREST error json verbosity config"
|
||||
, "# client-error-verbosity = \"verbose\""
|
||||
, ""
|
||||
, "## The database role to use when no client authentication is provided"
|
||||
, "# db-anon-role = \"anon\""
|
||||
, ""
|
||||
@@ -729,19 +672,16 @@ exampleConfigFile = S.unlines
|
||||
, "## The name of which database schema to expose to REST clients"
|
||||
, "db-schemas = \"public\""
|
||||
, ""
|
||||
, "## Enable quering pg_timezone_names from db"
|
||||
, "# db-timezone-enabled = true"
|
||||
, ""
|
||||
, "## How to terminate database transactions"
|
||||
, "## Possible values are:"
|
||||
, "## commit (default)"
|
||||
, "## Transaction is always committed, this can not be overridden"
|
||||
, "## Transaction is always committed, this can not be overriden"
|
||||
, "## commit-allow-override"
|
||||
, "## Transaction is committed, but can be overridden with Prefer tx=rollback header"
|
||||
, "## Transaction is committed, but can be overriden with Prefer tx=rollback header"
|
||||
, "## rollback"
|
||||
, "## Transaction is always rolled back, this can not be overridden"
|
||||
, "## Transaction is always rolled back, this can not be overriden"
|
||||
, "## rollback-allow-override"
|
||||
, "## Transaction is rolled back, but can be overridden with Prefer tx=commit header"
|
||||
, "## Transaction is rolled back, but can be overriden with Prefer tx=commit header"
|
||||
, "db-tx-end = \"commit\""
|
||||
, ""
|
||||
, "## The standard connection URI format, documented at"
|
||||
|
||||
@@ -16,6 +16,7 @@ import Control.Arrow ((***))
|
||||
import PostgREST.Config.PgVersion (PgVersion (..), pgVersion150)
|
||||
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.Text as T
|
||||
|
||||
import qualified Hasql.Decoders as HD
|
||||
import qualified Hasql.Encoders as HE
|
||||
@@ -32,8 +33,8 @@ type RoleSettings = (HM.HashMap ByteString (HM.HashMap ByteString ByteString
|
||||
type RoleIsolationLvl = HM.HashMap ByteString SQL.IsolationLevel
|
||||
type TimezoneNames = Set Text -- cache timezone names for prefer timezone=
|
||||
|
||||
toIsolationLevel :: (Eq a, IsString a) => a -> SQL.IsolationLevel
|
||||
toIsolationLevel a = case a of
|
||||
toIsolationLevel :: Text -> SQL.IsolationLevel
|
||||
toIsolationLevel a = case T.toLower a of
|
||||
"repeatable read" -> SQL.RepeatableRead
|
||||
"serializable" -> SQL.Serializable
|
||||
_ -> SQL.ReadCommitted
|
||||
@@ -46,7 +47,6 @@ dbSettingsNames :: [Text]
|
||||
dbSettingsNames =
|
||||
(prefix <>) <$>
|
||||
["db_aggregates_enabled"
|
||||
,"client_error_verbosity"
|
||||
,"db_anon_role"
|
||||
,"db_pre_config"
|
||||
,"db_extra_search_path"
|
||||
@@ -56,7 +56,6 @@ dbSettingsNames =
|
||||
,"db_prepared_statements"
|
||||
,"db_root_spec"
|
||||
,"db_schemas"
|
||||
,"db_timezone_enabled"
|
||||
,"db_tx_end"
|
||||
,"db_hoisted_tx_settings"
|
||||
,"jwt_aud"
|
||||
@@ -72,8 +71,8 @@ dbSettingsNames =
|
||||
,"server_timing_enabled"
|
||||
]
|
||||
|
||||
queryPgVersion :: Session PgVersion
|
||||
queryPgVersion = statement mempty $ pgVersionStatement False
|
||||
queryPgVersion :: Bool -> Session PgVersion
|
||||
queryPgVersion prepared = statement mempty $ pgVersionStatement prepared
|
||||
|
||||
pgVersionStatement :: Bool -> SQL.Statement () PgVersion
|
||||
pgVersionStatement = SQL.Statement sql HE.noParams versionRow
|
||||
@@ -92,9 +91,10 @@ pgVersionStatement = SQL.Statement sql HE.noParams versionRow
|
||||
--
|
||||
-- The example above will result in <prefix>jwt_aud = 'val'
|
||||
-- A setting on the database only will have no effect: ALTER DATABASE postgres SET <prefix>jwt_aud = 'xx'
|
||||
queryDbSettings :: Maybe Text -> Session [(Text, Text)]
|
||||
queryDbSettings preConfFunc =
|
||||
SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings True
|
||||
queryDbSettings :: Maybe Text -> Bool -> Session [(Text, Text)]
|
||||
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 = encodeUtf8 [trimming|
|
||||
WITH
|
||||
@@ -132,9 +132,10 @@ queryDbSettings preConfFunc =
|
||||
|]::Text
|
||||
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
||||
|
||||
queryRoleSettings :: PgVersion -> Session (RoleSettings, RoleIsolationLvl)
|
||||
queryRoleSettings pgVer =
|
||||
SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) True
|
||||
queryRoleSettings :: PgVersion -> Bool -> Session (RoleSettings, RoleIsolationLvl)
|
||||
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 = encodeUtf8 [trimming|
|
||||
with
|
||||
@@ -148,7 +149,7 @@ queryRoleSettings pgVer =
|
||||
SELECT
|
||||
rolname,
|
||||
substr(setting, 1, strpos(setting, '=') - 1) as key,
|
||||
lower(substr(setting, strpos(setting, '=') + 1)) as value
|
||||
substr(setting, strpos(setting, '=') + 1) as value
|
||||
FROM role_setting
|
||||
),
|
||||
iso_setting AS (
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
module PostgREST.Config.JSPath
|
||||
( JSPath
|
||||
, JSPathExp(..)
|
||||
, FilterExp(..)
|
||||
, dumpJSPath
|
||||
, pRoleClaimKey
|
||||
, walkJSPath
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.Key as K
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Vector as V
|
||||
import qualified Text.ParserCombinators.Parsec as P
|
||||
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
@@ -29,10 +22,9 @@ type JSPath = [JSPathExp]
|
||||
-- NOTE: We only accept one JSPFilter expr (at the end of input)
|
||||
-- | jspath expression
|
||||
data JSPathExp
|
||||
= JSPKey Text -- .property or ."property-dash"
|
||||
| JSPIdx Int -- [0]
|
||||
| JSPSlice (Maybe Int) (Maybe Int) -- [0:5] or [0:] or [:5] or [:]
|
||||
| JSPFilter FilterExp -- [?(@ == "match")]
|
||||
= JSPKey Text -- .property or ."property-dash"
|
||||
| JSPIdx Int -- [0]
|
||||
| JSPFilter FilterExp -- [?(@ == "match")]
|
||||
|
||||
data FilterExp
|
||||
= EqualsCond Text
|
||||
@@ -45,7 +37,6 @@ dumpJSPath :: JSPathExp -> Text
|
||||
-- TODO: this needs to be quoted properly for special chars
|
||||
dumpJSPath (JSPKey k) = "." <> show k
|
||||
dumpJSPath (JSPIdx i) = "[" <> show i <> "]"
|
||||
dumpJSPath (JSPSlice s e) = "[" <> maybe "" show s <> ":" <> maybe "" show e <> "]"
|
||||
dumpJSPath (JSPFilter cond) = "[?(@" <> expr <> ")]"
|
||||
where
|
||||
expr =
|
||||
@@ -56,35 +47,6 @@ dumpJSPath (JSPFilter cond) = "[?(@" <> expr <> ")]"
|
||||
EndsWithCond text -> " ==^ " <> show text
|
||||
ContainsCond text -> " *== " <> show text
|
||||
|
||||
-- | Evaluate JSPath on a JSON
|
||||
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
|
||||
walkJSPath x [] = x
|
||||
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest
|
||||
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
|
||||
walkJSPath (Just (JSON.String str)) (JSPSlice start end:rest) =
|
||||
let
|
||||
len = T.length str
|
||||
|
||||
norm :: Maybe Int -> Maybe Int -- Normalize negative indices to positive
|
||||
norm = fmap (\i -> max 0 $ min len $ if i < 0 then len + i else i)
|
||||
|
||||
s = fromMaybe 0 $ norm start -- normalized start index
|
||||
e = fromMaybe len $ norm end -- normalized end index
|
||||
slicedString = if s >= e then T.empty else T.take (e-s) $ T.drop s str
|
||||
in
|
||||
walkJSPath (Just $ JSON.String slicedString) rest
|
||||
|
||||
walkJSPath (Just (JSON.Array ar)) (JSPFilter jspFilter:rest) = case jspFilter of
|
||||
EqualsCond txt -> walkJSPath (findFirstMatch (==) txt ar) rest
|
||||
NotEqualsCond txt -> walkJSPath (findFirstMatch (/=) txt ar) rest
|
||||
StartsWithCond txt -> walkJSPath (findFirstMatch T.isPrefixOf txt ar) rest
|
||||
EndsWithCond txt -> walkJSPath (findFirstMatch T.isSuffixOf txt ar) rest
|
||||
ContainsCond txt -> walkJSPath (findFirstMatch T.isInfixOf txt ar) rest
|
||||
where
|
||||
findFirstMatch matchWith pattern = find (\case
|
||||
JSON.String txt -> pattern `matchWith` txt
|
||||
_ -> False)
|
||||
walkJSPath _ _ = Nothing
|
||||
|
||||
-- Used for the config value "role-claim-key"
|
||||
pRoleClaimKey :: Text -> Either Text JSPath
|
||||
@@ -95,7 +57,7 @@ pJSPath :: P.Parser JSPath
|
||||
pJSPath = P.many1 pJSPathExp <* P.eof
|
||||
|
||||
pJSPathExp :: P.Parser JSPathExp
|
||||
pJSPathExp = P.try pJSPKey <|> P.try pJSPFilter <|> P.try pJSPIdx <|> pJSPSlice
|
||||
pJSPathExp = pJSPKey <|> pJSPFilter <|> pJSPIdx
|
||||
|
||||
pJSPKey :: P.Parser JSPathExp
|
||||
pJSPKey = do
|
||||
@@ -110,25 +72,13 @@ pJSPIdx = do
|
||||
P.char ']'
|
||||
return (JSPIdx num) <?> "pJSPIdx: JSPath array index"
|
||||
|
||||
pJSPSlice :: P.Parser JSPathExp
|
||||
pJSPSlice = do
|
||||
P.char '['
|
||||
startSign <- P.optionMaybe $ P.char '-'
|
||||
startIndex <- P.optionMaybe (read <$> P.many1 P.digit)
|
||||
P.char ':'
|
||||
endSign <- P.optionMaybe $ P.char '-'
|
||||
endIndex <- P.optionMaybe (read <$> P.many1 P.digit)
|
||||
P.char ']'
|
||||
let start' = if isJust startSign then ((-1) *) <$> startIndex else startIndex
|
||||
end' = if isJust endSign then ((-1) *) <$> endIndex else endIndex
|
||||
return (JSPSlice start' end') <?> "pJSPSlice: JSPath string slice"
|
||||
|
||||
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
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
module PostgREST.Config.PgVersion
|
||||
( PgVersion(..)
|
||||
, minimumPgVersion
|
||||
, pgVersion140
|
||||
, pgVersion150
|
||||
, pgVersion170
|
||||
, pgVersion180
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
@@ -25,7 +25,10 @@ instance Ord PgVersion where
|
||||
|
||||
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
||||
minimumPgVersion :: PgVersion
|
||||
minimumPgVersion = pgVersion140
|
||||
minimumPgVersion = pgVersion130
|
||||
|
||||
pgVersion130 :: PgVersion
|
||||
pgVersion130 = PgVersion 130000 "13.0" "13.0"
|
||||
|
||||
pgVersion140 :: PgVersion
|
||||
pgVersion140 = PgVersion 140000 "14.0" "14.0"
|
||||
@@ -35,6 +38,3 @@ pgVersion150 = PgVersion 150000 "15.0" "15.0"
|
||||
|
||||
pgVersion170 :: PgVersion
|
||||
pgVersion170 = PgVersion 170000 "17.0" "17.0"
|
||||
|
||||
pgVersion180 :: PgVersion
|
||||
pgVersion180 = PgVersion 180000 "18.0" "18.0"
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
module PostgREST.Debounce
|
||||
( makeDebouncer) where
|
||||
|
||||
import Protolude
|
||||
|
||||
-- | Make a new debouncer action. An internal "worker" thread runs forever
|
||||
-- ensuring "action" runs when the "trigger" is called. The "action" is only
|
||||
-- executed once over a burst of calls.
|
||||
makeDebouncer :: IO () -> IO (IO ())
|
||||
makeDebouncer action = do
|
||||
flag <- newEmptyMVar
|
||||
|
||||
let worker = forever $ do
|
||||
takeMVar flag
|
||||
action
|
||||
trigger = void $ tryPutMVar flag ()
|
||||
|
||||
void $ forkIO worker
|
||||
pure trigger
|
||||
+192
-73
@@ -42,7 +42,6 @@ import Network.HTTP.Types.Header (Header)
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import qualified PostgREST.MediaType as MediaType
|
||||
|
||||
import PostgREST.Config (Verbosity (..))
|
||||
import PostgREST.SchemaCache (SchemaCache (SchemaCache, dbTablesFuzzyIndex))
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||
Schema)
|
||||
@@ -52,40 +51,22 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
RelationshipsMap)
|
||||
import PostgREST.SchemaCache.Routine (Routine (..),
|
||||
RoutineParam (..))
|
||||
|
||||
import PostgREST.Error.Types
|
||||
|
||||
import Protolude
|
||||
|
||||
-- | Encode Error to ByteString
|
||||
errorPayload :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> LByteString
|
||||
errorPayload verb = JSON.encode . toJsonPgrstError verb
|
||||
where
|
||||
toJsonPgrstError :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> JSON.Value
|
||||
toJsonPgrstError Verbose err = JSON.object [
|
||||
"code" .= code err
|
||||
, "message" .= message err
|
||||
, "details" .= details err
|
||||
, "hint" .= hint err
|
||||
]
|
||||
toJsonPgrstError Minimal err = JSON.object [
|
||||
"code" .= code err
|
||||
, "message" .= message err
|
||||
]
|
||||
class (ErrorBody a, JSON.ToJSON a) => PgrstError a where
|
||||
status :: a -> HTTP.Status
|
||||
headers :: a -> [Header]
|
||||
|
||||
-- | Create HTTP response from Error
|
||||
errorResponseFor :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> Response
|
||||
errorResponseFor verb err =
|
||||
let
|
||||
baseHeader = MediaType.toContentType MTApplicationJSON
|
||||
cLHeader body = (,) "Content-Length" (show $ LBS.length body) :: Header
|
||||
pSHeader code' = ("Proxy-Status", "PostgREST; error=" <> T.encodeUtf8 code')
|
||||
in
|
||||
responseLBS (status err) (baseHeader : cLHeader (errorPayload verb err) : pSHeader (code err) : headers err) $ errorPayload verb err
|
||||
errorPayload :: a -> LByteString
|
||||
errorPayload = JSON.encode
|
||||
|
||||
class ErrorHeaders a where
|
||||
status :: a -> HTTP.Status
|
||||
headers :: a -> [Header]
|
||||
errorResponseFor :: a -> Response
|
||||
errorResponseFor 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
|
||||
@@ -93,7 +74,49 @@ class ErrorBody a where
|
||||
details :: a -> Maybe JSON.Value
|
||||
hint :: a -> Maybe JSON.Value
|
||||
|
||||
instance ErrorHeaders ApiRequestError where
|
||||
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 MediaTypeError{} = HTTP.status406
|
||||
status InvalidBody{} = HTTP.status400
|
||||
@@ -217,7 +240,20 @@ instance ErrorBody ApiRequestError where
|
||||
|
||||
hint _ = Nothing
|
||||
|
||||
instance ErrorHeaders SchemaCacheError where
|
||||
instance JSON.ToJSON ApiRequestError where
|
||||
toJSON err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
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 SchemaCache
|
||||
deriving Show
|
||||
|
||||
instance PgrstError SchemaCacheError where
|
||||
status AmbiguousRelBetween{} = HTTP.status300
|
||||
status AmbiguousRpc{} = HTTP.status300
|
||||
status NoRelBetween{} = HTTP.status400
|
||||
@@ -281,6 +317,18 @@ instance ErrorBody SchemaCacheError where
|
||||
|
||||
hint _ = 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:
|
||||
--
|
||||
@@ -299,6 +347,9 @@ instance ErrorBody SchemaCacheError where
|
||||
-- >>> noRelBetweenHint "films" "role" "api" rels
|
||||
-- Just "Perhaps you meant 'roles' instead of 'role'."
|
||||
--
|
||||
-- >>> noRelBetweenHint "films" "role" "api" rels
|
||||
-- Just "Perhaps you meant 'roles' instead of 'role'."
|
||||
--
|
||||
-- >>> noRelBetweenHint "films" "actors" "api" rels
|
||||
-- Nothing
|
||||
--
|
||||
@@ -448,7 +499,12 @@ pgrstParseErrorHint err = case err of
|
||||
MsgParseError _ -> "MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and optional keys: 'details', 'hint'."
|
||||
_ -> "DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'."
|
||||
|
||||
instance ErrorHeaders PgError where
|
||||
data PgError = PgError Authenticated SQL.UsageError
|
||||
deriving Show
|
||||
|
||||
type Authenticated = Bool
|
||||
|
||||
instance PgrstError PgError where
|
||||
status (PgError authed usageError) = pgErrorStatus authed usageError
|
||||
|
||||
headers (PgError _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p))))) =
|
||||
@@ -463,33 +519,44 @@ instance ErrorHeaders 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) = 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 err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
instance ErrorBody SQL.UsageError where
|
||||
code (SQL.ConnectionUsageError _) = "PGRST000"
|
||||
code (SQL.SessionUsageError (SQL.PipelineError e)) = code e
|
||||
code (SQL.SessionUsageError (SQL.QueryError _ _ e)) = code e
|
||||
code SQL.AcquisitionTimeoutUsageError = "PGRST003"
|
||||
|
||||
message (SQL.ConnectionUsageError _) = "Database connection error. Retrying the connection."
|
||||
message (SQL.SessionUsageError (SQL.PipelineError e)) = message e
|
||||
message (SQL.ConnectionUsageError _) = "Database connection error."
|
||||
message (SQL.SessionUsageError (SQL.QueryError _ _ e)) = message e
|
||||
message SQL.AcquisitionTimeoutUsageError = "Timed out acquiring connection from connection pool."
|
||||
|
||||
details (SQL.ConnectionUsageError e) = JSON.String . T.decodeUtf8 <$> e
|
||||
details (SQL.SessionUsageError (SQL.PipelineError e)) = details e
|
||||
details (SQL.SessionUsageError (SQL.QueryError _ _ e)) = details e
|
||||
details SQL.AcquisitionTimeoutUsageError = Nothing
|
||||
|
||||
hint (SQL.ConnectionUsageError _) = Nothing
|
||||
hint (SQL.SessionUsageError (SQL.PipelineError e)) = hint e
|
||||
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
|
||||
code (SQL.ResultError (SQL.ServerError "PGRST" m d _ _)) =
|
||||
@@ -531,13 +598,8 @@ instance ErrorBody SQL.CommandError where
|
||||
pgErrorStatus :: Bool -> SQL.UsageError -> HTTP.Status
|
||||
pgErrorStatus _ (SQL.ConnectionUsageError _) = HTTP.status503
|
||||
pgErrorStatus _ SQL.AcquisitionTimeoutUsageError = HTTP.status504
|
||||
pgErrorStatus _ (SQL.SessionUsageError (SQL.PipelineError (SQL.ClientError _))) = HTTP.status503
|
||||
pgErrorStatus _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) = HTTP.status503
|
||||
pgErrorStatus authed (SQL.SessionUsageError (SQL.PipelineError (SQL.ResultError rError))) = mapSQLtoHTTP authed rError
|
||||
pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError rError))) = mapSQLtoHTTP authed rError
|
||||
|
||||
mapSQLtoHTTP :: Bool -> SQL.ResultError -> HTTP.Status
|
||||
mapSQLtoHTTP authed rError =
|
||||
pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError rError))) =
|
||||
case rError of
|
||||
(SQL.ServerError c m d _ _) ->
|
||||
case BS.unpack c of
|
||||
@@ -591,45 +653,86 @@ mapSQLtoHTTP authed rError =
|
||||
_ -> HTTP.status500
|
||||
|
||||
|
||||
instance ErrorHeaders Error where
|
||||
status (ApiRequestErr err) = status err
|
||||
status (SchemaCacheErr err) = status err
|
||||
status (JwtErr err) = status err
|
||||
status NoSchemaCacheError = HTTP.status503
|
||||
status (PgErr err) = status err
|
||||
data Error
|
||||
= ApiRequestError ApiRequestError
|
||||
| SchemaCacheErr SchemaCacheError
|
||||
| JwtErr JwtError
|
||||
| NoSchemaCacheError
|
||||
| PgErr PgError
|
||||
deriving Show
|
||||
|
||||
headers (ApiRequestErr err) = headers err
|
||||
headers (SchemaCacheErr err) = headers err
|
||||
headers (JwtErr err) = headers err
|
||||
headers (PgErr err) = headers err
|
||||
headers NoSchemaCacheError = mempty
|
||||
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 (SchemaCacheErr err) = status err
|
||||
status (JwtErr err) = status err
|
||||
status NoSchemaCacheError = HTTP.status503
|
||||
status (PgErr err) = status err
|
||||
|
||||
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 err = toJsonPgrstError
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
instance ErrorBody Error where
|
||||
code (ApiRequestErr err) = code err
|
||||
code (SchemaCacheErr err) = code err
|
||||
code (JwtErr err) = code err
|
||||
code NoSchemaCacheError = "PGRST002"
|
||||
code (PgErr err) = code err
|
||||
code (ApiRequestError err) = code err
|
||||
code (SchemaCacheErr err) = code err
|
||||
code (JwtErr err) = code err
|
||||
code NoSchemaCacheError = "PGRST002"
|
||||
code (PgErr err) = code err
|
||||
|
||||
message (ApiRequestErr err) = message err
|
||||
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
|
||||
|
||||
details (ApiRequestErr err) = details err
|
||||
details (SchemaCacheErr err) = details err
|
||||
details (JwtErr err) = details err
|
||||
details NoSchemaCacheError = Nothing
|
||||
details (PgErr err) = details err
|
||||
details (ApiRequestError err) = details err
|
||||
details (SchemaCacheErr err) = details err
|
||||
details (JwtErr err) = details err
|
||||
details NoSchemaCacheError = Nothing
|
||||
details (PgErr err) = details err
|
||||
|
||||
hint (ApiRequestErr err) = hint err
|
||||
hint (SchemaCacheErr err) = hint err
|
||||
hint (JwtErr err) = hint err
|
||||
hint NoSchemaCacheError = Nothing
|
||||
hint (PgErr err) = hint 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 ErrorHeaders JwtError where
|
||||
instance PgrstError JwtError where
|
||||
status JwtDecodeErr{} = HTTP.unauthorized401
|
||||
status JwtSecretMissing = HTTP.status500
|
||||
status JwtTokenRequired = HTTP.unauthorized401
|
||||
@@ -640,6 +743,10 @@ instance ErrorHeaders JwtError where
|
||||
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"
|
||||
@@ -683,6 +790,18 @@ requiredTokenHeader :: Header
|
||||
requiredTokenHeader = ("WWW-Authenticate", "Bearer")
|
||||
|
||||
-- For parsing byteString to JSON Object, used for allowing full response control
|
||||
data PgRaiseErrMessage = PgRaiseErrMessage {
|
||||
getCode :: Text,
|
||||
getMessage :: Text,
|
||||
getDetails :: Maybe Text,
|
||||
getHint :: Maybe Text
|
||||
}
|
||||
|
||||
data PgRaiseErrDetails = PgRaiseErrDetails {
|
||||
getStatus :: Int,
|
||||
getStatusText :: Maybe Text,
|
||||
getHeaders :: Map Text Text
|
||||
}
|
||||
|
||||
instance JSON.FromJSON PgRaiseErrMessage where
|
||||
parseJSON (JSON.Object m) =
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
{-|
|
||||
Module : PostgREST.Error.Types
|
||||
Description : PostgREST Error Data Types
|
||||
-}
|
||||
module PostgREST.Error.Types
|
||||
( ApiRequestError(..)
|
||||
, QPError(..)
|
||||
, RangeError(..)
|
||||
, RaiseError(..)
|
||||
, SchemaCacheError(..)
|
||||
, PgError(..)
|
||||
, Error(..)
|
||||
, JwtError (..)
|
||||
, JwtDecodeError(..)
|
||||
, JwtClaimsError(..)
|
||||
, PgRaiseErrMessage(..)
|
||||
, PgRaiseErrDetails(..)
|
||||
) where
|
||||
|
||||
import qualified Hasql.Pool as SQL
|
||||
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
||||
import PostgREST.SchemaCache.Relationship (Relationship (..),
|
||||
RelationshipsMap)
|
||||
import PostgREST.SchemaCache.Routine (Routine (..))
|
||||
import Protolude
|
||||
|
||||
data Error
|
||||
= ApiRequestErr ApiRequestError
|
||||
| SchemaCacheErr SchemaCacheError
|
||||
| JwtErr JwtError
|
||||
| NoSchemaCacheError
|
||||
| PgErr PgError
|
||||
deriving Show
|
||||
|
||||
-- API REQUEST ERRORS: PGRST1XX
|
||||
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
|
||||
|
||||
-- SCHEMA CACHE ERRORS: PGRST2XX
|
||||
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 SchemaCache
|
||||
deriving Show
|
||||
|
||||
-- JWT ERRORS: PGRST3XX
|
||||
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
|
||||
|
||||
-- PG ERRORS
|
||||
type Authenticated = Bool
|
||||
data PgError = PgError Authenticated SQL.UsageError
|
||||
deriving Show
|
||||
|
||||
-- For parsing byteString to JSON Object, used for allowing full response control
|
||||
data PgRaiseErrMessage = PgRaiseErrMessage {
|
||||
getCode :: Text,
|
||||
getMessage :: Text,
|
||||
getDetails :: Maybe Text,
|
||||
getHint :: Maybe Text
|
||||
}
|
||||
|
||||
data PgRaiseErrDetails = PgRaiseErrDetails {
|
||||
getStatus :: Int,
|
||||
getStatusText :: Maybe Text,
|
||||
getHeaders :: Map Text Text
|
||||
}
|
||||
+16
-10
@@ -11,6 +11,7 @@ import qualified Hasql.Notifications as SQL
|
||||
import PostgREST.AppState (AppState, getConfig)
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Observation (Observation (..))
|
||||
import PostgREST.Version (prettyVersion)
|
||||
|
||||
import qualified PostgREST.AppState as AppState
|
||||
import qualified PostgREST.Config as Config
|
||||
@@ -30,20 +31,20 @@ runListener :: AppState -> IO ()
|
||||
runListener appState = do
|
||||
AppConfig{..} <- getConfig appState
|
||||
when configDbChannelEnabled $
|
||||
void . forkIO . void $ retryingListen appState
|
||||
void . forkIO . void $ retryingListen appState False
|
||||
|
||||
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
|
||||
-- | This function never returns (but can throw) and return type enforces that.
|
||||
retryingListen :: AppState -> IO Void
|
||||
retryingListen appState = do
|
||||
cfg@AppConfig{..} <- AppState.getConfig appState
|
||||
retryingListen :: AppState -> Bool -> IO Void
|
||||
retryingListen appState hasDbListenerBug = do
|
||||
AppConfig{..} <- AppState.getConfig appState
|
||||
let
|
||||
dbChannel = toS configDbChannel
|
||||
onError err = do
|
||||
AppState.putIsListenerOn appState False
|
||||
observer $ DBListenFail dbChannel (Right err)
|
||||
when (isDbListenerBug err) $
|
||||
observer DBListenBugHint
|
||||
observer DBListenBugCallQueryFix
|
||||
unless configDbPoolAutomaticRecovery $
|
||||
killThread mainThreadId
|
||||
|
||||
@@ -54,23 +55,23 @@ retryingListen appState = do
|
||||
unless (delay == maxDelay) $
|
||||
AppState.putNextListenerDelay appState (delay * 2)
|
||||
-- loop running the listener
|
||||
retryingListen appState
|
||||
retryingListen appState (isDbListenerBug err)
|
||||
|
||||
-- Execute the listener with with error handling
|
||||
handle onError $ do
|
||||
-- Make sure we don't leak connections on errors
|
||||
bracket
|
||||
-- acquire connection
|
||||
(SQL.acquire $
|
||||
Config.toConnectionSettings Config.addTargetSessionAttrs cfg)
|
||||
(SQL.acquire $ toUtf8 (Config.addTargetSessionAttrs $ Config.addFallbackAppName prettyVersion configDbUri))
|
||||
-- release connection
|
||||
(`whenRight` releaseConnection) $
|
||||
-- use connection
|
||||
\case
|
||||
Right db -> do
|
||||
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
||||
(pqHost, pqPort) <- SQL.withLibPQConnection db $ bisequence . (LibPQ.host &&& LibPQ.port)
|
||||
pgFullName <- SQL.run queryPgVersion db >>= either throwIO (pure . pgvFullName)
|
||||
pgFullName <- SQL.run (queryPgVersion False) db >>= either throwIO (pure . pgvFullName)
|
||||
when hasDbListenerBug $ SQL.run callNotifQueryUsage db >>= either throwIO pure
|
||||
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
||||
|
||||
AppState.putIsListenerOn appState True
|
||||
|
||||
@@ -108,3 +109,8 @@ retryingListen appState = do
|
||||
releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail) . SQL.release
|
||||
|
||||
isDbListenerBug e = "could not access status of transaction" `T.isInfixOf` show e
|
||||
|
||||
-- Used to fix a Postgres bug in the listener, see: https://github.com/PostgREST/postgrest/issues/3147#issuecomment-3494591361
|
||||
-- This query advances the async notification query tail, which solves this issue.
|
||||
callNotifQueryUsage :: SQL.Session ()
|
||||
callNotifQueryUsage = SQL.sql "SELECT pg_notification_queue_usage();"
|
||||
|
||||
+103
-92
@@ -1,6 +1,5 @@
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE RecursiveDo #-}
|
||||
{-|
|
||||
Module : PostgREST.Logger
|
||||
Description : Logging based on the Observation.hs module. Access logs get sent to stdout and server diagnostic get sent to stderr.
|
||||
@@ -16,6 +15,7 @@ module PostgREST.Logger
|
||||
import Control.AutoUpdate (defaultUpdateSettings,
|
||||
mkAutoUpdate,
|
||||
updateAction)
|
||||
import Control.Debounce
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Hasql.Decoders as HD
|
||||
@@ -32,16 +32,13 @@ import qualified Network.Wai.Middleware.RequestLogger as Wai
|
||||
import Network.HTTP.Types.Status (Status, status400, status500)
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
|
||||
import PostgREST.Config (LogLevel (..), Verbosity (..))
|
||||
import PostgREST.Debounce (makeDebouncer)
|
||||
import PostgREST.Config (LogLevel (..))
|
||||
import PostgREST.Observation
|
||||
import PostgREST.Query (MainQuery (..))
|
||||
import PostgREST.SchemaCache (queryTimingsWLabels)
|
||||
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Data.Text as T
|
||||
import qualified Hasql.Connection as SQL
|
||||
import qualified Hasql.Pool as SQL
|
||||
import qualified Hasql.Pool.Observation as SQL
|
||||
import Numeric (showFFloat)
|
||||
import PostgREST.Config.PgVersion (pgvName)
|
||||
@@ -50,18 +47,29 @@ import Protolude
|
||||
|
||||
data LoggerState = LoggerState
|
||||
{ stateGetZTime :: IO ZonedTime -- ^ Time with time zone used for logs
|
||||
, stateLogDebouncePoolTimeout :: IO () -- ^ Logs with a debounce
|
||||
, stateLogDebouncePoolTimeout :: MVar (IO ()) -- ^ Logs with a debounce
|
||||
}
|
||||
|
||||
init :: IO LoggerState
|
||||
init = mdo
|
||||
let
|
||||
oneSecond = 1000000
|
||||
loggerState = LoggerState zTime debouncePoolTimeout
|
||||
init = do
|
||||
zTime <- mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
||||
debouncePoolTimeout <- makeDebouncer $
|
||||
logWithZTime loggerState (observationMessages PoolAcqTimeoutObs) *> threadDelay (5 * oneSecond)
|
||||
pure loggerState
|
||||
LoggerState zTime <$> newEmptyMVar
|
||||
|
||||
logWithDebounce :: LoggerState -> IO () -> IO ()
|
||||
logWithDebounce loggerState action = do
|
||||
debouncer <- tryReadMVar $ stateLogDebouncePoolTimeout loggerState
|
||||
case debouncer of
|
||||
Just d -> d
|
||||
Nothing -> do
|
||||
newDebouncer <-
|
||||
let oneSecond = 1000000 in
|
||||
mkDebounce defaultDebounceSettings
|
||||
{ debounceAction = action
|
||||
, debounceFreq = 5*oneSecond
|
||||
, debounceEdge = leadingEdge -- logs at the start and the end
|
||||
}
|
||||
putMVar (stateLogDebouncePoolTimeout loggerState) newDebouncer
|
||||
newDebouncer
|
||||
|
||||
-- TODO stop using this middleware to reuse the same "observer" pattern for all our logs
|
||||
middleware :: LogLevel -> (Wai.Request -> Maybe BS.ByteString) -> Wai.Middleware
|
||||
@@ -88,166 +96,169 @@ shouldLogResponse logLevel = case logLevel of
|
||||
-- All observations are logged except some that depend on the log-level
|
||||
observationLogger :: LoggerState -> LogLevel -> ObservationHandler
|
||||
observationLogger loggerState logLevel obs = case obs of
|
||||
PoolAcqTimeoutObs -> do
|
||||
when (logLevel >= LogError) $
|
||||
stateLogDebouncePoolTimeout loggerState
|
||||
o@(PoolAcqTimeoutObs _) -> do
|
||||
when (logLevel >= LogError) $ do
|
||||
logWithDebounce loggerState $
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
o@(QueryErrorCodeHighObs _) -> do
|
||||
when (logLevel >= LogError) $ do
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
o@SchemaCacheEmptyObs ->
|
||||
when (logLevel >= LogError) $ do
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
o@(HasqlPoolObs _) -> do
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
o@(QueryObs _ status) -> do
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
QueryObs gq status -> do
|
||||
when (shouldLogResponse logLevel status) $
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
logMainQ loggerState gq
|
||||
o@PoolRequest ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
o@PoolRequestFullfilled ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
o@PoolFlushed ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
o@JwtCacheEviction ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
o@(JwtCacheLookup _) ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
o@(WarpServerObs _) ->
|
||||
when (logLevel >= LogDebug) $ do
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
o ->
|
||||
logWithZTime loggerState $ observationMessages o
|
||||
logWithZTime loggerState $ observationMessage o
|
||||
|
||||
logWithZTime :: LoggerState -> [Text] -> IO ()
|
||||
logWithZTime loggerState txts = do
|
||||
logWithZTime :: LoggerState -> Text -> IO ()
|
||||
logWithZTime loggerState txt = do
|
||||
zTime <- stateGetZTime loggerState
|
||||
traverse_ (hPutStrLn stderr . (toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <>)) txts
|
||||
hPutStrLn stderr $ toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <> txt
|
||||
|
||||
logMainQ :: LoggerState -> MainQuery -> IO ()
|
||||
logMainQ loggerState MainQuery{mqOpenAPI=(x, y, z),..} =
|
||||
let snipts = renderSnippet <$> [mqTxVars, fromMaybe mempty mqPreReq, mqMain, x, y, z, fromMaybe mempty mqExplain]
|
||||
-- Does not log SQL when it's empty (happens on OPTIONS requests and when the openapi queries are not generated)
|
||||
logQ q = when (q /= mempty) $ logWithZTime loggerState $ showOnSingleLine '\n' $ T.decodeUtf8 q in
|
||||
mapM_ logQ snipts
|
||||
|
||||
-- TODO: maybe patch upstream hasql-dynamic-statements so we have a less hackish way to convert
|
||||
-- the SQL.Snippet or maybe don't use hasql-dynamic-statements and resort to plain strings for the queries and use regular hasql
|
||||
renderSnippet :: SQL.Snippet -> ByteString
|
||||
renderSnippet snippet =
|
||||
let SQL.Statement sql _ _ _ = SQL.dynamicallyParameterized snippet decoder False
|
||||
let SQL.Statement sql _ _ _ = SQL.dynamicallyParameterized snippet decoder prepared
|
||||
decoder = HD.noResult -- unused
|
||||
prepared = False -- unused
|
||||
in
|
||||
sql
|
||||
|
||||
observationMessages :: Observation -> [Text]
|
||||
observationMessages = \case
|
||||
|
||||
observationMessage :: Observation -> Text
|
||||
observationMessage = \case
|
||||
AdminStartObs address ->
|
||||
pure $ "Admin server listening on " <> address
|
||||
"Admin server listening on " <> address
|
||||
AdminServerCrashedObs ex ->
|
||||
"FAILURE: Admin server crashed unexpectedly: " <> (showOnSingleLine '\t' . show) ex
|
||||
AppStartObs ver ->
|
||||
pure $ "Starting PostgREST " <> T.decodeUtf8 ver <> "..."
|
||||
"Starting PostgREST " <> T.decodeUtf8 ver <> "..."
|
||||
AppServerAddressObs address ->
|
||||
pure $ "API server listening on " <> address
|
||||
"API server listening on " <> address
|
||||
DBConnectedObs ver ->
|
||||
pure $ "Successfully connected to " <> ver
|
||||
"Successfully connected to " <> ver
|
||||
ExitUnsupportedPgVersion pgVer minPgVer ->
|
||||
pure $ "Cannot run in this PostgreSQL version (" <> pgvName pgVer <> "), PostgREST needs at least " <> pgvName minPgVer
|
||||
"Cannot run in this PostgreSQL version (" <> pgvName pgVer <> "), PostgREST needs at least " <> pgvName minPgVer
|
||||
ExitDBNoRecoveryObs ->
|
||||
pure "Automatic recovery disabled, exiting."
|
||||
"Automatic recovery disabled, exiting."
|
||||
ExitDBFatalError ServerAuthError usageErr ->
|
||||
pure $ "Failed to establish a connection. " <> jsonMessage usageErr
|
||||
"Failed to establish a connection. " <> jsonMessage usageErr
|
||||
ExitDBFatalError ServerPgrstBug usageErr ->
|
||||
pure $ "This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. " <> jsonMessage usageErr
|
||||
"This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. " <> jsonMessage usageErr
|
||||
ExitDBFatalError ServerError42P05 usageErr ->
|
||||
pure $ "If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
|
||||
"If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
|
||||
ExitDBFatalError ServerError08P01 usageErr ->
|
||||
pure $ "Connection poolers in statement mode are not supported." <> jsonMessage usageErr
|
||||
"Connection poolers in statement mode are not supported." <> jsonMessage usageErr
|
||||
SchemaCacheEmptyObs ->
|
||||
pure $ T.decodeUtf8 . LBS.toStrict . Error.errorPayload Verbose $ Error.NoSchemaCacheError
|
||||
T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.NoSchemaCacheError
|
||||
SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
|
||||
pure $ "Failed to load the schema cache using "
|
||||
"Failed to load the schema cache using "
|
||||
<> "db-schemas=" <> T.intercalate "," (toList dbSchemas)
|
||||
<> " and "
|
||||
<> "db-extra-search-path=" <> T.intercalate "," extraPaths
|
||||
<> ". " <> jsonMessage usageErr
|
||||
SchemaCacheQueriedObs resultTime timings ->
|
||||
[ "Schema cache queried in " <> showMillis resultTime <> " milliseconds " ] <>
|
||||
let showTimings qt = [ T.intercalate ", " $ (\(l, v) -> T.decodeUtf8 l <> ": " <> v <> " ms") <$> queryTimingsWLabels qt ] in
|
||||
maybe mempty showTimings timings
|
||||
SchemaCacheLoadedObs resultTime summary ->
|
||||
[
|
||||
"Schema cache loaded " <> summary
|
||||
, "Schema cache loaded in " <> showMillis resultTime <> " milliseconds"
|
||||
]
|
||||
<> "db-extra-search-path=" <> T.intercalate "," extraPaths <> ". " <> jsonMessage usageErr
|
||||
SchemaCacheQueriedObs resultTime ->
|
||||
"Schema cache queried in " <> showMillis resultTime <> " milliseconds"
|
||||
SchemaCacheSummaryObs summary ->
|
||||
"Schema cache loaded " <> summary
|
||||
SchemaCacheLoadedObs resultTime ->
|
||||
"Schema cache loaded in " <> showMillis resultTime <> " milliseconds"
|
||||
ConnectionRetryObs delay ->
|
||||
pure $ "Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
|
||||
"Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
|
||||
QueryPgVersionError usageErr ->
|
||||
pure $ "Failed to query the PostgreSQL version. " <> jsonMessage usageErr
|
||||
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr
|
||||
DBListenStart host port fullName channel -> do
|
||||
pure $ "Listener connected to " <> fullName <> " on " <> show (fold $ host <> fmap (":" <>) port) <> " and listening for database notifications on the " <> show channel <> " channel"
|
||||
"Listener connected to " <> fullName <> " on " <> show (fold $ host <> fmap (":" <>) port) <> " and listening for database notifications on the " <> show channel <> " channel"
|
||||
DBListenFail channel listenErr ->
|
||||
pure $ "Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
||||
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
||||
either showListenerConnError showListenerException listenErr
|
||||
DBListenRetry delay ->
|
||||
pure $ "Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
||||
DBListenBugHint ->
|
||||
pure "HINT: This is likely a bug in the notification queue, try executing the following to solve it: select pg_notification_queue_usage();"
|
||||
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
||||
DBListenBugCallQueryFix ->
|
||||
"This is likely a PostgreSQL bug in the notification queue, executing the following to try to solve it: SELECT pg_notification_queue_usage();"
|
||||
DBListenerGotSCacheMsg channel ->
|
||||
pure $ "Received a schema cache reload message on the " <> show channel <> " channel"
|
||||
"Received a schema cache reload message on the " <> show channel <> " channel"
|
||||
DBListenerGotConfigMsg channel ->
|
||||
pure $ "Received a config reload message on the " <> show channel <> " channel"
|
||||
"Received a config reload message on the " <> show channel <> " channel"
|
||||
DBListenerConnectionCleanupFail ex ->
|
||||
pure $ "Failed during listener connection cleanup: " <> showOnSingleLine '\t' (show ex)
|
||||
(QueryObs MainQuery{mqOpenAPI=(x, y, z),..} _) ->
|
||||
let snipts = renderSnippet <$> [mqTxVars, fromMaybe mempty mqPreReq, mqMain, x, y, z, fromMaybe mempty mqExplain]
|
||||
in
|
||||
showOnSingleLine '\n' . T.decodeUtf8 <$> filter (/= mempty) snipts
|
||||
"Failed during listener connection cleanup: " <> showOnSingleLine '\t' (show ex)
|
||||
QueryObs{} ->
|
||||
mempty -- TODO pending refactor: The logic for printing the query cannot be done here. Join the observationMessage function into observationLogger to avoid this mempty.
|
||||
ConfigReadErrorObs usageErr ->
|
||||
pure $ "Failed to query database settings for the config parameters." <> jsonMessage usageErr
|
||||
"Failed to query database settings for the config parameters." <> jsonMessage usageErr
|
||||
QueryRoleSettingsErrorObs usageErr ->
|
||||
pure $ "Failed to query the role settings. " <> jsonMessage usageErr
|
||||
"Failed to query the role settings. " <> jsonMessage usageErr
|
||||
QueryErrorCodeHighObs usageErr ->
|
||||
pure $ jsonMessage usageErr
|
||||
jsonMessage usageErr
|
||||
ConfigInvalidObs err ->
|
||||
pure $ "Failed reloading config: " <> err
|
||||
"Failed reloading config: " <> err
|
||||
ConfigSucceededObs ->
|
||||
pure "Config reloaded"
|
||||
"Config reloaded"
|
||||
PoolInit poolSize ->
|
||||
pure $ "Connection Pool initialized with a maximum size of " <> show poolSize <> " connections"
|
||||
PoolAcqTimeoutObs -> pure $ jsonMessage SQL.AcquisitionTimeoutUsageError
|
||||
"Connection Pool initialized with a maximum size of " <> show poolSize <> " connections"
|
||||
PoolAcqTimeoutObs usageErr ->
|
||||
jsonMessage usageErr
|
||||
HasqlPoolObs (SQL.ConnectionObservation uuid status) ->
|
||||
pure $ "Connection " <> show uuid <> (
|
||||
"Connection " <> show uuid <> (
|
||||
case status of
|
||||
SQL.ConnectingConnectionStatus -> " is being established"
|
||||
SQL.ReadyForUseConnectionStatus reason -> " is available due to " <> case reason of
|
||||
SQL.EstablishedConnectionReadyForUseReason -> "connection establishment"
|
||||
SQL.SessionFailedConnectionReadyForUseReason _ -> "session failure"
|
||||
SQL.SessionSucceededConnectionReadyForUseReason -> "session success"
|
||||
SQL.ReadyForUseConnectionStatus -> " is available"
|
||||
SQL.InUseConnectionStatus -> " is used"
|
||||
SQL.TerminatedConnectionStatus reason -> " is terminated due to " <> case reason of
|
||||
SQL.AgingConnectionTerminationReason -> "max lifetime"
|
||||
SQL.IdlenessConnectionTerminationReason -> "max idletime"
|
||||
SQL.ReleaseConnectionTerminationReason -> "release"
|
||||
SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message.
|
||||
SQL.InitializationErrorTerminationReason _ -> "init failure"
|
||||
)
|
||||
PoolRequest ->
|
||||
pure "Trying to borrow a connection from pool"
|
||||
"Trying to borrow a connection from pool"
|
||||
PoolRequestFullfilled ->
|
||||
pure "Borrowed a connection from the pool"
|
||||
"Borrowed a connection from the pool"
|
||||
PoolFlushed ->
|
||||
pure "Database connection pool flushed"
|
||||
"Database connection pool flushed"
|
||||
JwtCacheLookup _ ->
|
||||
pure "Looked up a JWT in JWT cache"
|
||||
"Looked up a JWT in JWT cache"
|
||||
JwtCacheEviction ->
|
||||
pure "Evicted entry from JWT cache"
|
||||
"Evicted entry from JWT cache"
|
||||
TerminationUnixSignalObs signal ->
|
||||
pure $ "Received termination unix signal " <> signal
|
||||
"Received termination unix signal " <> signal
|
||||
WarpServerObs txt ->
|
||||
pure $ "Warp server: " <> txt
|
||||
"Warp server: " <> txt
|
||||
where
|
||||
showMillis :: Double -> Text
|
||||
showMillis x = toS $ showFFloat (Just 1) x ""
|
||||
|
||||
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload Verbose $ Error.PgError False err
|
||||
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
|
||||
|
||||
|
||||
showListenerConnError :: SQL.ConnectionError -> Text
|
||||
|
||||
@@ -96,8 +96,9 @@ data ResultSet
|
||||
mainTx :: MainQuery -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> MainTx
|
||||
mainTx _ _ _ _ (NoDb x) _ = NoDbTx $ NoDbResult x
|
||||
mainTx genQ@MainQuery{..} conf@AppConfig{..} AuthResult{..} apiReq (Db plan) sCache =
|
||||
DbTx isoLvl txMode dbHandler SQL.transactionNoRetry
|
||||
DbTx isoLvl txMode dbHandler transaction
|
||||
where
|
||||
transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction
|
||||
isoLvl = planIsoLvl conf authRole plan
|
||||
txMode = planTxMode plan
|
||||
dbHandler = do
|
||||
@@ -221,7 +222,7 @@ failPut :: ResultSet -> DbHandler ()
|
||||
failPut RSStandard{rsQueryTotal=queryTotal} =
|
||||
when (queryTotal /= 1) $ do
|
||||
lift SQL.condemn
|
||||
throwError $ Error.ApiRequestErr Error.PutMatchingPkError
|
||||
throwError $ Error.ApiRequestError Error.PutMatchingPkError
|
||||
|
||||
-- |
|
||||
-- Fail a response if a single JSON object was requested and not exactly one
|
||||
@@ -230,13 +231,13 @@ failNotSingular :: MediaType -> ResultSet -> DbHandler ()
|
||||
failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =
|
||||
when (elem mediaType [MTVndSingularJSON True, MTVndSingularJSON False] && queryTotal /= 1) $ do
|
||||
lift SQL.condemn
|
||||
throwError $ Error.ApiRequestErr . Error.SingularityError $ toInteger queryTotal
|
||||
throwError $ Error.ApiRequestError . Error.SingularityError $ toInteger queryTotal
|
||||
|
||||
failExceedsMaxAffectedPref :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> ResultSet -> DbHandler ()
|
||||
failExceedsMaxAffectedPref (Nothing,_) _ = pure ()
|
||||
failExceedsMaxAffectedPref (Just (PreferMaxAffected n), handling) RSStandard{rsQueryTotal=queryTotal} = when ((queryTotal > n) && (handling == Just Strict)) $ do
|
||||
lift SQL.condemn
|
||||
throwError $ Error.ApiRequestErr . Error.MaxAffectedViolationError $ toInteger queryTotal
|
||||
throwError $ Error.ApiRequestError . Error.MaxAffectedViolationError $ toInteger queryTotal
|
||||
|
||||
-- | Set a transaction to roll back if requested
|
||||
optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
|
||||
|
||||
+55
-13
@@ -5,7 +5,10 @@ Description : Metrics based on the Observation module. See Observation.hs.
|
||||
-}
|
||||
module PostgREST.Metrics
|
||||
( init
|
||||
, ConnTrack
|
||||
, ConnStats (..)
|
||||
, MetricsState (..)
|
||||
, connectionCounts
|
||||
, observationMetrics
|
||||
, metricsToText
|
||||
) where
|
||||
@@ -17,12 +20,18 @@ import Prometheus
|
||||
|
||||
import PostgREST.Observation
|
||||
|
||||
import Protolude
|
||||
import Control.Arrow ((&&&))
|
||||
import Data.Bitraversable (bisequenceA)
|
||||
import Data.Tuple.Extra (both)
|
||||
import Data.UUID (UUID)
|
||||
import qualified Focus
|
||||
import Protolude
|
||||
import qualified StmHamt.SizedHamt as SH
|
||||
|
||||
data MetricsState =
|
||||
MetricsState {
|
||||
poolTimeouts :: Counter,
|
||||
poolAvailable :: Gauge,
|
||||
connTrack :: ConnTrack,
|
||||
poolWaiting :: Gauge,
|
||||
poolMaxSize :: Gauge,
|
||||
schemaCacheLoads :: Vector Label1 Counter,
|
||||
@@ -36,7 +45,7 @@ init :: Int -> IO MetricsState
|
||||
init configDbPoolSize = do
|
||||
metricState <- MetricsState <$>
|
||||
register (counter (Info "pgrst_db_pool_timeouts_total" "The total number of pool connection timeouts")) <*>
|
||||
register (gauge (Info "pgrst_db_pool_available" "Available connections in the pool")) <*>
|
||||
register (Metric ((identity &&& dbPoolAvailable) <$> connectionTracker)) <*>
|
||||
register (gauge (Info "pgrst_db_pool_waiting" "Requests waiting to acquire a pool connection")) <*>
|
||||
register (gauge (Info "pgrst_db_pool_max" "Max pool connections")) <*>
|
||||
register (vector "status" $ counter (Info "pgrst_schema_cache_loads_total" "The total number of times the schema cache was loaded")) <*>
|
||||
@@ -46,25 +55,33 @@ init configDbPoolSize = do
|
||||
register (counter (Info "pgrst_jwt_cache_evictions_total" "The total number of JWT cache evictions"))
|
||||
setGauge (poolMaxSize metricState) (fromIntegral configDbPoolSize)
|
||||
pure metricState
|
||||
where
|
||||
dbPoolAvailable = (pure . noLabelsGroup (Info "pgrst_db_pool_available" "Available connections in the pool") GaugeType . calcAvailable <$>) . connectionCounts
|
||||
where
|
||||
calcAvailable = liftA2 (-) connected inUse
|
||||
toSample name labels = Sample name labels . encodeUtf8 . show
|
||||
noLabelsGroup info sampleType = SampleGroup info sampleType . pure . toSample (metricName info) mempty
|
||||
|
||||
-- Only some observations are used as metrics
|
||||
observationMetrics :: MetricsState -> ObservationHandler
|
||||
observationMetrics MetricsState{..} obs = case obs of
|
||||
PoolAcqTimeoutObs -> do
|
||||
(PoolAcqTimeoutObs _) -> do
|
||||
incCounter poolTimeouts
|
||||
(HasqlPoolObs (SQL.ConnectionObservation _ status)) -> case status of
|
||||
SQL.ReadyForUseConnectionStatus _ -> do
|
||||
incGauge poolAvailable
|
||||
SQL.InUseConnectionStatus -> do
|
||||
decGauge poolAvailable
|
||||
SQL.TerminatedConnectionStatus _ -> do
|
||||
decGauge poolAvailable
|
||||
SQL.ConnectingConnectionStatus -> pure ()
|
||||
-- Handle pool observations with connection tracking
|
||||
-- this is necessary because it is not possible
|
||||
-- to accurately maintain open/in use conneciton counts
|
||||
-- statelessly based only on pool observation events.
|
||||
-- The reason is that hasql-pool emits TerminatedConnectionStatus
|
||||
-- both for connections successfully established and failed when connecting.
|
||||
-- When receiving TerminatedConnectionStatus we have to find out
|
||||
-- if we can decrement established connection count. To do that we have to track
|
||||
-- established connections.
|
||||
(HasqlPoolObs sqlObs) -> trackConnections connTrack sqlObs
|
||||
PoolRequest ->
|
||||
incGauge poolWaiting
|
||||
PoolRequestFullfilled ->
|
||||
decGauge poolWaiting
|
||||
SchemaCacheLoadedObs resTime _ -> do
|
||||
SchemaCacheLoadedObs resTime -> do
|
||||
withLabel schemaCacheLoads "SUCCESS" incCounter
|
||||
setGauge schemaCacheQueryTime resTime
|
||||
SchemaCacheErrorObs{} -> do
|
||||
@@ -77,3 +94,28 @@ observationMetrics MetricsState{..} obs = case obs of
|
||||
|
||||
metricsToText :: IO LBS.ByteString
|
||||
metricsToText = exportMetricsAsText
|
||||
|
||||
data ConnStats = ConnStats {
|
||||
connected :: Int,
|
||||
inUse :: Int
|
||||
} deriving (Eq, Show)
|
||||
|
||||
data ConnTrack = ConnTrack { connTrackConnected :: SH.SizedHamt UUID, connTrackInUse :: SH.SizedHamt UUID }
|
||||
|
||||
connectionTracker :: IO ConnTrack
|
||||
connectionTracker = ConnTrack <$> SH.newIO <*> SH.newIO
|
||||
|
||||
trackConnections :: ConnTrack -> SQL.Observation -> IO ()
|
||||
trackConnections ConnTrack{..} (SQL.ConnectionObservation uuid status) = case status of
|
||||
SQL.ReadyForUseConnectionStatus -> atomically $
|
||||
SH.insert identity uuid connTrackConnected *>
|
||||
SH.focus Focus.delete identity uuid connTrackInUse
|
||||
SQL.TerminatedConnectionStatus _ -> atomically $
|
||||
SH.focus Focus.delete identity uuid connTrackConnected *>
|
||||
SH.focus Focus.delete identity uuid connTrackInUse
|
||||
SQL.InUseConnectionStatus -> atomically $
|
||||
SH.insert identity uuid connTrackInUse
|
||||
_ -> mempty
|
||||
|
||||
connectionCounts :: ConnTrack -> IO ConnStats
|
||||
connectionCounts = atomically . fmap (uncurry ConnStats) . bisequenceA . both SH.size . (connTrackConnected &&& connTrackInUse)
|
||||
|
||||
@@ -18,12 +18,12 @@ import qualified Hasql.Pool.Observation as SQL
|
||||
import Network.HTTP.Types.Status (Status)
|
||||
import PostgREST.Config.PgVersion
|
||||
import PostgREST.Query (MainQuery)
|
||||
import PostgREST.SchemaCache (QueryTimings)
|
||||
|
||||
import Protolude hiding (toList)
|
||||
|
||||
data Observation
|
||||
= AdminStartObs Text
|
||||
| AdminServerCrashedObs SomeException
|
||||
| AppStartObs ByteString
|
||||
| AppServerAddressObs Text
|
||||
| ExitUnsupportedPgVersion PgVersion PgVersion
|
||||
@@ -32,13 +32,14 @@ data Observation
|
||||
| DBConnectedObs Text
|
||||
| SchemaCacheEmptyObs
|
||||
| SchemaCacheErrorObs (NonEmpty Text) [Text] SQL.UsageError
|
||||
| SchemaCacheQueriedObs Double (Maybe QueryTimings)
|
||||
| SchemaCacheLoadedObs Double Text
|
||||
| SchemaCacheQueriedObs Double
|
||||
| SchemaCacheSummaryObs Text
|
||||
| SchemaCacheLoadedObs Double
|
||||
| ConnectionRetryObs Int
|
||||
| DBListenStart (Maybe ByteString) (Maybe ByteString) Text Text -- host, port, version string, channel
|
||||
| DBListenFail Text (Either SQL.ConnectionError SomeException)
|
||||
| DBListenRetry Int
|
||||
| DBListenBugHint -- https://github.com/PostgREST/postgrest/issues/3147
|
||||
| DBListenBugCallQueryFix
|
||||
| DBListenerGotSCacheMsg ByteString
|
||||
| DBListenerGotConfigMsg ByteString
|
||||
| DBListenerConnectionCleanupFail SomeException
|
||||
@@ -50,7 +51,7 @@ data Observation
|
||||
| QueryErrorCodeHighObs SQL.UsageError
|
||||
| QueryPgVersionError SQL.UsageError
|
||||
| PoolInit Int
|
||||
| PoolAcqTimeoutObs
|
||||
| PoolAcqTimeoutObs SQL.UsageError
|
||||
| HasqlPoolObs SQL.Observation
|
||||
| PoolRequest
|
||||
| PoolRequestFullfilled
|
||||
|
||||
+51
-17
@@ -43,7 +43,6 @@ import PostgREST.Error (ApiRequestError (..),
|
||||
Error (..),
|
||||
SchemaCacheError (..))
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.Plan.Negotiate (negotiateContent)
|
||||
import PostgREST.Query.SqlFragment (sourceCTEName)
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
convertToLimitZeroRange,
|
||||
@@ -51,6 +50,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier (..),
|
||||
RelIdentifier (..),
|
||||
Schema)
|
||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
Junction (..),
|
||||
@@ -60,6 +60,8 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
|
||||
RepresentationsMap)
|
||||
import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
||||
MediaHandlerMap,
|
||||
ResolvedHandler,
|
||||
Routine (..),
|
||||
RoutineMap,
|
||||
RoutineParam (..),
|
||||
@@ -172,8 +174,8 @@ wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest
|
||||
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do
|
||||
qi <- findTable identifier sCache
|
||||
rPlan <- readPlan qi conf sCache apiRequest
|
||||
(handler, mediaType) <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()
|
||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||
return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly qi
|
||||
|
||||
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan
|
||||
@@ -181,8 +183,8 @@ mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..}
|
||||
qi <- findTable identifier sCache
|
||||
rPlan <- readPlan qi conf sCache apiRequest
|
||||
mPlan <- mutatePlan mutation qi apiRequest sCache rPlan
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()
|
||||
(handler, mediaType) <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi
|
||||
|
||||
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CrudPlan
|
||||
@@ -204,15 +206,15 @@ callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferenc
|
||||
(Inv, Routine.Immutable) -> SQL.Read
|
||||
(Inv, Routine.Volatile) -> SQL.Write
|
||||
cPlan = callPlan proc apiRequest paramKeys args rPlan
|
||||
(handler, mediaType) <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()
|
||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||
failMaxAffectedRpcReturnsSingle (preferMaxAffected, preferHandling) proc
|
||||
return $ CallReadPlan rPlan cPlan txMode proc handler mediaType invMethod identifier
|
||||
where
|
||||
qsParams' = QueryParams.qsParams iQueryParams
|
||||
|
||||
failMaxAffectedRpcReturnsSingle :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> Routine -> Either Error ()
|
||||
failMaxAffectedRpcReturnsSingle (Just (PreferMaxAffected _), Just Strict) rout = if funcReturnsSingle rout then Left $ ApiRequestErr MaxAffectedRpcViolation else Right ()
|
||||
failMaxAffectedRpcReturnsSingle (Just (PreferMaxAffected _), Just Strict) rout = if funcReturnsSingle rout then Left $ ApiRequestError MaxAffectedRpcViolation else Right ()
|
||||
failMaxAffectedRpcReturnsSingle _ _ = Right ()
|
||||
|
||||
hasDefaultSelect :: ReadPlanTree -> Bool
|
||||
@@ -225,7 +227,7 @@ inspectPlan apiRequest headersOnly schema = do
|
||||
accepts = iAcceptMediaType apiRequest
|
||||
mediaType <- if not . null $ L.intersect accepts producedMTs
|
||||
then Right MTOpenAPI
|
||||
else Left . ApiRequestErr . MediaTypeError $ MediaType.toMime <$> accepts
|
||||
else Left . ApiRequestError . MediaTypeError $ MediaType.toMime <$> accepts
|
||||
return $ InspectPlan mediaType SQL.Read headersOnly schema
|
||||
|
||||
{-|
|
||||
@@ -784,7 +786,7 @@ hoistIntoRelSelectFields _ r = r
|
||||
-- to order once it's aggregated if it's not selected in the inner query beforehand.
|
||||
addToManyOrderSelects :: ReadPlanTree -> Either Error ReadPlanTree
|
||||
addToManyOrderSelects (Node rp@ReadPlan{order, select, relAggAlias, relSelect, relSpread = Just ToManySpread {}} forest)
|
||||
| anyAggSel || anyAggRelSel = Left $ ApiRequestErr $ NotImplemented "Aggregates are not implemented for one-to-many or many-to-many spreads."
|
||||
| anyAggSel || anyAggRelSel = Left $ ApiRequestError $ NotImplemented "Aggregates are not implemented for one-to-many or many-to-many spreads."
|
||||
| otherwise = Node rp { order = [], relSpread = newRelSpread } <$> addToManyOrderSelects `traverse` forest
|
||||
where
|
||||
newRelSpread = Just ToManySpread { stExtraSelect = addSprExtraSelects, stOrder = addSprOrder}
|
||||
@@ -806,7 +808,7 @@ addToManyOrderSelects (Node rp forest) = Node rp <$> addToManyOrderSelects `trav
|
||||
|
||||
validateAggFunctions :: Bool -> ReadPlanTree -> Either Error ReadPlanTree
|
||||
validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
|
||||
| not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left $ ApiRequestErr AggregatesNotAllowed
|
||||
| not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left $ ApiRequestError AggregatesNotAllowed
|
||||
| otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
|
||||
|
||||
-- | Lookup table in the schema cache before creating read plan
|
||||
@@ -860,9 +862,9 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
name = fromMaybe relName relAlias in
|
||||
if isToOne == Just True
|
||||
then Right $ cot{coRelation=relAggAlias}
|
||||
else Left $ ApiRequestErr $ RelatedOrderNotToOne (qiName from) name
|
||||
else Left $ ApiRequestError $ RelatedOrderNotToOne (qiName from) name
|
||||
Nothing ->
|
||||
Left $ ApiRequestErr $ NotEmbedded coRelation
|
||||
Left $ ApiRequestError $ NotEmbedded coRelation
|
||||
|
||||
-- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null`
|
||||
--
|
||||
@@ -956,7 +958,7 @@ addRanges ApiRequest{..} rReq =
|
||||
_ -> foldr addRangeToNode (Right rReq) =<< ranges
|
||||
where
|
||||
ranges :: Either Error [(EmbedPath, NonnegRange)]
|
||||
ranges = first (ApiRequestErr . QueryParamError) $ QueryParams.pRequestRange `traverse` HM.toList iRange
|
||||
ranges = first (ApiRequestError . QueryParamError) $ QueryParams.pRequestRange `traverse` HM.toList iRange
|
||||
|
||||
addRangeToNode :: (EmbedPath, NonnegRange) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||
addRangeToNode = updateNode (\r (Node q f) -> Node q{range_=r} f)
|
||||
@@ -990,13 +992,13 @@ updateNode f ([], a) rr = f a <$> rr
|
||||
updateNode _ _ (Left e) = Left e
|
||||
updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
|
||||
case findNode of
|
||||
Nothing -> Left $ ApiRequestErr $ NotEmbedded targetNodeName
|
||||
Nothing -> Left $ ApiRequestError $ NotEmbedded targetNodeName
|
||||
Just target ->
|
||||
(\node -> Node rootNode $ node : delete target forest) <$>
|
||||
updateNode f (remainingPath, a) (Right target)
|
||||
where
|
||||
findNode :: Maybe ReadPlanTree
|
||||
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> fromMaybe relName relAlias == targetNodeName) forest
|
||||
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> relName == targetNodeName || relAlias == Just targetNodeName) forest
|
||||
|
||||
mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan
|
||||
mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq =
|
||||
@@ -1014,7 +1016,7 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
|
||||
_ -> False) qsFiltersRoot
|
||||
then mapRight (\typedColumns -> Insert qi typedColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty False) typedColumnsOrError
|
||||
else
|
||||
Left $ ApiRequestErr InvalidFilters
|
||||
Left $ ApiRequestError InvalidFilters
|
||||
MutationDelete -> Right $ Delete qi combinedLogic returnings
|
||||
where
|
||||
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
||||
@@ -1121,3 +1123,35 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
||||
-- they are later concatenated with AND in the QueryBuilder
|
||||
addFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleLogicTree]
|
||||
addFilterToLogicForest flt lf = CoercibleStmnt flt : lf
|
||||
|
||||
-- | Do content negotiation. i.e. choose a media type based on the intersection of accepted/produced media types.
|
||||
negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Bool -> Either ApiRequestError ResolvedHandler
|
||||
negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces defaultSelect =
|
||||
case (act, firstAcceptedPick) of
|
||||
(_, Nothing) -> Left . MediaTypeError $ map MediaType.toMime accepts
|
||||
(ActDb (ActRelationMut _ _), Just (x, mt)) -> Right (if rep == Just Full then x else NoAgg, mt)
|
||||
-- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849
|
||||
-- TODO: despite no aggregate, these are responding with a Content-Type, which is not correct.
|
||||
(ActDb (ActRelationRead _ True), Just (_, mt)) -> Right (NoAgg, mt)
|
||||
(ActDb (ActRoutine _ (InvRead True)), Just (_, mt)) -> Right (NoAgg, mt)
|
||||
(_, Just (x, mt)) -> Right (x, mt)
|
||||
where
|
||||
firstAcceptedPick = listToMaybe $ mapMaybe matchMT accepts -- If there are multiple accepted media types, pick the first. This is usual in content negotiation.
|
||||
matchMT mt = case mt of
|
||||
-- all the vendored media types have special handling as they have media type parameters, they cannot be overridden
|
||||
m@(MTVndSingularJSON strip) -> Just (BuiltinAggSingleJson strip, m)
|
||||
m@MTVndArrayJSONStrip -> Just (BuiltinAggArrayJsonStrip, m)
|
||||
m@(MTVndPlan (MTVndSingularJSON strip) _ _) -> mtPlanToNothing $ Just (BuiltinAggSingleJson strip, m)
|
||||
m@(MTVndPlan MTVndArrayJSONStrip _ _) -> mtPlanToNothing $ Just (BuiltinAggArrayJsonStrip, m)
|
||||
-- TODO the plan should have its own MediaHandler instead of relying on MediaType
|
||||
m@(MTVndPlan mType _ _) -> mtPlanToNothing $ ((,) . fst <$> lookupHandler mType) <*> pure m
|
||||
-- all the other media types can be overridden
|
||||
x -> lookupHandler x
|
||||
mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed
|
||||
lookupHandler mt =
|
||||
when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) <|> -- lookup for identifier and `*/*`
|
||||
when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|> -- lookup for identifier and a particular media type
|
||||
HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type
|
||||
when' :: Bool -> Maybe a -> Maybe a
|
||||
when' True (Just a) = Just a
|
||||
when' _ _ = Nothing
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
{-|
|
||||
Module : PostgREST.Plan.Negotiate
|
||||
Description : PostgREST Content Negotiation
|
||||
|
||||
This module contains logic for content negotiation.
|
||||
RFC: https://datatracker.ietf.org/doc/html/rfc7231#section-3.4
|
||||
-}
|
||||
|
||||
module PostgREST.Plan.Negotiate
|
||||
( negotiateContent
|
||||
) where
|
||||
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
|
||||
import PostgREST.ApiRequest (ApiRequest (..))
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Error (ApiRequestError (..))
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||
RelIdentifier (..))
|
||||
import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
||||
MediaHandlerMap,
|
||||
ResolvedHandler)
|
||||
|
||||
import PostgREST.ApiRequest.Preferences
|
||||
import PostgREST.ApiRequest.Types
|
||||
import qualified PostgREST.MediaType as MediaType
|
||||
|
||||
import Protolude hiding (from)
|
||||
|
||||
-- We have two general cases of return values from database objects
|
||||
-- (tables/views/functions):
|
||||
--
|
||||
-- 1. "un-mime-typed" values, in most of the cases this is a composite/row
|
||||
-- value, for example for tables or views, but also often for functions.
|
||||
-- It can be simple integer values or text or bytea as well.
|
||||
--
|
||||
-- For this, we need handlers to transform the "non-mime-typed" values
|
||||
-- into "mimetypes". We have a default builtin handler that does
|
||||
-- "application/json". We can add more handlers via aggregates.
|
||||
--
|
||||
-- 2. "mime-typed" values, which specifically return a domain type that is
|
||||
-- associated to a certain mimetype. e.g, a function returning only
|
||||
-- "image/png".
|
||||
--
|
||||
-- FIXME:
|
||||
-- If the function returns a domain type - let's say image/png, we should
|
||||
-- accept */*, image/*, and image/png.
|
||||
-- Related issue: https://github.com/PostgREST/postgrest/issues/3391
|
||||
|
||||
-- | Do content negotiation. i.e. choose a media type based on the
|
||||
-- intersection of accepted/produced media types.
|
||||
negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Bool -> Either ApiRequestError ResolvedHandler
|
||||
negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces defaultSelect =
|
||||
case (act, firstAcceptedPick) of
|
||||
(_, Nothing) -> Left . MediaTypeError $ map MediaType.toMime accepts
|
||||
(ActDb (ActRelationMut _ _), Just (x, mt)) -> Right (if rep == Just Full then x else NoAgg, mt)
|
||||
-- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849
|
||||
-- TODO: despite no aggregate, these are responding with a Content-Type, which is not correct.
|
||||
(ActDb (ActRelationRead _ True), Just (_, mt)) -> Right (NoAgg, mt)
|
||||
(ActDb (ActRoutine _ (InvRead True)), Just (_, mt)) -> Right (NoAgg, mt)
|
||||
(_, Just (x, mt)) -> Right (x, mt)
|
||||
where
|
||||
firstAcceptedPick = listToMaybe $ mapMaybe matchMT accepts -- If there are multiple accepted media types, pick the first. This is usual in content negotiation.
|
||||
matchMT mt = case mt of
|
||||
-- all the vendored media types have special handling as they have media type parameters, they cannot be overridden
|
||||
m@(MTVndSingularJSON strip) -> Just (BuiltinAggSingleJson strip, m)
|
||||
m@MTVndArrayJSONStrip -> Just (BuiltinAggArrayJsonStrip, m)
|
||||
m@(MTVndPlan (MTVndSingularJSON strip) _ _) -> mtPlanToNothing $ Just (BuiltinAggSingleJson strip, m)
|
||||
m@(MTVndPlan MTVndArrayJSONStrip _ _) -> mtPlanToNothing $ Just (BuiltinAggArrayJsonStrip, m)
|
||||
-- TODO the plan should have its own MediaHandler instead of relying on MediaType
|
||||
m@(MTVndPlan mType _ _) -> mtPlanToNothing $ ((,) . fst <$> lookupHandler mType) <*> pure m
|
||||
-- all the other media types can be overridden
|
||||
x -> lookupHandler x
|
||||
mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed
|
||||
lookupHandler mt =
|
||||
when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) <|> -- lookup for identifier and `*/*`
|
||||
when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|> -- lookup for identifier and a particular media type
|
||||
HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type
|
||||
when' :: Bool -> Maybe a -> Maybe a
|
||||
when' True (Just a) = Just a
|
||||
when' _ _ = Nothing
|
||||
@@ -47,7 +47,7 @@ data CoercibleField = CoercibleField
|
||||
, cfBaseType :: Text -- ^ The base type of the field in case of domains, or just the type otherwise (without modifiers in case of pg_catalog types)
|
||||
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
||||
, cfDefault :: Maybe Text
|
||||
, cfFullRow :: Bool -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spread resources.
|
||||
, cfFullRow :: Bool -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spreaded resources.
|
||||
} deriving (Eq, Show)
|
||||
|
||||
unknownField :: FieldName -> JsonPath -> CoercibleField
|
||||
|
||||
@@ -43,16 +43,16 @@ data MainQuery = MainQuery
|
||||
|
||||
mainQuery :: ActionPlan -> AppConfig -> ApiRequest -> AuthResult -> Maybe QualifiedIdentifier -> MainQuery
|
||||
mainQuery (NoDb _) _ _ _ _ = MainQuery mempty Nothing mempty (mempty, mempty, mempty) mempty
|
||||
mainQuery (Db plan) conf@AppConfig{..} apiReq@ApiRequest{iTopLevelRange=range, iPreferences=Preferences{..}} authRes preReq =
|
||||
mainQuery (Db plan) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} authRes preReq =
|
||||
let genQ = MainQuery (PreQuery.txVarQuery plan conf authRes apiReq) (PreQuery.preReqQuery <$> preReq) in
|
||||
case plan of
|
||||
DbCrud _ WrappedReadPlan{..} ->
|
||||
let countQuery = QueryBuilder.readPlanToCountQuery wrReadPlan in
|
||||
genQ (Statements.mainRead wrReadPlan countQuery preferCount configDbMaxRows range pMedia wrHandler) (mempty, mempty, mempty)
|
||||
genQ (Statements.mainRead wrReadPlan countQuery preferCount configDbMaxRows pMedia wrHandler) (mempty, mempty, mempty)
|
||||
(if shouldExplainCount preferCount then Just (Statements.postExplain countQuery) else Nothing)
|
||||
DbCrud _ MutateReadPlan{..} ->
|
||||
genQ (Statements.mainWrite mrReadPlan mrMutatePlan pMedia mrHandler preferRepresentation preferResolution) (mempty, mempty, mempty) mempty
|
||||
DbCrud _ CallReadPlan{..} ->
|
||||
genQ (Statements.mainCall crProc crCallPlan crReadPlan preferCount configDbMaxRows range pMedia crHandler) (mempty, mempty, mempty) mempty
|
||||
genQ (Statements.mainCall crProc crCallPlan crReadPlan preferCount pMedia crHandler) (mempty, mempty, mempty) mempty
|
||||
MayUseDb InspectPlan{ipSchema=tSchema} ->
|
||||
genQ mempty (SqlFragment.accessibleTables tSchema, SqlFragment.accessibleFuncs tSchema, SqlFragment.schemaDescription tSchema) mempty
|
||||
|
||||
@@ -10,7 +10,6 @@ module PostgREST.Query.PreQuery
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Hasql.DynamicStatements.Snippet as SQL hiding (sql)
|
||||
@@ -47,10 +46,7 @@ txVarQuery dbActPlan AppConfig{..} AuthResult{..} ApiRequest{..} =
|
||||
pathSql = setConfigWithConstantName ("request.path", iPath)
|
||||
headersSql = setConfigWithConstantNameJSON "request.headers" iHeaders
|
||||
cookiesSql = setConfigWithConstantNameJSON "request.cookies" iCookies
|
||||
claimsSql = [setConfigWithConstantName ("request.jwt.claims", LBS.toStrict $ JSON.encode claims)]
|
||||
where
|
||||
claims = authClaims & KM.insert "role" (JSON.String $ decodeUtf8 authRole) -- insert "role" to claims as well
|
||||
|
||||
claimsSql = [setConfigWithConstantName ("request.jwt.claims", LBS.toStrict $ JSON.encode authClaims)]
|
||||
roleSql = [setConfigWithConstantName ("role", authRole)]
|
||||
roleSettingsSql = setConfigWithDynamicName <$> HM.toList (fromMaybe mempty $ HM.lookup authRole configRoleSettings)
|
||||
appSettingsSql = setConfigWithDynamicName . join bimap toUtf8 <$> configAppSettings
|
||||
|
||||
@@ -264,7 +264,7 @@ readPlanToCountQuery (Node ReadPlan{from=mainQi, fromAlias=tblAlias, where_=logi
|
||||
limitedQuery :: SQL.Snippet -> Maybe Integer -> SQL.Snippet
|
||||
limitedQuery query maxRows = query <> SQL.sql (maybe mempty (\x -> " LIMIT " <> BS.pack (show x)) maxRows)
|
||||
|
||||
-- TODO refactor so this function is unneeded and ComputedRelationship QualifiedIdentifier comes from the ReadPlan type
|
||||
-- TODO refactor so this function is uneeded and ComputedRelationship QualifiedIdentifier comes from the ReadPlan type
|
||||
getQualifiedIdentifier :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> QualifiedIdentifier
|
||||
getQualifiedIdentifier rel mainQi tblAlias = case rel of
|
||||
Just ComputedRelationship{relFunction} -> QualifiedIdentifier mempty $ fromMaybe (qiName relFunction) tblAlias
|
||||
|
||||
@@ -23,7 +23,6 @@ module PostgREST.Query.SqlFragment
|
||||
, locationF
|
||||
, noLocationF
|
||||
, orderF
|
||||
, pageCountSelectF
|
||||
, pgFmtColumn
|
||||
, pgFmtFilter
|
||||
, pgFmtIdent
|
||||
@@ -97,7 +96,6 @@ import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
||||
Routine (..),
|
||||
funcReturnsScalar,
|
||||
funcReturnsSetOfScalar,
|
||||
funcReturnsSingle,
|
||||
funcReturnsSingleComposite)
|
||||
|
||||
import Protolude hiding (Sum, cast)
|
||||
@@ -487,21 +485,15 @@ pgFmtGroup _ CoercibleSelectField{csAggFunction=Just _} = Nothing
|
||||
pgFmtGroup _ CoercibleSelectField{csAlias=Just alias, csAggFunction=Nothing} = Just $ pgFmtIdent alias
|
||||
pgFmtGroup qi CoercibleSelectField{csField=fld, csAlias=Nothing, csAggFunction=Nothing} = Just $ pgFmtField qi fld
|
||||
|
||||
countF :: SQL.Snippet -> SQL.Snippet -> Bool -> Maybe Integer -> NonnegRange -> (SQL.Snippet, SQL.Snippet)
|
||||
countF countQuery pageCountSelect shouldCount maxRows range
|
||||
| shouldCount = if isJust maxRows || range /= allRange
|
||||
then ( ", pgrst_source_count AS (" <> countQuery <> ")"
|
||||
, "(SELECT pg_catalog.count(*) FROM pgrst_source_count)" )
|
||||
-- When there are no db-max-rows and limits/offsets, the total count will be the same as the page count,
|
||||
-- so we use the same page count here to avoid doing a separate aggregated count.
|
||||
else ( mempty, pageCountSelect )
|
||||
| otherwise = ( mempty, "null::bigint" )
|
||||
|
||||
pageCountSelectF :: Maybe Routine -> SQL.Snippet
|
||||
pageCountSelectF rout =
|
||||
if maybe False funcReturnsSingle rout
|
||||
then "1"
|
||||
else "pg_catalog.count(_postgrest_t)"
|
||||
countF :: SQL.Snippet -> Bool -> (SQL.Snippet, SQL.Snippet)
|
||||
countF countQuery shouldCount =
|
||||
if shouldCount
|
||||
then (
|
||||
", pgrst_source_count AS (" <> countQuery <> ")"
|
||||
, "(SELECT pg_catalog.count(*) FROM pgrst_source_count)" )
|
||||
else (
|
||||
mempty
|
||||
, "null::bigint")
|
||||
|
||||
returningF :: QualifiedIdentifier -> [FieldName] -> SQL.Snippet
|
||||
returningF qi returnings =
|
||||
|
||||
@@ -20,8 +20,8 @@ import PostgREST.Plan.MutatePlan as MTPlan
|
||||
import PostgREST.Plan.ReadPlan
|
||||
import PostgREST.Query.QueryBuilder
|
||||
import PostgREST.Query.SqlFragment
|
||||
import PostgREST.RangeQuery (NonnegRange)
|
||||
import PostgREST.SchemaCache.Routine (MediaHandler (..), Routine)
|
||||
import PostgREST.SchemaCache.Routine (MediaHandler (..), Routine,
|
||||
funcReturnsSingle)
|
||||
|
||||
import Protolude
|
||||
|
||||
@@ -64,24 +64,23 @@ mainWrite rPlan mtplan mt handler rep resolution = mtSnippet mt snippet
|
||||
_ -> (False,False, mempty);
|
||||
|
||||
mainRead :: ReadPlanTree -> SQL.Snippet -> Maybe PreferCount -> Maybe Integer ->
|
||||
NonnegRange -> MediaType -> MediaHandler -> SQL.Snippet
|
||||
mainRead rPlan countQuery pCount maxRows range mt handler = mtSnippet mt snippet
|
||||
MediaType -> MediaHandler -> SQL.Snippet
|
||||
mainRead rPlan countQuery pCount maxRows mt handler = mtSnippet mt snippet
|
||||
where
|
||||
snippet =
|
||||
"WITH " <> sourceCTE <> " AS ( " <> selectQuery <> " ) " <>
|
||||
countCTEF <> " " <>
|
||||
"SELECT " <>
|
||||
countResultF <> " AS total_result_set, " <>
|
||||
pageCountSelect <> " AS page_total, " <>
|
||||
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
||||
handlerF Nothing handler <> " AS body, " <>
|
||||
responseHeadersF <> " AS response_headers, " <>
|
||||
responseStatusF <> " AS response_status, " <>
|
||||
"''" <> " AS response_inserted " <>
|
||||
"FROM ( SELECT * FROM " <> sourceCTE <> " ) _postgrest_t"
|
||||
|
||||
(countCTEF, countResultF) = countF countQ pageCountSelect (shouldCount pCount) maxRows range
|
||||
(countCTEF, countResultF) = countF countQ $ shouldCount pCount
|
||||
selectQuery = readPlanToQuery rPlan
|
||||
pageCountSelect = pageCountSelectF Nothing
|
||||
countQ =
|
||||
if pCount == Just EstimatedCount then
|
||||
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
||||
@@ -89,27 +88,28 @@ mainRead rPlan countQuery pCount maxRows range mt handler = mtSnippet mt snippet
|
||||
else
|
||||
countQuery
|
||||
|
||||
mainCall :: Routine -> CallPlan -> ReadPlanTree -> Maybe PreferCount -> Maybe Integer ->
|
||||
NonnegRange-> MediaType -> MediaHandler -> SQL.Snippet
|
||||
mainCall rout cPlan rPlan pCount maxRows range mt handler = mtSnippet mt snippet
|
||||
mainCall :: Routine -> CallPlan -> ReadPlanTree -> Maybe PreferCount ->
|
||||
MediaType -> MediaHandler -> SQL.Snippet
|
||||
mainCall rout cPlan rPlan pCount mt handler = mtSnippet mt snippet
|
||||
where
|
||||
snippet =
|
||||
"WITH " <> sourceCTE <> " AS (" <> callProcQuery <> ") " <>
|
||||
countCTEF <>
|
||||
"SELECT " <>
|
||||
countResultF <> " AS total_result_set, " <>
|
||||
pageCountSelect <> " AS page_total, " <>
|
||||
(if funcReturnsSingle rout
|
||||
then "1"
|
||||
else "pg_catalog.count(_postgrest_t)") <> " AS page_total, " <>
|
||||
handlerF (Just rout) handler <> " AS body, " <>
|
||||
responseHeadersF <> " AS response_headers, " <>
|
||||
responseStatusF <> " AS response_status, " <>
|
||||
"''" <> " AS response_inserted " <>
|
||||
"FROM (" <> selectQuery <> ") _postgrest_t"
|
||||
|
||||
(countCTEF, countResultF) = countF countQuery pageCountSelect (shouldCount pCount) maxRows range
|
||||
(countCTEF, countResultF) = countF countQuery $ shouldCount pCount
|
||||
selectQuery = readPlanToQuery rPlan
|
||||
callProcQuery = callPlanToQuery cPlan
|
||||
countQuery = readPlanToCountQuery rPlan
|
||||
pageCountSelect = pageCountSelectF (Just rout)
|
||||
|
||||
-- This occurs after the main query runs, that's why it's prefixed with "post"
|
||||
postExplain :: SQL.Snippet -> SQL.Snippet
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user