Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a506d18c0 | ||
|
|
711202c2a0 | ||
|
|
9dd3f3c094 | ||
|
|
cf4aa20d7c | ||
|
|
1afd98a739 | ||
|
|
077e4a857e | ||
|
|
903537d39d | ||
|
|
67f6b1c7e8 | ||
|
|
977710f442 | ||
|
|
687558937c | ||
|
|
491c7a0891 | ||
|
|
e4984dc751 | ||
|
|
1b0f29a8cc | ||
|
|
a0d3ebf590 | ||
|
|
ac217c120f | ||
|
|
961db7c7ef | ||
|
|
c128f03b61 | ||
|
|
b8b34f05df | ||
|
|
608ee5a9ad | ||
|
|
9737c5c5f6 | ||
|
|
e520f32297 | ||
|
|
19dcf7b344 | ||
|
|
a2b7b0e69d | ||
|
|
e01f08392f | ||
|
|
e6282d88d8 | ||
|
|
5cab03ad5d | ||
|
|
b71739ee50 | ||
|
|
0d60a79532 | ||
|
|
d38e5cdb02 | ||
|
|
908cdf0b6f | ||
|
|
ee55efe0c9 | ||
|
|
e60bae8e32 | ||
|
|
ffe44a2e71 | ||
|
|
dcc6772ce1 | ||
|
|
91ffee1d2e | ||
|
|
1b52ce1958 | ||
|
|
8186e78691 | ||
|
|
b9186b6d9a | ||
|
|
b663a9aa37 | ||
|
|
4d874fb725 | ||
|
|
68e145550e | ||
|
|
3406e62a77 | ||
|
|
3e5e8a22e4 | ||
|
|
414a0daca1 | ||
|
|
30f18cf22c | ||
|
|
fd9192836e | ||
|
|
02d8990ce2 | ||
|
|
06821fdbca | ||
|
|
b77a08ff10 | ||
|
|
278be7074e | ||
|
|
b9d46812f8 | ||
|
|
7f410f3be0 | ||
|
|
8aa8e682a2 | ||
|
|
867dc58ecc | ||
|
|
6962e41a37 | ||
|
|
8605edcb6f | ||
|
|
efcf36d970 | ||
|
|
f95c5127df | ||
|
|
a4e00ffdf6 | ||
|
|
cd62e39cd1 | ||
|
|
ec0f99c686 | ||
|
|
b2cd365866 | ||
|
|
fcc330311f | ||
|
|
e557161b84 | ||
|
|
962268fd6b | ||
|
|
cb501a9041 | ||
|
|
a7d2c8e6ad | ||
|
|
40e16bd468 | ||
|
|
49e257617e | ||
|
|
474bb93e89 | ||
|
|
d75538ec4a | ||
|
|
fc58262cb2 | ||
|
|
fdfc095b0e | ||
|
|
eb582fb4b0 | ||
|
|
cd38da56d5 | ||
|
|
229a4e4cd6 | ||
|
|
c3f7440e33 | ||
|
|
4e03ee252b | ||
|
|
0ac4d0d0a9 | ||
|
|
ceda77c5de | ||
|
|
a6e4a0078b | ||
|
|
15f4157cc5 | ||
|
|
50ea1999b2 | ||
|
|
c92f16a2cb | ||
|
|
1cebc03313 | ||
|
|
9f252678a4 | ||
|
|
802131aa96 | ||
|
|
275bdee47c | ||
|
|
f70e852a0a | ||
|
|
00580bc8cb | ||
|
|
70c0d88fa7 | ||
|
|
2b4e0cc10a | ||
|
|
1268b0a258 | ||
|
|
11f40c384a | ||
|
|
2bb9cee996 | ||
|
|
a38eee399e | ||
|
|
ee4321623f | ||
|
|
49903ca6db | ||
|
|
d15e357d2e | ||
|
|
62512268cd | ||
|
|
84152b483a | ||
|
|
9fe11249f9 | ||
|
|
7640de34e2 | ||
|
|
31ce39ba36 | ||
|
|
ca5eb64deb | ||
|
|
a72241ad4e | ||
|
|
b080f59bac | ||
|
|
558e9d40e8 | ||
|
|
958339b8d3 | ||
|
|
3d56f8435d | ||
|
|
e620ae5e9c | ||
|
|
bd6d8624ab | ||
|
|
03e384d55b | ||
|
|
dfa875c8c7 | ||
|
|
33891e3a73 | ||
|
|
8483459d59 | ||
|
|
7b5c2f03fc | ||
|
|
b538ab9823 | ||
|
|
850b15fe13 | ||
|
|
9eda78eda5 | ||
|
|
df97a5071f | ||
|
|
1c60b50e2e | ||
|
|
c3301a1653 | ||
|
|
abcb21c69a | ||
|
|
475b4601ca | ||
|
|
49e3d2c4b4 | ||
|
|
f7bf2157f3 | ||
|
|
125f10a60f | ||
|
|
5502dce556 | ||
|
|
084e8c2950 | ||
|
|
3c1a7f2641 | ||
|
|
f10d4139fe | ||
|
|
7e7e494b31 | ||
|
|
99b705d44f | ||
|
|
e0586c3afa | ||
|
|
379d9d6298 | ||
|
|
2c35257eec | ||
|
|
ed90774f53 | ||
|
|
96ca177c31 | ||
|
|
226400a5bc | ||
|
|
57ceabb260 | ||
|
|
b85ee953b7 | ||
|
|
b235227119 | ||
|
|
5c9c7f4ff4 | ||
|
|
82b38341bf | ||
|
|
4a90e9fbd9 | ||
|
|
14d030b96c | ||
|
|
6920a88dc0 | ||
|
|
966f8df33f | ||
|
|
98bd1de1cd | ||
|
|
d94286c185 | ||
|
|
618f93dec1 | ||
|
|
317619bf62 | ||
|
|
eb238ad678 | ||
|
|
54786a6c04 | ||
|
|
00f3cb3746 | ||
|
|
e977032847 | ||
|
|
5c314e3f02 | ||
|
|
f10b4c3268 | ||
|
|
dc01c748ae | ||
|
|
bae8dc3283 | ||
|
|
056c748c5f | ||
|
|
93f2b0093c | ||
|
|
5ba370fe0a | ||
|
|
818387f24e | ||
|
|
a9d6c318fd | ||
|
|
2aa58164ab | ||
|
|
df08d7f3ff | ||
|
|
3dd292be46 | ||
|
|
e29260d754 | ||
|
|
d9857e1c54 | ||
|
|
0703f27d2b | ||
|
|
8a7889b9b1 | ||
|
|
af0e369c65 | ||
|
|
910950dbac | ||
|
|
4c44782d15 | ||
|
|
3b1eb51744 | ||
|
|
cf7ee67dfe | ||
|
|
290d90609b | ||
|
|
3c1cdd434a | ||
|
|
2825ac059e | ||
|
|
a6e3eda5b2 | ||
|
|
10231bc1a8 | ||
|
|
90e3a5e29f | ||
|
|
add4dfeed5 | ||
|
|
fca039a54d | ||
|
|
1b4dae5a7a | ||
|
|
37a3f818dd | ||
|
|
c3169b7dc2 | ||
|
|
d64b71cbf0 | ||
|
|
c195eece65 | ||
|
|
fa182c216e | ||
|
|
30474c424c | ||
|
|
2888f351d1 | ||
|
|
69eaef461f | ||
|
|
8d3c9f8435 | ||
|
|
d8a91453d1 | ||
|
|
50c87fec88 | ||
|
|
4ee5410c3d | ||
|
|
3f5e840baf | ||
|
|
2358b6670f | ||
|
|
96b8fccd2a | ||
|
|
d719cbf59d | ||
|
|
a6f1eefe55 | ||
|
|
3dead2ac8b | ||
|
|
5a1ed11924 | ||
|
|
46eae105b7 | ||
|
|
8eed576826 | ||
|
|
d643aab261 | ||
|
|
07fef25591 | ||
|
|
9352a72195 | ||
|
|
7dc6e2b899 | ||
|
|
ff3a3a9100 | ||
|
|
57fa2719dd | ||
|
|
b8b3145c5c | ||
|
|
531a183b44 | ||
|
|
739f056b0a | ||
|
|
32b77cae6c | ||
|
|
46548dd09b | ||
|
|
2434724edd | ||
|
|
87d6a0d0fe |
+16
@@ -0,0 +1,16 @@
|
||||
freebsd_instance:
|
||||
image_family: freebsd-14-0
|
||||
|
||||
build_task:
|
||||
# Don't change this name without adjusting .github/workflows/ci.yaml
|
||||
name: Build FreeBSD (Stack)
|
||||
install_script: pkg install -y postgresql13-client hs-stack git
|
||||
|
||||
stack_cache:
|
||||
folders: /.stack
|
||||
fingerprint_script: cat postgrest.cabal stack.yaml.lock
|
||||
reupload_on_changes: false
|
||||
|
||||
build_script: stack build -j 1 --local-bin-path . --copy-bins
|
||||
bin_artifacts:
|
||||
path: postgrest
|
||||
@@ -3,4 +3,16 @@ When submitting a new feature or fix:
|
||||
|
||||
- Add a new entry to the CHANGELOG - https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md#unreleased
|
||||
- If relevant, update the docs
|
||||
- Use a prefix for the PR title or commits, e.g. "fix: description of the fix".
|
||||
+ `fix`, bug fixes
|
||||
+ `feat`, new features added
|
||||
+ `perf`, performance improvements
|
||||
+ `nix`, related to the Nix development environment
|
||||
+ `ci`, related to the Continuous Integration modules
|
||||
+ `test`, related to the testing modules
|
||||
+ `refactor`, refactoring code
|
||||
+ `deprecate`, deprecating a feature
|
||||
+ `chore`, maintenance (changelog, build process, etc.)
|
||||
+ Other prefixes may be used if necessary
|
||||
- If there's a breaking change, add `BREAKING CHANGE` and an explanation to your commit message
|
||||
-->
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
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@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: ${{ inputs.upload }}
|
||||
path: ${{ steps.download.outputs.artifacts }}
|
||||
if-no-files-found: error
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Setup Nix
|
||||
|
||||
description: Installs nix, sets up cachix and installs a subset of tooling.
|
||||
|
||||
inputs:
|
||||
authToken:
|
||||
description: Token to pass to cachix
|
||||
tools:
|
||||
description: Tools to install with nix-env -iA <tools>
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: nixbuild/nix-quick-install-action@60e9c39264d4714139af3cdf15f691b19eec3530 # v28
|
||||
with:
|
||||
nix_conf: |-
|
||||
always-allow-substitutes = true
|
||||
max-jobs = auto
|
||||
- uses: cachix/cachix-action@18cf96c7c98e048e10a83abd92116114cd8504be # v14
|
||||
with:
|
||||
name: postgrest
|
||||
authToken: ${{ inputs.authToken }}
|
||||
- if: ${{ inputs.tools }}
|
||||
run: nix-env -f default.nix -iA ${{ inputs.tools }}
|
||||
shell: bash
|
||||
@@ -0,0 +1,18 @@
|
||||
codecov:
|
||||
branch: main
|
||||
require_ci_to_pass: false
|
||||
|
||||
comment: false
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 1%
|
||||
only_pulls: false
|
||||
patch:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 1%
|
||||
only_pulls: true
|
||||
@@ -0,0 +1,11 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
||||
- package-ecosystem: github-actions
|
||||
directory: /.github/actions/setup-nix
|
||||
schedule:
|
||||
interval: weekly
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Tag a release that will be built with Github Actions. The version is
|
||||
# read from 'postgrest.cabal'.
|
||||
|
||||
version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||
|
||||
echo "Tagging version v$version"
|
||||
git tag -f "v$version"
|
||||
|
||||
echo "Pushing tag..."
|
||||
git push -f origin "refs/tags/v$version"
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# This script builds PostgREST in a remote ARM server
|
||||
|
||||
[ -z "$1" ] && { echo "Missing 1st argument: PostgREST github commit SHA"; exit 1; }
|
||||
[ -z "$2" ] && { echo "Missing 2nd argument: Build environment directory name"; exit 1; }
|
||||
[ -z "$3" ] && { echo "Missing 3rd argument: GHC version"; exit 1; }
|
||||
|
||||
PGRST_GITHUB_COMMIT="$1"
|
||||
SCRIPT_DIR="$2"
|
||||
|
||||
DOCKER_BUILD_DIR="$SCRIPT_DIR/docker-env"
|
||||
# latest is a shortcut documented on https://www.haskell.org/ghcup/guide/#tags-and-shortcuts
|
||||
CABAL_VERSION="latest"
|
||||
GHC_VERSION="$3"
|
||||
|
||||
install_packages() {
|
||||
sudo apt-get update -y
|
||||
sudo apt-get upgrade -y
|
||||
sudo apt-get install -y git build-essential curl libffi-dev libffi7 libgmp-dev libgmp10 libncurses-dev libncurses5 libtinfo5 llvm libnuma-dev zlib1g-dev libpq-dev jq gcc
|
||||
sudo apt-get clean
|
||||
}
|
||||
|
||||
install_ghcup() {
|
||||
export BOOTSTRAP_HASKELL_NONINTERACTIVE=1
|
||||
export BOOTSTRAP_HASKELL_MINIMAL=1
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
|
||||
source ~/.ghcup/env
|
||||
}
|
||||
|
||||
install_cabal() {
|
||||
ghcup upgrade
|
||||
ghcup install cabal $CABAL_VERSION
|
||||
ghcup set cabal $CABAL_VERSION
|
||||
}
|
||||
|
||||
install_ghc() {
|
||||
ghcup upgrade
|
||||
ghcup install ghc $GHC_VERSION
|
||||
ghcup set ghc $GHC_VERSION
|
||||
}
|
||||
|
||||
install_packages
|
||||
|
||||
# Add ghcup to the PATH for this session
|
||||
[ -f ~/.ghcup/env ] && source ~/.ghcup/env
|
||||
|
||||
ghcup --version || install_ghcup
|
||||
ghcup set cabal $CABAL_VERSION || install_cabal
|
||||
ghcup set ghc $GHC_VERSION || install_ghc
|
||||
|
||||
cd ~/$SCRIPT_DIR
|
||||
|
||||
# Clone the repository and build the project
|
||||
git clone https://github.com/PostgREST/postgrest.git
|
||||
cd postgrest
|
||||
git checkout $PGRST_GITHUB_COMMIT
|
||||
cabal v2-update && cabal v2-build
|
||||
|
||||
# Copy the built binary to the Dockerfile directory
|
||||
PGRST_BIN=$(cabal exec which postgrest | tail -1)
|
||||
cp $PGRST_BIN ~/$DOCKER_BUILD_DIR
|
||||
|
||||
# Move and compress the built binary
|
||||
mkdir -p ~/$SCRIPT_DIR/result
|
||||
mv $PGRST_BIN ~/$SCRIPT_DIR/result
|
||||
cd ~/$SCRIPT_DIR
|
||||
tar -cJf result.tar.xz result
|
||||
@@ -0,0 +1,18 @@
|
||||
# PostgREST docker hub image
|
||||
|
||||
FROM ubuntu:noble@sha256:3f85b7caad41a95462cf5b787d8a04604c8262cdcdf9a472b8c52ef83375fe15 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
|
||||
|
||||
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"]
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# This script publishes the Docker ARM images to Docker Hub.
|
||||
|
||||
[ -z "$1" ] && { echo "Missing 1st argument: PostgREST github commit SHA"; exit 1; }
|
||||
[ -z "$2" ] && { echo "Missing 2nd argument: Docker repo"; exit 1; }
|
||||
[ -z "$3" ] && { echo "Missing 3rd argument: Docker username"; exit 1; }
|
||||
[ -z "$4" ] && { echo "Missing 4th argument: Docker password"; exit 1; }
|
||||
[ -z "$5" ] && { echo "Missing 5th argument: Build environment directory name"; exit 1; }
|
||||
[ -z "$6" ] && { echo "Missing 6th argument: PostgREST version"; exit 1; }
|
||||
|
||||
PGRST_GITHUB_COMMIT="$1"
|
||||
DOCKER_REPO="$2"
|
||||
DOCKER_USER="$3"
|
||||
DOCKER_PASS="$4"
|
||||
SCRIPT_DIR="$5"
|
||||
PGRST_VERSION="$6"
|
||||
|
||||
DOCKER_BUILD_DIR="$SCRIPT_DIR/docker-env"
|
||||
|
||||
clean_env()
|
||||
{
|
||||
sudo docker logout
|
||||
}
|
||||
|
||||
# Login to Docker
|
||||
sudo docker logout
|
||||
{ echo $DOCKER_PASS | sudo docker login -u $DOCKER_USER --password-stdin; } || { echo "Couldn't login to docker"; exit 1; }
|
||||
|
||||
trap clean_env sigint sigterm exit
|
||||
|
||||
# Move to the docker build environment
|
||||
cd ~/$DOCKER_BUILD_DIR
|
||||
|
||||
# Push final images to Docker hub
|
||||
# NOTE: This command publishes a separate ARM image because the builds cannot
|
||||
# be added to the manifest if they are not in the registry beforehand.
|
||||
# This image must be manually deleted from Docker Hub at the end of the process.
|
||||
sudo docker buildx build --build-arg PGRST_GITHUB_COMMIT=$PGRST_GITHUB_COMMIT \
|
||||
-t $DOCKER_REPO/postgrest:$PGRST_VERSION-arm \
|
||||
--push .
|
||||
|
||||
# Add the arm images to the manifest
|
||||
# NOTE: This assumes that there already is a `postgrest:<version>` image
|
||||
# for the amd64 architecture pushed to Docker Hub
|
||||
sudo docker buildx imagetools create --append -t $DOCKER_REPO/postgrest:$PGRST_VERSION $DOCKER_REPO/postgrest:$PGRST_VERSION-arm
|
||||
[ "$PGRST_VERSION" != "devel" ] && sudo docker buildx imagetools create --append -t $DOCKER_REPO/postgrest:latest $DOCKER_REPO/postgrest:$PGRST_VERSION-arm
|
||||
|
||||
sudo docker logout
|
||||
@@ -0,0 +1,78 @@
|
||||
name: Cachix
|
||||
|
||||
# This workflow serves to
|
||||
# - keep cachix up to date with the main branch
|
||||
# - incrementally update cachix for large dependency
|
||||
# updates, e.g. after running postgrest-nixpkgs-upgrade,
|
||||
# which can cause the main CI workflow to time out
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- rel-*
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
Seed-Cachix:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: Linux
|
||||
runs-on: ubuntu-22.04
|
||||
- os: MacOS
|
||||
runs-on: macos-12
|
||||
name: Seed ${{ matrix.os }}
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
|
||||
- name: Install cachix tooling
|
||||
run: |
|
||||
nix-env -f default.nix -iA devTools.pushCachix.bin
|
||||
postgrest-push-cachix
|
||||
|
||||
- name: Seed dynamic postgrest build
|
||||
run: |
|
||||
nix-build -A postgrestPackage
|
||||
postgrest-push-cachix
|
||||
|
||||
- name: Seed style tools
|
||||
run: |
|
||||
nix-build -A style
|
||||
postgrest-push-cachix
|
||||
|
||||
- name: Seed test tools
|
||||
run: |
|
||||
nix-build -A tests
|
||||
postgrest-push-cachix
|
||||
|
||||
- name: Seed static toolchain
|
||||
if: matrix.os == 'Linux'
|
||||
run: |
|
||||
nix-build -A packagesStatic.haskellPackages.hello
|
||||
postgrest-push-cachix
|
||||
|
||||
- name: Seed static postgresql build (for libpq)
|
||||
if: matrix.os == 'Linux'
|
||||
run: |
|
||||
nix-build -A packagesStatic.pkgs.postgresql
|
||||
postgrest-push-cachix
|
||||
|
||||
- name: Seed static postgrest build
|
||||
if: matrix.os == 'Linux'
|
||||
run: |
|
||||
nix-build -A postgrestStatic
|
||||
postgrest-push-cachix
|
||||
|
||||
- name: Build and push everything to Cachix
|
||||
run: |
|
||||
nix-build
|
||||
postgrest-push-cachix
|
||||
@@ -0,0 +1,554 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v[0-9]+
|
||||
tags:
|
||||
- devel
|
||||
- v*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- v[0-9]+
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
# Terminate all previous runs of the same workflow for pull requests
|
||||
cancel-in-progress: "${{ github.event_name == 'pull_request' }}"
|
||||
|
||||
jobs:
|
||||
Lint-Style:
|
||||
name: Lint & check code style
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
tools: style
|
||||
- name: Run linter (check locally with `nix-shell --run postgrest-lint`)
|
||||
run: postgrest-lint
|
||||
- name: Run style check (auto-format with `nix-shell --run postgrest-style`)
|
||||
run: postgrest-style-check
|
||||
|
||||
|
||||
Test-Nix:
|
||||
name: Test (Nix)
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
# Hack for enabling color output, see:
|
||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
tools: tests
|
||||
|
||||
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
||||
run: postgrest-coverage
|
||||
- name: Upload coverage to codecov
|
||||
uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # v4.3.1
|
||||
with:
|
||||
files: ./coverage/codecov.json
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Run doctests
|
||||
if: always()
|
||||
run: nix-shell --run postgrest-test-doctests
|
||||
|
||||
- name: Check the spec tests for idempotence
|
||||
if: always()
|
||||
run: postgrest-test-spec-idempotence
|
||||
|
||||
|
||||
Test-Pg-Nix:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pgVersion: [9.6, 10, 11, 12, 13, 14, 15, 16]
|
||||
name: Test PG ${{ matrix.pgVersion }} (Nix)
|
||||
runs-on: ubuntu-22.04
|
||||
defaults:
|
||||
run:
|
||||
# Hack for enabling color output, see:
|
||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
tools: tests withTools
|
||||
|
||||
- name: Run spec tests
|
||||
if: always()
|
||||
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-spec
|
||||
|
||||
- name: Run IO tests
|
||||
if: always()
|
||||
run: postgrest-with-postgresql-${{ matrix.pgVersion }} -f test/io/fixtures.sql postgrest-test-io -vv
|
||||
|
||||
|
||||
Test-Memory-Nix:
|
||||
name: Test memory (Nix)
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
tools: memory
|
||||
- name: Run memory tests
|
||||
run: postgrest-test-memory
|
||||
|
||||
|
||||
Build-Static-Nix:
|
||||
name: Build Linux static (Nix)
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
tools: tests
|
||||
|
||||
- name: Build static executable
|
||||
run: nix-build -A postgrestStatic
|
||||
- name: Check static executable
|
||||
run: postgrest-check-static result/bin/postgrest
|
||||
- name: Save built executable as artifact
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: postgrest-linux-static-x64
|
||||
path: result/bin/postgrest
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Build Docker image
|
||||
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
|
||||
- name: Save built Docker image as artifact
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: postgrest-docker-x64
|
||||
path: postgrest-docker.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
Build-Macos-Nix:
|
||||
name: Build MacOS (Nix)
|
||||
runs-on: macos-12
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
|
||||
- name: Build everything
|
||||
run: |
|
||||
nix-build
|
||||
|
||||
|
||||
Build-Stack:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux
|
||||
runs-on: ubuntu-22.04
|
||||
cache: |
|
||||
~/.stack
|
||||
.stack-work
|
||||
artifact: postgrest-ubuntu-x64
|
||||
|
||||
- name: MacOS
|
||||
runs-on: macos-12
|
||||
cache: |
|
||||
~/.stack
|
||||
.stack-work
|
||||
artifact: postgrest-macos-x64
|
||||
|
||||
- name: Windows
|
||||
runs-on: windows-2022
|
||||
cache: |
|
||||
~\AppData\Roaming\stack
|
||||
~\AppData\Local\Programs\stack
|
||||
.stack-work
|
||||
deps: Add-Content $env:GITHUB_PATH $env:PGBIN
|
||||
artifact: postgrest-windows-x64
|
||||
|
||||
name: Build ${{ matrix.name }} (Stack)
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Stack working files cache
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
with:
|
||||
path: ${{ matrix.cache }}
|
||||
key: cache-stack-${{ runner.os }}-${{ hashFiles('stack.yaml.lock') }}
|
||||
- name: Install dependencies
|
||||
if: ${{ matrix.deps }}
|
||||
run: ${{ matrix.deps }}
|
||||
- name: Build with Stack
|
||||
run: stack build --local-bin-path result --copy-bins
|
||||
- name: Save built executable as artifact
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
path: |
|
||||
result/postgrest
|
||||
result/postgrest.exe
|
||||
if-no-files-found: error
|
||||
|
||||
Get-FreeBSD-CirrusCI:
|
||||
name: Get FreeBSD build from CirrusCI
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: ./.github/actions/artifact-from-cirrus
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
task: Build FreeBSD (Stack)
|
||||
download: bin
|
||||
upload: postgrest-freebsd-x64
|
||||
|
||||
Build-Cabal:
|
||||
strategy:
|
||||
matrix:
|
||||
ghc: ['9.0.2', '9.2.4']
|
||||
fail-fast: false
|
||||
name: Build Linux (Cabal, GHC ${{ matrix.ghc }})
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Workaround runner image issue
|
||||
# https://github.com/actions/runner-images/issues/7061
|
||||
run: sudo chown -R "$USER" /usr/local/.ghcup
|
||||
- name: ghcup
|
||||
run: |
|
||||
ghcup install ghc ${{ matrix.ghc }}
|
||||
ghcup set ghc ${{ matrix.ghc }}
|
||||
- name: Copy cabal.project & fix caching
|
||||
run: |
|
||||
mkdir ~/.cabal
|
||||
cp cabal.project.non-nix cabal.project
|
||||
- name: Cache
|
||||
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
|
||||
with:
|
||||
path: |
|
||||
~/.cabal/packages
|
||||
~/.cabal/store
|
||||
dist-newstyle
|
||||
key: cache-cabal-${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('**/*.cabal', '**/cabal.project') }}
|
||||
restore-keys: |
|
||||
cache-cabal-${{ runner.os }}-${{ matrix.ghc }}-
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cabal update
|
||||
cabal build --only-dependencies --enable-tests --enable-benchmarks
|
||||
- name: Build
|
||||
run: cabal build --enable-tests --enable-benchmarks all
|
||||
|
||||
Build-Cabal-Arm:
|
||||
strategy:
|
||||
matrix:
|
||||
ghc: ['9.2.4']
|
||||
fail-fast: false
|
||||
name: Build aarch64 (Cabal, GHC ${{ matrix.ghc }})
|
||||
if: "${{ github.event_name == 'push' }}"
|
||||
runs-on: ubuntu-22.04
|
||||
outputs:
|
||||
remotepath: ${{ steps.Remote-Dir.outputs.remotepath }}
|
||||
env:
|
||||
GITHUB_COMMIT: ${{ github.sha }}
|
||||
GHC_VERSION: ${{ matrix.ghc }}
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- id: Remote-Dir
|
||||
name: Unique directory name for the remote build
|
||||
run: echo "remotepath=postgrest-build-$(uuidgen)" >> "$GITHUB_OUTPUT"
|
||||
- name: Copy script files to the remote server
|
||||
uses: appleboy/scp-action@master
|
||||
with:
|
||||
host: ${{ secrets.SSH_ARM_HOST }}
|
||||
username: ubuntu
|
||||
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
|
||||
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
|
||||
source: ".github/scripts/arm/*"
|
||||
target: ${{ steps.Remote-Dir.outputs.remotepath }}
|
||||
strip_components: 3
|
||||
- name: Build ARM
|
||||
uses: appleboy/ssh-action@master
|
||||
env:
|
||||
REMOTE_DIR: ${{ steps.Remote-Dir.outputs.remotepath }}
|
||||
with:
|
||||
host: ${{ secrets.SSH_ARM_HOST }}
|
||||
username: ubuntu
|
||||
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
|
||||
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
|
||||
command_timeout: 120m
|
||||
script_stop: true
|
||||
envs: GITHUB_COMMIT,REMOTE_DIR,GHC_VERSION
|
||||
script: bash ~/$REMOTE_DIR/build.sh "$GITHUB_COMMIT" "$REMOTE_DIR" "$GHC_VERSION"
|
||||
- name: Download binaries from remote server
|
||||
uses: nicklasfrahm/scp-action@main
|
||||
with:
|
||||
direction: download
|
||||
host: ${{ secrets.SSH_ARM_HOST }}
|
||||
username: ubuntu
|
||||
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
|
||||
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
|
||||
source: "${{ steps.Remote-Dir.outputs.remotepath }}/result.tar.xz"
|
||||
target: "result.tar.xz"
|
||||
- name: Extract downloaded binaries
|
||||
run: tar -xvf result.tar.xz && rm result.tar.xz
|
||||
- name: Save aarch64 executable as artifact
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: postgrest-ubuntu-aarch64
|
||||
path: result/postgrest
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
Tag-Release:
|
||||
name: Tag Release
|
||||
if: startsWith(github.ref, 'refs/heads/')
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- Lint-Style
|
||||
- Test-Nix
|
||||
- Test-Pg-Nix
|
||||
- Test-Memory-Nix
|
||||
- Build-Static-Nix
|
||||
- Build-Stack
|
||||
- Get-FreeBSD-CirrusCI
|
||||
- Build-Cabal-Arm
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
with:
|
||||
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
|
||||
- name: Tag latest commit
|
||||
run: |
|
||||
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||
|
||||
if [[ "$cabal_version" == *.*.* ]]; then
|
||||
git fetch --tags
|
||||
|
||||
if [ -z "$(git tag --list "v$cabal_version")" ]; then
|
||||
git tag "v$cabal_version"
|
||||
git push origin "v$cabal_version"
|
||||
fi
|
||||
else
|
||||
git tag -f "devel"
|
||||
git push -f origin "devel"
|
||||
fi
|
||||
|
||||
|
||||
Prepare-Release:
|
||||
name: Prepare release
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- Lint-Style
|
||||
- Test-Nix
|
||||
- Test-Pg-Nix
|
||||
- Test-Memory-Nix
|
||||
- Build-Static-Nix
|
||||
- Build-Stack
|
||||
- Get-FreeBSD-CirrusCI
|
||||
- Build-Cabal-Arm
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Check the version to be released
|
||||
run: |
|
||||
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||
|
||||
if [ "${GITHUB_REF_NAME}" != "devel" ] && [ "${GITHUB_REF_NAME}" != "v$cabal_version" ]; then
|
||||
echo "Tagged version ($GITHUB_REF_NAME) does not match the one in postgrest.cabal (v$cabal_version). Aborting release..."
|
||||
exit 1
|
||||
fi
|
||||
- name: Identify changes from CHANGELOG.md
|
||||
run: |
|
||||
if [ "${GITHUB_REF_NAME}" == "devel" ]; then
|
||||
echo "Getting unreleased changes..."
|
||||
sed -n "1,/## Unreleased/d;/## \[/q;p" CHANGELOG.md > CHANGES.md
|
||||
else
|
||||
version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||
echo "Propper release, getting changes for version $version ..."
|
||||
sed -n "1,/## \[$version\]/d;/## \[/q;p" CHANGELOG.md > CHANGES.md
|
||||
fi
|
||||
|
||||
echo "Relevant extract from CHANGELOG.md:"
|
||||
cat CHANGES.md
|
||||
- name: Save CHANGES.md as artifact
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: release-changes
|
||||
path: CHANGES.md
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
Release-GitHub:
|
||||
name: Release on GitHub
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-22.04
|
||||
needs: Prepare-Release
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Create release bundle with archives for all builds
|
||||
run: |
|
||||
find artifacts -type f -iname postgrest -exec chmod +x {} \;
|
||||
|
||||
mkdir -p release-bundle
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-x64.tar.xz" \
|
||||
-C artifacts/postgrest-linux-static-x64 postgrest
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-x64.tar.xz" \
|
||||
-C artifacts/postgrest-macos-x64 postgrest
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x64.tar.xz" \
|
||||
-C artifacts/postgrest-freebsd-x64 postgrest
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-ubuntu-aarch64.tar.xz" \
|
||||
-C artifacts/postgrest-ubuntu-aarch64 postgrest
|
||||
|
||||
zip "release-bundle/postgrest-${GITHUB_REF_NAME}-windows-x64.zip" \
|
||||
artifacts/postgrest-windows-x64/postgrest.exe
|
||||
|
||||
- name: Save release bundle
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: release-bundle
|
||||
path: release-bundle
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Publish release on GitHub
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "Releasing version ${GITHUB_REF_NAME} on GitHub..."
|
||||
|
||||
if [ "${GITHUB_REF_NAME}" == "devel" ]; then
|
||||
# To replace the existing release, we must first delete the old assets,
|
||||
# then modify the release, then add the new assets.
|
||||
gh release view devel --json assets \
|
||||
| jq -r '.assets[] | .name' \
|
||||
| xargs -rn1 \
|
||||
gh release delete-asset -y devel
|
||||
gh release edit devel \
|
||||
-t devel \
|
||||
--verify-tag \
|
||||
-F artifacts/release-changes/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 \
|
||||
release-bundle/*
|
||||
fi
|
||||
|
||||
|
||||
Release-Docker:
|
||||
name: Release on Docker Hub
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- Prepare-Release
|
||||
env:
|
||||
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
|
||||
DOCKER_USER: ${{ vars.DOCKER_USER }}
|
||||
DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Download Docker image
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
name: postgrest-docker-x64
|
||||
- name: Publish images on Docker Hub
|
||||
run: |
|
||||
docker login -u "$DOCKER_USER" -p "$DOCKER_PASS"
|
||||
docker load -i postgrest-docker.tar.gz
|
||||
|
||||
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
|
||||
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
|
||||
|
||||
# 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 tag postgrest:latest "$DOCKER_REPO"/postgrest:latest
|
||||
docker push "$DOCKER_REPO"/postgrest:latest
|
||||
else
|
||||
echo "Skipping push to 'latest' tag for pre-release..."
|
||||
fi
|
||||
# TODO: Enable dockerhub description update again, once a solution for the permission problem is found:
|
||||
# https://github.com/docker/hub-feedback/issues/1927
|
||||
# - name: Update descriptions on Docker Hub
|
||||
# env:
|
||||
# DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
|
||||
# run: |
|
||||
# if [[ -z "$ISPRERELEASE" ]]; then
|
||||
# echo "Updating description on Docker Hub..."
|
||||
# postgrest-release-dockerhub-description
|
||||
# else
|
||||
# echo "Skipping updating description for pre-release..."
|
||||
# fi
|
||||
|
||||
Release-Docker-Arm:
|
||||
name: Release Arm Builds on Docker Hub
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
- Build-Cabal-Arm
|
||||
- Release-Docker
|
||||
env:
|
||||
GITHUB_COMMIT: ${{ github.sha }}
|
||||
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
|
||||
DOCKER_USER: ${{ vars.DOCKER_USER }}
|
||||
DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Publish images for ARM builds on Docker Hub
|
||||
uses: appleboy/ssh-action@master
|
||||
env:
|
||||
REMOTE_DIR: ${{ needs.Build-Cabal-Arm.outputs.remotepath }}
|
||||
with:
|
||||
host: ${{ secrets.SSH_ARM_HOST }}
|
||||
username: ubuntu
|
||||
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
|
||||
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
|
||||
script_stop: true
|
||||
envs: GITHUB_COMMIT,DOCKER_REPO,DOCKER_USER,DOCKER_PASS,REMOTE_DIR,GITHUB_REF_NAME
|
||||
script: bash ~/$REMOTE_DIR/docker-publish.sh "$GITHUB_COMMIT" "$DOCKER_REPO" "$DOCKER_USER" "$DOCKER_PASS" "$REMOTE_DIR" "$GITHUB_REF_NAME"
|
||||
|
||||
Clean-Arm-Server:
|
||||
name: Remove copied files from server
|
||||
needs:
|
||||
- Build-Cabal-Arm
|
||||
- Release-Docker-Arm
|
||||
if: success() ||
|
||||
needs.Build-Cabal-Arm.result == 'failure' ||
|
||||
needs.Build-Cabal-Arm.result == 'cancelled' ||
|
||||
(needs.Build-Cabal-Arm.result == 'success' && !startsWith(github.ref, 'refs/tags/v'))
|
||||
runs-on: ubuntu-22.04
|
||||
env:
|
||||
REMOTE_DIR: ${{ needs.Build-Cabal-Arm.outputs.remotepath }}
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- name: Remove uploaded files from server
|
||||
uses: appleboy/ssh-action@master
|
||||
with:
|
||||
host: ${{ secrets.SSH_ARM_HOST }}
|
||||
username: ubuntu
|
||||
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
|
||||
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
|
||||
envs: REMOTE_DIR
|
||||
script: rm -rf $REMOTE_DIR
|
||||
+12
-12
@@ -13,38 +13,38 @@ on:
|
||||
jobs:
|
||||
build:
|
||||
name: Build docs
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: cachix/install-nix-action@8887e596b4ee1134dae06b98d573bd674693f47c # v26
|
||||
- run: nix-env -f docs/default.nix -iA build
|
||||
- run: postgrest-docs-build
|
||||
|
||||
spellcheck:
|
||||
name: Run spellcheck
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: cachix/install-nix-action@8887e596b4ee1134dae06b98d573bd674693f47c # v26
|
||||
- run: nix-env -f docs/default.nix -iA spellcheck
|
||||
- run: postgrest-docs-spellcheck
|
||||
|
||||
dictcheck:
|
||||
name: Run dictcheck
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: cachix/install-nix-action@8887e596b4ee1134dae06b98d573bd674693f47c # v26
|
||||
- run: nix-env -f docs/default.nix -iA dictcheck
|
||||
- run: postgrest-docs-dictcheck
|
||||
|
||||
linkcheck:
|
||||
name: Run linkcheck
|
||||
if: github.base_ref == 'main'
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
- uses: cachix/install-nix-action@8887e596b4ee1134dae06b98d573bd674693f47c # v26
|
||||
- run: nix-env -f docs/default.nix -iA linkcheck
|
||||
- run: postgrest-docs-linkcheck
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
name: Loadtest
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- v*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
Loadtest-PR-Nix:
|
||||
name: Loadtest PR (Nix)
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
runs-on: ubuntu-22.04
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
tools: loadtest
|
||||
- uses: WyriHaximus/github-action-get-previous-tag@04e8485ecb6487243907e330d522ff60f02283ce # v1.4.0
|
||||
id: get-latest-tag
|
||||
with:
|
||||
prefix: v
|
||||
- name: Run loadtest
|
||||
run: |
|
||||
postgrest-loadtest-against main ${{ steps.get-latest-tag.outputs.tag }}
|
||||
postgrest-loadtest-report > loadtest/loadtest.md
|
||||
- name: Upload report
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: loadtest.md
|
||||
path: loadtest/loadtest.md
|
||||
if-no-files-found: error
|
||||
|
||||
Loadtest-Merge-Nix:
|
||||
name: Loadtest Merge (Nix)
|
||||
if: ${{ github.event_name == 'push' }}
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: WyriHaximus/github-action-get-previous-tag@04e8485ecb6487243907e330d522ff60f02283ce # v1.4.0
|
||||
id: get-latest-tag
|
||||
with:
|
||||
prefix: v
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
tools: loadtest
|
||||
- name: Run loadtest
|
||||
run: |
|
||||
postgrest-loadtest-against ${{ steps.get-latest-tag.outputs.tag }}
|
||||
postgrest-loadtest-report > loadtest/loadtest.md
|
||||
- name: Upload report
|
||||
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
|
||||
with:
|
||||
name: loadtest.md
|
||||
path: loadtest/loadtest.md
|
||||
if-no-files-found: error
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Upload Reports
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Loadtest"]
|
||||
types:
|
||||
- completed
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
name: Loadtest
|
||||
permissions:
|
||||
checks: write
|
||||
runs-on: ubuntu-22.04
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
steps:
|
||||
- name: Download from Artifacts
|
||||
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
name: loadtest.md
|
||||
path: artifacts
|
||||
- name: Upload to GitHub Checks
|
||||
uses: LouisBrunner/checks-action@6b626ffbad7cc56fd58627f774b9067e6118af23 # v2.0.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
sha: ${{ github.event.workflow_run.head_sha }}
|
||||
name: Loadtest Results
|
||||
conclusion: neutral
|
||||
output: |
|
||||
{"summary":""}
|
||||
output_text_description_file: artifacts/loadtest.md
|
||||
+35
-20
@@ -4,40 +4,40 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
|
||||
## Sponsors
|
||||
|
||||
<table>
|
||||
<table align="center">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest" target="_blank">
|
||||
<img width="222px" src="static/cybertec-new.png">
|
||||
<img width="296px" src="static/cybertec-new.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo" target="_blank">
|
||||
<img width="296px" src="static/2ndquadrant.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/retool.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/gnuhost.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
|
||||
<img width="296px" src="static/supabase.png">
|
||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/neon.jpg">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/code-build.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/oblivious.jpg">
|
||||
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/tembo.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -75,7 +75,22 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.timescale.com?utm_campaign=postgrest&utm_source=sponsor&utm_medium=referral&utm_content=github" target="_blank">
|
||||
<img width="222px" src="static/timescaledb.png">
|
||||
<img width="222px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/timescaledb.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img max-width="222px" height="88" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/retool.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo" target="_blank">
|
||||
<img width="222px" src="static/2ndquadrant.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="222px" src="static/oblivious.jpg">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -5,6 +5,74 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## Unreleased
|
||||
|
||||
## [12.0.3] - 2024-05-09
|
||||
|
||||
### Fixed
|
||||
|
||||
- #3149, Misleading "Starting PostgREST.." logs on schema cache reloading - @steve-chavez
|
||||
- #3205, Fix wrong subquery error returning a status of 400 Bad Request - @steve-chavez
|
||||
- #3224, Return status code 406 for non-accepted media type instead of code 415 - @wolfgangwalther
|
||||
- #3160, Fix using select= query parameter for custom media type handlers - @wolfgangwalther
|
||||
- #3361, Clarify PGRST204(column not found) error message - @steve-chavez
|
||||
- #3373, Remove rejected mediatype `application/vnd.pgrst.object+json` from response - @taimoorzaeem
|
||||
- #3418, Fix OpenAPI not tagging a FK column correctly on O2O relationships - @laurenceisla
|
||||
- #3256, Fix wrong http status for pg error `42P17 infinite recursion` - @taimoorzaeem
|
||||
|
||||
## [12.0.2] - 2023-12-20
|
||||
|
||||
### Fixed
|
||||
|
||||
- #3124, Fix table's media type handlers not working for all schemas - @steve-chavez
|
||||
- #3126, Fix empty row on media type handler function - @steve-chavez
|
||||
|
||||
## [12.0.1] - 2023-12-12
|
||||
|
||||
### Fixed
|
||||
|
||||
- #3054, Fix not allowing special characters in JSON keys - @laurenceisla
|
||||
- #2344, Replace JSON parser error with a clearer generic message - @develop7
|
||||
- #3100, Add missing in-database configuration option for `jwt-cache-max-lifetime` - @laurenceisla
|
||||
- #3089, The any media type handler now sets `Content-Type: application/octet-stream` by default instead of `Content-Type: application/json` - @steve-chavez
|
||||
|
||||
## [12.0.0] - 2023-12-01
|
||||
|
||||
### Added
|
||||
|
||||
- #1614, Add `db-pool-automatic-recovery` configuration to disable connection retrying - @taimoorzaeem
|
||||
- #2492, Allow full response control when raising exceptions - @taimoorzaeem, @laurenceisla
|
||||
- #2771, #2983, #3062, #3055 Add `Server-Timing` response header - @taimoorzaeem, @develop7, @laurenceisla
|
||||
- #2698, Add config `jwt-cache-max-lifetime` and implement JWT caching - @taimoorzaeem
|
||||
- #2943, Add `handling=strict/lenient` for Prefer header - @taimoorzaeem
|
||||
- #2441, Add config `server-cors-allowed-origins` to specify CORS origins - @taimoorzaeem
|
||||
- #2825, SQL handlers for custom media types - @steve-chavez
|
||||
+ Solves #1548, #2699, #2763, #2170, #1462, #1102, #1374, #2901
|
||||
- #2799, Add timezone in Prefer header - @taimoorzaeem
|
||||
- #3001, Add `statement_timeout` set on functions - @taimoorzaeem
|
||||
- #3045, Apply superuser settings on impersonated roles if they have PostgreSQL 15 `GRANT SET ON PARAMETER` privilege - @steve-chavez
|
||||
- #915, Add support for aggregate functions - @timabdulla
|
||||
+ The aggregate functions SUM(), MAX(), MIN(), AVG(), and COUNT() are now supported.
|
||||
+ It's disabled by default, you can enable it with `db-aggregates-enabled`.
|
||||
- #3057, Log all internal database errors to stderr - @laurenceisla
|
||||
|
||||
### Fixed
|
||||
|
||||
- #3015, Fix unnecessary count() on RPC returning single - @steve-chavez
|
||||
- #1070, Fix HTTP status responses for upserts - @taimoorzaeem
|
||||
+ `PUT` returns `201` instead of `200` when rows are inserted
|
||||
+ `POST` with `Prefer: resolution=merge-duplicates` returns `200` instead of `201` when no rows are inserted
|
||||
- #3019, Transaction-Scoped Settings are now shown clearly in the Postgres logs - @laurenceisla
|
||||
+ Shows `set_config('pgrst.setting_name', $1)` instead of `setconfig($1, $2)`
|
||||
+ Does not apply to role settings and `app.settings.*`
|
||||
- #2420, Fix bogus message when listening on port 0 - @develop7
|
||||
- #3067, Fix Acquision Timeout errors logging to stderr when `log-level=crit` - @laurenceisla
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed [raw-media-types config](https://postgrest.org/en/v11.1/references/configuration.html#raw-media-types) - @steve-chavez
|
||||
- Removed `application/octet-stream`, `text/plain`, `text/xml` [builtin support for scalar results](https://postgrest.org/en/v11.1/references/api/resource_representation.html#scalar-function-response-format) - @steve-chavez
|
||||
- Removed default `application/openapi+json` media type for [db-root-spec](https://postgrest.org/en/v11.1/references/configuration.html#db-root-spec) - @steve-chavez
|
||||
- Removed [db-use-legacy-gucs](https://postgrest.org/en/v11.2/references/configuration.html#db-use-legacy-gucs) - @laurenceisla
|
||||
|
||||
## [11.2.2] - 2023-10-25
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||

|
||||
|
||||
[](https://www.patreon.com/postgrest)
|
||||
[](https://www.paypal.me/postgrest)
|
||||
[](https://gitter.im/begriffs/postgrest)
|
||||
[](http://postgrest.org)
|
||||
[](https://hub.docker.com/r/postgrest/postgrest/)
|
||||
[](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)
|
||||
@@ -13,40 +15,40 @@ API than you are likely to write from scratch.
|
||||
|
||||
## Sponsors
|
||||
|
||||
<table>
|
||||
<table align="center">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest" target="_blank">
|
||||
<img width="222px" src="static/cybertec-new.png">
|
||||
<img width="296px" src="static/cybertec-new.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo" target="_blank">
|
||||
<img width="296px" src="static/2ndquadrant.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/retool.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/gnuhost.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
|
||||
<img width="296px" src="static/supabase.png">
|
||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/neon.jpg">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/code-build.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/oblivious.jpg">
|
||||
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/tembo.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -143,7 +145,11 @@ and the [API guide](http://postgrest.org/en/stable/api.html).
|
||||
|
||||
## Supporting development
|
||||
|
||||
You can help PostgREST ongoing maintenance and development by making a regular donation through Patreon https://www.patreon.com/postgrest
|
||||
You can help PostgREST ongoing maintenance and development by:
|
||||
|
||||
- Making a regular donation through Patreon https://www.patreon.com/postgrest
|
||||
|
||||
- Alternatively, you can make a one-time donation via Paypal https://www.paypal.me/postgrest
|
||||
|
||||
Every donation will be spent on making PostgREST better for the whole community.
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
index-state: hackage.haskell.org 2023-10-13T13:54:33Z
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 103 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 34 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
+22
-5
@@ -28,7 +28,11 @@ import os
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = ["sphinx_tabs.tabs", "sphinx_copybutton"]
|
||||
extensions = [
|
||||
"sphinx_tabs.tabs",
|
||||
"sphinx_copybutton",
|
||||
"sphinxext.opengraph",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
@@ -45,7 +49,7 @@ source_suffix = ".rst"
|
||||
master_doc = "index"
|
||||
|
||||
# This is overriden by readthedocs with the version tag anyway
|
||||
version = "11.2"
|
||||
version = "12.0"
|
||||
# To avoid repetition in <title> we set this to an empty string.
|
||||
release = ""
|
||||
|
||||
@@ -59,7 +63,7 @@ copyright = "2017, " + author
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = None
|
||||
language = "en"
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
@@ -70,7 +74,7 @@ language = None
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "shared/*.rst"]
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
@@ -287,7 +291,20 @@ def setup(app):
|
||||
app.add_css_file("css/custom.css")
|
||||
|
||||
|
||||
user_agent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"
|
||||
# taken from https://github.com/sphinx-doc/sphinx/blob/82dad44e5bd3776ecb6fd8ded656bc8151d0e63d/sphinx/util/requests.py#L42
|
||||
user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0"
|
||||
|
||||
# sphinx-tabs configuration
|
||||
sphinx_tabs_disable_tab_closing = True
|
||||
|
||||
# sphinxext-opengraph configuration
|
||||
|
||||
ogp_image = "_images/logo.png"
|
||||
ogp_use_first_image = True
|
||||
ogp_enable_meta_description = True
|
||||
ogp_description_length = 300
|
||||
|
||||
## RTD sets html_baseurl, ensures we use the correct env for canonical URLs
|
||||
## Useful to generate correct meta tags for Open Graph
|
||||
## Refs: https://github.com/readthedocs/readthedocs.org/issues/10226, https://github.com/urllib3/urllib3/pull/3064
|
||||
html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "/")
|
||||
|
||||
+15
-8
@@ -1,9 +1,9 @@
|
||||
let
|
||||
# Commit of the Nixpkgs repository that we want to use.
|
||||
nixpkgsVersion = {
|
||||
date = "2021-06-02";
|
||||
rev = "84aa23742f6c72501f9cc209f29c438766f5352d";
|
||||
tarballHash = "0h7xl6q0yjrbl9vm3h6lkxw692nm8bg3wy65gm95a2mivhrdjpxp";
|
||||
date = "2023-03-25";
|
||||
rev = "dbf5322e93bcc6cfc52268367a8ad21c09d76fea";
|
||||
tarballHash = "0lwk4v9dkvd28xpqch0b0jrac4xl9lwm6snrnzx8k5lby72kmkng";
|
||||
};
|
||||
|
||||
# Nix files that describe the Nixpkgs repository. We evaluate the expression
|
||||
@@ -15,12 +15,9 @@ let
|
||||
})
|
||||
{ };
|
||||
|
||||
sphinxTabsPkg = ps: ps.callPackage ./extensions/sphinx-tabs.nix { };
|
||||
sphinxCopybuttonPkg = ps: ps.callPackage ./extensions/sphinx-copybutton.nix { };
|
||||
|
||||
python = pkgs.python3.withPackages (ps: [ ps.sphinx ps.sphinx_rtd_theme ps.livereload (sphinxTabsPkg ps) (sphinxCopybuttonPkg ps) ]);
|
||||
python = pkgs.python3.withPackages (ps: [ ps.sphinx ps.sphinx_rtd_theme ps.livereload ps.sphinx-tabs ps.sphinx-copybutton ps.sphinxext-opengraph ]);
|
||||
in
|
||||
{
|
||||
rec {
|
||||
inherit pkgs;
|
||||
|
||||
build =
|
||||
@@ -90,4 +87,14 @@ in
|
||||
|
||||
${python}/bin/sphinx-build --color -b linkcheck . _build
|
||||
'';
|
||||
|
||||
check =
|
||||
pkgs.writeShellScriptBin "postgrest-docs-check"
|
||||
''
|
||||
set -euo pipefail
|
||||
${build}/bin/postgrest-docs-build
|
||||
${dictcheck}/bin/postgrest-docs-dictcheck
|
||||
${linkcheck}/bin/postgrest-docs-linkcheck
|
||||
${spellcheck}/bin/postgrest-docs-spellcheck
|
||||
'';
|
||||
}
|
||||
|
||||
+3
-3
@@ -6,7 +6,7 @@ Community Tutorials
|
||||
* `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ -
|
||||
In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.
|
||||
|
||||
* `PostgREST + Auth0: Create REST API in minutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest-1-introduction/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
|
||||
* `PostgREST + Auth0: Create REST API in mintutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
|
||||
|
||||
* `PostgREST + PostGIS API tutorial in 5 minutes <https://gis-ops.com/postgrest-postgis-api-tutorial-geospatial-api-in-5-minutes/>`_ -
|
||||
In this tutorial, GIS • OPS shows how to perform PostGIS calculations through PostgREST :ref:`s_procs` interface.
|
||||
@@ -20,7 +20,7 @@ Community Tutorials
|
||||
|
||||
* `Build data-driven applications with Workers and PostgreSQL <https://developers.cloudflare.com/workers/tutorials/postgres/>`_ - A tutorial on how to integrate with PostgREST and PostgreSQL using Cloudflare Workers.
|
||||
|
||||
* `A poor man's API <https://blog.frankel.ch/poor-man-api>`_ - Shows how to integrate PostgREST with Apache APISIX as an alternative to Nginx.
|
||||
.. * `A poor man's API <https://blog.frankel.ch/poor-man-api>`_ - Shows how to integrate PostgREST with Apache APISIX as an alternative to Nginx.
|
||||
|
||||
.. _templates:
|
||||
|
||||
@@ -87,7 +87,7 @@ Client-Side Libraries
|
||||
* `postgrest-go <https://github.com/supabase-community/postgrest-go>`_ - Go
|
||||
* `postgrest-js <https://github.com/supabase/postgrest-js>`_ - TypeScript/JavaScript
|
||||
* `postgrest-kt <https://github.com/supabase-community/postgrest-kt>`_ - Kotlin
|
||||
* `postgrest-py <https://github.com/supabase/postgrest-py>`_ - Python
|
||||
* `postgrest-py <https://github.com/supabase-community/postgrest-py>`_ - Python
|
||||
* `postgrest-rs <https://github.com/supabase-community/postgrest-rs>`_ - Rust
|
||||
* `postgrest-swift <https://github.com/supabase-community/postgrest-swift>`_ - Swift
|
||||
* `redux-postgrest <https://github.com/andytango/redux-postgrest>`_ - TypeScript/JS, client integrated with (React) Redux.
|
||||
|
||||
@@ -15,7 +15,7 @@ Roles for Each Web User
|
||||
|
||||
PostgREST can accommodate either viewpoint. If you treat a role as a single user then the :ref:`jwt_impersonation` does most of what you need. When an authenticated user makes a request PostgREST will switch into the database role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable.
|
||||
|
||||
You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example <https://www.enterprisedb.com:443/blog/application-users-vs-row-level-security>`_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users.
|
||||
You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example <https://www.2ndquadrant.com/en/blog/application-users-vs-row-level-security/>`_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
@@ -92,19 +92,10 @@ You can mix the group and individual role policies. For instance we could still
|
||||
-- allow authenticator to switch into user000 role
|
||||
-- (the role itself has nologin)
|
||||
|
||||
.. _schema_isolation:
|
||||
|
||||
Schemas
|
||||
=======
|
||||
|
||||
A PostgREST instance exposes all the tables, views, and stored procedures of the schemas configured in :ref:`db-schemas`. This means private data or implementation details can go inside private schemas and be invisible to HTTP clients.
|
||||
|
||||
It is recommended that you don't expose tables on the schemas you expose, instead expose views and stored procedures which insulate the internal details from the outside world.
|
||||
This allows you to change the internals of your schema and maintain backwards compatibility. It also keeps your code easier to refactor, and provides a natural way to do API versioning.
|
||||
|
||||
.. image:: ../_static/db.png
|
||||
|
||||
You must explicitly allow roles to access the exposed schemas:
|
||||
You must explicitly allow roles to access the exposed schemas in :ref:`db-schemas`.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
|
||||
@@ -3,58 +3,12 @@
|
||||
Installation
|
||||
############
|
||||
|
||||
The release page has `pre-compiled binaries for Mac OS X, Windows, Linux and FreeBSD <https://github.com/PostgREST/postgrest/releases/latest>`_ .
|
||||
The release page has `pre-compiled binaries for macOS, Windows, Linux and FreeBSD <https://github.com/PostgREST/postgrest/releases/latest>`_ .
|
||||
The Linux binary is a static executable that can be run on any Linux distribution.
|
||||
|
||||
You can also use your OS package manager.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: Mac OSX
|
||||
|
||||
You can install PostgREST from the `Homebrew official repo <https://formulae.brew.sh/formula/postgrest>`_.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
brew install postgrest
|
||||
|
||||
.. group-tab:: FreeBSD
|
||||
|
||||
You can install PostgREST from the `official ports <https://www.freshports.org/www/hs-postgrest>`_.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pkg install hs-postgrest
|
||||
|
||||
.. group-tab:: Linux
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. tab:: Arch Linux
|
||||
|
||||
You can install PostgREST from the `community repo <https://archlinux.org/packages/extra/x86_64/postgrest/>`_.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pacman -S postgrest
|
||||
|
||||
.. tab:: Nix
|
||||
|
||||
You can install PostgREST from nixpkgs.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
nix-env -i haskellPackages.postgrest
|
||||
|
||||
.. group-tab:: Windows
|
||||
|
||||
You can install PostgREST using `Chocolatey <https://community.chocolatey.org/packages/postgrest>`_ or `Scoop <https://github.com/ScoopInstaller/Scoop>`_.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
choco install postgrest
|
||||
scoop install postgrest
|
||||
|
||||
.. include:: ../shared/installation.rst
|
||||
|
||||
.. _pg-dependency:
|
||||
|
||||
@@ -233,7 +187,7 @@ When a pre-built binary does not exist for your system you can build the project
|
||||
|
||||
You can build PostgREST from source with `Stack <https://github.com/commercialhaskell/stack>`_. It will install any necessary Haskell dependencies on your system.
|
||||
|
||||
* `Install Stack <https://docs.haskellstack.org/en/stable/#how-to-install-stack>`_ for your platform
|
||||
* `Install Stack <https://docs.haskellstack.org/en/stable/README/#how-to-install-stack>`_ for your platform
|
||||
* Install Library Dependencies
|
||||
|
||||
===================== =======================================
|
||||
@@ -242,7 +196,7 @@ You can build PostgREST from source with `Stack <https://github.com/commercialha
|
||||
Ubuntu/Debian libpq-dev, libgmp-dev, zlib1g-dev
|
||||
CentOS/Fedora/Red Hat postgresql-devel, zlib-devel, gmp-devel
|
||||
BSD postgresql12-client
|
||||
OS X libpq, gmp
|
||||
macOS libpq, gmp
|
||||
===================== =======================================
|
||||
|
||||
* Build and install binary
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
.. note::
|
||||
|
||||
This page is a work in progress.
|
||||
|
||||
.. _schema_isolation:
|
||||
|
||||
Schema Isolation
|
||||
================
|
||||
|
||||
A PostgREST instance exposes all the tables, views, and stored procedures of a single `PostgreSQL schema <https://www.postgresql.org/docs/current/ddl-schemas.html>`_ (a namespace of database objects). This means private data or implementation details can go inside different private schemas and be invisible to HTTP clients.
|
||||
|
||||
It is recommended that you don't expose tables on your API schema. Instead expose views and stored procedures which insulate the internal details from the outside world.
|
||||
This allows you to change the internals of your schema and maintain backwards compatibility. It also keeps your code easier to refactor, and provides a natural way to do API versioning.
|
||||
|
||||
.. image:: ../_static/db.png
|
||||
@@ -1,33 +0,0 @@
|
||||
{ lib
|
||||
, buildPythonPackage
|
||||
, fetchFromGitHub
|
||||
, sphinx
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "sphinx-copybutton";
|
||||
version = "0.4.0";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "executablebooks";
|
||||
repo = "sphinx-copybutton";
|
||||
rev = "v${version}";
|
||||
sha256 = "sha256-vrEIvQeP7AMXSme1PBp0ox5k8Q1rz+1cbHIO+o17Jqc=";
|
||||
fetchSubmodules = true;
|
||||
};
|
||||
|
||||
propagatedBuildInputs = [
|
||||
sphinx
|
||||
];
|
||||
|
||||
doCheck = false; # no tests
|
||||
|
||||
pythonImportsCheck = [ "sphinx_copybutton" ];
|
||||
|
||||
meta = with lib; {
|
||||
description = "A small sphinx extension to add a \"copy\" button to code blocks";
|
||||
homepage = "https://github.com/executablebooks/sphinx-copybutton";
|
||||
license = licenses.mit;
|
||||
maintainers = with maintainers; [ Luflosi ];
|
||||
};
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{ lib
|
||||
, buildPythonPackage
|
||||
, fetchPypi
|
||||
, sphinx
|
||||
}:
|
||||
|
||||
buildPythonPackage rec {
|
||||
pname = "sphinx-tabs";
|
||||
version = "3.2.0";
|
||||
|
||||
src = fetchPypi {
|
||||
inherit pname version;
|
||||
sha256 = "sha256:1970aahi6sa7c37cpz8nwgdb2xzf21rk6ykdd1m6w9wvxla7j4rk";
|
||||
};
|
||||
|
||||
propagatedBuildInputs = [
|
||||
sphinx
|
||||
];
|
||||
|
||||
doCheck = false;
|
||||
|
||||
pythonImportsCheck = [ "sphinx_tabs" ];
|
||||
|
||||
meta = with lib; {
|
||||
description = "Create tabbed content in Sphinx documentation when building HTML";
|
||||
homepage = "https://sphinx-tabs.readthedocs.io";
|
||||
license = licenses.mit;
|
||||
};
|
||||
}
|
||||
@@ -5,22 +5,20 @@ Create a SOAP endpoint
|
||||
|
||||
:author: `fjf2002 <https://github.com/fjf2002>`_
|
||||
|
||||
PostgREST now has XML support. With a bit of work, SOAP endpoints become possible.
|
||||
|
||||
Please note that PostgREST supports just ``text/xml`` MIME type in request/response headers ``Content-Type`` and ``Accept``.
|
||||
If you have to use other MIME types such as ``application/soap+xml``, you could manipulate the headers in your reverse proxy.
|
||||
|
||||
|
||||
PostgREST supports :ref:`custom_media`. With a bit of work, SOAP endpoints become possible.
|
||||
|
||||
Minimal Example
|
||||
---------------
|
||||
|
||||
This example will simply return the request body, inside a tag ``therequestbodywas``.
|
||||
|
||||
Add the following function to your PostgreSQL database:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE OR REPLACE FUNCTION my_soap_endpoint(xml) RETURNS xml AS $$
|
||||
create domain "text/xml" as pg_catalog.xml;
|
||||
|
||||
CREATE OR REPLACE FUNCTION my_soap_endpoint(xml) RETURNS "text/xml" AS $$
|
||||
DECLARE
|
||||
nsarray CONSTANT text[][] := ARRAY[
|
||||
ARRAY['soapenv', 'http://schemas.xmlsoap.org/soap/envelope/']
|
||||
@@ -79,25 +77,6 @@ and should roughly look like:
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
|
||||
Unfortunately the ``Accept: text/xml`` header is currently mandatory concerning PostgREST, otherwise it will respond
|
||||
with a ``Content-Type: application/json`` header and enclose the response with quotes.
|
||||
(You can check the returned headers by adding ``-v`` to the curl call.)
|
||||
|
||||
If your SOAP clients do not send the ``Accept: text/xml`` header, you can fix that in your nginx reverse proxy
|
||||
by adding something like ...
|
||||
|
||||
.. code-block:: nginx
|
||||
|
||||
set $accept $http_accept;
|
||||
if ($contentType ~ "^text/xml($|;)") {
|
||||
set $accept "text/xml";
|
||||
}
|
||||
proxy_set_header Accept $accept;
|
||||
|
||||
to your ``location`` nginx configuration.
|
||||
(The given example sets the ``Accept`` header for each request of Content-Type ``text/xml``.)
|
||||
|
||||
|
||||
A more elaborate example
|
||||
------------------------
|
||||
|
||||
@@ -121,7 +100,7 @@ potentially disclosing internals to the client, but instead handle the errors di
|
||||
xmlelement(NAME "soapenv:Body", body)
|
||||
);
|
||||
$function$;
|
||||
|
||||
|
||||
-- helper function
|
||||
CREATE OR REPLACE FUNCTION _soap_exception(
|
||||
faultcode text,
|
||||
@@ -137,9 +116,9 @@ potentially disclosing internals to the client, but instead handle the errors di
|
||||
)
|
||||
);
|
||||
$function$;
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION fraction_to_decimal(xml)
|
||||
RETURNS xml
|
||||
RETURNS "text/xml"
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
@@ -207,14 +186,14 @@ The output should roughly look like:
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
|
||||
|
||||
References
|
||||
----------
|
||||
|
||||
For more information concerning PostgREST, cf.
|
||||
|
||||
- :ref:`s_proc_single_unnamed`
|
||||
- :ref:`scalar_return_formats`
|
||||
- :ref:`Nginx reverse proxy <admin>`
|
||||
- :ref:`custom_media`. See :ref:`any_handler`, if you need to support an ``application/soap+xml`` media type or if you want to respond with XML without sending a media type.
|
||||
- :ref:`Nginx reverse proxy <nginx>`
|
||||
|
||||
For SOAP reference, visit
|
||||
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
|
||||
.. _providing_html_htmx:
|
||||
|
||||
Providing HTML Content Using Htmx
|
||||
=================================
|
||||
|
||||
:author: `Laurence Isla <https://github.com/laurenceisla>`_
|
||||
|
||||
This how-to shows a way to return HTML content and use the `htmx library <https://htmx.org/>`_ to handle the AJAX requests.
|
||||
Htmx expects an HTML response and uses it to replace an element inside the DOM (see the `htmx introduction <https://htmx.org/docs/#introduction>`_ in the docs).
|
||||
|
||||
.. image:: ../_static/how-tos/htmx-demo.gif
|
||||
|
||||
.. warning::
|
||||
|
||||
This is a proof of concept showing what can be achieved using both technologies.
|
||||
We are working on `plmustache <https://github.com/PostgREST/plmustache>`_ which will further improve the HTML aspect of this how-to.
|
||||
|
||||
Preparatory Configuration
|
||||
-------------------------
|
||||
|
||||
We will make a to-do app based on the :ref:`tut0`, so make sure to complete it before continuing.
|
||||
|
||||
To simplify things, we won't be using authentication, so grant all permissions on the ``todos`` table to the ``web_anon`` user.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
grant all on api.todos to web_anon;
|
||||
grant usage, select on sequence api.todos_id_seq to web_anon;
|
||||
|
||||
Next, add the ``text/html`` as a :ref:`custom_media`. With this, PostgREST can identify the request made by your web browser (with the ``Accept: text/html`` header)
|
||||
and return a raw HTML document file.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "text/html" as text;
|
||||
|
||||
Creating an HTML Response
|
||||
-------------------------
|
||||
|
||||
Let's create a function that returns a basic HTML file, using `Tailwind CSS <https://v2.tailwindcss.com/>`_ for styling.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function api.index() returns "text/html" as $$
|
||||
select $html$
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PostgREST + HTMX To-Do List</title>
|
||||
<!-- Tailwind for CSS styling -->
|
||||
<link href="https://unpkg.com/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="bg-gray-900">
|
||||
<div class="flex justify-center">
|
||||
<div class="max-w-lg mt-5 p-6 bg-gray-800 border border-gray-800 rounded-lg shadow-xl">
|
||||
<h5 class="mb-3 text-2xl font-bold tracking-tight text-white">PostgREST + HTMX To-Do List</h5>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
$html$;
|
||||
$$ language sql;
|
||||
|
||||
The web browser will open the web page at ``http://localhost:3000/rpc/index``.
|
||||
|
||||
.. image:: ../_static/how-tos/htmx-simple.jpg
|
||||
|
||||
.. _html_htmx_list_create:
|
||||
|
||||
Listing and Creating To-Dos
|
||||
---------------------------
|
||||
|
||||
Now, let's show a list of the to-dos already inserted in the database.
|
||||
For that, we'll also need a function to help us sanitize the HTML content that may be present in the task.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function api.sanitize_html(text) returns text as $$
|
||||
select replace(replace(replace(replace(replace($1, '&', '&'), '"', '"'),'>', '>'),'<', '<'), '''', ''')
|
||||
$$ language sql;
|
||||
|
||||
create or replace function api.html_todo(api.todos) returns text as $$
|
||||
select format($html$
|
||||
<li class="py-3">
|
||||
<span class="ml-2 %2$s">
|
||||
%3$s
|
||||
</span>
|
||||
</li>
|
||||
$html$,
|
||||
$1.id,
|
||||
case when $1.done then 'line-through text-gray-400' else '' end,
|
||||
api.sanitize_html($1.task)
|
||||
);
|
||||
$$ language sql stable;
|
||||
|
||||
create or replace function api.html_all_todos() returns text as $$
|
||||
select coalesce(
|
||||
'<ul id="todo-list" role="list" class="divide-y divide-gray-700 text-gray-100">'
|
||||
|| string_agg(api.html_todo(t), '' order by t.id) ||
|
||||
'</ul>',
|
||||
'<p class="text-gray-100">There is nothing else to do.</p>'
|
||||
)
|
||||
from api.todos t;
|
||||
$$ language sql;
|
||||
|
||||
These two functions are used to build the to-do list template. We won't use them as PostgREST endpoints.
|
||||
|
||||
- The ``api.html_todo`` function uses the table ``api.todos`` as a parameter and formats each item into a list element ``<li>``.
|
||||
The PostgreSQL `format <https://www.postgresql.org/docs/current/functions-string.html#FUNCTIONS-STRING-FORMAT>`_ is useful to that end.
|
||||
It replaces the values according to the position in the template, e.g. ``%1$s`` will be replaced with the value of ``$1.id`` (the first parameter).
|
||||
|
||||
- The ``api.html_all_todos`` function returns the ``<ul>`` wrapper for all the list elements.
|
||||
It uses `string_arg <https://www.postgresql.org/docs/current/functions-aggregate.html>`_ to concatenate all the to-dos in a single text value.
|
||||
It also returns an alternative message, instead of a list, when the ``api.todos`` table is empty.
|
||||
|
||||
Next, let's add an endpoint to register a to-do in the database and modify the ``/rpc/index`` page accordingly.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function api.add_todo(_task text) returns "text/html" as $$
|
||||
insert into api.todos(task) values (_task);
|
||||
select api.html_all_todos();
|
||||
$$ language sql;
|
||||
|
||||
create or replace function api.index() returns "text/html" as $$
|
||||
select $html$
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PostgREST + HTMX To-Do List</title>
|
||||
<!-- Tailwind for CSS styling -->
|
||||
<link href="https://unpkg.com/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
<!-- htmx for AJAX requests -->
|
||||
<script src="https://unpkg.com/htmx.org"></script>
|
||||
</head>
|
||||
<body class="bg-gray-900"
|
||||
hx-headers='{"Accept": "text/html"}'>
|
||||
<div class="flex justify-center">
|
||||
<div class="max-w-lg mt-5 p-6 bg-gray-800 border border-gray-800 rounded-lg shadow-xl">
|
||||
<h5 class="mb-3 text-2xl font-bold tracking-tight text-white">PostgREST + HTMX To-Do List</h5>
|
||||
<form hx-post="/rpc/add_todo"
|
||||
hx-target="#todo-list-area"
|
||||
hx-trigger="submit"
|
||||
hx-on="htmx:afterRequest: this.reset()">
|
||||
<input class="bg-gray-50 border text-sm rounded-lg block w-full p-2.5 mb-3 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
|
||||
type="text" name="_task" placeholder="Add a todo...">
|
||||
</form>
|
||||
<div id="todo-list-area">
|
||||
$html$
|
||||
|| api.html_all_todos() ||
|
||||
$html$
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
$html$;
|
||||
$$ language sql;
|
||||
|
||||
- The ``/rpc/add_todo`` endpoint allows us to add a new to-do using the ``_task`` parameter and returns an ``html`` with all the to-dos in the database.
|
||||
|
||||
- The ``/rpc/index`` now adds the ``hx-headers='{"Accept": "text/html"}'`` tag to the ``<body>``.
|
||||
This will make sure that all htmx elements inside the body send this header, otherwise PostgREST won't recognize it as HTML.
|
||||
|
||||
There is also a ``<form>`` element that uses the htmx library. Let's break it down:
|
||||
|
||||
+ ``hx-post="/rpc/add_todo"``: sends an AJAX POST request to the ``/rpc/add_todo`` endpoint, with the value of the ``_task`` from the ``<input>`` element.
|
||||
|
||||
+ ``hx-target="#todo-list-area"``: the HTML content returned from the request will go inside ``<div id="todo-list-area"></div>`` (which is the list of to-dos).
|
||||
|
||||
+ ``hx-trigger="submit"``: htmx will do this request when submitting the form (by pressing enter while inside the ``<input>``).
|
||||
|
||||
+ ``hx-on="htmx:afterRequest: this.reset()">``: this is a Javascript command that clears the form `after the request is done <https://htmx.org/events/#htmx:afterRequest>`_.
|
||||
|
||||
With this, the ``http://localhost:3000/rpc/index`` page lists all the todos and adds new ones by submitting tasks in the input element.
|
||||
Don't forget to refresh the :ref:`schema cache <schema_reloading>`.
|
||||
|
||||
.. image:: ../_static/how-tos/htmx-insert.gif
|
||||
|
||||
Editing and Deleting To-Dos
|
||||
---------------------------
|
||||
|
||||
Now, let's modify ``api.html_todo`` and make it more functional.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function api.html_todo(api.todos) returns text as $$
|
||||
select format($html$
|
||||
<li class="py-3">
|
||||
<div class="flex justify-between items-center">
|
||||
<div id="todo-edit-area-%1$s" class="pr-5">
|
||||
<form id="edit-task-state-%1$s"
|
||||
hx-post="/rpc/change_todo_state"
|
||||
hx-vals='{"_id": %1$s, "_done": %4$s}'
|
||||
hx-target="#todo-list-area"
|
||||
hx-trigger="click">
|
||||
<span class="ml-2 %2$s cursor-pointer">
|
||||
%3$s
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<button class="p-1.5 rounded-full hover:bg-gray-700 focus:ring-gray-800"
|
||||
hx-get="/rpc/html_editable_task"
|
||||
hx-vals='{"_id": "%1$s"}'
|
||||
hx-target="#todo-edit-area-%1$s"
|
||||
hx-trigger="click">
|
||||
<svg class="w-4 h-4 text-blue-300" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 18">
|
||||
<path d="M12.687 14.408a3.01 3.01 0 0 1-1.533.821l-3.566.713a3 3 0 0 1-3.53-3.53l.713-3.566a3.01 3.01 0 0 1 .821-1.533L10.905 2H2.167A2.169 2.169 0 0 0 0 4.167v11.666A2.169 2.169 0 0 0 2.167 18h11.666A2.169 2.169 0 0 0 16 15.833V11.1l-3.313 3.308Zm5.53-9.065.546-.546a2.518 2.518 0 0 0 0-3.56 2.576 2.576 0 0 0-3.559 0l-.547.547 3.56 3.56Z"/>
|
||||
<path d="M13.243 3.2 7.359 9.081a.5.5 0 0 0-.136.256L6.51 12.9a.5.5 0 0 0 .59.59l3.566-.713a.5.5 0 0 0 .255-.136L16.8 6.757 13.243 3.2Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="p-1.5 rounded-full hover:bg-gray-700 focus:ring-gray-800"
|
||||
hx-post="/rpc/delete_todo"
|
||||
hx-vals='{"_id": %1$s}'
|
||||
hx-target="#todo-list-area"
|
||||
hx-trigger="click">
|
||||
<svg class="w-4 h-4 text-red-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h16M7 8v8m4-8v8M7 1h4a1 1 0 0 1 1 1v3H6V2a1 1 0 0 1 1-1ZM3 5h12v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5Z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
$html$,
|
||||
$1.id,
|
||||
case when $1.done then 'line-through text-gray-400' else '' end,
|
||||
api.sanitize_html($1.task),
|
||||
(not $1.done)::text
|
||||
);
|
||||
$$ language sql stable;
|
||||
|
||||
Let's deconstruct the new htmx features added:
|
||||
|
||||
- The ``<form>`` element is configured as follows:
|
||||
|
||||
+ ``hx-post="/rpc/change_todo_state"``: does an AJAX POST request to that endpoint. It will toggle the ``done`` state of the to-do.
|
||||
|
||||
+ ``hx-vals='{"_id": %1$s, "_done": %4$s}'``: adds the parameters to the request.
|
||||
This is an alternative to using hidden inputs inside the ``<form>``.
|
||||
|
||||
+ ``hx-trigger="click"``: htmx does the request after clicking on the element.
|
||||
|
||||
- For the first ``<button>``:
|
||||
|
||||
+ ``hx-get="/rpc/html_editable_task"``: it does an AJAX GET request to that endpoint.
|
||||
It returns an HTML with an input that will allow us to edit the task.
|
||||
|
||||
+ ``hx-target="#todo-edit-area"``: the returned HTML will replace the element with this id.
|
||||
In this case, this replaces an individual task, not the whole list.
|
||||
|
||||
+ ``hx-vals='{"id": "eq.%1$s"}'``: adds the query parameters to the GET request.
|
||||
Note that this needs the ``eq.`` operator because it represents a table column not a function parameter.
|
||||
|
||||
- For the second ``<button>``:
|
||||
|
||||
+ ``hx-post="/rpc/delete_todo"``: this post request will delete the corresponding to-do.
|
||||
|
||||
Clicking on the first button will enable the task editing.
|
||||
That's why we create the ``api.html_editable_task`` function as an endpoint:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function api.html_editable_task(_id int) returns "text/html" as $$
|
||||
select format ($html$
|
||||
<form id="edit-task-%1$s"
|
||||
hx-post="/rpc/change_todo_task"
|
||||
hx-headers='{"Accept": "text/html"}'
|
||||
hx-vals='{"_id": %1$s}'
|
||||
hx-target="#todo-list-area"
|
||||
hx-trigger="submit,focusout">
|
||||
<input class="bg-gray-50 border text-sm rounded-lg block w-full p-2.5 bg-gray-700 border-gray-600 text-white focus:ring-blue-500 focus:border-blue-500"
|
||||
id="task-%1$s" type="text" name="_task" value="%2$s" autofocus>
|
||||
</form>
|
||||
$html$,
|
||||
id,
|
||||
api.sanitize_html(task)
|
||||
)
|
||||
from api.todos
|
||||
where id = _id;
|
||||
$$ language sql;
|
||||
|
||||
In this example, this will return an input field that allows us to edit the corresponding to-do task.
|
||||
|
||||
Finally, let's add the endpoints that will modify and delete the to-dos in the database.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function api.change_todo_state(_id int, _done boolean) returns "text/html" as $$
|
||||
update api.todos set done = _done where id = _id;
|
||||
select api.html_all_todos();
|
||||
$$ language sql;
|
||||
|
||||
create or replace function api.change_todo_task(_id int, _task text) returns "text/html" as $$
|
||||
update api.todos set task = _task where id = _id;
|
||||
select api.html_all_todos();
|
||||
$$ language sql;
|
||||
|
||||
create or replace function api.delete_todo(_id int) returns "text/html" as $$
|
||||
delete from api.todos where id = _id;
|
||||
select api.html_all_todos();
|
||||
$$ language sql;
|
||||
|
||||
All of those functions return an HTML list of to-dos that will replace the outdated one:
|
||||
|
||||
- The ``api.change_todo_state`` function updates the ``done`` column using the ``_id`` and the ``_done`` values from the request.
|
||||
|
||||
- The ``api.delete_todo`` function deletes a to-do using the ``_id`` value from the request.
|
||||
|
||||
- The ``api.change_todo_task`` function modifies the ``task`` column using the ``_id`` and the ``_task`` value from the request.
|
||||
|
||||
After refreshing the :ref:`schema cache <schema_reloading>`, the page at ``http://localhost:3000/rpc/index`` will allow us to edit, delete and complete any to-do.
|
||||
|
||||
.. image:: ../_static/how-tos/htmx-edit-delete.gif
|
||||
|
||||
With that, we completed the to-do list functionality.
|
||||
@@ -26,18 +26,42 @@ First, we need a public table for storing the files.
|
||||
, blob bytea
|
||||
);
|
||||
|
||||
Let's assume this table contains an image of two cute kittens with id 42.
|
||||
We can retrieve this image in binary format from our PostgREST API by requesting :code:`/files?select=blob&id=eq.42` with the :code:`Accept: application/octet-stream` header.
|
||||
Unfortunately, putting the URL into the :code:`src` of an :code:`<img>` tag will not work.
|
||||
That's because browsers do not send the required :code:`Accept: application/octet-stream` header.
|
||||
Let's assume this table contains an image of two cute kittens with id 42. We can retrieve this image in binary format from our PostgREST API by using :ref:`custom_media`:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "application/octet-stream" as bytea;
|
||||
|
||||
create or replace function file(id int) returns "application/octet-stream" as $$
|
||||
select blob from files where id = file.id;
|
||||
$$ language sql;
|
||||
|
||||
Now we can request the RPC endpoint :code:`/rpc/file?id=42` with the :code:`Accept: application/octet-stream` header.
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "localhost:3000/rpc/file?id=42" -H "Accept: application/octet-stream"
|
||||
|
||||
|
||||
Unfortunately, putting the URL into the :code:`src` of an :code:`<img>` tag will not work. That's because browsers do not send the required :code:`Accept: application/octet-stream` header.
|
||||
Instead, the :code:`Accept: image/webp` header is sent by many web browsers by default.
|
||||
|
||||
Luckily we can change the accepted media type in the function like so:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "image/webp" as bytea;
|
||||
|
||||
create or replace function file(id int) returns "image/webp" as $$
|
||||
select blob from files where id = file.id;
|
||||
$$ language sql;
|
||||
|
||||
Luckily we can specify the accepted media types in the :ref:`raw-media-types` configuration variable.
|
||||
In this case, the :code:`Accept: image/webp` header is sent by many web browsers by default, so let's add it to the configuration variable, like this: :code:`raw-media-types="image/webp"`.
|
||||
Now, the image will be displayed in the HTML page:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<img src="http://localhost:3000/files?select=blob&id=eq.42" alt="Cute Kittens"/>
|
||||
<img src="http://localhost:3000/file?id=42" alt="Cute Kittens"/>
|
||||
|
||||
Improved Version
|
||||
----------------
|
||||
@@ -60,13 +84,15 @@ First, in addition to the minimal example, we need to store the media types and
|
||||
add column type text,
|
||||
add column name text;
|
||||
|
||||
Next, we set up an RPC endpoint that sets the content type and filename.
|
||||
Next, we set modify the function to set the content type and filename.
|
||||
We use this opportunity to configure some basic, client-side caching.
|
||||
For production, you probably want to configure additional caches, e.g. on the :ref:`reverse proxy <admin>`.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create function file(id int) returns bytea as
|
||||
create domain "*/*" as bytea;
|
||||
|
||||
create function file(id int) returns "*/*" as
|
||||
$$
|
||||
declare headers text;
|
||||
declare blob bytea;
|
||||
@@ -79,7 +105,7 @@ For production, you probably want to configure additional caches, e.g. on the :r
|
||||
from files where files.id = file.id into headers;
|
||||
perform set_config('response.headers', headers, true);
|
||||
select files.blob from files where files.id = file.id into blob;
|
||||
if found
|
||||
if FOUND -- special var, see https://www.postgresql.org/docs/current/plpgsql-statements.html#PLPGSQL-STATEMENTS-DIAGNOSTICS
|
||||
then return(blob);
|
||||
else raise sqlstate 'PT404' using
|
||||
message = 'NOT FOUND',
|
||||
|
||||
@@ -62,7 +62,7 @@ Someone located in Cairo can retrieve the data using their local time, too:
|
||||
}
|
||||
]
|
||||
|
||||
The response has the date in the time zone configured by the server: ``UTC -05:00``.
|
||||
The response has the date in the time zone configured by the server: ``UTC -05:00`` (see :ref:`prefer_timezone`).
|
||||
|
||||
You can use other comparative filters and also all the `PostgreSQL special date/time input values <https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-DATETIME-SPECIAL-TABLE>`_ as illustrated in this example:
|
||||
|
||||
@@ -506,33 +506,26 @@ Now, to send the file ``postgrest-logo.png`` we need to set the ``Content-Type:
|
||||
-X POST -H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@postgrest-logo.png"
|
||||
|
||||
To get the image from the database, set the ``Accept: application/octet-stream`` header and select only the
|
||||
``bytea`` type column.
|
||||
To get the image from the database, use :ref:`custom_media` like so:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "image/png" as bytea;
|
||||
|
||||
create or replace get_image(id int) returns "image/png" as $$
|
||||
select file from files where id = $1;
|
||||
$$ language sql;
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /files?select=file&id=eq.1 HTTP/1.1
|
||||
Accept: application/octet-stream
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/files?select=file&id=eq.1" \
|
||||
-H "Accept: application/octet-stream"
|
||||
|
||||
Use more accurate headers according to the type of the files by using the :ref:`raw-media-types` configuration. For example, adding the ``raw-media-types="image/png"`` setting to the configuration file will allow you to use the ``Accept: image/png`` header:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /files?select=file&id=eq.1 HTTP/1.1
|
||||
GET /get_image?id=1 HTTP/1.1
|
||||
Accept: image/png
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/files?select=file&id=eq.1" \
|
||||
curl "http://localhost:3000/get_image?id=1" \
|
||||
-H "Accept: image/png"
|
||||
|
||||
See :ref:`providing_img` for a step-by-step example on how to handle images in HTML.
|
||||
|
||||
+28
-10
@@ -16,9 +16,15 @@ PostgREST Documentation
|
||||
.. image:: https://img.shields.io/docker/pulls/postgrest/postgrest.svg
|
||||
:target: https://hub.docker.com/r/postgrest/postgrest/
|
||||
|
||||
.. image:: https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg
|
||||
:target: https://gitter.im/begriffs/postgrest
|
||||
|
||||
.. image:: https://img.shields.io/badge/Donate-Patreon-orange.svg?colorB=F96854
|
||||
:target: https://www.patreon.com/postgrest
|
||||
|
||||
.. image:: https://img.shields.io/badge/Donate-PayPal-green.svg
|
||||
:target: https://www.paypal.com/paypalme/postgrest
|
||||
|
||||
|
|
||||
|
||||
PostgREST is a standalone web server that turns your PostgreSQL database directly into a RESTful API. The structural constraints and permissions in the database determine the API endpoints and operations.
|
||||
@@ -33,17 +39,25 @@ Sponsors
|
||||
:width: 13em
|
||||
|
||||
.. image:: _static/gnuhost.png
|
||||
:target: https://euronodes.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: _static/neon.jpg
|
||||
:target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
|
|
||||
|
||||
.. image:: _static/code-build.webp
|
||||
:target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: _static/supabase.png
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
:width: 13em
|
||||
|
||||
.. image:: _static/neon.jpg
|
||||
:target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest
|
||||
.. image:: _static/tembo.png
|
||||
:target: https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
|
||||
@@ -78,15 +92,12 @@ PostgREST has a focused scope. It works well with other tools like Nginx. This f
|
||||
Getting Support
|
||||
----------------
|
||||
|
||||
The project has a friendly and growing community. For discussions, use the Github `discussions page <https://github.com/PostgREST/postgrest/discussions>`_. You can also report or search for bugs/features on the Github `issues <https://github.com/PostgREST/postgrest/issues>`_ page.
|
||||
The project has a friendly and growing community. For discussions, use the Github `discussions page <https://github.com/PostgREST/postgrest/discussions>`_ or join our `chat room <https://gitter.im/begriffs/postgrest>`_. You can also report or search for bugs/features on the Github `issues <https://github.com/PostgREST/postgrest/issues>`_ page.
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
:caption: Release Notes
|
||||
:reversed:
|
||||
:maxdepth: 1
|
||||
Release Notes
|
||||
-------------
|
||||
|
||||
releases/*
|
||||
The release notes are published on `PostgREST's GitHub release page <https://github.com/PostgREST/postgrest/releases>`_.
|
||||
|
||||
Tutorials
|
||||
---------
|
||||
@@ -184,12 +195,19 @@ Here are some companies that use PostgREST in production.
|
||||
* `Datrium <https://www.datrium.com>`_
|
||||
* `Drip Depot <https://www.dripdepot.com>`_
|
||||
* `Image-charts <https://www.image-charts.com>`_
|
||||
* `Moat <https://www.moat.com>`_
|
||||
* `Netwo <https://www.netwo.io>`_
|
||||
* `Nimbus <https://www.nimbusforwork.com>`_
|
||||
- See how Nimbus uses PostgREST in `Paul Copplestone's blog post <https://paul.copplest.one/blog/nimbus-tech-2019-04.html>`_.
|
||||
* `OpenBooking <https://www.openbooking.ch>`_
|
||||
* `Redsmin <https://www.redsmin.com>`_
|
||||
* `Sompani <https://www.sompani.com>`_
|
||||
* `Supabase <https://supabase.com>`_
|
||||
|
||||
.. Failing links
|
||||
* `eGull <http://www.egull.co>`_
|
||||
* `MotionDynamic - Fast highly dynamic video generation at scale <https://motiondynamic.tech>`_
|
||||
|
||||
Testimonials
|
||||
------------
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
Greenplum
|
||||
#########
|
||||
|
||||
`Greenplum <https://greenplum.org/>`_ has been reported to work by adding ``LOGIN`` to the :ref:`anonymous and user roles <roles>`.
|
||||
|
||||
For more details, see https://github.com/PostgREST/postgrest/issues/2021.
|
||||
@@ -12,6 +12,12 @@ First, create postgrest configuration in ``/etc/postgrest/config``
|
||||
db-anon-role = "<your_anon_role>"
|
||||
jwt-secret = "<your_secret>"
|
||||
|
||||
Create a dedicated ``postgrest`` user with:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
sudo useradd -M -U -d /nonexistent -s /usr/sbin/nologin postgrest
|
||||
|
||||
Then create the systemd service file in ``/etc/systemd/system/postgrest.service``
|
||||
|
||||
.. code-block:: ini
|
||||
@@ -21,6 +27,8 @@ Then create the systemd service file in ``/etc/systemd/system/postgrest.service`
|
||||
After=postgresql.service
|
||||
|
||||
[Service]
|
||||
User=postgrest
|
||||
Group=postgrest
|
||||
ExecStart=/bin/postgrest /etc/postgrest/config
|
||||
ExecReload=/bin/kill -SIGUSR1 $MAINPID
|
||||
|
||||
|
||||
+12
-25
@@ -10,54 +10,52 @@ authenticator
|
||||
backoff
|
||||
balancer
|
||||
booleans
|
||||
Bouscal
|
||||
buildpack
|
||||
BOM
|
||||
Bytea
|
||||
Cardano
|
||||
casted
|
||||
cd
|
||||
centric
|
||||
changelog
|
||||
coercible
|
||||
conf
|
||||
Cloudflare
|
||||
config
|
||||
cors
|
||||
CORS
|
||||
CPUs
|
||||
cryptographically
|
||||
CSV
|
||||
durations
|
||||
DDL
|
||||
DOM
|
||||
DevOps
|
||||
DiBiase
|
||||
dockerize
|
||||
eq
|
||||
ETH
|
||||
Ethereum
|
||||
EveryLayout
|
||||
Fenko
|
||||
Fernandes
|
||||
filename
|
||||
FreeBSD
|
||||
fts
|
||||
GC
|
||||
GeoJSON
|
||||
GHC
|
||||
Github
|
||||
Google
|
||||
grantor
|
||||
GraphQL
|
||||
Greenplum
|
||||
gte
|
||||
GUC
|
||||
GUCs
|
||||
gucs
|
||||
Haskell
|
||||
Heroku
|
||||
HMAC
|
||||
htmx
|
||||
Htmx
|
||||
Homebrew
|
||||
hstore
|
||||
HTTP
|
||||
HTTPS
|
||||
HV
|
||||
Ibarluzea
|
||||
Inlining
|
||||
inlined
|
||||
Integrations
|
||||
@@ -84,6 +82,7 @@ logins
|
||||
lon
|
||||
lt
|
||||
lte
|
||||
macOS
|
||||
misprediction
|
||||
multi
|
||||
namespace
|
||||
@@ -101,9 +100,7 @@ openapi
|
||||
ORM
|
||||
ov
|
||||
passphrase
|
||||
Pawel
|
||||
PBKDF
|
||||
Pelletier
|
||||
PgBouncer
|
||||
pgcrypto
|
||||
pgjwt
|
||||
@@ -117,7 +114,6 @@ phraseto
|
||||
plainto
|
||||
plfts
|
||||
poolers
|
||||
POSIX
|
||||
PostGIS
|
||||
PostgreSQL
|
||||
PostgreSQL's
|
||||
@@ -129,52 +125,43 @@ pre
|
||||
preflight
|
||||
plpgsql
|
||||
psql
|
||||
Qin
|
||||
RabbitMQ
|
||||
Rafaj
|
||||
RDS
|
||||
reallyreallyreallyreallyverysafe
|
||||
Rechkemmer
|
||||
Redux
|
||||
refactor
|
||||
reloadable
|
||||
Reloadable
|
||||
Remo
|
||||
requester's
|
||||
RESTful
|
||||
RLS
|
||||
RPC
|
||||
RSA
|
||||
Saleeba
|
||||
safeupdate
|
||||
savepoint
|
||||
schemas
|
||||
schema's
|
||||
Severin
|
||||
SHA
|
||||
Sommer
|
||||
signup
|
||||
SIGUSR
|
||||
sl
|
||||
spreaded
|
||||
Spreaded
|
||||
SQL
|
||||
sql
|
||||
sr
|
||||
SSL
|
||||
stateful
|
||||
stdout
|
||||
Stolarz
|
||||
supervisees
|
||||
SvelteKit
|
||||
SwaggerUI
|
||||
syslog
|
||||
systemd
|
||||
todo
|
||||
todos
|
||||
tos
|
||||
Tsingson
|
||||
tsquery
|
||||
tx
|
||||
Tyll
|
||||
TypeScript
|
||||
UI
|
||||
ui
|
||||
@@ -182,6 +169,7 @@ unicode
|
||||
unikernel
|
||||
unix
|
||||
updatable
|
||||
unfulfillable
|
||||
Untyped
|
||||
UPSERT
|
||||
Upsert
|
||||
@@ -201,4 +189,3 @@ Websockets
|
||||
webuser
|
||||
wfts
|
||||
www
|
||||
Zac
|
||||
|
||||
@@ -106,9 +106,11 @@ When debugging a problem it's important to verify the running PostgREST version.
|
||||
------------------------------
|
||||
PostgREST 11.1.0
|
||||
|
||||
.. note::
|
||||
.. important::
|
||||
|
||||
The server sets the `fallback_application_name <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-FALLBACK-APPLICATION-NAME>`_ for this query to work. To override the value set ``application_name`` on the connection string.
|
||||
- The server sets the `fallback_application_name <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-FALLBACK-APPLICATION-NAME>`_ to the connection URI for this query to work. To override the value set ``application_name`` on the connection string.
|
||||
- The version will only be set if it's a valid URI (`RFC 3986 <https://datatracker.ietf.org/doc/html/rfc3986>`_). This means any special characters must be urlencoded.
|
||||
- The version will not be set if the connection string is in `keyword/value format <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-KEYWORD-VALUE>`_.
|
||||
|
||||
- The ``stderr`` logs also contain the version, as noted on :ref:`pgrst_logging`.
|
||||
|
||||
@@ -141,6 +143,41 @@ You can enable tracing HTTP requests by setting :ref:`server-trace-header`. Spec
|
||||
HTTP/1.1 200 OK
|
||||
X-Request-Id: 123
|
||||
|
||||
.. _server-timing_header:
|
||||
|
||||
Server-Timing Header
|
||||
--------------------
|
||||
|
||||
You can enable the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>`_ header by setting :ref:`server-timing-enabled` on.
|
||||
This header communicates metrics of the different phases in the request-response cycle.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /users HTTP/1.1
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/users" -i
|
||||
|
||||
.. code::
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
|
||||
Server-Timing: jwt;dur=14.9, parse;dur=71.1, plan;dur=109.0, transaction;dur=353.2, response;dur=4.4
|
||||
|
||||
- All the durations (``dur``) are in milliseconds.
|
||||
- The ``jwt`` stage is when :ref:`jwt_impersonation` is done. This duration can be lowered with :ref:`jwt_caching`.
|
||||
- On the ``parse`` stage, the :ref:`url_grammar` is parsed.
|
||||
- On the ``plan`` stage, the :ref:`schema_cache` is used to generate the :ref:`main_query` of the transaction.
|
||||
- The ``transaction`` stage corresponds to the database transaction. See :ref:`transactions`.
|
||||
- The ``response`` stage is where the response status and headers are computed.
|
||||
|
||||
.. note::
|
||||
|
||||
We're working on lowering the duration of the ``parse`` and ``plan`` stages on https://github.com/PostgREST/postgrest/issues/2816.
|
||||
|
||||
.. _explain_plan:
|
||||
|
||||
Execution plan
|
||||
|
||||
@@ -14,9 +14,13 @@ PostgREST exposes three database objects of a schema as resources: tables, views
|
||||
api/schemas.rst
|
||||
api/computed_fields.rst
|
||||
api/domain_representations.rst
|
||||
api/pagination_count.rst
|
||||
api/resource_embedding.rst
|
||||
api/resource_representation.rst
|
||||
api/media_type_handlers.rst
|
||||
api/aggregate_functions.rst
|
||||
api/openapi.rst
|
||||
api/preferences.rst
|
||||
api/*
|
||||
|
||||
.. raw:: html
|
||||
@@ -100,7 +104,7 @@ PostgREST exposes three database objects of a schema as resources: tables, views
|
||||
'#immutable-and-stable-functions': 'transactions.html#access-mode',
|
||||
'#http-context': 'transactions.html#transaction-scoped-settings',
|
||||
'#accessing-request-headers-cookies-and-jwt-claims': 'transactions.html#request-headers-cookies-and-jwt-claims',
|
||||
'#legacy-guc-variable-names': 'transactions.html#legacy-settings',
|
||||
'#legacy-guc-variable-names': 'transactions.html#transaction-scoped-settings',
|
||||
'#accessing-request-path-and-method': 'transactions.html#request-path-and-method',
|
||||
'#setting-response-headers': 'transactions.html#response-headers',
|
||||
'#setting-headers-via-pre-request': 'transactions.html#setting-headers-via-pre-request',
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
.. _aggregate_functions:
|
||||
|
||||
Aggregate Functions
|
||||
###################
|
||||
|
||||
Aggregate functions allow you to summarize data by performing calculations across groups of rows. For instance, if you have an ``orders`` table that has an ``amount`` column, you could use an aggregate function to get the sum of the ``amount`` column, either for all rows, or for each group of rows that share specific values, for instance all rows that share the same ``order_date``.
|
||||
|
||||
.. note::
|
||||
Aggregate functions are *disabled* by default in PostgREST, as without appropriate safeguards, aggregate functions can create performance problems. See :ref:`db-aggregates-enabled` for further details.
|
||||
|
||||
PostgREST supports the following aggregate functions: ``avg()``, ``count()``, ``max()``, ``min()``, and ``sum()``. Please refer to the `section on aggregate functions in the PostgreSQL documentation <https://www.postgresql.org/docs/current/functions-aggregate.html>`_ for a detailed explanation of these functions.
|
||||
|
||||
To use an aggregate function, you append the function to a value in the ``select`` parameter, like so:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /orders?select=amount.sum() HTTP/1.1
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/orders?select=amount.sum()"
|
||||
|
||||
With the above query, PostgREST will return a single row with a single column named ``sum`` that contains the sum of all the values in the ``amount`` column:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"sum": 1234.56
|
||||
}
|
||||
]
|
||||
|
||||
You can use multiple aggregate functions by just adding more columns with aggregate functions to the ``select`` parameter.
|
||||
|
||||
To group by other columns, you simply add those columns to the ``select`` parameter. For instance:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /orders?select=amount.sum(),amount.avg(),order_date HTTP/1.1
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/orders?select=amount.sum(),amount.avg(),order_date"
|
||||
|
||||
This will return a row for each unique value in the ``order_date`` column, with the sum and average of the ``amount`` column for all rows that share the same ``order_date``:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"sum": 1234.56,
|
||||
"avg": 123.45,
|
||||
"order_date": "2023-01-01"
|
||||
},
|
||||
{
|
||||
"sum": 2345.67,
|
||||
"avg": 234.56,
|
||||
"order_date": "2023-01-02"
|
||||
}
|
||||
]
|
||||
|
||||
.. note::
|
||||
Aggregate functions work alongside other PostgREST features, like :ref:`h_filter`, :ref:`json_columns`, and :ref:`ordering`. Please note at this time aggregate functions are not compatible with :ref:`domain_reps`. Additionally, PostgreSQL's ``HAVING`` clause and ordering by aggregated columns are not yet supported.
|
||||
|
||||
The Case of ``count()``
|
||||
===========================
|
||||
|
||||
.. note::
|
||||
Before the addition of aggregate functions, it was possible to count by adding ``count`` (without parentheses) to the ``select`` parameter. While this is still supported, it may be deprecated in the future, and thus use of this legacy feature is **not recommended.** Please use ``count()`` (with parentheses) instead.
|
||||
|
||||
|
||||
``count()`` is treated specially, as it can be used without an associated column. Take for example the following query:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /orders?select=count(),order_date HTTP/1.1
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/orders?select=count(),order_date"
|
||||
|
||||
This would return a row for each unique value in the ``order_date`` column, with the count of all rows that share the same ``order_date``:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"count": 4,
|
||||
"order_date": "2023-01-01"
|
||||
},
|
||||
{
|
||||
"count": 2,
|
||||
"order_date": "2023-01-02"
|
||||
}
|
||||
]
|
||||
|
||||
When ``count()`` is used with an associated column, its behaviour is slightly different: It will return the count of all values that are not ``NULL``. This is due to how PostgreSQL itself implements the ``count()`` function.
|
||||
|
||||
Renaming and Casting
|
||||
====================
|
||||
|
||||
Renaming Aggregates
|
||||
-------------------
|
||||
|
||||
Just like with other columns, you can rename aggregated columns too. See :ref:`renaming_columns` for details.
|
||||
|
||||
Renaming columns is especially helpful in the context of aggregate functions, as by default a column with an aggregate function applied will take on the name of the applied aggregate function. You may want to provide a more semantically meaningful name or prevent collisions when using multiple aggregate functions of the same type.
|
||||
|
||||
Casting Aggregates
|
||||
------------------
|
||||
|
||||
When applying an aggregate function to a column, you are able to cast both the value of the input to the aggregate function *and* the value of the output from the aggregate function. In both cases, the syntax works as described in :ref:`casting_columns`, with the only difference being the placement of the cast.
|
||||
|
||||
Casting the Value of the Input
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For instance, imagine that the ``orders`` table has a JSON column, ``order_details``, and this column contains a JSON object that has a key, ``tax_amount``. Let's say you want to get the sum of the tax amount for every order. You can use the ``->`` or ``->>`` operators to extract the value with this key (see :ref:`json_columns`), but these operators will return values of the types JSON and ``text`` respectively, and neither of these types can be used with ``sum()``.
|
||||
|
||||
Therefore, you will need to first cast the input value to a type that is compatible with ``sum()`` (e.g. ``numeric``). Casting the input value is done in exactly the same way as casting any other value:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /orders?select=order_details->tax_amount::numeric.sum() HTTP/1.1
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/orders?select=order_details->tax_amount::numeric.sum()"
|
||||
|
||||
With this, you will receive the sum of the casted ``tax_amount`` value:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"sum": 1234.56
|
||||
}
|
||||
]
|
||||
|
||||
Casting the Value of the Output
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Now let's return to an example involving the ``amount`` column of the ``orders`` table. Imagine that we want to get the rounded average of the ``amount`` column. One way to do this is to use the ``avg()`` aggregate function and then to cast the output value of the function to ``int``. To cast the value of the output of the function, we simply place the cast *after* the aggregate function:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /orders?select=amount.avg()::int HTTP/1.1
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/orders?select=amount.avg()::int"
|
||||
|
||||
You will then receive the rounded average as the result:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"avg": 201
|
||||
}
|
||||
]
|
||||
|
||||
Of course, you can use both input and output casts at the same time, if you so desire.
|
||||
|
||||
|
||||
Using Aggregate Functions with Resource Embedding
|
||||
=================================================
|
||||
|
||||
Aggregate functions can be used in conjunction with :ref:`resource_embedding`. You can use embedded resources as grouping columns, use aggregate functions within the context of an embedded resource, or use columns from a spreaded resource as grouping columns or as inputs to aggregate functions.
|
||||
|
||||
Using Embedded Resources as Grouping Columns
|
||||
--------------------------------------------
|
||||
|
||||
Using an embedded resource as a grouping column allows you to use data from an association to group the results of an aggregation.
|
||||
|
||||
For example, imagine that the ``orders`` table from the examples above is related to a ``customers`` table. If you want to get the sum of the ``amount`` column grouped by the ``name`` column from the ``customers`` table, you can include the customer name, using the standard :ref:`resource_embedding` syntax, and perform a sum on the ``amount`` column.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /orders?select=amount.sum(),customers(name) HTTP/1.1
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/orders?select=amount.sum(),customers(name)"
|
||||
|
||||
You will then get the summed amount, along with the embedded customer resource:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"sum": 100,
|
||||
"customers": {
|
||||
"name": "Customer A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"sum": 200,
|
||||
"customers": {
|
||||
"name": "Customer B"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
.. note::
|
||||
The previous example uses a has-one association to demonstrate this functionality, but you may also use has-many associations as grouping columns, although there are few obvious use cases for this.
|
||||
|
||||
Using Aggregate Functions Within the Context of an Embedded Resource
|
||||
--------------------------------------------------------------------
|
||||
|
||||
When embedding a resource, you can apply aggregate functions to columns from the associated resource to perform aggregations within the context of an embedded resource.
|
||||
|
||||
Continuing with the example relationship between ``orders`` and ``customers`` from the previous section, imagine that you want to fetch the ``name``, ``city``, and ``state`` for each customer, along with the sum of amount of the customer's orders, grouped by the order date. This can be done in the following way:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /customers?select=name,city,state,orders(amount.sum(),order_date) HTTP/1.1
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/customers?select=name,city,state,orders(amount.sum(),order_date)"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"name": "Customer A",
|
||||
"city": "New York",
|
||||
"state": "NY",
|
||||
"orders": [
|
||||
{
|
||||
"sum": 215.22,
|
||||
"order_date": "2023-09-01"
|
||||
},
|
||||
{
|
||||
"sum": 905.73,
|
||||
"order_date": "2023-09-02"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Customer B",
|
||||
"city": "Los Angeles",
|
||||
"state": "CA",
|
||||
"orders": [
|
||||
{
|
||||
"sum": 329.71,
|
||||
"order_date": "2023-09-01"
|
||||
},
|
||||
{
|
||||
"sum": 425.87,
|
||||
"order_date": "2023-09-03"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
In this example, the ``amount`` column is summed and grouped by the ``order_date`` *within* the context of the embedded resource. That is, the ``name``, ``city``, and ``state`` from the ``customers`` table have no bearing on the aggregation performed in the context of the ``orders`` association; instead, each aggregation can be seen as being performed independently on just the orders belonging to a particular customer, using only the data from the embedded resource for both grouping and aggregation.
|
||||
|
||||
Using Columns from a Spreaded Resource
|
||||
--------------------------------------
|
||||
|
||||
When you :ref:`spread an embedded resource <spread_embed>`, the columns from the spreaded resource are treated as if they were columns of the top-level resource, both when using them as grouping columns and when applying aggregate functions to them.
|
||||
|
||||
Grouping with Columns from a Spreaded Resource
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For instance, assume you want to sum the ``amount`` column from the ``orders`` table, using the ``city`` and ``state`` columns from the ``customers`` table as grouping columns. To achieve this, you may select these two columns from the ``customers`` table and spread them; they will then be used as grouping columns:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /orders?select=amount.sum(),...customers(city,state) HTTP/1.1
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/orders?select=amount.sum(),...customers(city,state)
|
||||
|
||||
The result will be the same as if ``city`` and ``state`` were columns from the ``orders`` table:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"sum": 2000.29,
|
||||
"city": "New York",
|
||||
"state": "NY"
|
||||
},
|
||||
{
|
||||
"sum": 9241.21,
|
||||
"city": "Los Angeles",
|
||||
"state": "CA"
|
||||
}
|
||||
]
|
||||
|
||||
Aggregate Functions with Columns from a Spreaded Resource
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Now imagine that the ``customers`` table has a ``joined_date`` column that represents the date that the customer joined. You want to get both the most recent and the oldest ``joined_date`` for customers that placed an order on every distinct order date. This can be expressed as follows:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /orders?select=order_date,...customers(joined_date.max(),joined_date.min()) HTTP/1.1
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/orders?select=order_date,...customers(joined_date.max(),joined_date.min())
|
||||
|
||||
As columns from a spreaded resource are treated as if they were columns from the top-level resource, the ``max()`` and ``min()`` are applied *within* the context of the top-level, rather than within the context of the embedded resource, as in the previous section.
|
||||
|
||||
The result will be the same as if the aggregations were applied to columns from the top-level:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"order_date": "2023-11-01",
|
||||
"max": "2023-10-15",
|
||||
"min": "2013-10-01"
|
||||
},
|
||||
{
|
||||
"order_date": "2023-11-02",
|
||||
"max": "2023-10-30",
|
||||
"min": "2016-02-11"
|
||||
}
|
||||
]
|
||||
@@ -1,7 +1,10 @@
|
||||
.. _cors:
|
||||
|
||||
CORS
|
||||
====
|
||||
####
|
||||
|
||||
By default, PostgREST sets highly permissive cross origin resource sharing, that is why it accepts Ajax requests from any domain. This behavior can be configured by using :ref:`server_cors_allowed_origins`.
|
||||
|
||||
PostgREST sets highly permissive cross origin resource sharing, that is why it accepts Ajax requests from any domain.
|
||||
|
||||
It also handles `preflight requests <https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request>`_ done by the browser, which are cached using the returned ``Access-Control-Max-Age: 86400`` header (86400 seconds = 24 hours). This is useful to reduce the latency of the subsequent requests.
|
||||
|
||||
@@ -32,3 +35,16 @@ A ``POST`` preflight request would look like this:
|
||||
Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD
|
||||
Access-Control-Allow-Headers: Authorization, Content-Type, Accept, Accept-Language, Content-Language
|
||||
Access-Control-Max-Age: 86400
|
||||
|
||||
.. _allowed_origins:
|
||||
|
||||
Allowed Origins
|
||||
===============
|
||||
|
||||
With the following config setting, PostgREST will accept CORS requests from domains :code:`http://example.com` and :code:`http://example2.com`.
|
||||
|
||||
|
||||
.. code-block::
|
||||
|
||||
server-cors-allowed-origins="http://example.com, http://example2.com"
|
||||
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
.. _custom_media:
|
||||
|
||||
Media Type Handlers
|
||||
###################
|
||||
|
||||
Media Type Handlers allow PostgREST to deliver custom media types. These handlers extend the :ref:`builtin ones <builtin_media>` and can also override them.
|
||||
|
||||
Media types are expressed as type aliases using `domains <https://www.postgresql.org/docs/current/sql-createdomain.html>`_ and their name must comply to `RFC 6838 requirements <https://datatracker.ietf.org/doc/html/rfc6838#section-4.2>`_.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE DOMAIN "application/json" AS json;
|
||||
|
||||
Using these domains, :ref:`functions <s_procs>` can become handlers and `user-defined aggregates <https://www.postgresql.org/docs/current/xaggr.html>`_ can serve as handlers for :ref:`tables_views` and :ref:`table_functions`.
|
||||
|
||||
.. important::
|
||||
|
||||
- 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`.
|
||||
|
||||
Handler Function
|
||||
================
|
||||
|
||||
As an example, let's obtain the `TWKB <https://postgis.net/docs/ST_AsTWKB.html>`_ compressed binary format for a PostGIS geometry.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create extension postgis;
|
||||
|
||||
create table lines (
|
||||
id int primary key
|
||||
, name text
|
||||
, geom geometry(LINESTRING, 4326)
|
||||
);
|
||||
|
||||
insert into lines values (1, 'line-1', 'LINESTRING(1 1,5 5)'::geometry), (2, 'line-2', 'LINESTRING(2 2,6 6)'::geometry);
|
||||
|
||||
For this you can create a vendor media type.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "application/vnd.twkb" as bytea;
|
||||
|
||||
And use it as a return type on a function, to make it a handler.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function get_line (id int)
|
||||
returns "application/vnd.twkb" as $$
|
||||
select st_astwkb(geom) from lines where id = get_line.id;
|
||||
$$ language sql;
|
||||
|
||||
.. note::
|
||||
|
||||
For PostgreSQL <= 12, you'll need a cast on the function body :code:`st_astwkb(geom)::"application/vnd.twkb"`.
|
||||
|
||||
Now you can request the ``TWKB`` output like so:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/rpc/get_line?id=1' -i \
|
||||
-H "Accept: application/vnd.twkb"
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/vnd.twkb
|
||||
|
||||
# binary output
|
||||
|
||||
Note that PostgREST will automatically set the ``Content-Type`` to ``application/vnd.twkb``.
|
||||
|
||||
Handlers for Tables/Views
|
||||
=========================
|
||||
|
||||
To benefit from a compressed format like ``TWKB``, it makes more sense to obtain many rows instead of one. Let's allow that by adding a handler for the table.
|
||||
|
||||
User-defined aggregates can be turned into handlers by using domain media types as the return type of their transition or final functions.
|
||||
|
||||
Let's create a transition function for this example.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function twkb_handler_transition (state bytea, next lines)
|
||||
returns "application/vnd.twkb" as $$
|
||||
select state || st_astwkb(next.geom);
|
||||
$$ language sql;
|
||||
|
||||
Now we'll use it on a new aggregate defined for the ``lines`` table.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace aggregate twkb_agg (lines) (
|
||||
initcond = ''
|
||||
, stype = "application/vnd.twkb"
|
||||
, sfunc = twkb_handler_transition
|
||||
);
|
||||
|
||||
Make a quick test on SQL to see it working.
|
||||
|
||||
.. code-block:: psql
|
||||
|
||||
SELECT twkb_agg(l) from lines l;
|
||||
|
||||
twkb_agg
|
||||
---------------------------------------------------------------
|
||||
\xa20002c09a0cc09a0c80ea3080ea30a2000280b51880b51880ea3080ea30
|
||||
(1 row)
|
||||
|
||||
Now you can request the table endpoint with the ``twkb`` media type:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/lines' -i \
|
||||
-H "Accept: application/vnd.twkb"
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/vnd.twkb
|
||||
|
||||
# binary output
|
||||
|
||||
If you have a table-valued function returning the same table type, the handler can also act upon on it.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function get_lines ()
|
||||
returns setof lines as $$
|
||||
select * from lines;
|
||||
$$ language sql;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/get_lines' -i \
|
||||
-H "Accept: application/vnd.twkb"
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/vnd.twkb
|
||||
|
||||
# binary output
|
||||
|
||||
Overriding a Builtin Handler
|
||||
============================
|
||||
|
||||
Let's override the existing ``text/csv`` handler for the table to provide a more complex CSV output.
|
||||
It'll include a `Byte order mark (BOM) <https://en.wikipedia.org/wiki/Byte_order_mark>`_ plus a ``Content-Disposition`` header to set a name for the downloaded file.
|
||||
|
||||
Create a domain for the standard ``text/csv`` media type.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "text/csv" as text;
|
||||
|
||||
And a transition function that returns the domain.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function bom_csv_trans (state text, next lines)
|
||||
returns "text/csv" as $$
|
||||
select state || next.id::text || ',' || next.name || ',' || next.geom::text || E'\n';
|
||||
$$ language sql;
|
||||
|
||||
This time we'll add a final function. This will add the CSV header, the BOM and the ``Content-Disposition`` header.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function bom_csv_final (data "text/csv")
|
||||
returns "text/csv" as $$
|
||||
-- set the Content-Disposition header
|
||||
select set_config('response.headers', '[{"Content-Disposition": "attachment; filename=\"lines.csv\""}]', true);
|
||||
select
|
||||
-- EFBBBF is the BOM in UTF8 https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
|
||||
convert_from (decode (E'EFBBBF', 'hex'),'UTF8') ||
|
||||
-- the header for the CSV
|
||||
(E'id,name,geom\n' || data);
|
||||
$$ language sql;
|
||||
|
||||
Now use the transition and final function as part of the new aggregate.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace aggregate bom_csv_agg (lines) (
|
||||
initcond = ''
|
||||
, stype = "text/csv"
|
||||
, sfunc = bom_csv_trans
|
||||
, finalfunc = bom_csv_final
|
||||
);
|
||||
|
||||
.. code-block:: psql
|
||||
|
||||
select bom_csv_agg(l) from lines l;
|
||||
bom_csv_agg
|
||||
-----------------------------------------------------------------------------------------------------
|
||||
id,name,geom +
|
||||
1,line-1,0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440+
|
||||
2,line-2,0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840+
|
||||
|
||||
(1 row)
|
||||
|
||||
And request it like:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/lines' -i \
|
||||
-H "Accept: text/csv"
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/csv
|
||||
Content-Disposition: attachment; filename="lines.csv"
|
||||
|
||||
id,name,geom
|
||||
1,line-1,0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440
|
||||
2,line-2,0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840
|
||||
|
||||
.. _any_handler:
|
||||
|
||||
The "Any" Handler
|
||||
=================
|
||||
|
||||
For more flexibility, you can also define a catch-all handler by using a domain named ``*/*`` (any media type). This obeys to the following rules:
|
||||
|
||||
- Responds to all media types and even to requests that don't include an ``Accept`` header.
|
||||
- Sets the ``Content-Type`` header to ``application/octet-stream`` by default, but this can be overridden inside the function with :ref:`guc_resp_hdrs`.
|
||||
- This overrides all other handlers (:ref:`builtin <builtin_media>` or custom), so it's better to do it for an isolated function or view.
|
||||
|
||||
Let's define an any handler for a view that will always respond with ``XML`` output. It will accept ``text/xml``, ``application/xml``, ``*/*`` and reject other media types.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "*/*" as bytea;
|
||||
|
||||
-- we'll use an .xml suffix for the view to be clear its output is always XML
|
||||
create view "lines.xml" as
|
||||
select * from lines;
|
||||
|
||||
-- transition function
|
||||
create or replace function lines_xml_trans (state "*/*", next "lines.xml")
|
||||
returns "*/*" as $$
|
||||
select state || xmlelement(name line, xmlattributes(next.id as id, next.name as name), next.geom)::text::bytea || E'\n' ;
|
||||
$$ language sql;
|
||||
|
||||
-- final function
|
||||
create or replace function lines_xml_final (data "*/*")
|
||||
returns "*/*" as $$
|
||||
declare
|
||||
-- get the Accept header
|
||||
req_accept text := current_setting('request.headers', true)::json->>'accept';
|
||||
begin
|
||||
-- when we need to override the default Content-Type (application/octet-stream) set by PostgREST
|
||||
if req_accept = '*/*' then
|
||||
perform set_config('response.headers', json_build_array(json_build_object('Content-Type', 'text/xml'))::text, true);
|
||||
elsif req_accept IN ('application/xml', 'text/xml') then
|
||||
perform set_config('response.headers', json_build_array(json_build_object('Content-Type', req_accept))::text, true);
|
||||
else
|
||||
-- we'll reject other non XML media types, we need to reject manually since */* will command PostgREST to accept all media types
|
||||
raise sqlstate 'PT415' using message = 'Unsupported Media Type';
|
||||
end if;
|
||||
|
||||
return data;
|
||||
end; $$ language plpgsql;
|
||||
|
||||
-- new aggregate
|
||||
create or replace aggregate lines_xml_agg ("lines.xml") (
|
||||
stype = "*/*"
|
||||
, sfunc = lines_xml_trans
|
||||
, finalfunc = lines_xml_final
|
||||
);
|
||||
|
||||
Test it on SQL:
|
||||
|
||||
.. code-block:: psql
|
||||
|
||||
select (encode(lines_xml_agg(x), 'escape'))::xml from "lines.xml" x;
|
||||
encode
|
||||
------------------------------------------------------------------------------------------------------------------------------
|
||||
<line id="1" name="line-1">0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440</line>+
|
||||
<line id="2" name="line-2">0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840</line>+
|
||||
|
||||
Now we can omit the ``Accept`` header and it will respond with XML.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/lines.xml' -i
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/xml
|
||||
|
||||
<line id="1" name="line-1">0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440</line>
|
||||
<line id="2" name="line-2">0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840</line>
|
||||
|
||||
And it will accept only XML media types.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/lines.xml' -i \
|
||||
-H "Accept: text/xml"
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/xml
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/lines.xml' -i \
|
||||
-H "Accept: application/xml"
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/xml
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/lines.xml' -i \
|
||||
-H "Accept: unknown/media"
|
||||
|
||||
HTTP/1.1 415 Unsupported Media Type
|
||||
@@ -0,0 +1,188 @@
|
||||
Pagination and Count
|
||||
####################
|
||||
|
||||
Pagination controls the number of rows returned for an :doc:`API resource <../api>` response. Combined with the count, you can traverse all the rows of a response.
|
||||
|
||||
.. _limits:
|
||||
|
||||
Limits and Pagination
|
||||
---------------------
|
||||
|
||||
PostgREST uses HTTP range headers to describe the size of results. Every response contains the current range and, if requested, the total number of results:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Range-Unit: items
|
||||
Content-Range: 0-14/*
|
||||
|
||||
Here items zero through fourteen are returned. This information is available in every response and can help you render pagination controls on the client. This is an RFC7233-compliant solution that keeps the response JSON cleaner.
|
||||
|
||||
Query Parameters
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
One way to request limits and offsets is by using query parameters. For example:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /people?limit=15&offset=30 HTTP/1.1
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/people?limit=15&offset=30"
|
||||
|
||||
This method is also useful for embedded resources, which we will cover in another section. The server always responds with range headers even if you use query parameters to limit the query.
|
||||
|
||||
Range Header
|
||||
~~~~~~~~~~~~
|
||||
|
||||
You can use headers to specify the range of rows desired.
|
||||
This request gets the first twenty people:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /people HTTP/1.1
|
||||
Range-Unit: items
|
||||
Range: 0-19
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/people" -i \
|
||||
-H "Range-Unit: items" \
|
||||
-H "Range: 0-19"
|
||||
|
||||
Note that the server may respond with fewer if unable to meet your request:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Range-Unit: items
|
||||
Content-Range: 0-17/*
|
||||
|
||||
You may also request open-ended ranges for an offset with no limit, e.g. :code:`Range: 10-`.
|
||||
|
||||
.. _prefer_count:
|
||||
|
||||
Counting
|
||||
--------
|
||||
|
||||
In order to obtain the total size of the table (such as when rendering the last page link in a pagination control), you can specify a ``Prefer: count=<value>`` header. The values can be ``exact``, ``planned`` and ``estimated``.
|
||||
|
||||
This also works on views and :ref:`table_functions`.
|
||||
|
||||
|
||||
.. _exact_count:
|
||||
|
||||
Exact Count
|
||||
~~~~~~~~~~~
|
||||
|
||||
To get the exact count, use ``Prefer: count=exact``.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
HEAD /bigtable HTTP/1.1
|
||||
Range-Unit: items
|
||||
Range: 0-24
|
||||
Prefer: count=exact
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/bigtable" -I \
|
||||
-H "Range-Unit: items" \
|
||||
-H "Range: 0-24" \
|
||||
-H "Prefer: count=exact"
|
||||
|
||||
Note that the larger the table the slower this query runs in the database. The server will respond with the selected range and total
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 206 Partial Content
|
||||
Range-Unit: items
|
||||
Content-Range: 0-24/3573458
|
||||
|
||||
.. _planned_count:
|
||||
|
||||
Planned Count
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
To avoid the shortcomings of :ref:`exact count <exact_count>`, PostgREST can leverage PostgreSQL statistics and get a fairly accurate and fast count.
|
||||
To do this, specify the ``Prefer: count=planned`` header.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
HEAD /bigtable?limit=25 HTTP/1.1
|
||||
Prefer: count=planned
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/bigtable?limit=25" -I \
|
||||
-H "Prefer: count=planned"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 206 Partial Content
|
||||
Content-Range: 0-24/3572000
|
||||
|
||||
Note that the accuracy of this count depends on how up-to-date are the PostgreSQL statistics tables.
|
||||
For example in this case, to increase the accuracy of the count you can do ``ANALYZE bigtable``.
|
||||
See `ANALYZE <https://www.postgresql.org/docs/current/sql-analyze.html>`_ for more details.
|
||||
|
||||
.. _estimated_count:
|
||||
|
||||
Estimated Count
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
When you are interested in the count, the relative error is important. If you have a :ref:`planned count <planned_count>` of 1000000 and the exact count is
|
||||
1001000, the error is small enough to be ignored. But with a planned count of 7, an exact count of 28 would be a huge misprediction.
|
||||
|
||||
In general, when having smaller row-counts, the estimated count should be as close to the exact count as possible.
|
||||
|
||||
To help with these cases, PostgREST can get the exact count up until a threshold and get the planned count when
|
||||
that threshold is surpassed. To use this behavior, you can specify the ``Prefer: count=estimated`` header. The **threshold** is
|
||||
defined by :ref:`db-max-rows`.
|
||||
|
||||
Here's an example. Suppose we set ``db-max-rows=1000`` and ``smalltable`` has 321 rows, then we'll get the exact count:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
HEAD /smalltable?limit=25 HTTP/1.1
|
||||
Prefer: count=estimated
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/smalltable?limit=25" -I \
|
||||
-H "Prefer: count=estimated"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 206 Partial Content
|
||||
Content-Range: 0-24/321
|
||||
|
||||
If we make a similar request on ``bigtable``, which has 3573458 rows, we would get the planned count:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
HEAD /bigtable?limit=25 HTTP/1.1
|
||||
Prefer: count=estimated
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/bigtable?limit=25" -I \
|
||||
-H "Prefer: count=estimated"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 206 Partial Content
|
||||
Content-Range: 0-24/3572000
|
||||
@@ -0,0 +1,225 @@
|
||||
.. _preferences:
|
||||
|
||||
Prefer Header
|
||||
#############
|
||||
|
||||
PostgREST honors the Prefer HTTP header specified on `RFC 7240 <https://www.rfc-editor.org/rfc/rfc7240.html>`_. It allows clients to specify required and optional behaviors for their requests.
|
||||
|
||||
The following preferences are supported.
|
||||
|
||||
- ``Prefer: handling``. See :ref:`prefer_handling`.
|
||||
- ``Prefer: timezone``. See :ref:`prefer_timezone`.
|
||||
- ``Prefer: return``. See :ref:`prefer_return`.
|
||||
- ``Prefer: count``. See :ref:`prefer_count`.
|
||||
- ``Prefer: resolution``. See :ref:`prefer_resolution`.
|
||||
- ``Prefer: missing``. See :ref:`bulk_insert_default`.
|
||||
|
||||
.. _prefer_handling:
|
||||
|
||||
Strict or Lenient Handling
|
||||
==========================
|
||||
|
||||
The server ignores unrecognized or unfulfillable preferences by default. You can control this behavior with the ``handling`` preference. It can take two values: ``lenient`` (the default) or ``strict``.
|
||||
|
||||
``handling=strict`` will throw an error if you specify invalid preferences. For instance:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /projects HTTP/1.1
|
||||
Prefer: handling=strict, foo, bar
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl -i "http://localhost:3000/projects" \
|
||||
-H "Prefer: handling=strict, foo, bar"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"code": "PGRST122",
|
||||
"message": "Invalid preferences given with handling=strict",
|
||||
"details": "Invalid preferences: foo, bar",
|
||||
"hint": null
|
||||
}
|
||||
|
||||
|
||||
``handling=lenient`` ignores invalid preferences.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /projects HTTP/1.1
|
||||
Prefer: handling=lenient, foo, bar
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl -i "http://localhost:3000/projects" \
|
||||
-H "Prefer: handling=lenient, foo, bar"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
.. _prefer_timezone:
|
||||
|
||||
Timezone
|
||||
========
|
||||
|
||||
The ``timezone`` preference allows you to change the `PostgreSQL timezone <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-TIMEZONE>`_. It accepts all timezones in `pg_timezone_names <https://www.postgresql.org/docs/current/view-pg-timezone-names.html>`_.
|
||||
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /timestamps HTTP/1.1
|
||||
Prefer: timezone=America/Los_Angeles
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl -i "http://localhost:3000/timestamps" \
|
||||
-H "Prefer: timezone=America/Los_Angeles"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Preference-Applied: timezone=America/Los_Angeles
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{"t":"2023-10-18T05:37:59.611-07:00"},
|
||||
{"t":"2023-10-18T07:37:59.611-07:00"},
|
||||
{"t":"2023-10-18T09:37:59.611-07:00"}
|
||||
]
|
||||
|
||||
For an invalid timezone, PostgREST returns values with the default timezone (configured on ``postgresql.conf`` or as a setting on the :ref:`authenticator <roles>`).
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /timestamps HTTP/1.1
|
||||
Prefer: timezone=Jupiter/Red_Spot
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl -i "http://localhost:3000/timestamps" \
|
||||
-H "Prefer: timezone=Jupiter/Red_Spot"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{"t":"2023-10-18T12:37:59.611+00:00"},
|
||||
{"t":"2023-10-18T14:37:59.611+00:00"},
|
||||
{"t":"2023-10-18T16:37:59.611+00:00"}
|
||||
]
|
||||
|
||||
Note that there's no ``Preference-Applied`` in the response.
|
||||
|
||||
However, with ``handling=strict``, an invalid timezone preference will throw an :ref:`error <pgrst122>`.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /timestamps HTTP/1.1
|
||||
Prefer: handling=strict, timezone=Jupiter/Red_Spot
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl -i "http://localhost:3000/timestamps" \
|
||||
-H "Prefer: handling=strict, timezone=Jupiter/Red_Spot"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
|
||||
.. _prefer_return:
|
||||
|
||||
Return Representation
|
||||
=====================
|
||||
|
||||
The ``return`` preference can be used to obtain information about affected resource when it's :ref:`inserted <insert>`, :ref:`updated <update>` or :ref:`deleted <delete>`.
|
||||
This helps avoid a subsequent GET request.
|
||||
|
||||
Minimal
|
||||
-------
|
||||
|
||||
With ``Prefer: return=minimal``, no response body will be returned. This is the default mode for all write requests.
|
||||
|
||||
Headers Only
|
||||
------------
|
||||
|
||||
If the table has a primary key, the response can contain a :code:`Location` header describing where to find the new object by including the header :code:`Prefer: return=headers-only` in the request. Make sure that the table is not write-only, otherwise constructing the :code:`Location` header will cause a permissions error.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
POST /projects HTTP/1.1
|
||||
Prefer: return=headers-only
|
||||
|
||||
{"id":33, "name": "x"}
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl -i "http://localhost:3000/projects" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: return=headers-only" \
|
||||
-d '{"id":33, "name": "x"}'
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Location: /projects?id=eq.34
|
||||
Preference-Applied: return=headers-only
|
||||
|
||||
Full
|
||||
----
|
||||
|
||||
On the other end of the spectrum you can get the full created object back in the response to your request by including the header :code:`Prefer: return=representation`. That way you won't have to make another HTTP call to discover properties that may have been filled in on the server side. You can also apply the standard :ref:`v_filter` to these results.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
POST /projects HTTP/1.1
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Prefer: return=representation
|
||||
|
||||
{"id":33, "name": "x"}
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl -i "http://localhost:3000/projects" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: return=representation" \
|
||||
-d '{"id":33, "name": "x"}'
|
||||
|
||||
.. code::
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Preference-Applied: return=representation
|
||||
|
||||
[
|
||||
{
|
||||
"id": 33,
|
||||
"name": "x"
|
||||
}
|
||||
]
|
||||
@@ -23,15 +23,44 @@ Use the Accept request header to specify the acceptable format (or formats) for
|
||||
curl "http://localhost:3000/people" \
|
||||
-H "Accept: application/json"
|
||||
|
||||
For tables and views the current possibilities are:
|
||||
.. _builtin_media:
|
||||
|
||||
* ``*/*``
|
||||
* ``text/csv``
|
||||
* ``application/json``
|
||||
* ``application/openapi+json``
|
||||
* ``application/geo+json``
|
||||
Builtin Media Type Handlers
|
||||
===========================
|
||||
|
||||
The server will default to JSON for API endpoints and OpenAPI on the root.
|
||||
Builtin handlers are offered for common standard media types.
|
||||
|
||||
* ``text/csv`` and ``application/json``, for all API endpoints. See :ref:`tables_views` and :ref:`s_procs`.
|
||||
* ``application/openapi+json``, for the root endpoint. See :ref:`open-api`.
|
||||
* ``application/geo+json``, see :ref:`ww_postgis`.
|
||||
* ``*/*``, 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.
|
||||
|
||||
* ``application/vnd.pgrst.plan``, see :ref:`explain_plan`.
|
||||
* ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``, see :ref:`singular_plural` and :ref:`stripped_nulls`.
|
||||
|
||||
Any unrecognized media type will throw an error.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /people HTTP/1.1
|
||||
Accept: unknown/unknown
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/people" \
|
||||
-H "Accept: unknown/unknown"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 415 Unsupported Media Type
|
||||
|
||||
{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: unknown/unknown"}
|
||||
|
||||
To extend the accepted media types, you can use :ref:`custom_media`.
|
||||
|
||||
.. _singular_plural:
|
||||
|
||||
@@ -85,6 +114,8 @@ When a singular response is requested but no entries are found, the server respo
|
||||
|
||||
Admittedly PostgREST could detect when there is an equality condition holding on all columns constituting the primary key and automatically convert to singular. However this could lead to a surprising change of format that breaks unwary client code just by filtering on an extra column. Instead we allow manually specifying singular vs plural to decouple that choice from the URL format.
|
||||
|
||||
.. _stripped_nulls:
|
||||
|
||||
Stripped Nulls
|
||||
--------------
|
||||
|
||||
@@ -122,72 +153,6 @@ This returns
|
||||
{ "id": 13, "name": "Y"}
|
||||
]
|
||||
|
||||
.. _scalar_return_formats:
|
||||
|
||||
Scalar Function Response Format
|
||||
-------------------------------
|
||||
|
||||
In the special case of a :ref:`scalar_functions` there are three additional formats:
|
||||
|
||||
* ``application/octet-stream``
|
||||
* ``text/plain``
|
||||
* ``text/xml``
|
||||
|
||||
Example 1: If you want to return raw binary data from a :code:`bytea` column, you must specify :code:`application/octet-stream` as part of the :code:`Accept` header
|
||||
and select a single column :code:`?select=bin_data`.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /items?select=bin_data&id=eq.1 HTTP/1.1
|
||||
Accept: application/octet-stream
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/items?select=bin_data&id=eq.1" \
|
||||
-H "Accept: application/octet-stream"
|
||||
|
||||
Example 2: You can request XML output when having a scalar function that returns a type of ``text/xml``. You are not forced to use select for this case.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE FUNCTION generate_xml_content(..) RETURNS xml ..
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
POST /rpc/generate_xml_content HTTP/1.1
|
||||
Accept: text/xml
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/rpc/generate_xml_content" \
|
||||
-X POST -H "Accept: text/xml"
|
||||
|
||||
Example 3: If the stored procedure returns non-scalar values, you need to do a :code:`select` in the same way as for GET binary output.
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
CREATE FUNCTION get_descriptions(..) RETURNS SETOF TABLE(id int, description text) ..
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
POST /rpc/get_descriptions?select=description HTTP/1.1
|
||||
Accept: text/plain
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/rpc/get_descriptions?select=description" \
|
||||
-X POST -H "Accept: text/plain"
|
||||
|
||||
.. note::
|
||||
|
||||
If more than one row would be returned the binary/plain-text/xml results will be concatenated with no delimiter.
|
||||
|
||||
.. _req_body:
|
||||
|
||||
Request Body
|
||||
|
||||
@@ -311,7 +311,9 @@ Repeating also works in POST requests with ``Content-Type: application/x-www-for
|
||||
-X POST -H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d 'v=1&v=2&v=3&v=4'
|
||||
|
||||
Table-Valued functions
|
||||
.. _table_functions:
|
||||
|
||||
Table-Valued Functions
|
||||
----------------------
|
||||
|
||||
A function that returns a table type can be filtered using the same filters as :ref:`tables and views <tables_views>`. They can also use :ref:`Resource Embedding <s_proc_embed>`.
|
||||
@@ -418,7 +420,7 @@ PostgREST will detect if the function is scalar or table-valued and will shape t
|
||||
{ "title": "Blade Runner 2049", "rating": 8.1}
|
||||
]
|
||||
|
||||
To manually choose a return format such as binary, plain text or XML, see the section :ref:`scalar_return_formats`.
|
||||
To manually choose a return format such as binary, see :ref:`custom_media`.
|
||||
|
||||
.. _untyped_functions:
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ cs :code:`@>` contains e.g. :code:`?tags=cs.{example,
|
||||
cd :code:`<@` contained in e.g. :code:`?values=cd.{1,2,3}`
|
||||
ov :code:`&&` overlap (have points in common), e.g. :code:`?period=ov.[2017-01-01,2017-06-30]` –
|
||||
also supports array types, use curly braces instead of square brackets e.g.
|
||||
:code:`?arr=ov.{1,3}`
|
||||
:code: `?arr=ov.{1,3}`
|
||||
sl :code:`<<` strictly left of, e.g. :code:`?range=sl.(1,10)`
|
||||
sr :code:`>>` strictly right of
|
||||
nxr :code:`&<` does not extend to the right of, e.g. :code:`?range=nxr.(1,10)`
|
||||
@@ -196,7 +196,7 @@ Pattern Matching
|
||||
|
||||
The pattern-matching operators (:code:`like`, :code:`ilike`, :code:`match`, :code:`imatch`) exist to support filtering data using patterns instead of concrete strings, as described in the `PostgreSQL docs <https://www.postgresql.org/docs/current/functions-matching.html>`__.
|
||||
|
||||
To ensure best performance on larger data sets, an `appropriate index <https://www.postgresql.org/docs/current/pgtrgm.html#id-1.11.7.44.8>`__ should be used and even then, it depends on the pattern value and actual data statistics whether an existing index will be used by the query planner or not.
|
||||
To ensure best performance on larger data sets, an `appropriate index <https://www.postgresql.org/docs/current/pgtrgm.html#PGTRGM-INDEX>`__ should be used and even then, it depends on the pattern value and actual data statistics whether an existing index will be used by the query planner or not.
|
||||
|
||||
.. _fts:
|
||||
|
||||
@@ -273,6 +273,8 @@ When certain columns are wide (such as those holding binary data), it is more ef
|
||||
|
||||
The default is ``*``, meaning all columns. This value will become more important below in :ref:`resource_embedding`.
|
||||
|
||||
.. _renaming_columns:
|
||||
|
||||
Renaming Columns
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -518,173 +520,6 @@ You can also sort on fields of :ref:`composite_array_columns` or :ref:`json_colu
|
||||
|
||||
curl "http://localhost:3000/countries?order=location->>lat"
|
||||
|
||||
.. _limits:
|
||||
|
||||
Limits and Pagination
|
||||
---------------------
|
||||
|
||||
PostgREST uses HTTP range headers to describe the size of results. Every response contains the current range and, if requested, the total number of results:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Range-Unit: items
|
||||
Content-Range: 0-14/*
|
||||
|
||||
Here items zero through fourteen are returned. This information is available in every response and can help you render pagination controls on the client. This is an RFC7233-compliant solution that keeps the response JSON cleaner.
|
||||
|
||||
There are two ways to apply a limit and offset rows: through request headers or query parameters. When using headers you specify the range of rows desired. This request gets the first twenty people.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /people HTTP/1.1
|
||||
Range-Unit: items
|
||||
Range: 0-19
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/people" -i \
|
||||
-H "Range-Unit: items" \
|
||||
-H "Range: 0-19"
|
||||
|
||||
Note that the server may respond with fewer if unable to meet your request:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Range-Unit: items
|
||||
Content-Range: 0-17/*
|
||||
|
||||
You may also request open-ended ranges for an offset with no limit, e.g. :code:`Range: 10-`.
|
||||
|
||||
The other way to request a limit or offset is with query parameters. For example
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /people?limit=15&offset=30 HTTP/1.1
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/people?limit=15&offset=30"
|
||||
|
||||
This method is also useful for embedded resources, which we will cover in another section. The server always responds with range headers even if you use query parameters to limit the query.
|
||||
|
||||
.. _exact_count:
|
||||
|
||||
Exact Count
|
||||
-----------
|
||||
|
||||
In order to obtain the total size of the table or view (such as when rendering the last page link in a pagination control), specify ``Prefer: count=exact`` as a request header:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
HEAD /bigtable HTTP/1.1
|
||||
Range-Unit: items
|
||||
Range: 0-24
|
||||
Prefer: count=exact
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/bigtable" -I \
|
||||
-H "Range-Unit: items" \
|
||||
-H "Range: 0-24" \
|
||||
-H "Prefer: count=exact"
|
||||
|
||||
Note that the larger the table the slower this query runs in the database. The server will respond with the selected range and total
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 206 Partial Content
|
||||
Range-Unit: items
|
||||
Content-Range: 0-24/3573458
|
||||
|
||||
.. _planned_count:
|
||||
|
||||
Planned Count
|
||||
-------------
|
||||
|
||||
To avoid the shortcomings of :ref:`exact count <exact_count>`, PostgREST can leverage PostgreSQL statistics and get a fairly accurate and fast count.
|
||||
To do this, specify the ``Prefer: count=planned`` header.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
HEAD /bigtable?limit=25 HTTP/1.1
|
||||
Prefer: count=planned
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/bigtable?limit=25" -I \
|
||||
-H "Prefer: count=planned"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 206 Partial Content
|
||||
Content-Range: 0-24/3572000
|
||||
|
||||
Note that the accuracy of this count depends on how up-to-date are the PostgreSQL statistics tables.
|
||||
For example in this case, to increase the accuracy of the count you can do ``ANALYZE bigtable``.
|
||||
See `ANALYZE <https://www.postgresql.org/docs/current/sql-analyze.html>`_ for more details.
|
||||
|
||||
.. _estimated_count:
|
||||
|
||||
Estimated Count
|
||||
---------------
|
||||
|
||||
When you are interested in the count, the relative error is important. If you have a :ref:`planned count <planned_count>` of 1000000 and the exact count is
|
||||
1001000, the error is small enough to be ignored. But with a planned count of 7, an exact count of 28 would be a huge misprediction.
|
||||
|
||||
In general, when having smaller row-counts, the estimated count should be as close to the exact count as possible.
|
||||
|
||||
To help with these cases, PostgREST can get the exact count up until a threshold and get the planned count when
|
||||
that threshold is surpassed. To use this behavior, you can specify the ``Prefer: count=estimated`` header. The **threshold** is
|
||||
defined by :ref:`db-max-rows`.
|
||||
|
||||
Here's an example. Suppose we set ``db-max-rows=1000`` and ``smalltable`` has 321 rows, then we'll get the exact count:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
HEAD /smalltable?limit=25 HTTP/1.1
|
||||
Prefer: count=estimated
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/smalltable?limit=25" -I \
|
||||
-H "Prefer: count=estimated"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 206 Partial Content
|
||||
Content-Range: 0-24/321
|
||||
|
||||
If we make a similar request on ``bigtable``, which has 3573458 rows, we would get the planned count:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
HEAD /bigtable?limit=25 HTTP/1.1
|
||||
Prefer: count=estimated
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/bigtable?limit=25" -I \
|
||||
-H "Prefer: count=estimated"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 206 Partial Content
|
||||
Content-Range: 0-24/3572000
|
||||
|
||||
.. _head_req:
|
||||
|
||||
HEAD
|
||||
@@ -720,70 +555,7 @@ To create a row in a database table post a JSON object whose keys are the names
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
|
||||
No request body will be returned by default.
|
||||
|
||||
.. note::
|
||||
|
||||
You can use the ``Prefer: return=minimal`` header to get the same behavior. This is only provided for completeness because it's basically a no-op.
|
||||
|
||||
Prefer: return=headers-only
|
||||
---------------------------
|
||||
|
||||
If the table has a primary key, the response can contain a :code:`Location` header describing where to find the new object by including the header :code:`Prefer: return=headers-only` in the request. Make sure that the table is not write-only, otherwise constructing the :code:`Location` header will cause a permissions error.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
POST /projects HTTP/1.1
|
||||
Prefer: return=headers-only
|
||||
|
||||
{"id":33, "name": "x"}
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/projects" \
|
||||
-X POST -H "Content-Type: application/json" -H "Prefer: return=headers-only" \
|
||||
-d '{"id":33, "name": "x"}'
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Location: /projects?id=eq.34
|
||||
Preference-Applied: return=headers-only
|
||||
|
||||
Prefer: return=representation
|
||||
-----------------------------
|
||||
|
||||
On the other end of the spectrum you can get the full created object back in the response to your request by including the header :code:`Prefer: return=representation`. That way you won't have to make another HTTP call to discover properties that may have been filled in on the server side. You can also apply the standard :ref:`v_filter` to these results.
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
POST /projects HTTP/1.1
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Prefer: return=representation
|
||||
|
||||
{"id":33, "name": "x"}
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/projects" \
|
||||
-X POST -H "Content-Type: application/json" -H "Prefer: return=representation" \
|
||||
-d '{"id":33, "name": "x"}'
|
||||
|
||||
.. code::
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Preference-Applied: return=representation
|
||||
|
||||
[
|
||||
{
|
||||
"id": 33,
|
||||
"name": "x"
|
||||
}
|
||||
]
|
||||
No response body will be returned by default but you can use :ref:`prefer_return` to get the affected resource.
|
||||
|
||||
x-www-form-urlencoded
|
||||
---------------------
|
||||
@@ -1014,12 +786,14 @@ To update a row or rows in a table, use the PATCH verb. Use :ref:`h_filter` to s
|
||||
-X PATCH -H "Content-Type: application/json" \
|
||||
-d '{ "category": "child" }'
|
||||
|
||||
Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`.
|
||||
Updates also support :ref:`prefer_return` plus :ref:`v_filter`.
|
||||
|
||||
.. warning::
|
||||
|
||||
Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`.
|
||||
|
||||
.. _prefer_resolution:
|
||||
|
||||
.. _upsert:
|
||||
|
||||
Upsert
|
||||
@@ -1131,7 +905,7 @@ To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instanc
|
||||
|
||||
curl "http://localhost:3000/user?active=is.false" -X DELETE
|
||||
|
||||
Deletions also support :code:`Prefer: return=representation` plus :ref:`v_filter`.
|
||||
Deletions also support :ref:`prefer_return` plus :ref:`v_filter`.
|
||||
|
||||
.. tabs::
|
||||
|
||||
@@ -1200,6 +974,12 @@ Using ``offset`` to target a different subset of rows is also possible.
|
||||
const redirects = {
|
||||
// Tables and Views
|
||||
'#computed-virtual-columns': 'computed_fields.html#computed-fields',
|
||||
'#limits-and-pagination': 'pagination_count.html#limits-and-pagination',
|
||||
'#exact-count': 'pagination_count.html#exact-count',
|
||||
'#planned-count': 'pagination_count.html#planned-count',
|
||||
'#estimated-count': 'pagination_count.html#estimated-count',
|
||||
'#prefer-return-headers-only': 'preferences.html#headers-only',
|
||||
'#prefer-return-representation': 'preferences.html#full',
|
||||
};
|
||||
|
||||
let willRedirectTo = redirects[hash];
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
This page is a work in progress.
|
||||
|
||||
.. _url_grammar:
|
||||
|
||||
URL Grammar
|
||||
===========
|
||||
|
||||
|
||||
+17
-24
@@ -21,6 +21,8 @@ The authenticator role is used for connecting to the database and should be conf
|
||||
|
||||
|
||||
CREATE ROLE authenticator LOGIN NOINHERIT NOCREATEDB NOCREATEROLE NOSUPERUSER;
|
||||
CREATE ROLE anonymous NOLOGIN;
|
||||
CREATE ROLE webuser NOLOGIN;
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -37,23 +39,9 @@ The picture below shows how the server handles authentication. If auth succeeds,
|
||||
|
||||
This role switching mechanism is called **user impersonation**. In PostgreSQL it's done with the ``SET ROLE`` statement.
|
||||
|
||||
.. _impersonated_settings:
|
||||
|
||||
Impersonated Role Settings
|
||||
--------------------------
|
||||
|
||||
The impersonated role has its settings applied. For example, if you do:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
ALTER ROLE webuser SET statement_timeout TO '5s';
|
||||
|
||||
Every ``webuser`` :ref:`transaction <transactions>` gets its queries executed with a ``statement_timeout`` of 5 seconds.
|
||||
|
||||
.. note::
|
||||
|
||||
Settings that have a high privilege context (like ``superuser``) won't be applied, only settings that have a ``user`` context will be. This is so we don't cause permission errors.
|
||||
For more details see `Understanding Postgres Parameter Context <https://www.enterprisedb.com/blog/understanding-postgres-parameter-context>`_.
|
||||
The impersonated roles will have their settings applied. See :ref:`impersonated_settings`.
|
||||
|
||||
.. _jwt_impersonation:
|
||||
|
||||
@@ -112,6 +100,19 @@ To make an authenticated request the client must include an :code:`Authorization
|
||||
|
||||
The ``Bearer`` header value can be used with or without capitalization(``bearer``).
|
||||
|
||||
.. _jwt_caching:
|
||||
|
||||
JWT Caching
|
||||
-----------
|
||||
|
||||
PostgREST validates ``JWTs`` on every request. We can cache ``JWTs`` to avoid this performance overhead.
|
||||
|
||||
To enable JWT caching, the config :code:`jwt-cache-max-lifetime` is to be set. It is the maximum number of seconds for which the cache stores the JWT validation results. The cache uses the :code:`exp` claim to set the cache entry lifetime. If the JWT does not have an :code:`exp` claim, it uses the config value. See :ref:`jwt-cache-max-lifetime` for more details.
|
||||
|
||||
.. note::
|
||||
|
||||
You can use the :ref:`server-timing_header` to see the effect of JWT caching.
|
||||
|
||||
Symmetric Keys
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
@@ -165,15 +166,7 @@ You can specify the literal value as we saw earlier, or reference a filename to
|
||||
JWT Claims Validation
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PostgREST honors the following `JWT claims <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4>`_:
|
||||
|
||||
- ``exp`` Expiration Time
|
||||
- ``iat`` Issued At
|
||||
- ``nbf`` Not Before
|
||||
- ``aud`` Audience, see :ref:`jwt-aud`
|
||||
|
||||
.. note::
|
||||
PostgREST allows for a 30-second clock skew when validating the ``exp`` and ``iat`` claims. In other words, it gives an extra 30 seconds before the token is rejected if there is a slight discrepancy in the timestamps.
|
||||
PostgREST honors the :code:`exp` claim for token expiration, rejecting expired tokens.
|
||||
|
||||
JWT Security
|
||||
~~~~~~~~~~~~
|
||||
|
||||
@@ -178,6 +178,28 @@ app.settings.*
|
||||
|
||||
Arbitrary settings that can be used to pass in secret keys directly as strings, or via OS environment variables. For instance: :code:`app.settings.jwt_secret = "$(MYAPP_JWT_SECRET)"` will take :code:`MYAPP_JWT_SECRET` from the environment and make it available to postgresql functions as :code:`current_setting('app.settings.jwt_secret')`.
|
||||
|
||||
.. _db-aggregates-enabled:
|
||||
|
||||
db-aggregates-enabled
|
||||
---------------------
|
||||
|
||||
=============== =======================
|
||||
**Type** Boolean
|
||||
**Default** False
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_AGGREGATES_ENABLED
|
||||
**In-Database** pgrst.db_aggregates_enabled
|
||||
=============== =======================
|
||||
|
||||
|
||||
When this is set to :code:`true`, the use of :ref:`aggregate_functions` is allowed.
|
||||
|
||||
It is recommended that this be set to ``false`` unless proper safeguards are in place to prevent potential performance problems from arising. For example, it is possible that a user may request the ``max()`` of an unindexed column in a table with millions of rows. At best, this would result in a slow query, and at worst, it could be abused to prevent other users from accessing your API (i.e. a form of denial-of-service attack.)
|
||||
|
||||
Proper safeguards could include:
|
||||
- Use of a statement timeout. See :ref:`impersonated_settings`.
|
||||
- Use of the `pg_plan_filter extension <https://github.com/pgexperts/pg_plan_filter>`_ to block excessively expensive queries.
|
||||
|
||||
.. _db-anon-role:
|
||||
|
||||
db-anon-role
|
||||
@@ -370,6 +392,23 @@ db-pool-max-lifetime
|
||||
|
||||
Specifies the maximum time in seconds of an existing connection in the pool.
|
||||
|
||||
.. _db-pool-automatic-recovery:
|
||||
|
||||
db-pool-automatic-recovery
|
||||
--------------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Boolean
|
||||
**Default** True
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_POOL_AUTOMATIC_RECOVERY
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
Enables or disables connection retrying.
|
||||
|
||||
When disabled, PostgREST would terminate immediately after connection loss instead of retrying indefinitely. See :ref:`this section <automatic_recovery>` for more information.
|
||||
|
||||
.. _db-pre-request:
|
||||
|
||||
db-pre-request
|
||||
@@ -515,24 +554,6 @@ External config file
|
||||
|
||||
Choosing a value for this parameter beginning with the at sign such as ``@filename`` (e.g. ``@./configs/my-config``) loads the connection string out of an external file.
|
||||
|
||||
|
||||
.. _db-use-legacy-gucs:
|
||||
|
||||
db-use-legacy-gucs
|
||||
------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Boolean
|
||||
**Default** True
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_USE_LEGACY_GUCS
|
||||
**In-Database** pgrst.db_use_legacy_gucs
|
||||
=============== =================================
|
||||
|
||||
Determine if GUC request settings for headers, cookies and jwt claims use the :ref:`legacy names <guc_legacy_names>` (string with dashes, invalid starting from PostgreSQL v14) with text values instead of the :ref:`new names <guc_req_headers_cookies_claims>` (string without dashes, valid on all PostgreSQL versions) with json values.
|
||||
|
||||
On PostgreSQL versions 14 and above, this parameter is ignored.
|
||||
|
||||
.. _jwt-aud:
|
||||
|
||||
jwt-aud
|
||||
@@ -611,6 +632,21 @@ jwt-secret-is-base64
|
||||
|
||||
When this is set to :code:`true`, the value derived from :code:`jwt-secret` will be treated as a base64 encoded secret.
|
||||
|
||||
.. _jwt-cache-max-lifetime:
|
||||
|
||||
jwt-cache-max-lifetime
|
||||
----------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Int
|
||||
**Default** 0
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_JWT_CACHE_MAX_LIFETIME
|
||||
**In-Database** pgrst.jwt_cache_max_lifetime
|
||||
=============== =================================
|
||||
|
||||
Maximum number of seconds of lifetime for cached entries. The default :code:`0` disables caching. See :ref:`jwt_caching`.
|
||||
|
||||
.. _log-level:
|
||||
|
||||
log-level
|
||||
@@ -718,29 +754,22 @@ openapi-server-proxy-uri
|
||||
]
|
||||
}
|
||||
|
||||
.. _raw-media-types:
|
||||
.. _server_cors_allowed_origins:
|
||||
|
||||
raw-media-types
|
||||
---------------
|
||||
server-cors-allowed-origins
|
||||
---------------------------
|
||||
|
||||
=============== =================================
|
||||
=============== ===================================
|
||||
**Type** String
|
||||
**Default** `n/a`
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_RAW_MEDIA_TYPES
|
||||
**In-Database** pgrst.raw_media_types
|
||||
=============== =================================
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_SERVER_CORS_ALLOWED_ORIGINS
|
||||
**In-Database** `pgrst.server_cors_allowed_origins`
|
||||
=============== ===================================
|
||||
|
||||
This serves to extend the `Media Types <https://en.wikipedia.org/wiki/Media_type>`_ that PostgREST currently accepts through an ``Accept`` header.
|
||||
Specifies allowed CORS origins in this config. See :ref:`cors`.
|
||||
|
||||
These media types can be requested by following the same rules as the ones defined in :ref:`scalar_return_formats`.
|
||||
|
||||
As an example, the below config would allow you to request an **image** and a **XML** file by doing a request with ``Accept: image/png``
|
||||
or ``Accept: font/woff2``, respectively.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
raw-media-types="image/png, font/woff2"
|
||||
When this is not set or set to :code:`""`, PostgREST **accepts** CORS requests from any domain.
|
||||
|
||||
.. _server-host:
|
||||
|
||||
@@ -776,7 +805,7 @@ server-port
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
The TCP port to bind the web server.
|
||||
The TCP port to bind the web server. Use ``0`` to automatically assign a port.
|
||||
|
||||
.. _server-trace-header:
|
||||
|
||||
@@ -793,6 +822,22 @@ server-trace-header
|
||||
|
||||
The header name used to trace HTTP requests. See :ref:`trace_header`.
|
||||
|
||||
.. _server-timing-enabled:
|
||||
|
||||
server-timing-enabled
|
||||
---------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Boolean
|
||||
**Default** False
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_SERVER_TIMING_ENABLED
|
||||
**In-Database** pgrst.server_timing_enabled
|
||||
=============== =================================
|
||||
|
||||
Enables the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>`_ header.
|
||||
See :ref:`server-timing_header`.
|
||||
|
||||
.. _server-unix-socket:
|
||||
|
||||
server-unix-socket
|
||||
|
||||
@@ -10,7 +10,7 @@ Minimizing connections is paramount to performance. Each PostgreSQL connection c
|
||||
Connection String
|
||||
-----------------
|
||||
|
||||
For connecting to the database, the pool requires a connection string. You can can configure it using :ref:`db-uri`.
|
||||
For connecting to the database, the pool requires a connection string. You can configure it using :ref:`db-uri`.
|
||||
|
||||
.. _pool_growth_limit:
|
||||
.. _dyn_conn_pool:
|
||||
@@ -78,6 +78,7 @@ The server will retry reconnecting to the database if connection loss happens.
|
||||
- The retries happen immediately after a connection loss, if :ref:`db-channel-enabled` is set to true (the default). Otherwise they'll happen once a request arrives.
|
||||
- To ensure a valid state, the server reloads the :ref:`schema_cache` and :ref:`configuration` when recovering.
|
||||
- To notify the client of the next retry, the server sends a ``503 Service Unavailable`` status with the ``Retry-After: x`` header. Where ``x`` is the number of seconds programmed for the next retry.
|
||||
- Automatic recovery can be disabled by setting :ref:`db-pool-automatic-recovery` to ``false``.
|
||||
|
||||
.. _external_connection_poolers:
|
||||
|
||||
|
||||
+119
-69
@@ -8,24 +8,25 @@ PostgREST error messages follow the PostgreSQL error structure. It includes ``ME
|
||||
Errors from PostgreSQL
|
||||
======================
|
||||
|
||||
PostgREST will forward errors coming from PostgreSQL. For instance, when querying a nonexistent table:
|
||||
PostgREST will forward errors coming from PostgreSQL. For instance, on a failed constraint:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET /nonexistent_table?id=eq.1 HTTP/1.1
|
||||
POST /projects HTTP/1.1
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 404 Not Found
|
||||
HTTP/1.1 400 Bad Request
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
|
||||
{
|
||||
"hint": null,
|
||||
"details": null,
|
||||
"code": "42P01",
|
||||
"message": "relation \"api.nonexistent_table\" does not exist"
|
||||
"code": "23502",
|
||||
"details": "Failing row contains (null, foo, null).",
|
||||
"hint": null,
|
||||
"message": "null value in column \"id\" of relation \"projects\" violates not-null constraint"
|
||||
}
|
||||
|
||||
.. _status_codes:
|
||||
@@ -90,72 +91,18 @@ PostgREST translates `PostgreSQL error codes <https://www.postgresql.org/docs/cu
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 42P01 | 404 | undefined table |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 42P17 | 500 | infinite recursion |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 42501 | | if authenticated 403, | insufficient privileges |
|
||||
| | | else 401 | |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| other | 400 | |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
|
||||
.. _raise_error:
|
||||
|
||||
RAISE errors with HTTP Status Codes
|
||||
-----------------------------------
|
||||
|
||||
You can return custom HTTP status codes by raising SQL exceptions inside :ref:`functions <s_procs>`. For instance, here's a saucy function that always responds with an error:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
CREATE OR REPLACE FUNCTION just_fail() RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'I refuse!'
|
||||
USING DETAIL = 'Pretty simple',
|
||||
HINT = 'There is nothing you can do.';
|
||||
END
|
||||
$$;
|
||||
|
||||
Calling the function returns HTTP 400 with the body
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"message":"I refuse!",
|
||||
"details":"Pretty simple",
|
||||
"hint":"There is nothing you can do.",
|
||||
"code":"P0001"
|
||||
}
|
||||
|
||||
One way to customize the HTTP status code is by raising particular exceptions according to the PostgREST :ref:`error to status code mapping <status_codes>`. For example, :code:`RAISE insufficient_privilege` will respond with HTTP 401/403 as appropriate.
|
||||
|
||||
For even greater control of the HTTP status code, raise an exception of the ``PTxyz`` type. For instance to respond with HTTP 402, raise ``PT402``:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
RAISE sqlstate 'PT402' using
|
||||
message = 'Payment Required',
|
||||
detail = 'Quota exceeded',
|
||||
hint = 'Upgrade your plan';
|
||||
|
||||
Returns:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 402 Payment Required
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
{
|
||||
"message": "Payment Required",
|
||||
"details": "Quota exceeded",
|
||||
"hint": "Upgrade your plan",
|
||||
"code": "PT402"
|
||||
}
|
||||
|
||||
|
||||
Errors from PostgREST
|
||||
=====================
|
||||
|
||||
Errors that come from PostgREST itself maintain the same structure. But differ in the ``PGRST`` prefix in the ``code`` field. For instance, when querying a function that does not exist in the :doc:`schema cache <schema_cache>`:
|
||||
Errors that come from PostgREST itself maintain the same structure but differ in the ``PGRST`` prefix in the ``code`` field. For instance, when querying a function that does not exist in the :doc:`schema cache <schema_cache>`:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
@@ -181,7 +128,7 @@ Errors that come from PostgREST itself maintain the same structure. But differ i
|
||||
PostgREST Error Codes
|
||||
---------------------
|
||||
|
||||
PostgREST error codes have the form ``PGRSTgxx``
|
||||
PostgREST error codes have the form ``PGRSTgxx``.
|
||||
|
||||
- ``PGRST`` is the prefix that differentiates the error from a PostgreSQL error.
|
||||
- ``g`` is the error group
|
||||
@@ -272,10 +219,6 @@ Related to the HTTP request elements.
|
||||
| | | See :ref:`guc_resp_status`. |
|
||||
| PGRST112 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst113: | 406 | More than one column was returned for a scalar result. |
|
||||
| | | See :ref:`scalar_return_formats`. |
|
||||
| PGRST113 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst114: | 400 | For an :ref:`UPSERT using PUT <upsert_put>`, when |
|
||||
| | | :ref:`limits and offsets <limits>` are used. |
|
||||
| PGRST114 | | |
|
||||
@@ -304,6 +247,14 @@ Related to the HTTP request elements.
|
||||
| | | ``is.null`` or ``not.is.null`` :ref:`operators <operators>`.|
|
||||
| PGRST120 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst121: | 400 | PostgREST can't parse the JSON objects in RAISE |
|
||||
| | | ``PGRST`` error. See :ref:`raise headers <raise_headers>`. |
|
||||
| PGRST121 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst122: | 400 | Invalid preferences found in ``Prefer`` header with |
|
||||
| | | ``Prefer: handling=strict``. See :ref:`prefer_handling`. |
|
||||
| PGRST122 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
|
||||
.. _pgrst2**:
|
||||
|
||||
@@ -377,3 +328,102 @@ Internal errors. If you encounter any of these, you may have stumbled on a Postg
|
||||
| | | to the database. |
|
||||
| PGRSTX00 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
|
||||
Custom Errors
|
||||
=============
|
||||
|
||||
You can customize the errors by using the `RAISE statement <https://www.postgresql.org/docs/current/plpgsql-errors-and-messages.html#PLPGSQL-STATEMENTS-RAISE>`_ on functions.
|
||||
|
||||
.. _raise_error:
|
||||
|
||||
RAISE errors with HTTP Status Codes
|
||||
-----------------------------------
|
||||
|
||||
Custom status codes can be done by raising SQL exceptions inside :ref:`functions <s_procs>`. For instance, here's a saucy function that always responds with an error:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
CREATE OR REPLACE FUNCTION just_fail() RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'I refuse!'
|
||||
USING DETAIL = 'Pretty simple',
|
||||
HINT = 'There is nothing you can do.';
|
||||
END
|
||||
$$;
|
||||
|
||||
Calling the function returns HTTP 400 with the body
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"message":"I refuse!",
|
||||
"details":"Pretty simple",
|
||||
"hint":"There is nothing you can do.",
|
||||
"code":"P0001"
|
||||
}
|
||||
|
||||
One way to customize the HTTP status code is by raising particular exceptions according to the PostgREST :ref:`error to status code mapping <status_codes>`. For example, :code:`RAISE insufficient_privilege` will respond with HTTP 401/403 as appropriate.
|
||||
|
||||
For even greater control of the HTTP status code, raise an exception of the ``PTxyz`` type. For instance to respond with HTTP 402, raise ``PT402``:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
RAISE sqlstate 'PT402' using
|
||||
message = 'Payment Required',
|
||||
detail = 'Quota exceeded',
|
||||
hint = 'Upgrade your plan';
|
||||
|
||||
Returns:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 402 Payment Required
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
{
|
||||
"message": "Payment Required",
|
||||
"details": "Quota exceeded",
|
||||
"hint": "Upgrade your plan",
|
||||
"code": "PT402"
|
||||
}
|
||||
|
||||
.. _raise_headers:
|
||||
|
||||
Add HTTP Headers with RAISE
|
||||
---------------------------
|
||||
|
||||
For full control over headers and status you can raise a ``PGRST`` SQLSTATE error. You can achieve this by adding the ``code``, ``message``, ``detail`` and ``hint`` in the postgresql error message field as a JSON object. Here, the ``details`` and ``hint`` are optional. Similarly, the ``status`` and ``headers`` must be added to the SQL error detail field as a JSON object. For instance:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
RAISE sqlstate 'PGRST' USING
|
||||
message = '{"code":"123","message":"Payment Required","details":"Quota exceeded","hint":"Upgrade your plan"}',
|
||||
detail = '{"status":402,"headers":{"X-Powered-By":"Nerd Rage"}}';
|
||||
|
||||
Returns:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 402 Payment Required
|
||||
Content-Type: application/json; charset=utf-8
|
||||
X-Powered-By: Nerd Rage
|
||||
|
||||
{
|
||||
"message": "Payment Required",
|
||||
"details": "Quota exceeded",
|
||||
"hint": "Upgrade your plan",
|
||||
"code": "123"
|
||||
}
|
||||
|
||||
|
||||
For non standard HTTP status, you can optionally add ``status_text`` to describe the status code. For status code ``419`` the detail field may look like this:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
detail = '{"status":419,"status_text":"Page Expired","headers":{"X-Powered-By":"Nerd Rage"}}';
|
||||
|
||||
If PostgREST can't parse the JSON objects ``message`` and ``detail``, it will throw a ``PGRST121`` error. See :ref:`Errors from PostgREST<pgrst1**>`.
|
||||
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ Some PostgREST features need metadata from the database schema. Getting this met
|
||||
Stale Schema Cache
|
||||
------------------
|
||||
|
||||
One operational problem that comes a cache is that it can go stale. This can happen for PostgREST when you make changes to the metadata before mentioned. Requests that depend on the metadata will fail.
|
||||
One operational problem that comes with a cache is that it can go stale. This can happen for PostgREST when you make changes to the metadata before mentioned. Requests that depend on the metadata will fail.
|
||||
|
||||
You can solve this by reloading the cache manually or automatically.
|
||||
|
||||
|
||||
@@ -7,16 +7,48 @@ After :ref:`user_impersonation`, every request to an :doc:`API resource <api>` r
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
BEGIN; -- <Access Mode> <Isolation Level>
|
||||
START TRANSACTION; -- <Access Mode> <Isolation Level>
|
||||
-- <Transaction-scoped settings>
|
||||
-- <Main Query>;
|
||||
END;
|
||||
-- <Main Query>
|
||||
END; -- <Transaction End>
|
||||
|
||||
.. _access_mode:
|
||||
|
||||
Access Mode
|
||||
-----------
|
||||
|
||||
The access mode determines whether the transaction can modify the database or not. There are 2 possible values: READ ONLY and READ WRITE.
|
||||
|
||||
Modifying the database inside READ ONLY transactions is not possible. PostgREST uses this fact to enforce HTTP semantics in GET and HEAD requests. Consider the following:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
CREATE SEQUENCE callcounter_count START 1;
|
||||
|
||||
CREATE VIEW callcounter AS
|
||||
SELECT nextval('callcounter_count');
|
||||
|
||||
Since the ``callcounter`` view modifies the sequence, calling it with GET or HEAD will result in an error:
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. code-tab:: http
|
||||
|
||||
GET /callcounter HTTP/1.1
|
||||
|
||||
.. code-tab:: bash Curl
|
||||
|
||||
curl "http://localhost:3000/callcounter"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 405 Method Not Allowed
|
||||
|
||||
{"code":"25006","details":null,"hint":null,"message":"cannot execute nextval() in a read-only transaction"}
|
||||
|
||||
Access Mode on Tables and Views
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The access mode on :ref:`tables_views` is determined by the HTTP method.
|
||||
|
||||
.. list-table::
|
||||
@@ -29,6 +61,9 @@ The access mode on :ref:`tables_views` is determined by the HTTP method.
|
||||
* - POST, PATCH, PUT, DELETE
|
||||
- READ WRITE
|
||||
|
||||
Access Mode on Functions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:ref:`s_procs` additionally depend on the function `volatility <https://www.postgresql.org/docs/current/xfunc-volatility.html>`_.
|
||||
|
||||
.. list-table::
|
||||
@@ -51,13 +86,10 @@ The access mode on :ref:`tables_views` is determined by the HTTP method.
|
||||
- READ ONLY
|
||||
- READ ONLY
|
||||
|
||||
Modifying the database inside READ ONLY transactions is not possible. PostgREST uses this fact to enforce HTTP semantics in GET and HEAD requests.
|
||||
|
||||
.. note::
|
||||
|
||||
The volatility marker is a promise about the behavior of the function. PostgreSQL will let you mark a function that modifies the database as ``IMMUTABLE`` or ``STABLE`` without failure. But, because of the READ ONLY transaction the function will fail under PostgREST.
|
||||
|
||||
The :ref:`options_requests` method doesn't start a transaction, so it's not relevant here.
|
||||
- The volatility marker is a promise about the behavior of the function. PostgreSQL will let you mark a function that modifies the database as ``IMMUTABLE`` or ``STABLE`` without failure. But, because of the READ ONLY transaction the function will fail under PostgREST.
|
||||
- The :ref:`options_requests` method doesn't start a transaction, so it's not relevant here.
|
||||
|
||||
.. _isolation_lvl:
|
||||
|
||||
@@ -66,12 +98,12 @@ Isolation Level
|
||||
|
||||
Every transaction uses the PostgreSQL default isolation level: READ COMMITTED. Unless you modify `default_transaction_isolation <https://www.postgresql.org/docs/15/runtime-config-client.html#GUC-DEFAULT-TRANSACTION-ISOLATION>`_ for an impersonated role or function.
|
||||
|
||||
Using :ref:`impersonated_settings`, change the isolation level for all the role's requests with:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
ALTER ROLE webuser SET default_transaction_isolation TO 'repeatable read';
|
||||
|
||||
Every ``webuser`` gets its queries executed with ``default_transaction_isolation`` set to REPEATABLE READ.
|
||||
|
||||
Or to change the isolation level per function call.
|
||||
|
||||
.. code-block:: postgresql
|
||||
@@ -113,10 +145,6 @@ Request Headers, Cookies and JWT claims
|
||||
|
||||
PostgREST stores the headers, cookies and headers as JSON. To get them:
|
||||
|
||||
.. important::
|
||||
|
||||
The headers names are lowercased. e.g. If the request sends ``User-Agent: x`` this will be obtainable as ``current_setting('request.headers', true)::json->>'user-agent'``.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
-- To get all the headers sent in the request
|
||||
@@ -131,45 +159,21 @@ PostgREST stores the headers, cookies and headers as JSON. To get them:
|
||||
-- value of the email claim in a jwt
|
||||
SELECT current_setting('request.jwt.claims', true)::json->>'email';
|
||||
|
||||
.. note::
|
||||
.. important::
|
||||
|
||||
The ``role`` in ``request.jwt.claims`` defaults to the value of :ref:`db-anon-role`.
|
||||
- The headers names are lowercased. e.g. If the request sends ``User-Agent: x`` this will be obtainable as ``current_setting('request.headers', true)::json->>'user-agent'``.
|
||||
- The ``role`` in ``request.jwt.claims`` defaults to the value of :ref:`db-anon-role`.
|
||||
- Settings don't become NULL after the transaction is committed, instead they're set to a an empty string ``''``.
|
||||
|
||||
.. _guc_legacy_names:
|
||||
+ This is considered expected behavior by PostgreSQL. For more details, see `this discussion <https://www.postgresql.org/message-id/flat/CAB_pDVVa84w7hXhzvyuMTb8f5kKV3bee_p9QTZZ58Rg7zYM7sw%40mail.gmail.com>`_.
|
||||
+ To avoid this inconsistency, you can create a wrapper function like:
|
||||
|
||||
Legacy settings
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
For PostgreSQL versions below 14, PostgREST will take into consideration the :ref:`db-use-legacy-gucs` config, which is set to true by default.
|
||||
This means that the interface for accessing these GUCs is the same as in older versions (see below).
|
||||
You can opt in to use the JSON GUCs mentioned above by setting the ``db-use-legacy-gucs`` to false.
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<p>
|
||||
<details>
|
||||
<summary>Old GUCs</summary>
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
-- To read the value of the User-Agent request header:
|
||||
SELECT current_setting('request.header.user-agent', true);
|
||||
|
||||
-- To read the value of sessionId in a cookie:
|
||||
SELECT current_setting('request.cookie.sessionId', true);
|
||||
|
||||
-- To read the value of the email claim in a jwt:
|
||||
SELECT current_setting('request.jwt.claim.email', true);
|
||||
|
||||
.. note::
|
||||
|
||||
``request.jwt.claim.role`` defaults to the value of :ref:`db-anon-role`.
|
||||
|
||||
.. raw:: html
|
||||
|
||||
</details>
|
||||
</p>
|
||||
.. code-block:: postgresql
|
||||
|
||||
CREATE FUNCTION my_current_setting(text) RETURNS text
|
||||
LANGUAGE SQL AS $$
|
||||
SELECT nullif(current_setting($1, true), '');
|
||||
$$;
|
||||
|
||||
.. _guc_req_path_method:
|
||||
|
||||
@@ -223,7 +227,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 overriden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you also set :ref:`raw-media-types` to something like ``text/html``.
|
||||
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:
|
||||
|
||||
@@ -263,12 +267,62 @@ You can set the ``response.status`` to override the default status code PostgRES
|
||||
|
||||
If the status code is standard, PostgREST will complete the status message(**I'm a teapot** in this example).
|
||||
|
||||
.. _impersonated_settings:
|
||||
|
||||
Impersonated Role Settings
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PostgreSQL applies the connection role (:ref:`authenticator <roles>`) settings. Additionally, PostgREST applies the :ref:`impersonated roles <user_impersonation>` settings as transaction-scoped settings.
|
||||
This allows finer-grained control over actions made by a role.
|
||||
|
||||
For example, consider `statement_timeout <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT>`__. It allows you to abort any statement that takes more than a specified time. It is disabled by default.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
ALTER ROLE authenticator SET statement_timeout TO '10s';
|
||||
ALTER ROLE anonymous SET statement_timeout TO '1s';
|
||||
|
||||
With the above settings, all users get a global statement timeout of 10 seconds and :ref:`anonymous <roles>` users get a timeout of 1 second.
|
||||
|
||||
Settings with privileged context
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Settings that have a context which requires privileges won't be applied by default. This is so we don't cause permission errors.
|
||||
For more details see `Understanding Postgres Parameter Context <https://www.enterprisedb.com/blog/understanding-postgres-parameter-context>`_.
|
||||
|
||||
However, starting from PostgreSQL 15, you can grant privileges for these settings with:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
GRANT SET ON PARAMETER <setting> TO <authenticator>;
|
||||
|
||||
Function Settings
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
In addition to :ref:`impersonated_settings`, PostgREST will also apply function settings as transaction-scoped settings. This allows functions settings to override
|
||||
the impersonated and connection role settings.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
CREATE OR REPLACE FUNCTION myfunc()
|
||||
RETURNS void as $$
|
||||
SELECT pg_sleep(3); -- simulating some long-running process
|
||||
$$
|
||||
LANGUAGE SQL
|
||||
SET statement_timeout TO '4s';
|
||||
|
||||
When calling the above function (see :ref:`s_procs`), the statement timeout will be 4 seconds.
|
||||
|
||||
.. note::
|
||||
|
||||
Currently, only ``statement_timeout`` is applied for functions.
|
||||
|
||||
.. _main_query:
|
||||
|
||||
Main query
|
||||
----------
|
||||
|
||||
The main query is generated by requesting :ref:`tables_views` or :ref:`s_procs`. All generated queries use prepared statements(:ref:`db-prepared-statements`).
|
||||
The main query is generated by requesting :ref:`tables_views` or :ref:`s_procs`. All generated queries use prepared statements (:ref:`db-prepared-statements`).
|
||||
|
||||
Transaction End
|
||||
---------------
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
|
||||
9.0.0
|
||||
=====
|
||||
|
||||
This major version is released with PostgreSQL 14 compatibility and is accompanied with new features and bug fixes. You can look at the detailed changelog and download the pre-compiled binaries on the `GitHub release page <https://github.com/PostgREST/postgrest/releases/tag/v9.0.0>`_.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
PostgreSQL 14 compatibility
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PostgreSQL 14 Beta 1 tightened its GUC naming scheme making it impossible to use multiple dots (``.``) and dashes (``-``) on custom GUC parameters, this caused our :ref:`old HTTP Context <guc_legacy_names>` to fail across all requests. Thankfully, `@robertsosinski <https://github.com/robertsosinski>`_ got the PostgreSQL team to reconsider allowing multiple dots in the GUC name, allowing us to avoid a major breaking change. You can see the full discussion `here <https://www.postgresql.org/message-id/17045-6a4a9f0d1513f72b%40postgresql.org>`_.
|
||||
|
||||
Still, dashes cannot be used on PostgreSQL 14 custom GUC parameters, so we changed our HTTP Context :ref:`to namespace using a mix of dots and JSON <guc_req_headers_cookies_claims>`. On older PostgreSQL versions we still use the settings legacy names. If you wish to use the new JSON GUCs on these versions, set the :ref:`db-use-legacy-gucs` config option to false.
|
||||
|
||||
Resource Embedding with Top-level Filtering
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Historically, Resource Embedding was always done with a query that included the equivalent of a ``LEFT JOIN``, which meant you could not
|
||||
exclude any of the top-level resource rows. You can now use :ref:`embedding_top_level_filter` to do the equivalent of an ``INNER JOIN``, thus you can filter the top-level resource rows with any of the available operators.
|
||||
|
||||
Partitioned Tables
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Partitioned tables now integrate with all the feature set. You can :ref:`embed partitioned tables <embedding_partitioned_tables>`, UPSERT, INSERT(with a correctly generated Location header) and make OPTIONS requests on them. They're also included in the generated OpenAPI.
|
||||
|
||||
Functions(RPC)
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
* Functions with a :ref:`single unnamed parameter <s_proc_single_unnamed>` can now be used to POST raw ``bytea``, ``text`` or ``json/jsonb``.
|
||||
|
||||
Horizontal Filtering
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* The ``unknown`` value for three-valued logic can now be used on the ``is`` :ref:`operator <operators>`.
|
||||
|
||||
* Escaping double quotes(``"``) in double-quoted surrounded strings is now possible by using backslashes, e.g. ``?col=in.("Double\"Quote")``. Backslashes can be escaped with a preceding backslash, e.g. ``?col=in.("Back\\slash")``. See :ref:`reserved-chars`.
|
||||
|
||||
Administration
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
* A ``Retry-After`` header is now added when PostgREST is doing :ref:`automatic_recovery`.
|
||||
|
||||
Error messages
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
* :ref:`embed_disamb` now shows an improved error message that includes relevant hints for clearing out the ambiguous embedding.
|
||||
|
||||
Documentation improvements
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Added ``curl`` snippets to the :ref:`api` page.
|
||||
|
||||
* Added the :ref:`automatic_recovery` section.
|
||||
|
||||
* Added the :ref:`nested_embedding` section.
|
||||
|
||||
* Added the :ref:`logical_operators` section.
|
||||
|
||||
* Added the :ref:`templates` and :ref:`devops` sections to the :doc:`Ecosystem </ecosystem>`.
|
||||
|
||||
Bug fixes
|
||||
---------
|
||||
|
||||
* Correct RPC return type handling for RETURNS TABLE with a single column (`#1930 <https://github.com/PostgREST/postgrest/issues/1930>`_).
|
||||
|
||||
* Schema Cache query failing with ``standard_conforming_strings = off`` (`#1992 <https://github.com/PostgREST/postgrest/issues/1992>`_).
|
||||
|
||||
* OpenAPI missing default values for String types (`#1871 <https://github.com/PostgREST/postgrest/issues/1871>`_).
|
||||
|
||||
Breaking changes
|
||||
----------------
|
||||
|
||||
* Dropped support for PostgreSQL 9.5 as it already reached its end-of-life according to `PostgreSQL versioning policy <https://www.postgresql.org/support/versioning/>`_.
|
||||
|
||||
* Partitions of a `partitioned table <https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE>`_ are no longer included in the :ref:`schema_cache`. This is so errors are not generated when doing resource embedding on partitioned tables.
|
||||
|
||||
* Dropped support for doing :ref:`hint_disamb` using dots instead of exclamation marks, e.g. doing ``select=*,projects.client_id(*)`` instead of ``select=*,projects!client_id(*)``). Using dots was undocumented and deprecated back in `v6.0.2 <https://github.com/PostgREST/postgrest/releases/tag/v6.0.2>`_.
|
||||
|
||||
Thanks
|
||||
------
|
||||
|
||||
Big thanks from the `PostgREST team <https://github.com/orgs/PostgREST/people>`_ to our sponsors!
|
||||
|
||||
.. container:: image-container
|
||||
|
||||
.. image:: ../_static/cybertec-new.png
|
||||
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/2ndquadrant.png
|
||||
:target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/retool.png
|
||||
:target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/gnuhost.png
|
||||
:target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/supabase.png
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/oblivious.jpg
|
||||
:target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
* Evans Fernandes
|
||||
* Jan Sommer
|
||||
* `Franz Gusenbauer <https://www.igutech.at/>`_
|
||||
* `Daniel Babiak <https://github.com/dbabiak>`_
|
||||
* Tsingson Qin
|
||||
* Michel Pelletier
|
||||
* Jay Hannah
|
||||
* Robert Stolarz
|
||||
* Nicholas DiBiase
|
||||
* Christopher Reid
|
||||
* Nathan Bouscal
|
||||
* Daniel Rafaj
|
||||
* David Fenko
|
||||
* Remo Rechkemmer
|
||||
* Severin Ibarluzea
|
||||
* Tom Saleeba
|
||||
* Pawel Tyll
|
||||
|
||||
If you like to join them please consider `supporting PostgREST development <https://github.com/PostgREST/postgrest#user-content-supporting-development>`_.
|
||||
@@ -1,89 +0,0 @@
|
||||
|
||||
9.0.1
|
||||
=====
|
||||
|
||||
This version includes important fixes for production environments and other miscellaneous fixes. You can download the pre-compiled binaries on the `GitHub release page <https://github.com/PostgREST/postgrest/releases/tag/v9.0.1>`_.
|
||||
|
||||
Bug Fixes
|
||||
---------
|
||||
|
||||
* Keep working when ``EMFILE (Too many open files)`` is reached. (`#2042 <https://github.com/PostgREST/postgrest/issues/2042>`_)
|
||||
|
||||
* Disable parallel GC for better performance on higher core CPUs (`#2294 <https://github.com/PostgREST/postgrest/issues/2294>`_). Thanks to `NoRedInk for their blog post <https://blog.noredink.com/post/666654908557180928/tuning-haskell-rts-for-kubernetes-part-2>`_ that lead us to this fix.
|
||||
|
||||
* Fix using CPU while idle. (`#1076 <https://github.com/PostgREST/postgrest/issues/1076>`_)
|
||||
|
||||
* Fix reading database configuration properly when ``=`` is present in the value. (`#2120 <https://github.com/PostgREST/postgrest/issues/2120>`_)
|
||||
|
||||
* Fix ``is`` not working with upper or mixed case values like ``NULL``, ``TrUe``, ``FaLsE``. (`#2077 <https://github.com/PostgREST/postgrest/issues/2077>`_)
|
||||
|
||||
* Execute deferred constraint triggers when using ``Prefer: tx=rollback``. (`#2020 <https://github.com/PostgREST/postgrest/issues/2020>`_)
|
||||
|
||||
* Ignore ``Content-Type`` headers for ``GET`` requests when calling RPCs. (`#2147 <https://github.com/PostgREST/postgrest/issues/2147>`_)
|
||||
|
||||
* Previously, ``GET`` without parameters, but with ``Content-Type: text/plain`` or ``Content-Type: application/octet-stream`` would fail with ``404 Not Found``, even if a function without arguments was available.
|
||||
|
||||
* Fix wrong CORS header from ``Authentication`` to ``Authorization``. (`#1724 <https://github.com/PostgREST/postgrest/issues/1724>`_)
|
||||
|
||||
* Fix ``json`` and ``jsonb`` columns showing a type in OpenAPI spec. (`#2165 <https://github.com/PostgREST/postgrest/issues/2165>`_)
|
||||
|
||||
* Remove trigger functions from the schema cache and OpenAPI output, because they can't be called directly anyway. (`#2135 <https://github.com/PostgREST/postgrest/issues/2135>`_)
|
||||
|
||||
* Remove aggregates, procedures and window functions from the schema cache and OpenAPI output. (`#2101 <https://github.com/PostgREST/postgrest/issues/2101>`_)
|
||||
|
||||
* Fix schema cache loading when views with ``XMLTABLE`` and ``DEFAULT`` are present. (`#2024 <https://github.com/PostgREST/postgrest/issues/2024>`_)
|
||||
|
||||
* Fix ``--dump-schema`` running with a wrong PG version. (`#2153 <https://github.com/PostgREST/postgrest/issues/2153>`_)
|
||||
|
||||
* Fix misleading disambiguation error where the content of the ``relationship`` key looks like valid syntax. (`#2239 <https://github.com/PostgREST/postgrest/issues/2239>`_)
|
||||
|
||||
Thanks
|
||||
------
|
||||
|
||||
Big thanks from the `PostgREST team <https://github.com/orgs/PostgREST/people>`_ to our sponsors!
|
||||
|
||||
.. container:: image-container
|
||||
|
||||
.. image:: ../_static/cybertec-new.png
|
||||
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/2ndquadrant.png
|
||||
:target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/retool.png
|
||||
:target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/gnuhost.png
|
||||
:target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/supabase.png
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/oblivious.jpg
|
||||
:target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
* Evans Fernandes
|
||||
* Jan Sommer
|
||||
* `Franz Gusenbauer <https://www.igutech.at/>`_
|
||||
* `Daniel Babiak <https://github.com/dbabiak>`_
|
||||
* Tsingson Qin
|
||||
* Michel Pelletier
|
||||
* Jay Hannah
|
||||
* Robert Stolarz
|
||||
* Nicholas DiBiase
|
||||
* Christopher Reid
|
||||
* Nathan Bouscal
|
||||
* Daniel Rafaj
|
||||
* David Fenko
|
||||
* Remo Rechkemmer
|
||||
* Severin Ibarluzea
|
||||
* Tom Saleeba
|
||||
* Pawel Tyll
|
||||
|
||||
If you like to join them please consider `supporting PostgREST development <https://github.com/PostgREST/postgrest#user-content-supporting-development>`_.
|
||||
@@ -1,227 +0,0 @@
|
||||
|
||||
10.0.0
|
||||
======
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
XML/SOAP support for RPC
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
RPC now understands the ``text/xml`` media type, allowing SQL functions to send XML output(``Accept: text/xml``) and receive XML input(``Content-Type: text/xml``). This makes SOAP endpoints possible, check the :ref:`create_soap_endpoint` how-to and the :ref:`scalar_return_formats` reference for more details.
|
||||
|
||||
GeoJSON support
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
GeoJSON is supported across the board(reads, writes, RPC) with the ``Accept: application/geo+json`` header, this depends on PostGIS from the versions 3.0.0 and up. The :ref:`working with PostGIS section <ww_postgis>` has an example to get you started.
|
||||
|
||||
Execution Plan
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
The :ref:`execution plan <explain_plan>` of a request is now obtainable with the ``Accept: application/vnd.pgrst.plan`` header. The result can be in ``text`` or ``json`` formats and is compatible with EXPLAIN vizualizers like `explain.depesz.com <https://explain.depesz.com>`_ or `explain.dalibo.com <https://explain.dalibo.com>`_.
|
||||
|
||||
Resource Embedding
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- A :ref:`one-to-one relationship <one-to-one>` is now detected when a foreign key is unique.
|
||||
|
||||
- Using :ref:`computed_relationships`, you can add custom relationships or override automatically detected ones. This makes :ref:`resource_embedding` possible on Foreign Data Wrappers and complex SQL views.
|
||||
|
||||
Horizontal/Vertical Filtering
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- :ref:`Accessing fields of a Composite type or elements of an Array type <composite_array_columns>` is now possible with the arrow operators(``->``, ``->>``) in the same way you would access a JSON type fields.
|
||||
|
||||
- :ref:`pattern_matching` operators for `POSIX regular expressions <https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-POSIX-REGEXP>`_ are now available: ``match`` and ``imatch``, equivalent in PostgreSQL to ``~`` and ``~*`` respectively.
|
||||
|
||||
Insertions/Updates
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- ``limit`` can now affect the number of updated/deleted rows. See :ref:`limited_update_delete`.
|
||||
|
||||
OpenAPI
|
||||
~~~~~~~
|
||||
|
||||
You can now activate the "Authorize" button in SwaggerUI by enabling the :ref:`openapi-security-active` configuration. Add your JWT token prepending :code:`Bearer` to it and you'll be able to request protected resources.
|
||||
|
||||
Administration
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
- Two :ref:`health check endpoints <health_check>` are now exposed in a secondary port.
|
||||
|
||||
- :ref:`pgrst_logging` now shows the database user.
|
||||
|
||||
- It is now possible to execute PostgREST without specifying any configuration variable. The three that were mandatory on the previous versions, are no longer so.
|
||||
|
||||
- If :ref:`db-uri` is not set, PostgREST will use the `libpq environment variables <https://www.postgresql.org/docs/current/libpq-envars.html>`_ for the database connection.
|
||||
- If :ref:`db-schemas` is not set, it will use the database ``public`` schema.
|
||||
- If :ref:`db-anon-role` is not set, it will not allow anonymous requests.
|
||||
|
||||
Error messages
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
- To increase consistency, all the errors messages are now normalized. The ``hint``, ``details``, ``code`` and ``message`` fields will always be present in the body, each one defaulting to a ``null`` value. In the same way, the :ref:`errors that were raised <raise_error>` with ``SQLSTATE`` now include the ``message`` and ``code`` in the body.
|
||||
|
||||
- To further clarify the source of an error, we now add a ``PGRST`` prefix to the error code of all the errors that are PostgREST-specific and don't come from the database. These errors have unique codes that identify them and are documented in the :ref:`pgrst_errors` section.
|
||||
|
||||
Documentation improvements
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Added a :doc:`/how-tos/working-with-postgresql-data-types` how-to, which contains explanations and examples on how to work with different PostgreSQL data types such as timestamps, ranges or PostGIS types, among others.
|
||||
|
||||
* Added in-database and environment variable settings for each :ref:`configuration variable <config_full_list>`.
|
||||
|
||||
* Added the :ref:`file_descriptors` subsection.
|
||||
|
||||
* Added a reference page for :ref:`error_source`.
|
||||
|
||||
* Moved the :ref:`error_source` and the :ref:`status_codes` sections to the :ref:`errors reference page <error_source>`.
|
||||
|
||||
* Moved the *Casting type to custom JSON* how-to to the :ref:`casting_range_to_json` subsection.
|
||||
|
||||
* Removed direct links for PostgREST versions older than 8.0 from the versions menu.
|
||||
|
||||
* Removed the *Embedding table from another schema* how-to.
|
||||
|
||||
* Restructured the :ref:`resource_embedding` section:
|
||||
|
||||
- Added a :ref:`one-to-many` and :ref:`many-to-one` subsections.
|
||||
|
||||
- Renamed the *Embedding through join tables* subsection to :ref:`many-to-many`.
|
||||
|
||||
* Split up the *Insertions/Updates* section into :ref:`insert` and :ref:`update`.
|
||||
|
||||
Breaking changes
|
||||
----------------
|
||||
|
||||
* Many-to-many relationships now require that foreign key columns be part of the join table composite key
|
||||
|
||||
- This was needed to reduce :ref:`embed_disamb` errors in complex schemas(`#2070 <https://github.com/PostgREST/postgrest/issues/2070>`_).
|
||||
|
||||
- For migrating to this version, the less invasive method is to use :ref:`computed_relationships` to replace the previous many-to-many relationships.
|
||||
|
||||
- Otherwise you can change your join table primary key. For example with ``alter table permission_user drop constraint permission_user_pkey, add primary key (id, user_id, permission_id);``
|
||||
|
||||
* Views now are not detected when embedding using :ref:`target_disamb`.
|
||||
|
||||
- This embedding form was easily made ambiguous whenever a new view was added(`#2277 <https://github.com/PostgREST/postgrest/issues/2277>`_).
|
||||
|
||||
- For migrating to this version, you can use :ref:`computed_relationships` to replace the previous view relationships.
|
||||
|
||||
- :ref:`hint_disamb` works as usual on views.
|
||||
|
||||
* ``limit/offset`` now limits the affected rows on ``UPDATE``/``DELETE``
|
||||
|
||||
- Previously, ``limit``/``offset`` only limited the returned rows but not the actual updated rows(`#2156 <https://github.com/PostgREST/postgrest/issues/2156>`_)
|
||||
|
||||
* ``max-rows`` is no longer applied on ``POST``, ``PATCH``, ``PUT`` and ``DELETE`` returned rows
|
||||
|
||||
- This was misleading because the affected rows were not really affected by ``max-rows``, only the returned rows were limited(`#2155 <https://github.com/PostgREST/postgrest/issues/2155>`_)
|
||||
|
||||
* Return ``204 No Content`` without ``Content-Type`` for RPCs returning ``VOID``
|
||||
|
||||
- Previously, those RPCs would return ``null`` as a body with ``Content-Type: application/json`` (`#2001 <https://github.com/PostgREST/postgrest/issues/2001>`_).
|
||||
|
||||
* Using ``Prefer: return=representation`` no longer returns a ``Location`` header
|
||||
|
||||
- This reduces unnecessary computing for all insertions (`#2312 <https://github.com/PostgREST/postgrest/issues/2312>`_)
|
||||
|
||||
Bug fixes
|
||||
---------
|
||||
|
||||
* Return ``204 No Content`` without ``Content-Type`` for ``PUT`` (`#2058 <https://github.com/PostgREST/postgrest/issues/2058>`_)
|
||||
|
||||
* Clarify error for failed schema cache load. (`#2107 <https://github.com/PostgREST/postgrest/issues/2107>`_)
|
||||
|
||||
- From ``Database connection lost. Retrying the connection`` to ``Could not query the database for the schema cache. Retrying.``
|
||||
|
||||
* Fix silently ignoring filter on a non-existent embedded resource (`#1771 <https://github.com/PostgREST/postgrest/issues/1771>`_)
|
||||
|
||||
* Remove functions, which are not callable due to unnamed arguments, from schema cache and OpenAPI output. (`#2152 <https://github.com/PostgREST/postgrest/issues/2152>`_)
|
||||
|
||||
* Fix accessing JSON array fields with ``->`` and ``->>`` in ``?select=`` and ``?order=``. (`#2145 <https://github.com/PostgREST/postgrest/issues/2145>`_)
|
||||
|
||||
* Ignore ``max-rows`` on ``POST``, ``PATCH``, ``PUT`` and ``DELETE`` (`#2155 <https://github.com/PostgREST/postgrest/issues/2155>`_)
|
||||
|
||||
* Fix inferring a foreign key column as a primary key column on views (`#2254 <https://github.com/PostgREST/postgrest/issues/2254>`_)
|
||||
|
||||
* Restrict generated many-to-many relationships (`#2070 <https://github.com/PostgREST/postgrest/issues/2070>`_)
|
||||
|
||||
- Only adds many-to-many relationships when a table has foreign keys to two other tables and these foreign key columns are part of the table's primary key columns.
|
||||
|
||||
* Allow casting to types with underscores and numbers (e.g. ``select=oid_array::_int4``) (`#2278 <https://github.com/PostgREST/postgrest/issues/2278>`_)
|
||||
|
||||
* Prevent views from breaking one-to-many/many-to-one embeds when using column or foreign key as target (`#2277 <https://github.com/PostgREST/postgrest/issues/2277>`_, `#2238 <https://github.com/PostgREST/postgrest/issues/2238>`_, `#1643 <https://github.com/PostgREST/postgrest/issues/1643>`_)
|
||||
|
||||
- When using a column or foreign key as target for embedding (``/tbl?select=*,col-or-fk(*)``), only tables are now detected and views are not.
|
||||
|
||||
- You can still use a column or an inferred foreign key on a view to embed a table (``/view?select=*,col-or-fk(*)``)
|
||||
|
||||
* Increase the ``db-pool-timeout`` to 1 hour to prevent frequent high connection latency (`#2317 <https://github.com/PostgREST/postgrest/issues/2317>`_)
|
||||
|
||||
* The search path now correctly identifies schemas with uppercase and special characters in their names (regression) (`#2341 <https://github.com/PostgREST/postgrest/issues/2341>`_)
|
||||
|
||||
* "404 Not Found" on nested routes and "405 Method Not Allowed" errors no longer start an empty database transaction (`#2364 <https://github.com/PostgREST/postgrest/issues/2364>`_)
|
||||
|
||||
* Fix inaccurate result count when an inner embed was selected after a normal embed in the query string (`#2342 <https://github.com/PostgREST/postgrest/issues/2342>`_)
|
||||
|
||||
* ``OPTIONS`` requests no longer start an empty database transaction (`#2376 <https://github.com/PostgREST/postgrest/issues/2376>`_)
|
||||
|
||||
* Allow using columns with dollar sign ($) without double quoting in filters and ``select`` (`#2395 <https://github.com/PostgREST/postgrest/issues/2395>`_)
|
||||
|
||||
* Fix loop crash error on startup in PostgreSQL 15 beta 3. ``Log: "UNION types \"char\" and text cannot be matched."`` (`#2410 <https://github.com/PostgREST/postgrest/issues/2410>`_)
|
||||
|
||||
* Fix race conditions managing database connection helper (`#2397 <https://github.com/PostgREST/postgrest/issues/2397>`_)
|
||||
|
||||
* Allow ``limit=0`` in the request query to return an empty array (`#2269 <https://github.com/PostgREST/postgrest/issues/2269>`_)
|
||||
|
||||
Thanks
|
||||
------
|
||||
|
||||
Big thanks from the `PostgREST team <https://github.com/orgs/PostgREST/people>`_ to our sponsors!
|
||||
|
||||
.. container:: image-container
|
||||
|
||||
.. image:: ../_static/cybertec-new.png
|
||||
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/2ndquadrant.png
|
||||
:target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/retool.png
|
||||
:target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/gnuhost.png
|
||||
:target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/supabase.png
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/oblivious.jpg
|
||||
:target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
* Evans Fernandes
|
||||
* Jan Sommer
|
||||
* `Franz Gusenbauer <https://www.igutech.at/>`_
|
||||
* `Daniel Babiak <https://github.com/dbabiak>`_
|
||||
* Tsingson Qin
|
||||
* Michel Pelletier
|
||||
* Jay Hannah
|
||||
* Robert Stolarz
|
||||
* Nicholas DiBiase
|
||||
* Christopher Reid
|
||||
* Nathan Bouscal
|
||||
* Daniel Rafaj
|
||||
* David Fenko
|
||||
* Remo Rechkemmer
|
||||
* Severin Ibarluzea
|
||||
* Tom Saleeba
|
||||
* Pawel Tyll
|
||||
|
||||
If you like to join them please consider `supporting PostgREST development <https://github.com/PostgREST/postgrest#user-content-supporting-development>`_.
|
||||
@@ -1,153 +0,0 @@
|
||||
|
||||
10.2.0
|
||||
======
|
||||
|
||||
This minor version adds bug fixes and some features that provide stability to v10.0.0. These release notes include the changes added in versions `10.1.0 <https://github.com/PostgREST/postgrest/releases/tag/v10.1.0>`_, `10.1.1 <https://github.com/PostgREST/postgrest/releases/tag/v10.1.1>`_ and `10.1.2 <https://github.com/PostgREST/postgrest/releases/tag/v10.1.2>`_. You can look at the detailed changelog and download the pre-compiled binaries on the `GitHub release page <https://github.com/PostgREST/postgrest/releases/tag/v10.2.0>`_.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
Pool Connection Lifetime
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
To prevent memory leaks caused by long-lived connections, PostgREST limits their lifetime in the pool through :ref:`db-pool-max-lifetime`.
|
||||
|
||||
Pool Connection Acquisition Timeout
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
There is now a time limit to wait for pool connections to be acquired. If a new request cannot get a connection in the time specified in :ref:`db-pool-acquisition-timeout` then a response with a ``504`` status is returned.
|
||||
|
||||
Documentation improvements
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Added HTTP status codes to the :ref:`pgrst_errors`.
|
||||
|
||||
* Added a how-to on :ref:`sql-user-management-using-postgres-users-and-passwords`.
|
||||
|
||||
* Updated the :ref:`Heroku installation page <deploy_heroku>`.
|
||||
|
||||
Changes
|
||||
-------
|
||||
|
||||
* Removed ``db-pool-timeout`` option because it was removed in the ``hasql-pool`` library that PostgREST uses for SQL connections. (`#2444 <https://github.com/PostgREST/postgrest/issues/2444>`_)
|
||||
|
||||
Deprecated
|
||||
----------
|
||||
|
||||
* Deprecate bulk-calls when including the ``Prefer: params=multiple-objects`` in the request. It is preferable to use a function with an :ref:`array <s_procs_array>` or JSON parameter for a better performance. (`#1385 <https://github.com/PostgREST/postgrest/issues/1385>`_)
|
||||
|
||||
Bug fixes
|
||||
---------
|
||||
|
||||
* Reduce allocations communication with PostgreSQL, particularly for request bodies. (`#2261 <https://github.com/PostgREST/postgrest/issues/2261>`_, `#2349 <https://github.com/PostgREST/postgrest/issues/2349>`_, `#2467 <https://github.com/PostgREST/postgrest/issues/2467>`_)
|
||||
|
||||
* Fix ``SIGUSR1`` to fully flush the connection pool. (`#2401 <https://github.com/PostgREST/postgrest/issues/2401>`_, `#2444 <https://github.com/PostgREST/postgrest/issues/2444>`_)
|
||||
|
||||
* Fix opening an empty transaction on failed resource embedding. (`#2428 <https://github.com/PostgREST/postgrest/issues/2428>`_)
|
||||
|
||||
* Fix embedding the same table multiple times. (`#2455 <https://github.com/PostgREST/postgrest/issues/2455>`_)
|
||||
|
||||
* Fix a regression when embedding views where base tables have a different column order for foreign key columns (`#2518 <https://github.com/PostgREST/postgrest/issues/2518>`_)
|
||||
|
||||
* Fix a regression with the ``Location`` header when :ref:`inserting <insert>` into views with primary keys from multiple tables (`#2458 <https://github.com/PostgREST/postgrest/issues/2458>`_)
|
||||
|
||||
* Fix a regression in OpenAPI output with mode ``follow-privileges`` (`#2356 <https://github.com/PostgREST/postgrest/issues/2356>`_)
|
||||
|
||||
* Fix infinite recursion when loading schema cache with self-referencing view (`#2283 <https://github.com/PostgREST/postgrest/issues/2283>`_)
|
||||
|
||||
* Return status code ``200`` instead of ``404`` for ``PATCH`` requests which don't affect any rows (`#2343 <https://github.com/PostgREST/postgrest/issues/2343>`_)
|
||||
|
||||
* Treat the :ref:`computed relationships <computed_relationships>` that do not return ``SETOF`` as M2O/O2O relationship (`#2481 <https://github.com/PostgREST/postgrest/issues/2481>`_)
|
||||
|
||||
* Fix embedding a computed relationship with a normal relationship (`#2534 <https://github.com/PostgREST/postgrest/issues/2534>`_)
|
||||
|
||||
* Fix error message when ``[]`` is used inside ``select`` (`#2362 <https://github.com/PostgREST/postgrest/issues/2362>`_)
|
||||
|
||||
* Disallow ``!inner`` on computed columns (`#2475 <https://github.com/PostgREST/postgrest/issues/2475>`_)
|
||||
|
||||
* Ignore leading and trailing spaces in column names when parsing the query string (`#2285 <https://github.com/PostgREST/postgrest/issues/2285>`_)
|
||||
|
||||
* Fix ``UPSERT`` with PostgreSQL 15 (`#2545 <https://github.com/PostgREST/postgrest/issues/2545>`_)
|
||||
|
||||
* Fix embedding views with multiple references to the same base column (`#2459 <https://github.com/PostgREST/postgrest/issues/2459>`_)
|
||||
|
||||
* Fix regression when embedding views with partial references to multi column foreign keys (`#2548 <https://github.com/PostgREST/postgrest/issues/2548>`_)
|
||||
|
||||
* Fix regression when requesting ``limit=0`` and ``db-max-row`` is set (`#2558 <https://github.com/PostgREST/postgrest/issues/2558>`_)
|
||||
|
||||
* Return a clear error without hitting the database when trying to update or insert an unknown column with ``?columns`` (`#2542 <https://github.com/PostgREST/postgrest/issues/2542>`_)
|
||||
|
||||
* Fix bad M2M embedding on RPC (`#2565 <https://github.com/PostgREST/postgrest/issues/2565>`_)
|
||||
|
||||
* Replace misleading error message when no function is found with a hint containing functions/parameters names suggestions (`#2575 <https://github.com/PostgREST/postgrest/issues/2575>`_)
|
||||
|
||||
* Move explanation about "single parameters" from the ``message`` to the ``details`` in the error output (`#2582 <https://github.com/PostgREST/postgrest/issues/2582>`_)
|
||||
|
||||
* Replace misleading error message when no relationship is found with a hint containing parent/child names suggestions (`#2569 <https://github.com/PostgREST/postgrest/issues/2569>`_)
|
||||
|
||||
* Add the required OpenAPI items object when the parameter is an array (`#1405 <https://github.com/PostgREST/postgrest/issues/1405>`_)
|
||||
|
||||
* Add upsert headers for ``POST`` requests to the OpenAPI output (`#2592 <https://github.com/PostgREST/postgrest/issues/2592>`_)
|
||||
|
||||
* Fix foreign keys pointing to ``VIEW`` instead of ``TABLE`` in OpenAPI output (`#2623 <https://github.com/PostgREST/postgrest/issues/2623>`_)
|
||||
|
||||
* Consider any PostgreSQL authentication failure as fatal and exit immediately (`#2622 <https://github.com/PostgREST/postgrest/issues/2622>`_)
|
||||
|
||||
* Fix ``NOTIFY pgrst`` not reloading the db connections catalog cache (`#2620 <https://github.com/PostgREST/postgrest/issues/2620>`_)
|
||||
|
||||
* Fix ``db-pool-acquisition-timeout`` not logging to stderr when the timeout is reached (`#2667 <https://github.com/PostgREST/postgrest/issues/2667>`_)
|
||||
|
||||
* Fix PostgreSQL resource leak with long-lived connections through the :ref:`db-pool-max-lifetime` configuration (`#2638 <https://github.com/PostgREST/postgrest/issues/2638>`_)
|
||||
|
||||
* There is now a stricter parsing of the query string. Instead of silently ignoring, the parser now returns a :ref:`PostgREST error <pgrst100>` on invalid syntax. (`#2537 <https://github.com/PostgREST/postgrest/issues/2537>`_)
|
||||
|
||||
Thanks
|
||||
------
|
||||
|
||||
Big thanks from the `PostgREST team <https://github.com/orgs/PostgREST/people>`_ to our sponsors!
|
||||
|
||||
.. container:: image-container
|
||||
|
||||
.. image:: ../_static/cybertec-new.png
|
||||
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/2ndquadrant.png
|
||||
:target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/retool.png
|
||||
:target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/gnuhost.png
|
||||
:target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/supabase.png
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/oblivious.jpg
|
||||
:target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
* Evans Fernandes
|
||||
* Jan Sommer
|
||||
* `Franz Gusenbauer <https://www.igutech.at/>`_
|
||||
* `Daniel Babiak <https://github.com/dbabiak>`_
|
||||
* Tsingson Qin
|
||||
* Michel Pelletier
|
||||
* Jay Hannah
|
||||
* Robert Stolarz
|
||||
* Nicholas DiBiase
|
||||
* Christopher Reid
|
||||
* Nathan Bouscal
|
||||
* Daniel Rafaj
|
||||
* David Fenko
|
||||
* Remo Rechkemmer
|
||||
* Severin Ibarluzea
|
||||
* Tom Saleeba
|
||||
* Pawel Tyll
|
||||
|
||||
If you like to join them please consider `supporting PostgREST development <https://github.com/PostgREST/postgrest#user-content-supporting-development>`_.
|
||||
@@ -1,123 +0,0 @@
|
||||
11.0.1
|
||||
======
|
||||
|
||||
These release notes include the changes in versions `11.0.0 <https://github.com/PostgREST/postgrest/releases/tag/v11.0.0>`_ and `11.0.1 <https://github.com/PostgREST/postgrest/releases/tag/v11.0.1>`_. The detailed changelog and pre-compiled binaries are available on the `GitHub release pages <https://github.com/PostgREST/postgrest/releases>`_.
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
Transactions
|
||||
~~~~~~~~~~~~
|
||||
|
||||
- New configurable :ref:`isolation_lvl`.
|
||||
- New :ref:`impersonated_settings`.
|
||||
|
||||
Inserts
|
||||
~~~~~~~
|
||||
|
||||
- New :ref:`bulk_insert_default`.
|
||||
|
||||
Functions
|
||||
~~~~~~~~~
|
||||
|
||||
- Filters on Table-Valued Functions are now guaranteed to be inlined. See :ref:`function_inlining`.
|
||||
|
||||
Horizontal Filtering
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- New ``isdistinct`` :ref:`operator <operators>`. It is a direct translation of `IS DINTINCT FROM <https://www.postgresql.org/docs/current/functions-comparison.html#FUNCTIONS-COMPARISON-PRED-TABLE>`_.
|
||||
- New ``and/all`` :ref:`modifiers`.
|
||||
|
||||
Resource Embedding
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- New :ref:`top_level_order`.
|
||||
- New :ref:`spread_embed`.
|
||||
- New :ref:`null_embed`.
|
||||
- New :ref:`empty_embed`.
|
||||
|
||||
OpenAPI
|
||||
~~~~~~~
|
||||
|
||||
- Allow :ref:`overriding the OpenAPI title <open-api>`.
|
||||
- Allow :ref:`override_openapi`.
|
||||
|
||||
Admin
|
||||
~~~~~
|
||||
|
||||
- New :ref:`trace_header`.
|
||||
|
||||
Documentation improvements
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- New :ref:`transactions` reference.
|
||||
- New :ref:`connection_pool` reference.
|
||||
- Split :ref:`api` into other sub-references.
|
||||
- Split :ref:`authn` into :ref:`db_authz` and :ref:`sql_user_management`.
|
||||
- Split :ref:`admin` into :ref:`intgrs`, :ref:`nginx` and :ref:`connection_pool`.
|
||||
|
||||
Bug fixes
|
||||
---------
|
||||
|
||||
- Please check the "Fixed" section on https://github.com/PostgREST/postgrest/releases/tag/v11.0.0 and https://github.com/PostgREST/postgrest/releases/tag/v11.0.1.
|
||||
|
||||
Breaking changes
|
||||
----------------
|
||||
|
||||
- Removed Bulk Call with ``Prefer: params=multiple-objects`` on Stored Procedures.
|
||||
- To comply with `RFC 9110 <https://www.rfc-editor.org/rfc/rfc9110.html#name-range>`_, the Range header is now only considered on GET.
|
||||
|
||||
+ Other methods will ignore it and instead should use the ``limit/offset``.
|
||||
+ PUT requests no longer return an error when this header is present (using limit/offset still triggers the error)
|
||||
|
||||
Thanks
|
||||
------
|
||||
|
||||
Big thanks from the `PostgREST team <https://github.com/orgs/PostgREST/people>`_ to our sponsors!
|
||||
|
||||
.. container:: image-container
|
||||
|
||||
.. image:: ../_static/cybertec-new.png
|
||||
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/2ndquadrant.png
|
||||
:target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/retool.png
|
||||
:target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/gnuhost.png
|
||||
:target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/supabase.png
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/oblivious.jpg
|
||||
:target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
* `Roboflow <https://github.com/roboflow>`_
|
||||
* Evans Fernandes
|
||||
* Jan Sommer
|
||||
* `Franz Gusenbauer <https://www.igutech.at/>`_
|
||||
* Zac Miller
|
||||
* Tsingson Qin
|
||||
* Michel Pelletier
|
||||
* Jay Hannah
|
||||
* Robert Stolarz
|
||||
* Nicholas DiBiase
|
||||
* Christopher Reid
|
||||
* Nathan Bouscal
|
||||
* Daniel Rafaj
|
||||
* David Fenko
|
||||
* Remo Rechkemmer
|
||||
* Severin Ibarluzea
|
||||
* Tom Saleeba
|
||||
* Pawel Tyll
|
||||
|
||||
If you like to join them please consider `supporting PostgREST development <https://github.com/PostgREST/postgrest#user-content-supporting-development>`_.
|
||||
@@ -1,73 +0,0 @@
|
||||
11.1.0
|
||||
======
|
||||
|
||||
Features
|
||||
--------
|
||||
|
||||
Connection Pool
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
- New :ref:`db-pool-max-idletime`. It enables a :ref:`dyn_conn_pool`.
|
||||
|
||||
Configuration
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
- New :ref:`in_db_config`. It no longer requires high privileges and can be used on cloud-hosted databases.
|
||||
|
||||
Bug fixes
|
||||
---------
|
||||
|
||||
- Fix dropping schema cache reload notifications.
|
||||
- Stop automatic recovery when the error is "no password supplied".
|
||||
|
||||
Thanks
|
||||
------
|
||||
|
||||
Big thanks from the `PostgREST team <https://github.com/orgs/PostgREST/people>`_ to our sponsors!
|
||||
|
||||
.. container:: image-container
|
||||
|
||||
.. image:: ../_static/cybertec-new.png
|
||||
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/2ndquadrant.png
|
||||
:target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/retool.png
|
||||
:target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/gnuhost.png
|
||||
:target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/supabase.png
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
:width: 13em
|
||||
|
||||
.. image:: ../_static/oblivious.jpg
|
||||
:target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
* `Roboflow <https://github.com/roboflow>`_
|
||||
* Evans Fernandes
|
||||
* Jan Sommer
|
||||
* `Franz Gusenbauer <https://www.igutech.at/>`_
|
||||
* Zac Miller
|
||||
* Tsingson Qin
|
||||
* Michel Pelletier
|
||||
* Jay Hannah
|
||||
* Robert Stolarz
|
||||
* Nicholas DiBiase
|
||||
* Christopher Reid
|
||||
* Nathan Bouscal
|
||||
* Daniel Rafaj
|
||||
* David Fenko
|
||||
* Remo Rechkemmer
|
||||
* Severin Ibarluzea
|
||||
* Tom Saleeba
|
||||
* Pawel Tyll
|
||||
|
||||
If you like to join them please consider `supporting PostgREST development <https://github.com/PostgREST/postgrest#user-content-supporting-development>`_.
|
||||
@@ -1,4 +0,0 @@
|
||||
11.2.0
|
||||
======
|
||||
|
||||
Starting from this version, the release notes will be posted on PostgREST's GitHub release page. Please see https://github.com/PostgREST/postgrest/releases/tag/v11.2.0.
|
||||
@@ -1,4 +0,0 @@
|
||||
11.2.1
|
||||
======
|
||||
|
||||
v11.2.1 is a maintenance release that addresses bugs introduced in v11.2.0. Also PostgreSQL 16 is now tested and confirmed to work. For more details, see https://github.com/PostgREST/postgrest/releases/tag/v11.2.1.
|
||||
@@ -1,6 +1,7 @@
|
||||
docutils==0.20.1
|
||||
sphinx>=4.3.0
|
||||
sphinx>=5.0.2
|
||||
sphinx-copybutton
|
||||
sphinx-rtd-theme>=0.5.1
|
||||
sphinx-tabs
|
||||
urllib3==2.2.3
|
||||
sphinx-tabs>=3.2.0
|
||||
urllib3==2.2.1
|
||||
sphinxext-opengraph==0.9.1
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: macOS
|
||||
|
||||
You can install PostgREST from the `Homebrew official repo <https://formulae.brew.sh/formula/postgrest>`_.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
brew install postgrest
|
||||
|
||||
.. group-tab:: FreeBSD
|
||||
|
||||
You can install PostgREST from the `official ports <https://www.freshports.org/www/hs-postgrest>`_.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pkg install hs-postgrest
|
||||
|
||||
.. group-tab:: Linux
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. tab:: Arch Linux
|
||||
|
||||
You can install PostgREST from the `community repo <https://archlinux.org/packages/extra/x86_64/postgrest/>`_.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pacman -S postgrest
|
||||
|
||||
.. tab:: Nix
|
||||
|
||||
You can install PostgREST from nixpkgs.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
nix-env -i haskellPackages.postgrest
|
||||
|
||||
.. group-tab:: Windows
|
||||
|
||||
You can install PostgREST using `Chocolatey <https://community.chocolatey.org/packages/postgrest>`_ or `Scoop <https://github.com/ScoopInstaller/Scoop>`_.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
choco install postgrest
|
||||
scoop install postgrest
|
||||
@@ -13,6 +13,7 @@ pkgs.mkShell {
|
||||
docs.spellcheck
|
||||
docs.dictcheck
|
||||
docs.linkcheck
|
||||
docs.check
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
|
||||
+33
-6
@@ -15,7 +15,12 @@ To make an API we'll simply be building a database. All the endpoints and permis
|
||||
|
||||
By the end of this tutorial you'll have a working database, PostgREST server, and a simple single-user todo list API.
|
||||
|
||||
Step 1. Install PostgreSQL
|
||||
Step 1. Relax, we'll help
|
||||
-------------------------
|
||||
|
||||
As you begin the tutorial, pop open the project `chat room <https://gitter.im/begriffs/postgrest>`_ in another tab. There are a nice group of people active in the project and we'll help you out if you get stuck.
|
||||
|
||||
Step 2. 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.
|
||||
@@ -30,10 +35,28 @@ If Docker is not installed, you can get it `here <https://www.docker.com/get-sta
|
||||
|
||||
This will run the Docker instance as a daemon and expose port 5433 to the host system so that it looks like an ordinary PostgreSQL server to the rest of the system.
|
||||
|
||||
Step 2. Install PostgREST
|
||||
Step 3. Install PostgREST
|
||||
-------------------------
|
||||
|
||||
PostgREST is distributed as a single binary, with versions compiled for major distributions of Linux/BSD/Windows. Visit the `latest release <https://github.com/PostgREST/postgrest/releases/latest>`_ for a list of downloads. In the event that your platform is not among those already pre-built, see :ref:`build_source` for instructions how to build it yourself. Also let us know to add your platform in the next release.
|
||||
Using a Package Manager
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can use your OS package manager to install PostgREST.
|
||||
|
||||
.. include:: ../shared/installation.rst
|
||||
|
||||
Then, try running it with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
postgrest -h
|
||||
|
||||
It should print the help page with its version and the available options.
|
||||
|
||||
Downloading a Pre-Built Binary
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PostgREST is also distributed as a single binary, with versions compiled for major distributions of macOS, Windows, Linux and FreeBSD. Visit the `latest release <https://github.com/PostgREST/postgrest/releases/latest>`_ for a list of downloads. In the event that your platform is not among those already pre-built, see :ref:`build_source` for instructions how to build it yourself. Also let us know to add your platform in the next release.
|
||||
|
||||
The pre-built binaries for download are :code:`.tar.xz` compressed files (except Windows which is a zip file). To extract the binary, go into the terminal and run
|
||||
|
||||
@@ -71,7 +94,7 @@ If everything is working correctly it will print out its version and the availab
|
||||
</div></div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>OS X</summary>
|
||||
<summary>macOS</summary>
|
||||
<div class="highlight-bash"><div class="highlight">
|
||||
<pre>brew install postgresql</pre>
|
||||
</div></div>
|
||||
@@ -86,7 +109,7 @@ If everything is working correctly it will print out its version and the availab
|
||||
</details>
|
||||
</p>
|
||||
|
||||
Step 3. Create Database for API
|
||||
Step 4. Create Database for API
|
||||
-------------------------------
|
||||
|
||||
Connect to the SQL console (psql) inside the container. To do so, run this from your command line:
|
||||
@@ -149,7 +172,7 @@ Now quit out of psql; it's time to start the API!
|
||||
|
||||
\q
|
||||
|
||||
Step 4. Run PostgREST
|
||||
Step 5. Run PostgREST
|
||||
---------------------
|
||||
|
||||
PostgREST can use a configuration file to tell it how to connect to the database. Create a file :code:`tutorial.conf` with this inside:
|
||||
@@ -167,6 +190,10 @@ Now run the server:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Running postgrest installed from a package manager
|
||||
postgrest tutorial.conf
|
||||
|
||||
# Running postgrest binary
|
||||
./postgrest tutorial.conf
|
||||
|
||||
You should see
|
||||
|
||||
@@ -41,16 +41,15 @@ Let's create a password and provide it to PostgREST. Think of a nice long one, o
|
||||
export LC_CTYPE=C
|
||||
|
||||
# read random bytes and keep only alphanumerics
|
||||
< /dev/urandom tr -dc A-Za-z0-9 | head -c32
|
||||
echo "jwt-secret = \"$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c32)\"" >> tutorial.conf
|
||||
|
||||
Open the :code:`tutorial.conf` (created in the previous tutorial) and add a line with the password:
|
||||
|
||||
check the :code:`tutorial.conf` (created in the previous tutorial) the line with the password:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# PASSWORD MUST BE AT LEAST 32 CHARS LONG
|
||||
# add this line to tutorial.conf:
|
||||
|
||||
jwt-secret = "<the password you made>"
|
||||
cat tutorial.conf
|
||||
|
||||
If the PostgREST server is still running from the previous tutorial, restart it to load the updated configuration file.
|
||||
|
||||
|
||||
+1
-22
@@ -1,37 +1,16 @@
|
||||
{-# LANGUAGE CPP #-}
|
||||
|
||||
module Main (main) where
|
||||
|
||||
import System.IO (BufferMode (..), hSetBuffering)
|
||||
|
||||
import qualified PostgREST.App as App
|
||||
import qualified PostgREST.CLI as CLI
|
||||
|
||||
import Protolude
|
||||
|
||||
#ifndef mingw32_HOST_OS
|
||||
import qualified PostgREST.Unix as Unix
|
||||
#endif
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
setBuffering
|
||||
opts <- CLI.readCLIShowHelp
|
||||
CLI.main installSignalHandlers runAppInSocket opts
|
||||
|
||||
installSignalHandlers :: App.SignalHandlerInstaller
|
||||
#ifndef mingw32_HOST_OS
|
||||
installSignalHandlers = Unix.installSignalHandlers
|
||||
#else
|
||||
installSignalHandlers _ = pass
|
||||
#endif
|
||||
|
||||
runAppInSocket :: Maybe App.SocketRunner
|
||||
#ifndef mingw32_HOST_OS
|
||||
runAppInSocket = Just Unix.runAppWithSocket
|
||||
#else
|
||||
runAppInSocket = Nothing
|
||||
#endif
|
||||
CLI.main opts
|
||||
|
||||
setBuffering :: IO ()
|
||||
setBuffering = do
|
||||
|
||||
+34
-9
@@ -22,6 +22,15 @@ build the `postgrestPackage` attribute from the Nix expression it finds in our
|
||||
`default.nix` (see below for details). Nix will take care of getting the right
|
||||
GHC version and all the build dependencies.
|
||||
|
||||
You can also build a statically linked binary with:
|
||||
|
||||
```bash
|
||||
$ nix-build --attr postgrestStatic
|
||||
|
||||
$ ldd result/bin/postgrest
|
||||
$ not a dynamic executable
|
||||
```
|
||||
|
||||
## Binary cache
|
||||
|
||||
We recommend that you use the PostgREST binary cache on
|
||||
@@ -82,7 +91,7 @@ Some additional modules like `memory`, `docker` and `release`
|
||||
have large dependencies that would need to be built before the shell becomes
|
||||
available, which could take an especially long time if the cachix binary cache
|
||||
is not used. You can activate those by passing a flag to `nix-shell` with
|
||||
`nix-shell --arg <module> true`. This will make the respective utilites available:
|
||||
`nix-shell --arg <module> true`. This will make the respective utilities available:
|
||||
|
||||
```bash
|
||||
$ nix-shell --arg memory true
|
||||
@@ -104,7 +113,7 @@ postgrest-test-memory
|
||||
Note that `postgrest-test-memory` is now also available.
|
||||
|
||||
To run one-off commands, you can also use `nix-shell --run <command>`, which
|
||||
will lauch the Nix shell, run that one command and exit. Note that the tab
|
||||
will launch the Nix shell, run that one command and exit. Note that the tab
|
||||
completion will not work with `nix-shell --run`, as Nix has yet to evaluate
|
||||
our Nix expressions to see which utilities are available.
|
||||
|
||||
@@ -212,15 +221,14 @@ doctests for some of our modules are also available:
|
||||
|
||||
## Code coverage
|
||||
|
||||
Code coverage is available under the `postgrest-coverage` command. This will produce a `./coverage` directory that can be visualized with a simple http server.
|
||||
Code coverage is available under the `postgrest-coverage` command. This will produce a `./coverage` directory that can be visualized on a browser.
|
||||
|
||||
```bash
|
||||
# Will run all the tests and produce a coverage dir
|
||||
[nix-shell]$ postgrest-coverage
|
||||
...
|
||||
|
||||
# Visualize the output
|
||||
[nix-shell]$ cd coverage
|
||||
[nix-shell]$ python -mSimpleHTTPServer 8080
|
||||
postgrest-coverage: To see the results, visit file://$(pwd)/coverage/check/hpc_index.html
|
||||
```
|
||||
|
||||
## Linting and styling code
|
||||
@@ -238,11 +246,11 @@ $ nix-shell --run postgrest-style
|
||||
```
|
||||
|
||||
There is also `postgrest-style-check` that exits with a non-zero exit code if
|
||||
the check resulted in any uncommited changes. It's mostly useful for CI.
|
||||
the check resulted in any uncommitted changes. It's mostly useful for CI.
|
||||
|
||||
## General development tools
|
||||
|
||||
Tools like `postgrest-build`, `postgrest-run` etc. are simple wrappers around
|
||||
Tools like `postgrest-build`, `postgrest-run`, `postgrest-repl` etc. are simple wrappers around
|
||||
`cabal` and should do what you expect. `postgrest-check` runs most checks that will
|
||||
also run in CI, with the exception of the IO and Memory checks that need to be run
|
||||
separately.
|
||||
@@ -256,6 +264,23 @@ run against the latest PostgreSQL version by default.
|
||||
file is changed. For example, `postgrest-watch postgrest-with-all postgrest-test-spec`
|
||||
will re-run the full spec test suite against all PostgreSQL versions on every change.
|
||||
|
||||
## REPL
|
||||
|
||||
You can use `postgrest-repl` to manually inspect the PostgREST modules.
|
||||
|
||||
```bash
|
||||
$ postgrest-repl
|
||||
|
||||
ghci> import PostgREST.<tab>
|
||||
PostgREST.Admin PostgREST.Config.Database PostgREST.Plan.MutatePlan PostgREST.Response.OpenAPI
|
||||
PostgREST.ApiRequest PostgREST.Config.JSPath PostgREST.Plan.ReadPlan PostgREST.SchemaCache
|
||||
...
|
||||
|
||||
ghci> import PostgREST.MediaType
|
||||
ghci> decodeMediaType "application/json"
|
||||
MTApplicationJSON
|
||||
```
|
||||
|
||||
## Tour
|
||||
|
||||
The following is not required for working on PostgREST with Nix, but it will
|
||||
@@ -284,7 +309,7 @@ version.
|
||||
### `shell.nix`
|
||||
|
||||
[`shell.nix`](../shell.nix) defines an environment in which PostgREST can be
|
||||
built and developed. It extends the build enviroment from our `postgrest`
|
||||
built and developed. It extends the build environment from our `postgrest`
|
||||
attribute with useful utilities that will be put on the PATH in `nix-shell`.
|
||||
|
||||
### `nix/overlays`
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ required to avoid build timeouts in CI.
|
||||
|
||||
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 you shell history.
|
||||
avoids having the secret in your shell history.
|
||||
|
||||
To push all new artifacts to Cachix, run:
|
||||
|
||||
|
||||
@@ -57,6 +57,18 @@ let
|
||||
postgrest "''${_arg_leftovers[@]}"
|
||||
'';
|
||||
|
||||
repl =
|
||||
checkedShellScript
|
||||
{
|
||||
name = "postgrest-repl";
|
||||
docs = "Interact with PostgREST modules using the cabal repl";
|
||||
args = [ "ARG_LEFTOVERS([cabal v2-repl arguments])" ];
|
||||
inRootDir = true;
|
||||
withEnv = postgrest.env;
|
||||
}
|
||||
''
|
||||
exec ${cabal-install}/bin/cabal v2-repl "''${_arg_leftovers[@]}"
|
||||
'';
|
||||
in
|
||||
buildToolbox
|
||||
{
|
||||
@@ -65,5 +77,6 @@ buildToolbox
|
||||
build
|
||||
clean
|
||||
run
|
||||
repl
|
||||
];
|
||||
}
|
||||
|
||||
+11
-8
@@ -76,13 +76,12 @@ let
|
||||
inherit name;
|
||||
docs =
|
||||
''
|
||||
Run the vegeta loadtest twice:
|
||||
- once on the <target> branch
|
||||
Run the vegeta loadtest against every target branch and HEAD:
|
||||
- once on the every <target-#> branch
|
||||
- once in the current worktree
|
||||
'';
|
||||
args = [
|
||||
"ARG_POSITIONAL_SINGLE([target], [Commit-ish reference to compare with])"
|
||||
"ARG_LEFTOVERS([additional vegeta arguments])"
|
||||
"ARG_POSITIONAL_INF([target], [Commit-ish reference to compare with], 1)"
|
||||
];
|
||||
positionalCompletion =
|
||||
''
|
||||
@@ -93,9 +92,11 @@ let
|
||||
inRootDir = true;
|
||||
}
|
||||
''
|
||||
for tgt in "''${_arg_target[@]}"; do
|
||||
|
||||
cat << EOF
|
||||
|
||||
Running loadtest on "$_arg_target"...
|
||||
Running loadtest on "$tgt"...
|
||||
|
||||
EOF
|
||||
|
||||
@@ -104,21 +105,23 @@ let
|
||||
# Save the results in the current working tree, too,
|
||||
# otherwise they'd be lost in the temporary working tree
|
||||
# created by withTools.withGit.
|
||||
${withTools.withGit} "$_arg_target" ${loadtest} --output "$PWD/loadtest/$_arg_target.bin" --testdir "$PWD/test/load" "''${_arg_leftovers[@]}"
|
||||
${withTools.withGit} "$tgt" ${loadtest} --output "$PWD/loadtest/$tgt.bin" --testdir "$PWD/test/load"
|
||||
|
||||
cat << EOF
|
||||
|
||||
Done running on "$_arg_target".
|
||||
Done running on "$tgt".
|
||||
|
||||
EOF
|
||||
|
||||
done
|
||||
|
||||
cat << EOF
|
||||
|
||||
Running loadtest on HEAD...
|
||||
|
||||
EOF
|
||||
|
||||
${loadtest} --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load" "''${_arg_leftovers[@]}"
|
||||
${loadtest} --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load"
|
||||
|
||||
cat << EOF
|
||||
|
||||
|
||||
@@ -56,8 +56,8 @@ let
|
||||
inRootDir = true;
|
||||
}
|
||||
''
|
||||
trap "echo You need to be on the main branch to proceed. Exiting ..." ERR
|
||||
[ "$(git rev-parse --abbrev-ref HEAD)" == "main" ]
|
||||
trap "echo You need to be on the main branch or a release branch to proceed. Exiting ..." ERR
|
||||
[[ "$(git rev-parse --abbrev-ref HEAD)" =~ ^main$|^v[0-9]+$ ]]
|
||||
trap "" ERR
|
||||
|
||||
trap "echo You have uncommitted changes in postgrest.cabal. Exiting ..." ERR
|
||||
@@ -111,7 +111,7 @@ let
|
||||
remote="$(git remote -v | grep PostgREST/postgrest | grep push | cut -f1)"
|
||||
trap "" ERR
|
||||
|
||||
push="git push --atomic $remote main v$new_version"
|
||||
push="git push --atomic $remote $(git rev-parse --abbrev-ref HEAD) v$new_version"
|
||||
|
||||
echo "To push both the branch and the new tag, the following will be run:"
|
||||
echo
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# PostgREST
|
||||
|
||||
[](https://gitter.im/begriffs/postgrest)
|
||||
[](https://www.patreon.com/postgrest)
|
||||
[](https://www.paypal.me/postgrest)
|
||||
[](http://postgrest.org)
|
||||
[](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)
|
||||
|
||||
@@ -10,30 +12,30 @@ write from scratch.
|
||||
|
||||
## Sponsors
|
||||
|
||||
<table>
|
||||
<table align="center">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest" target="_blank">
|
||||
<img width="222px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/cybertec-new.png">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/cybertec-new.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/2ndquadrant.png">
|
||||
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/gnuhost.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/retool.png">
|
||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/neon.jpg">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/gnuhost.png">
|
||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/code-build.png">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
@@ -42,8 +44,8 @@ write from scratch.
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/oblivious.jpg">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/tembo.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -56,13 +58,13 @@ To learn how to use this container, see the [PostgREST Docker
|
||||
documentation](https://postgrest.org/en/stable/install.html#docker).
|
||||
|
||||
You can configure the PostgREST image by setting
|
||||
[enviroment variables](https://postgrest.org/en/stable/configuration.html).
|
||||
[environment variables](https://postgrest.org/en/stable/configuration.html).
|
||||
|
||||
# How this image is built
|
||||
|
||||
The image is built from scratch using
|
||||
[Nix](https://nixos.org/nixpkgs/manual/#sec-pkgs-dockerTools) instead of a
|
||||
`Dockerfile`, which yields a higly secure and optimized image. This is also why
|
||||
`Dockerfile`, which yields a highly secure and optimized image. This is also why
|
||||
no commands are listed in the image history. See the [PostgREST
|
||||
respository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for
|
||||
details on the build process and how to inspect the image.
|
||||
|
||||
@@ -68,7 +68,6 @@ let
|
||||
|
||||
echo "Linting bash scripts..."
|
||||
${shellcheck}/bin/shellcheck \
|
||||
.github/get_cirrusci_freebsd \
|
||||
.github/release
|
||||
|
||||
echo "Linting workflows..."
|
||||
|
||||
+2
-2
@@ -164,7 +164,7 @@ let
|
||||
${ghc}/bin/hpc markup --highlight-covered --destdir=coverage/overlay "$tmpdir"/overlay.tix || true
|
||||
${ghc}/bin/hpc markup --highlight-covered --destdir=coverage/check "$tmpdir"/check.tix || true
|
||||
echo "ERROR: Something is covered by both the tests and the overlay:"
|
||||
echo "file://$(pwd)/coverage/check/hpc_index.html"
|
||||
echo "postgrest-coverage: To see the results, visit file://$(pwd)/coverage/check/hpc_index.html"
|
||||
exit 1
|
||||
else
|
||||
# copy the result .tix file to the coverage/ dir to make it available to postgrest-coverage-draft-overlay, too
|
||||
@@ -174,7 +174,7 @@ let
|
||||
|
||||
# create html and stdout reports
|
||||
${ghc}/bin/hpc markup --destdir=coverage coverage/postgrest.tix
|
||||
echo "file://$(pwd)/coverage/hpc_index.html"
|
||||
echo "postgrest-coverage: To see the results, visit file://$(pwd)/coverage/hpc_index.html"
|
||||
${ghc}/bin/hpc report coverage/postgrest.tix "''${_arg_leftovers[@]}"
|
||||
fi
|
||||
''
|
||||
|
||||
@@ -72,7 +72,7 @@ let
|
||||
# We try to make the database cluster as independent as possible from the host
|
||||
# by specifying the timezone, locale and encoding.
|
||||
# initdb -U creates a superuser(man initdb)
|
||||
PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "${superuserRole}" --auth=trust \
|
||||
TZ=$PGTZ initdb --no-locale --encoding=UTF8 --nosync -U "${superuserRole}" --auth=trust \
|
||||
>> "$setuplog"
|
||||
|
||||
log "Starting the database cluster..."
|
||||
|
||||
+16
-9
@@ -1,5 +1,5 @@
|
||||
name: postgrest
|
||||
version: 11.2.2
|
||||
version: 12.0.3
|
||||
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
|
||||
@@ -64,6 +64,7 @@ library
|
||||
PostgREST.Plan.ReadPlan
|
||||
PostgREST.Plan.Types
|
||||
PostgREST.RangeQuery
|
||||
PostgREST.Unix
|
||||
PostgREST.ApiRequest
|
||||
PostgREST.ApiRequest.Preferences
|
||||
PostgREST.ApiRequest.QueryParams
|
||||
@@ -71,6 +72,7 @@ library
|
||||
PostgREST.Response
|
||||
PostgREST.Response.OpenAPI
|
||||
PostgREST.Response.GucHeader
|
||||
PostgREST.Response.Performance
|
||||
PostgREST.Version
|
||||
other-modules: Paths_postgrest
|
||||
build-depends: base >= 4.9 && < 4.17
|
||||
@@ -80,12 +82,15 @@ library
|
||||
, auto-update >= 0.1.4 && < 0.2
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
, bytestring >= 0.10.8 && < 0.12
|
||||
, cache >= 0.1.3 && < 0.2.0
|
||||
, case-insensitive >= 1.2 && < 1.3
|
||||
, cassava >= 0.4.5 && < 0.6
|
||||
, clock >= 0.8.3 && < 0.9.0
|
||||
, configurator-pg >= 0.2 && < 0.3
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
, contravariant-extras >= 0.3.3 && < 0.4
|
||||
, cookie >= 0.4.2 && < 0.5
|
||||
, directory >= 1.2.6 && < 1.4
|
||||
, either >= 4.4.1 && < 5.1
|
||||
, extra >= 1.7.0 && < 2.0
|
||||
, fuzzyset >= 0.2.3
|
||||
@@ -111,10 +116,13 @@ library
|
||||
, regex-tdfa >= 1.2.2 && < 1.4
|
||||
, retry >= 0.7.4 && < 0.10
|
||||
, scientific >= 0.3.4 && < 0.4
|
||||
, streaming-commons >= 0.1.1 && < 0.3
|
||||
, swagger2 >= 2.4 && < 2.9
|
||||
, text >= 1.2.2 && < 1.3
|
||||
, time >= 1.6 && < 1.12
|
||||
, timeit >= 2.0 && < 2.1
|
||||
, unordered-containers >= 0.2.8 && < 0.3
|
||||
, unix-compat >= 0.5.4 && < 0.6
|
||||
, vault >= 0.3.1.5 && < 0.4
|
||||
, vector >= 0.11 && < 0.14
|
||||
, wai >= 3.2.1 && < 3.3
|
||||
@@ -144,9 +152,6 @@ library
|
||||
if !os(windows)
|
||||
build-depends:
|
||||
unix
|
||||
, directory >= 1.2.6 && < 1.4
|
||||
exposed-modules:
|
||||
PostgREST.Unix
|
||||
|
||||
executable postgrest
|
||||
default-language: Haskell2010
|
||||
@@ -188,7 +193,6 @@ test-suite spec
|
||||
Feature.ConcurrentSpec
|
||||
Feature.CorsSpec
|
||||
Feature.ExtraSearchPathSpec
|
||||
Feature.LegacyGucsSpec
|
||||
Feature.NoSuperuserSpec
|
||||
Feature.ObservabilitySpec
|
||||
Feature.OpenApi.DisabledOpenApiSpec
|
||||
@@ -198,27 +202,30 @@ test-suite spec
|
||||
Feature.OpenApi.RootSpec
|
||||
Feature.OpenApi.SecurityOpenApiSpec
|
||||
Feature.OptionsSpec
|
||||
Feature.Query.AggregateFunctionsSpec
|
||||
Feature.Query.AndOrParamsSpec
|
||||
Feature.Query.ComputedRelsSpec
|
||||
Feature.Query.CustomMediaSpec
|
||||
Feature.Query.DeleteSpec
|
||||
Feature.Query.EmbedDisambiguationSpec
|
||||
Feature.Query.EmbedInnerJoinSpec
|
||||
Feature.Query.PlanSpec
|
||||
Feature.Query.HtmlRawOutputSpec
|
||||
Feature.Query.ErrorSpec
|
||||
Feature.Query.InsertSpec
|
||||
Feature.Query.JsonOperatorSpec
|
||||
Feature.Query.MultipleSchemaSpec
|
||||
Feature.Query.ErrorSpec
|
||||
Feature.Query.NullsStripSpec
|
||||
Feature.Query.PgSafeUpdateSpec
|
||||
Feature.Query.PlanSpec
|
||||
Feature.Query.PostGISSpec
|
||||
Feature.Query.PreferencesSpec
|
||||
Feature.Query.QueryLimitedSpec
|
||||
Feature.Query.QuerySpec
|
||||
Feature.Query.RangeSpec
|
||||
Feature.Query.RawOutputTypesSpec
|
||||
Feature.Query.RelatedQueriesSpec
|
||||
Feature.Query.RpcSpec
|
||||
Feature.Query.ServerTimingSpec
|
||||
Feature.Query.SingularSpec
|
||||
Feature.Query.NullsStrip
|
||||
Feature.Query.SpreadQueriesSpec
|
||||
Feature.Query.UnicodeSpec
|
||||
Feature.Query.UpdateSpec
|
||||
|
||||
+17
-40
@@ -1,11 +1,9 @@
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
|
||||
module PostgREST.Admin
|
||||
( runAdmin
|
||||
) where
|
||||
|
||||
import qualified Data.Text as T
|
||||
import qualified Hasql.Session as SQL
|
||||
import qualified Network.HTTP.Types.Status as HTTP
|
||||
import qualified Network.Wai as Wai
|
||||
@@ -22,24 +20,25 @@ import PostgREST.Config (AppConfig (..))
|
||||
import qualified PostgREST.AppState as AppState
|
||||
|
||||
import Protolude
|
||||
import Protolude.Partial (fromJust)
|
||||
|
||||
runAdmin :: AppConfig -> AppState -> Warp.Settings -> IO ()
|
||||
runAdmin conf@AppConfig{configAdminServerPort} appState settings =
|
||||
whenJust configAdminServerPort $ \adminPort -> do
|
||||
AppState.logWithZTime appState $ "Admin server listening on port " <> show adminPort
|
||||
void . forkIO $ Warp.runSettings (settings & Warp.setPort adminPort) adminApp
|
||||
whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do
|
||||
AppState.logWithZTime appState $ "Admin server listening on port " <> show (fromIntegral (fromJust configAdminServerPort) :: Integer)
|
||||
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
|
||||
where
|
||||
adminApp = admin appState conf
|
||||
|
||||
-- | PostgREST admin application
|
||||
admin :: AppState.AppState -> AppConfig -> Wai.Application
|
||||
admin appState appConfig req respond = do
|
||||
isMainAppReachable <- any isRight <$> reachMainApp appConfig
|
||||
isMainAppReachable <- isRight <$> reachMainApp (AppState.getSocketREST appState)
|
||||
isSchemaCacheLoaded <- isJust <$> AppState.getSchemaCache appState
|
||||
isConnectionUp <-
|
||||
if configDbChannelEnabled appConfig
|
||||
then AppState.getIsListenerOn appState
|
||||
else isRight <$> AppState.usePool appState (SQL.sql "SELECT 1")
|
||||
else isRight <$> AppState.usePool appState appConfig (SQL.sql "SELECT 1")
|
||||
|
||||
case Wai.pathInfo req of
|
||||
["ready"] ->
|
||||
@@ -51,37 +50,15 @@ admin appState appConfig req respond = do
|
||||
|
||||
-- Try to connect to the main app socket
|
||||
-- Note that it doesn't even send a valid HTTP request, we just want to check that the main app is accepting connections
|
||||
-- The code for resolving the "*4", "!4", "*6", "!6", "*" special values is taken from
|
||||
-- https://hackage.haskell.org/package/streaming-commons-0.2.2.4/docs/src/Data.Streaming.Network.html#bindPortGenEx
|
||||
reachMainApp :: AppConfig -> IO [Either IOException ()]
|
||||
reachMainApp AppConfig{..} =
|
||||
case configServerUnixSocket of
|
||||
Just path -> do
|
||||
sock <- socket AF_UNIX Stream 0
|
||||
(:[]) <$> try (do
|
||||
connect sock $ SockAddrUnix path
|
||||
withSocketsDo $ bracket (pure sock) close sendEmpty)
|
||||
Nothing -> do
|
||||
let
|
||||
host | configServerHost `elem` ["*4", "!4", "*6", "!6", "*"] = Nothing
|
||||
| otherwise = Just configServerHost
|
||||
filterAddrs xs =
|
||||
case configServerHost of
|
||||
"*4" -> ipv4Addrs xs ++ ipv6Addrs xs
|
||||
"!4" -> ipv4Addrs xs
|
||||
"*6" -> ipv6Addrs xs ++ ipv4Addrs xs
|
||||
"!6" -> ipv6Addrs xs
|
||||
_ -> xs
|
||||
ipv4Addrs = filter ((/=) AF_INET6 . addrFamily)
|
||||
ipv6Addrs = filter ((==) AF_INET6 . addrFamily)
|
||||
|
||||
addrs <- getAddrInfo (Just $ defaultHints { addrSocketType = Stream }) (T.unpack <$> host) (Just . show $ configServerPort)
|
||||
tryAddr `traverse` filterAddrs addrs
|
||||
reachMainApp :: Socket -> IO (Either IOException ())
|
||||
reachMainApp appSock = do
|
||||
sockAddr <- getSocketName appSock
|
||||
sock <- socket (addrFamily sockAddr) Stream defaultProtocol
|
||||
try $ do
|
||||
connect sock sockAddr
|
||||
withSocketsDo $ bracket (pure sock) close sendEmpty
|
||||
where
|
||||
sendEmpty sock = void $ send sock mempty
|
||||
tryAddr :: AddrInfo -> IO (Either IOException ())
|
||||
tryAddr addr = do
|
||||
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
|
||||
try $ do
|
||||
connect sock $ addrAddress addr
|
||||
withSocketsDo $ bracket (pure sock) close sendEmpty
|
||||
addrFamily (SockAddrInet _ _) = AF_INET
|
||||
addrFamily (SockAddrInet6 {}) = AF_INET6
|
||||
addrFamily (SockAddrUnix _) = AF_UNIX
|
||||
|
||||
+12
-51
@@ -26,7 +26,6 @@ import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.Csv as CSV
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.List as L
|
||||
import qualified Data.List.NonEmpty as NonEmptyList
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.Set as S
|
||||
@@ -37,7 +36,7 @@ import Data.Either.Combinators (mapBoth)
|
||||
|
||||
import Control.Arrow ((***))
|
||||
import Data.Aeson.Types (emptyArray, emptyObject)
|
||||
import Data.List (lookup, union)
|
||||
import Data.List (lookup)
|
||||
import Data.Ranged.Ranges (emptyRange, rangeIntersection,
|
||||
rangeIsEmpty)
|
||||
import Network.HTTP.Types.Header (RequestHeaders, hCookie)
|
||||
@@ -51,12 +50,12 @@ import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
||||
RangeError (..))
|
||||
import PostgREST.Config (AppConfig (..),
|
||||
OpenAPIMode (..))
|
||||
import PostgREST.MediaType (MTPlanFormat (..),
|
||||
MediaType (..))
|
||||
import PostgREST.MediaType (MediaType (..))
|
||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
convertToLimitZeroRange,
|
||||
hasLimitZero,
|
||||
rangeRequested)
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier (..),
|
||||
Schema)
|
||||
@@ -131,17 +130,16 @@ data ApiRequest = ApiRequest {
|
||||
, iMethod :: ByteString -- ^ Raw request method
|
||||
, iSchema :: Schema -- ^ The request schema. Can vary depending on profile headers.
|
||||
, iNegotiatedByProfile :: Bool -- ^ If schema was was chosen according to the profile spec https://www.w3.org/TR/dx-prof-conneg/
|
||||
, iAcceptMediaType :: MediaType -- ^ The media type in the Accept header
|
||||
, iAcceptMediaType :: [MediaType] -- ^ The resolved media types in the Accept, considering quality(q) factors
|
||||
, iContentMediaType :: MediaType -- ^ The media type in the Content-Type header
|
||||
}
|
||||
|
||||
-- | Examines HTTP request and translates it into user intent.
|
||||
userApiRequest :: AppConfig -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
||||
userApiRequest conf req reqBody = do
|
||||
userApiRequest :: AppConfig -> Request -> RequestBody -> SchemaCache -> Either ApiRequestError ApiRequest
|
||||
userApiRequest conf req reqBody sCache = do
|
||||
pInfo@PathInfo{..} <- getPathInfo conf $ pathInfo req
|
||||
act <- getAction pInfo method
|
||||
qPrms <- first QueryParamError $ QueryParams.parse (pathIsProc && act `elem` [ActionInvoke InvGet, ActionInvoke InvHead]) $ rawQueryString req
|
||||
(acceptMediaType, contentMediaType) <- getMediaTypes conf hdrs act pInfo
|
||||
(schema, negotiatedByProfile) <- getSchema conf hdrs method
|
||||
(topLevelRange, ranges) <- getRanges method qPrms hdrs
|
||||
(payload, columns) <- getPayload reqBody contentMediaType qPrms act pInfo
|
||||
@@ -153,7 +151,7 @@ userApiRequest conf req reqBody = do
|
||||
, iRange = ranges
|
||||
, iTopLevelRange = topLevelRange
|
||||
, iPayload = payload
|
||||
, iPreferences = Preferences.fromHeaders (configDbTxAllowOverride conf) hdrs
|
||||
, iPreferences = Preferences.fromHeaders (configDbTxAllowOverride conf) (dbTimezones sCache) hdrs
|
||||
, iQueryParams = qPrms
|
||||
, iColumns = columns
|
||||
, iHeaders = iHdrs
|
||||
@@ -162,7 +160,7 @@ userApiRequest conf req reqBody = do
|
||||
, iMethod = method
|
||||
, iSchema = schema
|
||||
, iNegotiatedByProfile = negotiatedByProfile
|
||||
, iAcceptMediaType = acceptMediaType
|
||||
, iAcceptMediaType = maybe [MTAny] (map MediaType.decodeMediaType . parseHttpAccept) $ lookupHeader "accept"
|
||||
, iContentMediaType = contentMediaType
|
||||
}
|
||||
where
|
||||
@@ -171,6 +169,7 @@ userApiRequest conf req reqBody = do
|
||||
lookupHeader = flip lookup hdrs
|
||||
iHdrs = [ (CI.foldedCase k, v) | (k,v) <- hdrs, k /= hCookie]
|
||||
iCkies = maybe [] parseCookies $ lookupHeader "Cookie"
|
||||
contentMediaType = maybe MTApplicationJSON MediaType.decodeMediaType $ lookupHeader "content-type"
|
||||
|
||||
getPathInfo :: AppConfig -> [Text] -> Either ApiRequestError PathInfo
|
||||
getPathInfo AppConfig{configOpenApiMode, configDbRootSpec} path =
|
||||
@@ -204,15 +203,6 @@ getAction PathInfo{pathIsProc, pathIsDefSpec} method =
|
||||
"OPTIONS" -> Right ActionInfo
|
||||
_ -> Left $ UnsupportedMethod method
|
||||
|
||||
getMediaTypes :: AppConfig -> RequestHeaders -> Action -> PathInfo -> Either ApiRequestError (MediaType, MediaType)
|
||||
getMediaTypes conf hdrs action path = do
|
||||
acceptMediaType <- negotiateContent conf action path accepts
|
||||
pure (acceptMediaType, contentMediaType)
|
||||
where
|
||||
accepts = maybe [MTAny] (map MediaType.decodeMediaType . parseHttpAccept) $ lookupHeader "accept"
|
||||
contentMediaType = maybe MTApplicationJSON MediaType.decodeMediaType $ lookupHeader "content-type"
|
||||
lookupHeader = flip lookup hdrs
|
||||
|
||||
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
|
||||
getSchema AppConfig{configDbSchemas} hdrs method = do
|
||||
case profile of
|
||||
@@ -269,7 +259,9 @@ getPayload reqBody contentMediaType QueryParams{qsColumns} action PathInfo{pathI
|
||||
else note "All object keys must match" . payloadAttributes reqBody
|
||||
=<< if LBS.null reqBody && pathIsProc
|
||||
then Right emptyObject
|
||||
else first BS.pack $ JSON.eitherDecode reqBody
|
||||
else first BS.pack $
|
||||
-- Drop parsing error message in favor of generic one (https://github.com/PostgREST/postgrest/issues/2344)
|
||||
maybe (Left "Empty or invalid json") Right $ JSON.decode reqBody
|
||||
(MTTextCSV, _) -> do
|
||||
json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)
|
||||
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
|
||||
@@ -346,34 +338,3 @@ payloadAttributes raw json =
|
||||
_ -> Just emptyPJArray
|
||||
where
|
||||
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
|
||||
|
||||
|
||||
-- | Do content negotiation. i.e. choose a media type based on the intersection of accepted/produced media types.
|
||||
negotiateContent :: AppConfig -> Action -> PathInfo -> [MediaType] -> Either ApiRequestError MediaType
|
||||
negotiateContent conf action path accepts =
|
||||
case firstAcceptedPick of
|
||||
Just MTAny -> Right MTApplicationJSON -- by default(for */*) we respond with json
|
||||
Just mt -> Right mt
|
||||
Nothing -> Left . MediaTypeError $ map MediaType.toMime accepts
|
||||
where
|
||||
-- if there are multiple accepted media types, pick the first
|
||||
firstAcceptedPick = listToMaybe $ L.intersect accepts $ producedMediaTypes conf action path
|
||||
|
||||
producedMediaTypes :: AppConfig -> Action -> PathInfo -> [MediaType]
|
||||
producedMediaTypes conf action path =
|
||||
case action of
|
||||
ActionRead _ -> defaultMediaTypes ++ rawMediaTypes
|
||||
ActionInvoke _ -> invokeMediaTypes
|
||||
ActionInfo -> defaultMediaTypes
|
||||
ActionMutate _ -> defaultMediaTypes
|
||||
ActionInspect _ -> inspectMediaTypes
|
||||
where
|
||||
inspectMediaTypes = [MTOpenAPI, MTApplicationJSON, MTArrayJSONStrip, MTAny]
|
||||
invokeMediaTypes =
|
||||
defaultMediaTypes
|
||||
++ rawMediaTypes
|
||||
++ [MTOpenAPI | pathIsRootSpec path]
|
||||
defaultMediaTypes =
|
||||
[MTApplicationJSON, MTArrayJSONStrip, MTSingularJSON True, MTSingularJSON False, MTGeoJSON, MTTextCSV] ++
|
||||
[MTPlan MTApplicationJSON PlanText mempty | configDbPlanEnabled conf] ++ [MTAny]
|
||||
rawMediaTypes = configRawMediaTypes conf `union` [MTOctetStream, MTTextPlain, MTTextXML]
|
||||
|
||||
@@ -10,11 +10,13 @@
|
||||
module PostgREST.ApiRequest.Preferences
|
||||
( Preferences(..)
|
||||
, PreferCount(..)
|
||||
, PreferHandling(..)
|
||||
, PreferMissing(..)
|
||||
, PreferParameters(..)
|
||||
, PreferRepresentation(..)
|
||||
, PreferResolution(..)
|
||||
, PreferTransaction(..)
|
||||
, PreferTimezone(..)
|
||||
, fromHeaders
|
||||
, shouldCount
|
||||
, prefAppliedHeader
|
||||
@@ -22,10 +24,12 @@ module PostgREST.ApiRequest.Preferences
|
||||
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.Map as Map
|
||||
import qualified Data.Set as S
|
||||
import qualified Network.HTTP.Types.Header as HTTP
|
||||
|
||||
import Protolude
|
||||
import PostgREST.Config.Database (TimezoneNames)
|
||||
|
||||
import Protolude
|
||||
|
||||
-- $setup
|
||||
-- Setup for doctests
|
||||
@@ -36,6 +40,8 @@ import Protolude
|
||||
-- >>> deriving instance Show PreferCount
|
||||
-- >>> deriving instance Show PreferTransaction
|
||||
-- >>> deriving instance Show PreferMissing
|
||||
-- >>> deriving instance Show PreferHandling
|
||||
-- >>> deriving instance Show PreferTimezone
|
||||
-- >>> deriving instance Show Preferences
|
||||
|
||||
-- | Preferences recognized by the application.
|
||||
@@ -47,14 +53,18 @@ data Preferences
|
||||
, preferCount :: Maybe PreferCount
|
||||
, preferTransaction :: Maybe PreferTransaction
|
||||
, preferMissing :: Maybe PreferMissing
|
||||
, preferHandling :: Maybe PreferHandling
|
||||
, preferTimezone :: Maybe PreferTimezone
|
||||
, invalidPrefs :: [ByteString]
|
||||
}
|
||||
|
||||
-- |
|
||||
-- Parse HTTP headers based on RFC7240[1] to identify preferences.
|
||||
--
|
||||
-- One header with comma-separated values can be used to set multiple preferences:
|
||||
-- >>> let sc = S.fromList ["America/Los_Angeles"]
|
||||
--
|
||||
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates, count=exact")]
|
||||
-- One header with comma-separated values can be used to set multiple preferences:
|
||||
-- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates, count=exact, timezone=America/Los_Angeles")]
|
||||
-- Preferences
|
||||
-- { preferResolution = Just IgnoreDuplicates
|
||||
-- , preferRepresentation = Nothing
|
||||
@@ -62,11 +72,15 @@ data Preferences
|
||||
-- , preferCount = Just ExactCount
|
||||
-- , preferTransaction = Nothing
|
||||
-- , preferMissing = Nothing
|
||||
-- , preferHandling = Nothing
|
||||
-- , preferTimezone = Just
|
||||
-- ( PreferTimezone "America/Los_Angeles" )
|
||||
-- , invalidPrefs = []
|
||||
-- }
|
||||
--
|
||||
-- Multiple headers can also be used:
|
||||
--
|
||||
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null")]
|
||||
-- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null"), ("Prefer", "handling=lenient"), ("Prefer", "invalid")]
|
||||
-- Preferences
|
||||
-- { preferResolution = Just IgnoreDuplicates
|
||||
-- , preferRepresentation = Nothing
|
||||
@@ -74,31 +88,30 @@ data Preferences
|
||||
-- , preferCount = Just ExactCount
|
||||
-- , preferTransaction = Nothing
|
||||
-- , preferMissing = Just ApplyNulls
|
||||
-- , preferHandling = Just Lenient
|
||||
-- , preferTimezone = Nothing
|
||||
-- , invalidPrefs = [ "invalid" ]
|
||||
-- }
|
||||
--
|
||||
-- If a preference is set more than once, only the first is used:
|
||||
--
|
||||
-- >>> preferTransaction $ fromHeaders True [("Prefer", "tx=commit, tx=rollback")]
|
||||
-- >>> preferTransaction $ fromHeaders True sc [("Prefer", "tx=commit, tx=rollback")]
|
||||
-- Just Commit
|
||||
--
|
||||
-- This is also the case across multiple headers:
|
||||
--
|
||||
-- >>> :{
|
||||
-- preferResolution . fromHeaders True $
|
||||
-- preferResolution . fromHeaders True sc $
|
||||
-- [ ("Prefer", "resolution=ignore-duplicates")
|
||||
-- , ("Prefer", "resolution=merge-duplicates")
|
||||
-- ]
|
||||
-- :}
|
||||
-- Just IgnoreDuplicates
|
||||
--
|
||||
-- Preferences not recognized by the application are ignored:
|
||||
--
|
||||
-- >>> preferResolution $ fromHeaders True [("Prefer", "resolution=foo")]
|
||||
-- Nothing
|
||||
--
|
||||
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
|
||||
--
|
||||
-- >>> pPrint $ fromHeaders True [("prefer", "count=exact, tx=commit ,return=representation , missing=default")]
|
||||
-- >>> pPrint $ fromHeaders True sc [("prefer", "count=exact, tx=commit ,return=representation , missing=default, handling=strict, anything")]
|
||||
-- Preferences
|
||||
-- { preferResolution = Nothing
|
||||
-- , preferRepresentation = Just Full
|
||||
@@ -106,22 +119,43 @@ data Preferences
|
||||
-- , preferCount = Just ExactCount
|
||||
-- , preferTransaction = Just Commit
|
||||
-- , preferMissing = Just ApplyDefaults
|
||||
-- , preferHandling = Just Strict
|
||||
-- , preferTimezone = Nothing
|
||||
-- , invalidPrefs = [ "anything" ]
|
||||
-- }
|
||||
--
|
||||
fromHeaders :: Bool -> [HTTP.Header] -> Preferences
|
||||
fromHeaders allowTxEndOverride headers =
|
||||
fromHeaders :: Bool -> TimezoneNames -> [HTTP.Header] -> Preferences
|
||||
fromHeaders allowTxDbOverride acceptedTzNames headers =
|
||||
Preferences
|
||||
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
|
||||
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
|
||||
, preferParameters = parsePrefs [SingleObject]
|
||||
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
||||
, preferTransaction = if allowTxEndOverride then parsePrefs [Commit, Rollback] else Nothing
|
||||
, preferTransaction = if allowTxDbOverride then parsePrefs [Commit, Rollback] else Nothing
|
||||
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
|
||||
, preferHandling = parsePrefs [Strict, Lenient]
|
||||
, preferTimezone = if isTimezonePrefAccepted then PreferTimezone <$> timezonePref else Nothing
|
||||
, invalidPrefs = filter checkPrefs prefs
|
||||
}
|
||||
where
|
||||
mapToHeadVal :: ToHeaderValue a => [a] -> [ByteString]
|
||||
mapToHeadVal = map toHeaderValue
|
||||
acceptedPrefs = mapToHeadVal [MergeDuplicates, IgnoreDuplicates] ++
|
||||
mapToHeadVal [Full, None, HeadersOnly] ++
|
||||
mapToHeadVal [SingleObject] ++
|
||||
mapToHeadVal [ExactCount, PlannedCount, EstimatedCount] ++
|
||||
mapToHeadVal [Commit, Rollback] ++
|
||||
mapToHeadVal [ApplyDefaults, ApplyNulls] ++
|
||||
mapToHeadVal [Strict, Lenient]
|
||||
|
||||
prefHeaders = filter ((==) HTTP.hPrefer . fst) headers
|
||||
prefs = fmap BS.strip . concatMap (BS.split ',' . snd) $ prefHeaders
|
||||
|
||||
timezonePref = listToMaybe $ mapMaybe (BS.stripPrefix "timezone=") prefs
|
||||
isTimezonePrefAccepted = (S.member <$> timezonePref <*> pure acceptedTzNames) == Just True
|
||||
|
||||
checkPrefs p = p `notElem` acceptedPrefs && not isTimezonePrefAccepted
|
||||
|
||||
parsePrefs :: ToHeaderValue a => [a] -> Maybe a
|
||||
parsePrefs vals =
|
||||
head $ mapMaybe (flip Map.lookup $ prefMap vals) prefs
|
||||
@@ -130,7 +164,7 @@ fromHeaders allowTxEndOverride headers =
|
||||
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
|
||||
|
||||
prefAppliedHeader :: Preferences -> Maybe HTTP.Header
|
||||
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing } =
|
||||
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone } =
|
||||
if null prefsVals
|
||||
then Nothing
|
||||
else Just (HTTP.hPreferenceApplied, combined)
|
||||
@@ -143,6 +177,8 @@ prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferPar
|
||||
, toHeaderValue <$> preferParameters
|
||||
, toHeaderValue <$> preferCount
|
||||
, toHeaderValue <$> preferTransaction
|
||||
, toHeaderValue <$> preferHandling
|
||||
, toHeaderValue <$> preferTimezone
|
||||
]
|
||||
|
||||
-- |
|
||||
@@ -158,6 +194,7 @@ class ToHeaderValue a where
|
||||
data PreferResolution
|
||||
= MergeDuplicates
|
||||
| IgnoreDuplicates
|
||||
deriving Eq
|
||||
|
||||
instance ToHeaderValue PreferResolution where
|
||||
toHeaderValue MergeDuplicates = "resolution=merge-duplicates"
|
||||
@@ -223,3 +260,21 @@ data PreferMissing
|
||||
instance ToHeaderValue PreferMissing where
|
||||
toHeaderValue ApplyDefaults = "missing=default"
|
||||
toHeaderValue ApplyNulls = "missing=null"
|
||||
|
||||
-- |
|
||||
-- Handling of unrecognised preferences
|
||||
data PreferHandling
|
||||
= Strict -- ^ Throw error on unrecognised preferences
|
||||
| Lenient -- ^ Ignore unrecognised preferences
|
||||
deriving Eq
|
||||
|
||||
instance ToHeaderValue PreferHandling where
|
||||
toHeaderValue Strict = "handling=strict"
|
||||
toHeaderValue Lenient = "handling=lenient"
|
||||
|
||||
-- |
|
||||
-- Change timezone
|
||||
newtype PreferTimezone = PreferTimezone ByteString
|
||||
|
||||
instance ToHeaderValue PreferTimezone where
|
||||
toHeaderValue (PreferTimezone tz) = "timezone=" <> tz
|
||||
|
||||
@@ -31,8 +31,8 @@ import Data.Tree (Tree (..))
|
||||
import Text.Parsec.Error (errorMessages,
|
||||
showErrorMessages)
|
||||
import Text.ParserCombinators.Parsec (GenParser, ParseError, Parser,
|
||||
anyChar, between, char, digit,
|
||||
eof, errorPos, letter,
|
||||
anyChar, between, char, choice,
|
||||
digit, eof, errorPos, letter,
|
||||
lookAhead, many1, noneOf,
|
||||
notFollowedBy, oneOf,
|
||||
optionMaybe, sepBy, sepBy1,
|
||||
@@ -43,7 +43,8 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
rangeOffset, restrictRange)
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName)
|
||||
|
||||
import PostgREST.ApiRequest.Types (EmbedParam (..), EmbedPath, Field,
|
||||
import PostgREST.ApiRequest.Types (AggregateFunction (..),
|
||||
EmbedParam (..), EmbedPath, Field,
|
||||
Filter (..), FtsOperator (..),
|
||||
Hint, JoinType (..),
|
||||
JsonOperand (..),
|
||||
@@ -58,7 +59,7 @@ import PostgREST.ApiRequest.Types (EmbedParam (..), EmbedPath, Field,
|
||||
SimpleOperator (..), SingleVal,
|
||||
TrileanVal (..))
|
||||
|
||||
import Protolude hiding (try)
|
||||
import Protolude hiding (Sum, try)
|
||||
|
||||
data QueryParams =
|
||||
QueryParams
|
||||
@@ -99,7 +100,7 @@ data QueryParams =
|
||||
-- 'select' is a reserved parameter that selects the fields to be returned:
|
||||
--
|
||||
-- >>> qsSelect <$> parse False "select=name,location"
|
||||
-- Right [Node {rootLabel = SelectField {selField = ("name",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectField {selField = ("location",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]
|
||||
-- Right [Node {rootLabel = SelectField {selField = ("name",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectField {selField = ("location",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]
|
||||
--
|
||||
-- Filters are parameters whose value contains an operator, separated by a '.' from its value:
|
||||
--
|
||||
@@ -282,16 +283,16 @@ pTreePath = do
|
||||
-- Parse select= into a Forest of SelectItems
|
||||
--
|
||||
-- >>> P.parse pFieldForest "" "id"
|
||||
-- Right [Node {rootLabel = SelectField {selField = ("id",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]
|
||||
-- Right [Node {rootLabel = SelectField {selField = ("id",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]
|
||||
--
|
||||
-- >>> P.parse pFieldForest "" "client(id)"
|
||||
-- Right [Node {rootLabel = SelectRelation {selRelation = "client", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("id",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
|
||||
-- Right [Node {rootLabel = SelectRelation {selRelation = "client", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("id",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
|
||||
--
|
||||
-- >>> P.parse pFieldForest "" "*,client(*,nested(*))"
|
||||
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "client", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "nested", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]}]
|
||||
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "client", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "nested", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]}]}]
|
||||
--
|
||||
-- >>> P.parse pFieldForest "" "*,...client(*),other(*)"
|
||||
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SpreadRelation {selRelation = "client", selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]},Node {rootLabel = SelectRelation {selRelation = "other", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
|
||||
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SpreadRelation {selRelation = "client", selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]},Node {rootLabel = SelectRelation {selRelation = "other", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
|
||||
--
|
||||
-- >>> P.parse pFieldForest "" ""
|
||||
-- Right []
|
||||
@@ -299,7 +300,7 @@ pTreePath = do
|
||||
-- >>> P.parse pFieldForest "" "id,clients(name[])"
|
||||
-- Left (line 1, column 16):
|
||||
-- unexpected '['
|
||||
-- expecting letter, digit, "-", "->>", "->", "::", ")", "," or end of input
|
||||
-- expecting letter, digit, "-", "->>", "->", "::", ".", ")", "," or end of input
|
||||
--
|
||||
-- >>> P.parse pFieldForest "" "data->>-78xy"
|
||||
-- Left (line 1, column 11):
|
||||
@@ -349,8 +350,12 @@ pFieldForest = pFieldTree `sepBy` lexeme (char ',')
|
||||
pFieldName :: Parser Text
|
||||
pFieldName =
|
||||
pQuotedValue <|>
|
||||
T.intercalate "-" . map toS <$> (pIdentifier `sepBy1` dash) <?>
|
||||
sepByDash pIdentifier <?>
|
||||
"field name (* or [a..z0..9_$])"
|
||||
|
||||
sepByDash :: Parser Text -> Parser Text
|
||||
sepByDash fieldIdent =
|
||||
T.intercalate "-" . map toS <$> (fieldIdent `sepBy1` dash)
|
||||
where
|
||||
isDash :: GenParser Char st ()
|
||||
isDash = try ( char '-' >> notFollowedBy (char '>') )
|
||||
@@ -363,12 +368,18 @@ pFieldName =
|
||||
-- >>> P.parse pJsonPath "" "->text"
|
||||
-- Right [JArrow {jOp = JKey {jVal = "text"}}]
|
||||
--
|
||||
-- >>> P.parse pJsonPath "" "->!@#$%^&*_a"
|
||||
-- Right [JArrow {jOp = JKey {jVal = "!@#$%^&*_a"}}]
|
||||
--
|
||||
-- >>> P.parse pJsonPath "" "->1"
|
||||
-- Right [JArrow {jOp = JIdx {jVal = "+1"}}]
|
||||
--
|
||||
-- >>> P.parse pJsonPath "" "->>text"
|
||||
-- Right [J2Arrow {jOp = JKey {jVal = "text"}}]
|
||||
--
|
||||
-- >>> P.parse pJsonPath "" "->>!@#$%^&*_a"
|
||||
-- Right [J2Arrow {jOp = JKey {jVal = "!@#$%^&*_a"}}]
|
||||
--
|
||||
-- >>> P.parse pJsonPath "" "->>1"
|
||||
-- Right [J2Arrow {jOp = JIdx {jVal = "+1"}}]
|
||||
--
|
||||
@@ -405,7 +416,7 @@ pJsonPath = many pJsonOperation
|
||||
try (string "->" $> JArrow)
|
||||
|
||||
pJsonOperand =
|
||||
let pJKey = JKey . toS <$> pFieldName
|
||||
let pJKey = JKey . toS <$> pJsonKeyName
|
||||
pJIdx = JIdx . toS <$> ((:) <$> P.option '+' (char '-') <*> many1 digit) <* pEnd
|
||||
pEnd = try (void $ lookAhead (string "->")) <|>
|
||||
try (void $ lookAhead (string "::")) <|>
|
||||
@@ -414,6 +425,15 @@ pJsonPath = many pJsonOperation
|
||||
try eof in
|
||||
try pJIdx <|> try pJKey
|
||||
|
||||
pJsonKeyName :: Parser Text
|
||||
pJsonKeyName =
|
||||
pQuotedValue <|>
|
||||
sepByDash pJsonKeyIdentifier <?>
|
||||
"any non reserved character different from: .,>()"
|
||||
|
||||
pJsonKeyIdentifier :: Parser Text
|
||||
pJsonKeyIdentifier = T.strip . toS <$> many1 (noneOf "(-:.,>)")
|
||||
|
||||
pField :: Parser Field
|
||||
pField = lexeme $ (,) <$> pFieldName <*> P.option [] pJsonPath
|
||||
|
||||
@@ -452,35 +472,40 @@ pRelationSelect :: Parser SelectItem
|
||||
pRelationSelect = lexeme $ do
|
||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||
name <- pFieldName
|
||||
guard (name /= "count")
|
||||
(hint, jType) <- pEmbedParams
|
||||
try (void $ lookAhead (string "("))
|
||||
return $ SelectRelation name alias hint jType
|
||||
|
||||
|
||||
-- |
|
||||
-- Parse regular fields in select
|
||||
--
|
||||
-- >>> P.parse pFieldSelect "" "name"
|
||||
-- Right (SelectField {selField = ("name",[]), selCast = Nothing, selAlias = Nothing})
|
||||
-- Right (SelectField {selField = ("name",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing})
|
||||
--
|
||||
-- >>> P.parse pFieldSelect "" "name->jsonpath"
|
||||
-- Right (SelectField {selField = ("name",[JArrow {jOp = JKey {jVal = "jsonpath"}}]), selCast = Nothing, selAlias = Nothing})
|
||||
-- Right (SelectField {selField = ("name",[JArrow {jOp = JKey {jVal = "jsonpath"}}]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing})
|
||||
--
|
||||
-- >>> P.parse pFieldSelect "" "name::cast"
|
||||
-- Right (SelectField {selField = ("name",[]), selCast = Just "cast", selAlias = Nothing})
|
||||
-- Right (SelectField {selField = ("name",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Just "cast", selAlias = Nothing})
|
||||
--
|
||||
-- >>> P.parse pFieldSelect "" "alias:name"
|
||||
-- Right (SelectField {selField = ("name",[]), selCast = Nothing, selAlias = Just "alias"})
|
||||
-- Right (SelectField {selField = ("name",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Just "alias"})
|
||||
--
|
||||
-- >>> P.parse pFieldSelect "" "alias:name->jsonpath::cast"
|
||||
-- Right (SelectField {selField = ("name",[JArrow {jOp = JKey {jVal = "jsonpath"}}]), selCast = Just "cast", selAlias = Just "alias"})
|
||||
-- Right (SelectField {selField = ("name",[JArrow {jOp = JKey {jVal = "jsonpath"}}]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Just "cast", selAlias = Just "alias"})
|
||||
--
|
||||
-- >>> P.parse pFieldSelect "" "alias:name->!@#$%^&*_a::cast"
|
||||
-- Right (SelectField {selField = ("name",[JArrow {jOp = JKey {jVal = "!@#$%^&*_a"}}]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Just "cast", selAlias = Just "alias"})
|
||||
--
|
||||
-- >>> P.parse pFieldSelect "" "*"
|
||||
-- Right (SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing})
|
||||
-- Right (SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing})
|
||||
--
|
||||
-- >>> P.parse pFieldSelect "" "name!hint"
|
||||
-- Left (line 1, column 5):
|
||||
-- unexpected '!'
|
||||
-- expecting letter, digit, "-", "->>", "->", "::", ")", "," or end of input
|
||||
-- expecting letter, digit, "-", "->>", "->", "::", ".", ")", "," or end of input
|
||||
--
|
||||
-- >>> P.parse pFieldSelect "" "*!hint"
|
||||
-- Left (line 1, column 2):
|
||||
@@ -495,18 +520,36 @@ pFieldSelect :: Parser SelectItem
|
||||
pFieldSelect = lexeme $ try (do
|
||||
s <- pStar
|
||||
pEnd
|
||||
return $ SelectField (s, []) Nothing Nothing)
|
||||
<|> do
|
||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||
fld <- pField
|
||||
cast' <- optionMaybe (string "::" *> pIdentifier)
|
||||
return $ SelectField (s, []) Nothing Nothing Nothing Nothing)
|
||||
<|> try (do
|
||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||
_ <- string "count()"
|
||||
aggCast' <- optionMaybe (string "::" *> pIdentifier)
|
||||
pEnd
|
||||
return $ SelectField fld (toS <$> cast') alias
|
||||
return $ SelectField ("*", []) (Just Count) (toS <$> aggCast') Nothing alias)
|
||||
<|> do
|
||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||
fld <- pField
|
||||
cast' <- optionMaybe (string "::" *> pIdentifier)
|
||||
agg <- optionMaybe (try (char '.' *> pAggregation <* string "()"))
|
||||
aggCast' <- optionMaybe (string "::" *> pIdentifier)
|
||||
pEnd
|
||||
return $ SelectField fld agg (toS <$> aggCast') (toS <$> cast') alias
|
||||
where
|
||||
pEnd = try (void $ lookAhead (string ")")) <|>
|
||||
try (void $ lookAhead (string ",")) <|>
|
||||
try eof
|
||||
pStar = string "*" $> "*"
|
||||
pAggregation = choice
|
||||
[ string "sum" $> Sum
|
||||
, string "avg" $> Avg
|
||||
, string "count" $> Count
|
||||
-- Using 'try' for "min" and "max" to allow backtracking.
|
||||
-- This is necessary because both start with the same character 'm',
|
||||
-- and without 'try', a partial match on "max" would prevent "min" from being tried.
|
||||
, try (string "max") $> Max
|
||||
, try (string "min") $> Min
|
||||
]
|
||||
|
||||
|
||||
-- |
|
||||
@@ -657,9 +700,15 @@ pDelimiter = char '.' <?> "delimiter (.)"
|
||||
-- >>> P.parse pOrder "" "json_col->key.asc.nullslast"
|
||||
-- Right [OrderTerm {otTerm = ("json_col",[JArrow {jOp = JKey {jVal = "key"}}]), otDirection = Just OrderAsc, otNullOrder = Just OrderNullsLast}]
|
||||
--
|
||||
-- >>> P.parse pOrder "" "json_col->!@#$%^&*_a.asc.nullslast"
|
||||
-- Right [OrderTerm {otTerm = ("json_col",[JArrow {jOp = JKey {jVal = "!@#$%^&*_a"}}]), otDirection = Just OrderAsc, otNullOrder = Just OrderNullsLast}]
|
||||
--
|
||||
-- >>> P.parse pOrder "" "clients(json_col->key).desc.nullsfirst"
|
||||
-- Right [OrderRelationTerm {otRelation = "clients", otRelTerm = ("json_col",[JArrow {jOp = JKey {jVal = "key"}}]), otDirection = Just OrderDesc, otNullOrder = Just OrderNullsFirst}]
|
||||
--
|
||||
-- >>> P.parse pOrder "" "clients(json_col->!@#$%^&*_a).desc.nullsfirst"
|
||||
-- Right [OrderRelationTerm {otRelation = "clients", otRelTerm = ("json_col",[JArrow {jOp = JKey {jVal = "!@#$%^&*_a"}}]), otDirection = Just OrderDesc, otNullOrder = Just OrderNullsFirst}]
|
||||
--
|
||||
-- >>> P.parse pOrder "" "clients(name,id)"
|
||||
-- Left (line 1, column 8):
|
||||
-- unexpected '('
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
module PostgREST.ApiRequest.Types
|
||||
( Alias
|
||||
( AggregateFunction(..)
|
||||
, Alias
|
||||
, Cast
|
||||
, Depth
|
||||
, EmbedParam(..)
|
||||
@@ -42,12 +43,14 @@ import PostgREST.SchemaCache.Routine (Routine (..))
|
||||
|
||||
import Protolude
|
||||
|
||||
-- | The value in `/tbl?select=alias:field::cast`
|
||||
-- | The value in `/tbl?select=alias:field.aggregateFunction()::cast`
|
||||
data SelectItem
|
||||
= SelectField
|
||||
{ selField :: Field
|
||||
, selCast :: Maybe Cast
|
||||
, selAlias :: Maybe Alias
|
||||
{ selField :: Field
|
||||
, selAggregateFunction :: Maybe AggregateFunction
|
||||
, selAggregateCast :: Maybe Cast
|
||||
, selCast :: Maybe Cast
|
||||
, selAlias :: Maybe Alias
|
||||
}
|
||||
-- | The value in `/tbl?select=alias:another_tbl(*)`
|
||||
| SelectRelation
|
||||
@@ -65,12 +68,13 @@ data SelectItem
|
||||
deriving (Eq, Show)
|
||||
|
||||
data ApiRequestError
|
||||
= AmbiguousRelBetween Text Text [Relationship]
|
||||
= AggregatesNotAllowed
|
||||
| AmbiguousRelBetween Text Text [Relationship]
|
||||
| AmbiguousRpc [Routine]
|
||||
| BinaryFieldError MediaType
|
||||
| MediaTypeError [ByteString]
|
||||
| InvalidBody ByteString
|
||||
| InvalidFilters
|
||||
| InvalidPreferences [ByteString]
|
||||
| InvalidRange RangeError
|
||||
| InvalidRpcMethod ByteString
|
||||
| LimitNoOrderError
|
||||
@@ -86,6 +90,12 @@ data ApiRequestError
|
||||
| UnacceptableSchema [Text]
|
||||
| UnsupportedMethod ByteString
|
||||
| ColumnNotFound Text Text
|
||||
| GucHeadersError
|
||||
| GucStatusError
|
||||
| OffLimitsChangesError Int64 Integer
|
||||
| PutMatchingPkError
|
||||
| SingularityError Integer
|
||||
| PGRSTParseError
|
||||
deriving Show
|
||||
|
||||
data QPError = QPError Text Text
|
||||
@@ -128,6 +138,9 @@ type Cast = Text
|
||||
type Alias = Text
|
||||
type Hint = Text
|
||||
|
||||
data AggregateFunction = Sum | Avg | Max | Min | Count
|
||||
deriving (Show, Eq)
|
||||
|
||||
data EmbedParam
|
||||
-- | Disambiguates an embedding operation when there's multiple relationships
|
||||
-- between two tables. Can be the name of a foreign key constraint, column
|
||||
|
||||
+119
-77
@@ -9,11 +9,10 @@ Some of its functionality includes:
|
||||
- Producing HTTP Headers according to RFCs.
|
||||
- Content Negotiation
|
||||
-}
|
||||
{-# LANGUAGE NamedFieldPuns #-}
|
||||
{-# LANGUAGE RecordWildCards #-}
|
||||
module PostgREST.App
|
||||
( SignalHandlerInstaller
|
||||
, SocketRunner
|
||||
, postgrest
|
||||
( postgrest
|
||||
, run
|
||||
) where
|
||||
|
||||
@@ -24,7 +23,6 @@ import Data.Maybe (fromJust)
|
||||
import Data.String (IsString (..))
|
||||
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
||||
setServerName)
|
||||
import System.Posix.Types (FileMode)
|
||||
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.Text.Encoding as T
|
||||
@@ -43,32 +41,38 @@ import qualified PostgREST.Logger as Logger
|
||||
import qualified PostgREST.Plan as Plan
|
||||
import qualified PostgREST.Query as Query
|
||||
import qualified PostgREST.Response as Response
|
||||
import qualified PostgREST.Unix as Unix (installSignalHandlers)
|
||||
|
||||
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
|
||||
Mutation (..), Target (..))
|
||||
import PostgREST.AppState (AppState)
|
||||
import PostgREST.Auth (AuthResult (..))
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||
import PostgREST.Error (Error)
|
||||
import PostgREST.Query (DbHandler)
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.SchemaCache.Routine (Routine (..))
|
||||
import PostgREST.Version (docsVersion, prettyVersion)
|
||||
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
|
||||
Mutation (..), Target (..))
|
||||
import PostgREST.AppState (AppState)
|
||||
import PostgREST.Auth (AuthResult (..))
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||
import PostgREST.Error (Error)
|
||||
import PostgREST.Query (DbHandler)
|
||||
import PostgREST.Response.Performance (ServerTiming (..),
|
||||
serverTimingHeader)
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.SchemaCache.Routine (Routine (..))
|
||||
import PostgREST.Version (docsVersion, prettyVersion)
|
||||
|
||||
import Protolude hiding (Handler)
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.List as L
|
||||
import qualified Network.HTTP.Types as HTTP
|
||||
import qualified Network.Socket as NS
|
||||
import Protolude hiding (Handler)
|
||||
import System.TimeIt (timeItT)
|
||||
|
||||
type Handler = ExceptT Error
|
||||
|
||||
type SignalHandlerInstaller = AppState -> IO()
|
||||
run :: AppState -> IO ()
|
||||
run appState = do
|
||||
AppState.logWithZTime appState $ "Starting PostgREST " <> T.decodeUtf8 prettyVersion <> "..."
|
||||
|
||||
type SocketRunner = Warp.Settings -> Wai.Application -> FileMode -> FilePath -> IO()
|
||||
|
||||
run :: SignalHandlerInstaller -> Maybe SocketRunner -> AppState -> IO ()
|
||||
run installHandlers maybeRunWithSocket appState = do
|
||||
conf@AppConfig{..} <- AppState.getConfig appState
|
||||
AppState.connectionWorker appState -- Loads the initial SchemaCache
|
||||
installHandlers appState
|
||||
Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.connectionWorker appState) (AppState.reReadConfig False appState)
|
||||
-- reload schema cache + config on NOTIFY
|
||||
AppState.runListener conf appState
|
||||
|
||||
@@ -76,19 +80,14 @@ run installHandlers maybeRunWithSocket appState = do
|
||||
|
||||
let app = postgrest conf appState (AppState.connectionWorker appState)
|
||||
|
||||
case configServerUnixSocket of
|
||||
Just socket ->
|
||||
-- run the postgrest application with user defined socket. Only for UNIX systems
|
||||
case maybeRunWithSocket of
|
||||
Just runWithSocket -> do
|
||||
AppState.logWithZTime appState $ "Listening on unix socket " <> show socket
|
||||
runWithSocket (serverSettings conf) app configServerUnixSocketMode socket
|
||||
Nothing ->
|
||||
panic "Cannot run with unix socket on non-unix platforms."
|
||||
Nothing ->
|
||||
do
|
||||
AppState.logWithZTime appState $ "Listening on port " <> show configServerPort
|
||||
Warp.runSettings (serverSettings conf) app
|
||||
what <- case configServerUnixSocket of
|
||||
Just path -> pure $ "unix socket " <> show path
|
||||
Nothing -> do
|
||||
port <- NS.socketPort $ AppState.getSocketREST appState
|
||||
pure $ "port " <> show port
|
||||
AppState.logWithZTime appState $ "Listening on " <> what
|
||||
|
||||
Warp.runSettingsSocket (serverSettings conf) (AppState.getSocketREST appState) app
|
||||
|
||||
serverSettings :: AppConfig -> Warp.Settings
|
||||
serverSettings AppConfig{..} =
|
||||
@@ -100,8 +99,8 @@ serverSettings AppConfig{..} =
|
||||
-- | PostgREST application
|
||||
postgrest :: AppConfig -> AppState.AppState -> IO () -> Wai.Application
|
||||
postgrest conf appState connWorker =
|
||||
Response.traceHeaderMiddleware conf .
|
||||
Cors.middleware .
|
||||
traceHeaderMiddleware conf .
|
||||
Cors.middleware (configServerCorsAllowedOrigins conf) .
|
||||
Auth.middleware appState .
|
||||
Logger.middleware (configLogLevel conf) $
|
||||
-- fromJust can be used, because the auth middleware will **always** add
|
||||
@@ -122,10 +121,10 @@ postgrest conf appState connWorker =
|
||||
-- Launch the connWorker when the connection is down. The postgrest
|
||||
-- function can respond successfully (with a stale schema cache) before
|
||||
-- the connWorker is done.
|
||||
when (Response.isServiceUnavailable response) connWorker
|
||||
when (isServiceUnavailable response) connWorker
|
||||
resp <- do
|
||||
delay <- AppState.getRetryNextIn appState
|
||||
return $ Response.addRetryHint delay response
|
||||
return $ addRetryHint delay response
|
||||
respond resp
|
||||
|
||||
postgrestResponse
|
||||
@@ -146,17 +145,19 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@
|
||||
|
||||
body <- lift $ Wai.strictRequestBody req
|
||||
|
||||
apiRequest <-
|
||||
liftEither . mapLeft Error.ApiRequestError $
|
||||
ApiRequest.userApiRequest conf req body
|
||||
(parseTime, apiRequest) <-
|
||||
calcTiming configServerTimingEnabled $
|
||||
liftEither . mapLeft Error.ApiRequestError $
|
||||
ApiRequest.userApiRequest conf req body sCache
|
||||
|
||||
handleRequest authResult conf appState (Just authRole /= configDbAnonRole) configDbPreparedStatements pgVer apiRequest sCache
|
||||
let jwtTime = if configServerTimingEnabled then Auth.getJwtDur req else Nothing
|
||||
handleRequest authResult conf appState (Just authRole /= configDbAnonRole) configDbPreparedStatements pgVer apiRequest sCache jwtTime parseTime
|
||||
|
||||
runDbHandler :: AppState.AppState -> SQL.IsolationLevel -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
|
||||
runDbHandler appState isoLvl mode authenticated prepared handler = do
|
||||
runDbHandler :: AppState.AppState -> AppConfig -> SQL.IsolationLevel -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
|
||||
runDbHandler appState config isoLvl mode authenticated prepared handler = do
|
||||
dbResp <- lift $ do
|
||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction
|
||||
AppState.usePool appState . transaction isoLvl mode $ runExceptT handler
|
||||
AppState.usePool appState config . transaction isoLvl mode $ runExceptT handler
|
||||
|
||||
resp <-
|
||||
liftEither . mapLeft Error.PgErr $
|
||||
@@ -164,52 +165,63 @@ runDbHandler appState isoLvl mode authenticated prepared handler = do
|
||||
|
||||
liftEither resp
|
||||
|
||||
handleRequest :: AuthResult -> AppConfig -> AppState.AppState -> Bool -> Bool -> PgVersion -> ApiRequest -> SchemaCache -> Handler IO Wai.Response
|
||||
handleRequest AuthResult{..} conf appState authenticated prepared pgVer apiReq@ApiRequest{..} sCache =
|
||||
handleRequest :: AuthResult -> AppConfig -> AppState.AppState -> Bool -> Bool -> PgVersion -> ApiRequest -> SchemaCache -> Maybe Double -> Maybe Double -> Handler IO Wai.Response
|
||||
handleRequest AuthResult{..} conf appState authenticated prepared pgVer apiReq@ApiRequest{..} sCache jwtTime parseTime =
|
||||
case (iAction, iTarget) of
|
||||
(ActionRead headersOnly, TargetIdent identifier) -> do
|
||||
wrPlan <- liftEither $ Plan.wrappedReadPlan identifier conf sCache apiReq
|
||||
resultSet <- runQuery roleIsoLvl (Plan.wrTxMode wrPlan) $ Query.readQuery wrPlan conf apiReq
|
||||
return $ Response.readResponse headersOnly identifier apiReq resultSet
|
||||
(planTime', wrPlan) <- withTiming $ liftEither $ Plan.wrappedReadPlan identifier conf sCache apiReq
|
||||
(txTime', resultSet) <- withTiming $ runQuery roleIsoLvl Nothing (Plan.wrTxMode wrPlan) $ Query.readQuery wrPlan conf apiReq
|
||||
(respTime', pgrst) <- withTiming $ liftEither $ Response.readResponse wrPlan headersOnly identifier apiReq resultSet
|
||||
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
|
||||
|
||||
(ActionMutate MutationCreate, TargetIdent identifier) -> do
|
||||
mrPlan <- liftEither $ Plan.mutateReadPlan MutationCreate apiReq identifier conf sCache
|
||||
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.createQuery mrPlan apiReq conf
|
||||
return $ Response.createResponse identifier mrPlan apiReq resultSet
|
||||
(planTime', mrPlan) <- withTiming $ liftEither $ Plan.mutateReadPlan MutationCreate apiReq identifier conf sCache
|
||||
(txTime', resultSet) <- withTiming $ runQuery roleIsoLvl Nothing (Plan.mrTxMode mrPlan) $ Query.createQuery mrPlan apiReq conf
|
||||
(respTime', pgrst) <- withTiming $ liftEither $ Response.createResponse identifier mrPlan apiReq resultSet
|
||||
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
|
||||
|
||||
(ActionMutate MutationUpdate, TargetIdent identifier) -> do
|
||||
mrPlan <- liftEither $ Plan.mutateReadPlan MutationUpdate apiReq identifier conf sCache
|
||||
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.updateQuery mrPlan apiReq conf
|
||||
return $ Response.updateResponse apiReq resultSet
|
||||
(planTime', mrPlan) <- withTiming $ liftEither $ Plan.mutateReadPlan MutationUpdate apiReq identifier conf sCache
|
||||
(txTime', resultSet) <- withTiming $ runQuery roleIsoLvl Nothing (Plan.mrTxMode mrPlan) $ Query.updateQuery mrPlan apiReq conf
|
||||
(respTime', pgrst) <- withTiming $ liftEither $ Response.updateResponse mrPlan apiReq resultSet
|
||||
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
|
||||
|
||||
(ActionMutate MutationSingleUpsert, TargetIdent identifier) -> do
|
||||
mrPlan <- liftEither $ Plan.mutateReadPlan MutationSingleUpsert apiReq identifier conf sCache
|
||||
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.singleUpsertQuery mrPlan apiReq conf
|
||||
return $ Response.singleUpsertResponse apiReq resultSet
|
||||
(planTime', mrPlan) <- withTiming $ liftEither $ Plan.mutateReadPlan MutationSingleUpsert apiReq identifier conf sCache
|
||||
(txTime', resultSet) <- withTiming $ runQuery roleIsoLvl Nothing (Plan.mrTxMode mrPlan) $ Query.singleUpsertQuery mrPlan apiReq conf
|
||||
(respTime', pgrst) <- withTiming $ liftEither $ Response.singleUpsertResponse mrPlan apiReq resultSet
|
||||
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
|
||||
|
||||
(ActionMutate MutationDelete, TargetIdent identifier) -> do
|
||||
mrPlan <- liftEither $ Plan.mutateReadPlan MutationDelete apiReq identifier conf sCache
|
||||
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.deleteQuery mrPlan apiReq conf
|
||||
return $ Response.deleteResponse apiReq resultSet
|
||||
(planTime', mrPlan) <- withTiming $ liftEither $ Plan.mutateReadPlan MutationDelete apiReq identifier conf sCache
|
||||
(txTime', resultSet) <- withTiming $ runQuery roleIsoLvl Nothing (Plan.mrTxMode mrPlan) $ Query.deleteQuery mrPlan apiReq conf
|
||||
(respTime', pgrst) <- withTiming $ liftEither $ Response.deleteResponse mrPlan apiReq resultSet
|
||||
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
|
||||
|
||||
(ActionInvoke invMethod, TargetProc identifier _) -> do
|
||||
cPlan <- liftEither $ Plan.callReadPlan identifier conf sCache apiReq invMethod
|
||||
resultSet <- runQuery (fromMaybe roleIsoLvl $ pdIsoLvl (Plan.crProc cPlan))(Plan.crTxMode cPlan) $ Query.invokeQuery (Plan.crProc cPlan) cPlan apiReq conf pgVer
|
||||
return $ Response.invokeResponse invMethod (Plan.crProc cPlan) apiReq resultSet
|
||||
(planTime', cPlan) <- withTiming $ liftEither $ Plan.callReadPlan identifier conf sCache apiReq invMethod
|
||||
(txTime', resultSet) <- withTiming $ runQuery (fromMaybe roleIsoLvl $ pdIsoLvl (Plan.crProc cPlan)) (pdTimeout $ Plan.crProc cPlan) (Plan.crTxMode cPlan) $ Query.invokeQuery (Plan.crProc cPlan) cPlan apiReq conf pgVer
|
||||
(respTime', pgrst) <- withTiming $ liftEither $ Response.invokeResponse cPlan invMethod (Plan.crProc cPlan) apiReq resultSet
|
||||
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
|
||||
|
||||
(ActionInspect headersOnly, TargetDefaultSpec tSchema) -> do
|
||||
oaiResult <- runQuery roleIsoLvl Plan.inspectPlanTxMode $ Query.openApiQuery sCache pgVer conf tSchema
|
||||
return $ Response.openApiResponse (T.decodeUtf8 prettyVersion, docsVersion) headersOnly oaiResult conf sCache iSchema iNegotiatedByProfile
|
||||
(planTime', iPlan) <- withTiming $ liftEither $ Plan.inspectPlan apiReq
|
||||
(txTime', oaiResult) <- withTiming $ runQuery roleIsoLvl Nothing (Plan.ipTxmode iPlan) $ Query.openApiQuery sCache pgVer conf tSchema
|
||||
(respTime', pgrst) <- withTiming $ liftEither $ Response.openApiResponse (T.decodeUtf8 prettyVersion, docsVersion) headersOnly oaiResult conf sCache iSchema iNegotiatedByProfile
|
||||
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
|
||||
|
||||
(ActionInfo, TargetIdent identifier) ->
|
||||
return $ Response.infoIdentResponse identifier sCache
|
||||
(ActionInfo, TargetIdent identifier) -> do
|
||||
(respTime', pgrst) <- withTiming $ liftEither $ Response.infoIdentResponse identifier sCache
|
||||
return $ pgrstResponse (ServerTiming jwtTime parseTime Nothing Nothing respTime') pgrst
|
||||
|
||||
(ActionInfo, TargetProc identifier _) -> do
|
||||
cPlan <- liftEither $ Plan.callReadPlan identifier conf sCache apiReq ApiRequest.InvHead
|
||||
return $ Response.infoProcResponse (Plan.crProc cPlan)
|
||||
(planTime', cPlan) <- withTiming $ liftEither $ Plan.callReadPlan identifier conf sCache apiReq ApiRequest.InvHead
|
||||
(respTime', pgrst) <- withTiming $ liftEither $ Response.infoProcResponse (Plan.crProc cPlan)
|
||||
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' Nothing respTime') pgrst
|
||||
|
||||
(ActionInfo, TargetDefaultSpec _) ->
|
||||
return Response.infoRootResponse
|
||||
(ActionInfo, TargetDefaultSpec _) -> do
|
||||
(respTime', pgrst) <- withTiming $ liftEither Response.infoRootResponse
|
||||
return $ pgrstResponse (ServerTiming jwtTime parseTime Nothing Nothing respTime') pgrst
|
||||
|
||||
_ ->
|
||||
-- This is unreachable as the ApiRequest.hs rejects it before
|
||||
@@ -218,8 +230,38 @@ handleRequest AuthResult{..} conf appState authenticated prepared pgVer apiReq@A
|
||||
where
|
||||
roleSettings = fromMaybe mempty (HM.lookup authRole $ configRoleSettings conf)
|
||||
roleIsoLvl = HM.findWithDefault SQL.ReadCommitted authRole $ configRoleIsoLvl conf
|
||||
runQuery isoLvl mode query =
|
||||
runDbHandler appState isoLvl mode authenticated prepared $ do
|
||||
Query.setPgLocals conf authClaims authRole (HM.toList roleSettings) apiReq pgVer
|
||||
runQuery isoLvl timeout mode query =
|
||||
runDbHandler appState conf isoLvl mode authenticated prepared $ do
|
||||
Query.setPgLocals conf authClaims authRole (HM.toList roleSettings) apiReq timeout
|
||||
Query.runPreReq conf
|
||||
query
|
||||
|
||||
pgrstResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response
|
||||
pgrstResponse timing (Response.PgrstResponse st hdrs bod) = Wai.responseLBS st (hdrs ++ ([serverTimingHeader timing | configServerTimingEnabled conf])) bod
|
||||
|
||||
withTiming = calcTiming $ configServerTimingEnabled conf
|
||||
|
||||
calcTiming :: Bool -> Handler IO a -> Handler IO (Maybe Double, a)
|
||||
calcTiming timingEnabled f = if timingEnabled
|
||||
then do
|
||||
(t, r) <- timeItT f
|
||||
pure (Just t, r)
|
||||
else do
|
||||
r <- f
|
||||
pure (Nothing, r)
|
||||
|
||||
traceHeaderMiddleware :: AppConfig -> Wai.Middleware
|
||||
traceHeaderMiddleware AppConfig{configServerTraceHeader} app req respond =
|
||||
case configServerTraceHeader of
|
||||
Nothing -> app req respond
|
||||
Just hdr ->
|
||||
let hdrVal = L.lookup hdr $ Wai.requestHeaders req in
|
||||
app req (respond . Wai.mapResponseHeaders ([(hdr, fromMaybe mempty hdrVal)] ++))
|
||||
|
||||
addRetryHint :: Int -> Wai.Response -> Wai.Response
|
||||
addRetryHint delay response = do
|
||||
let h = ("Retry-After", BS.pack $ show delay)
|
||||
Wai.mapResponseHeaders (\hs -> if isServiceUnavailable response then h:hs else hs) response
|
||||
|
||||
isServiceUnavailable :: Wai.Response -> Bool
|
||||
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
|
||||
|
||||
+107
-23
@@ -4,6 +4,7 @@
|
||||
|
||||
module PostgREST.AppState
|
||||
( AppState
|
||||
, AuthResult(..)
|
||||
, destroy
|
||||
, getConfig
|
||||
, getSchemaCache
|
||||
@@ -12,7 +13,11 @@ module PostgREST.AppState
|
||||
, getPgVersion
|
||||
, getRetryNextIn
|
||||
, getTime
|
||||
, getJwtCache
|
||||
, getSocketREST
|
||||
, getSocketAdmin
|
||||
, init
|
||||
, initSockets
|
||||
, initWithPool
|
||||
, logWithZTime
|
||||
, putSchemaCache
|
||||
@@ -24,15 +29,21 @@ module PostgREST.AppState
|
||||
, runListener
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import qualified Data.Cache as C
|
||||
import Data.Either.Combinators (whenLeft)
|
||||
import qualified Data.Text as T (unpack)
|
||||
import qualified Data.Text.Encoding as T
|
||||
import Hasql.Connection (acquire)
|
||||
import qualified Hasql.Notifications as SQL
|
||||
import qualified Hasql.Pool as SQL
|
||||
import qualified Hasql.Session as SQL
|
||||
import qualified Hasql.Transaction.Sessions as SQL
|
||||
import qualified Network.HTTP.Types.Status as HTTP
|
||||
import qualified Network.Socket as NS
|
||||
import qualified PostgREST.Error as Error
|
||||
import PostgREST.Version (prettyVersion)
|
||||
|
||||
@@ -48,6 +59,7 @@ import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
|
||||
import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||
|
||||
import PostgREST.Config (AppConfig (..),
|
||||
LogLevel (..),
|
||||
addFallbackAppName,
|
||||
readAppConfig)
|
||||
import PostgREST.Config.Database (queryDbSettings,
|
||||
@@ -58,9 +70,16 @@ import PostgREST.Config.PgVersion (PgVersion (..),
|
||||
import PostgREST.SchemaCache (SchemaCache,
|
||||
querySchemaCache)
|
||||
import PostgREST.SchemaCache.Identifiers (dumpQi)
|
||||
import PostgREST.Unix (createAndBindDomainSocket)
|
||||
|
||||
import Data.Streaming.Network (bindPortTCP, bindRandomPortTCP)
|
||||
import Data.String (IsString (..))
|
||||
import Protolude
|
||||
|
||||
data AuthResult = AuthResult
|
||||
{ authClaims :: KM.KeyMap JSON.Value
|
||||
, authRole :: BS.ByteString
|
||||
}
|
||||
|
||||
data AppState = AppState
|
||||
-- | Database connection pool
|
||||
@@ -87,15 +106,25 @@ data AppState = AppState
|
||||
, stateRetryNextIn :: IORef Int
|
||||
-- | Logs a pool error with a debounce
|
||||
, debounceLogAcquisitionTimeout :: IO ()
|
||||
-- | JWT Cache
|
||||
, jwtCache :: C.Cache ByteString AuthResult
|
||||
-- | Network socket for REST API
|
||||
, stateSocketREST :: NS.Socket
|
||||
-- | Network socket for the admin UI
|
||||
, stateSocketAdmin :: Maybe NS.Socket
|
||||
}
|
||||
|
||||
type AppSockets = (NS.Socket, Maybe NS.Socket)
|
||||
|
||||
init :: AppConfig -> IO AppState
|
||||
init conf = do
|
||||
pool <- initPool conf
|
||||
initWithPool pool conf
|
||||
(sock, adminSock) <- initSockets conf
|
||||
state' <- initWithPool (sock, adminSock) pool conf
|
||||
pure state' { stateSocketREST = sock, stateSocketAdmin = adminSock }
|
||||
|
||||
initWithPool :: SQL.Pool -> AppConfig -> IO AppState
|
||||
initWithPool pool conf = do
|
||||
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> IO AppState
|
||||
initWithPool (sock, adminSock) pool conf = 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
|
||||
@@ -108,6 +137,9 @@ initWithPool pool conf = do
|
||||
<*> myThreadId
|
||||
<*> newIORef 0
|
||||
<*> pure (pure ())
|
||||
<*> C.newCache Nothing
|
||||
<*> pure sock
|
||||
<*> pure adminSock
|
||||
|
||||
|
||||
debLogTimeout <-
|
||||
@@ -131,6 +163,39 @@ initWithPool pool conf = do
|
||||
destroy :: AppState -> IO ()
|
||||
destroy = destroyPool
|
||||
|
||||
initSockets :: AppConfig -> IO AppSockets
|
||||
initSockets AppConfig{..} = do
|
||||
let
|
||||
cfg'usp = configServerUnixSocket
|
||||
cfg'uspm = configServerUnixSocketMode
|
||||
cfg'host = configServerHost
|
||||
cfg'port = configServerPort
|
||||
cfg'adminport = configAdminServerPort
|
||||
|
||||
sock <- case cfg'usp of
|
||||
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
|
||||
-- but we need to have runtime error if we try to use it in Windows, not compile time error
|
||||
Just path -> createAndBindDomainSocket path cfg'uspm
|
||||
Nothing -> do
|
||||
(_, sock) <-
|
||||
if cfg'port /= 0
|
||||
then do
|
||||
sock <- bindPortTCP cfg'port (fromString $ T.unpack cfg'host)
|
||||
pure (cfg'port, sock)
|
||||
else do
|
||||
-- explicitly bind to a random port, returning bound port number
|
||||
(num, sock) <- bindRandomPortTCP (fromString $ T.unpack cfg'host)
|
||||
pure (num, sock)
|
||||
pure sock
|
||||
|
||||
adminSock <- case cfg'adminport of
|
||||
Just adminPort -> do
|
||||
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'host)
|
||||
pure $ Just adminSock
|
||||
Nothing -> pure Nothing
|
||||
|
||||
pure (sock, adminSock)
|
||||
|
||||
initPool :: AppConfig -> IO SQL.Pool
|
||||
initPool AppConfig{..} =
|
||||
SQL.acquire
|
||||
@@ -141,12 +206,18 @@ initPool AppConfig{..} =
|
||||
(toUtf8 $ addFallbackAppName prettyVersion configDbUri)
|
||||
|
||||
-- | Run an action with a database connection.
|
||||
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
||||
usePool AppState{..} x = do
|
||||
usePool :: AppState -> AppConfig -> SQL.Session a -> IO (Either SQL.UsageError a)
|
||||
usePool appState@AppState{..} AppConfig{configLogLevel} x = do
|
||||
res <- SQL.use statePool x
|
||||
whenLeft res (\case
|
||||
SQL.AcquisitionTimeoutUsageError -> debounceLogAcquisitionTimeout -- this can happen rapidly for many requests, so we debounce
|
||||
_ -> pure ())
|
||||
|
||||
when (configLogLevel > LogCrit) $ do
|
||||
whenLeft res (\case
|
||||
SQL.AcquisitionTimeoutUsageError -> debounceLogAcquisitionTimeout -- this can happen rapidly for many requests, so we debounce
|
||||
error
|
||||
-- TODO We're using the 500 HTTP status for getting all internal db errors but there's no response here. We need a new intermediate type to not rely on the HTTP status.
|
||||
| Error.status (Error.PgError False error) >= HTTP.status500 -> logPgrstError appState error
|
||||
| otherwise -> pure ())
|
||||
|
||||
return res
|
||||
|
||||
-- | Flush the connection pool so that any future use of the pool will
|
||||
@@ -188,6 +259,15 @@ putConfig = atomicWriteIORef . stateConf
|
||||
getTime :: AppState -> IO UTCTime
|
||||
getTime = stateGetTime
|
||||
|
||||
getJwtCache :: AppState -> C.Cache ByteString AuthResult
|
||||
getJwtCache = jwtCache
|
||||
|
||||
getSocketREST :: AppState -> NS.Socket
|
||||
getSocketREST = stateSocketREST
|
||||
|
||||
getSocketAdmin :: AppState -> Maybe NS.Socket
|
||||
getSocketAdmin = stateSocketAdmin
|
||||
|
||||
-- | Log to stderr with local time
|
||||
logWithZTime :: AppState -> Text -> IO ()
|
||||
logWithZTime appState txt = do
|
||||
@@ -229,7 +309,7 @@ loadSchemaCache appState = do
|
||||
conf@AppConfig{..} <- getConfig appState
|
||||
result <-
|
||||
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
||||
usePool appState . transaction SQL.ReadCommitted SQL.Read $
|
||||
usePool appState conf . transaction SQL.ReadCommitted SQL.Read $
|
||||
querySchemaCache conf
|
||||
case result of
|
||||
Left e -> do
|
||||
@@ -272,17 +352,17 @@ internalConnectionWorker :: AppState -> IO ()
|
||||
internalConnectionWorker appState = work
|
||||
where
|
||||
work = do
|
||||
AppConfig{..} <- getConfig appState
|
||||
logWithZTime appState $ "Starting PostgREST " <> T.decodeUtf8 prettyVersion <> "..."
|
||||
config@AppConfig{..} <- getConfig appState
|
||||
logWithZTime appState "Attempting to connect to the database..."
|
||||
connected <- establishConnection appState
|
||||
connected <- establishConnection appState config
|
||||
case connected of
|
||||
FatalConnectionError reason ->
|
||||
-- Fatal error when connecting
|
||||
logWithZTime appState reason >> killThread (getMainThreadId appState)
|
||||
NotConnected ->
|
||||
-- Unreachable because establishConnection will keep trying to connect
|
||||
return ()
|
||||
-- Unreachable because establishConnection will keep trying to connect, unless disable-recovery is turned on
|
||||
unless configDbPoolAutomaticRecovery
|
||||
$ logWithZTime appState "Automatic recovery disabled, exiting." >> killThread (getMainThreadId appState)
|
||||
Connected actualPgVersion -> do
|
||||
-- Procede with initialization
|
||||
putPgVersion appState actualPgVersion
|
||||
@@ -314,8 +394,8 @@ internalConnectionWorker appState = work
|
||||
--
|
||||
-- The connection tries are capped, but if the connection times out no error is
|
||||
-- thrown, just 'False' is returned.
|
||||
establishConnection :: AppState -> IO ConnectionStatus
|
||||
establishConnection appState =
|
||||
establishConnection :: AppState -> AppConfig -> IO ConnectionStatus
|
||||
establishConnection appState config =
|
||||
retrying retrySettings shouldRetry $
|
||||
const $ flushPool appState >> getConnectionStatus
|
||||
where
|
||||
@@ -325,7 +405,7 @@ establishConnection appState =
|
||||
|
||||
getConnectionStatus :: IO ConnectionStatus
|
||||
getConnectionStatus = do
|
||||
pgVersion <- usePool appState $ queryPgVersion False -- No need to prepare the query here, as the connection might not be established
|
||||
pgVersion <- usePool appState config $ queryPgVersion False -- No need to prepare the query here, as the connection might not be established
|
||||
case pgVersion of
|
||||
Left e -> do
|
||||
logPgrstError appState e
|
||||
@@ -344,9 +424,10 @@ establishConnection appState =
|
||||
|
||||
shouldRetry :: RetryStatus -> ConnectionStatus -> IO Bool
|
||||
shouldRetry rs isConnSucc = do
|
||||
AppConfig{..} <- getConfig appState
|
||||
let
|
||||
delay = fromMaybe 0 (rsPreviousDelay rs) `div` backoffMicroseconds
|
||||
itShould = NotConnected == isConnSucc
|
||||
itShould = NotConnected == isConnSucc && configDbPoolAutomaticRecovery
|
||||
when itShould . logWithZTime appState $
|
||||
"Attempting to reconnect to the database in "
|
||||
<> (show delay::Text)
|
||||
@@ -357,10 +438,11 @@ establishConnection appState =
|
||||
-- | Re-reads the config plus config options from the db
|
||||
reReadConfig :: Bool -> AppState -> IO ()
|
||||
reReadConfig startingUp appState = do
|
||||
AppConfig{..} <- getConfig appState
|
||||
config@AppConfig{..} <- getConfig appState
|
||||
pgVer <- getPgVersion appState
|
||||
dbSettings <-
|
||||
if configDbConfig then do
|
||||
qDbSettings <- usePool appState $ queryDbSettings (dumpQi <$> configDbPreConfig) configDbPreparedStatements
|
||||
qDbSettings <- usePool appState config $ queryDbSettings (dumpQi <$> configDbPreConfig) configDbPreparedStatements
|
||||
case qDbSettings of
|
||||
Left e -> do
|
||||
logWithZTime appState
|
||||
@@ -378,7 +460,7 @@ reReadConfig startingUp appState = do
|
||||
pure mempty
|
||||
(roleSettings, roleIsolationLvl) <-
|
||||
if configDbConfig then do
|
||||
rSettings <- usePool appState $ queryRoleSettings configDbPreparedStatements
|
||||
rSettings <- usePool appState config $ queryRoleSettings pgVer configDbPreparedStatements
|
||||
case rSettings of
|
||||
Left e -> do
|
||||
logWithZTime appState "An error ocurred when trying to query the role settings"
|
||||
@@ -420,7 +502,7 @@ listener appState = do
|
||||
waitListener appState
|
||||
|
||||
-- forkFinally allows to detect if the thread dies
|
||||
void . flip forkFinally (handleFinally dbChannel) $ do
|
||||
void . flip forkFinally (handleFinally dbChannel configDbPoolAutomaticRecovery) $ do
|
||||
dbOrError <- acquire $ toUtf8 (addFallbackAppName prettyVersion configDbUri)
|
||||
case dbOrError of
|
||||
Right db -> do
|
||||
@@ -431,7 +513,9 @@ listener appState = do
|
||||
_ ->
|
||||
die $ "Could not listen for notifications on the " <> dbChannel <> " channel"
|
||||
where
|
||||
handleFinally dbChannel _ = do
|
||||
handleFinally _ False _ =
|
||||
logWithZTime appState "Automatic recovery disabled, exiting." >> killThread (getMainThreadId appState)
|
||||
handleFinally dbChannel True _ = do
|
||||
-- if the thread dies, we try to recover
|
||||
logWithZTime appState $ "Retrying listening for notifications on the " <> dbChannel <> " channel.."
|
||||
putIsListenerOn appState False
|
||||
|
||||
+59
-12
@@ -14,6 +14,7 @@ very simple authentication system inside the PostgreSQL database.
|
||||
module PostgREST.Auth
|
||||
( AuthResult (..)
|
||||
, getResult
|
||||
, getJwtDur
|
||||
, getRole
|
||||
, middleware
|
||||
) where
|
||||
@@ -25,6 +26,8 @@ import qualified Data.Aeson.KeyMap as KM
|
||||
import qualified Data.Aeson.Types as JSON
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||
import qualified Data.Cache as C
|
||||
import qualified Data.Scientific as Sci
|
||||
import qualified Data.Vault.Lazy as Vault
|
||||
import qualified Data.Vector as V
|
||||
import qualified Network.HTTP.Types.Header as HTTP
|
||||
@@ -35,21 +38,20 @@ import Control.Lens (set)
|
||||
import Control.Monad.Except (liftEither)
|
||||
import Data.Either.Combinators (mapLeft)
|
||||
import Data.List (lookup)
|
||||
import Data.Time.Clock (UTCTime)
|
||||
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
|
||||
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
|
||||
import System.Clock (TimeSpec (..))
|
||||
import System.IO.Unsafe (unsafePerformIO)
|
||||
import System.TimeIt (timeItT)
|
||||
|
||||
import PostgREST.AppState (AppState, getConfig, getTime)
|
||||
import PostgREST.AppState (AppState, AuthResult (..), getConfig,
|
||||
getJwtCache, getTime)
|
||||
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
|
||||
import PostgREST.Error (Error (..))
|
||||
|
||||
import Protolude
|
||||
|
||||
|
||||
data AuthResult = AuthResult
|
||||
{ authClaims :: KM.KeyMap JSON.Value
|
||||
, authRole :: BS.ByteString
|
||||
}
|
||||
|
||||
-- | Receives the JWT secret and audience (from config) and a JWT and returns a
|
||||
-- JSON object of JWT claims.
|
||||
parseToken :: Monad m =>
|
||||
@@ -102,14 +104,52 @@ middleware appState app req respond = do
|
||||
conf <- getConfig appState
|
||||
time <- getTime appState
|
||||
|
||||
let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
|
||||
authResult <- runExceptT $
|
||||
parseToken conf (LBS.fromStrict token) time >>=
|
||||
parseClaims conf
|
||||
let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
|
||||
parseJwt = runExceptT $ parseToken conf (LBS.fromStrict token) time >>= parseClaims conf
|
||||
|
||||
-- If DbPlanEnabled -> calculate JWT validation time
|
||||
-- If JwtCacheMaxLifetime -> cache JWT validation result
|
||||
req' <- case (configServerTimingEnabled conf, configJwtCacheMaxLifetime conf) of
|
||||
(True, 0) -> do
|
||||
(dur, authResult) <- timeItT parseJwt
|
||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
|
||||
|
||||
(True, maxLifetime) -> do
|
||||
(dur, authResult) <- timeItT $ getJWTFromCache appState token maxLifetime parseJwt time
|
||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
|
||||
|
||||
(False, 0) -> do
|
||||
authResult <- parseJwt
|
||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
||||
|
||||
(False, maxLifetime) -> do
|
||||
authResult <- getJWTFromCache appState token maxLifetime parseJwt time
|
||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
||||
|
||||
let req' = req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
||||
app req' respond
|
||||
|
||||
-- | Used to retrieve and insert JWT to JWT Cache
|
||||
getJWTFromCache :: AppState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
|
||||
getJWTFromCache appState token maxLifetime parseJwt utc = do
|
||||
checkCache <- C.lookup (getJwtCache appState) token
|
||||
authResult <- maybe parseJwt (pure . Right) checkCache
|
||||
|
||||
case (authResult,checkCache) of
|
||||
(Right res, Nothing) -> C.insert' (getJwtCache appState) (getTimeSpec res maxLifetime utc) token res
|
||||
_ -> pure ()
|
||||
|
||||
return authResult
|
||||
|
||||
-- Used to extract JWT exp claim and add to JWT Cache
|
||||
getTimeSpec :: AuthResult -> Int -> UTCTime -> Maybe TimeSpec
|
||||
getTimeSpec res maxLifetime utc = do
|
||||
let expireJSON = KM.lookup "exp" (authClaims res)
|
||||
utcToSecs = floor . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds
|
||||
sciToInt = fromMaybe 0 . Sci.toBoundedInteger
|
||||
case expireJSON of
|
||||
Just (JSON.Number seconds) -> Just $ TimeSpec (sciToInt seconds - utcToSecs utc) 0
|
||||
_ -> Just $ TimeSpec (fromIntegral maxLifetime :: Int64) 0
|
||||
|
||||
authResultKey :: Vault.Key (Either Error AuthResult)
|
||||
authResultKey = unsafePerformIO Vault.newKey
|
||||
{-# NOINLINE authResultKey #-}
|
||||
@@ -117,5 +157,12 @@ authResultKey = unsafePerformIO Vault.newKey
|
||||
getResult :: Wai.Request -> Maybe (Either Error AuthResult)
|
||||
getResult = Vault.lookup authResultKey . Wai.vault
|
||||
|
||||
jwtDurKey :: Vault.Key Double
|
||||
jwtDurKey = unsafePerformIO Vault.newKey
|
||||
{-# NOINLINE jwtDurKey #-}
|
||||
|
||||
getJwtDur :: Wai.Request -> Maybe Double
|
||||
getJwtDur = Vault.lookup jwtDurKey . Wai.vault
|
||||
|
||||
getRole :: Wai.Request -> Maybe BS.ByteString
|
||||
getRole req = authRole <$> (rightToMaybe =<< getResult req)
|
||||
|
||||
+15
-10
@@ -29,8 +29,8 @@ import qualified PostgREST.Config as Config
|
||||
import Protolude hiding (hPutStrLn)
|
||||
|
||||
|
||||
main :: App.SignalHandlerInstaller -> Maybe App.SocketRunner -> CLI -> IO ()
|
||||
main installSignalHandlers runAppWithSocket CLI{cliCommand, cliPath} = do
|
||||
main :: CLI -> IO ()
|
||||
main CLI{cliCommand, cliPath} = do
|
||||
conf@AppConfig{..} <-
|
||||
either panic identity <$> Config.readAppConfig mempty cliPath Nothing mempty mempty
|
||||
|
||||
@@ -45,7 +45,7 @@ main installSignalHandlers runAppWithSocket CLI{cliCommand, cliPath} = do
|
||||
when configDbConfig $ AppState.reReadConfig True appState
|
||||
putStr . Config.toText =<< AppState.getConfig appState
|
||||
CmdDumpSchema -> putStrLn =<< dumpSchema appState
|
||||
CmdRun -> App.run installSignalHandlers runAppWithSocket appState)
|
||||
CmdRun -> App.run appState)
|
||||
|
||||
-- | Dump SchemaCache schema to JSON
|
||||
dumpSchema :: AppState -> IO LBS.ByteString
|
||||
@@ -53,7 +53,7 @@ dumpSchema appState = do
|
||||
conf@AppConfig{..} <- AppState.getConfig appState
|
||||
result <-
|
||||
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
||||
AppState.usePool appState $
|
||||
AppState.usePool appState conf $
|
||||
transaction SQL.ReadCommitted SQL.Read $
|
||||
querySchemaCache conf
|
||||
case result of
|
||||
@@ -162,6 +162,9 @@ exampleConfigFile =
|
||||
|## Time in seconds after which to recycle unused pool connections
|
||||
|# db-pool-max-idletime = 30
|
||||
|
|
||||
|## Allow automatic database connection retrying
|
||||
|# db-pool-automatic-recovery = true
|
||||
|
|
||||
|## Stored proc to exec immediately after auth
|
||||
|# db-pre-request = "stored_proc_name"
|
||||
|
|
||||
@@ -188,10 +191,6 @@ exampleConfigFile =
|
||||
|## https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
|
||||
|db-uri = "postgresql://"
|
||||
|
|
||||
|## Determine if GUC request settings for headers, cookies and jwt claims use the legacy names (string with dashes, invalid starting from PostgreSQL v14) with text values instead of the new names (string without dashes, valid on all PostgreSQL versions) with json values.
|
||||
|## For PostgreSQL v14 and up, this setting will be ignored.
|
||||
|db-use-legacy-gucs = true
|
||||
|
|
||||
|# jwt-aud = "your_audience_claim"
|
||||
|
|
||||
|## Jspath to the role claim key
|
||||
@@ -202,6 +201,9 @@ exampleConfigFile =
|
||||
|# jwt-secret = "secret_with_at_least_32_characters"
|
||||
|jwt-secret-is-base64 = false
|
||||
|
|
||||
|## Enables and set JWT Cache max lifetime, disables caching with 0
|
||||
|# jwt-cache-max-lifetime = 0
|
||||
|
|
||||
|## Logging level, the admitted values are: crit, error, warn and info.
|
||||
|log-level = "error"
|
||||
|
|
||||
@@ -212,12 +214,15 @@ exampleConfigFile =
|
||||
|## Base url for the OpenAPI output
|
||||
|openapi-server-proxy-uri = ""
|
||||
|
|
||||
|## Content types to produce raw output
|
||||
|# raw-media-types="image/png, image/jpg"
|
||||
|## Configurable CORS origins
|
||||
|# server-cors-allowed-origins = ""
|
||||
|
|
||||
|server-host = "!4"
|
||||
|server-port = 3000
|
||||
|
|
||||
|## Allow getting the request-response timing information through the `Server-Timing` header
|
||||
|server-timing-enabled = false
|
||||
|
|
||||
|## Unix socket location
|
||||
|## if specified it takes precedence over server-port
|
||||
|# server-unix-socket = "/tmp/pgrst.sock"
|
||||
|
||||
+22
-8
@@ -61,7 +61,6 @@ import PostgREST.Config.JSPath (JSPath, JSPathExp (..),
|
||||
dumpJSPath, pRoleClaimKey)
|
||||
import PostgREST.Config.Proxy (Proxy (..),
|
||||
isMalformedProxyUri, toURI)
|
||||
import PostgREST.MediaType (MediaType (..), toMime)
|
||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi,
|
||||
toQi)
|
||||
|
||||
@@ -70,6 +69,7 @@ import Protolude hiding (Proxy, toList)
|
||||
|
||||
data AppConfig = AppConfig
|
||||
{ configAppSettings :: [(Text, Text)]
|
||||
, configDbAggregates :: Bool
|
||||
, configDbAnonRole :: Maybe BS.ByteString
|
||||
, configDbChannel :: Text
|
||||
, configDbChannelEnabled :: Bool
|
||||
@@ -80,6 +80,7 @@ data AppConfig = AppConfig
|
||||
, configDbPoolAcquisitionTimeout :: Int
|
||||
, configDbPoolMaxLifetime :: Int
|
||||
, configDbPoolMaxIdletime :: Int
|
||||
, configDbPoolAutomaticRecovery :: Bool
|
||||
, configDbPreRequest :: Maybe QualifiedIdentifier
|
||||
, configDbPreparedStatements :: Bool
|
||||
, configDbRootSpec :: Maybe QualifiedIdentifier
|
||||
@@ -89,21 +90,22 @@ data AppConfig = AppConfig
|
||||
, configDbTxAllowOverride :: Bool
|
||||
, configDbTxRollbackAll :: Bool
|
||||
, configDbUri :: Text
|
||||
, configDbUseLegacyGucs :: Bool
|
||||
, configFilePath :: Maybe FilePath
|
||||
, configJWKS :: Maybe JWKSet
|
||||
, configJwtAudience :: Maybe StringOrURI
|
||||
, configJwtRoleClaimKey :: JSPath
|
||||
, configJwtSecret :: Maybe BS.ByteString
|
||||
, configJwtSecretIsBase64 :: Bool
|
||||
, configJwtCacheMaxLifetime :: Int
|
||||
, configLogLevel :: LogLevel
|
||||
, configOpenApiMode :: OpenAPIMode
|
||||
, configOpenApiSecurityActive :: Bool
|
||||
, configOpenApiServerProxyUri :: Maybe Text
|
||||
, configRawMediaTypes :: [MediaType]
|
||||
, configServerCorsAllowedOrigins :: Maybe [Text]
|
||||
, configServerHost :: Text
|
||||
, configServerPort :: Int
|
||||
, configServerTraceHeader :: Maybe (CI.CI BS.ByteString)
|
||||
, configServerTimingEnabled :: Bool
|
||||
, configServerUnixSocket :: Maybe FilePath
|
||||
, configServerUnixSocketMode :: FileMode
|
||||
, configAdminServerPort :: Maybe Int
|
||||
@@ -113,6 +115,7 @@ data AppConfig = AppConfig
|
||||
}
|
||||
|
||||
data LogLevel = LogCrit | LogError | LogWarn | LogInfo
|
||||
deriving (Eq, Ord)
|
||||
|
||||
dumpLogLevel :: LogLevel -> Text
|
||||
dumpLogLevel = \case
|
||||
@@ -137,7 +140,8 @@ toText conf =
|
||||
where
|
||||
-- apply conf to all pgrst settings
|
||||
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
||||
[("db-anon-role", q . T.decodeUtf8 . fromMaybe "" . configDbAnonRole)
|
||||
[("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)
|
||||
,("db-extra-search-path", q . T.intercalate "," . configDbExtraSearchPath)
|
||||
@@ -147,6 +151,7 @@ toText conf =
|
||||
,("db-pool-acquisition-timeout", show . configDbPoolAcquisitionTimeout)
|
||||
,("db-pool-max-lifetime", show . configDbPoolMaxLifetime)
|
||||
,("db-pool-max-idletime", show . configDbPoolMaxIdletime)
|
||||
,("db-pool-automatic-recovery", T.toLower . show . configDbPoolAutomaticRecovery)
|
||||
,("db-pre-request", q . maybe mempty dumpQi . configDbPreRequest)
|
||||
,("db-prepared-statements", T.toLower . show . configDbPreparedStatements)
|
||||
,("db-root-spec", q . maybe mempty dumpQi . configDbRootSpec)
|
||||
@@ -155,19 +160,20 @@ toText conf =
|
||||
,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig)
|
||||
,("db-tx-end", q . showTxEnd)
|
||||
,("db-uri", q . configDbUri)
|
||||
,("db-use-legacy-gucs", T.toLower . show . configDbUseLegacyGucs)
|
||||
,("jwt-aud", T.decodeUtf8 . LBS.toStrict . JSON.encode . maybe "" toJSON . configJwtAudience)
|
||||
,("jwt-role-claim-key", q . T.intercalate mempty . fmap dumpJSPath . configJwtRoleClaimKey)
|
||||
,("jwt-secret", q . T.decodeUtf8 . showJwtSecret)
|
||||
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
|
||||
,("jwt-cache-max-lifetime", show . configJwtCacheMaxLifetime)
|
||||
,("log-level", q . dumpLogLevel . configLogLevel)
|
||||
,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode)
|
||||
,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive)
|
||||
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
|
||||
,("raw-media-types", q . T.decodeUtf8 . BS.intercalate "," . fmap toMime . configRawMediaTypes)
|
||||
,("server-cors-allowed-origins", q . maybe "" (T.intercalate ",") . configServerCorsAllowedOrigins)
|
||||
,("server-host", q . configServerHost)
|
||||
,("server-port", show . configServerPort)
|
||||
,("server-trace-header", q . T.decodeUtf8 . maybe mempty CI.original . configServerTraceHeader)
|
||||
,("server-timing-enabled", T.toLower . show . configServerTimingEnabled)
|
||||
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
|
||||
,("server-unix-socket-mode", q . T.pack . showSocketMode)
|
||||
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
|
||||
@@ -229,6 +235,7 @@ parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> Rol
|
||||
parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
AppConfig
|
||||
<$> parseAppSettings "app.settings"
|
||||
<*> (fromMaybe False <$> optBool "db-aggregates-enabled")
|
||||
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
|
||||
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
||||
<*> (fromMaybe True <$> optBool "db-channel-enabled")
|
||||
@@ -241,6 +248,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> (fromMaybe 1800 <$> optInt "db-pool-max-lifetime")
|
||||
<*> (fromMaybe 30 <$> optWithAlias (optInt "db-pool-timeout")
|
||||
(optInt "db-pool-max-idletime"))
|
||||
<*> (fromMaybe True <$> optBool "db-pool-automatic-recovery")
|
||||
<*> (fmap toQi <$> optWithAlias (optString "db-pre-request")
|
||||
(optString "pre-request"))
|
||||
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
||||
@@ -253,7 +261,6 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> parseTxEnd "db-tx-end" snd
|
||||
<*> parseTxEnd "db-tx-end" fst
|
||||
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
|
||||
<*> (fromMaybe True <$> optBool "db-use-legacy-gucs")
|
||||
<*> pure optPath
|
||||
<*> pure Nothing
|
||||
<*> parseJwtAudience "jwt-aud"
|
||||
@@ -262,14 +269,16 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> (fromMaybe False <$> optWithAlias
|
||||
(optBool "jwt-secret-is-base64")
|
||||
(optBool "secret-is-base64"))
|
||||
<*> (fromMaybe 0 <$> optInt "jwt-cache-max-lifetime")
|
||||
<*> parseLogLevel "log-level"
|
||||
<*> parseOpenAPIMode "openapi-mode"
|
||||
<*> (fromMaybe False <$> optBool "openapi-security-active")
|
||||
<*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
|
||||
<*> (maybe [] (fmap (MTOther . encodeUtf8) . splitOnCommas) <$> optValue "raw-media-types")
|
||||
<*> parseCORSAllowedOrigins "server-cors-allowed-origins"
|
||||
<*> (fromMaybe "!4" <$> optString "server-host")
|
||||
<*> (fromMaybe 3000 <$> optInt "server-port")
|
||||
<*> (fmap (CI.mk . encodeUtf8) <$> optString "server-trace-header")
|
||||
<*> (fromMaybe False <$> optBool "server-timing-enabled")
|
||||
<*> (fmap T.unpack <$> optString "server-unix-socket")
|
||||
<*> parseSocketFileMode "server-unix-socket-mode"
|
||||
<*> optInt "admin-server-port"
|
||||
@@ -348,6 +357,11 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
Nothing -> pure [JSPKey "role"]
|
||||
Just rck -> either (fail . show) pure $ pRoleClaimKey rck
|
||||
|
||||
parseCORSAllowedOrigins k =
|
||||
optString k >>= \case
|
||||
Nothing -> pure Nothing
|
||||
Just orig -> pure $ Just (T.strip <$> T.splitOn "," orig)
|
||||
|
||||
optWithAlias :: C.Parser C.Config (Maybe a) -> C.Parser C.Config (Maybe a) -> C.Parser C.Config (Maybe a)
|
||||
optWithAlias orig alias =
|
||||
orig >>= \case
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
module PostgREST.Config.Database
|
||||
( pgVersionStatement
|
||||
, queryDbSettings
|
||||
, queryRoleSettings
|
||||
, queryPgVersion
|
||||
, queryRoleSettings
|
||||
, RoleSettings
|
||||
, RoleIsolationLvl
|
||||
, TimezoneNames
|
||||
, toIsolationLevel
|
||||
) where
|
||||
|
||||
import Control.Arrow ((***))
|
||||
|
||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||
import PostgREST.Config.PgVersion (PgVersion (..), pgVersion150)
|
||||
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
|
||||
@@ -29,6 +30,7 @@ import Protolude
|
||||
|
||||
type RoleSettings = (HM.HashMap ByteString (HM.HashMap ByteString ByteString))
|
||||
type RoleIsolationLvl = HM.HashMap ByteString SQL.IsolationLevel
|
||||
type TimezoneNames = Set ByteString -- cache timezone names for prefer timezone=
|
||||
|
||||
toIsolationLevel :: (Eq a, IsString a) => a -> SQL.IsolationLevel
|
||||
toIsolationLevel a = case a of
|
||||
@@ -43,7 +45,8 @@ prefix = "pgrst."
|
||||
dbSettingsNames :: [Text]
|
||||
dbSettingsNames =
|
||||
(prefix <>) <$>
|
||||
["db_anon_role"
|
||||
["db_aggregates_enabled"
|
||||
,"db_anon_role"
|
||||
,"db_pre_config"
|
||||
,"db_extra_search_path"
|
||||
,"db_max_rows"
|
||||
@@ -53,16 +56,17 @@ dbSettingsNames =
|
||||
,"db_root_spec"
|
||||
,"db_schemas"
|
||||
,"db_tx_end"
|
||||
,"db_use_legacy_gucs"
|
||||
,"jwt_aud"
|
||||
,"jwt_role_claim_key"
|
||||
,"jwt_secret"
|
||||
,"jwt_secret_is_base64"
|
||||
,"jwt_cache_max_lifetime"
|
||||
,"openapi_mode"
|
||||
,"openapi_security_active"
|
||||
,"openapi_server_proxy_uri"
|
||||
,"raw_media_types"
|
||||
,"server_trace_header"
|
||||
,"server_timing_enabled"
|
||||
]
|
||||
|
||||
queryPgVersion :: Bool -> Session PgVersion
|
||||
@@ -126,8 +130,8 @@ queryDbSettings preConfFunc prepared =
|
||||
|]::Text
|
||||
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
||||
|
||||
queryRoleSettings :: Bool -> Session (RoleSettings, RoleIsolationLvl)
|
||||
queryRoleSettings prepared =
|
||||
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
|
||||
@@ -156,7 +160,10 @@ queryRoleSettings prepared =
|
||||
i.value as iso_lvl,
|
||||
coalesce(array_agg(row(kv.key, kv.value)) filter (where key <> 'default_transaction_isolation'), '{}') as role_settings
|
||||
from kv_settings kv
|
||||
join pg_settings ps on ps.name = kv.key and ps.context = 'user'
|
||||
join pg_settings ps on ps.name = kv.key |] <>
|
||||
(if pgVer >= pgVersion150
|
||||
then "and (ps.context = 'user' or has_parameter_privilege(current_user::regrole::oid, ps.name, 'set')) "
|
||||
else "and ps.context = 'user' ") <> [q|
|
||||
left join iso_setting i on i.rolname = kv.rolname
|
||||
group by kv.rolname, i.value;
|
||||
|]
|
||||
|
||||
@@ -13,6 +13,7 @@ module PostgREST.Config.PgVersion
|
||||
, pgVersion121
|
||||
, pgVersion130
|
||||
, pgVersion140
|
||||
, pgVersion150
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
@@ -62,3 +63,6 @@ pgVersion130 = PgVersion 130000 "13.0"
|
||||
|
||||
pgVersion140 :: PgVersion
|
||||
pgVersion140 = PgVersion 140000 "14.0"
|
||||
|
||||
pgVersion150 :: PgVersion
|
||||
pgVersion150 = PgVersion 150000 "15.0"
|
||||
|
||||
+10
-6
@@ -2,10 +2,14 @@
|
||||
Module : PostgREST.Cors
|
||||
Description : Wai Middleware to set cors policy.
|
||||
-}
|
||||
|
||||
{-# LANGUAGE TupleSections #-}
|
||||
|
||||
module PostgREST.Cors (middleware) where
|
||||
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Network.Wai as Wai
|
||||
import qualified Network.Wai.Middleware.Cors as Wai
|
||||
|
||||
@@ -13,15 +17,15 @@ import Data.List (lookup)
|
||||
|
||||
import Protolude
|
||||
|
||||
middleware :: Wai.Middleware
|
||||
middleware = Wai.cors corsPolicy
|
||||
middleware :: Maybe [Text] -> Wai.Middleware
|
||||
middleware corsAllowedOrigins = Wai.cors $ corsPolicy corsAllowedOrigins
|
||||
|
||||
-- | CORS policy to be used in by Wai Cors middleware
|
||||
corsPolicy :: Wai.Request -> Maybe Wai.CorsResourcePolicy
|
||||
corsPolicy req = case lookup "origin" headers of
|
||||
Just origin ->
|
||||
corsPolicy :: Maybe [Text] -> Wai.Request -> Maybe Wai.CorsResourcePolicy
|
||||
corsPolicy corsAllowedOrigins req = case lookup "origin" headers of
|
||||
Just _ ->
|
||||
Just Wai.CorsResourcePolicy
|
||||
{ Wai.corsOrigins = Just ([origin], True)
|
||||
{ Wai.corsOrigins = (, True) . map T.encodeUtf8 <$> corsAllowedOrigins
|
||||
, Wai.corsMethods = ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]
|
||||
, Wai.corsRequestHeaders = "Authorization" : accHeaders
|
||||
, Wai.corsExposedHeaders = Just
|
||||
|
||||
+248
-212
@@ -11,13 +11,15 @@ module PostgREST.Error
|
||||
, PgError(..)
|
||||
, Error(..)
|
||||
, errorPayload
|
||||
, singularityError
|
||||
, status
|
||||
) where
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Data.CaseInsensitive as CI
|
||||
import qualified Data.FuzzySet as Fuzzy
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import qualified Data.Map.Internal as M
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Data.Text.Encoding.Error as T
|
||||
@@ -25,7 +27,7 @@ import qualified Hasql.Pool as SQL
|
||||
import qualified Hasql.Session as SQL
|
||||
import qualified Network.HTTP.Types.Status as HTTP
|
||||
|
||||
import Data.Aeson ((.=))
|
||||
import Data.Aeson ((.:), (.:?), (.=))
|
||||
import Network.Wai (Response, responseLBS)
|
||||
|
||||
import Network.HTTP.Types.Header (Header)
|
||||
@@ -55,15 +57,18 @@ class (JSON.ToJSON a) => PgrstError a where
|
||||
errorPayload = JSON.encode
|
||||
|
||||
errorResponseFor :: a -> Response
|
||||
errorResponseFor err = responseLBS (status err) (headers err) $ errorPayload err
|
||||
errorResponseFor err =
|
||||
let baseHeader = MediaType.toContentType MTApplicationJSON in
|
||||
responseLBS (status err) (baseHeader : headers err) $ errorPayload err
|
||||
|
||||
instance PgrstError ApiRequestError where
|
||||
status AggregatesNotAllowed{} = HTTP.status400
|
||||
status AmbiguousRelBetween{} = HTTP.status300
|
||||
status AmbiguousRpc{} = HTTP.status300
|
||||
status BinaryFieldError{} = HTTP.status406
|
||||
status MediaTypeError{} = HTTP.status415
|
||||
status MediaTypeError{} = HTTP.status406
|
||||
status InvalidBody{} = HTTP.status400
|
||||
status InvalidFilters = HTTP.status405
|
||||
status InvalidPreferences{} = HTTP.status400
|
||||
status InvalidRpcMethod{} = HTTP.status405
|
||||
status InvalidRange{} = HTTP.status416
|
||||
status NotFound = HTTP.status404
|
||||
@@ -80,108 +85,131 @@ instance PgrstError ApiRequestError where
|
||||
status UnsupportedMethod{} = HTTP.status405
|
||||
status LimitNoOrderError = HTTP.status400
|
||||
status ColumnNotFound{} = HTTP.status400
|
||||
status GucHeadersError = HTTP.status500
|
||||
status GucStatusError = HTTP.status500
|
||||
status OffLimitsChangesError{} = HTTP.status400
|
||||
status PutMatchingPkError = HTTP.status400
|
||||
status SingularityError{} = HTTP.status406
|
||||
status PGRSTParseError = HTTP.status500
|
||||
|
||||
headers _ = [MediaType.toContentType MTApplicationJSON]
|
||||
headers _ = mempty
|
||||
|
||||
toJsonPgrstError :: ErrorCode -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value
|
||||
toJsonPgrstError code msg details hint = JSON.object [
|
||||
"code" .= code
|
||||
, "message" .= msg
|
||||
, "details" .= details
|
||||
, "hint" .= hint
|
||||
]
|
||||
|
||||
instance JSON.ToJSON ApiRequestError where
|
||||
toJSON (QueryParamError (QPError message details)) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode00,
|
||||
"message" .= message,
|
||||
"details" .= details,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (InvalidRpcMethod method) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode01,
|
||||
"message" .= ("Cannot use the " <> T.decodeUtf8 method <> " method on RPC"),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (InvalidBody errorMessage) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode02,
|
||||
"message" .= T.decodeUtf8 errorMessage,
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (InvalidRange rangeError) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode03,
|
||||
"message" .= ("Requested range not satisfiable" :: Text),
|
||||
"details" .= (case rangeError of
|
||||
NegativeLimit -> "Limit should be greater than or equal to zero."
|
||||
LowerGTUpper -> "The lower boundary must be lower than or equal to the upper boundary in the Range header."
|
||||
OutOfBounds lower total -> "An offset of " <> lower <> " was requested, but there are only " <> total <> " rows."),
|
||||
"hint" .= JSON.Null]
|
||||
toJSON InvalidFilters = JSON.object [
|
||||
"code" .= ApiRequestErrorCode05,
|
||||
"message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (UnacceptableSchema schemas) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode06,
|
||||
"message" .= ("The schema must be one of the following: " <> T.intercalate ", " schemas),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (MediaTypeError cts) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode07,
|
||||
"message" .= ("None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (QueryParamError (QPError message details)) = toJsonPgrstError
|
||||
ApiRequestErrorCode00 message (Just (JSON.String details)) Nothing
|
||||
|
||||
toJSON (InvalidRpcMethod method) = toJsonPgrstError
|
||||
ApiRequestErrorCode01 ("Cannot use the " <> T.decodeUtf8 method <> " method on RPC") Nothing Nothing
|
||||
|
||||
toJSON (InvalidBody errorMessage) = toJsonPgrstError
|
||||
ApiRequestErrorCode02 (T.decodeUtf8 errorMessage) Nothing Nothing
|
||||
|
||||
toJSON (InvalidRange rangeError) = toJsonPgrstError
|
||||
ApiRequestErrorCode03
|
||||
"Requested range not satisfiable"
|
||||
(Just $ case rangeError of
|
||||
NegativeLimit -> "Limit should be greater than or equal to zero."
|
||||
LowerGTUpper -> "The lower boundary must be lower than or equal to the upper boundary in the Range header."
|
||||
OutOfBounds lower total -> JSON.String $ "An offset of " <> lower <> " was requested, but there are only " <> total <> " rows.")
|
||||
Nothing
|
||||
|
||||
toJSON InvalidFilters = toJsonPgrstError
|
||||
ApiRequestErrorCode05 "Filters must include all and only primary key columns with 'eq' operators" Nothing Nothing
|
||||
|
||||
toJSON (UnacceptableSchema schemas) = toJsonPgrstError
|
||||
ApiRequestErrorCode06 ("The schema must be one of the following: " <> T.intercalate ", " schemas) Nothing Nothing
|
||||
|
||||
toJSON (MediaTypeError cts) = toJsonPgrstError
|
||||
ApiRequestErrorCode07 ("None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)) Nothing Nothing
|
||||
|
||||
toJSON NotFound = JSON.object []
|
||||
toJSON (NotEmbedded resource) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode08,
|
||||
"message" .= ("'" <> resource <> "' is not an embedded resource in this request" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= ("Verify that '" <> resource <> "' is included in the 'select' query parameter." :: Text)]
|
||||
|
||||
toJSON LimitNoOrderError = JSON.object [
|
||||
"code" .= ApiRequestErrorCode09,
|
||||
"message" .= ("A 'limit' was applied without an explicit 'order'":: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= ("Apply an 'order' using unique column(s)" :: Text)]
|
||||
toJSON (NotEmbedded resource) = toJsonPgrstError
|
||||
ApiRequestErrorCode08
|
||||
("'" <> resource <> "' is not an embedded resource in this request")
|
||||
Nothing
|
||||
(Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter.")
|
||||
|
||||
toJSON (BinaryFieldError ct) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode13,
|
||||
"message" .= ((T.decodeUtf8 (MediaType.toMime ct) <> " requested but more than one column was selected") :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON LimitNoOrderError = toJsonPgrstError
|
||||
ApiRequestErrorCode09 "A 'limit' was applied without an explicit 'order'" Nothing (Just "Apply an 'order' using unique column(s)")
|
||||
|
||||
toJSON PutLimitNotAllowedError = JSON.object [
|
||||
"code" .= ApiRequestErrorCode14,
|
||||
"message" .= ("limit/offset querystring parameters are not allowed for PUT" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (OffLimitsChangesError n maxs) = toJsonPgrstError
|
||||
ApiRequestErrorCode10
|
||||
"The maximum number of rows allowed to change was surpassed"
|
||||
(Just $ JSON.String $ T.unwords ["Results contain", show n, "rows changed but the maximum number allowed is", show maxs])
|
||||
Nothing
|
||||
|
||||
toJSON (UnsupportedMethod method) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode17,
|
||||
"message" .= ("Unsupported HTTP method: " <> T.decodeUtf8 method),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON GucHeadersError = toJsonPgrstError
|
||||
ApiRequestErrorCode11 "response.headers guc must be a JSON array composed of objects with a single key and a string value" Nothing Nothing
|
||||
|
||||
toJSON (RelatedOrderNotToOne origin target) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode18,
|
||||
"message" .= ("A related order on '" <> target <> "' is not possible" :: Text),
|
||||
"details" .= ("'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship" :: Text),
|
||||
"hint" .= JSON.Null]
|
||||
toJSON GucStatusError = toJsonPgrstError
|
||||
ApiRequestErrorCode12 "response.status guc must be a valid status code" Nothing Nothing
|
||||
|
||||
toJSON (SpreadNotToOne origin target) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode19,
|
||||
"message" .= ("A spread operation on '" <> target <> "' is not possible" :: Text),
|
||||
"details" .= ("'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship" :: Text),
|
||||
"hint" .= JSON.Null]
|
||||
toJSON PutLimitNotAllowedError = toJsonPgrstError
|
||||
ApiRequestErrorCode14 "limit/offset querystring parameters are not allowed for PUT" Nothing Nothing
|
||||
|
||||
toJSON (UnacceptableFilter target) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode20,
|
||||
"message" .= ("Bad operator on the '" <> target <> "' embedded resource":: Text),
|
||||
"details" .= ("Only is null or not is null filters are allowed on embedded resources":: Text),
|
||||
"hint" .= JSON.Null]
|
||||
toJSON PutMatchingPkError = toJsonPgrstError
|
||||
ApiRequestErrorCode15 "Payload values do not match URL in primary key column(s)" Nothing Nothing
|
||||
|
||||
toJSON (NoRelBetween parent child embedHint schema allRels) = JSON.object [
|
||||
"code" .= SchemaCacheErrorCode00,
|
||||
"message" .= ("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache" :: Text),
|
||||
"details" .= ("Searched for a foreign key relationship between '" <> parent <> "' and '" <> child <> maybe mempty ("' using the hint '" <>) embedHint <> "' in the schema '" <> schema <> "', but no matches were found."),
|
||||
"hint" .= noRelBetweenHint parent child schema allRels]
|
||||
toJSON (SingularityError n) = toJsonPgrstError
|
||||
ApiRequestErrorCode16
|
||||
"JSON object requested, multiple (or no) rows returned"
|
||||
(Just $ JSON.String $ T.unwords ["The result contains", show n, "rows"])
|
||||
Nothing
|
||||
|
||||
toJSON (UnsupportedMethod method) = toJsonPgrstError
|
||||
ApiRequestErrorCode17 ("Unsupported HTTP method: " <> T.decodeUtf8 method) Nothing Nothing
|
||||
|
||||
toJSON (RelatedOrderNotToOne origin target) = toJsonPgrstError
|
||||
ApiRequestErrorCode18
|
||||
("A related order on '" <> target <> "' is not possible")
|
||||
(Just $ JSON.String $ "'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship")
|
||||
Nothing
|
||||
|
||||
toJSON (SpreadNotToOne origin target) = toJsonPgrstError
|
||||
ApiRequestErrorCode19
|
||||
("A spread operation on '" <> target <> "' is not possible")
|
||||
(Just $ JSON.String $ "'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship")
|
||||
Nothing
|
||||
|
||||
toJSON (UnacceptableFilter target) = toJsonPgrstError
|
||||
ApiRequestErrorCode20
|
||||
("Bad operator on the '" <> target <> "' embedded resource")
|
||||
(Just "Only is null or not is null filters are allowed on embedded resources")
|
||||
Nothing
|
||||
|
||||
toJSON PGRSTParseError = toJsonPgrstError
|
||||
ApiRequestErrorCode21 "The message and detail field of RAISE 'PGRST' error expects JSON" Nothing Nothing
|
||||
|
||||
toJSON (InvalidPreferences prefs) = toJsonPgrstError
|
||||
ApiRequestErrorCode22
|
||||
"Invalid preferences given with handling=strict"
|
||||
(Just $ JSON.String $ T.decodeUtf8 ("Invalid preferences: " <> BS.intercalate ", " prefs))
|
||||
Nothing
|
||||
|
||||
toJSON AggregatesNotAllowed = toJsonPgrstError
|
||||
ApiRequestErrorCode23 "Use of aggregate functions is not allowed" Nothing Nothing
|
||||
|
||||
toJSON (NoRelBetween parent child embedHint schema allRels) = toJsonPgrstError
|
||||
SchemaCacheErrorCode00
|
||||
("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache")
|
||||
(Just $ JSON.String $ "Searched for a foreign key relationship between '" <> parent <> "' and '" <> child <> maybe mempty ("' using the hint '" <>) embedHint <> "' in the schema '" <> schema <> "', but no matches were found.")
|
||||
(JSON.String <$> noRelBetweenHint parent child schema allRels)
|
||||
|
||||
toJSON (AmbiguousRelBetween parent child rels) = toJsonPgrstError
|
||||
SchemaCacheErrorCode01
|
||||
("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'")
|
||||
(Just $ JSON.toJSONList (compressedRel <$> rels))
|
||||
(Just $ JSON.String $ "Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key.")
|
||||
|
||||
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
|
||||
"code" .= SchemaCacheErrorCode01,
|
||||
"message" .= ("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'" :: Text),
|
||||
"details" .= (compressedRel <$> rels),
|
||||
"hint" .= ("Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key." :: Text)]
|
||||
toJSON (NoRpc schema procName argumentKeys hasPreferSingleObject contentType isInvPost allProcs overloadedProcs) =
|
||||
let func = schema <> "." <> procName
|
||||
prms = T.intercalate ", " argumentKeys
|
||||
@@ -189,10 +217,10 @@ instance JSON.ToJSON ApiRequestError where
|
||||
prmsDet = " with parameter" <> (if length argumentKeys > 1 then "s " else " ") <> prms
|
||||
fmtPrms p = if null argumentKeys then " without parameters" else p
|
||||
onlySingleParams = hasPreferSingleObject || (isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream])
|
||||
in JSON.object [
|
||||
"code" .= SchemaCacheErrorCode02,
|
||||
"message" .= ("Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache"),
|
||||
"details" .= ("Searched for the function " <> func <>
|
||||
in toJsonPgrstError
|
||||
SchemaCacheErrorCode02
|
||||
("Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache")
|
||||
(Just $ JSON.String $ "Searched for the function " <> func <>
|
||||
(case (hasPreferSingleObject, isInvPost, contentType) of
|
||||
(True, _, _) -> " with a single json/jsonb parameter"
|
||||
(_, True, MTTextPlain) -> " with a single unnamed text parameter"
|
||||
@@ -200,21 +228,20 @@ instance JSON.ToJSON ApiRequestError where
|
||||
(_, True, MTOctetStream) -> " with a single unnamed bytea parameter"
|
||||
(_, True, MTApplicationJSON) -> fmtPrms prmsDet <> " or with a single unnamed json/jsonb parameter"
|
||||
_ -> fmtPrms prmsDet) <>
|
||||
", but no matches were found in the schema cache."),
|
||||
", but no matches were found in the schema cache.")
|
||||
-- The hint will be null in the case of single unnamed parameter functions
|
||||
"hint" .= if onlySingleParams
|
||||
then Nothing
|
||||
else noRpcHint schema procName argumentKeys allProcs overloadedProcs ]
|
||||
toJSON (AmbiguousRpc procs) = JSON.object [
|
||||
"code" .= SchemaCacheErrorCode03,
|
||||
"message" .= ("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs]),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text)]
|
||||
toJSON (ColumnNotFound relName colName) = JSON.object [
|
||||
"code" .= SchemaCacheErrorCode04,
|
||||
"message" .= ("Column '" <> colName <> "' of relation '" <> relName <> "' does not exist" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
(if onlySingleParams
|
||||
then Nothing
|
||||
else JSON.String <$> noRpcHint schema procName argumentKeys allProcs overloadedProcs)
|
||||
|
||||
toJSON (AmbiguousRpc procs) = toJsonPgrstError
|
||||
SchemaCacheErrorCode03
|
||||
("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs])
|
||||
Nothing
|
||||
(Just "Try renaming the parameters or the function itself in the database so function overloading can be resolved")
|
||||
|
||||
toJSON (ColumnNotFound relName colName) = toJsonPgrstError
|
||||
SchemaCacheErrorCode04 ("Could not find the '" <> colName <> "' column of '" <> relName <> "' in the schema cache") Nothing Nothing
|
||||
|
||||
-- |
|
||||
-- If no relationship is found then:
|
||||
@@ -359,49 +386,60 @@ 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))))) =
|
||||
case (parseMessage m, parseDetails d) of
|
||||
(Just _, Just r) -> headers PGRSTParseError ++ map intoHeader (M.toList $ getHeaders r)
|
||||
_ -> headers PGRSTParseError
|
||||
where
|
||||
intoHeader (k,v) = (CI.mk $ T.encodeUtf8 k, T.encodeUtf8 v)
|
||||
|
||||
headers err =
|
||||
if status err == HTTP.status401
|
||||
then [MediaType.toContentType MTApplicationJSON, ("WWW-Authenticate", "Bearer") :: Header]
|
||||
else [MediaType.toContentType MTApplicationJSON]
|
||||
then [("WWW-Authenticate", "Bearer") :: Header]
|
||||
else mempty
|
||||
|
||||
instance JSON.ToJSON PgError where
|
||||
toJSON (PgError _ usageError) = JSON.toJSON usageError
|
||||
|
||||
instance JSON.ToJSON SQL.UsageError where
|
||||
toJSON (SQL.ConnectionUsageError e) = JSON.object [
|
||||
"code" .= ConnectionErrorCode00,
|
||||
"message" .= ("Database connection error. Retrying the connection." :: Text),
|
||||
"details" .= (T.decodeUtf8With T.lenientDecode $ fromMaybe "" e :: Text),
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (SQL.ConnectionUsageError e) = toJsonPgrstError
|
||||
ConnectionErrorCode00
|
||||
"Database connection error. Retrying the connection."
|
||||
(Just $ JSON.String $ T.decodeUtf8With T.lenientDecode $ fromMaybe "" e)
|
||||
Nothing
|
||||
|
||||
toJSON (SQL.SessionUsageError e) = JSON.toJSON e -- SQL.Error
|
||||
toJSON SQL.AcquisitionTimeoutUsageError = JSON.object [
|
||||
"code" .= ConnectionErrorCode03,
|
||||
"message" .= ("Timed out acquiring connection from connection pool." :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON SQL.AcquisitionTimeoutUsageError = toJsonPgrstError
|
||||
ConnectionErrorCode03 "Timed out acquiring connection from connection pool." Nothing Nothing
|
||||
|
||||
instance JSON.ToJSON SQL.QueryError where
|
||||
toJSON (SQL.QueryError _ _ e) = JSON.toJSON e
|
||||
|
||||
instance JSON.ToJSON SQL.CommandError where
|
||||
-- Special error raised with code PGRST, to allow full response control
|
||||
toJSON (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p)) =
|
||||
case (parseMessage m, parseDetails d) of
|
||||
(Just r, Just _) -> JSON.object [
|
||||
"code" .= getCode r,
|
||||
"message" .= getMessage r,
|
||||
"details" .= checkMaybe (getDetails r),
|
||||
"hint" .= checkMaybe (getHint r)]
|
||||
_ -> JSON.toJSON PGRSTParseError
|
||||
where
|
||||
checkMaybe = maybe JSON.Null JSON.String
|
||||
|
||||
toJSON (SQL.ResultError (SQL.ServerError c m d h _p)) = JSON.object [
|
||||
"code" .= (T.decodeUtf8 c :: Text),
|
||||
"message" .= (T.decodeUtf8 m :: Text),
|
||||
"details" .= (fmap T.decodeUtf8 d :: Maybe Text),
|
||||
"hint" .= (fmap T.decodeUtf8 h :: Maybe Text)]
|
||||
|
||||
toJSON (SQL.ResultError resultError) = JSON.object [
|
||||
"code" .= InternalErrorCode00,
|
||||
"message" .= (show resultError :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON (SQL.ClientError d) = JSON.object [
|
||||
"code" .= ConnectionErrorCode01,
|
||||
"message" .= ("Database client error. Retrying the connection." :: Text),
|
||||
"details" .= (fmap T.decodeUtf8 d :: Maybe Text),
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (SQL.ResultError resultError) = toJsonPgrstError
|
||||
InternalErrorCode00 (show resultError) Nothing Nothing
|
||||
|
||||
toJSON (SQL.ClientError d) = toJsonPgrstError
|
||||
ConnectionErrorCode01 "Database client error. Retrying the connection." (JSON.String <$> fmap T.decodeUtf8 d) Nothing
|
||||
|
||||
pgErrorStatus :: Bool -> SQL.UsageError -> HTTP.Status
|
||||
pgErrorStatus _ (SQL.ConnectionUsageError _) = HTTP.status503
|
||||
@@ -409,7 +447,7 @@ pgErrorStatus _ SQL.AcquisitionTimeoutUsageError = HTTP.status504
|
||||
pgErrorStatus _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) = HTTP.status503
|
||||
pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError rError))) =
|
||||
case rError of
|
||||
(SQL.ServerError c m _ _ _) ->
|
||||
(SQL.ServerError c m d _ _) ->
|
||||
case BS.unpack c of
|
||||
'0':'8':_ -> HTTP.status503 -- pg connection err
|
||||
'0':'9':_ -> HTTP.status500 -- triggered action exception
|
||||
@@ -418,6 +456,10 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
|
||||
"23503" -> HTTP.status409 -- foreign_key_violation
|
||||
"23505" -> HTTP.status409 -- unique_violation
|
||||
"25006" -> HTTP.status405 -- read_only_sql_transaction
|
||||
"21000" -> -- cardinality_violation
|
||||
if BS.isSuffixOf "requires a WHERE clause" m
|
||||
then HTTP.status400 -- special case for pg-safeupdate, which we consider as client error
|
||||
else HTTP.status500 -- generic function or view server error, e.g. "more than one row returned by a subquery used as an expression"
|
||||
'2':'5':_ -> HTTP.status500 -- invalid tx state
|
||||
'2':'8':_ -> HTTP.status403 -- invalid auth specification
|
||||
'2':'D':_ -> HTTP.status500 -- invalid tx termination
|
||||
@@ -440,98 +482,52 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
|
||||
then HTTP.status406
|
||||
else HTTP.status404 -- undefined function
|
||||
"42P01" -> HTTP.status404 -- undefined table
|
||||
"42P17" -> HTTP.status500 -- infinite recursion
|
||||
"42501" -> if authed then HTTP.status403 else HTTP.status401 -- insufficient privilege
|
||||
'P':'T':n -> fromMaybe HTTP.status500 (HTTP.mkStatus <$> readMaybe n <*> pure m)
|
||||
"PGRST" ->
|
||||
case (parseMessage m, parseDetails d) of
|
||||
(Just _, Just r) -> maybe (toEnum $ getStatus r) (HTTP.mkStatus (getStatus r) . T.encodeUtf8) (getStatusText r)
|
||||
_ -> status PGRSTParseError
|
||||
_ -> HTTP.status400
|
||||
|
||||
_ -> HTTP.status500
|
||||
|
||||
|
||||
|
||||
data Error
|
||||
= ApiRequestError ApiRequestError
|
||||
| GucHeadersError
|
||||
| GucStatusError
|
||||
| JwtTokenInvalid Text
|
||||
| JwtTokenMissing
|
||||
| JwtTokenRequired
|
||||
| NoSchemaCacheError
|
||||
| OffLimitsChangesError Int64 Integer
|
||||
| PgErr PgError
|
||||
| PutMatchingPkError
|
||||
| SingularityError Integer
|
||||
|
||||
instance PgrstError Error where
|
||||
status (ApiRequestError err) = status err
|
||||
status GucHeadersError = HTTP.status500
|
||||
status GucStatusError = HTTP.status500
|
||||
status JwtTokenInvalid{} = HTTP.unauthorized401
|
||||
status JwtTokenMissing = HTTP.status500
|
||||
status JwtTokenRequired = HTTP.unauthorized401
|
||||
status NoSchemaCacheError = HTTP.status503
|
||||
status OffLimitsChangesError{} = HTTP.status400
|
||||
status (PgErr err) = status err
|
||||
status PutMatchingPkError = HTTP.status400
|
||||
status SingularityError{} = HTTP.status406
|
||||
status (ApiRequestError err) = status err
|
||||
status JwtTokenInvalid{} = HTTP.unauthorized401
|
||||
status JwtTokenMissing = HTTP.status500
|
||||
status JwtTokenRequired = HTTP.unauthorized401
|
||||
status NoSchemaCacheError = HTTP.status503
|
||||
status (PgErr err) = status err
|
||||
|
||||
headers (ApiRequestError err) = headers err
|
||||
headers (JwtTokenInvalid m) = [MediaType.toContentType MTApplicationJSON, invalidTokenHeader m]
|
||||
headers JwtTokenRequired = [MediaType.toContentType MTApplicationJSON, requiredTokenHeader]
|
||||
headers (PgErr err) = headers err
|
||||
headers SingularityError{} = [MediaType.toContentType (MTSingularJSON False)]
|
||||
headers _ = [MediaType.toContentType MTApplicationJSON]
|
||||
headers (ApiRequestError err) = headers err
|
||||
headers (JwtTokenInvalid m) = [invalidTokenHeader m]
|
||||
headers JwtTokenRequired = [requiredTokenHeader]
|
||||
headers (PgErr err) = headers err
|
||||
headers _ = mempty
|
||||
|
||||
instance JSON.ToJSON Error where
|
||||
toJSON NoSchemaCacheError = JSON.object [
|
||||
"code" .= ConnectionErrorCode02,
|
||||
"message" .= ("Could not query the database for the schema cache. Retrying." :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON NoSchemaCacheError = toJsonPgrstError
|
||||
ConnectionErrorCode02 "Could not query the database for the schema cache. Retrying." Nothing Nothing
|
||||
|
||||
toJSON JwtTokenMissing = JSON.object [
|
||||
"code" .= JWTErrorCode00,
|
||||
"message" .= ("Server lacks JWT secret" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (JwtTokenInvalid message) = JSON.object [
|
||||
"code" .= JWTErrorCode01,
|
||||
"message" .= (message :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON JwtTokenRequired = JSON.object [
|
||||
"code" .= JWTErrorCode02,
|
||||
"message" .= ("Anonymous access is disabled" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON JwtTokenMissing = toJsonPgrstError
|
||||
JWTErrorCode00 "Server lacks JWT secret" Nothing Nothing
|
||||
|
||||
toJSON (OffLimitsChangesError n maxs) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode10,
|
||||
"message" .= ("The maximum number of rows allowed to change was surpassed" :: Text),
|
||||
"details" .= T.unwords ["Results contain", show n, "rows changed but the maximum number allowed is", show maxs],
|
||||
"hint" .= JSON.Null]
|
||||
toJSON (JwtTokenInvalid message) = toJsonPgrstError
|
||||
JWTErrorCode01 message Nothing Nothing
|
||||
|
||||
toJSON GucHeadersError = JSON.object [
|
||||
"code" .= ApiRequestErrorCode11,
|
||||
"message" .= ("response.headers guc must be a JSON array composed of objects with a single key and a string value" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
toJSON GucStatusError = JSON.object [
|
||||
"code" .= ApiRequestErrorCode12,
|
||||
"message" .= ("response.status guc must be a valid status code" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON PutMatchingPkError = JSON.object [
|
||||
"code" .= ApiRequestErrorCode15,
|
||||
"message" .= ("Payload values do not match URL in primary key column(s)" :: Text),
|
||||
"details" .= JSON.Null,
|
||||
"hint" .= JSON.Null]
|
||||
|
||||
toJSON (SingularityError n) = JSON.object [
|
||||
"code" .= ApiRequestErrorCode16,
|
||||
"message" .= ("JSON object requested, multiple (or no) rows returned" :: Text),
|
||||
"details" .= T.unwords ["The result contains", show n, "rows"],
|
||||
"hint" .= JSON.Null]
|
||||
toJSON JwtTokenRequired = toJsonPgrstError
|
||||
JWTErrorCode02 "Anonymous access is disabled" Nothing Nothing
|
||||
|
||||
toJSON (PgErr err) = JSON.toJSON err
|
||||
toJSON (ApiRequestError err) = JSON.toJSON err
|
||||
@@ -543,8 +539,44 @@ invalidTokenHeader m =
|
||||
requiredTokenHeader :: Header
|
||||
requiredTokenHeader = ("WWW-Authenticate", "Bearer")
|
||||
|
||||
singularityError :: (Integral a) => a -> Error
|
||||
singularityError = SingularityError . toInteger
|
||||
-- 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) =
|
||||
PgRaiseErrMessage
|
||||
<$> m .: "code"
|
||||
<*> m .: "message"
|
||||
<*> m .:? "details"
|
||||
<*> m .:? "hint"
|
||||
|
||||
parseJSON _ = mzero
|
||||
|
||||
instance JSON.FromJSON PgRaiseErrDetails where
|
||||
parseJSON (JSON.Object d) =
|
||||
PgRaiseErrDetails
|
||||
<$> d .: "status"
|
||||
<*> d .:? "status_text"
|
||||
<*> d .: "headers"
|
||||
|
||||
parseJSON _ = mzero
|
||||
|
||||
parseMessage :: ByteString -> Maybe PgRaiseErrMessage
|
||||
parseMessage = JSON.decodeStrict
|
||||
|
||||
parseDetails :: Maybe ByteString -> Maybe PgRaiseErrDetails
|
||||
parseDetails d = JSON.decodeStrict =<< d
|
||||
|
||||
-- Error codes are grouped by common modules or characteristics
|
||||
data ErrorCode
|
||||
@@ -558,7 +590,7 @@ data ErrorCode
|
||||
| ApiRequestErrorCode01
|
||||
| ApiRequestErrorCode02
|
||||
| ApiRequestErrorCode03
|
||||
| ApiRequestErrorCode04 -- no longer used (used to be mapped to ParseRequestError)
|
||||
-- | ApiRequestErrorCode04 -- no longer used (used to be mapped to ParseRequestError)
|
||||
| ApiRequestErrorCode05
|
||||
| ApiRequestErrorCode06
|
||||
| ApiRequestErrorCode07
|
||||
@@ -566,8 +598,8 @@ data ErrorCode
|
||||
| ApiRequestErrorCode09
|
||||
| ApiRequestErrorCode10
|
||||
| ApiRequestErrorCode11
|
||||
-- | ApiRequestErrorCode13 -- no longer used (used to be mapped to BinaryFieldError)
|
||||
| ApiRequestErrorCode12
|
||||
| ApiRequestErrorCode13
|
||||
| ApiRequestErrorCode14
|
||||
| ApiRequestErrorCode15
|
||||
| ApiRequestErrorCode16
|
||||
@@ -575,6 +607,9 @@ data ErrorCode
|
||||
| ApiRequestErrorCode18
|
||||
| ApiRequestErrorCode19
|
||||
| ApiRequestErrorCode20
|
||||
| ApiRequestErrorCode21
|
||||
| ApiRequestErrorCode22
|
||||
| ApiRequestErrorCode23
|
||||
-- Schema Cache errors
|
||||
| SchemaCacheErrorCode00
|
||||
| SchemaCacheErrorCode01
|
||||
@@ -604,7 +639,6 @@ buildErrorCode code = "PGRST" <> case code of
|
||||
ApiRequestErrorCode01 -> "101"
|
||||
ApiRequestErrorCode02 -> "102"
|
||||
ApiRequestErrorCode03 -> "103"
|
||||
ApiRequestErrorCode04 -> "104"
|
||||
ApiRequestErrorCode05 -> "105"
|
||||
ApiRequestErrorCode06 -> "106"
|
||||
ApiRequestErrorCode07 -> "107"
|
||||
@@ -613,7 +647,6 @@ buildErrorCode code = "PGRST" <> case code of
|
||||
ApiRequestErrorCode10 -> "110"
|
||||
ApiRequestErrorCode11 -> "111"
|
||||
ApiRequestErrorCode12 -> "112"
|
||||
ApiRequestErrorCode13 -> "113"
|
||||
ApiRequestErrorCode14 -> "114"
|
||||
ApiRequestErrorCode15 -> "115"
|
||||
ApiRequestErrorCode16 -> "116"
|
||||
@@ -621,6 +654,9 @@ buildErrorCode code = "PGRST" <> case code of
|
||||
ApiRequestErrorCode18 -> "118"
|
||||
ApiRequestErrorCode19 -> "119"
|
||||
ApiRequestErrorCode20 -> "120"
|
||||
ApiRequestErrorCode21 -> "121"
|
||||
ApiRequestErrorCode22 -> "122"
|
||||
ApiRequestErrorCode23 -> "123"
|
||||
|
||||
SchemaCacheErrorCode00 -> "200"
|
||||
SchemaCacheErrorCode01 -> "201"
|
||||
|
||||
+32
-42
@@ -1,9 +1,10 @@
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
|
||||
module PostgREST.MediaType
|
||||
( MediaType(..)
|
||||
, MTPlanOption (..)
|
||||
, MTPlanFormat (..)
|
||||
, MTVndPlanOption (..)
|
||||
, MTVndPlanFormat (..)
|
||||
, toContentType
|
||||
, toMime
|
||||
, decodeMediaType
|
||||
@@ -19,8 +20,6 @@ import Protolude
|
||||
-- | Enumeration of currently supported media types
|
||||
data MediaType
|
||||
= MTApplicationJSON
|
||||
| MTArrayJSONStrip
|
||||
| MTSingularJSON Bool
|
||||
| MTGeoJSON
|
||||
| MTTextCSV
|
||||
| MTTextPlain
|
||||
@@ -30,32 +29,23 @@ data MediaType
|
||||
| MTOctetStream
|
||||
| MTAny
|
||||
| MTOther ByteString
|
||||
-- TODO MTPlan should only have its options as [Text]. Its ResultAggregate should have the typed attributes.
|
||||
| MTPlan MediaType MTPlanFormat [MTPlanOption]
|
||||
deriving Show
|
||||
instance Eq MediaType where
|
||||
MTApplicationJSON == MTApplicationJSON = True
|
||||
MTArrayJSONStrip == MTArrayJSONStrip = True
|
||||
MTSingularJSON x == MTSingularJSON y = x == y
|
||||
MTGeoJSON == MTGeoJSON = True
|
||||
MTTextCSV == MTTextCSV = True
|
||||
MTTextPlain == MTTextPlain = True
|
||||
MTTextXML == MTTextXML = True
|
||||
MTOpenAPI == MTOpenAPI = True
|
||||
MTUrlEncoded == MTUrlEncoded = True
|
||||
MTOctetStream == MTOctetStream = True
|
||||
MTAny == MTAny = True
|
||||
MTOther x == MTOther y = x == y
|
||||
MTPlan{} == MTPlan{} = True
|
||||
_ == _ = False
|
||||
-- vendored media types
|
||||
| MTVndArrayJSONStrip
|
||||
| MTVndSingularJSON Bool
|
||||
-- TODO MTVndPlan should only have its options as [Text]. Its ResultAggregate should have the typed attributes.
|
||||
| MTVndPlan MediaType MTVndPlanFormat [MTVndPlanOption]
|
||||
deriving (Eq, Show, Generic)
|
||||
instance Hashable MediaType
|
||||
|
||||
data MTPlanOption
|
||||
data MTVndPlanOption
|
||||
= PlanAnalyze | PlanVerbose | PlanSettings | PlanBuffers | PlanWAL
|
||||
deriving (Eq, Show)
|
||||
deriving (Eq, Show, Generic)
|
||||
instance Hashable MTVndPlanOption
|
||||
|
||||
data MTPlanFormat
|
||||
data MTVndPlanFormat
|
||||
= PlanJSON | PlanText
|
||||
deriving (Eq, Show)
|
||||
deriving (Eq, Show, Generic)
|
||||
instance Hashable MTVndPlanFormat
|
||||
|
||||
-- | Convert MediaType to a Content-Type HTTP Header
|
||||
toContentType :: MediaType -> Header
|
||||
@@ -69,31 +59,31 @@ toContentType ct = (hContentType, toMime ct <> charset)
|
||||
-- | Convert from MediaType to a ByteString representing the mime type
|
||||
toMime :: MediaType -> ByteString
|
||||
toMime MTApplicationJSON = "application/json"
|
||||
toMime MTArrayJSONStrip = "application/vnd.pgrst.array+json;nulls=stripped"
|
||||
toMime MTVndArrayJSONStrip = "application/vnd.pgrst.array+json;nulls=stripped"
|
||||
toMime MTGeoJSON = "application/geo+json"
|
||||
toMime MTTextCSV = "text/csv"
|
||||
toMime MTTextPlain = "text/plain"
|
||||
toMime MTTextXML = "text/xml"
|
||||
toMime MTOpenAPI = "application/openapi+json"
|
||||
toMime (MTSingularJSON True) = "application/vnd.pgrst.object+json;nulls=stripped"
|
||||
toMime (MTSingularJSON False) = "application/vnd.pgrst.object+json"
|
||||
toMime (MTVndSingularJSON True) = "application/vnd.pgrst.object+json;nulls=stripped"
|
||||
toMime (MTVndSingularJSON False) = "application/vnd.pgrst.object+json"
|
||||
toMime MTUrlEncoded = "application/x-www-form-urlencoded"
|
||||
toMime MTOctetStream = "application/octet-stream"
|
||||
toMime MTAny = "*/*"
|
||||
toMime (MTOther ct) = ct
|
||||
toMime (MTPlan mt fmt opts) =
|
||||
toMime (MTVndPlan mt fmt opts) =
|
||||
"application/vnd.pgrst.plan+" <> toMimePlanFormat fmt <>
|
||||
("; for=\"" <> toMime mt <> "\"") <>
|
||||
(if null opts then mempty else "; options=" <> BS.intercalate "|" (toMimePlanOption <$> opts))
|
||||
|
||||
toMimePlanOption :: MTPlanOption -> ByteString
|
||||
toMimePlanOption :: MTVndPlanOption -> ByteString
|
||||
toMimePlanOption PlanAnalyze = "analyze"
|
||||
toMimePlanOption PlanVerbose = "verbose"
|
||||
toMimePlanOption PlanSettings = "settings"
|
||||
toMimePlanOption PlanBuffers = "buffers"
|
||||
toMimePlanOption PlanWAL = "wal"
|
||||
|
||||
toMimePlanFormat :: MTPlanFormat -> ByteString
|
||||
toMimePlanFormat :: MTVndPlanFormat -> ByteString
|
||||
toMimePlanFormat PlanJSON = "json"
|
||||
toMimePlanFormat PlanText = "text"
|
||||
|
||||
@@ -103,25 +93,25 @@ toMimePlanFormat PlanText = "text"
|
||||
-- MTApplicationJSON
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.plan;"
|
||||
-- MTPlan MTApplicationJSON PlanText []
|
||||
-- MTVndPlan MTApplicationJSON PlanText []
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.plan;for=\"application/json\""
|
||||
-- MTPlan MTApplicationJSON PlanText []
|
||||
-- MTVndPlan MTApplicationJSON PlanText []
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
|
||||
-- MTPlan MTTextCSV PlanJSON []
|
||||
-- MTVndPlan MTTextCSV PlanJSON []
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.array+json;nulls=stripped"
|
||||
-- MTArrayJSONStrip
|
||||
-- MTVndArrayJSONStrip
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.array+json"
|
||||
-- MTApplicationJSON
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.object+json;nulls=stripped"
|
||||
-- MTSingularJSON True
|
||||
-- MTVndSingularJSON True
|
||||
--
|
||||
-- >>> decodeMediaType "application/vnd.pgrst.object+json"
|
||||
-- MTSingularJSON False
|
||||
-- MTVndSingularJSON False
|
||||
|
||||
decodeMediaType :: BS.ByteString -> MediaType
|
||||
decodeMediaType mt =
|
||||
@@ -145,11 +135,11 @@ decodeMediaType mt =
|
||||
other:_ -> MTOther other
|
||||
_ -> MTAny
|
||||
where
|
||||
checkArrayNullStrip ["nulls=stripped"] = MTArrayJSONStrip
|
||||
checkArrayNullStrip ["nulls=stripped"] = MTVndArrayJSONStrip
|
||||
checkArrayNullStrip _ = MTApplicationJSON
|
||||
|
||||
checkSingularNullStrip ["nulls=stripped"] = MTSingularJSON True
|
||||
checkSingularNullStrip _ = MTSingularJSON False
|
||||
checkSingularNullStrip ["nulls=stripped"] = MTVndSingularJSON True
|
||||
checkSingularNullStrip _ = MTVndSingularJSON False
|
||||
|
||||
getPlan fmt rest =
|
||||
let
|
||||
@@ -161,7 +151,7 @@ decodeMediaType mt =
|
||||
strippedFor <- BS.stripPrefix "for=" foundFor
|
||||
pure . decodeMediaType $ dropAround (== BS.c2w '"') strippedFor
|
||||
in
|
||||
MTPlan mtFor fmt $
|
||||
MTVndPlan mtFor fmt $
|
||||
[PlanAnalyze | inOpts "analyze" ] ++
|
||||
[PlanVerbose | inOpts "verbose" ] ++
|
||||
[PlanSettings | inOpts "settings"] ++
|
||||
|
||||
+271
-117
@@ -19,10 +19,11 @@ module PostgREST.Plan
|
||||
( wrappedReadPlan
|
||||
, mutateReadPlan
|
||||
, callReadPlan
|
||||
, inspectPlan
|
||||
, WrappedReadPlan(..)
|
||||
, MutateReadPlan(..)
|
||||
, CallReadPlan(..)
|
||||
, inspectPlanTxMode
|
||||
, InspectPlan(..)
|
||||
) where
|
||||
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
@@ -33,7 +34,7 @@ import qualified Data.Set as S
|
||||
import qualified PostgREST.SchemaCache.Routine as Routine
|
||||
|
||||
import Data.Either.Combinators (mapLeft, mapRight)
|
||||
import Data.List (delete)
|
||||
import Data.List (delete, lookup)
|
||||
import Data.Tree (Tree (..))
|
||||
|
||||
import PostgREST.ApiRequest (Action (..),
|
||||
@@ -51,6 +52,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||
import PostgREST.SchemaCache (SchemaCache (..))
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier (..),
|
||||
RelIdentifier (..),
|
||||
Schema)
|
||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
Junction (..),
|
||||
@@ -59,7 +61,9 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
relIsToOne)
|
||||
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
|
||||
RepresentationsMap)
|
||||
import PostgREST.SchemaCache.Routine (ResultAggregate (..),
|
||||
import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
||||
MediaHandlerMap,
|
||||
ResolvedHandler,
|
||||
Routine (..),
|
||||
RoutineMap,
|
||||
RoutineParam (..),
|
||||
@@ -80,6 +84,7 @@ import PostgREST.Plan.Types
|
||||
|
||||
import qualified Hasql.Transaction.Sessions as SQL
|
||||
import qualified PostgREST.ApiRequest.QueryParams as QueryParams
|
||||
import qualified PostgREST.MediaType as MediaType
|
||||
|
||||
import Protolude hiding (from)
|
||||
|
||||
@@ -90,14 +95,16 @@ import Protolude hiding (from)
|
||||
data WrappedReadPlan = WrappedReadPlan {
|
||||
wrReadPlan :: ReadPlanTree
|
||||
, wrTxMode :: SQL.Mode
|
||||
, wrResAgg :: ResultAggregate
|
||||
, wrHandler :: MediaHandler
|
||||
, wrMedia :: MediaType
|
||||
}
|
||||
|
||||
data MutateReadPlan = MutateReadPlan {
|
||||
mrReadPlan :: ReadPlanTree
|
||||
, mrMutatePlan :: MutatePlan
|
||||
, mrTxMode :: SQL.Mode
|
||||
, mrResAgg :: ResultAggregate
|
||||
, mrHandler :: MediaHandler
|
||||
, mrMedia :: MediaType
|
||||
}
|
||||
|
||||
data CallReadPlan = CallReadPlan {
|
||||
@@ -105,37 +112,45 @@ data CallReadPlan = CallReadPlan {
|
||||
, crCallPlan :: CallPlan
|
||||
, crTxMode :: SQL.Mode
|
||||
, crProc :: Routine
|
||||
, crResAgg :: ResultAggregate
|
||||
, crHandler :: MediaHandler
|
||||
, crMedia :: MediaType
|
||||
}
|
||||
|
||||
data InspectPlan = InspectPlan {
|
||||
ipMedia :: MediaType
|
||||
, ipTxmode :: SQL.Mode
|
||||
}
|
||||
|
||||
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error WrappedReadPlan
|
||||
wrappedReadPlan identifier conf sCache apiRequest = do
|
||||
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} = do
|
||||
rPlan <- readPlan identifier conf sCache apiRequest
|
||||
binField <- mapLeft ApiRequestError $ binaryField conf (iAcceptMediaType apiRequest) Nothing rPlan
|
||||
return $ WrappedReadPlan rPlan SQL.Read $ mediaToAggregate (iAcceptMediaType apiRequest) binField apiRequest
|
||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier 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
|
||||
|
||||
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error MutateReadPlan
|
||||
mutateReadPlan mutation apiRequest identifier conf sCache = do
|
||||
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
|
||||
rPlan <- readPlan identifier conf sCache apiRequest
|
||||
binField <- mapLeft ApiRequestError $ binaryField conf (iAcceptMediaType apiRequest) Nothing rPlan
|
||||
mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan
|
||||
return $ MutateReadPlan rPlan mPlan SQL.Write $ mediaToAggregate (iAcceptMediaType apiRequest) binField apiRequest
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType
|
||||
|
||||
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
|
||||
callReadPlan identifier conf sCache apiRequest invMethod = do
|
||||
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} invMethod = do
|
||||
let paramKeys = case invMethod of
|
||||
InvGet -> S.fromList $ fst <$> qsParams'
|
||||
InvHead -> S.fromList $ fst <$> qsParams'
|
||||
InvPost -> iColumns apiRequest
|
||||
InvPost -> iColumns
|
||||
proc@Function{..} <- mapLeft ApiRequestError $
|
||||
findProc identifier paramKeys (preferParameters == Just SingleObject) (dbRoutines sCache) (iContentMediaType apiRequest) (invMethod == InvPost)
|
||||
findProc identifier paramKeys (preferParameters == Just SingleObject) (dbRoutines sCache) iContentMediaType (invMethod == InvPost)
|
||||
let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations
|
||||
rPlan <- readPlan relIdentifier conf sCache apiRequest
|
||||
let args = case (invMethod, iContentMediaType apiRequest) of
|
||||
let args = case (invMethod, iContentMediaType) of
|
||||
(InvGet, _) -> jsonRpcParams proc qsParams'
|
||||
(InvHead, _) -> jsonRpcParams proc qsParams'
|
||||
(InvPost, MTUrlEncoded) -> maybe mempty (jsonRpcParams proc . payArray) $ iPayload apiRequest
|
||||
(InvPost, _) -> maybe mempty payRaw $ iPayload apiRequest
|
||||
(InvPost, MTUrlEncoded) -> maybe mempty (jsonRpcParams proc . payArray) iPayload
|
||||
(InvPost, _) -> maybe mempty payRaw iPayload
|
||||
txMode = case (invMethod, pdVolatility) of
|
||||
(InvGet, _) -> SQL.Read
|
||||
(InvHead, _) -> SQL.Read
|
||||
@@ -143,11 +158,24 @@ callReadPlan identifier conf sCache apiRequest invMethod = do
|
||||
(InvPost, Routine.Immutable) -> SQL.Read
|
||||
(InvPost, Routine.Volatile) -> SQL.Write
|
||||
cPlan = callPlan proc apiRequest paramKeys args rPlan
|
||||
binField <- mapLeft ApiRequestError $ binaryField conf (iAcceptMediaType apiRequest) (Just proc) rPlan
|
||||
return $ CallReadPlan rPlan cPlan txMode proc $ mediaToAggregate (iAcceptMediaType apiRequest) binField apiRequest
|
||||
(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 ()
|
||||
return $ CallReadPlan rPlan cPlan txMode proc handler mediaType
|
||||
where
|
||||
Preferences{..} = iPreferences apiRequest
|
||||
qsParams' = QueryParams.qsParams (iQueryParams apiRequest)
|
||||
qsParams' = QueryParams.qsParams iQueryParams
|
||||
|
||||
hasDefaultSelect :: ReadPlanTree -> Bool
|
||||
hasDefaultSelect (Node ReadPlan{select=[CoercibleSelectField{csField=CoercibleField{cfName}}]} []) = cfName == "*"
|
||||
hasDefaultSelect _ = False
|
||||
|
||||
inspectPlan :: ApiRequest -> Either Error InspectPlan
|
||||
inspectPlan apiRequest = do
|
||||
let producedMTs = [MTOpenAPI, MTApplicationJSON, MTAny]
|
||||
accepts = iAcceptMediaType apiRequest
|
||||
mediaType <- if not . null $ L.intersect accepts producedMTs
|
||||
then Right MTOpenAPI
|
||||
else Left . ApiRequestError . MediaTypeError $ MediaType.toMime <$> accepts
|
||||
return $ InspectPlan mediaType SQL.Read
|
||||
|
||||
{-|
|
||||
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
|
||||
@@ -206,9 +234,6 @@ findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPo
|
||||
-- and can match any or none of the default parameters.
|
||||
(reqParams, optParams) -> argumentsKeys `S.difference` S.fromList (ppName <$> optParams) == S.fromList (ppName <$> reqParams)
|
||||
|
||||
inspectPlanTxMode :: SQL.Mode
|
||||
inspectPlanTxMode = SQL.Read
|
||||
|
||||
-- | During planning we need to resolve Field -> CoercibleField (finding the context specific target type and map function).
|
||||
-- | ResolverContext facilitates this without the need to pass around a laundry list of parameters.
|
||||
data ResolverContext = ResolverContext
|
||||
@@ -274,18 +299,21 @@ resolveQueryInputField ctx field = withTextParse ctx $ resolveTypeOrUnknown ctx
|
||||
-- | Adds filters, order, limits on its respective nodes.
|
||||
-- | Adds joins conditions obtained from resource embedding.
|
||||
readPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error ReadPlanTree
|
||||
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows} SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest =
|
||||
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregates} SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest =
|
||||
let
|
||||
-- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV.
|
||||
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
||||
in
|
||||
mapLeft ApiRequestError $
|
||||
treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
|
||||
validateAggFunctions configDbAggregates =<<
|
||||
hoistSpreadAggFunctions =<<
|
||||
addRelSelects =<<
|
||||
addNullEmbedFilters =<<
|
||||
validateSpreadEmbeds =<<
|
||||
addRelatedOrders =<<
|
||||
addDataRepresentationAliases =<<
|
||||
expandStarsForDataRepresentations ctx =<<
|
||||
addAliases =<<
|
||||
expandStars ctx =<<
|
||||
addRels qiSchema (iAction apiRequest) dbRelationships Nothing =<<
|
||||
addLogicTrees ctx apiRequest =<<
|
||||
addRanges apiRequest =<<
|
||||
@@ -298,7 +326,7 @@ initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
|
||||
foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi ctx, relName=qiName, depth=rootDepth} []
|
||||
where
|
||||
rootDepth = 0
|
||||
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing False rootDepth
|
||||
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing False [] rootDepth
|
||||
treeEntry :: Depth -> Tree SelectItem -> ReadPlanTree -> ReadPlanTree
|
||||
treeEntry depth (Node si fldForest) (Node q rForest) =
|
||||
let nxtDepth = succ depth in
|
||||
@@ -314,49 +342,86 @@ initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
|
||||
(Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth, relIsSpread=True} [])
|
||||
fldForest:rForest
|
||||
SelectField{..} ->
|
||||
Node q{select=(resolveOutputField ctx{qi=from q} selField, selCast, selAlias):select q} rForest
|
||||
Node q{select=CoercibleSelectField (resolveOutputField ctx{qi=from q} selField) selAggregateFunction selAggregateCast selCast selAlias:select q} rForest
|
||||
|
||||
-- | Preserve the original field name if data representation is used to coerce the value.
|
||||
addDataRepresentationAliases :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addDataRepresentationAliases rPlanTree = Right $ fmap (\rPlan@ReadPlan{select=sel} -> rPlan{select=map aliasSelectItem sel}) rPlanTree
|
||||
-- If an alias is explicitly specified, it is always respected. However, an alias may be
|
||||
-- determined automatically in the case of a select term with a JSON path, or in the case
|
||||
-- of domain representations.
|
||||
addAliases :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addAliases = Right . fmap addAliasToPlan
|
||||
where
|
||||
aliasSelectItem :: (CoercibleField, Maybe Cast, Maybe Alias) -> (CoercibleField, Maybe Cast, Maybe Alias)
|
||||
-- If there already is an alias, don't overwrite it.
|
||||
aliasSelectItem (fld@(CoercibleField{cfName=fieldName, cfTransform=(Just _)}), Nothing, Nothing) = (fld, Nothing, Just fieldName)
|
||||
aliasSelectItem fld = fld
|
||||
addAliasToPlan rp@ReadPlan{select=sel} = rp{select=map aliasSelectField sel}
|
||||
|
||||
aliasSelectField :: CoercibleSelectField -> CoercibleSelectField
|
||||
aliasSelectField field@CoercibleSelectField{csField=fieldDetails, csAggFunction=aggFun, csAlias=alias}
|
||||
| isJust alias || isJust aggFun = field
|
||||
| isJsonKeyPath fieldDetails, Just key <- lastJsonKey fieldDetails = field { csAlias = Just key }
|
||||
| isTransformPath fieldDetails = field { csAlias = Just (cfName fieldDetails) }
|
||||
| otherwise = field
|
||||
|
||||
isJsonKeyPath CoercibleField{cfJsonPath=(_: _)} = True
|
||||
isJsonKeyPath _ = False
|
||||
|
||||
isTransformPath CoercibleField{cfTransform=(Just _), cfName=_} = True
|
||||
isTransformPath _ = False
|
||||
|
||||
lastJsonKey CoercibleField{cfName=fieldName, cfJsonPath=jsonPath} =
|
||||
case jOp <$> lastMay jsonPath of
|
||||
Just (JKey key) -> Just key
|
||||
Just (JIdx _) -> Just $ fromMaybe fieldName lastKey
|
||||
-- We get the lastKey because on:
|
||||
-- `select=data->1->mycol->>2`, we need to show the result as [ {"mycol": ..}, {"mycol": ..} ]
|
||||
-- `select=data->3`, we need to show the result as [ {"data": ..}, {"data": ..} ]
|
||||
where lastKey = jVal <$> find (\case JKey{} -> True; _ -> False) (jOp <$> reverse jsonPath)
|
||||
Nothing -> Nothing
|
||||
|
||||
knownColumnsInContext :: ResolverContext -> [Column]
|
||||
knownColumnsInContext ResolverContext{..} =
|
||||
fromMaybe [] $ HM.lookup qi tables >>=
|
||||
Just . tableColumnsList
|
||||
|
||||
-- | Expand "select *" into explicit field names of the table, if necessary to apply data representations.
|
||||
expandStarsForDataRepresentations :: ResolverContext -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
expandStarsForDataRepresentations ctx@ResolverContext{qi} rPlanTree = Right $ fmap expandStars rPlanTree
|
||||
-- | Expand "select *" into explicit field names of the table in the following situations:
|
||||
-- * When there are data representations present.
|
||||
-- * When there is an aggregate function in a given ReadPlan or its parent.
|
||||
expandStars :: ResolverContext -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree
|
||||
where
|
||||
expandStars :: ReadPlan -> ReadPlan
|
||||
expandStarsForReadPlan :: Bool -> ReadPlanTree -> ReadPlanTree
|
||||
expandStarsForReadPlan hasAgg (Node rp@ReadPlan{select, from=fromQI, fromAlias=alias} children) =
|
||||
let
|
||||
newHasAgg = hasAgg || any (isJust . csAggFunction) select
|
||||
newCtx = adjustContext ctx fromQI alias
|
||||
newRPlan = expandStarsForTable newCtx newHasAgg rp
|
||||
in Node newRPlan (map (expandStarsForReadPlan newHasAgg) children)
|
||||
|
||||
-- Choose the appropriate context based on whether we're dealing with "pgrst_source"
|
||||
adjustContext :: ResolverContext -> QualifiedIdentifier -> Maybe Text -> ResolverContext
|
||||
-- When the schema is "" and the table is the source CTE, we assume the true source table is given in the from
|
||||
-- alias and belongs to the request schema. See the bit in `addRels` with `newFrom = ...`.
|
||||
expandStars rPlan@ReadPlan{from=(QualifiedIdentifier "" "pgrst_source"), fromAlias=(Just tblAlias)} =
|
||||
expandStarsForTable ctx{qi=qi{qiName=tblAlias}} rPlan
|
||||
expandStars rPlan@ReadPlan{from=fromTable} =
|
||||
expandStarsForTable ctx{qi=fromTable} rPlan
|
||||
adjustContext context@ResolverContext{qi=ctxQI} (QualifiedIdentifier "" "pgrst_source") (Just a) = context{qi=ctxQI{qiName=a}}
|
||||
adjustContext context fromQI _ = context{qi=fromQI}
|
||||
|
||||
expandStarsForTable :: ResolverContext -> ReadPlan -> ReadPlan
|
||||
expandStarsForTable ctx@ResolverContext{representations, outputType} rplan@ReadPlan{select=selectItems} =
|
||||
-- If we have a '*' select AND the target table has at least one data representation, expand.
|
||||
if ("*" `elem` map (\(field, _, _) -> cfName field) selectItems) && any hasOutputRep knownColumns
|
||||
then rplan{select=concatMap (expandStarSelectItem knownColumns) selectItems}
|
||||
else rplan
|
||||
expandStarsForTable :: ResolverContext -> Bool -> ReadPlan -> ReadPlan
|
||||
expandStarsForTable ctx@ResolverContext{representations, outputType} hasAgg rp@ReadPlan{select=selectFields}
|
||||
-- We expand if either of the below are true:
|
||||
-- * We have a '*' select AND there is an aggregate function in this ReadPlan's sub-tree.
|
||||
-- * We have a '*' select AND the target table has at least one data representation.
|
||||
-- We ignore any '*' selects that have an aggregate function attached (i.e for COUNT(*)).
|
||||
| hasStarSelect && (hasAgg || hasDataRepresentation) = rp{select = concatMap (expandStarSelectField knownColumns) selectFields}
|
||||
| otherwise = rp
|
||||
where
|
||||
hasStarSelect = "*" `elem` map (cfName . csField) filteredSelectFields
|
||||
filteredSelectFields = filter (isNothing . csAggFunction) selectFields
|
||||
hasDataRepresentation = any hasOutputRep knownColumns
|
||||
knownColumns = knownColumnsInContext ctx
|
||||
|
||||
hasOutputRep :: Column -> Bool
|
||||
hasOutputRep col = HM.member (colNominalType col, outputType) representations
|
||||
|
||||
expandStarSelectItem :: [Column] -> (CoercibleField, Maybe Cast, Maybe Alias) -> [(CoercibleField, Maybe Cast, Maybe Alias)]
|
||||
expandStarSelectItem columns (CoercibleField{cfName="*", cfJsonPath=[]}, b, c) = map (\col -> (withOutputFormat ctx $ resolveColumnField col, b, c)) columns
|
||||
expandStarSelectItem _ selectItem = [selectItem]
|
||||
expandStarSelectField :: [Column] -> CoercibleSelectField -> [CoercibleSelectField]
|
||||
expandStarSelectField columns sel@CoercibleSelectField{csField=CoercibleField{cfName="*", cfJsonPath=[]}, csAggFunction=Nothing} =
|
||||
map (\col -> sel { csField = withOutputFormat ctx $ resolveColumnField col }) columns
|
||||
expandStarSelectField _ selectField = [selectField]
|
||||
|
||||
-- | Enforces the `max-rows` config on the result
|
||||
treeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
@@ -513,6 +578,123 @@ findRel schema allRels origin target hint =
|
||||
)
|
||||
) $ fromMaybe mempty $ HM.lookup (QualifiedIdentifier schema origin, schema) allRels
|
||||
|
||||
|
||||
addRelSelects :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addRelSelects node@(Node rp forest)
|
||||
| null forest = Right node
|
||||
| otherwise =
|
||||
let newForest = rights $ addRelSelects <$> forest
|
||||
newRelSelects = mapMaybe generateRelSelectField newForest
|
||||
in Right $ Node rp { relSelect = newRelSelects } newForest
|
||||
|
||||
generateRelSelectField :: ReadPlanTree -> Maybe RelSelectField
|
||||
generateRelSelectField (Node rp@ReadPlan{relToParent=Just _, relAggAlias, relIsSpread = True} _) =
|
||||
Just $ Spread { rsSpreadSel = generateSpreadSelectFields rp, rsAggAlias = relAggAlias }
|
||||
generateRelSelectField (Node ReadPlan{relToParent=Just rel, select, relName, relAlias, relAggAlias, relIsSpread = False} forest) =
|
||||
Just $ JsonEmbed { rsEmbedMode, rsSelName, rsAggAlias = relAggAlias, rsEmptyEmbed }
|
||||
where
|
||||
rsSelName = fromMaybe relName relAlias
|
||||
rsEmbedMode = if relIsToOne rel then JsonObject else JsonArray
|
||||
rsEmptyEmbed = null select && null forest
|
||||
generateRelSelectField _ = Nothing
|
||||
|
||||
generateSpreadSelectFields :: ReadPlan -> [SpreadSelectField]
|
||||
generateSpreadSelectFields ReadPlan{select, relSelect} =
|
||||
-- We combine the select and relSelect fields into a single list of SpreadSelectField.
|
||||
selectSpread ++ relSelectSpread
|
||||
where
|
||||
selectSpread = map selectToSpread select
|
||||
selectToSpread :: CoercibleSelectField -> SpreadSelectField
|
||||
selectToSpread CoercibleSelectField{csField = CoercibleField{cfName}, csAlias} =
|
||||
SpreadSelectField { ssSelName = fromMaybe cfName csAlias, ssSelAggFunction = Nothing, ssSelAggCast = Nothing, ssSelAlias = Nothing }
|
||||
|
||||
relSelectSpread = concatMap relSelectToSpread relSelect
|
||||
relSelectToSpread :: RelSelectField -> [SpreadSelectField]
|
||||
relSelectToSpread (JsonEmbed{rsSelName}) =
|
||||
[SpreadSelectField { ssSelName = rsSelName, ssSelAggFunction = Nothing, ssSelAggCast = Nothing, ssSelAlias = Nothing }]
|
||||
relSelectToSpread (Spread{rsSpreadSel}) =
|
||||
rsSpreadSel
|
||||
|
||||
-- When aggregates are present in a ReadPlan that will be spread, we "hoist"
|
||||
-- to the highest level possible so that their semantics make sense. For instance,
|
||||
-- imagine the user performs the following request:
|
||||
-- `GET /projects?select=client_id,...project_invoices(invoice_total.sum())`
|
||||
--
|
||||
-- In this case, it is sensible that we would expect to receive the sum of the
|
||||
-- `invoice_total`, grouped by the `client_id`. Without hoisting, the sum would
|
||||
-- be performed in the sub-query for the joined table `project_invoices`, thus
|
||||
-- making it essentially a no-op. With hoisting, we hoist the aggregate function
|
||||
-- so that the aggregate function is performed in a more sensible context.
|
||||
--
|
||||
-- We will try to hoist the aggregate function to the highest possible level,
|
||||
-- which means that we hoist until we reach the root node, or until we reach a
|
||||
-- ReadPlan that will be embedded a JSON object or JSON array.
|
||||
|
||||
-- This type alias represents an aggregate that is to be hoisted to the next
|
||||
-- level up. The first tuple of `Alias` and `FieldName` contain the alias for
|
||||
-- the joined table and the original field name for the hoisted field.
|
||||
--
|
||||
-- The second tuple contains the aggregate function to be applied, the cast, and
|
||||
-- the alias, if it was supplied by the user or otherwise determined.
|
||||
type HoistedAgg = ((Alias, FieldName), (AggregateFunction, Maybe Cast, Maybe Alias))
|
||||
|
||||
hoistSpreadAggFunctions :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
hoistSpreadAggFunctions tree = Right $ fst $ applySpreadAggHoistingToNode tree
|
||||
|
||||
applySpreadAggHoistingToNode :: ReadPlanTree -> (ReadPlanTree, [HoistedAgg])
|
||||
applySpreadAggHoistingToNode (Node rp@ReadPlan{relAggAlias, relToParent, relIsSpread} children) =
|
||||
let (newChildren, childAggLists) = unzip $ map applySpreadAggHoistingToNode children
|
||||
allChildAggLists = concat childAggLists
|
||||
(newSelects, aggList) = if depth rp == 0 || (isJust relToParent && not relIsSpread)
|
||||
then (select rp, [])
|
||||
else hoistFromSelectFields relAggAlias (select rp)
|
||||
|
||||
newRelSelects = if null children
|
||||
then relSelect rp
|
||||
else map (hoistIntoRelSelectFields allChildAggLists) $ relSelect rp
|
||||
in (Node rp { select = newSelects, relSelect = newRelSelects } newChildren, aggList)
|
||||
|
||||
-- Hoist aggregate functions from the select list of a ReadPlan, and return the
|
||||
-- updated select list and the list of hoisted aggregates.
|
||||
hoistFromSelectFields :: Alias -> [CoercibleSelectField] -> ([CoercibleSelectField], [HoistedAgg])
|
||||
hoistFromSelectFields aggAlias fields =
|
||||
let (newFields, maybeAggs) = foldr processField ([], []) fields
|
||||
in (newFields, catMaybes maybeAggs)
|
||||
where
|
||||
processField field (newFields, aggList) =
|
||||
let (modifiedField, maybeAgg) = modifyField field
|
||||
in (modifiedField : newFields, maybeAgg : aggList)
|
||||
|
||||
modifyField field =
|
||||
case csAggFunction field of
|
||||
Just aggFunc ->
|
||||
( field { csAggFunction = Nothing, csAggCast = Nothing },
|
||||
Just ((aggAlias, determineFieldName field), (aggFunc, csAggCast field, csAlias field)))
|
||||
Nothing -> (field, Nothing)
|
||||
|
||||
determineFieldName field = fromMaybe (cfName $ csField field) (csAlias field)
|
||||
|
||||
-- Taking the hoisted aggregates, modify the rel selects to apply the aggregates,
|
||||
-- and any applicable casts or aliases.
|
||||
hoistIntoRelSelectFields :: [HoistedAgg] -> RelSelectField -> RelSelectField
|
||||
hoistIntoRelSelectFields aggList r@(Spread {rsSpreadSel = spreadSelects, rsAggAlias = aggAlias}) =
|
||||
r { rsSpreadSel = map updateSelect spreadSelects }
|
||||
where
|
||||
updateSelect s =
|
||||
case lookup (aggAlias, ssSelName s) aggList of
|
||||
Just (aggFunc, aggCast, fldAlias) ->
|
||||
s { ssSelAggFunction = Just aggFunc,
|
||||
ssSelAggCast = aggCast,
|
||||
ssSelAlias = fldAlias }
|
||||
Nothing -> s
|
||||
hoistIntoRelSelectFields _ r = r
|
||||
|
||||
validateAggFunctions :: Bool -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
|
||||
| aggFunctionsAllowed = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
|
||||
| any (isJust . csAggFunction) select = Left AggregatesNotAllowed
|
||||
| otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
|
||||
|
||||
addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||
addFilters ctx ApiRequest{..} rReq =
|
||||
foldr addFilterToNode (Right rReq) flts
|
||||
@@ -586,7 +768,8 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
-- relName = "projects",
|
||||
-- relToParent = Nothing,
|
||||
-- relJoinConds = [],
|
||||
-- relAlias = Nothing, relAggAlias = "clients_projects_1", relHint = Nothing, relJoinType = Nothing, relIsSpread = False, depth = 1
|
||||
-- relAlias = Nothing, relAggAlias = "clients_projects_1", relHint = Nothing, relJoinType = Nothing, relIsSpread = False, depth = 1,
|
||||
-- relSelect = []
|
||||
-- },
|
||||
-- subForest = []
|
||||
-- }
|
||||
@@ -611,7 +794,8 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
-- )
|
||||
-- ],
|
||||
-- order = [], range_ = fullRange, relName = "clients", relToParent = Nothing, relJoinConds = [], relAlias = Nothing, relAggAlias = "", relHint = Nothing,
|
||||
-- relJoinType = Nothing, relIsSpread = False, depth = 0
|
||||
-- relJoinType = Nothing, relIsSpread = False, depth = 0,
|
||||
-- relSelect = []
|
||||
-- },
|
||||
-- subForest = subForst
|
||||
-- }
|
||||
@@ -767,7 +951,7 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
||||
| "*" `elem` fldNames = ["*"]
|
||||
| otherwise = returnings
|
||||
where
|
||||
fldNames = cfName . (\(f, _, _) -> f) <$> select
|
||||
fldNames = cfName . csField <$> select
|
||||
-- Without fkCols, when a mutatePlan to
|
||||
-- /projects?select=name,clients(name) occurs, the RETURNING SQL part would
|
||||
-- be `RETURNING name`(see QueryBuilder). This would make the embedding
|
||||
@@ -809,64 +993,34 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
||||
addFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleLogicTree]
|
||||
addFilterToLogicForest flt lf = CoercibleStmnt flt : lf
|
||||
|
||||
-- | If raw(binary) output is requested, check that MediaType is one of the
|
||||
-- admitted rawMediaTypes and that`?select=...` contains only one field other
|
||||
-- than `*`
|
||||
binaryField :: AppConfig -> MediaType -> Maybe Routine -> ReadPlanTree -> Either ApiRequestError (Maybe FieldName)
|
||||
binaryField AppConfig{configRawMediaTypes} acceptMediaType proc rpTree
|
||||
| isRawMediaType =
|
||||
if (funcReturnsScalar <$> proc) == Just True ||
|
||||
(funcReturnsSetOfScalar <$> proc) == Just True
|
||||
then Right $ Just "pgrst_scalar"
|
||||
else
|
||||
let
|
||||
fieldName = fstFieldName rpTree
|
||||
in
|
||||
case fieldName of
|
||||
Just fld -> Right $ Just fld
|
||||
Nothing -> Left $ BinaryFieldError acceptMediaType
|
||||
| otherwise =
|
||||
Right Nothing
|
||||
-- | 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
|
||||
(ActionMutate _, 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.
|
||||
(ActionRead True, Just (_, mt)) -> Right (NoAgg, mt)
|
||||
(ActionInvoke InvHead, Just (_, mt)) -> Right (NoAgg, mt)
|
||||
(_, Just (x, mt)) -> Right (x, mt)
|
||||
where
|
||||
isRawMediaType = acceptMediaType `elem` configRawMediaTypes `L.union` [MTOctetStream, MTTextPlain, MTTextXML] || isRawPlan acceptMediaType
|
||||
isRawPlan mt = case mt of
|
||||
MTPlan MTOctetStream _ _ -> True
|
||||
MTPlan MTTextPlain _ _ -> True
|
||||
MTPlan MTTextXML _ _ -> True
|
||||
_ -> False
|
||||
|
||||
fstFieldName :: ReadPlanTree -> Maybe FieldName
|
||||
fstFieldName (Node ReadPlan{select=(CoercibleField{cfName="*", cfJsonPath=[]}, _, _):_} []) = Nothing
|
||||
fstFieldName (Node ReadPlan{select=[(CoercibleField{cfName=fld, cfJsonPath=[]}, _, _)]} []) = Just fld
|
||||
fstFieldName _ = Nothing
|
||||
|
||||
|
||||
mediaToAggregate :: MediaType -> Maybe FieldName -> ApiRequest -> ResultAggregate
|
||||
mediaToAggregate mt binField apiReq@ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} =
|
||||
if noAgg then NoAgg
|
||||
else case mt of
|
||||
MTApplicationJSON -> BuiltinAggJson
|
||||
MTSingularJSON strip -> BuiltinAggSingleJson strip
|
||||
MTArrayJSONStrip -> BuiltinAggArrayJsonStrip
|
||||
MTGeoJSON -> BuiltinAggGeoJson
|
||||
MTTextCSV -> BuiltinAggCsv
|
||||
MTAny -> BuiltinAggJson
|
||||
MTOpenAPI -> BuiltinAggJson
|
||||
MTUrlEncoded -> NoAgg -- TODO: unreachable since a previous step (producedMediaTypes) whitelists the media types that can become aggregates.
|
||||
|
||||
-- binary types
|
||||
MTTextPlain -> BuiltinAggBinary binField
|
||||
MTTextXML -> BuiltinAggXml binField
|
||||
MTOctetStream -> BuiltinAggBinary binField
|
||||
MTOther _ -> BuiltinAggBinary binField
|
||||
|
||||
-- Doing `Accept: application/vnd.pgrst.plan; for="application/vnd.pgrst.plan"` doesn't make sense, so we just empty the body.
|
||||
-- TODO: fail instead to be more strict
|
||||
MTPlan (MTPlan{}) _ _ -> NoAgg
|
||||
MTPlan media _ _ -> mediaToAggregate media binField apiReq
|
||||
where
|
||||
noAgg = case act of
|
||||
ActionMutate _ -> rep == Just HeadersOnly || rep == Just None || isNothing rep
|
||||
ActionRead _isHead -> _isHead -- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849
|
||||
ActionInvoke invMethod -> invMethod == InvHead
|
||||
_ -> False
|
||||
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
|
||||
|
||||
@@ -6,11 +6,12 @@ module PostgREST.Plan.ReadPlan
|
||||
|
||||
import Data.Tree (Tree (..))
|
||||
|
||||
import PostgREST.ApiRequest.Types (Alias, Cast, Depth, Hint,
|
||||
import PostgREST.ApiRequest.Types (Alias, Depth, Hint,
|
||||
JoinType, NodeName)
|
||||
import PostgREST.Plan.Types (CoercibleField (..),
|
||||
CoercibleLogicTree,
|
||||
CoercibleOrderTerm)
|
||||
import PostgREST.Plan.Types (CoercibleLogicTree,
|
||||
CoercibleOrderTerm,
|
||||
CoercibleSelectField (..),
|
||||
RelSelectField (..))
|
||||
import PostgREST.RangeQuery (NonnegRange)
|
||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||
QualifiedIdentifier)
|
||||
@@ -28,7 +29,7 @@ data JoinCondition =
|
||||
deriving (Eq, Show)
|
||||
|
||||
data ReadPlan = ReadPlan
|
||||
{ select :: [(CoercibleField, Maybe Cast, Maybe Alias)]
|
||||
{ select :: [CoercibleSelectField]
|
||||
, from :: QualifiedIdentifier
|
||||
, fromAlias :: Maybe Alias
|
||||
, where_ :: [CoercibleLogicTree]
|
||||
@@ -42,6 +43,7 @@ data ReadPlan = ReadPlan
|
||||
, relHint :: Maybe Hint
|
||||
, relJoinType :: Maybe JoinType
|
||||
, relIsSpread :: Bool
|
||||
, relSelect :: [RelSelectField]
|
||||
, depth :: Depth
|
||||
-- ^ used for aliasing
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user