Compare commits
163
Commits
-42
@@ -1,42 +0,0 @@
|
|||||||
freebsd_instance:
|
|
||||||
image_family: freebsd-14-3
|
|
||||||
|
|
||||||
build_task:
|
|
||||||
# Don't change this name without adjusting .github/workflows/build.yaml
|
|
||||||
name: Build FreeBSD (Stack)
|
|
||||||
install_script: pkg install -y postgresql16-client hs-stack git
|
|
||||||
|
|
||||||
only_if: |
|
|
||||||
$CIRRUS_TAG != '' || $CIRRUS_BRANCH == 'main' || $CIRRUS_BRANCH =~ 'v*' ||
|
|
||||||
changesInclude(
|
|
||||||
'.github/workflows/build.yaml',
|
|
||||||
'.github/actions/artifact-from-cirrus/**',
|
|
||||||
'.cirrus.yml',
|
|
||||||
'postgrest.cabal',
|
|
||||||
'stack.yaml*',
|
|
||||||
'**.hs'
|
|
||||||
)
|
|
||||||
|
|
||||||
stack_cache:
|
|
||||||
folders: /.stack
|
|
||||||
fingerprint_script:
|
|
||||||
- echo $CIRRUS_OS
|
|
||||||
- stack --version
|
|
||||||
- md5sum postgrest.cabal
|
|
||||||
- md5sum stack.yaml.lock
|
|
||||||
|
|
||||||
stack_work_cache:
|
|
||||||
folders: .stack-work
|
|
||||||
fingerprint_script:
|
|
||||||
- echo $CIRRUS_OS
|
|
||||||
- stack --version
|
|
||||||
- md5sum postgrest.cabal
|
|
||||||
- md5sum stack.yaml.lock
|
|
||||||
- find main src -type f -iname '*.hs' -exec md5sum "{}" +
|
|
||||||
|
|
||||||
build_script: |
|
|
||||||
stack build -j 1 --local-bin-path . --copy-bins
|
|
||||||
strip postgrest
|
|
||||||
|
|
||||||
bin_artifacts:
|
|
||||||
path: postgrest
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
# Ignore blame for commit that moved protolude files under src/protolude
|
|
||||||
d4949c633e8172d0e4dd8f5c991eaaae6b48fbb0
|
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# TODO: Remove this once a new actionlint release has been cut
|
||||||
|
# and made its way to us through nixpkgs.
|
||||||
|
self-hosted-runner:
|
||||||
|
labels:
|
||||||
|
- macos-15-intel
|
||||||
|
- macos-26
|
||||||
|
- ubuntu-24.04-arm
|
||||||
|
- ubuntu-slim
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
name: Artifact from Cirrus
|
|
||||||
|
|
||||||
description: Waits for a specific Cirrus CI run to complete, then downloads the artifact and uploads it to the current workflow. This will silently succeed if Cirrus CI did not schedule a task within 2 minutes.
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
download:
|
|
||||||
description: Name of Artifact to download from Cirrus CI
|
|
||||||
required: true
|
|
||||||
task:
|
|
||||||
description: Name of Cirrus Task
|
|
||||||
required: true
|
|
||||||
token:
|
|
||||||
description: GitHub Token
|
|
||||||
required: true
|
|
||||||
upload:
|
|
||||||
description: Name of Artifact to upload on GitHub Actions
|
|
||||||
required: true
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- shell: bash
|
|
||||||
run: echo "GH_TOKEN=${{ inputs.token }}" >> "$GITHUB_ENV"
|
|
||||||
- name: Wait for Check Suite to be created
|
|
||||||
id: check-suite
|
|
||||||
env:
|
|
||||||
# GITHUB_SHA does weird things for pull request, so we roll our own:
|
|
||||||
COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
get_check_runs_url() {
|
|
||||||
gh api "repos/{owner}/{repo}/commits/${COMMIT}/check-suites" \
|
|
||||||
| jq -r '.check_suites[] | select(.app.slug == "cirrus-ci") | .check_runs_url'
|
|
||||||
}
|
|
||||||
for _ in $(seq 1 12); do
|
|
||||||
check_runs_url="$(get_check_runs_url)"
|
|
||||||
if [ -z "$check_runs_url" ]; then
|
|
||||||
echo "Cirrus CI task has not started, yet. Waiting..."
|
|
||||||
sleep 10
|
|
||||||
else
|
|
||||||
echo "check_runs_url=$check_runs_url" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
>&2 echo "Cirrus CI check suite not found. Is Cirrus CI enabled for this repo?"
|
|
||||||
- name: Find task by name
|
|
||||||
id: find-task
|
|
||||||
if: steps.check-suite.outputs.check_runs_url
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
get_number_of_tasks() {
|
|
||||||
gh api "${{ steps.check-suite.outputs.check_runs_url }}" \
|
|
||||||
| jq -r '.check_runs | map(select(.name == "${{ inputs.task }}")) | length'
|
|
||||||
}
|
|
||||||
tasks="$(get_number_of_tasks)"
|
|
||||||
case "$tasks" in
|
|
||||||
0)
|
|
||||||
echo "Task not found, assuming it's skipped intentionally..."
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
1)
|
|
||||||
echo "task_found=1" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
>&2 echo "More than 1 task with the same name found. Don't know what to do..."
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
- name: Wait for Cirrus CI to complete task
|
|
||||||
if: steps.find-task.outputs.task_found
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
get_conclusion() {
|
|
||||||
gh api "${{ steps.check-suite.outputs.check_runs_url }}" \
|
|
||||||
| jq -r '.check_runs[] | select(.name == "${{ inputs.task }}" and .status == "completed") | .conclusion'
|
|
||||||
}
|
|
||||||
while true; do
|
|
||||||
conclusion="$(get_conclusion)"
|
|
||||||
if [ -z "$conclusion" ]; then
|
|
||||||
echo "Cirrus CI task has not completed, yet. Waiting..."
|
|
||||||
sleep 30
|
|
||||||
else
|
|
||||||
if [ "$conclusion" == "success" ]; then
|
|
||||||
break
|
|
||||||
else
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
- name: Download artifact from Cirrus CI
|
|
||||||
if: steps.find-task.outputs.task_found
|
|
||||||
id: download
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
get_external_id() {
|
|
||||||
gh api "${{ steps.check-suite.outputs.check_runs_url }}" \
|
|
||||||
| jq -er '.check_runs[] | select(.name == "${{ inputs.task }}") | .external_id'
|
|
||||||
}
|
|
||||||
archive="$(mktemp)"
|
|
||||||
artifacts="$(mktemp -d)"
|
|
||||||
until curl --no-progress-meter --fail -o "${archive}" \
|
|
||||||
"https://api.cirrus-ci.com/v1/artifact/task/$(get_external_id)/${{ inputs.download }}.zip"
|
|
||||||
do
|
|
||||||
# This happens when a tag is pushed on the same commit. In this case the
|
|
||||||
# job is immediately marked as "completed" for us, so we end up here after a few
|
|
||||||
# seconds - but the actual Cirrus CI task is still running and didn't produce its artifact, yet.
|
|
||||||
echo "Artifact not found on Cirrus CI, yet. Waiting..."
|
|
||||||
sleep 30
|
|
||||||
done
|
|
||||||
unzip "${archive}" -d "${artifacts}"
|
|
||||||
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
|
|
||||||
- name: Save artifact to GitHub Actions
|
|
||||||
if: steps.find-task.outputs.task_found
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: ${{ inputs.upload }}
|
|
||||||
path: ${{ steps.download.outputs.artifacts }}
|
|
||||||
if-no-files-found: error
|
|
||||||
@@ -8,6 +8,7 @@ inputs:
|
|||||||
required: true
|
required: true
|
||||||
save-prs:
|
save-prs:
|
||||||
description: Whether to additionally store the cache in a pull request, too. Should only be used for very small caches.
|
description: Whether to additionally store the cache in a pull request, too. Should only be used for very small caches.
|
||||||
|
type: boolean
|
||||||
prefix:
|
prefix:
|
||||||
description: Cache key prefix to be used in both primary key and restore-keys.
|
description: Cache key prefix to be used in both primary key and restore-keys.
|
||||||
required: true
|
required: true
|
||||||
@@ -22,13 +23,13 @@ runs:
|
|||||||
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.path }}
|
path: ${{ inputs.path }}
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-
|
${{ runner.os }}-${{ inputs.prefix }}-
|
||||||
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
- uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||||
with:
|
with:
|
||||||
path: ${{ inputs.path }}
|
path: ${{ inputs.path }}
|
||||||
key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ runner.arch }}-${{ inputs.prefix }}-
|
${{ runner.os }}-${{ inputs.prefix }}-
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: Run anywhere
|
||||||
|
|
||||||
|
description: Runs the same code either in a VM or on the bare machine
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
vm:
|
||||||
|
description: Which VM to run on.
|
||||||
|
envs:
|
||||||
|
description: List of relevant environment variables, which might need to be copied into the VM.
|
||||||
|
prepare:
|
||||||
|
description: Code to run in a prepare step, e.g. installing dependencies.
|
||||||
|
run:
|
||||||
|
description: Code to run as the main action.
|
||||||
|
required: true
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- if: ${{ inputs.vm == 'freebsd' }}
|
||||||
|
uses: vmactions/freebsd-vm@a6de9343ef5747433d9c25784c90e84998b9d69a # v1.4.6
|
||||||
|
with:
|
||||||
|
envs: ${{ inputs.envs }}
|
||||||
|
prepare: ${{ inputs.prepare }}
|
||||||
|
# Work around https://github.com/vmactions/freebsd-vm/issues/59
|
||||||
|
run: |
|
||||||
|
pw user add -n action -m
|
||||||
|
su action -c '${{ inputs.run }}'
|
||||||
|
- if: ${{ inputs.vm == '' }}
|
||||||
|
name: Prepare
|
||||||
|
shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
|
||||||
|
run: ${{ inputs.prepare }}
|
||||||
|
- if: ${{ inputs.vm == '' }}
|
||||||
|
name: Run
|
||||||
|
shell: ${{ runner.os == 'Windows' && 'pwsh' || 'bash' }}
|
||||||
|
run: ${{ inputs.run }}
|
||||||
@@ -16,7 +16,7 @@ runs:
|
|||||||
nix_conf: |-
|
nix_conf: |-
|
||||||
always-allow-substitutes = true
|
always-allow-substitutes = true
|
||||||
max-jobs = auto
|
max-jobs = auto
|
||||||
- uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
|
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||||
with:
|
with:
|
||||||
name: postgrest
|
name: postgrest
|
||||||
authToken: ${{ inputs.authToken }}
|
authToken: ${{ inputs.authToken }}
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ codecov:
|
|||||||
|
|
||||||
comment: false
|
comment: false
|
||||||
|
|
||||||
github_checks:
|
|
||||||
annotations: true
|
|
||||||
|
|
||||||
coverage:
|
coverage:
|
||||||
status:
|
status:
|
||||||
project:
|
project:
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
},
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
|
"matchBaseBranches": [
|
||||||
|
"/^v[0-9]+/"
|
||||||
|
],
|
||||||
"matchManagers": [
|
"matchManagers": [
|
||||||
"haskell-cabal"
|
"haskell-cabal"
|
||||||
],
|
],
|
||||||
@@ -23,6 +26,44 @@
|
|||||||
"/^v[0-9]+/"
|
"/^v[0-9]+/"
|
||||||
],
|
],
|
||||||
"groupName": "all dependencies"
|
"groupName": "all dependencies"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": [
|
||||||
|
"haskell-cabal"
|
||||||
|
],
|
||||||
|
"matchPackageNames": [
|
||||||
|
"base",
|
||||||
|
"bytestring",
|
||||||
|
"containers",
|
||||||
|
"directory",
|
||||||
|
"mtl",
|
||||||
|
"parsec",
|
||||||
|
"process",
|
||||||
|
"text"
|
||||||
|
],
|
||||||
|
"groupName": "GHC dependencies"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": [
|
||||||
|
"haskell-cabal"
|
||||||
|
],
|
||||||
|
"matchPackageNames": [
|
||||||
|
"hasql",
|
||||||
|
"hasql-dynamic-statements",
|
||||||
|
"hasql-notifications",
|
||||||
|
"hasql-transaction",
|
||||||
|
"hasql-pool"
|
||||||
|
],
|
||||||
|
"groupName": "hasql"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": [
|
||||||
|
"haskell-cabal"
|
||||||
|
],
|
||||||
|
"matchPackageNames": [
|
||||||
|
"fuzzyset"
|
||||||
|
],
|
||||||
|
"allowedVersions": "<0.3"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
backport:
|
backport:
|
||||||
name: Backport
|
name: Backport
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-slim
|
||||||
# It triggers only when PR is already merged on either:
|
# It triggers only when PR is already merged on either:
|
||||||
#
|
#
|
||||||
# - The merge event itself (action != labeled) or
|
# - The merge event itself (action != labeled) or
|
||||||
@@ -28,9 +28,9 @@ jobs:
|
|||||||
# This actions creates the github token using the postgrest app secrets
|
# This actions creates the github token using the postgrest app secrets
|
||||||
- name: Create Github App Token
|
- name: Create Github App Token
|
||||||
id: app-token
|
id: app-token
|
||||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ vars.POSTGREST_CI_APP_ID }}
|
client-id: ${{ vars.POSTGREST_CI_APP_ID }}
|
||||||
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
|
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
|
||||||
permission-contents: write
|
permission-contents: write
|
||||||
permission-pull-requests: write
|
permission-pull-requests: write
|
||||||
@@ -38,14 +38,14 @@ jobs:
|
|||||||
|
|
||||||
# This is required for backport action to cherry-pick the PR
|
# This is required for backport action to cherry-pick the PR
|
||||||
- name: Fetch PR ref
|
- name: Fetch PR ref
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
token: ${{ steps.app-token.outputs.token }}
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
|
||||||
# Backport action that creates the PR with given settings
|
# Backport action that creates the PR with given settings
|
||||||
- name: Create backport PR
|
- name: Create backport PR
|
||||||
uses: korthout/backport-action@7c3f6cd5843cac11bc59a04a1b7699af93261670 # v4.5
|
uses: korthout/backport-action@66065406958f46e82238fd59546f5a99e69e22aa # v4.5
|
||||||
with:
|
with:
|
||||||
github_token: ${{ steps.app-token.outputs.token }}
|
github_token: ${{ steps.app-token.outputs.token }}
|
||||||
pull_description: 'Backport for #${pull_number}.'
|
pull_description: 'Backport for #${pull_number}.'
|
||||||
|
|||||||
@@ -31,20 +31,10 @@ concurrency:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
static:
|
static:
|
||||||
strategy:
|
name: Nix - Linux x86-64 static
|
||||||
fail-fast: false
|
runs-on: ubuntu-24.04
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- name: Linux aarch64
|
|
||||||
runs-on: ubuntu-24.04-arm
|
|
||||||
artifact: aarch64
|
|
||||||
- name: Linux x86-64
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
artifact: x86-64
|
|
||||||
name: Nix - ${{ matrix.name }} static
|
|
||||||
runs-on: ${{ matrix.runs-on }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -55,25 +45,25 @@ jobs:
|
|||||||
- name: Save built executable as artifact
|
- name: Save built executable as artifact
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: postgrest-linux-static-${{ matrix.artifact }}
|
name: postgrest-linux-static-x86-64
|
||||||
path: result/bin/postgrest
|
path: result/bin/postgrest
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
run: nix-build -A docker.image --out-link postgrest-docker-${{ matrix.artifact }}.tar.gz
|
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
|
||||||
- name: Save built Docker image as artifact
|
- name: Save built Docker image as artifact
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: postgrest-docker-${{ matrix.artifact }}
|
name: postgrest-docker-x86-64
|
||||||
path: postgrest-docker-${{ matrix.artifact }}.tar.gz
|
path: postgrest-docker.tar.gz
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
macos:
|
macos:
|
||||||
name: Nix - MacOS
|
name: Nix - MacOS
|
||||||
runs-on: macos-15
|
runs-on: macos-26
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -93,64 +83,69 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
|
- name: FreeBSD x86-64
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
vm: freebsd
|
||||||
|
artifact: postgrest-freebsd-x86-64
|
||||||
|
deps: pkg install -y postgresql16-client hs-stack
|
||||||
|
|
||||||
|
- name: Linux aarch64
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
artifact: postgrest-ubuntu-aarch64
|
||||||
|
deps: sudo apt-get update && sudo apt-get install libpq-dev
|
||||||
|
|
||||||
- name: MacOS aarch64
|
- name: MacOS aarch64
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
cache: |
|
|
||||||
~/.stack/pantry
|
|
||||||
~/.stack/snapshots
|
|
||||||
~/.stack/stack.sqlite3
|
|
||||||
artifact: postgrest-macos-aarch64
|
artifact: postgrest-macos-aarch64
|
||||||
deps: brew link --force libpq
|
deps: brew link --force libpq
|
||||||
|
|
||||||
- name: MacOS x86-64
|
- name: MacOS x86-64
|
||||||
runs-on: macos-15-intel
|
runs-on: macos-15-intel
|
||||||
cache: |
|
|
||||||
~/.stack/pantry
|
|
||||||
~/.stack/snapshots
|
|
||||||
~/.stack/stack.sqlite3
|
|
||||||
artifact: postgrest-macos-x86-64
|
artifact: postgrest-macos-x86-64
|
||||||
deps: brew link --force libpq
|
deps: brew link --force libpq
|
||||||
|
|
||||||
- name: Windows
|
- name: Windows
|
||||||
runs-on: windows-2022
|
runs-on: windows-2022
|
||||||
cache: |
|
|
||||||
C:\sr\pantry
|
|
||||||
C:\sr\snapshots
|
|
||||||
C:\sr\stack.sqlite3
|
|
||||||
deps: Add-Content $env:GITHUB_PATH $env:PGBIN
|
deps: Add-Content $env:GITHUB_PATH $env:PGBIN
|
||||||
artifact: postgrest-windows-x86-64
|
artifact: postgrest-windows-x86-64
|
||||||
|
|
||||||
name: Stack - ${{ matrix.name }}
|
name: Stack - ${{ matrix.name }}
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
|
env:
|
||||||
|
# Putting .stack in the working directory helps with moving this in and out of the FreeBSD VM.
|
||||||
|
STACK_ROOT: ${{ github.workspace }}/.stack
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
|
- if: ${{ !matrix.vm }}
|
||||||
|
uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
|
||||||
with:
|
with:
|
||||||
# This must match the version in stack.yaml's resolver
|
# This must match the version in stack.yaml's resolver
|
||||||
ghc-version: 9.10.3
|
ghc-version: 9.6.7
|
||||||
enable-stack: true
|
enable-stack: true
|
||||||
stack-no-global: true
|
stack-no-global: true
|
||||||
stack-setup-ghc: true
|
stack-setup-ghc: true
|
||||||
- name: Cache ~/.stack
|
- name: Cache .stack
|
||||||
uses: ./.github/actions/cache-on-main
|
uses: ./.github/actions/cache-on-main
|
||||||
with:
|
with:
|
||||||
path: ${{ matrix.cache }}
|
path: .stack
|
||||||
prefix: stack
|
prefix: ${{ matrix.vm }}${{ matrix.vm && '-' }}stack
|
||||||
suffix: ${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
|
suffix: ${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
|
||||||
- name: Cache .stack-work
|
- name: Cache .stack-work
|
||||||
uses: ./.github/actions/cache-on-main
|
uses: ./.github/actions/cache-on-main
|
||||||
with:
|
with:
|
||||||
path: .stack-work
|
path: .stack-work
|
||||||
save-prs: true
|
save-prs: true
|
||||||
prefix: stack-work-${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
|
prefix: ${{ matrix.vm }}${{ matrix.vm && '-' }}stack-work-${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
|
||||||
suffix: ${{ hashFiles('main/**/*.hs', 'src/**/*.hs') }}
|
suffix: ${{ hashFiles('main/**/*.hs', 'src/**/*.hs') }}
|
||||||
- name: Install dependencies
|
|
||||||
if: matrix.deps
|
|
||||||
run: ${{ matrix.deps }}
|
|
||||||
- name: Build with Stack
|
- name: Build with Stack
|
||||||
run: stack build --lock-file error-on-write --local-bin-path result --copy-bins
|
uses: ./.github/actions/run-anywhere
|
||||||
- name: Strip Executable
|
with:
|
||||||
run: strip result/postgrest*
|
vm: ${{ matrix.vm }}
|
||||||
|
envs: STACK_ROOT
|
||||||
|
prepare: ${{ matrix.deps }}
|
||||||
|
run: |
|
||||||
|
stack build --lock-file error-on-write --local-bin-path result --copy-bins
|
||||||
|
strip result/postgrest*
|
||||||
- name: Save built executable as artifact
|
- name: Save built executable as artifact
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
@@ -161,28 +156,15 @@ jobs:
|
|||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
freebsd:
|
|
||||||
name: Stack - FreeBSD from CirrusCI
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
- uses: ./.github/actions/artifact-from-cirrus
|
|
||||||
with:
|
|
||||||
token: ${{ github.token }}
|
|
||||||
task: Build FreeBSD (Stack)
|
|
||||||
download: bin
|
|
||||||
upload: postgrest-freebsd-x86-64
|
|
||||||
|
|
||||||
|
|
||||||
cabal:
|
cabal:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
ghc: ['9.10.3', '9.12.3']
|
ghc: ['9.6.7', '9.8.4']
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
|
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
|
- uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
|
||||||
with:
|
with:
|
||||||
ghc-version: ${{ matrix.ghc }}
|
ghc-version: ${{ matrix.ghc }}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
name: Lint & Style
|
name: Lint & Style
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
name: Commit
|
name: Commit
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 100 # fetch history (last 100 commits) instead of default shallow clone history, this is deemed enough for a PR history
|
fetch-depth: 100 # fetch history (last 100 commits) instead of default shallow clone history, this is deemed enough for a PR history
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
|
|||||||
@@ -29,28 +29,19 @@ jobs:
|
|||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: Build
|
|
||||||
uses: ./.github/workflows/build.yaml
|
|
||||||
secrets:
|
|
||||||
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
|
||||||
|
|
||||||
|
|
||||||
tag:
|
tag:
|
||||||
name: Tag
|
name: Tag
|
||||||
concurrency:
|
concurrency:
|
||||||
# Never tag outdated commits on the main branch by skipping superseded commits
|
# Never tag outdated commits on the main branch by skipping superseded commits
|
||||||
group: ci-tag-${{ (github.ref == 'refs/heads/main' && github.ref) || github.run_id }}
|
group: ci-tag-${{ (github.ref == 'refs/heads/main' && github.ref) || github.run_id }}
|
||||||
# TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved
|
cancel-in-progress: true
|
||||||
cancel-in-progress: false
|
|
||||||
if: vars.RELEASE_ENABLED
|
if: vars.RELEASE_ENABLED
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-slim
|
||||||
needs:
|
needs:
|
||||||
- docs
|
- docs
|
||||||
- test
|
- test
|
||||||
- build
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
|
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
|
||||||
- name: Tag latest commit
|
- name: Tag latest commit
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ jobs:
|
|||||||
name: Build
|
name: Build
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
name: Spellcheck
|
name: Spellcheck
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -7,12 +7,37 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
linkcheck:
|
linkcheck:
|
||||||
|
name: Linkcheck
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
tools: docs.linkcheck.bin
|
tools: docs.linkcheck.bin
|
||||||
- run: postgrest-docs-linkcheck
|
|
||||||
|
- name: Run Linkcheck
|
||||||
|
id: linkcheck
|
||||||
|
run: postgrest-docs-linkcheck
|
||||||
|
|
||||||
|
# This actions creates the github token using the postgrest app secrets
|
||||||
|
- name: Create Github App Token (Runs only on linkcheck failure)
|
||||||
|
id: app-token
|
||||||
|
if: ${{ failure() && steps.linkcheck.outcome == 'failure' }} # only create the token on linkcheck failure
|
||||||
|
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||||
|
with:
|
||||||
|
client-id: ${{ vars.POSTGREST_CI_APP_ID }}
|
||||||
|
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
|
||||||
|
permission-issues: write # required for commenting on issues
|
||||||
|
|
||||||
|
- name: Notify on linkcheck failure by commenting on GH Issue 4106
|
||||||
|
if: ${{ failure() && steps.linkcheck.outcome == 'failure' }}
|
||||||
|
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
|
||||||
|
with:
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
issue-number: 4106
|
||||||
|
body: |
|
||||||
|
**Linkcheck Job Failed!**
|
||||||
|
|
||||||
|
A broken link was detected in the docs. Please check the [failed run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ on:
|
|||||||
concurrency:
|
concurrency:
|
||||||
# Terminate all previous runs of the same workflow for the same tag.
|
# Terminate all previous runs of the same workflow for the same tag.
|
||||||
group: release-${{ github.ref }}
|
group: release-${{ github.ref }}
|
||||||
# TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved
|
cancel-in-progress: true
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -20,13 +19,15 @@ jobs:
|
|||||||
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||||
|
|
||||||
|
|
||||||
prepare:
|
github:
|
||||||
name: Prepare
|
name: GitHub
|
||||||
runs-on: ubuntu-24.04
|
permissions:
|
||||||
|
contents: write
|
||||||
|
runs-on: ubuntu-slim
|
||||||
needs:
|
needs:
|
||||||
- build
|
- build
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Check the version to be released
|
- name: Check the version to be released
|
||||||
run: |
|
run: |
|
||||||
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||||
@@ -48,23 +49,7 @@ jobs:
|
|||||||
|
|
||||||
echo "Relevant extract from CHANGELOG.md:"
|
echo "Relevant extract from CHANGELOG.md:"
|
||||||
cat CHANGES.md
|
cat CHANGES.md
|
||||||
- name: Save CHANGES.md as artifact
|
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
||||||
with:
|
|
||||||
name: release-changes
|
|
||||||
path: CHANGES.md
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
|
|
||||||
github:
|
|
||||||
name: GitHub
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs:
|
|
||||||
- prepare
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
@@ -75,9 +60,6 @@ jobs:
|
|||||||
|
|
||||||
mkdir -p release-bundle
|
mkdir -p release-bundle
|
||||||
|
|
||||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-aarch64.tar.xz" \
|
|
||||||
-C artifacts/postgrest-linux-static-aarch64 postgrest
|
|
||||||
|
|
||||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-x86-64.tar.xz" \
|
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-x86-64.tar.xz" \
|
||||||
-C artifacts/postgrest-linux-static-x86-64 postgrest
|
-C artifacts/postgrest-linux-static-x86-64 postgrest
|
||||||
|
|
||||||
@@ -90,6 +72,9 @@ jobs:
|
|||||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \
|
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \
|
||||||
-C artifacts/postgrest-freebsd-x86-64 postgrest
|
-C artifacts/postgrest-freebsd-x86-64 postgrest
|
||||||
|
|
||||||
|
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-ubuntu-aarch64.tar.xz" \
|
||||||
|
-C artifacts/postgrest-ubuntu-aarch64 postgrest
|
||||||
|
|
||||||
zip --junk-paths "release-bundle/postgrest-${GITHUB_REF_NAME}-windows-x86-64.zip" \
|
zip --junk-paths "release-bundle/postgrest-${GITHUB_REF_NAME}-windows-x86-64.zip" \
|
||||||
artifacts/postgrest-windows-x86-64/postgrest.exe
|
artifacts/postgrest-windows-x86-64/postgrest.exe
|
||||||
|
|
||||||
@@ -116,14 +101,14 @@ jobs:
|
|||||||
gh release edit devel \
|
gh release edit devel \
|
||||||
-t devel \
|
-t devel \
|
||||||
--verify-tag \
|
--verify-tag \
|
||||||
-F artifacts/release-changes/CHANGES.md \
|
-F CHANGES.md \
|
||||||
--prerelease
|
--prerelease
|
||||||
gh release upload --clobber devel release-bundle/*
|
gh release upload --clobber devel release-bundle/*
|
||||||
else
|
else
|
||||||
gh release create "${GITHUB_REF_NAME}" \
|
gh release create "${GITHUB_REF_NAME}" \
|
||||||
-t "${GITHUB_REF_NAME}" \
|
-t "${GITHUB_REF_NAME}" \
|
||||||
--verify-tag \
|
--verify-tag \
|
||||||
-F artifacts/release-changes/CHANGES.md \
|
-F CHANGES.md \
|
||||||
release-bundle/*
|
release-bundle/*
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -132,62 +117,63 @@ jobs:
|
|||||||
name: Docker Hub
|
name: Docker Hub
|
||||||
runs-on: ubuntu-24.04-arm
|
runs-on: ubuntu-24.04-arm
|
||||||
needs:
|
needs:
|
||||||
- prepare
|
- github
|
||||||
if: |
|
if: |
|
||||||
vars.DOCKER_REPO && vars.DOCKER_USER
|
vars.DOCKER_REPO && vars.DOCKER_USER
|
||||||
env:
|
env:
|
||||||
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
|
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Download aarch64 Docker image
|
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
||||||
with:
|
|
||||||
name: postgrest-docker-aarch64
|
|
||||||
- name: Download x86-64 Docker image
|
- name: Download x86-64 Docker image
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: postgrest-docker-x86-64
|
name: postgrest-docker-x86-64
|
||||||
- uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
- name: Download aarch64 binary
|
||||||
- uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
|
with:
|
||||||
|
name: postgrest-ubuntu-aarch64
|
||||||
|
- uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
|
||||||
|
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
username: ${{ vars.DOCKER_USER }}
|
username: ${{ vars.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_PASS }}
|
password: ${{ secrets.DOCKER_PASS }}
|
||||||
|
- name: Build aarch64 Docker image
|
||||||
|
run: |
|
||||||
|
# This only pushes the image via digest, not a tag. This will not appear
|
||||||
|
# in the image list on Docker Hub, yet. It will be later added to the main
|
||||||
|
# tag's manifest.
|
||||||
|
docker buildx build \
|
||||||
|
-t "$DOCKER_REPO/postgrest" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--output push-by-digest=true,type=image,push=true \
|
||||||
|
--metadata-file metadata.json \
|
||||||
|
.
|
||||||
|
echo "SHA256_ARM=$(jq -r '."containerimage.digest"' metadata.json)" >> "$GITHUB_ENV"
|
||||||
- name: Publish images on Docker Hub
|
- name: Publish images on Docker Hub
|
||||||
run: |
|
run: |
|
||||||
docker load -i postgrest-docker-aarch64.tar.gz
|
docker load -i postgrest-docker.tar.gz
|
||||||
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64"
|
|
||||||
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64"
|
|
||||||
|
|
||||||
docker load -i postgrest-docker-x86-64.tar.gz
|
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
|
||||||
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64"
|
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
|
||||||
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64"
|
docker buildx imagetools create --append \
|
||||||
|
-t "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}" \
|
||||||
docker manifest create "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}" \
|
"$DOCKER_REPO/postgrest@$SHA256_ARM"
|
||||||
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64" \
|
|
||||||
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64"
|
|
||||||
docker manifest push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
|
|
||||||
|
|
||||||
# Only tag 'latest' for full releases
|
# Only tag 'latest' for full releases
|
||||||
if [ "${GITHUB_REF_NAME}" != "devel" ]; then
|
if [ "${GITHUB_REF_NAME}" != "devel" ]; then
|
||||||
echo "Pushing to 'latest' tag for full release of ${GITHUB_REF_NAME} ..."
|
echo "Pushing to 'latest' tag for full release of ${GITHUB_REF_NAME} ..."
|
||||||
docker manifest create "$DOCKER_REPO/postgrest:latest" \
|
docker tag postgrest:latest "$DOCKER_REPO"/postgrest:latest
|
||||||
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-arm64" \
|
docker push "$DOCKER_REPO"/postgrest:latest
|
||||||
"$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}-linux-amd64"
|
docker buildx imagetools create --append \
|
||||||
docker manifest push "$DOCKER_REPO/postgrest:latest"
|
-t "$DOCKER_REPO/postgrest:latest" \
|
||||||
|
"$DOCKER_REPO/postgrest@$SHA256_ARM"
|
||||||
else
|
else
|
||||||
echo "Skipping push to 'latest' tag for pre-release..."
|
echo "Skipping push to 'latest' tag for pre-release..."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
docker-description:
|
|
||||||
name: Docker Hub Description
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
if: |
|
|
||||||
vars.DOCKER_REPO && vars.DOCKER_USER &&
|
|
||||||
github.ref == 'refs/tags/devel'
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
||||||
- uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
|
- uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
|
||||||
|
if: github.ref == 'refs/tags/devel'
|
||||||
|
name: Docker Hub Description
|
||||||
with:
|
with:
|
||||||
username: ${{ vars.DOCKER_USER }}
|
username: ${{ vars.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_PASS }}
|
password: ${{ secrets.DOCKER_PASS }}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ jobs:
|
|||||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -49,10 +49,10 @@ jobs:
|
|||||||
|
|
||||||
- run: postgrest-cabal-update
|
- run: postgrest-cabal-update
|
||||||
|
|
||||||
- name: Run coverage (IO tests and Spec tests against latest supported PostgreSQL)
|
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
||||||
run: postgrest-coverage
|
run: postgrest-coverage
|
||||||
- name: Upload coverage to codecov
|
- name: Upload coverage to codecov
|
||||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6.0.1
|
||||||
with:
|
with:
|
||||||
files: ./coverage/codecov.json
|
files: ./coverage/codecov.json
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
@@ -70,7 +70,8 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
pgVersion: [14, 15, 16, 17, 18]
|
# Latest version is tested via `coverage` above.
|
||||||
|
pgVersion: [13, 14, 15, 16]
|
||||||
name: PG ${{ matrix.pgVersion }}
|
name: PG ${{ matrix.pgVersion }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
defaults:
|
defaults:
|
||||||
@@ -79,7 +80,7 @@ jobs:
|
|||||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -109,7 +110,7 @@ jobs:
|
|||||||
name: Memory
|
name: Memory
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
@@ -124,12 +125,13 @@ jobs:
|
|||||||
|
|
||||||
loadtest:
|
loadtest:
|
||||||
strategy:
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
kind: ['mixed', 'errors', 'jwt-hs', 'jwt-hs-cache', 'jwt-hs-cache-worst', 'jwt-rsa', 'jwt-rsa-cache', 'jwt-rsa-cache-worst']
|
kind: ['mixed', 'jwt-hs', 'jwt-hs-cache', 'jwt-hs-cache-worst', 'jwt-rsa', 'jwt-rsa-cache', 'jwt-rsa-cache-worst']
|
||||||
name: Loadtest
|
name: Loadtest
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
@@ -164,7 +166,7 @@ jobs:
|
|||||||
name: Flake Check
|
name: Flake Check
|
||||||
runs-on: ${{ matrix.runs-on }}
|
runs-on: ${{ matrix.runs-on }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
|
|||||||
@@ -26,4 +26,3 @@ loadtest
|
|||||||
.docs-build
|
.docs-build
|
||||||
gen_targets.http
|
gen_targets.http
|
||||||
gen_jwk.json
|
gen_jwk.json
|
||||||
gen_private.json
|
|
||||||
|
|||||||
+1
-1
@@ -7,4 +7,4 @@ python:
|
|||||||
build:
|
build:
|
||||||
os: ubuntu-24.04
|
os: ubuntu-24.04
|
||||||
tools:
|
tools:
|
||||||
python: "3.12"
|
python: "3.11"
|
||||||
|
|||||||
+21
-27
@@ -4,34 +4,28 @@ All notable changes to this project will be documented in this file. From versio
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
### Added
|
## [14.13] - 2026-06-04
|
||||||
|
|
||||||
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
|
### Fixed
|
||||||
- Add string slicing operator for `jwt-role-claim-key` by @taimoorzaeem in #4599
|
|
||||||
- Optimize requests with `Prefer: count=exact` that do not use ranges or `db-max-rows` by @laurenceisla in #3957
|
- Fix connection retrying message in `PGRST000` error by @netqo in #4980
|
||||||
+ Removed unnecessary double count when building the `Content-Range`.
|
+ Remove redundant "Retrying the connection." from message because it is logged separately
|
||||||
- Add config `client-error-verbosity` to customize error verbosity by @taimoorzaeem in #4088, #3980, #3824
|
- Fix request failures when `work_mem` is set on a role by @laurenceisla in #4955
|
||||||
- Add `Vary` header to responses by @develop7 in #4609
|
|
||||||
- Add config `db-timezone-enabled` for optional querying of timezones by @taimoorzaeem in #4751
|
## [14.12] - 2026-05-20
|
||||||
- Log schema cache queries timings on `log-level=debug` by @steve-chavez in #4805
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix race condition in pool_available metric causing negative values during network instability by @mkleczek in #4622
|
||||||
|
|
||||||
|
## [14.11] - 2026-05-04
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- Shutdown should wait for in flight requests by @mkleczek in #4702
|
|
||||||
- Fix login with uppercase and mixed case role names by @taimoorzaeem in #4678
|
- Fix login with uppercase and mixed case role names by @taimoorzaeem in #4678
|
||||||
- Remove automatic transaction retries on `40001 (serialization_failure)` errors to prevent replication lag by @laurenceisla in #3673
|
- Restore Listener query shape so it can be found in `pg_stat_activity` by @mkleczek in #4857 #4859
|
||||||
- Fix unexpected results when embedding and filtering the same table more than once by @laurenceisla in #4075
|
- The LISTEN channel now automatically recovers when it stops working due to a PostgreSQL bug @laurenceisla in #3147
|
||||||
|
- Fix misleading "Functions" name on schema cache summary in startup logs by @taimoorzaeem in #4821
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Drop support for PostgreSQL EOL version 13 by @wolfgangwalther in #4193
|
|
||||||
- All responses now include a `Vary` header by @develop7 in #4609
|
|
||||||
- Log error when `db-schemas` config contains schema `pg_catalog` or `information_schema` by @taimoorzaeem in #4359
|
|
||||||
+ Now fails at startup. Prior to this, it failed with `PGRST205` on requests related to these schemas.
|
|
||||||
- Build a static executable for aarch64-linux by @wolfgangwalther in #4193
|
|
||||||
- Build the minimal docker image for aarch64-linux by @wolfgangwalther in #4193
|
|
||||||
- The name of an embedded table can no longer be used in filters if it has an alias by @laurenceisla in #4075
|
|
||||||
+ e.g. `?select=alias:table(*)&table.id=eq.1` is not possible anymore, use `?select=alias:table(*)&alias.id=eq.1` instead.
|
|
||||||
|
|
||||||
## [14.10] - 2026-04-16
|
## [14.10] - 2026-04-16
|
||||||
|
|
||||||
@@ -87,7 +81,7 @@ All notable changes to this project will be documented in this file. From versio
|
|||||||
|
|
||||||
- Ensure Listener connections are released by @mkleczek in #4614
|
- Ensure Listener connections are released by @mkleczek in #4614
|
||||||
- Fix incorrectly filtering the returned representation for PATCH requests when using `or/and` filters by @laurenceisla in #3707
|
- Fix incorrectly filtering the returned representation for PATCH requests when using `or/and` filters by @laurenceisla in #3707
|
||||||
- Fix listener running with exception masked after first failure by @mkleczek in #4615
|
- Fix listener running with exception masked after first failure by @mkleczek #4615
|
||||||
|
|
||||||
## [14.3] - 2026-01-03
|
## [14.3] - 2026-01-03
|
||||||
|
|
||||||
@@ -691,7 +685,7 @@ All notable changes to this project will be documented in this file. From versio
|
|||||||
### Added
|
### Added
|
||||||
|
|
||||||
- #1933, #2109, Add a minimal health check endpoint - @steve-chavez
|
- #1933, #2109, Add a minimal health check endpoint - @steve-chavez
|
||||||
+ For enabling this, the `admin-server-port` config must be set explicitly
|
+ For enabling this, the `admin-server-port` config must be set explictly
|
||||||
+ A `<host>:<admin_server_port>/live` endpoint is available for checking if postgrest is running on its port/socket. 200 OK = alive, 503 = dead.
|
+ A `<host>:<admin_server_port>/live` endpoint is available for checking if postgrest is running on its port/socket. 200 OK = alive, 503 = dead.
|
||||||
+ A `<host>:<admin_server_port>/ready` endpoint is available for checking a correct internal state(the database connection plus the schema cache). 200 OK = ready, 503 = not ready.
|
+ A `<host>:<admin_server_port>/ready` endpoint is available for checking a correct internal state(the database connection plus the schema cache). 200 OK = ready, 503 = not ready.
|
||||||
- #1988, Add the current user to the request log on stdout - @DavidLindbom, @wolfgangwalther
|
- #1988, Add the current user to the request log on stdout - @DavidLindbom, @wolfgangwalther
|
||||||
@@ -1174,7 +1168,7 @@ All notable changes to this project will be documented in this file. From versio
|
|||||||
- Customize content negotiation per route - @begriffs
|
- Customize content negotiation per route - @begriffs
|
||||||
- Allow using nulls order without explicit order direction - @steve-chavez
|
- Allow using nulls order without explicit order direction - @steve-chavez
|
||||||
- Fatal error on postgres unsupported version, format supported version in error message - @steve-chavez
|
- Fatal error on postgres unsupported version, format supported version in error message - @steve-chavez
|
||||||
- Prevent database memory consumption by prepared statements caches - @ruslantalpa
|
- Prevent database memory cosumption by prepared statements caches - @ruslantalpa
|
||||||
- Use specific columns in the RETURNING section - @ruslantalpa
|
- Use specific columns in the RETURNING section - @ruslantalpa
|
||||||
- Fix columns alias for RETURNING - @steve-chavez
|
- Fix columns alias for RETURNING - @steve-chavez
|
||||||
|
|
||||||
|
|||||||
+14
-21
@@ -1,12 +1,17 @@
|
|||||||
# Contributing to PostgREST
|
# Contributing to PostgREST
|
||||||
|
|
||||||
## AI Policy
|
**First:** if you're unsure or afraid of _anything_, just ask or
|
||||||
|
submit the issue or pull request anyways. You won't be yelled at
|
||||||
|
for giving your best effort. The worst that can happen is that
|
||||||
|
you'll be politely asked to change something. We appreciate any
|
||||||
|
sort of contributions, and don't want a wall of rules to get in the
|
||||||
|
way of that.
|
||||||
|
|
||||||
We adhere to [Gentoo's AI policy](https://wiki.gentoo.org/wiki/Project:Council/AI_policy):
|
However, for those individuals who want a bit more guidance on the
|
||||||
|
best way to contribute to the project, read on. This document will
|
||||||
> It is expressly forbidden to contribute [...] any content that has been created with the assistance of Natural Language Processing artificial intelligence tools. This motion can be revisited, should a case been made over such a tool that does not pose copyright, ethical and quality concerns.
|
cover what we're looking for. By addressing all the points we're
|
||||||
|
looking for, it raises the chances we can quickly merge or address
|
||||||
You can find more about its rationale [here](https://wiki.gentoo.org/wiki/Project:Council/AI_policy#Rationale).
|
your contributions.
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
|
|
||||||
@@ -35,12 +40,12 @@ For questions on how to use PostgREST, please use
|
|||||||
We have a fully nix-based development environment with many tools for a smooth development workflow available.
|
We have a fully nix-based development environment with many tools for a smooth development workflow available.
|
||||||
Check the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md) on how to set it up and use it.
|
Check the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md) on how to set it up and use it.
|
||||||
|
|
||||||
|
### Haskell Conventions
|
||||||
|
|
||||||
* All contributions must pass the tests before being merged. When
|
* All contributions must pass the tests before being merged. When
|
||||||
you create a pull request your code will automatically be tested.
|
you create a pull request your code will automatically be tested.
|
||||||
|
|
||||||
* All fixes or features must have a test proving the improvement.
|
* All code must also pass [hlint](http://community.haskell.org/~ndm/hlint/) and [stylish-haskell](https://github.com/jaspervdj/stylish-haskell)
|
||||||
|
|
||||||
* All code must also pass a [linter](http://community.haskell.org/~ndm/hlint/) and [styler](https://github.com/jaspervdj/stylish-haskell)
|
|
||||||
with no warnings. This helps enforce a uniform style for all committers. Continuous integration will check this as well on every
|
with no warnings. This helps enforce a uniform style for all committers. Continuous integration will check this as well on every
|
||||||
pull request. There are useful tools in the nix-shell that help with checking this locally. You can run `postgrest-check` to do this manually but
|
pull request. There are useful tools in the nix-shell that help with checking this locally. You can run `postgrest-check` to do this manually but
|
||||||
we recommend adding it to `.git/hooks/pre-commit` as `nix-shell --run postgrest-check` to automatically check this before doing a commit.
|
we recommend adding it to `.git/hooks/pre-commit` as `nix-shell --run postgrest-check` to automatically check this before doing a commit.
|
||||||
@@ -48,15 +53,3 @@ Check the [development docs](https://github.com/PostgREST/postgrest/blob/main/ni
|
|||||||
### Running Tests
|
### Running Tests
|
||||||
|
|
||||||
For instructions on running tests, see the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md#testing).
|
For instructions on running tests, see the [development docs](https://github.com/PostgREST/postgrest/blob/main/nix/README.md#testing).
|
||||||
|
|
||||||
### Structuring commits in pull requests
|
|
||||||
|
|
||||||
To simplify reviews, make it easy to split pull requests if deemed necessary, and to maintain clean and meaningful history of changes, you will be asked to update your PR if it does not follow the below rules:
|
|
||||||
|
|
||||||
* It must be possible to merge the PR branch into target using `git merge --ff-only`, ie. the source branch must be rebased on top of target.
|
|
||||||
* No merge commits in the source branch.
|
|
||||||
* All commits in the source branch must be self contained, meaning: it should be possible to treat each commit as a separate PR.
|
|
||||||
* Commits in the source branch must contain only related changes (related means the changes target a single problem/goal). For example, any refactorings should be isolated from the actual change implementation into separate commits.
|
|
||||||
* Tests, documentation, and changelog updates should be contained in the same commits as the actual code changes they relate to. An exception to this rule is when test or documentation changes are made in separate PR.
|
|
||||||
* Commit messages must be prefixed with one of the prefixes defined in [the list used by commit verification scripts](https://github.com/PostgREST/postgrest/blob/main/nix/tools/gitTools.nix#L11).
|
|
||||||
* Commit messages should contain a longer description of the purpose of the changes contained in the commit and, for non-trivial changes, a description of the changes themselves.
|
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# PostgREST Docker Hub image for aarch64.
|
||||||
|
# The x86-64 is a single-static-binary image built via Nix, see:
|
||||||
|
# nix/tools/docker/README.md
|
||||||
|
|
||||||
|
FROM ubuntu:resolute@sha256:f3d28607ddd78734bb7f71f117f3c6706c666b8b76cbff7c9ff6e5718d46ff64 AS postgrest
|
||||||
|
|
||||||
|
RUN apt-get update -y \
|
||||||
|
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
|
||||||
|
&& apt-get clean \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY postgrest /usr/bin/postgrest
|
||||||
|
RUN chmod +x /usr/bin/postgrest
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
USER 1000
|
||||||
|
|
||||||
|
# Use the array form to avoid running the command using bash, which does not handle `SIGTERM` properly.
|
||||||
|
# See https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop
|
||||||
|
CMD ["postgrest"]
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
Copyright (c) 2014-2026 The PostgREST contributors
|
Copyright (c) 2014 Joe Nelson
|
||||||
|
Copyright (c) 2019 Steve Chavez
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
a copy of this software and associated documentation files (the
|
a copy of this software and associated documentation files (the
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ and limited with - range headers. More about
|
|||||||
## Data Integrity
|
## Data Integrity
|
||||||
|
|
||||||
Rather than relying on an Object Relational Mapper and custom
|
Rather than relying on an Object Relational Mapper and custom
|
||||||
imperative coding, this system requires you to put declarative constraints
|
imperative coding, this system requires you put declarative constraints
|
||||||
directly into your database. Hence no application can corrupt your
|
directly into your database. Hence no application can corrupt your
|
||||||
data (including your API server).
|
data (including your API server).
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,2 @@
|
|||||||
packages: postgrest.cabal
|
packages: postgrest.cabal
|
||||||
tests: true
|
tests: true
|
||||||
allow-newer:
|
|
||||||
hasql:postgresql-libpq
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
index-state: hackage.haskell.org 2026-04-18T18:42:36Z
|
index-state: hackage.haskell.org 2025-10-29T04:02:18Z
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,6 @@
|
|||||||
{ system ? builtins.currentSystem
|
{ system ? builtins.currentSystem
|
||||||
|
|
||||||
, compiler ? "ghc9123"
|
, compiler ? "ghc948"
|
||||||
|
|
||||||
, # Commit of the Nixpkgs repository that we want to use.
|
, # Commit of the Nixpkgs repository that we want to use.
|
||||||
# It defaults to reading the inputs from flake.lock, which serves
|
# It defaults to reading the inputs from flake.lock, which serves
|
||||||
@@ -44,6 +44,7 @@ let
|
|||||||
allOverlays.checked-shell-script
|
allOverlays.checked-shell-script
|
||||||
allOverlays.gitignore
|
allOverlays.gitignore
|
||||||
(allOverlays.haskell-packages { inherit compiler; })
|
(allOverlays.haskell-packages { inherit compiler; })
|
||||||
|
allOverlays.slocat
|
||||||
];
|
];
|
||||||
|
|
||||||
# Evaluated expression of the Nixpkgs repository.
|
# Evaluated expression of the Nixpkgs repository.
|
||||||
@@ -52,11 +53,11 @@ let
|
|||||||
|
|
||||||
postgresqlVersions =
|
postgresqlVersions =
|
||||||
[
|
[
|
||||||
{ name = "pg-18"; postgresql = pkgs.postgresql_18.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
|
||||||
{ name = "pg-17"; postgresql = pkgs.postgresql_17.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
{ name = "pg-17"; postgresql = pkgs.postgresql_17.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
{ name = "pg-16"; postgresql = pkgs.postgresql_16.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
{ name = "pg-16"; postgresql = pkgs.postgresql_16.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
{ name = "pg-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
{ name = "pg-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
{ name = "pg-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
{ name = "pg-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
|
{ name = "pg-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
];
|
];
|
||||||
|
|
||||||
haskellPackages = pkgs.haskell.packages."${compiler}";
|
haskellPackages = pkgs.haskell.packages."${compiler}";
|
||||||
|
|||||||
@@ -61,3 +61,5 @@ The image is built from scratch using
|
|||||||
no commands are listed in the image history. See the [PostgREST
|
no commands are listed in the image history. See the [PostgREST
|
||||||
repository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for
|
repository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for
|
||||||
details on the build process and how to inspect the image.
|
details on the build process and how to inspect the image.
|
||||||
|
|
||||||
|
This does not apply to the arm64 variant, which is based on Ubuntu.
|
||||||
|
|||||||
+3
-3
@@ -48,14 +48,14 @@ source_suffix = ".rst"
|
|||||||
# The master toctree document.
|
# The master toctree document.
|
||||||
master_doc = "index"
|
master_doc = "index"
|
||||||
|
|
||||||
# This is overridden by readthedocs with the version tag anyway
|
# This is overriden by readthedocs with the version tag anyway
|
||||||
version = "devel"
|
version = "14"
|
||||||
# To avoid repetition in <title> we set this to an empty string.
|
# To avoid repetition in <title> we set this to an empty string.
|
||||||
release = ""
|
release = ""
|
||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = "PostgREST " + version
|
project = "PostgREST " + version
|
||||||
author = "The PostgREST contributors"
|
author = "Joe Nelson, Steve Chavez"
|
||||||
copyright = "2017, " + author
|
copyright = "2017, " + author
|
||||||
|
|
||||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ Example Apps
|
|||||||
* `archtika <https://github.com/thiloho/archtika>`_ - self-hosted CMS
|
* `archtika <https://github.com/thiloho/archtika>`_ - self-hosted CMS
|
||||||
* `delibrium-postgrest <https://gitlab.com/delibrium/delibrium-postgrest/>`_ - example school API and front-end in Vue.js
|
* `delibrium-postgrest <https://gitlab.com/delibrium/delibrium-postgrest/>`_ - example school API and front-end in Vue.js
|
||||||
* `ETH-transactions-storage <https://github.com/Adamant-im/ETH-transactions-storage>`_ - indexer for Ethereum to get transaction list by ETH address
|
* `ETH-transactions-storage <https://github.com/Adamant-im/ETH-transactions-storage>`_ - indexer for Ethereum to get transaction list by ETH address
|
||||||
* `fullstack template <https://github.com/jenstroeger/fullstack-webapp-template>`_ - a complete fullstack webapp template using PG as db and message queue, Python and Dramatiq to implement async jobs, db migrations, test runners, and more.
|
|
||||||
* `general <https://github.com/PierreRochard/general>`_ - example auth back-end
|
* `general <https://github.com/PierreRochard/general>`_ - example auth back-end
|
||||||
* `guild-operators <https://github.com/cardano-community/koios-artifacts/tree/main/files/grest>`_ - example queries and functions that the Cardano Community uses for their Guild Operators' Repository
|
* `guild-operators <https://github.com/cardano-community/koios-artifacts/tree/main/files/grest>`_ - example queries and functions that the Cardano Community uses for their Guild Operators' Repository
|
||||||
* `PostGUI <https://github.com/priyank-purohit/PostGUI>`_ - React Material UI admin panel
|
* `PostGUI <https://github.com/priyank-purohit/PostGUI>`_ - React Material UI admin panel
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ Another option is to define the function with the :code:`SECURITY DEFINER` optio
|
|||||||
|
|
||||||
.. code-block:: postgres
|
.. code-block:: postgres
|
||||||
|
|
||||||
-- login as a user which has privileges on the private schemas
|
-- login as a user wich has privileges on the private schemas
|
||||||
|
|
||||||
-- create a sample function
|
-- create a sample function
|
||||||
create or replace function login(email text, pass text, out token text) as $$
|
create or replace function login(email text, pass text, out token text) as $$
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Supported PostgreSQL versions
|
|||||||
=============================
|
=============================
|
||||||
|
|
||||||
=============== =================================
|
=============== =================================
|
||||||
**Supported** PostgreSQL >= 14
|
**Supported** PostgreSQL >= 13
|
||||||
=============== =================================
|
=============== =================================
|
||||||
|
|
||||||
PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_.
|
PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_.
|
||||||
@@ -181,23 +181,6 @@ If you want to have a visual overview of your API in your browser you can add sw
|
|||||||
|
|
||||||
With this you can see the swagger-ui in your browser on port 8080.
|
With this you can see the swagger-ui in your browser on port 8080.
|
||||||
|
|
||||||
.. _docker_cpu_contraint:
|
|
||||||
|
|
||||||
Docker Resource Constraints
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
PostgREST does not support ``--cpus`` `constraint option <https://docs.docker.com/engine/containers/resource_constraints/#configure-the-default-cfs-scheduler>`_.
|
|
||||||
|
|
||||||
As a workaround, you may use the `GHC RTS <https://ghc.gitlab.haskell.org/ghc/doc/users_guide/runtime_control.html#runtime-system-rts-options>`_ ``-N`` option. For instance, to limit it to 2 CPU cores, do:
|
|
||||||
|
|
||||||
.. code::
|
|
||||||
|
|
||||||
# Set environment variable GHCRTS set to "-N2"
|
|
||||||
docker run --rm -p 3000:3000 \
|
|
||||||
-e PGRST_DB_URI="postgres://app_user:password@10.0.0.10/postgres" \
|
|
||||||
-e GHCRTS="-N2"
|
|
||||||
postgrest/postgrest
|
|
||||||
|
|
||||||
.. _build_source:
|
.. _build_source:
|
||||||
|
|
||||||
Building from Source
|
Building from Source
|
||||||
|
|||||||
@@ -318,144 +318,6 @@ You can insert a new product using a JSON object for the ``extra_info`` column:
|
|||||||
|
|
||||||
To query and filter the data see :ref:`json_columns` for a complete reference.
|
To query and filter the data see :ref:`json_columns` for a complete reference.
|
||||||
|
|
||||||
.. _ww_postgis:
|
|
||||||
|
|
||||||
PostGIS
|
|
||||||
-------
|
|
||||||
|
|
||||||
You can use the string representation for `PostGIS <https://postgis.net/>`_ data types such as ``geometry`` or ``geography`` (you need to `install PostGIS <https://postgis.net/documentation/getting_started/>`_ first).
|
|
||||||
|
|
||||||
.. code-block:: postgres
|
|
||||||
|
|
||||||
-- Activate the postgis module in the current database
|
|
||||||
create extension if not exists postgis;
|
|
||||||
|
|
||||||
create table coverage (
|
|
||||||
id int primary key,
|
|
||||||
name text unique,
|
|
||||||
area geometry
|
|
||||||
);
|
|
||||||
|
|
||||||
To add areas in polygon format, you can use string representation:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl "http://localhost:3000/coverage" \
|
|
||||||
-X POST -H "Content-Type: application/json" \
|
|
||||||
-d @- << EOF
|
|
||||||
[
|
|
||||||
{ "id": 1, "name": "small", "area": "SRID=4326;POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))" },
|
|
||||||
{ "id": 2, "name": "big", "area": "SRID=4326;POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))" }
|
|
||||||
]
|
|
||||||
EOF
|
|
||||||
|
|
||||||
Now, when you request the information, PostgREST will automatically cast the ``area`` column into a ``Polygon`` geometry type. Although this is useful, you may need the whole output to be in `GeoJSON <https://geojson.org/>`_ format out of the box, which can be done by including the ``Accept: application/geo+json`` in the request. This will work for PostGIS versions 3.0.0 and up and will return the output as a `FeatureCollection Object <https://www.rfc-editor.org/rfc/rfc7946#section-3.3>`_:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl "http://localhost:3000/coverage" \
|
|
||||||
-H "Accept: application/geo+json"
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "FeatureCollection",
|
|
||||||
"features": [
|
|
||||||
{
|
|
||||||
"type": "Feature",
|
|
||||||
"geometry": {
|
|
||||||
"type": "Polygon",
|
|
||||||
"coordinates": [
|
|
||||||
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "small"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "Feature",
|
|
||||||
"geometry": {
|
|
||||||
"type": "Polygon",
|
|
||||||
"coordinates": [
|
|
||||||
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"id": 2,
|
|
||||||
"name": "big"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
If you need to add an extra property, like the area in square units by using ``st_area(area)``, you could add a generated column to the table and it will appear in the ``properties`` key of each ``Feature``.
|
|
||||||
|
|
||||||
.. code-block:: postgres
|
|
||||||
|
|
||||||
alter table coverage
|
|
||||||
add square_units double precision generated always as ( st_area(area) ) stored;
|
|
||||||
|
|
||||||
In the case that you are using older PostGIS versions, then creating a function is your best option:
|
|
||||||
|
|
||||||
.. code-block:: postgres
|
|
||||||
|
|
||||||
create or replace function coverage_geo_collection() returns json as $$
|
|
||||||
select
|
|
||||||
json_build_object(
|
|
||||||
'type', 'FeatureCollection',
|
|
||||||
'features', json_agg(
|
|
||||||
json_build_object(
|
|
||||||
'type', 'Feature',
|
|
||||||
'geometry', st_AsGeoJSON(c.area)::json,
|
|
||||||
'properties', json_build_object('id', c.id, 'name', c.name)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
from coverage c;
|
|
||||||
$$ language sql;
|
|
||||||
|
|
||||||
Now this query will return the same results:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl "http://localhost:3000/rpc/coverage_geo_collection"
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "FeatureCollection",
|
|
||||||
"features": [
|
|
||||||
{
|
|
||||||
"type": "Feature",
|
|
||||||
"geometry": {
|
|
||||||
"type": "Polygon",
|
|
||||||
"coordinates": [
|
|
||||||
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"id": 1,
|
|
||||||
"name": "small"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "Feature",
|
|
||||||
"geometry": {
|
|
||||||
"type": "Polygon",
|
|
||||||
"coordinates": [
|
|
||||||
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"properties": {
|
|
||||||
"id": 2,
|
|
||||||
"name": "big"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
Ranges
|
Ranges
|
||||||
------
|
------
|
||||||
|
|
||||||
@@ -609,3 +471,20 @@ You can use other comparative filters and also all the `PostgreSQL special date/
|
|||||||
"due_date": "2022-02-27T06:00:00-05:00"
|
"due_date": "2022-02-27T06:00:00-05:00"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
.. raw:: html
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
let hash = window.location.hash;
|
||||||
|
|
||||||
|
const redirects = {
|
||||||
|
// PostGIS
|
||||||
|
'#postgis': '../integrations/postgis.html#postgis',
|
||||||
|
};
|
||||||
|
|
||||||
|
let willRedirectTo = redirects[hash];
|
||||||
|
|
||||||
|
if (willRedirectTo) {
|
||||||
|
window.location.href = willRedirectTo;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
.. _ww_postgis:
|
||||||
|
|
||||||
|
PostGIS
|
||||||
|
=======
|
||||||
|
|
||||||
|
To work with `PostGIS <https://postgis.net/>`_ data types such as ``geometry`` or ``geography``, you'll need to `install PostGIS <https://postgis.net/documentation/getting_started/>`_ first.
|
||||||
|
|
||||||
|
.. code-block:: postgres
|
||||||
|
|
||||||
|
-- Activate the postgis module in the current database
|
||||||
|
create extension if not exists postgis;
|
||||||
|
|
||||||
|
create table coverage (
|
||||||
|
id int primary key,
|
||||||
|
name text unique,
|
||||||
|
area geometry
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into coverage (id, name, area) values
|
||||||
|
(1, 'small', ST_GeomFromText('POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))',4326)),
|
||||||
|
(2, 'big', ST_GeomFromText('POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))', 4326);
|
||||||
|
|
||||||
|
.. _application/geo+json:
|
||||||
|
|
||||||
|
``application/geo+json``
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
PostgREST supports the `standard <https://www.iana.org/assignments/media-types/application/geo+json>`_ ``application/geo+json`` media type which can be used to get the output in `GeoJSON <https://geojson.org/>`_ format. This will work for PostGIS versions 3.0.0 and up and will return the output as a `FeatureCollection Object <https://www.rfc-editor.org/rfc/rfc7946#section-3.3>`_:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl "http://localhost:3000/coverage" \
|
||||||
|
-H "Accept: application/geo+json"
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [
|
||||||
|
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "small"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [
|
||||||
|
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": 2,
|
||||||
|
"name": "big"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Using generated columns
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
If you need to add an extra property, like the area in square units by using ``st_area(area)``, you could add a generated column to the table and it will appear in the ``properties`` key of each ``Feature``.
|
||||||
|
|
||||||
|
.. code-block:: postgres
|
||||||
|
|
||||||
|
alter table coverage
|
||||||
|
add square_units double precision generated always as ( st_area(area) ) stored;
|
||||||
|
|
||||||
|
In the case that you are using older PostGIS versions, then creating a function is your best option:
|
||||||
|
|
||||||
|
.. code-block:: postgres
|
||||||
|
|
||||||
|
create or replace function coverage_geo_collection() returns json as $$
|
||||||
|
select
|
||||||
|
json_build_object(
|
||||||
|
'type', 'FeatureCollection',
|
||||||
|
'features', json_agg(
|
||||||
|
json_build_object(
|
||||||
|
'type', 'Feature',
|
||||||
|
'geometry', st_AsGeoJSON(c.area)::json,
|
||||||
|
'properties', json_build_object('id', c.id, 'name', c.name)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
from coverage c;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
Now this query will return the same results:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl "http://localhost:3000/rpc/coverage_geo_collection"
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
|
{
|
||||||
|
"type": "FeatureCollection",
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [
|
||||||
|
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": 1,
|
||||||
|
"name": "small"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [
|
||||||
|
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"id": 2,
|
||||||
|
"name": "big"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
Using string representation
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
To insert areas in polygon format, you can use string representation:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl "http://localhost:3000/coverage" \
|
||||||
|
-X POST -H "Content-Type: application/json" \
|
||||||
|
-d @- << EOF
|
||||||
|
[
|
||||||
|
{ "id": 3, "name": "strip", "area": "SRID=4326;POLYGON((0 0, 50 0, 50 2, 0 2, 0 0))" },
|
||||||
|
{ "id": 4, "name": "diamond", "area": "SRID=4326;POLYGON((5 0, 10 5, 5 10, 0 5, 5 0))" }
|
||||||
|
]
|
||||||
|
EOF
|
||||||
|
|
||||||
|
PostgREST will automatically cast the ``area`` column into a ``Polygon`` geometry type.
|
||||||
@@ -4,7 +4,6 @@ API's
|
|||||||
APIs
|
APIs
|
||||||
APISIX
|
APISIX
|
||||||
AST
|
AST
|
||||||
async
|
|
||||||
aud
|
aud
|
||||||
Auth
|
Auth
|
||||||
auth
|
auth
|
||||||
@@ -15,7 +14,6 @@ BOM
|
|||||||
Bytea
|
Bytea
|
||||||
Cardano
|
Cardano
|
||||||
cd
|
cd
|
||||||
CDNs
|
|
||||||
centric
|
centric
|
||||||
CLI
|
CLI
|
||||||
CMS
|
CMS
|
||||||
@@ -32,7 +30,6 @@ DDL
|
|||||||
DOM
|
DOM
|
||||||
DSL
|
DSL
|
||||||
DevOps
|
DevOps
|
||||||
Dramatiq
|
|
||||||
dockerize
|
dockerize
|
||||||
enum
|
enum
|
||||||
Enums
|
Enums
|
||||||
@@ -44,7 +41,6 @@ EveryLayout
|
|||||||
filename
|
filename
|
||||||
FreeBSD
|
FreeBSD
|
||||||
fts
|
fts
|
||||||
fullstack
|
|
||||||
GeoJSON
|
GeoJSON
|
||||||
Github
|
Github
|
||||||
Google
|
Google
|
||||||
@@ -192,7 +188,6 @@ verifier
|
|||||||
versioning
|
versioning
|
||||||
Vondra
|
Vondra
|
||||||
Vue
|
Vue
|
||||||
webapp
|
|
||||||
webhooks
|
webhooks
|
||||||
websearch
|
websearch
|
||||||
Websockets
|
Websockets
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ PostgREST exposes three database objects of a schema as resources: tables, views
|
|||||||
api/aggregate_functions.rst
|
api/aggregate_functions.rst
|
||||||
api/openapi.rst
|
api/openapi.rst
|
||||||
api/preferences.rst
|
api/preferences.rst
|
||||||
api/vary_header.rst
|
|
||||||
api/*
|
api/*
|
||||||
|
|
||||||
.. raw:: html
|
.. raw:: html
|
||||||
|
|||||||
@@ -69,26 +69,6 @@ If the function doesn't modify the database, it will also run under the GET meth
|
|||||||
|
|
||||||
The function parameter names match the JSON object keys in the POST case, for the GET case they match the query parameters ``?a=1&b=2``.
|
The function parameter names match the JSON object keys in the POST case, for the GET case they match the query parameters ``?a=1&b=2``.
|
||||||
|
|
||||||
If the function is defined to have default values for the parameters then arguments for these parameters can be omitted in the request. For instance:
|
|
||||||
|
|
||||||
.. code-block:: postgres
|
|
||||||
|
|
||||||
CREATE FUNCTION greet_user(username TEXT DEFAULT 'guest')
|
|
||||||
RETURNS TEXT AS $$
|
|
||||||
SELECT 'Hello ' || username || '!';
|
|
||||||
$$ LANGUAGE SQL IMMUTABLE;
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl -i "http://localhost:3000/rpc/greet_user"
|
|
||||||
|
|
||||||
.. code-block:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Context-Type: application/json; charset=utf-8
|
|
||||||
|
|
||||||
"Hello guest!"
|
|
||||||
|
|
||||||
.. _function_single_json:
|
.. _function_single_json:
|
||||||
|
|
||||||
Functions with an array of JSON objects
|
Functions with an array of JSON objects
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ Using these domains, :ref:`functions <functions>` can become handlers and `user-
|
|||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
|
|
||||||
- PostgREST vendor media types (``application/vnd.pgrst.plan``, ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``) cannot be overridden.
|
- PostgREST vendor media types (``application/vnd.pgrst.plan``, ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``) cannot be overriden.
|
||||||
- Long media types like ``application/vnd.openxmlformats-officedocument.wordprocessingml.document`` cannot be expressed as domains since they surpass `PostgreSQL identifier length <https://www.postgresql.org/docs/current/limits.html#LIMITS-TABLE>`_.
|
- Long media types like ``application/vnd.openxmlformats-officedocument.wordprocessingml.document`` cannot be expressed as domains since they surpass `PostgreSQL identifier length <https://www.postgresql.org/docs/current/limits.html#LIMITS-TABLE>`_.
|
||||||
For these you can use the :ref:`any_handler`.
|
For these you can use the :ref:`any_handler`.
|
||||||
|
|
||||||
|
|||||||
@@ -117,10 +117,6 @@ However, with ``handling=strict``, an invalid time zone preference will throw an
|
|||||||
|
|
||||||
HTTP/1.1 400 Bad Request
|
HTTP/1.1 400 Bad Request
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This feature requires querying `pg_timezone_names <https://www.postgresql.org/docs/current/view-pg-timezone-names.html>`_ during :ref:`schema_cache` load. If this is not desired, you can disable the feature with :ref:`db-timezone-enabled`.
|
|
||||||
|
|
||||||
.. _prefer_return:
|
.. _prefer_return:
|
||||||
|
|
||||||
Return Representation
|
Return Representation
|
||||||
|
|||||||
@@ -1244,7 +1244,7 @@ You can order the correlated arrays explicitly. For example, to order by the fil
|
|||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
Aliasing spread columns is recommended since JSON allows duplicate keys. Example:
|
Aliasing spreaded columns is recommended since JSON allows duplicate keys. Example:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ Builtin handlers are offered for common standard media types.
|
|||||||
|
|
||||||
* ``text/csv`` and ``application/json``, for all API endpoints. See :ref:`tables_views` and :ref:`functions`.
|
* ``text/csv`` and ``application/json``, for all API endpoints. See :ref:`tables_views` and :ref:`functions`.
|
||||||
* ``application/openapi+json``, for the root endpoint. See :ref:`open-api`.
|
* ``application/openapi+json``, for the root endpoint. See :ref:`open-api`.
|
||||||
* ``application/geo+json``, see :ref:`ww_postgis`.
|
* ``application/geo+json``, see :ref:`application/geo+json`.
|
||||||
* ``*/*``, resolves to ``application/json`` for API endpoints and to ``application/openapi+json`` for the root endpoint.
|
* ``*/*``, 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.
|
The following vendor media types handlers are also supported.
|
||||||
|
|||||||
@@ -5,10 +5,6 @@ Schemas
|
|||||||
|
|
||||||
PostgREST can expose a single or multiple schema's tables, views and functions. The :ref:`active database role <roles>` must have the usage privilege on the schemas to access them.
|
PostgREST can expose a single or multiple schema's tables, views and functions. The :ref:`active database role <roles>` must have the usage privilege on the schemas to access them.
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
``pg_catalog`` and ``information_schema`` are not allowed in :ref:`db-schemas`. This is done to prevent leaking sensitive information and hence they cannot be accessed directly. If you wish to expose objects of these schemas, expose another schema that contains wrapper views or functions over ``pg_catalog`` or ``information_schema`` objects.
|
|
||||||
|
|
||||||
Single schema
|
Single schema
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
|||||||
@@ -639,7 +639,7 @@ However, it can work with surrogate primary keys (e.g. ``id serial primary key``
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
curl "http://localhost:3000/employees?columns=id,name,salary" \
|
curl "http://localhost:3000/employees?colums=id,name,salary" \
|
||||||
-X POST -H "Content-Type: application/json" \
|
-X POST -H "Content-Type: application/json" \
|
||||||
-H "Prefer: resolution=merge-duplicates, missing=default" \
|
-H "Prefer: resolution=merge-duplicates, missing=default" \
|
||||||
-d @- << EOF
|
-d @- << EOF
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ You can request table/columns with spaces in them by percent encoding the spaces
|
|||||||
Reserved characters
|
Reserved characters
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
If filters include PostgREST reserved characters(``,``, ``.``, ``:``, ``*``, ``(``, ``)``) you'll have to surround them in percent encoded double quotes ``%22`` for correct processing.
|
If filters include PostgREST reserved characters(``,``, ``.``, ``:``, ``()``) you'll have to surround them in percent encoded double quotes ``%22`` for correct processing.
|
||||||
|
|
||||||
Here ``Hebdon,John`` and ``Williams,Mary`` are values.
|
Here ``Hebdon,John`` and ``Williams,Mary`` are values.
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
.. _vary_header:
|
|
||||||
|
|
||||||
Vary Header
|
|
||||||
===========
|
|
||||||
|
|
||||||
In order to assist caching proxies and CDNs, PostgREST includes a ``Vary`` header of value
|
|
||||||
``Accept, Prefer, Range`` in its responses which should fit most of the bills. As any other
|
|
||||||
response header, it's available for override
|
|
||||||
by updating ``response.headers`` GUC variable accordingly, for example:
|
|
||||||
|
|
||||||
.. code-block:: postgres
|
|
||||||
|
|
||||||
-- Override the Vary header to include Accept, Prefer and X-Test-Vary headers
|
|
||||||
perform set_config('response.headers', '[{"Vary": "Accept, Prefer, X-Test-Vary"}]', true);
|
|
||||||
|
|
||||||
In this case PostgREST will use provided value verbatim.
|
|
||||||
@@ -217,7 +217,7 @@ It's recommended to leave the JWT cache enabled as our load tests indicate ~20%
|
|||||||
|
|
||||||
- If the ``jwt-secret`` is changed and the config is reloaded, the JWT cache will reset.
|
- If the ``jwt-secret`` is changed and the config is reloaded, the JWT cache will reset.
|
||||||
- JWTs that pass :ref:`jwt_signature` are cached, regardless if they pass :ref:`jwt_claims_validation`. We do this to ensure responses stays fast under common failure cases (such as expired JWTs).
|
- JWTs that pass :ref:`jwt_signature` are cached, regardless if they pass :ref:`jwt_claims_validation`. We do this to ensure responses stays fast under common failure cases (such as expired JWTs).
|
||||||
- You can use the :ref:`server-timing_header` to see the performance benefit of JWT caching.
|
- You can use the :ref:`server-timing_header` to see the peformance benefit of JWT caching.
|
||||||
|
|
||||||
.. _jwt_role_extract:
|
.. _jwt_role_extract:
|
||||||
|
|
||||||
@@ -234,17 +234,6 @@ The DSL follows the `JSONPath <https://goessner.net/articles/JsonPath/>`_ expres
|
|||||||
- ``==^`` selects the first array element that ends with the right operand
|
- ``==^`` selects the first array element that ends with the right operand
|
||||||
- ``*==`` selects the first array element that contains the right operand
|
- ``*==`` selects the first array element that contains the right operand
|
||||||
|
|
||||||
The selected role value can also be sliced using the slice operator ``[a:b]``. It is similar to `slice operator in python <https://docs.python.org/3/library/functions.html#slice>`_. Negative index values are also supported. The syntax is as:
|
|
||||||
|
|
||||||
- ``[a:b]`` take slice from index ``a`` up to ``b``
|
|
||||||
- ``[a:]`` take slice from index ``a`` to end
|
|
||||||
- ``[:b]`` take slice from start to index ``b``
|
|
||||||
- ``[:]`` select everything, no slicing
|
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
Make sure that you are not taking a slice where the start index comes after the end index like ``[11:2]``. The result of this would be empty string and so no role would get selected.
|
|
||||||
|
|
||||||
Usage examples:
|
Usage examples:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
@@ -266,11 +255,6 @@ Usage examples:
|
|||||||
jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]"
|
jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]"
|
||||||
jwt-role-claim-key = ".postgrest.roles[?(@ *== \"utho\")]"
|
jwt-role-claim-key = ".postgrest.roles[?(@ *== \"utho\")]"
|
||||||
|
|
||||||
# {"postgrest":{"wlcg": ["/groupa", "/groupb/"]}}
|
|
||||||
# skip the "/" character using slice operator
|
|
||||||
jwt-role-claim-key = ".postgrest.wlcg[0][1:]"
|
|
||||||
jwt-role-claim-key = ".postgrest.wlcg[1][1:-1]"
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
The string comparison operators are implemented as a custom extension to the JSPath and does not strictly follow the `RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_.
|
The string comparison operators are implemented as a custom extension to the JSPath and does not strictly follow the `RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_.
|
||||||
|
|||||||
@@ -195,33 +195,6 @@ app.settings.*
|
|||||||
|
|
||||||
The :code:`current_setting` function has `an optional boolean second <https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET>`_ argument to avoid it from raising an error if the value was not defined. Default values to :code:`app.settings` can then be given by combining this argument with :code:`coalesce` and :code:`nullif` : :code:`coalesce(nullif(current_setting('app.settings.my_custom_variable', true), ''), 'default value')`. The use of :code:`nullif` is necessary because if set in a transaction, the setting is sometimes not "rolled back" to :code:`null`. See also :ref:`this section <guc_req_headers_cookies_claims>` for more information on this behaviour.
|
The :code:`current_setting` function has `an optional boolean second <https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET>`_ argument to avoid it from raising an error if the value was not defined. Default values to :code:`app.settings` can then be given by combining this argument with :code:`coalesce` and :code:`nullif` : :code:`coalesce(nullif(current_setting('app.settings.my_custom_variable', true), ''), 'default value')`. The use of :code:`nullif` is necessary because if set in a transaction, the setting is sometimes not "rolled back" to :code:`null`. See also :ref:`this section <guc_req_headers_cookies_claims>` for more information on this behaviour.
|
||||||
|
|
||||||
.. _client-error-verbosity:
|
|
||||||
|
|
||||||
client-error-verbosity
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
=============== =======================
|
|
||||||
**Type** String
|
|
||||||
**Default** verbose
|
|
||||||
**Reloadable** Y
|
|
||||||
**Environment** PGRST_CLIENT_ERROR_VERBOSITY
|
|
||||||
**In-Database** pgrst.client_error_verbosity
|
|
||||||
=============== =======================
|
|
||||||
|
|
||||||
Specifies the verbosity of PostgREST errors. See :ref:`client_error_verbosity`.
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
# Return error "code", "message", "details" and "hint"
|
|
||||||
client-error-verbosity = "verbose"
|
|
||||||
|
|
||||||
# Return only "code" and "message"
|
|
||||||
client-error-verbosity = "minimal"
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
This setting only affects client side error messages. Server side logs are not affected by this setting.
|
|
||||||
|
|
||||||
.. _db-aggregates-enabled:
|
.. _db-aggregates-enabled:
|
||||||
|
|
||||||
db-aggregates-enabled
|
db-aggregates-enabled
|
||||||
@@ -291,7 +264,7 @@ db-channel-enabled
|
|||||||
|
|
||||||
When this is set to :code:`true`, the notification channel specified in :ref:`db-channel` is enabled.
|
When this is set to :code:`true`, the notification channel specified in :ref:`db-channel` is enabled.
|
||||||
|
|
||||||
You should set this to ``false`` when using PostgreSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
You should set this to ``false`` when using PostgresSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
||||||
|
|
||||||
.. _db-config:
|
.. _db-config:
|
||||||
|
|
||||||
@@ -506,7 +479,7 @@ db-prepared-statements
|
|||||||
|
|
||||||
When disabled, the generated queries will be parameterized (invulnerable to SQL injection) but they will not be prepared (cached in the database session). Not using prepared statements will noticeably decrease performance, so it's recommended to always have this setting enabled.
|
When disabled, the generated queries will be parameterized (invulnerable to SQL injection) but they will not be prepared (cached in the database session). Not using prepared statements will noticeably decrease performance, so it's recommended to always have this setting enabled.
|
||||||
|
|
||||||
You should only set this to ``false`` when using PostgreSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
You should only set this to ``false`` when using PostgresSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
||||||
|
|
||||||
.. _db-root-spec:
|
.. _db-root-spec:
|
||||||
|
|
||||||
@@ -540,21 +513,6 @@ db-schemas
|
|||||||
|
|
||||||
The list of database schemas to expose to clients. See :ref:`schemas`.
|
The list of database schemas to expose to clients. See :ref:`schemas`.
|
||||||
|
|
||||||
.. _db-timezone-enabled:
|
|
||||||
|
|
||||||
db-timezone-enabled
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
=============== =================================
|
|
||||||
**Type** Boolean
|
|
||||||
**Default** True
|
|
||||||
**Reloadable** Y
|
|
||||||
**Environment** PGRST_DB_TIMEZONE_ENABLED
|
|
||||||
**In-Database** pgrst.db_timezone_enabled
|
|
||||||
=============== =================================
|
|
||||||
|
|
||||||
Enables the use of :ref:`prefer_timezone` preference header. Disabled when set to ``false``.
|
|
||||||
|
|
||||||
.. _db-tx-end:
|
.. _db-tx-end:
|
||||||
|
|
||||||
db-tx-end
|
db-tx-end
|
||||||
@@ -568,7 +526,7 @@ db-tx-end
|
|||||||
**In-Database** pgrst.db_tx_end
|
**In-Database** pgrst.db_tx_end
|
||||||
=============== =================================
|
=============== =================================
|
||||||
|
|
||||||
Specifies how to terminate the database transactions. See :ref:`prefer_tx`.
|
Specifies how to terminate the database transactions.
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
|
|||||||
@@ -473,38 +473,3 @@ For example, doing a request on a table with high count (say 30_000_000), we get
|
|||||||
Proxy-Status: PostgREST; error=57014
|
Proxy-Status: PostgREST; error=57014
|
||||||
|
|
||||||
The PostgreSQL error code ``57014`` (`ref <https://www.postgresql.org/docs/current/errcodes-appendix.html>`_) reveals that the error is due to a short ``statement_timeout`` value.
|
The PostgreSQL error code ``57014`` (`ref <https://www.postgresql.org/docs/current/errcodes-appendix.html>`_) reveals that the error is due to a short ``statement_timeout`` value.
|
||||||
|
|
||||||
.. _client_error_verbosity:
|
|
||||||
|
|
||||||
Client Error Verbosity
|
|
||||||
======================
|
|
||||||
|
|
||||||
For HTTP clients, the error verbosity can be set via :ref:`client-error-verbosity` config.
|
|
||||||
|
|
||||||
With ``verbose``, it returns ``code``, ``message``, ``details`` and ``hint``.
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
curl "localhost:3000/itemsxx"
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"code": "PGRST205",
|
|
||||||
"message": "Could not find the table 'public.itemsxx' in the schema cache",
|
|
||||||
"details": "Perhaps you meant the table 'public.items'",
|
|
||||||
"hint": null
|
|
||||||
}
|
|
||||||
|
|
||||||
With ``minimal``, just ``code`` and ``message`` is returned.
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
curl "localhost:3000/itemsxx"
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
{
|
|
||||||
"code": "PGRST205",
|
|
||||||
"message": "Could not find the table 'public.itemsxx' in the schema cache"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ For diagnostic information about the server itself, PostgREST logs to ``stderr``
|
|||||||
06/May/2024:08:16:11 -0500: Listening for database notifications on the "pgrst" channel
|
06/May/2024:08:16:11 -0500: Listening for database notifications on the "pgrst" channel
|
||||||
06/May/2024:08:16:11 -0500: Config reloaded
|
06/May/2024:08:16:11 -0500: Config reloaded
|
||||||
06/May/2024:08:16:11 -0500: Schema cache queried in 3.8 milliseconds
|
06/May/2024:08:16:11 -0500: Schema cache queried in 3.8 milliseconds
|
||||||
06/May/2024:08:16:11 -0500: Schema cache loaded 15 Relations, 8 Relationships, 8 Functions, 0 Domain Representations, 4 Media Type Handlers
|
06/May/2024:08:16:11 -0500: Schema cache loaded 15 Relations, 8 Relationships, 8 RPCs, 0 Domain Representations, 4 Media Type Handlers
|
||||||
06/May/2024:14:11:27 -0500: Received a config reload message on the "pgrst" channel
|
06/May/2024:14:11:27 -0500: Received a config reload message on the "pgrst" channel
|
||||||
06/May/2024:14:11:27 -0500: Config reloaded
|
06/May/2024:14:11:27 -0500: Config reloaded
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,10 @@
|
|||||||
Schema Cache
|
Schema Cache
|
||||||
============
|
============
|
||||||
|
|
||||||
PostgREST requires metadata from the database to provide a REST API that abstracts SQL details. One example of this is the interface for :ref:`resource_embedding`.
|
PostgREST requires metadata from the database schema to provide a REST API that abstracts SQL details. One example of this is the interface for :ref:`resource_embedding`.
|
||||||
|
|
||||||
Getting this metadata requires expensive queries. To avoid repeating this work, PostgREST uses a schema cache.
|
Getting this metadata requires expensive queries. To avoid repeating this work, PostgREST uses a schema cache.
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
- Schema cache queries have been optimized over time to stay fast, even on complex databases. You can see a summary of their execution time in :ref:`pgrst_logging` and :ref:`metrics`.
|
|
||||||
- If the schema cache queries are slow, the most likely cause is *system catalog bloat*, see `issue#3212 <https://github.com/PostgREST/postgrest/issues/3212>`_ for more details.
|
|
||||||
- You can turn the :ref:`log-level` to ``debug`` to see the time of each schema cache query.
|
|
||||||
|
|
||||||
.. _schema_reloading:
|
.. _schema_reloading:
|
||||||
|
|
||||||
Schema Cache Reloading
|
Schema Cache Reloading
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ Notice that the ``response.headers`` should be set to an *array* of single-key o
|
|||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overridden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you use :ref:`custom_media`.
|
PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overriden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you use :ref:`custom_media`.
|
||||||
|
|
||||||
.. _guc_resp_status:
|
.. _guc_resp_status:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# This file is auto-generated by postgrest-nixpkgs-upgrade
|
# This file is auto-generated by postgrest-nixpkgs-upgrade
|
||||||
sphinx==9.1.0
|
sphinx==8.2.3
|
||||||
sphinx-copybutton==0.5.2
|
sphinx-copybutton==0.5.2
|
||||||
sphinx-rtd-dark-mode==1.3.0
|
sphinx-rtd-dark-mode==1.3.0
|
||||||
sphinx-rtd-theme==3.1.0
|
sphinx-rtd-theme==3.0.2
|
||||||
sphinx-tabs==3.5.0
|
sphinx-tabs==3.4.7
|
||||||
sphinxext-opengraph==0.13.0
|
sphinxext-opengraph==0.9.1
|
||||||
@@ -22,7 +22,7 @@ Step 1. Install PostgreSQL
|
|||||||
|
|
||||||
If you're already familiar with using PostgreSQL and have it installed on your system you can use the existing installation (see :ref:`pg-dependency` for minimum requirements). For this tutorial we'll describe how to use the database in Docker because database configuration is otherwise too complicated for a simple tutorial.
|
If you're already familiar with using PostgreSQL and have it installed on your system you can use the existing installation (see :ref:`pg-dependency` for minimum requirements). For this tutorial we'll describe how to use the database in Docker because database configuration is otherwise too complicated for a simple tutorial.
|
||||||
|
|
||||||
If Docker is not installed, you can get it `here <https://www.docker.com/get-started>`_. Make sure that Docker service is `started <https://docs.docker.com/engine/daemon/start/#start-the-daemon-using-operating-system-utilities>`_. Next, let's pull and start the database image:
|
If Docker is not installed, you can get it `here <https://www.docker.com/get-started>`_. Next, let's pull and start the database image:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
|
|||||||
Generated
+4
-4
@@ -2,16 +2,16 @@
|
|||||||
"nodes": {
|
"nodes": {
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1776949667,
|
"lastModified": 1752006229,
|
||||||
"narHash": "sha256-GMSVw35Q+294GlrTUKlx087E31z7KurReQ1YHSKp5iw=",
|
"narHash": "sha256-BeuAPwNM2RBc5bvUTb0j4GRs2yBkDeRCw/8Y3v9Xesc=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "01fbdeef22b76df85ea168fbfe1bfd9e63681b30",
|
"rev": "c80edd02003fe3d8af527215a3ac069be9cfd47f",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"ref": "nixpkgs-unstable",
|
"ref": "nixpkgs-25.05-darwin",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
description = "REST API for any Postgres database";
|
description = "REST API for any Postgres database";
|
||||||
|
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-25.05-darwin";
|
||||||
};
|
};
|
||||||
|
|
||||||
nixConfig = {
|
nixConfig = {
|
||||||
@@ -46,9 +46,5 @@
|
|||||||
meta.description = "REST API for any Postgres database";
|
meta.description = "REST API for any Postgres database";
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
devShells = genSystems (postgrest: {
|
|
||||||
default = import ./shell.nix { inherit postgrest; };
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -91,11 +91,11 @@ postgrest-gen-ctags postgrest-watch
|
|||||||
postgrest-gen-jwt postgrest-with-all
|
postgrest-gen-jwt postgrest-with-all
|
||||||
postgrest-gen-secret postgrest-with-git
|
postgrest-gen-secret postgrest-with-git
|
||||||
postgrest-git-hooks postgrest-with-pgrst
|
postgrest-git-hooks postgrest-with-pgrst
|
||||||
postgrest-hsie-graph-modules postgrest-with-pg-14
|
postgrest-hsie-graph-modules postgrest-with-pg-13
|
||||||
postgrest-hsie-graph-symbols postgrest-with-pg-15
|
postgrest-hsie-graph-symbols postgrest-with-pg-14
|
||||||
postgrest-hsie-minimal-imports postgrest-with-pg-16
|
postgrest-hsie-minimal-imports postgrest-with-pg-15
|
||||||
postgrest-lint postgrest-with-pg-17
|
postgrest-lint postgrest-with-pg-16
|
||||||
postgrest-loadtest postgrest-with-pg-18
|
postgrest-loadtest postgrest-with-pg-17
|
||||||
postgrest-loadtest-against postgrest-with-slow-pg
|
postgrest-loadtest-against postgrest-with-slow-pg
|
||||||
postgrest-loadtest-report postgrest-with-slow-postgrest
|
postgrest-loadtest-report postgrest-with-slow-postgrest
|
||||||
postgrest-nixpkgs-upgrade
|
postgrest-nixpkgs-upgrade
|
||||||
@@ -174,7 +174,7 @@ $ nix-shell --run "postgrest-with-all postgrest-test-spec"
|
|||||||
|
|
||||||
# Run the tests against a specific version of PostgreSQL (use tab-completion in
|
# Run the tests against a specific version of PostgreSQL (use tab-completion in
|
||||||
# nix-shell to see all available versions):
|
# nix-shell to see all available versions):
|
||||||
$ nix-shell --run "postgrest-with-pg-17 postgrest-test-spec"
|
$ nix-shell --run "postgrest-with-pg-13 postgrest-test-spec"
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+24
-6
@@ -16,8 +16,11 @@ The following checklist guides you through the complete process in more detail.
|
|||||||
## Upgrade the pinned version of `nixpkgs`
|
## Upgrade the pinned version of `nixpkgs`
|
||||||
|
|
||||||
The pinned version of [`nixpkgs`](https://github.com/NixOS/nixpkgs) is defined
|
The pinned version of [`nixpkgs`](https://github.com/NixOS/nixpkgs) is defined
|
||||||
in [`flake.nix`](../flake.nix). To upgrade it, you can use a small utility
|
in [`nix/nixpkgs-version.nix`](nixpkgs-version.nix). The pin refers directly to
|
||||||
script defined in [`nix/tools/nixpkgsTools.nix`](tools/nixpkgsTools.nix):
|
a GitHub tarball for the given revision, which is more efficient than pulling
|
||||||
|
the complete Git repository. To upgrade it to the current `main` of
|
||||||
|
`nixpkgs`, you can use a small utility script defined in
|
||||||
|
[`nix/nixpkgs-update.nix`](nixpkgs-update.nix):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# From the root of the repository, enter nix-shell
|
# From the root of the repository, enter nix-shell
|
||||||
@@ -27,12 +30,21 @@ nix-shell
|
|||||||
postgrest-nixpkgs-upgrade
|
postgrest-nixpkgs-upgrade
|
||||||
|
|
||||||
# Exit the nix-shell with Ctrl-d
|
# Exit the nix-shell with Ctrl-d
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Review overlays
|
## Review overlays
|
||||||
|
|
||||||
Check whether the individual [overlays](overlays) are still required.
|
Check whether the individual [overlays](overlays) are still required.
|
||||||
|
|
||||||
|
## Check if patches are still required and update them as needed
|
||||||
|
|
||||||
|
We track a number of PostgREST-specific patches in [`nix/patches`](patches).
|
||||||
|
Check whether the pull-requests/issues linked in the
|
||||||
|
[`default.nix`](patches/default.nix) have progressed and remove/modify the
|
||||||
|
patches if they did. If conflicting changes occurred, you might have to rebase
|
||||||
|
the respective patches.
|
||||||
|
|
||||||
## Build everything
|
## Build everything
|
||||||
|
|
||||||
Using the PostgREST binary Nix cache is recommended. Install
|
Using the PostgREST binary Nix cache is recommended. Install
|
||||||
@@ -46,19 +58,25 @@ errors, this is probably due to one of our patches. Try to fix them and re-run
|
|||||||
|
|
||||||
## Update the PostgREST binary cache
|
## Update the PostgREST binary cache
|
||||||
|
|
||||||
If you have access to the PostgREST cachix project, you can push the
|
If you have access to the PostgREST cachix signing key, you can push the
|
||||||
artifacts that you built locally to the binary cache. This will accelerate the
|
artifacts that you built locally to the binary cache. This will accelerate the
|
||||||
CI builds and tests, sometimes dramatically. This might sometimes even be
|
CI builds and tests, sometimes dramatically. This might sometimes even be
|
||||||
required to avoid build timeouts in CI.
|
required to avoid build timeouts in CI.
|
||||||
|
|
||||||
You'll need to login with your token with `cachix authtoken <token>`.
|
You'll need to set the `CACHIX_SIGNING_KEY` before proceeding, e.g. by creating
|
||||||
|
a file containing `export CACHIX_SIGNING_KEY=...` and sourcing that file, which
|
||||||
|
avoids having the secret in your shell history.
|
||||||
|
|
||||||
To push all new artifacts to Cachix, run:
|
To push all new artifacts to Cachix, run:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
nix-store -qR --include-outputs $$(nix-instantiate) | cachix push postgrest
|
||||||
|
|
||||||
|
# Or, equivalently
|
||||||
nix-shell --run postgrest-push-cachix
|
nix-shell --run postgrest-push-cachix
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `postgrest-push-cachix` command will query the nix-store to list all
|
The `nix-store` command will query the nix-store to list all dependencies and
|
||||||
dependencies and build artifacts of PostgREST. It will then push
|
build artifacts of PostgREST. The `cachix` command will efficiently push
|
||||||
everything that is not yet cached to the binary cache.
|
everything that is not yet cached to the binary cache.
|
||||||
|
|||||||
+7
-20
@@ -4,7 +4,6 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
{-# LANGUAGE TupleSections #-}
|
{-# LANGUAGE TupleSections #-}
|
||||||
{-# LANGUAGE TypeApplications #-}
|
|
||||||
{-# LANGUAGE TypeFamilies #-}
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
|
||||||
-- | Haskell Imports and Exports tool
|
-- | Haskell Imports and Exports tool
|
||||||
@@ -34,16 +33,13 @@ import Data.Function ((&))
|
|||||||
import Data.List (intercalate)
|
import Data.List (intercalate)
|
||||||
import Data.Maybe (catMaybes, mapMaybe)
|
import Data.Maybe (catMaybes, mapMaybe)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import GHC.Driver.Errors.Types (GhcMessage)
|
|
||||||
import GHC.Generics (Generic)
|
import GHC.Generics (Generic)
|
||||||
import GHC.Hs.Extension (GhcPs)
|
import GHC.Hs.Extension (GhcPs)
|
||||||
import GHC.Types.Error (Messages, defaultDiagnosticOpts,
|
import GHC.Types.Error (getMessages)
|
||||||
getMessages)
|
|
||||||
import GHC.Types.Name.Occurrence (occNameString)
|
import GHC.Types.Name.Occurrence (occNameString)
|
||||||
import GHC.Types.Name.Reader (rdrNameOcc)
|
import GHC.Types.Name.Reader (rdrNameOcc)
|
||||||
import GHC.Unit.Module (moduleNameString)
|
import GHC.Unit.Module.Name (moduleNameString)
|
||||||
import GHC.Utils.Error (pprMsgEnvelopeBagWithLoc)
|
import GHC.Utils.Error (pprMsgEnvelopeBagWithLoc)
|
||||||
import GHC.Utils.Outputable (showSDocUnsafe)
|
|
||||||
import System.Directory.Recursive (getFilesRecursive)
|
import System.Directory.Recursive (getFilesRecursive)
|
||||||
import System.Exit (exitFailure)
|
import System.Exit (exitFailure)
|
||||||
|
|
||||||
@@ -202,7 +198,7 @@ sourceSymbols source = do
|
|||||||
return $ concatMap (importSymbols source filepath . GHC.unLoc) hsmodImports
|
return $ concatMap (importSymbols source filepath . GHC.unLoc) hsmodImports
|
||||||
|
|
||||||
-- | Parse a Haskell module
|
-- | Parse a Haskell module
|
||||||
parseModule :: FilePath -> IO (GHC.HsModule GhcPs)
|
parseModule :: FilePath -> IO GHC.HsModule
|
||||||
parseModule filepath = do
|
parseModule filepath = do
|
||||||
result <- ExactPrint.parseModule GHC.Paths.libdir filepath
|
result <- ExactPrint.parseModule GHC.Paths.libdir filepath
|
||||||
case result of
|
case result of
|
||||||
@@ -210,13 +206,7 @@ parseModule filepath = do
|
|||||||
return $ GHC.unLoc hsmod
|
return $ GHC.unLoc hsmod
|
||||||
Left errs ->
|
Left errs ->
|
||||||
fail $ "Errors with " <> show filepath <> ":\n "
|
fail $ "Errors with " <> show filepath <> ":\n "
|
||||||
<> formatParseErrors errs
|
<> show (pprMsgEnvelopeBagWithLoc $ getMessages errs)
|
||||||
|
|
||||||
formatParseErrors :: Messages GhcMessage -> String
|
|
||||||
formatParseErrors errs =
|
|
||||||
intercalate "\n "
|
|
||||||
. fmap showSDocUnsafe
|
|
||||||
$ pprMsgEnvelopeBagWithLoc (defaultDiagnosticOpts @GhcMessage) (getMessages errs)
|
|
||||||
|
|
||||||
-- | Symbols imported in an import declaration.
|
-- | Symbols imported in an import declaration.
|
||||||
--
|
--
|
||||||
@@ -224,12 +214,9 @@ formatParseErrors errs =
|
|||||||
-- only one item is returned.
|
-- only one item is returned.
|
||||||
importSymbols :: FilePath -> FilePath -> GHC.ImportDecl GhcPs -> [ImportedSymbol]
|
importSymbols :: FilePath -> FilePath -> GHC.ImportDecl GhcPs -> [ImportedSymbol]
|
||||||
importSymbols source filepath GHC.ImportDecl{..} =
|
importSymbols source filepath GHC.ImportDecl{..} =
|
||||||
case ideclImportList of
|
case ideclHiding of
|
||||||
Just (importListInterpretation, syms) ->
|
Just (hiding, syms) ->
|
||||||
symbol (if importListInterpretation == GHC.EverythingBut then Hiding else Explicit)
|
symbol (if hiding then Hiding else Explicit) . Just . GHC.unLoc <$> GHC.unLoc syms
|
||||||
. Just
|
|
||||||
. GHC.unLoc
|
|
||||||
<$> GHC.unLoc syms
|
|
||||||
Nothing ->
|
Nothing ->
|
||||||
[ symbol Wildcard Nothing ]
|
[ symbol Wildcard Nothing ]
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ let
|
|||||||
''
|
''
|
||||||
|
|
||||||
+ lib.optionalString withTmpDir ''
|
+ lib.optionalString withTmpDir ''
|
||||||
tmpdir="$(${coreutils}/bin/mktemp -d --tmpdir=/tmp ${name}-XXX)"
|
tmpdir="$(${coreutils}/bin/mktemp -d --tmpdir ${name}-XXX)"
|
||||||
|
|
||||||
# we keep the tmpdir when an error occurs for debugging
|
# we keep the tmpdir when an error occurs for debugging
|
||||||
trap 'echo Temporary directory kept at: $tmpdir' ERR
|
trap 'echo Temporary directory kept at: $tmpdir' ERR
|
||||||
|
|||||||
@@ -3,4 +3,5 @@
|
|||||||
checked-shell-script = import ./checked-shell-script;
|
checked-shell-script = import ./checked-shell-script;
|
||||||
gitignore = import ./gitignore.nix;
|
gitignore = import ./gitignore.nix;
|
||||||
haskell-packages = import ./haskell-packages.nix;
|
haskell-packages = import ./haskell-packages.nix;
|
||||||
|
slocat = import ./slocat.nix;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,43 +47,37 @@ let
|
|||||||
# - To modify and try packages locally, see "Working with locally modified Haskell packages" in the Nix README.
|
# - To modify and try packages locally, see "Working with locally modified Haskell packages" in the Nix README.
|
||||||
|
|
||||||
# Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329
|
# Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329
|
||||||
|
# jailbreak, because hspec limit for tests
|
||||||
fuzzyset = prev.fuzzyset_0_2_4;
|
fuzzyset = prev.fuzzyset_0_2_4;
|
||||||
|
|
||||||
http2 =
|
# TODO: Remove once available in nixpkgs haskellPackages
|
||||||
|
configurator-pg =
|
||||||
prev.callHackageDirect
|
prev.callHackageDirect
|
||||||
{
|
{
|
||||||
pkg = "http2";
|
pkg = "configurator-pg";
|
||||||
ver = "5.4.0";
|
ver = "0.2.11";
|
||||||
sha256 = "sha256-PeEWVd61bQ8G7LvfLeXklzXqNJFaAjE2ecRMWJZESPE=";
|
sha256 = "sha256-mtGtNawDJgz2ZIEVca+IYXVu4oNw9xsfJiYWAqAbbgc=";
|
||||||
}
|
}
|
||||||
{ };
|
{ };
|
||||||
|
|
||||||
http-semantics =
|
# TODO: Remove once available in nixpkgs haskellPackages
|
||||||
|
streaming-commons =
|
||||||
prev.callHackageDirect
|
prev.callHackageDirect
|
||||||
{
|
{
|
||||||
pkg = "http-semantics";
|
pkg = "streaming-commons";
|
||||||
ver = "0.4.0";
|
ver = "0.2.3.1";
|
||||||
sha256 = "sha256-rh0z51EKvsu5rQd5n2z3fSRjjEObouNZSBPO9NFYOF0=";
|
sha256 = "sha256-Gl2eaJcWe1sxmcE/octWlH9uSnERguf+5H66K4fV87s=";
|
||||||
}
|
}
|
||||||
{ };
|
{ };
|
||||||
|
|
||||||
network-run =
|
# Downgrade hasql and related packages while we are still on GHC 9.4 for the static build.
|
||||||
prev.callHackageDirect
|
hasql = lib.dontCheck (lib.doJailbreak prev.hasql_1_6_4_4);
|
||||||
{
|
hasql-dynamic-statements = lib.dontCheck prev.hasql-dynamic-statements_0_3_1_5;
|
||||||
pkg = "network-run";
|
hasql-implicits = lib.dontCheck prev.hasql-implicits_0_1_1_3;
|
||||||
ver = "0.5.0";
|
hasql-notifications = lib.dontCheck prev.hasql-notifications_0_2_2_2;
|
||||||
sha256 = "sha256-vbXh+CzxDsGApjqHxCYf/ijpZtUCApFbkcF5gyN0THU=";
|
hasql-pool = lib.dontCheck prev.hasql-pool_1_0_1;
|
||||||
}
|
hasql-transaction = lib.dontCheck prev.hasql-transaction_1_1_0_1;
|
||||||
{ };
|
postgresql-binary = lib.dontCheck (lib.doJailbreak prev.postgresql-binary_0_13_1_3);
|
||||||
|
|
||||||
warp =
|
|
||||||
lib.dontCheck (prev.callHackageDirect
|
|
||||||
{
|
|
||||||
pkg = "warp";
|
|
||||||
ver = "3.4.13";
|
|
||||||
sha256 = "sha256-jmr8kpeSPDkOhT0i9PhozZapX4nUs92cOX7POAGb7/M=";
|
|
||||||
}
|
|
||||||
{ });
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
_: prev:
|
||||||
|
{
|
||||||
|
slocat = prev.buildGoModule {
|
||||||
|
name = "slocat";
|
||||||
|
src = prev.fetchFromGitHub {
|
||||||
|
owner = "robx";
|
||||||
|
repo = "slocat";
|
||||||
|
rev = "52e7512c6029fd00483e41ccce260a3b4b9b3b64";
|
||||||
|
sha256 = "sha256-qn6luuh5wqREu3s8RfuMCP5PKdS2WdwPrujRYTpfzQ8=";
|
||||||
|
};
|
||||||
|
vendorHash = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -172,7 +172,7 @@ let
|
|||||||
# The following unsets all GIT_ variables.
|
# The following unsets all GIT_ variables.
|
||||||
unset "''${!GIT_@}"
|
unset "''${!GIT_@}"
|
||||||
|
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2317
|
||||||
function restore () {
|
function restore () {
|
||||||
ref="$(git stash list --format=format:%gD --grep "$1" -n1)"
|
ref="$(git stash list --format=format:%gD --grep "$1" -n1)"
|
||||||
# this will avoid merge conflicts when applying the stash
|
# this will avoid merge conflicts when applying the stash
|
||||||
@@ -205,7 +205,7 @@ let
|
|||||||
${git}/bin/git add .
|
${git}/bin/git add .
|
||||||
;;
|
;;
|
||||||
pre-push)
|
pre-push)
|
||||||
# Create a clean working tree without any uncommitted changes.
|
# Create a clean working tree without any uncomitted changes.
|
||||||
${withTools.withGit} HEAD ${style}/bin/postgrest-lint
|
${withTools.withGit} HEAD ${style}/bin/postgrest-lint
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@@ -232,7 +232,7 @@ let
|
|||||||
${style}/bin/postgrest-lint
|
${style}/bin/postgrest-lint
|
||||||
;;
|
;;
|
||||||
pre-push)
|
pre-push)
|
||||||
# Create a clean working tree without any uncommitted changes.
|
# Create a clean working tree without any uncomitted changes.
|
||||||
${withTools.withGit} HEAD ${check}
|
${withTools.withGit} HEAD ${check}
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
+6
-2
@@ -43,7 +43,7 @@ let
|
|||||||
}
|
}
|
||||||
|
|
||||||
if [ "$_arg_language" == "" ]; then
|
if [ "$_arg_language" == "" ]; then
|
||||||
# clean previous build, otherwise some errors might be suppressed
|
# clean previous build, otherwise some errors might be supressed
|
||||||
rm -rf "../.docs-build/html/default"
|
rm -rf "../.docs-build/html/default"
|
||||||
|
|
||||||
if [ -d languages ]; then
|
if [ -d languages ]; then
|
||||||
@@ -54,7 +54,7 @@ let
|
|||||||
|
|
||||||
build html "../.docs-build/html/default"
|
build html "../.docs-build/html/default"
|
||||||
else
|
else
|
||||||
# clean previous build, otherwise some errors might be suppressed
|
# clean previous build, otherwise some errors might be supressed
|
||||||
rm -rf "../.docs-build/html/$_arg_language"
|
rm -rf "../.docs-build/html/$_arg_language"
|
||||||
|
|
||||||
# update and build specific locale, can be used to create new locale
|
# update and build specific locale, can be used to create new locale
|
||||||
@@ -122,6 +122,8 @@ let
|
|||||||
workingDir = "/docs";
|
workingDir = "/docs";
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
|
echo "Checking spelling mistakes..."
|
||||||
|
|
||||||
export LC_ALL=C
|
export LC_ALL=C
|
||||||
|
|
||||||
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
|
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
|
||||||
@@ -144,6 +146,8 @@ let
|
|||||||
workingDir = "/docs";
|
workingDir = "/docs";
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
|
echo "Detecting obsolete dictionary entries..."
|
||||||
|
|
||||||
export LC_ALL=C
|
export LC_ALL=C
|
||||||
|
|
||||||
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
|
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
# Generate RSA JWK/public material for loadtests.
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import jwcrypto.jwk as jwk
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Generate RSA JWK/private key pair for loadtests"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--rsa",
|
|
||||||
dest="jwk_path",
|
|
||||||
metavar="JWK_PATH",
|
|
||||||
type=Path,
|
|
||||||
required=True,
|
|
||||||
help="Path to write the RSA JWK file",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--private-key",
|
|
||||||
dest="private_key_path",
|
|
||||||
metavar="PRIVATE_KEY_PATH",
|
|
||||||
type=Path,
|
|
||||||
required=True,
|
|
||||||
help="Path to write the RSA private key file",
|
|
||||||
)
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
key = jwk.JWK.generate(kty="RSA", size=4096)
|
|
||||||
private_jwk, public_jwk = key.export_private(), key.export_public()
|
|
||||||
|
|
||||||
try:
|
|
||||||
args.jwk_path.write_text(public_jwk)
|
|
||||||
print(f"Created RSA JWK on {args.jwk_path}")
|
|
||||||
except OSError as e:
|
|
||||||
print(f"Error writing to {args.jwk_path}:{e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
args.private_key_path.write_text(private_jwk)
|
|
||||||
print(f"Created private key on {args.private_key_path}")
|
|
||||||
except OSError as e:
|
|
||||||
print(f"Error writing to {args.private_key_path}:{e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
+25
-106
@@ -12,24 +12,23 @@
|
|||||||
# from an array
|
# from an array
|
||||||
import time
|
import time
|
||||||
import argparse
|
import argparse
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
import random
|
import random
|
||||||
import jwt
|
import jwt
|
||||||
|
import jwcrypto.jwk as jwk
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from enum import Enum
|
|
||||||
|
|
||||||
URL = "http://postgrest"
|
URL = "http://postgrest"
|
||||||
|
|
||||||
secret_key = b"reallyreallyreallyreallyverysafe"
|
secret_key = b"reallyreallyreallyreallyverysafe"
|
||||||
|
|
||||||
|
key = jwk.JWK.generate(kty="RSA", size=4096)
|
||||||
|
private_key = jwt.algorithms.RSAAlgorithm.from_jwk(key.export_private())
|
||||||
|
public_key = key.export_public()
|
||||||
|
|
||||||
def generate_jwt(
|
|
||||||
now: int,
|
def generate_jwt(now: int, exp_inc: Optional[int], is_hs: bool) -> str:
|
||||||
exp_inc: Optional[int],
|
|
||||||
rsa_private_key: Optional[jwt.algorithms.RSAAlgorithm],
|
|
||||||
) -> str:
|
|
||||||
"""Generate an HS256 or RS256 JWT"""
|
"""Generate an HS256 or RS256 JWT"""
|
||||||
payload = {
|
payload = {
|
||||||
"sub": f"user_{random.getrandbits(32)}",
|
"sub": f"user_{random.getrandbits(32)}",
|
||||||
@@ -40,72 +39,25 @@ def generate_jwt(
|
|||||||
if exp_inc is not None:
|
if exp_inc is not None:
|
||||||
payload["exp"] = now + exp_inc
|
payload["exp"] = now + exp_inc
|
||||||
|
|
||||||
if rsa_private_key is None:
|
k = secret_key if is_hs else private_key
|
||||||
key = secret_key
|
alg = "HS256" if is_hs else "RS256"
|
||||||
alg = "HS256"
|
return jwt.encode(payload, k, alg)
|
||||||
else:
|
|
||||||
key = rsa_private_key
|
|
||||||
alg = "RS256"
|
|
||||||
return jwt.encode(payload, key, alg)
|
|
||||||
|
|
||||||
|
|
||||||
HTTP_METHODS = (
|
def append_targets(lines: list[str], token: str):
|
||||||
"GET",
|
lines.append(f"OPTIONS {URL}/authors_only")
|
||||||
"OPTIONS",
|
|
||||||
)
|
|
||||||
|
|
||||||
HttpMethod = Enum(
|
|
||||||
"HttpMethod",
|
|
||||||
{method: method for method in HTTP_METHODS},
|
|
||||||
type=str,
|
|
||||||
module=__name__,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def append_targets(lines: list[str], token: str, http_method: HttpMethod):
|
|
||||||
lines.append(f"{http_method.value} {URL}/authors_only")
|
|
||||||
lines.append(f"Authorization: Bearer {token}")
|
lines.append(f"Authorization: Bearer {token}")
|
||||||
lines.append("") # blank line to separate requests
|
lines.append("") # blank line to separate requests
|
||||||
|
|
||||||
|
|
||||||
# we use this to chain commands on loadtest.nix
|
|
||||||
def run_command(command: list[str]):
|
|
||||||
if not command:
|
|
||||||
return
|
|
||||||
|
|
||||||
if command[0] == "--":
|
|
||||||
command = command[1:]
|
|
||||||
|
|
||||||
if not command:
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
subprocess.run(command, check=True)
|
|
||||||
except subprocess.CalledProcessError as exc:
|
|
||||||
print(
|
|
||||||
f"Error executing command {' '.join(command)}: {exc}",
|
|
||||||
file=sys.stderr,
|
|
||||||
)
|
|
||||||
sys.exit(exc.returncode)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
description="Generate Vegeta targets with unique JWTs"
|
description="Generate Vegeta targets with unique JWTs"
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"targets_path",
|
"output",
|
||||||
metavar="TARGETS_PATH",
|
|
||||||
help="Path to write the generated targets file",
|
help="Path to write the generated targets file",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
|
||||||
"--private-key",
|
|
||||||
dest="private_key_path",
|
|
||||||
metavar="PRIVATE_KEY_PATH",
|
|
||||||
type=Path,
|
|
||||||
default=None,
|
|
||||||
help="Path to the RSA private key file (required when --rsa is used)",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--worst",
|
"--worst",
|
||||||
dest="worst",
|
dest="worst",
|
||||||
@@ -119,31 +71,14 @@ def main():
|
|||||||
metavar="JWK_PATH",
|
metavar="JWK_PATH",
|
||||||
type=Path,
|
type=Path,
|
||||||
default=None,
|
default=None,
|
||||||
help="Path to an existing RSA JWK file used for signing tokens",
|
help="Path for generating a RSA JWK file to sign tokens with",
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--method",
|
|
||||||
dest="http_method",
|
|
||||||
choices=list(HTTP_METHODS),
|
|
||||||
required=True,
|
|
||||||
help="HTTP method for the vegeta targets",
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"command",
|
|
||||||
nargs=argparse.REMAINDER,
|
|
||||||
help="Command (and arguments) to run after generating the targets",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
rsa_private_key: Optional[jwt.algorithms.RSAAlgorithm] = None
|
|
||||||
|
|
||||||
is_hs = args.jwk_path is None
|
is_hs = args.jwk_path is None
|
||||||
|
|
||||||
http_method = HttpMethod(args.http_method)
|
|
||||||
|
|
||||||
nsamples = 1000
|
nsamples = 1000
|
||||||
|
|
||||||
if is_hs:
|
if is_hs:
|
||||||
ntargets = 200000
|
ntargets = 200000
|
||||||
else:
|
else:
|
||||||
@@ -151,25 +86,12 @@ def main():
|
|||||||
ntargets = 50000
|
ntargets = 50000
|
||||||
|
|
||||||
if not is_hs:
|
if not is_hs:
|
||||||
if args.private_key_path is None:
|
|
||||||
parser.error("--rsa requires the --private-key option")
|
|
||||||
try:
|
try:
|
||||||
private_key_data = args.private_key_path.read_text()
|
with open(args.jwk_path, "w") as jwk:
|
||||||
except OSError as e:
|
jwk.write(public_key)
|
||||||
err = (
|
print(f"Created {args.jwk_path} file containing the RSA JWK")
|
||||||
f"Error reading RSA private key from {args.private_key_path}: "
|
except IOError as e:
|
||||||
f"{e}. Generate RSA materials first with gen_rsa_materials.py."
|
print(f"Error writing to {args.jwk_path}: {e}", file=sys.stderr)
|
||||||
)
|
|
||||||
print(err, file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
|
||||||
rsa_private_key = jwt.algorithms.RSAAlgorithm.from_jwk(private_key_data)
|
|
||||||
except Exception as exc: # broad exception to capture parsing errors
|
|
||||||
err = (
|
|
||||||
f"Error loading RSA private key from {args.private_key_path}: " f"{exc}"
|
|
||||||
)
|
|
||||||
print(err, file=sys.stderr)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
print(f"Generating {ntargets} targets...")
|
print(f"Generating {ntargets} targets...")
|
||||||
@@ -188,7 +110,6 @@ def main():
|
|||||||
if args.worst:
|
if args.worst:
|
||||||
# estimated time takes to build and run postgrest itself
|
# estimated time takes to build and run postgrest itself
|
||||||
build_run_postgrest_time = 2
|
build_run_postgrest_time = 2
|
||||||
|
|
||||||
# estimated time it takes to generate the targets file
|
# estimated time it takes to generate the targets file
|
||||||
# the division numbers are tuned by hand
|
# the division numbers are tuned by hand
|
||||||
if is_hs: # hs generation is much faster
|
if is_hs: # hs generation is much faster
|
||||||
@@ -200,27 +121,25 @@ def main():
|
|||||||
inc = build_run_postgrest_time + gen_time
|
inc = build_run_postgrest_time + gen_time
|
||||||
|
|
||||||
for i in range(ntargets):
|
for i in range(ntargets):
|
||||||
token = generate_jwt(now, inc + i // 1000, rsa_private_key)
|
token = generate_jwt(now, inc + i // 1000, is_hs)
|
||||||
append_targets(lines, token, http_method)
|
append_targets(lines, token)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
tokens = [generate_jwt(now, None, rsa_private_key) for _ in range(nsamples)]
|
tokens = [generate_jwt(now, None, is_hs) for _ in range(nsamples)]
|
||||||
for i in range(ntargets):
|
for i in range(ntargets):
|
||||||
token = random.choice(tokens)
|
token = random.choice(tokens)
|
||||||
append_targets(lines, token, http_method)
|
append_targets(lines, token)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(args.targets_path, "w") as f:
|
with open(args.output, "w") as f:
|
||||||
f.write("\n".join(lines))
|
f.write("\n".join(lines))
|
||||||
except IOError as e:
|
except IOError as e:
|
||||||
print(f"Error writing to {args.targets_path}: {e}", file=sys.stderr)
|
print(f"Error writing to {args.output}: {e}", file=sys.stderr)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
elapsed = time.time() - start_time
|
elapsed = time.time() - start_time
|
||||||
print(f"Created {ntargets} targets", end=" ")
|
print(f"Created {ntargets} targets", end=" ")
|
||||||
print(f"in {args.targets_path} ({elapsed:.2f}s)")
|
print(f"in {args.output} ({elapsed:.2f}s)")
|
||||||
|
|
||||||
run_command(args.command)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+32
-105
@@ -18,8 +18,6 @@ let
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
echo "Starting vegeta loadtest..."
|
|
||||||
|
|
||||||
# ARG_USE_ENV only adds defaults or docs for environment variables
|
# ARG_USE_ENV only adds defaults or docs for environment variables
|
||||||
# We manually implement a required check here
|
# We manually implement a required check here
|
||||||
# See also: https://github.com/matejak/argbash/issues/80
|
# See also: https://github.com/matejak/argbash/issues/80
|
||||||
@@ -44,9 +42,7 @@ let
|
|||||||
"ARG_OPTIONAL_SINGLE([output], [o], [Filename to dump json output to], [./loadtest/result.bin])"
|
"ARG_OPTIONAL_SINGLE([output], [o], [Filename to dump json output to], [./loadtest/result.bin])"
|
||||||
"ARG_OPTIONAL_SINGLE([testdir], [t], [Directory to load tests and fixtures from], [./test/load])"
|
"ARG_OPTIONAL_SINGLE([testdir], [t], [Directory to load tests and fixtures from], [./test/load])"
|
||||||
"ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest], [mixed])"
|
"ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest], [mixed])"
|
||||||
"ARG_OPTIONAL_SINGLE([method],, [HTTP method used for the jwt loadtests], [OPTIONS])"
|
"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,jwt-hs,jwt-hs-cache,jwt-hs-cache-worst,jwt-rsa,jwt-rsa-cache,jwt-rsa-cache-worst])"
|
||||||
"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,errors,jwt-hs,jwt-hs-cache,jwt-hs-cache-worst,jwt-rsa,jwt-rsa-cache,jwt-rsa-cache-worst])"
|
|
||||||
"ARG_TYPE_GROUP_SET([METHOD], [METHOD], [method], [OPTIONS,GET])"
|
|
||||||
"ARG_OPTIONAL_SINGLE([monitor], [m], [Monitoring file], [./loadtest/result.csv])"
|
"ARG_OPTIONAL_SINGLE([monitor], [m], [Monitoring file], [./loadtest/result.csv])"
|
||||||
"ARG_LEFTOVERS([additional vegeta arguments])"
|
"ARG_LEFTOVERS([additional vegeta arguments])"
|
||||||
];
|
];
|
||||||
@@ -63,127 +59,67 @@ let
|
|||||||
export PGRST_DB_TX_END="rollback-allow-override"
|
export PGRST_DB_TX_END="rollback-allow-override"
|
||||||
export PGRST_LOG_LEVEL="crit"
|
export PGRST_LOG_LEVEL="crit"
|
||||||
export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe"
|
export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe"
|
||||||
|
# set previous PGRST_JWT_CACHE_MAX_LIFETIME configuration so that
|
||||||
|
# load test works across branches
|
||||||
|
# TODO clean once PGRST_JWT_CACHE_MAX_ENTRIES merged and released
|
||||||
|
export PGRST_JWT_CACHE_MAX_LIFETIME="86400"
|
||||||
|
|
||||||
mkdir -p "$(dirname "$_arg_output")"
|
mkdir -p "$(dirname "$_arg_output")"
|
||||||
abs_output="$(realpath "$_arg_output")"
|
abs_output="$(realpath "$_arg_output")"
|
||||||
|
|
||||||
case "$_arg_kind" in
|
case "$_arg_kind" in
|
||||||
jwt-hs)
|
jwt-hs)
|
||||||
|
${genTargetsHS} "$_arg_testdir"/gen_targets.http
|
||||||
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
|
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
|
||||||
|
export PGRST_JWT_CACHE_MAX_LIFETIME="0"
|
||||||
# shellcheck disable=SC2145
|
|
||||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
|
||||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
|
||||||
${withGenTargets} --method "$_arg_method" "$_arg_testdir"/gen_targets.http \
|
|
||||||
sh -c "cd \"$_arg_testdir\" && \
|
|
||||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
jwt-hs-cache)
|
jwt-hs-cache)
|
||||||
# shellcheck disable=SC2145
|
${genTargetsHS} "$_arg_testdir"/gen_targets.http
|
||||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
|
||||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
|
||||||
${withGenTargets} --method "$_arg_method" "$_arg_testdir"/gen_targets.http \
|
|
||||||
sh -c "cd \"$_arg_testdir\" && \
|
|
||||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
jwt-hs-cache-worst)
|
jwt-hs-cache-worst)
|
||||||
# shellcheck disable=SC2145
|
${genTargetsHS} --worst "$_arg_testdir"/gen_targets.http
|
||||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
|
||||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
|
||||||
${withGenTargets} --method "$_arg_method" --worst "$_arg_testdir"/gen_targets.http \
|
|
||||||
sh -c "cd \"$_arg_testdir\" && \
|
|
||||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
jwt-rsa)
|
jwt-rsa)
|
||||||
|
${genTargetsHS} --rsa="$_arg_testdir"/gen_jwk.json "$_arg_testdir"/gen_targets.http
|
||||||
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
|
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
|
||||||
|
export PGRST_JWT_CACHE_MAX_LIFETIME="0"
|
||||||
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
|
|
||||||
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
||||||
|
|
||||||
# shellcheck disable=SC2145
|
|
||||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
|
||||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
|
||||||
${withGenTargets} --method "$_arg_method" --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
|
|
||||||
sh -c "cd \"$_arg_testdir\" && \
|
|
||||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
jwt-rsa-cache)
|
jwt-rsa-cache)
|
||||||
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
|
${genTargetsHS} --rsa="$_arg_testdir"/gen_jwk.json "$_arg_testdir"/gen_targets.http
|
||||||
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
||||||
|
|
||||||
# shellcheck disable=SC2145
|
|
||||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
|
||||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
|
||||||
${withGenTargets} --method "$_arg_method" --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
|
|
||||||
sh -c "cd \"$_arg_testdir\" && \
|
|
||||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
jwt-rsa-cache-worst)
|
jwt-rsa-cache-worst)
|
||||||
|
${genTargetsHS} --worst --rsa="$_arg_testdir"/gen_jwk.json "$_arg_testdir"/gen_targets.http
|
||||||
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
||||||
|
|
||||||
${genRsaMaterials} --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json
|
|
||||||
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
|
|
||||||
|
|
||||||
# shellcheck disable=SC2145
|
|
||||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
|
||||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
|
||||||
${withGenTargets} --method "$_arg_method" --worst --rsa="$_arg_testdir"/gen_jwk.json --private-key="$_arg_testdir"/gen_private.json "$_arg_testdir"/gen_targets.http \
|
|
||||||
sh -c "cd \"$_arg_testdir\" && \
|
|
||||||
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
|
||||||
;;
|
;;
|
||||||
|
|
||||||
mixed)
|
*)
|
||||||
# shellcheck disable=SC2145
|
|
||||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
|
||||||
${withTools.withPgrst} -m "$_arg_monitor" \
|
|
||||||
sh -c "cd \"$_arg_testdir\" && \
|
|
||||||
${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
|
||||||
;;
|
|
||||||
|
|
||||||
# here we sleep purposefully to check how much memory does the schema cache consume in the final report
|
|
||||||
errors)
|
|
||||||
# shellcheck disable=SC2145
|
|
||||||
${withTools.withPg} -f "$_arg_testdir"/errors.sql \
|
|
||||||
${withTools.withPgrst} --timeout 2 --sleep 5 -m "$_arg_monitor" \
|
|
||||||
sh -c "cd \"$_arg_testdir\" && \
|
|
||||||
${runner} -targets errors.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
if [ "$_arg_kind" == "mixed" ]; then
|
||||||
|
# shellcheck disable=SC2145
|
||||||
if [ "$_arg_kind" != "errors" ]; then
|
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||||
# fail in case 401 happened on jwt loadtests
|
${withTools.withSlowPg} \
|
||||||
unauthorized_count="$(${vegeta}/bin/vegeta report -type=json "$_arg_output" \
|
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||||
| ${jq}/bin/jq -r '.status_codes["401"] // 0')"
|
${withTools.withSlowPgrst} \
|
||||||
|
sh -c "cd \"$_arg_testdir\" && \
|
||||||
if [ "$unauthorized_count" -gt 0 ]; then
|
${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||||
last_unauthorized_body="$(${vegeta}/bin/vegeta encode "$_arg_output" \
|
else
|
||||||
| ${jq}/bin/jq -rn '
|
# shellcheck disable=SC2145
|
||||||
reduce inputs as $item (null;
|
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||||
if $item.code == 401 then $item else . end
|
${withTools.withPgrst} -m "$_arg_monitor" \
|
||||||
)
|
sh -c "cd \"$_arg_testdir\" && \
|
||||||
| if . == null then
|
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||||
empty
|
|
||||||
else
|
|
||||||
(.body | @base64d)
|
|
||||||
end
|
|
||||||
')"
|
|
||||||
|
|
||||||
echo "loadtest failed: found $unauthorized_count 401 Unauthorized responses" >&2
|
|
||||||
if [ -n "$last_unauthorized_body" ]; then
|
|
||||||
printf '%s\n' "Last 401 response body:" >&2
|
|
||||||
printf '%s\n' "$last_unauthorized_body" >&2
|
|
||||||
fi
|
|
||||||
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
loadtestAgainst =
|
loadtestAgainst =
|
||||||
@@ -314,22 +250,13 @@ let
|
|||||||
| ${mergeMonitorResults}
|
| ${mergeMonitorResults}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
withGenTargets =
|
genTargetsHS =
|
||||||
writers.writePython3 "postgrest-with-gen-loadtest-targets"
|
writers.writePython3 "postgrest-gen-loadtest-targets-hs"
|
||||||
{
|
{
|
||||||
libraries = [ python3Packages.pyjwt python3Packages.jwcrypto ];
|
libraries = [ python3Packages.pyjwt python3Packages.jwcrypto ];
|
||||||
doCheck = false; # postgrest-style conflicts with this
|
|
||||||
}
|
}
|
||||||
(builtins.readFile ./generate_targets.py);
|
(builtins.readFile ./generate_targets.py);
|
||||||
|
|
||||||
genRsaMaterials =
|
|
||||||
writers.writePython3 "postgrest-gen-rsa-materials"
|
|
||||||
{
|
|
||||||
libraries = [ python3Packages.jwcrypto ];
|
|
||||||
doCheck = false; # postgrest-style conflicts with this
|
|
||||||
}
|
|
||||||
(builtins.readFile ./gen_rsa_materials.py);
|
|
||||||
|
|
||||||
mergeMonitorResults =
|
mergeMonitorResults =
|
||||||
writers.writePython3 "postgrest-merge-monitor-results"
|
writers.writePython3 "postgrest-merge-monitor-results"
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -62,19 +62,19 @@ let
|
|||||||
git add CHANGELOG.md > /dev/null
|
git add CHANGELOG.md > /dev/null
|
||||||
|
|
||||||
echo "Committing ..."
|
echo "Committing ..."
|
||||||
git commit -m "bump version to $new_version" > /dev/null
|
git commit -m "chore: bump version to $new_version" > /dev/null
|
||||||
|
|
||||||
if [[ "$current_branch" == "main" ]]; then
|
if [[ "$current_branch" == "main" ]]; then
|
||||||
bump devel
|
bump devel
|
||||||
|
|
||||||
# The order of operations is important here:
|
# The order of operations is important here:
|
||||||
# - bump devel is run and $A is updated to the new version
|
# - bump devel is run and $A is upated to the new version
|
||||||
# - the branch is created with the new A, but the commit before the devel bump
|
# - the branch is created with the new A, but the commit before the devel bump
|
||||||
# - the devel bump is committed
|
# - the devel bump is committed
|
||||||
git branch "v$A"
|
git branch "v$A"
|
||||||
|
|
||||||
echo "Committing (devel bump)..."
|
echo "Committing (devel bump)..."
|
||||||
git commit -m "bump version to $new_version" > /dev/null
|
git commit -m "chore: bump version to $new_version" > /dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
trap "echo Remote not found. Please push manually ..." ERR
|
trap "echo Remote not found. Please push manually ..." ERR
|
||||||
|
|||||||
+6
-8
@@ -29,20 +29,19 @@ let
|
|||||||
|
|
||||||
# Format Haskell files
|
# Format Haskell files
|
||||||
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
|
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
|
||||||
# TODO: fix style issues in src/protolude and include it
|
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \
|
||||||
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' --ignore-dir=src/protolude . \
|
|
||||||
| xargs ${stylish-haskell}/bin/stylish-haskell -i
|
| xargs ${stylish-haskell}/bin/stylish-haskell -i
|
||||||
|
|
||||||
# Format Python files
|
# Format Python files
|
||||||
${black}/bin/black . 2> /dev/null
|
${black}/bin/black . 2> /dev/null
|
||||||
'';
|
'';
|
||||||
|
|
||||||
# Script to check whether any uncommitted changes result from postgrest-style
|
# Script to check whether any uncommited changes result from postgrest-style
|
||||||
styleCheck =
|
styleCheck =
|
||||||
checkedShellScript
|
checkedShellScript
|
||||||
{
|
{
|
||||||
name = "postgrest-style-check";
|
name = "postgrest-style-check";
|
||||||
docs = "Check whether postgrest-style results in any uncommitted changes.";
|
docs = "Check whether postgrest-style results in any uncommited changes.";
|
||||||
workingDir = "/";
|
workingDir = "/";
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
@@ -84,18 +83,17 @@ let
|
|||||||
# ruff has gaps in scanning for unused code, so we use vulture
|
# ruff has gaps in scanning for unused code, so we use vulture
|
||||||
echo "Scanning python files for unused code..."
|
echo "Scanning python files for unused code..."
|
||||||
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?py$' . \
|
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?py$' . \
|
||||||
| xargs ${python3Packages.vulture}/bin/vulture --exclude docs/conf.py --min-confidence 80
|
| xargs ${python3Packages.vulture}/bin/vulture --exclude docs/conf.py
|
||||||
|
|
||||||
echo "Linting python files..."
|
echo "Linting python files..."
|
||||||
${ruff}/bin/ruff check .
|
${ruff}/bin/ruff check .
|
||||||
|
|
||||||
echo "Checking consistency of import aliases in Haskell code..."
|
echo "Checking consistency of import aliases in Haskell code..."
|
||||||
${hsie} check-aliases main src/PostgREST
|
${hsie} check-aliases main src
|
||||||
|
|
||||||
echo "Linting Haskell files..."
|
echo "Linting Haskell files..."
|
||||||
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
|
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
|
||||||
# TODO: fix lint issues in src/protolude and include it
|
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \
|
||||||
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' --ignore-dir=src/protolude . \
|
|
||||||
| xargs ${hlint}/bin/hlint --hint=${hlintConfig}
|
| xargs ${hlint}/bin/hlint --hint=${hlintConfig}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -7,8 +7,10 @@
|
|||||||
, glibcLocales ? null
|
, glibcLocales ? null
|
||||||
, gnugrep
|
, gnugrep
|
||||||
, hpc-codecov
|
, hpc-codecov
|
||||||
|
, hostPlatform
|
||||||
, jq
|
, jq
|
||||||
, lib
|
, lib
|
||||||
|
, nginx
|
||||||
, postgrest
|
, postgrest
|
||||||
, python3
|
, python3
|
||||||
, runtimeShell
|
, runtimeShell
|
||||||
@@ -93,6 +95,7 @@ let
|
|||||||
args = [ "ARG_LEFTOVERS([pytest arguments])" ];
|
args = [ "ARG_LEFTOVERS([pytest arguments])" ];
|
||||||
workingDir = "/";
|
workingDir = "/";
|
||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
|
withPath = [ nginx ];
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
||||||
@@ -155,10 +158,11 @@ let
|
|||||||
redirectTixFiles = false;
|
redirectTixFiles = false;
|
||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
withTmpDir = true;
|
withTmpDir = true;
|
||||||
|
withPath = [ nginx ];
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
# required for `hpc markup` in CI; glibcLocales is not available e.g. on Darwin
|
# required for `hpc markup` in CI; glibcLocales is not available e.g. on Darwin
|
||||||
lib.optionalString (stdenv.isLinux && stdenv.hostPlatform.libc == "glibc") ''
|
lib.optionalString (stdenv.isLinux && hostPlatform.libc == "glibc") ''
|
||||||
export LOCALE_ARCHIVE="${glibcLocales}/lib/locale/locale-archive"
|
export LOCALE_ARCHIVE="${glibcLocales}/lib/locale/locale-archive"
|
||||||
'' +
|
'' +
|
||||||
|
|
||||||
|
|||||||
+103
-46
@@ -6,6 +6,7 @@
|
|||||||
, postgresqlVersions
|
, postgresqlVersions
|
||||||
, postgrest
|
, postgrest
|
||||||
, python3Packages
|
, python3Packages
|
||||||
|
, slocat
|
||||||
, writeText
|
, writeText
|
||||||
, writers
|
, writers
|
||||||
}:
|
}:
|
||||||
@@ -105,8 +106,7 @@ let
|
|||||||
|
|
||||||
log "Starting replica on $replica_host"
|
log "Starting replica on $replica_host"
|
||||||
|
|
||||||
# We set a low max_standby_streaming_delay to make the replication conflict fail faster in tests (otherwise it waits for the default 30s)
|
pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" " \
|
||||||
pg_ctl -D "$replica_dir" -l "$replica_dblog" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $replica_host -c log_statement=\"all\" -c max_standby_streaming_delay=\"3s\" " \
|
|
||||||
>> "$setuplog"
|
>> "$setuplog"
|
||||||
|
|
||||||
>&2 echo "${commandName}: Replica enabled. You can connect to it with: psql 'postgres:///$PGDATABASE?host=$replica_host' -U postgres"
|
>&2 echo "${commandName}: Replica enabled. You can connect to it with: psql 'postgres:///$PGDATABASE?host=$replica_host' -U postgres"
|
||||||
@@ -117,7 +117,7 @@ let
|
|||||||
export PGRST_DB_URI="postgres:///$PGDATABASE?host=$PGREPLICAHOST,$PGHOST"
|
export PGRST_DB_URI="postgres:///$PGDATABASE?host=$PGREPLICAHOST,$PGHOST"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2317
|
||||||
stop () {
|
stop () {
|
||||||
log "Stopping the database cluster..."
|
log "Stopping the database cluster..."
|
||||||
pg_ctl stop --mode=immediate >> "$setuplog"
|
pg_ctl stop --mode=immediate >> "$setuplog"
|
||||||
@@ -132,11 +132,9 @@ let
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
if test "$_arg_fixtures"; then
|
if test "$_arg_fixtures"; then
|
||||||
load_start=$SECONDS
|
log "Loading fixtures under the postgres role..."
|
||||||
>&2 printf "${commandName}: Loading fixtures under the postgres role..."
|
|
||||||
psql -U postgres -v PGUSER="$PGUSER" -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog"
|
psql -U postgres -v PGUSER="$PGUSER" -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog"
|
||||||
load_end=$((SECONDS - load_start))
|
log "Done. Running command..."
|
||||||
>&2 printf " done in %ss. Running command...\n" "$load_end"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
("$_arg_command" "''${_arg_leftovers[@]}")
|
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||||
@@ -185,6 +183,81 @@ let
|
|||||||
|
|
||||||
withPg = withTmpDb (builtins.head postgresqlVersions);
|
withPg = withTmpDb (builtins.head postgresqlVersions);
|
||||||
|
|
||||||
|
withSlowPg =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-with-slow-pg";
|
||||||
|
docs = "Run the given command with simulated high latency postgresql";
|
||||||
|
args =
|
||||||
|
[
|
||||||
|
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||||
|
"ARG_LEFTOVERS([command arguments])"
|
||||||
|
"ARG_USE_ENV([PGHOST], [], [PG host (socket name)])"
|
||||||
|
"ARG_USE_ENV([PGDELAY], [0ms], [extra PG latency (duration)])"
|
||||||
|
];
|
||||||
|
positionalCompletion = "_command";
|
||||||
|
workingDir = "/";
|
||||||
|
redirectTixFiles = false;
|
||||||
|
withTmpDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
delay="''${PGDELAY:-0ms}"
|
||||||
|
echo "delaying data to/from postgres by $delay"
|
||||||
|
|
||||||
|
REALPGHOST="$PGHOST"
|
||||||
|
export PGHOST="$tmpdir/socket"
|
||||||
|
mkdir -p "$PGHOST"
|
||||||
|
|
||||||
|
${slocat}/bin/slocat -delay "$delay" -src "$PGHOST/.s.PGSQL.5432" -dst "$REALPGHOST/.s.PGSQL.5432" &
|
||||||
|
SLOCAT_PID=$!
|
||||||
|
# shellcheck disable=SC2317
|
||||||
|
stop_slocat() {
|
||||||
|
kill "$SLOCAT_PID" || true
|
||||||
|
wait "$SLOCAT_PID" || true
|
||||||
|
}
|
||||||
|
trap stop_slocat EXIT
|
||||||
|
sleep 1 # should wait for socket file to appear instead
|
||||||
|
|
||||||
|
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||||
|
'';
|
||||||
|
|
||||||
|
withSlowPgrst =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-with-slow-postgrest";
|
||||||
|
docs = "Run the given command with simulated high latency postgrest";
|
||||||
|
args =
|
||||||
|
[
|
||||||
|
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||||
|
"ARG_LEFTOVERS([command arguments])"
|
||||||
|
"ARG_USE_ENV([PGRST_SERVER_UNIX_SOCKET], [], [PostgREST host (socket name)])"
|
||||||
|
"ARG_USE_ENV([PGRST_DELAY], [0ms], [extra PostgREST latency (duration)])"
|
||||||
|
];
|
||||||
|
positionalCompletion = "_command";
|
||||||
|
workingDir = "/";
|
||||||
|
redirectTixFiles = false;
|
||||||
|
withTmpDir = true;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
delay="''${PGRST_DELAY:-0ms}"
|
||||||
|
echo "delaying data to/from PostgREST by $delay"
|
||||||
|
|
||||||
|
REAL_PGRST_SERVER_UNIX_SOCKET="$PGRST_SERVER_UNIX_SOCKET"
|
||||||
|
export PGRST_SERVER_UNIX_SOCKET="$tmpdir/postgrest.socket"
|
||||||
|
|
||||||
|
${slocat}/bin/slocat -delay "$delay" -src "$PGRST_SERVER_UNIX_SOCKET" -dst "$REAL_PGRST_SERVER_UNIX_SOCKET" &
|
||||||
|
SLOCAT_PID=$!
|
||||||
|
# shellcheck disable=SC2317
|
||||||
|
stop_slocat() {
|
||||||
|
kill "$SLOCAT_PID" || true
|
||||||
|
wait "$SLOCAT_PID" || true
|
||||||
|
}
|
||||||
|
trap stop_slocat EXIT
|
||||||
|
sleep 1 # should wait for socket file to appear instead
|
||||||
|
|
||||||
|
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||||
|
'';
|
||||||
|
|
||||||
withGit =
|
withGit =
|
||||||
let
|
let
|
||||||
name = "postgrest-with-git";
|
name = "postgrest-with-git";
|
||||||
@@ -279,21 +352,15 @@ let
|
|||||||
'';
|
'';
|
||||||
|
|
||||||
withPgrst =
|
withPgrst =
|
||||||
let
|
|
||||||
commandName = "postgrest-with-pgrst";
|
|
||||||
in
|
|
||||||
checkedShellScript
|
checkedShellScript
|
||||||
{
|
{
|
||||||
name = commandName;
|
name = "postgrest-with-pgrst";
|
||||||
docs = "Build and run PostgREST and run <command> with PGRST_SERVER_UNIX_SOCKET set.";
|
docs = "Build and run PostgREST and run <command> with PGRST_SERVER_UNIX_SOCKET set.";
|
||||||
args =
|
args =
|
||||||
[
|
[
|
||||||
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
||||||
"ARG_LEFTOVERS([command arguments])"
|
"ARG_LEFTOVERS([command arguments])"
|
||||||
"ARG_OPTIONAL_SINGLE([monitor], [m], [Enable CPU and memory monitoring of the PostgREST process and output to the designated file as markdown])"
|
"ARG_OPTIONAL_SINGLE([monitor], [m], [Enable CPU and memory monitoring of the PostgREST process and output to the designated file as markdown])"
|
||||||
"ARG_OPTIONAL_SINGLE([timeout], [t], [Maximum time to wait for PostgREST to be ready], [5])"
|
|
||||||
"ARG_OPTIONAL_SINGLE([sleep], [s], [Sleep time after PostgREST is ready, this is useful for monitoring])"
|
|
||||||
"ARG_USE_ENV([PGRST_CMD], [], [PostgREST executable to run])"
|
|
||||||
];
|
];
|
||||||
positionalCompletion = "_command";
|
positionalCompletion = "_command";
|
||||||
workingDir = "/";
|
workingDir = "/";
|
||||||
@@ -303,34 +370,30 @@ let
|
|||||||
''
|
''
|
||||||
export PGRST_SERVER_UNIX_SOCKET="$tmpdir"/postgrest.socket
|
export PGRST_SERVER_UNIX_SOCKET="$tmpdir"/postgrest.socket
|
||||||
|
|
||||||
if [ -z "''${PGRST_CMD:-}" ]; then
|
rm -f result
|
||||||
rm -f result
|
if [ -z "''${PGRST_BUILD_CABAL:-}" ]; then
|
||||||
build_start=$SECONDS
|
echo -n "Building postgrest (nix)... "
|
||||||
if [ -z "''${PGRST_BUILD_CABAL:-}" ]; then
|
# Using lib.getBin to also make this work with older checkouts, where .bin was not a thing, yet.
|
||||||
echo -n "${commandName}: Building postgrest (nix)... "
|
nix-build -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage' > "$tmpdir"/build.log 2>&1 || {
|
||||||
# Using lib.getBin to also make this work with older checkouts, where .bin was not a thing, yet.
|
echo "failed, output:"
|
||||||
nix-build -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage' > "$tmpdir"/build.log 2>&1 || {
|
cat "$tmpdir"/build.log
|
||||||
echo "failed, output:"
|
exit 1
|
||||||
cat "$tmpdir"/build.log
|
}
|
||||||
exit 1
|
PGRST_CMD=$(echo ./result*/bin/postgrest)
|
||||||
}
|
else
|
||||||
PGRST_CMD=$(echo ./result*/bin/postgrest)
|
echo -n "Building postgrest (cabal)... "
|
||||||
else
|
postgrest-build
|
||||||
echo -n "${commandName}: Building postgrest (cabal)... "
|
PGRST_CMD=postgrest-run
|
||||||
postgrest-build
|
|
||||||
PGRST_CMD=postgrest-run
|
|
||||||
fi
|
|
||||||
build_end=$((SECONDS - build_start))
|
|
||||||
printf "done in %ss.\n" "$build_end"
|
|
||||||
fi
|
fi
|
||||||
|
echo "done."
|
||||||
|
|
||||||
ver=$($PGRST_CMD ${legacyConfig} --version)
|
ver=$($PGRST_CMD ${legacyConfig} --version)
|
||||||
|
|
||||||
echo -n "${commandName}: Starting $ver... "
|
echo -n "Starting $ver... "
|
||||||
|
|
||||||
$PGRST_CMD ${legacyConfig} > "$tmpdir"/run.log 2>&1 &
|
$PGRST_CMD ${legacyConfig} > "$tmpdir"/run.log 2>&1 &
|
||||||
pid=$!
|
pid=$!
|
||||||
# shellcheck disable=SC2329
|
# shellcheck disable=SC2317
|
||||||
cleanup() {
|
cleanup() {
|
||||||
# Send INT to all postgrest processes.
|
# Send INT to all postgrest processes.
|
||||||
# Workaround to trigger dumping postgrest.prof for postgrest-profiled-run
|
# Workaround to trigger dumping postgrest.prof for postgrest-profiled-run
|
||||||
@@ -344,25 +407,17 @@ let
|
|||||||
}
|
}
|
||||||
trap cleanup EXIT
|
trap cleanup EXIT
|
||||||
|
|
||||||
wait_start=$SECONDS
|
timeout -s TERM 5 ${waitForPgrstReady} || {
|
||||||
timeout -s TERM "$_arg_timeout" ${waitForPgrstReady} || {
|
|
||||||
echo "timed out, output:"
|
echo "timed out, output:"
|
||||||
cat "$tmpdir"/run.log
|
cat "$tmpdir"/run.log
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
wait_duration=$((SECONDS - wait_start))
|
echo "done."
|
||||||
printf "done in %ss.\n" "$wait_duration"
|
|
||||||
|
|
||||||
echo "${commandName}: You can tail the server logs with: tail -f $tmpdir/run.log"
|
|
||||||
|
|
||||||
if [[ -n "$_arg_monitor" ]]; then
|
if [[ -n "$_arg_monitor" ]]; then
|
||||||
${monitorPid} "$pid" > "$_arg_monitor" &
|
${monitorPid} "$pid" > "$_arg_monitor" &
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -n "$_arg_sleep" ]]; then
|
|
||||||
sleep "$_arg_sleep"
|
|
||||||
fi
|
|
||||||
|
|
||||||
("$_arg_command" "''${_arg_leftovers[@]}")
|
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||||
'';
|
'';
|
||||||
|
|
||||||
@@ -380,7 +435,9 @@ buildToolbox
|
|||||||
inherit
|
inherit
|
||||||
withGit
|
withGit
|
||||||
withPgAll
|
withPgAll
|
||||||
withPgrst;
|
withPgrst
|
||||||
|
withSlowPg
|
||||||
|
withSlowPgrst;
|
||||||
} // builtins.listToAttrs (
|
} // builtins.listToAttrs (
|
||||||
# Create a `postgrest-with-pg-` for each PostgreSQL version
|
# Create a `postgrest-with-pg-` for each PostgreSQL version
|
||||||
builtins.map (pg: { inherit (pg) name; value = withTmpDb pg; }) postgresqlVersions
|
builtins.map (pg: { inherit (pg) name; value = withTmpDb pg; }) postgresqlVersions
|
||||||
|
|||||||
+35
-88
@@ -1,27 +1,28 @@
|
|||||||
cabal-version: 3.0
|
|
||||||
name: postgrest
|
name: postgrest
|
||||||
version: 15
|
version: 14.13
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for tables, views, and functions, supporting all HTTP methods that security
|
for tables, views, and functions, supporting all HTTP methods that security
|
||||||
permits.
|
permits.
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Joe Nelson, Adam Baker, Steve Chavez, Wolfgang Walther
|
author: Joe Nelson, Adam Baker, Steve Chavez
|
||||||
maintainer: Steve Chavez <stevechavezast@gmail.com>
|
maintainer: Steve Chavez <stevechavezast@gmail.com>
|
||||||
category: Executable, PostgreSQL, Network APIs
|
category: Executable, PostgreSQL, Network APIs
|
||||||
homepage: https://postgrest.org
|
homepage: https://postgrest.org
|
||||||
bug-reports: https://github.com/PostgREST/postgrest/issues
|
bug-reports: https://github.com/PostgREST/postgrest/issues
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
extra-source-files: CHANGELOG.md
|
extra-source-files: CHANGELOG.md
|
||||||
|
cabal-version: >= 1.10
|
||||||
|
|
||||||
tested-with:
|
tested-with:
|
||||||
|
-- nix
|
||||||
|
GHC == 9.4.8
|
||||||
-- cabal on Ubuntu
|
-- cabal on Ubuntu
|
||||||
-- stack on FreeBSD, MacOS, Ubuntu, Windows
|
-- stack on FreeBSD, MacOS, Ubuntu, Windows
|
||||||
, GHC == 9.10.3
|
, GHC == 9.6.7
|
||||||
-- cabal on Ubuntu
|
-- cabal on Ubuntu
|
||||||
-- nix
|
, GHC == 9.8.4
|
||||||
, GHC == 9.12.3
|
|
||||||
|
|
||||||
source-repository head
|
source-repository head
|
||||||
type: git
|
type: git
|
||||||
@@ -54,7 +55,6 @@ library
|
|||||||
PostgREST.Client
|
PostgREST.Client
|
||||||
PostgREST.Config
|
PostgREST.Config
|
||||||
PostgREST.Config.Database
|
PostgREST.Config.Database
|
||||||
PostgREST.Debounce
|
|
||||||
PostgREST.Config.JSPath
|
PostgREST.Config.JSPath
|
||||||
PostgREST.Config.PgVersion
|
PostgREST.Config.PgVersion
|
||||||
PostgREST.Config.Proxy
|
PostgREST.Config.Proxy
|
||||||
@@ -66,7 +66,6 @@ library
|
|||||||
PostgREST.SchemaCache.Representations
|
PostgREST.SchemaCache.Representations
|
||||||
PostgREST.SchemaCache.Table
|
PostgREST.SchemaCache.Table
|
||||||
PostgREST.Error
|
PostgREST.Error
|
||||||
PostgREST.Error.Types
|
|
||||||
PostgREST.Listener
|
PostgREST.Listener
|
||||||
PostgREST.Logger
|
PostgREST.Logger
|
||||||
PostgREST.MainTx
|
PostgREST.MainTx
|
||||||
@@ -82,7 +81,6 @@ library
|
|||||||
PostgREST.Plan
|
PostgREST.Plan
|
||||||
PostgREST.Plan.CallPlan
|
PostgREST.Plan.CallPlan
|
||||||
PostgREST.Plan.MutatePlan
|
PostgREST.Plan.MutatePlan
|
||||||
PostgREST.Plan.Negotiate
|
|
||||||
PostgREST.Plan.ReadPlan
|
PostgREST.Plan.ReadPlan
|
||||||
PostgREST.Plan.Types
|
PostgREST.Plan.Types
|
||||||
PostgREST.RangeQuery
|
PostgREST.RangeQuery
|
||||||
@@ -98,9 +96,9 @@ library
|
|||||||
PostgREST.Response.Performance
|
PostgREST.Response.Performance
|
||||||
PostgREST.TimeIt
|
PostgREST.TimeIt
|
||||||
PostgREST.Version
|
PostgREST.Version
|
||||||
build-depends: base >= 4.9 && < 4.22
|
build-depends: base >= 4.9 && < 4.20
|
||||||
, HTTP >= 4000.3.7 && < 4000.5
|
, HTTP >= 4000.3.7 && < 4000.5
|
||||||
, Ranged-sets >= 0.3 && < 0.6
|
, Ranged-sets >= 0.3 && < 0.5
|
||||||
, aeson >= 2.0.3 && < 2.3
|
, aeson >= 2.0.3 && < 2.3
|
||||||
, auto-update >= 0.1.4 && < 0.3
|
, auto-update >= 0.1.4 && < 0.3
|
||||||
, base64-bytestring >= 1 && < 1.3
|
, base64-bytestring >= 1 && < 1.3
|
||||||
@@ -108,20 +106,17 @@ library
|
|||||||
, case-insensitive >= 1.2 && < 1.3
|
, case-insensitive >= 1.2 && < 1.3
|
||||||
, cassava >= 0.4.5 && < 0.6
|
, cassava >= 0.4.5 && < 0.6
|
||||||
, configurator-pg >= 0.2.11 && < 0.3
|
, configurator-pg >= 0.2.11 && < 0.3
|
||||||
, containers >= 0.5.7 && < 0.8
|
, containers >= 0.5.7 && < 0.7
|
||||||
, cookie >= 0.4.2 && < 0.6
|
, cookie >= 0.4.2 && < 0.6
|
||||||
-- crypton 1.1.0 moved from `memory` to `ram`, which jose-jwt fails to build with right now.
|
|
||||||
-- should be possible to remove this once jose-jwt had a new release.
|
|
||||||
, crypton < 1.1.0
|
|
||||||
, directory >= 1.2.6 && < 1.4
|
, directory >= 1.2.6 && < 1.4
|
||||||
, either >= 4.4.1 && < 5.1
|
, either >= 4.4.1 && < 5.1
|
||||||
, extra >= 1.7.0 && < 2.0
|
, extra >= 1.7.0 && < 2.0
|
||||||
, fuzzyset >= 0.2.4 && < 0.3
|
, fuzzyset >= 0.2.4 && < 0.3
|
||||||
, hasql >= 1.9 && <= 1.9.3.1
|
, hasql >= 1.6.1.1 && < 1.7
|
||||||
, hasql-dynamic-statements >= 0.3.1 && <= 0.3.1.8
|
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||||
, hasql-notifications >= 0.2.4.0 && < 0.3
|
, hasql-notifications >= 0.2.2.2 && < 0.2.3
|
||||||
, hasql-pool >= 1.1 && <= 1.3.0.4
|
, hasql-pool >= 1.0.1 && < 1.1
|
||||||
, hasql-transaction >= 1.0.1 && <= 1.2.1
|
, hasql-transaction >= 1.0.1 && < 1.2
|
||||||
, http-client >= 0.7.19 && < 0.8
|
, http-client >= 0.7.19 && < 0.8
|
||||||
, http-types >= 0.12.2 && < 0.13
|
, http-types >= 0.12.2 && < 0.13
|
||||||
, insert-ordered-containers >= 0.2.2 && < 0.3
|
, insert-ordered-containers >= 0.2.2 && < 0.3
|
||||||
@@ -134,16 +129,17 @@ library
|
|||||||
, network-uri >= 2.6.1 && < 2.8
|
, network-uri >= 2.6.1 && < 2.8
|
||||||
, optparse-applicative >= 0.13 && < 0.19
|
, optparse-applicative >= 0.13 && < 0.19
|
||||||
, parsec >= 3.1.11 && < 3.2
|
, parsec >= 3.1.11 && < 3.2
|
||||||
|
-- Technically unused, can be removed after updating to hasql >= 1.7
|
||||||
, postgresql-libpq >= 0.10
|
, postgresql-libpq >= 0.10
|
||||||
, prometheus-client >= 1.1.1 && < 1.2.0
|
, prometheus-client >= 1.1.1 && < 1.2.0
|
||||||
, protolude
|
, protolude >= 0.3.1 && < 0.4
|
||||||
, regex-tdfa >= 1.2.2 && < 1.4
|
, regex-tdfa >= 1.2.2 && < 1.4
|
||||||
, retry >= 0.7.4 && < 0.10
|
, retry >= 0.7.4 && < 0.10
|
||||||
, scientific >= 0.3.4 && < 0.4
|
, scientific >= 0.3.4 && < 0.4
|
||||||
, streaming-commons >= 0.2.3.1 && < 0.3
|
, streaming-commons >= 0.2.3.1 && < 0.3
|
||||||
, swagger2 >= 2.4 && < 2.9
|
, swagger2 >= 2.4 && < 2.9
|
||||||
, text >= 1.2.2 && < 2.2
|
, text >= 1.2.2 && < 2.2
|
||||||
, time >= 1.6 && < 1.15
|
, time >= 1.6 && < 1.13
|
||||||
, unordered-containers >= 0.2.8 && < 0.3
|
, unordered-containers >= 0.2.8 && < 0.3
|
||||||
, unix-compat >= 0.5.4 && < 0.8
|
, unix-compat >= 0.5.4 && < 0.8
|
||||||
, vault >= 0.3.1.5 && < 0.4
|
, vault >= 0.3.1.5 && < 0.4
|
||||||
@@ -156,11 +152,12 @@ library
|
|||||||
-- for unix sockets; this is tested in test/io/test_io.py. See
|
-- for unix sockets; this is tested in test/io/test_io.py. See
|
||||||
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
|
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
|
||||||
, wai-logger >= 2.4.0
|
, wai-logger >= 2.4.0
|
||||||
, warp >= 3.4.13 && < 3.5
|
, warp >= 3.3.19 && < 3.5
|
||||||
, stm >= 2.5 && < 3
|
, stm >= 2.5 && < 3
|
||||||
, stm-hamt >= 1.2 && < 2
|
, stm-hamt >= 1.2 && < 2
|
||||||
, focus >= 1.0 && < 2
|
, focus >= 1.0 && < 2
|
||||||
, some >= 1.0.4.1 && < 2
|
, some >= 1.0.4.1 && < 2
|
||||||
|
, uuid >= 1.3 && < 2
|
||||||
-- -fno-spec-constr may help keep compile time memory use in check,
|
-- -fno-spec-constr may help keep compile time memory use in check,
|
||||||
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
||||||
-- -optP-Wno-nonportable-include-path
|
-- -optP-Wno-nonportable-include-path
|
||||||
@@ -180,64 +177,16 @@ library
|
|||||||
build-depends:
|
build-depends:
|
||||||
unix
|
unix
|
||||||
|
|
||||||
library protolude
|
|
||||||
visibility: private
|
|
||||||
default-language: Haskell2010
|
|
||||||
default-extensions: NoImplicitPrelude
|
|
||||||
FlexibleContexts
|
|
||||||
MultiParamTypeClasses
|
|
||||||
OverloadedStrings
|
|
||||||
hs-source-dirs: src/protolude
|
|
||||||
exposed-modules: Protolude
|
|
||||||
Protolude.Applicative
|
|
||||||
Protolude.Base
|
|
||||||
Protolude.Bifunctor
|
|
||||||
Protolude.Bool
|
|
||||||
Protolude.CallStack
|
|
||||||
Protolude.Conv
|
|
||||||
Protolude.ConvertText
|
|
||||||
Protolude.Debug
|
|
||||||
Protolude.Either
|
|
||||||
Protolude.Error
|
|
||||||
Protolude.Exceptions
|
|
||||||
Protolude.Functor
|
|
||||||
Protolude.List
|
|
||||||
Protolude.Monad
|
|
||||||
Protolude.Panic
|
|
||||||
Protolude.Partial
|
|
||||||
Protolude.Safe
|
|
||||||
Protolude.Semiring
|
|
||||||
Protolude.Show
|
|
||||||
Protolude.Unsafe
|
|
||||||
build-depends: array >= 0.4 && < 0.6
|
|
||||||
, async >= 2.0 && < 2.3
|
|
||||||
, base >= 4.6 && < 4.22
|
|
||||||
, bytestring >= 0.10.8 && < 0.13
|
|
||||||
, containers >= 0.5.7 && < 0.8
|
|
||||||
, deepseq >= 1.3 && < 1.6
|
|
||||||
, ghc-prim >= 0.3 && < 0.14
|
|
||||||
, hashable >= 1.2 && < 1.6
|
|
||||||
, mtl >= 2.1 && < 2.4
|
|
||||||
, mtl-compat >= 0.2 && < 0.3
|
|
||||||
, stm >= 2.5 && < 3
|
|
||||||
, text >= 1.2.2 && < 2.2
|
|
||||||
, transformers >= 0.2 && < 0.7
|
|
||||||
, transformers-compat >= 0.4 && < 0.8
|
|
||||||
-- Protolude has some partial functions, so
|
|
||||||
-- it is fine to disable that specific warning
|
|
||||||
ghc-options: -Werror -Wall -fwarn-identities -Wno-x-partial
|
|
||||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
|
||||||
|
|
||||||
executable postgrest
|
executable postgrest
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
default-extensions: OverloadedStrings
|
default-extensions: OverloadedStrings
|
||||||
NoImplicitPrelude
|
NoImplicitPrelude
|
||||||
hs-source-dirs: main
|
hs-source-dirs: main
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
build-depends: base >= 4.9 && < 4.22
|
build-depends: base >= 4.9 && < 4.20
|
||||||
, containers >= 0.5.7 && < 0.8
|
, containers >= 0.5.7 && < 0.7
|
||||||
, postgrest
|
, postgrest
|
||||||
, protolude
|
, protolude >= 0.3.1 && < 0.4
|
||||||
ghc-options: -threaded -rtsopts "-with-rtsopts=-N -I0 -qg"
|
ghc-options: -threaded -rtsopts "-with-rtsopts=-N -I0 -qg"
|
||||||
-O2 -Werror -Wall -fwarn-identities
|
-O2 -Werror -Wall -fwarn-identities
|
||||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||||
@@ -292,9 +241,7 @@ test-suite spec
|
|||||||
Feature.Query.PgSafeUpdateSpec
|
Feature.Query.PgSafeUpdateSpec
|
||||||
Feature.Query.PlanSpec
|
Feature.Query.PlanSpec
|
||||||
Feature.Query.PostGISSpec
|
Feature.Query.PostGISSpec
|
||||||
Feature.Query.Preferences.HandlingSpec
|
Feature.Query.PreferencesSpec
|
||||||
Feature.Query.Preferences.MaxAffectedSpec
|
|
||||||
Feature.Query.Preferences.TimezoneSpec
|
|
||||||
Feature.Query.QueryLimitedSpec
|
Feature.Query.QueryLimitedSpec
|
||||||
Feature.Query.QuerySpec
|
Feature.Query.QuerySpec
|
||||||
Feature.Query.RangeSpec
|
Feature.Query.RangeSpec
|
||||||
@@ -310,16 +257,16 @@ test-suite spec
|
|||||||
Feature.RollbackSpec
|
Feature.RollbackSpec
|
||||||
Feature.RpcPreRequestGucsSpec
|
Feature.RpcPreRequestGucsSpec
|
||||||
SpecHelper
|
SpecHelper
|
||||||
build-depends: base >= 4.9 && < 4.22
|
build-depends: base >= 4.9 && < 4.20
|
||||||
, aeson >= 2.0.3 && < 2.3
|
, aeson >= 2.0.3 && < 2.3
|
||||||
, aeson-qq >= 0.8.1 && < 0.9
|
, aeson-qq >= 0.8.1 && < 0.9
|
||||||
, async >= 2.1.1 && < 2.3
|
, async >= 2.1.1 && < 2.3
|
||||||
, base64-bytestring >= 1 && < 1.3
|
, base64-bytestring >= 1 && < 1.3
|
||||||
, bytestring >= 0.10.8 && < 0.13
|
, bytestring >= 0.10.8 && < 0.13
|
||||||
, case-insensitive >= 1.2 && < 1.3
|
, case-insensitive >= 1.2 && < 1.3
|
||||||
, containers >= 0.5.7 && < 0.8
|
, containers >= 0.5.7 && < 0.7
|
||||||
, hasql-pool >= 1.0.1 && <= 1.3.0.4
|
, hasql-pool >= 1.0.1 && < 1.1
|
||||||
, hasql-transaction >= 1.0.1 && <= 1.2.1
|
, hasql-transaction >= 1.0.1 && < 1.2
|
||||||
, heredoc >= 0.2 && < 0.3
|
, heredoc >= 0.2 && < 0.3
|
||||||
, hspec >= 2.3 && < 2.12
|
, hspec >= 2.3 && < 2.12
|
||||||
, hspec-expectations >= 0.8.4 && < 0.9
|
, hspec-expectations >= 0.8.4 && < 0.9
|
||||||
@@ -333,7 +280,7 @@ test-suite spec
|
|||||||
, postgrest
|
, postgrest
|
||||||
, process >= 1.4.2 && < 1.7
|
, process >= 1.4.2 && < 1.7
|
||||||
, prometheus-client >= 1.1.1 && < 1.2.0
|
, prometheus-client >= 1.1.1 && < 1.2.0
|
||||||
, protolude
|
, protolude >= 0.3.1 && < 0.4
|
||||||
, regex-tdfa >= 1.2.2 && < 1.4
|
, regex-tdfa >= 1.2.2 && < 1.4
|
||||||
, scientific >= 0.3.4 && < 0.4
|
, scientific >= 0.3.4 && < 0.4
|
||||||
, text >= 1.2.2 && < 2.2
|
, text >= 1.2.2 && < 2.2
|
||||||
@@ -359,11 +306,11 @@ test-suite observability
|
|||||||
Observation.JwtCache
|
Observation.JwtCache
|
||||||
Observation.MetricsSpec
|
Observation.MetricsSpec
|
||||||
Observation.SchemaCacheSpec
|
Observation.SchemaCacheSpec
|
||||||
build-depends: base >= 4.9 && < 4.22
|
build-depends: base >= 4.9 && < 4.20
|
||||||
, base64-bytestring >= 1 && < 1.3
|
, base64-bytestring >= 1 && < 1.3
|
||||||
, bytestring >= 0.10.8 && < 0.13
|
, bytestring >= 0.10.8 && < 0.13
|
||||||
, hasql-pool >= 1.0.1 && <= 1.3.0.4
|
, hasql-pool >= 1.0.1 && < 1.1
|
||||||
, hasql-transaction >= 1.0.1 && <= 1.2.1
|
, hasql-transaction >= 1.0.1 && < 1.2
|
||||||
, hspec >= 2.3 && < 2.12
|
, hspec >= 2.3 && < 2.12
|
||||||
, hspec-expectations >= 0.8.4 && < 0.9
|
, hspec-expectations >= 0.8.4 && < 0.9
|
||||||
, hspec-wai >= 0.10 && < 0.12
|
, hspec-wai >= 0.10 && < 0.12
|
||||||
@@ -372,7 +319,7 @@ test-suite observability
|
|||||||
, jose-jwt >= 0.9.6 && < 0.11
|
, jose-jwt >= 0.9.6 && < 0.11
|
||||||
, postgrest
|
, postgrest
|
||||||
, prometheus-client >= 1.1.1 && < 1.2.0
|
, prometheus-client >= 1.1.1 && < 1.2.0
|
||||||
, protolude
|
, protolude >= 0.3.1 && < 0.4
|
||||||
, text >= 1.2.2 && < 2.2
|
, text >= 1.2.2 && < 2.2
|
||||||
, wai >= 3.2.1 && < 3.3
|
, wai >= 3.2.1 && < 3.3
|
||||||
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
|
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
|
||||||
@@ -388,10 +335,10 @@ test-suite doctests
|
|||||||
NoImplicitPrelude
|
NoImplicitPrelude
|
||||||
hs-source-dirs: test/doc
|
hs-source-dirs: test/doc
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
build-depends: base >= 4.9 && < 4.22
|
build-depends: base >= 4.9 && < 4.20
|
||||||
, doctest >= 0.8
|
, doctest >= 0.8
|
||||||
, postgrest
|
, postgrest
|
||||||
, pretty-simple
|
, pretty-simple
|
||||||
, protolude
|
, protolude >= 0.3.1 && < 0.4
|
||||||
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
|
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
|
||||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||||
|
|||||||
@@ -7,9 +7,11 @@
|
|||||||
# We highly recommend that use the PostgREST binary cache by installing cachix
|
# We highly recommend that use the PostgREST binary cache by installing cachix
|
||||||
# (https://app.cachix.org/) and running `cachix use postgrest`.
|
# (https://app.cachix.org/) and running `cachix use postgrest`.
|
||||||
{ docker ? false
|
{ docker ? false
|
||||||
, postgrest ? import ./default.nix { }
|
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
|
postgrest =
|
||||||
|
import ./default.nix { };
|
||||||
|
|
||||||
inherit (postgrest) pkgs;
|
inherit (postgrest) pkgs;
|
||||||
|
|
||||||
inherit (pkgs) lib;
|
inherit (pkgs) lib;
|
||||||
@@ -35,7 +37,10 @@ lib.overrideDerivation postgrest.env (
|
|||||||
buildInputs =
|
buildInputs =
|
||||||
base.buildInputs ++ [
|
base.buildInputs ++ [
|
||||||
pkgs.cabal-install
|
pkgs.cabal-install
|
||||||
|
pkgs.cabal2nix
|
||||||
|
pkgs.git
|
||||||
pkgs.postgresql
|
pkgs.postgresql
|
||||||
|
pkgs.update-nix-fetchgit
|
||||||
postgrest.hsie.bin
|
postgrest.hsie.bin
|
||||||
]
|
]
|
||||||
++ toolboxes;
|
++ toolboxes;
|
||||||
@@ -44,10 +49,6 @@ lib.overrideDerivation postgrest.env (
|
|||||||
''
|
''
|
||||||
export HISTFILE=.history
|
export HISTFILE=.history
|
||||||
|
|
||||||
# Bypass proxy for all hosts, it prevents HTTP client failures used in test
|
|
||||||
# suites. See: https://github.com/PostgREST/postgrest/issues/4633 for more info
|
|
||||||
export NO_PROXY=*
|
|
||||||
|
|
||||||
source ${pkgs.bash-completion}/etc/profile.d/bash_completion.sh
|
source ${pkgs.bash-completion}/etc/profile.d/bash_completion.sh
|
||||||
source ${pkgs.git}/share/git/contrib/completion/git-completion.bash
|
source ${pkgs.git}/share/git/contrib/completion/git-completion.bash
|
||||||
source ${postgrest.hsie.bash-completion}
|
source ${postgrest.hsie.bash-completion}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ data ApiRequest = ApiRequest {
|
|||||||
, iPayload :: Maybe Payload -- ^ Data sent by client and used for mutation actions
|
, iPayload :: Maybe Payload -- ^ Data sent by client and used for mutation actions
|
||||||
, iPreferences :: Preferences.Preferences -- ^ Prefer header values
|
, iPreferences :: Preferences.Preferences -- ^ Prefer header values
|
||||||
, iQueryParams :: QueryParams.QueryParams
|
, iQueryParams :: QueryParams.QueryParams
|
||||||
, iColumns :: S.Set FieldName -- ^ parsed columns from &columns parameter and payload
|
, iColumns :: S.Set FieldName -- ^ parsed colums from &columns parameter and payload
|
||||||
, iHeaders :: [(ByteString, ByteString)] -- ^ HTTP request headers
|
, iHeaders :: [(ByteString, ByteString)] -- ^ HTTP request headers
|
||||||
, iCookies :: [(ByteString, ByteString)] -- ^ Request Cookies
|
, iCookies :: [(ByteString, ByteString)] -- ^ Request Cookies
|
||||||
, iPath :: ByteString -- ^ Raw request path
|
, iPath :: ByteString -- ^ Raw request path
|
||||||
|
|||||||
+37
-51
@@ -22,7 +22,6 @@ import GHC.IO.Exception (IOErrorType (..))
|
|||||||
import System.IO.Error (ioeGetErrorType)
|
import System.IO.Error (ioeGetErrorType)
|
||||||
|
|
||||||
import Control.Monad.Except (liftEither)
|
import Control.Monad.Except (liftEither)
|
||||||
import Control.Monad.Extra (whenJust)
|
|
||||||
import Data.Either.Combinators (mapLeft, whenLeft)
|
import Data.Either.Combinators (mapLeft, whenLeft)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.String (IsString (..))
|
import Data.String (IsString (..))
|
||||||
@@ -61,16 +60,15 @@ import PostgREST.SchemaCache (SchemaCache (..))
|
|||||||
import PostgREST.TimeIt (timeItT)
|
import PostgREST.TimeIt (timeItT)
|
||||||
import PostgREST.Version (docsVersion, prettyVersion)
|
import PostgREST.Version (docsVersion, prettyVersion)
|
||||||
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.List as L
|
import qualified Data.List as L
|
||||||
import Data.Streaming.Network (bindPortTCP,
|
import Data.Streaming.Network (bindPortTCP,
|
||||||
bindRandomPortTCP)
|
bindRandomPortTCP)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Network.HTTP.Types as HTTP
|
import qualified Network.HTTP.Types as HTTP
|
||||||
import qualified Network.HTTP.Types.Header as HTTP (hVary)
|
import qualified Network.Socket as NS
|
||||||
import qualified Network.Socket as NS
|
import PostgREST.Unix (createAndBindDomainSocket)
|
||||||
import PostgREST.Unix (createAndBindDomainSocket)
|
import Protolude hiding (Handler)
|
||||||
import Protolude hiding (Handler)
|
|
||||||
|
|
||||||
type Handler = ExceptT Error
|
type Handler = ExceptT Error
|
||||||
|
|
||||||
@@ -80,10 +78,8 @@ run appState = do
|
|||||||
|
|
||||||
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
|
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
|
||||||
(mainSocket, adminSocket) <- initSockets conf
|
(mainSocket, adminSocket) <- initSockets conf
|
||||||
let closeSockets = do
|
|
||||||
whenJust adminSocket NS.close
|
Unix.installSignalHandlers observer (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
|
||||||
NS.close mainSocket
|
|
||||||
Unix.installSignalHandlers observer closeSockets (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
|
|
||||||
|
|
||||||
Listener.runListener appState
|
Listener.runListener appState
|
||||||
|
|
||||||
@@ -130,31 +126,30 @@ postgrest logLevel appState connWorker =
|
|||||||
Logger.middleware logLevel Auth.getRole $
|
Logger.middleware logLevel Auth.getRole $
|
||||||
-- fromJust can be used, because the auth middleware will **always** add
|
-- fromJust can be used, because the auth middleware will **always** add
|
||||||
-- some AuthResult to the vault.
|
-- some AuthResult to the vault.
|
||||||
\req respond -> do
|
\req respond -> case fromJust $ Auth.getResult req of
|
||||||
appConf@AppConfig{..} <- AppState.getConfig appState -- the config must be read again because it can reload
|
Left err -> respond $ Error.errorResponseFor err
|
||||||
case fromJust $ Auth.getResult req of
|
Right authResult -> do
|
||||||
Left err -> respond $ Error.errorResponseFor configClientErrorVerbosity err
|
appConf <- AppState.getConfig appState -- the config must be read again because it can reload
|
||||||
Right authResult -> do
|
maybeSchemaCache <- AppState.getSchemaCache appState
|
||||||
maybeSchemaCache <- AppState.getSchemaCache appState
|
|
||||||
|
|
||||||
let
|
let
|
||||||
eitherResponse :: IO (Either Error Wai.Response)
|
eitherResponse :: IO (Either Error Wai.Response)
|
||||||
eitherResponse =
|
eitherResponse =
|
||||||
runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req
|
runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req
|
||||||
|
|
||||||
response <- either (Error.errorResponseFor configClientErrorVerbosity) identity <$> eitherResponse
|
response <- either Error.errorResponseFor identity <$> eitherResponse
|
||||||
-- Launch the connWorker when the connection is down. The postgrest
|
-- Launch the connWorker when the connection is down. The postgrest
|
||||||
-- function can respond successfully (with a stale schema cache) before
|
-- function can respond successfully (with a stale schema cache) before
|
||||||
-- the connWorker is done. However, when there's an empty schema cache
|
-- the connWorker is done. However, when there's an empty schema cache
|
||||||
-- postgrest responds with the error `PGRST002`; this means that the schema
|
-- postgrest responds with the error `PGRST002`; this means that the schema
|
||||||
-- cache is still loading, so we don't launch the connWorker here because
|
-- cache is still loading, so we don't launch the connWorker here because
|
||||||
-- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704
|
-- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704
|
||||||
-- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done
|
-- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done
|
||||||
when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker
|
when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker
|
||||||
resp <- do
|
resp <- do
|
||||||
delay <- AppState.getNextDelay appState
|
delay <- AppState.getNextDelay appState
|
||||||
return $ addRetryHint delay response
|
return $ addRetryHint delay response
|
||||||
respond resp
|
respond resp
|
||||||
|
|
||||||
postgrestResponse
|
postgrestResponse
|
||||||
:: AppState.AppState
|
:: AppState.AppState
|
||||||
@@ -180,7 +175,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthRe
|
|||||||
timezones = dbTimezones sCache
|
timezones = dbTimezones sCache
|
||||||
prefs = ApiRequest.userPreferences conf req timezones
|
prefs = ApiRequest.userPreferences conf req timezones
|
||||||
|
|
||||||
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestErr $ ApiRequest.userApiRequest conf prefs req body
|
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestError $ ApiRequest.userApiRequest conf prefs req body
|
||||||
(planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
|
(planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
|
||||||
|
|
||||||
let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest
|
let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest
|
||||||
@@ -201,7 +196,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthRe
|
|||||||
liftEither eitherResp
|
liftEither eitherResp
|
||||||
|
|
||||||
(respTime, resp) <- withTiming $ do
|
(respTime, resp) <- withTiming $ do
|
||||||
let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache
|
let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
|
||||||
status' = either Error.status Response.pgrstStatus response
|
status' = either Error.status Response.pgrstStatus response
|
||||||
|
|
||||||
-- TODO: see above obsQuery, only this obsQuery should remain after refactoring (because the QueryObs depends on the status)
|
-- TODO: see above obsQuery, only this obsQuery should remain after refactoring (because the QueryObs depends on the status)
|
||||||
@@ -212,17 +207,7 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthRe
|
|||||||
|
|
||||||
where
|
where
|
||||||
toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response
|
toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response
|
||||||
toWaiResponse timing (Response.PgrstResponse st hdrs bod) =
|
toWaiResponse timing (Response.PgrstResponse st hdrs bod) = Wai.responseLBS st (hdrs ++ ([serverTimingHeader timing | configServerTimingEnabled])) bod
|
||||||
Wai.responseLBS st (hdrs ++ serverTimingHeaders timing ++ [varyHeader | not $ varyHeaderPresent hdrs]) bod
|
|
||||||
|
|
||||||
serverTimingHeaders :: ServerTiming -> [HTTP.Header]
|
|
||||||
serverTimingHeaders timing = [serverTimingHeader timing | configServerTimingEnabled]
|
|
||||||
|
|
||||||
varyHeader :: HTTP.Header
|
|
||||||
varyHeader = (HTTP.hVary, "Accept, Prefer, Range")
|
|
||||||
|
|
||||||
varyHeaderPresent :: [HTTP.Header] -> Bool
|
|
||||||
varyHeaderPresent = any (\(h, _v) -> h == HTTP.hVary)
|
|
||||||
|
|
||||||
withTiming :: Handler IO a -> Handler IO (Maybe Double, a)
|
withTiming :: Handler IO a -> Handler IO (Maybe Double, a)
|
||||||
withTiming f = if configServerTimingEnabled
|
withTiming f = if configServerTimingEnabled
|
||||||
@@ -286,3 +271,4 @@ initSockets AppConfig{..} = do
|
|||||||
Nothing -> pure Nothing
|
Nothing -> pure Nothing
|
||||||
|
|
||||||
pure (sock, adminSock)
|
pure (sock, adminSock)
|
||||||
|
|
||||||
|
|||||||
+62
-69
@@ -1,7 +1,6 @@
|
|||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
{-# LANGUAGE RecursiveDo #-}
|
|
||||||
|
|
||||||
module PostgREST.AppState
|
module PostgREST.AppState
|
||||||
( AppState
|
( AppState
|
||||||
@@ -46,6 +45,7 @@ import PostgREST.Version (prettyVersion)
|
|||||||
|
|
||||||
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
||||||
updateAction)
|
updateAction)
|
||||||
|
import Control.Debounce
|
||||||
import Control.Retry (RetryPolicy, RetryStatus (..), capDelay,
|
import Control.Retry (RetryPolicy, RetryStatus (..), capDelay,
|
||||||
exponentialBackoff, retrying,
|
exponentialBackoff, retrying,
|
||||||
rsPreviousDelay)
|
rsPreviousDelay)
|
||||||
@@ -55,14 +55,13 @@ import Data.Time.Clock (UTCTime, getCurrentTime)
|
|||||||
|
|
||||||
import PostgREST.Auth.JwtCache (JwtCacheState, update)
|
import PostgREST.Auth.JwtCache (JwtCacheState, update)
|
||||||
import PostgREST.Config (AppConfig (..),
|
import PostgREST.Config (AppConfig (..),
|
||||||
readAppConfig,
|
addFallbackAppName,
|
||||||
toConnectionSettings)
|
readAppConfig)
|
||||||
import PostgREST.Config.Database (queryDbSettings,
|
import PostgREST.Config.Database (queryDbSettings,
|
||||||
queryPgVersion,
|
queryPgVersion,
|
||||||
queryRoleSettings)
|
queryRoleSettings)
|
||||||
import PostgREST.Config.PgVersion (PgVersion (..),
|
import PostgREST.Config.PgVersion (PgVersion (..),
|
||||||
minimumPgVersion)
|
minimumPgVersion)
|
||||||
import PostgREST.Debounce (makeDebouncer)
|
|
||||||
import PostgREST.SchemaCache (SchemaCache (..),
|
import PostgREST.SchemaCache (SchemaCache (..),
|
||||||
querySchemaCache,
|
querySchemaCache,
|
||||||
showSummary)
|
showSummary)
|
||||||
@@ -78,7 +77,7 @@ data AppState = AppState
|
|||||||
-- | Schema cache
|
-- | Schema cache
|
||||||
, stateSchemaCache :: IORef (Maybe SchemaCache)
|
, stateSchemaCache :: IORef (Maybe SchemaCache)
|
||||||
-- | The schema cache status
|
-- | The schema cache status
|
||||||
, stateSCacheStatus :: SchemaCacheStatus
|
, stateSCacheStatus :: IORef SchemaCacheStatus
|
||||||
-- | State of the LISTEN channel
|
-- | State of the LISTEN channel
|
||||||
, stateIsListenerOn :: IORef Bool
|
, stateIsListenerOn :: IORef Bool
|
||||||
-- | starts the connection worker with a debounce
|
-- | starts the connection worker with a debounce
|
||||||
@@ -101,11 +100,11 @@ data AppState = AppState
|
|||||||
, stateMetrics :: Metrics.MetricsState
|
, stateMetrics :: Metrics.MetricsState
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Schema cache status.
|
-- | Schema cache status
|
||||||
-- Empty means pending and full means loaded.
|
data SchemaCacheStatus
|
||||||
newtype SchemaCacheStatus = SchemaCacheStatus
|
= SCLoaded
|
||||||
{ getSCStatusMVar :: MVar ()
|
| SCPending
|
||||||
}
|
deriving Eq
|
||||||
|
|
||||||
init :: AppConfig -> IO AppState
|
init :: AppConfig -> IO AppState
|
||||||
init conf@AppConfig{configLogLevel, configDbPoolSize} = do
|
init conf@AppConfig{configLogLevel, configDbPoolSize} = do
|
||||||
@@ -116,17 +115,17 @@ init conf@AppConfig{configLogLevel, configDbPoolSize} = do
|
|||||||
observer $ AppStartObs prettyVersion
|
observer $ AppStartObs prettyVersion
|
||||||
|
|
||||||
pool <- initPool conf observer
|
pool <- initPool conf observer
|
||||||
initWithPool pool conf loggerState metricsState observer
|
initWithPool pool conf loggerState metricsState observer --{ stateSocketREST = sock, stateSocketAdmin = adminSock}
|
||||||
|
|
||||||
initWithPool :: SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
|
initWithPool :: SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
|
||||||
initWithPool pool conf loggerState metricsState observer = mdo
|
initWithPool pool conf loggerState metricsState observer = do
|
||||||
|
|
||||||
appState <- AppState pool
|
appState <- AppState pool
|
||||||
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
||||||
<*> newIORef Nothing
|
<*> newIORef Nothing
|
||||||
<*> newSchemaCacheStatus
|
<*> newIORef SCPending
|
||||||
<*> newIORef False
|
<*> newIORef False
|
||||||
<*> makeDebouncer (retryingSchemaCacheLoad appState *> threadDelay 100000) -- 100ms cooldown
|
<*> pure (pure ())
|
||||||
<*> newIORef conf
|
<*> newIORef conf
|
||||||
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
|
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
|
||||||
<*> myThreadId
|
<*> myThreadId
|
||||||
@@ -137,53 +136,48 @@ initWithPool pool conf loggerState metricsState observer = mdo
|
|||||||
<*> pure loggerState
|
<*> pure loggerState
|
||||||
<*> pure metricsState
|
<*> pure metricsState
|
||||||
|
|
||||||
return appState
|
deb <-
|
||||||
|
let decisecond = 100000 in
|
||||||
|
mkDebounce defaultDebounceSettings
|
||||||
|
{ debounceAction = retryingSchemaCacheLoad appState
|
||||||
|
, debounceFreq = decisecond
|
||||||
|
, debounceEdge = leadingEdge -- runs the worker at the start and the end
|
||||||
|
}
|
||||||
|
|
||||||
|
return appState { debouncedSCacheLoader = deb}
|
||||||
|
|
||||||
destroy :: AppState -> IO ()
|
destroy :: AppState -> IO ()
|
||||||
destroy = destroyPool
|
destroy = destroyPool
|
||||||
|
|
||||||
initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool
|
initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool
|
||||||
initPool cfg@AppConfig{..} observer = do
|
initPool AppConfig{..} observer = do
|
||||||
SQL.acquire $ SQL.settings
|
SQL.acquire $ SQL.settings
|
||||||
[ SQL.size configDbPoolSize
|
[ SQL.size configDbPoolSize
|
||||||
, SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout
|
, SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout
|
||||||
, SQL.agingTimeout $ fromIntegral configDbPoolMaxLifetime
|
, SQL.agingTimeout $ fromIntegral configDbPoolMaxLifetime
|
||||||
, SQL.idlenessTimeout $ fromIntegral configDbPoolMaxIdletime
|
, SQL.idlenessTimeout $ fromIntegral configDbPoolMaxIdletime
|
||||||
, SQL.staticConnectionSettings $ toConnectionSettings identity cfg
|
, SQL.staticConnectionSettings (toUtf8 $ addFallbackAppName prettyVersion configDbUri)
|
||||||
, SQL.observationHandler $ observer . HasqlPoolObs
|
, SQL.observationHandler $ observer . HasqlPoolObs
|
||||||
]
|
]
|
||||||
|
|
||||||
-- | Run an action with a database connection.
|
-- | Run an action with a database connection.
|
||||||
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
||||||
usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} sess = do
|
usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} sess = do
|
||||||
observer PoolRequest
|
observer PoolRequest
|
||||||
|
|
||||||
res <- SQL.use statePool sess
|
res <- SQL.use statePool sess
|
||||||
|
|
||||||
observer PoolRequestFullfilled
|
observer PoolRequestFullfilled
|
||||||
|
|
||||||
whenLeft res (\case
|
whenLeft res (\case
|
||||||
SQL.AcquisitionTimeoutUsageError ->
|
SQL.AcquisitionTimeoutUsageError ->
|
||||||
observer PoolAcqTimeoutObs
|
observer $ PoolAcqTimeoutObs SQL.AcquisitionTimeoutUsageError
|
||||||
err@(SQL.ConnectionUsageError e) ->
|
err@(SQL.ConnectionUsageError e) ->
|
||||||
let failureMessage = BS.unpack $ fromMaybe mempty e in
|
let failureMessage = BS.unpack $ fromMaybe mempty e in
|
||||||
when (("FATAL: password authentication failed" `isInfixOf` failureMessage) || ("no password supplied" `isInfixOf` failureMessage)) $ do
|
when (("FATAL: password authentication failed" `isInfixOf` failureMessage) || ("no password supplied" `isInfixOf` failureMessage)) $ do
|
||||||
observer $ ExitDBFatalError ServerAuthError err
|
observer $ ExitDBFatalError ServerAuthError err
|
||||||
killThread mainThreadId
|
killThread mainThreadId
|
||||||
err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) ->
|
err@(SQL.SessionUsageError (SQL.QueryError tpl _ (SQL.ResultError resultErr))) -> do
|
||||||
handleResultError err tpl resultErr
|
|
||||||
err@(SQL.SessionUsageError (SQL.PipelineError (SQL.ResultError resultErr))) ->
|
|
||||||
-- Passing the empty template will not work for schema cache queries, see TODO further below.
|
|
||||||
handleResultError err mempty resultErr
|
|
||||||
err@(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) ->
|
|
||||||
-- An error on the client-side, usually indicates problems with connection
|
|
||||||
observer $ QueryErrorCodeHighObs err
|
|
||||||
SQL.SessionUsageError (SQL.PipelineError (SQL.ClientError _)) -> pure ()
|
|
||||||
)
|
|
||||||
|
|
||||||
return res
|
|
||||||
where
|
|
||||||
handleResultError err tpl resultErr = do
|
|
||||||
case resultErr of
|
case resultErr of
|
||||||
SQL.UnexpectedResult{} -> do
|
SQL.UnexpectedResult{} -> do
|
||||||
observer $ ExitDBFatalError ServerPgrstBug err
|
observer $ ExitDBFatalError ServerPgrstBug err
|
||||||
@@ -216,6 +210,12 @@ usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} ses
|
|||||||
SQL.ServerError{} ->
|
SQL.ServerError{} ->
|
||||||
when (Error.status (Error.PgError False err) >= HTTP.status500) $
|
when (Error.status (Error.PgError False err) >= HTTP.status500) $
|
||||||
observer $ QueryErrorCodeHighObs err
|
observer $ QueryErrorCodeHighObs err
|
||||||
|
err@(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) ->
|
||||||
|
-- An error on the client-side, usually indicates problems wth connection
|
||||||
|
observer $ QueryErrorCodeHighObs err
|
||||||
|
)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
-- | Flush the connection pool so that any future use of the pool will
|
-- | Flush the connection pool so that any future use of the pool will
|
||||||
-- use connections freshly established after this call.
|
-- use connections freshly established after this call.
|
||||||
@@ -282,15 +282,18 @@ putIsListenerOn = atomicWriteIORef . stateIsListenerOn
|
|||||||
|
|
||||||
isLoaded :: AppState -> IO Bool
|
isLoaded :: AppState -> IO Bool
|
||||||
isLoaded x = do
|
isLoaded x = do
|
||||||
scacheLoaded <- isSchemaCacheLoaded x
|
scacheStatus <- readIORef $ stateSCacheStatus x
|
||||||
connEstablished <- isConnEstablished x
|
connEstablished <- isConnEstablished x
|
||||||
return $ scacheLoaded && connEstablished
|
return $ scacheStatus == SCLoaded && connEstablished
|
||||||
|
|
||||||
isPending :: AppState -> IO Bool
|
isPending :: AppState -> IO Bool
|
||||||
isPending x = do
|
isPending x = do
|
||||||
scacheLoaded <- isSchemaCacheLoaded x
|
scacheStatus <- readIORef $ stateSCacheStatus x
|
||||||
connEstablished <- isConnEstablished x
|
connEstablished <- isConnEstablished x
|
||||||
return $ not scacheLoaded || not connEstablished
|
return $ scacheStatus == SCPending || not connEstablished
|
||||||
|
|
||||||
|
putSCacheStatus :: AppState -> SchemaCacheStatus -> IO ()
|
||||||
|
putSCacheStatus = atomicWriteIORef . stateSCacheStatus
|
||||||
|
|
||||||
getObserver :: AppState -> ObservationHandler
|
getObserver :: AppState -> ObservationHandler
|
||||||
getObserver = stateObserver
|
getObserver = stateObserver
|
||||||
@@ -308,6 +311,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
|||||||
when (rsIterNumber > 0) $ do
|
when (rsIterNumber > 0) $ do
|
||||||
let delay = fromMaybe 0 rsPreviousDelay `div` oneSecondInUs
|
let delay = fromMaybe 0 rsPreviousDelay `div` oneSecondInUs
|
||||||
observer $ ConnectionRetryObs delay
|
observer $ ConnectionRetryObs delay
|
||||||
|
putNextListenerDelay appState delay
|
||||||
|
|
||||||
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
|
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
|
||||||
)
|
)
|
||||||
@@ -315,7 +319,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
|||||||
qPgVersion :: IO (Maybe PgVersion)
|
qPgVersion :: IO (Maybe PgVersion)
|
||||||
qPgVersion = do
|
qPgVersion = do
|
||||||
AppConfig{..} <- getConfig appState
|
AppConfig{..} <- getConfig appState
|
||||||
pgVersion <- usePool appState queryPgVersion
|
pgVersion <- usePool appState (queryPgVersion False) -- No need to prepare the query here, as the connection might not be established
|
||||||
case pgVersion of
|
case pgVersion of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
observer $ QueryPgVersionError e
|
observer $ QueryPgVersionError e
|
||||||
@@ -343,27 +347,28 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
|||||||
qSchemaCache = do
|
qSchemaCache = do
|
||||||
conf@AppConfig{..} <- getConfig appState
|
conf@AppConfig{..} <- getConfig appState
|
||||||
(resultTime, result) <-
|
(resultTime, result) <-
|
||||||
timeItT $ usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
||||||
|
timeItT $ usePool appState (transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
||||||
case result of
|
case result of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
markSchemaCachePending appState
|
putSCacheStatus appState SCPending
|
||||||
putSchemaCache appState Nothing
|
putSchemaCache appState Nothing
|
||||||
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
|
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
|
||||||
return Nothing
|
return Nothing
|
||||||
|
|
||||||
Right sCache -> do
|
Right sCache -> do
|
||||||
-- IMPORTANT: While the pending schema cache state starts from running the above querySchemaCache, only at this stage we block API requests due to the usage of an
|
-- IMPORTANT: While the pending schema cache state starts from running the above querySchemaCache, only at this stage we block API requests due to the usage of an
|
||||||
-- IORef on putSchemaCache. This is why schema cache status is marked as pending here to signal the Admin server (using isPending) that we're on a recovery state.
|
-- IORef on putSchemaCache. This is why SCacheStatus is put at SCPending here to signal the Admin server (using isPending) that we're on a recovery state.
|
||||||
markSchemaCachePending appState
|
putSCacheStatus appState SCPending
|
||||||
putSchemaCache appState $ Just sCache
|
putSchemaCache appState $ Just sCache
|
||||||
(loadTime, summary) <- timeItT (evaluate $ showSummary sCache)
|
|
||||||
-- Flush the pool after loading the schema cache to reset any stale session cache entries
|
-- Flush the pool after loading the schema cache to reset any stale session cache entries
|
||||||
-- We do it after successfully querying the schema cache (because this can fail and during retries we would flush the pool repeatedly unnecessarily)
|
-- We do it after successfully querying the schema cache (because this can fail and during retries we would flush the pool repeatedly unnecessarily)
|
||||||
-- and after marking sCacheStatus as pending,
|
-- and after marking sCacheStatus as pending,
|
||||||
flushPool appState
|
flushPool appState
|
||||||
observer $ SchemaCacheQueriedObs resultTime $ dbQueryTimings sCache
|
observer $ SchemaCacheQueriedObs resultTime
|
||||||
observer $ SchemaCacheLoadedObs loadTime summary
|
(t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache
|
||||||
markSchemaCacheLoaded appState
|
observer $ SchemaCacheLoadedObs t
|
||||||
|
putSCacheStatus appState SCLoaded
|
||||||
return $ Just sCache
|
return $ Just sCache
|
||||||
|
|
||||||
shouldRetry :: RetryStatus -> (Maybe PgVersion, Maybe SchemaCache) -> IO Bool
|
shouldRetry :: RetryStatus -> (Maybe PgVersion, Maybe SchemaCache) -> IO Bool
|
||||||
@@ -379,18 +384,6 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
|||||||
|
|
||||||
oneSecondInUs = 1000000 -- one second in microseconds
|
oneSecondInUs = 1000000 -- one second in microseconds
|
||||||
|
|
||||||
newSchemaCacheStatus :: IO SchemaCacheStatus
|
|
||||||
newSchemaCacheStatus = SchemaCacheStatus <$> newEmptyMVar
|
|
||||||
|
|
||||||
markSchemaCachePending :: AppState -> IO ()
|
|
||||||
markSchemaCachePending = void . tryTakeMVar . getSCStatusMVar . stateSCacheStatus
|
|
||||||
|
|
||||||
markSchemaCacheLoaded :: AppState -> IO ()
|
|
||||||
markSchemaCacheLoaded = void . (`tryPutMVar` ()) . getSCStatusMVar . stateSCacheStatus
|
|
||||||
|
|
||||||
isSchemaCacheLoaded :: AppState -> IO Bool
|
|
||||||
isSchemaCacheLoaded = fmap not . isEmptyMVar . getSCStatusMVar . stateSCacheStatus
|
|
||||||
|
|
||||||
-- | Reads the in-db config and reads the config file again
|
-- | Reads the in-db config and reads the config file again
|
||||||
-- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue.
|
-- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue.
|
||||||
readInDbConfig :: Bool -> AppState -> IO ()
|
readInDbConfig :: Bool -> AppState -> IO ()
|
||||||
@@ -399,7 +392,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
|
|||||||
pgVer <- getPgVersion appState
|
pgVer <- getPgVersion appState
|
||||||
dbSettings <-
|
dbSettings <-
|
||||||
if configDbConfig conf then do
|
if configDbConfig conf then do
|
||||||
qDbSettings <- usePool appState (queryDbSettings (quoteQi <$> configDbPreConfig conf))
|
qDbSettings <- usePool appState (queryDbSettings (quoteQi <$> configDbPreConfig conf) (configDbPreparedStatements conf))
|
||||||
case qDbSettings of
|
case qDbSettings of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
observer $ ConfigReadErrorObs e
|
observer $ ConfigReadErrorObs e
|
||||||
@@ -409,7 +402,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
|
|||||||
pure mempty
|
pure mempty
|
||||||
(roleSettings, roleIsolationLvl) <-
|
(roleSettings, roleIsolationLvl) <-
|
||||||
if configDbConfig conf then do
|
if configDbConfig conf then do
|
||||||
rSettings <- usePool appState (queryRoleSettings pgVer)
|
rSettings <- usePool appState (queryRoleSettings pgVer (configDbPreparedStatements conf))
|
||||||
case rSettings of
|
case rSettings of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
observer $ QueryRoleSettingsErrorObs e
|
observer $ QueryRoleSettingsErrorObs e
|
||||||
|
|||||||
@@ -16,10 +16,14 @@ module PostgREST.Auth.Jwt
|
|||||||
, parseClaims) where
|
, parseClaims) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
|
import qualified Data.Aeson.Key as K
|
||||||
|
import qualified Data.Aeson.KeyMap as KM
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.ByteString.Internal as BS
|
import qualified Data.ByteString.Internal as BS
|
||||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||||
import qualified Data.Scientific as Sci
|
import qualified Data.Scientific as Sci
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Vector as V
|
||||||
import qualified Jose.Jwk as JWT
|
import qualified Jose.Jwk as JWT
|
||||||
import qualified Jose.Jwt as JWT
|
import qualified Jose.Jwt as JWT
|
||||||
|
|
||||||
@@ -29,11 +33,12 @@ import Data.Text ()
|
|||||||
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
|
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
|
||||||
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
|
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
|
||||||
|
|
||||||
import PostgREST.Auth.Types (AuthResult (..))
|
import PostgREST.Auth.Types (AuthResult (..))
|
||||||
import PostgREST.Config (AppConfig (..), audMatchesCfg)
|
import PostgREST.Config (AppConfig (..), FilterExp (..), JSPath,
|
||||||
import PostgREST.Config.JSPath (walkJSPath)
|
JSPathExp (..), audMatchesCfg)
|
||||||
import PostgREST.Error (Error (..), JwtClaimsError (..),
|
import PostgREST.Error (Error (..),
|
||||||
JwtDecodeError (..), JwtError (..))
|
JwtClaimsError (AudClaimNotStringOrArray, ExpClaimNotNumber, IatClaimNotNumber, JWTExpired, JWTIssuedAtFuture, JWTNotInAudience, JWTNotYetValid, NbfClaimNotNumber, ParsingClaimsFailed),
|
||||||
|
JwtDecodeError (..), JwtError (..))
|
||||||
|
|
||||||
import Data.Aeson ((.:?))
|
import Data.Aeson ((.:?))
|
||||||
import Data.Aeson.Types (parseMaybe)
|
import Data.Aeson.Types (parseMaybe)
|
||||||
@@ -90,10 +95,13 @@ checkForErrors time audMatches = mconcat
|
|||||||
parseToken :: (MonadError Error m, MonadIO m) => JwkSet -> ByteString -> m JWT.JwtContent
|
parseToken :: (MonadError Error m, MonadIO m) => JwkSet -> ByteString -> m JWT.JwtContent
|
||||||
parseToken _ "" = throwError $ JwtErr $ JwtDecodeErr EmptyAuthHeader
|
parseToken _ "" = throwError $ JwtErr $ JwtDecodeErr EmptyAuthHeader
|
||||||
parseToken secret tkn = do
|
parseToken secret tkn = do
|
||||||
|
-- secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
|
||||||
tknWith3Parts <- hasThreeParts tkn
|
tknWith3Parts <- hasThreeParts tkn
|
||||||
eitherContent <- liftIO $ JWT.decode (JWT.keys secret) Nothing tknWith3Parts
|
eitherContent <- liftIO $ JWT.decode (JWT.keys secret) Nothing tknWith3Parts
|
||||||
liftEither . mapLeft (JwtErr . jwtDecodeError) $ eitherContent
|
liftEither . mapLeft (JwtErr . jwtDecodeError) $ eitherContent
|
||||||
|
--liftEither $ mapLeft JwtErr $ verifyClaims content
|
||||||
where
|
where
|
||||||
|
--hasThreeParts :: ByteString -> Either Error ByteString
|
||||||
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
|
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
|
||||||
3 -> pure token
|
3 -> pure token
|
||||||
n -> throwError $ JwtErr $ JwtDecodeErr $ UnexpectedParts n
|
n -> throwError $ JwtErr $ JwtDecodeErr $ UnexpectedParts n
|
||||||
@@ -116,10 +124,28 @@ parseClaims cfg@AppConfig{configJwtRoleClaimKey, configDbAnonRole} time mclaims
|
|||||||
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
|
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
|
||||||
unquoted <$> walkJSPath (Just $ JSON.Object mclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
unquoted <$> walkJSPath (Just $ JSON.Object mclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
||||||
pure AuthResult
|
pure AuthResult
|
||||||
{ authClaims = mclaims
|
{ authClaims = mclaims & KM.insert "role" (JSON.toJSON $ decodeUtf8 role)
|
||||||
, authRole = role
|
, authRole = role
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
|
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
|
||||||
|
walkJSPath x [] = x
|
||||||
|
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest
|
||||||
|
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
|
||||||
|
walkJSPath (Just (JSON.Array ar)) [JSPFilter (EqualsCond txt)] = findFirstMatch (==) txt ar
|
||||||
|
walkJSPath (Just (JSON.Array ar)) [JSPFilter (NotEqualsCond txt)] = findFirstMatch (/=) txt ar
|
||||||
|
walkJSPath (Just (JSON.Array ar)) [JSPFilter (StartsWithCond txt)] = findFirstMatch T.isPrefixOf txt ar
|
||||||
|
walkJSPath (Just (JSON.Array ar)) [JSPFilter (EndsWithCond txt)] = findFirstMatch T.isSuffixOf txt ar
|
||||||
|
walkJSPath (Just (JSON.Array ar)) [JSPFilter (ContainsCond txt)] = findFirstMatch T.isInfixOf txt ar
|
||||||
|
walkJSPath _ _ = Nothing
|
||||||
|
|
||||||
|
findFirstMatch matchWith pattern = foldr checkMatch Nothing
|
||||||
|
where
|
||||||
|
checkMatch (JSON.String txt) acc
|
||||||
|
| pattern `matchWith` txt = Just $ JSON.String txt
|
||||||
|
| otherwise = acc
|
||||||
|
checkMatch _ acc = acc
|
||||||
|
|
||||||
unquoted :: JSON.Value -> BS.ByteString
|
unquoted :: JSON.Value -> BS.ByteString
|
||||||
unquoted (JSON.String t) = encodeUtf8 t
|
unquoted (JSON.String t) = encodeUtf8 t
|
||||||
unquoted v = LBS.toStrict $ JSON.encode v
|
unquoted v = LBS.toStrict $ JSON.encode v
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import qualified Data.Aeson as JSON
|
|||||||
import qualified Data.Aeson.KeyMap as KM
|
import qualified Data.Aeson.KeyMap as KM
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
|
|
||||||
-- |
|
-- | Parse result for JWT Claims
|
||||||
-- Parse and store result for JWT Claims. Can be accessed in
|
|
||||||
-- db through GUCs (for RLS etc)
|
|
||||||
data AuthResult = AuthResult
|
data AuthResult = AuthResult
|
||||||
{ authClaims :: KM.KeyMap JSON.Value
|
{ authClaims :: KM.KeyMap JSON.Value
|
||||||
, authRole :: BS.ByteString
|
, authRole :: BS.ByteString
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ dumpSchema :: AppState -> IO LBS.ByteString
|
|||||||
dumpSchema appState = do
|
dumpSchema appState = do
|
||||||
conf@AppConfig{..} <- AppState.getConfig appState
|
conf@AppConfig{..} <- AppState.getConfig appState
|
||||||
result <-
|
result <-
|
||||||
AppState.usePool appState (SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
||||||
|
AppState.usePool appState
|
||||||
|
(transaction SQL.ReadCommitted SQL.Read $ querySchemaCache conf)
|
||||||
case result of
|
case result of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
let observer = AppState.getObserver appState
|
let observer = AppState.getObserver appState
|
||||||
|
|||||||
+22
-82
@@ -9,7 +9,6 @@ Description : Manages PostgREST configuration type and parser.
|
|||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
|
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
|
||||||
|
|
||||||
module PostgREST.Config
|
module PostgREST.Config
|
||||||
( AppConfig (..)
|
( AppConfig (..)
|
||||||
@@ -28,25 +27,21 @@ module PostgREST.Config
|
|||||||
, parseSecret
|
, parseSecret
|
||||||
, addFallbackAppName
|
, addFallbackAppName
|
||||||
, addTargetSessionAttrs
|
, addTargetSessionAttrs
|
||||||
, toConnectionSettings
|
|
||||||
, exampleConfigFile
|
, exampleConfigFile
|
||||||
, audMatchesCfg
|
, audMatchesCfg
|
||||||
, Verbosity (..)
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.ByteString.Base64 as B64
|
import qualified Data.ByteString.Base64 as B64
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
import qualified Data.Configurator as C
|
import qualified Data.Configurator as C
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
import qualified Data.String as S
|
import qualified Data.String as S
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Encoding as T
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Hasql.Connection.Setting as SQL
|
import qualified Jose.Jwa as JWT
|
||||||
import qualified Hasql.Connection.Setting.Connection as SQL
|
import qualified Jose.Jwk as JWT
|
||||||
import qualified Jose.Jwa as JWT
|
|
||||||
import qualified Jose.Jwk as JWT
|
|
||||||
|
|
||||||
import Control.Monad (fail)
|
import Control.Monad (fail)
|
||||||
import Data.Either.Combinators (mapLeft)
|
import Data.Either.Combinators (mapLeft)
|
||||||
@@ -68,18 +63,16 @@ import PostgREST.Config.JSPath (FilterExp (..), JSPath,
|
|||||||
pRoleClaimKey)
|
pRoleClaimKey)
|
||||||
import PostgREST.Config.Proxy (Proxy (..),
|
import PostgREST.Config.Proxy (Proxy (..),
|
||||||
isMalformedProxyUri, toURI)
|
isMalformedProxyUri, toURI)
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi,
|
||||||
toQi)
|
toQi)
|
||||||
|
|
||||||
import PostgREST.Version (prettyVersion)
|
import Protolude hiding (Proxy, toList)
|
||||||
import Protolude hiding (Proxy, toList)
|
|
||||||
|
|
||||||
audMatchesCfg :: AppConfig -> Text -> Bool
|
audMatchesCfg :: AppConfig -> Text -> Bool
|
||||||
audMatchesCfg = maybe (const True) (==) . configJwtAudience
|
audMatchesCfg = maybe (const True) (==) . configJwtAudience
|
||||||
|
|
||||||
data AppConfig = AppConfig
|
data AppConfig = AppConfig
|
||||||
{ configAppSettings :: [(Text, Text)]
|
{ configAppSettings :: [(Text, Text)]
|
||||||
, configClientErrorVerbosity :: Verbosity
|
|
||||||
, configDbAggregates :: Bool
|
, configDbAggregates :: Bool
|
||||||
, configDbAnonRole :: Maybe BS.ByteString
|
, configDbAnonRole :: Maybe BS.ByteString
|
||||||
, configDbChannel :: Text
|
, configDbChannel :: Text
|
||||||
@@ -99,7 +92,6 @@ data AppConfig = AppConfig
|
|||||||
, configDbSchemas :: NonEmpty Text
|
, configDbSchemas :: NonEmpty Text
|
||||||
, configDbConfig :: Bool
|
, configDbConfig :: Bool
|
||||||
, configDbPreConfig :: Maybe QualifiedIdentifier
|
, configDbPreConfig :: Maybe QualifiedIdentifier
|
||||||
, configDbTimezoneEnabled :: Bool
|
|
||||||
, configDbTxAllowOverride :: Bool
|
, configDbTxAllowOverride :: Bool
|
||||||
, configDbTxRollbackAll :: Bool
|
, configDbTxRollbackAll :: Bool
|
||||||
, configDbUri :: Text
|
, configDbUri :: Text
|
||||||
@@ -127,8 +119,6 @@ data AppConfig = AppConfig
|
|||||||
, configRoleSettings :: RoleSettings
|
, configRoleSettings :: RoleSettings
|
||||||
, configRoleIsoLvl :: RoleIsolationLvl
|
, configRoleIsoLvl :: RoleIsolationLvl
|
||||||
, configInternalSCQuerySleep :: Maybe Int32
|
, configInternalSCQuerySleep :: Maybe Int32
|
||||||
, configInternalSCLoadSleep :: Maybe Int32
|
|
||||||
, configInternalSCRelLoadSleep :: Maybe Int32
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data LogLevel = LogCrit | LogError | LogWarn | LogInfo | LogDebug
|
data LogLevel = LogCrit | LogError | LogWarn | LogInfo | LogDebug
|
||||||
@@ -142,15 +132,6 @@ dumpLogLevel = \case
|
|||||||
LogInfo -> "info"
|
LogInfo -> "info"
|
||||||
LogDebug -> "debug"
|
LogDebug -> "debug"
|
||||||
|
|
||||||
data Verbosity
|
|
||||||
= Minimal
|
|
||||||
| Verbose
|
|
||||||
|
|
||||||
dumpClientErrorVerbosity :: Verbosity -> Text
|
|
||||||
dumpClientErrorVerbosity = \case
|
|
||||||
Minimal -> "minimal"
|
|
||||||
Verbose -> "verbose"
|
|
||||||
|
|
||||||
data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
|
data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
@@ -167,8 +148,7 @@ toText conf =
|
|||||||
where
|
where
|
||||||
-- apply conf to all pgrst settings
|
-- apply conf to all pgrst settings
|
||||||
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
||||||
[("client-error-verbosity", q . dumpClientErrorVerbosity . configClientErrorVerbosity)
|
[("db-aggregates-enabled", T.toLower . show . configDbAggregates)
|
||||||
,("db-aggregates-enabled", T.toLower . show . configDbAggregates)
|
|
||||||
,("db-anon-role", q . T.decodeUtf8 . fromMaybe "" . configDbAnonRole)
|
,("db-anon-role", q . T.decodeUtf8 . fromMaybe "" . configDbAnonRole)
|
||||||
,("db-channel", q . configDbChannel)
|
,("db-channel", q . configDbChannel)
|
||||||
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
|
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
|
||||||
@@ -187,7 +167,6 @@ toText conf =
|
|||||||
,("db-schemas", q . T.intercalate "," . toList . configDbSchemas)
|
,("db-schemas", q . T.intercalate "," . toList . configDbSchemas)
|
||||||
,("db-config", T.toLower . show . configDbConfig)
|
,("db-config", T.toLower . show . configDbConfig)
|
||||||
,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig)
|
,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig)
|
||||||
,("db-timezone-enabled", T.toLower . show . configDbTimezoneEnabled)
|
|
||||||
,("db-tx-end", q . showTxEnd)
|
,("db-tx-end", q . showTxEnd)
|
||||||
,("db-uri", q . configDbUri)
|
,("db-uri", q . configDbUri)
|
||||||
,("jwt-aud", q . fromMaybe mempty . configJwtAudience)
|
,("jwt-aud", q . fromMaybe mempty . configJwtAudience)
|
||||||
@@ -217,10 +196,6 @@ toText conf =
|
|||||||
-- quote strings and replace " with \"
|
-- quote strings and replace " with \"
|
||||||
q s = "\"" <> T.replace "\"" "\\\"" s <> "\""
|
q s = "\"" <> T.replace "\"" "\\\"" s <> "\""
|
||||||
|
|
||||||
dumpQi :: QualifiedIdentifier -> Text
|
|
||||||
dumpQi (QualifiedIdentifier s i) =
|
|
||||||
(if T.null s then mempty else s <> ".") <> i
|
|
||||||
|
|
||||||
showTxEnd c = case (configDbTxRollbackAll c, configDbTxAllowOverride c) of
|
showTxEnd c = case (configDbTxRollbackAll c, configDbTxAllowOverride c) of
|
||||||
( False, False ) -> "commit"
|
( False, False ) -> "commit"
|
||||||
( False, True ) -> "commit-allow-override"
|
( False, True ) -> "commit-allow-override"
|
||||||
@@ -273,7 +248,6 @@ parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> Rol
|
|||||||
parser optPath env dbSettings roleSettings roleIsolationLvl =
|
parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||||
AppConfig
|
AppConfig
|
||||||
<$> parseAppSettings "app.settings"
|
<$> parseAppSettings "app.settings"
|
||||||
<*> parseErrorVerbosity "client-error-verbosity"
|
|
||||||
<*> (fromMaybe False <$> optBool "db-aggregates-enabled")
|
<*> (fromMaybe False <$> optBool "db-aggregates-enabled")
|
||||||
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
|
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
|
||||||
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
||||||
@@ -294,10 +268,10 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
||||||
<*> (fmap toQi <$> optWithAlias (optString "db-root-spec")
|
<*> (fmap toQi <$> optWithAlias (optString "db-root-spec")
|
||||||
(optString "root-spec"))
|
(optString "root-spec"))
|
||||||
<*> parseDbSchemas "db-schemas" "db-schema"
|
<*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optString "db-schemas")
|
||||||
|
(optString "db-schema"))
|
||||||
<*> (fromMaybe True <$> optBool "db-config")
|
<*> (fromMaybe True <$> optBool "db-config")
|
||||||
<*> (fmap toQi <$> optString "db-pre-config")
|
<*> (fmap toQi <$> optString "db-pre-config")
|
||||||
<*> (fromMaybe True <$> optBool "db-timezone-enabled")
|
|
||||||
<*> parseTxEnd "db-tx-end" snd
|
<*> parseTxEnd "db-tx-end" snd
|
||||||
<*> parseTxEnd "db-tx-end" fst
|
<*> parseTxEnd "db-tx-end" fst
|
||||||
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
|
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
|
||||||
@@ -328,17 +302,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
<*> pure roleSettings
|
<*> pure roleSettings
|
||||||
<*> pure roleIsolationLvl
|
<*> pure roleIsolationLvl
|
||||||
<*> optInt "internal-schema-cache-query-sleep"
|
<*> optInt "internal-schema-cache-query-sleep"
|
||||||
<*> optInt "internal-schema-cache-load-sleep"
|
|
||||||
<*> optInt "internal-schema-cache-relationship-load-sleep"
|
|
||||||
where
|
where
|
||||||
parseErrorVerbosity :: C.Key -> C.Parser C.Config Verbosity
|
|
||||||
parseErrorVerbosity k =
|
|
||||||
optString k >>= \case
|
|
||||||
Nothing -> pure Verbose -- default
|
|
||||||
Just "minimal" -> pure Minimal
|
|
||||||
Just "verbose" -> pure Verbose
|
|
||||||
Just _ -> fail "Invalid client-error-verbosity. Check your configuration."
|
|
||||||
|
|
||||||
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
|
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
|
||||||
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
|
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
|
||||||
where
|
where
|
||||||
@@ -357,18 +321,6 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
Just asp | asp == serverPort -> fail "admin-server-port cannot be the same as server-port"
|
Just asp | asp == serverPort -> fail "admin-server-port cannot be the same as server-port"
|
||||||
| otherwise -> pure $ Just asp
|
| otherwise -> pure $ Just asp
|
||||||
|
|
||||||
parseDbSchemas :: C.Key -> C.Key -> C.Parser C.Config (NonEmpty Text)
|
|
||||||
parseDbSchemas k al =
|
|
||||||
optWithAlias (optString k) (optString al) >>= \case
|
|
||||||
Nothing -> pure $ fromList ["public"]
|
|
||||||
Just s
|
|
||||||
| "pg_catalog" `elem` schemas -> fail (errMsg "pg_catalog")
|
|
||||||
| "information_schema" `elem` schemas -> fail (errMsg "information_schema")
|
|
||||||
| otherwise -> pure $ fromList schemas
|
|
||||||
where
|
|
||||||
schemas = splitOnCommas s
|
|
||||||
errMsg x = "db-schemas does not allow schema: '" <> x <> "'"
|
|
||||||
|
|
||||||
parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
|
parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
|
||||||
parseSocketFileMode k =
|
parseSocketFileMode k =
|
||||||
optString k >>= \case
|
optString k >>= \case
|
||||||
@@ -621,10 +573,10 @@ pgConnString conn | uriDesignator `T.isPrefixOf` conn || shortUriDesignator `T.i
|
|||||||
-- >>> addFallbackAppName ver "postgres://admin2:?pass?special?@localhost:5432/postgres"
|
-- >>> addFallbackAppName ver "postgres://admin2:?pass?special?@localhost:5432/postgres"
|
||||||
-- "postgres://admin2:?pass?special?@localhost:5432/postgres?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
-- "postgres://admin2:?pass?special?@localhost:5432/postgres?fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||||
--
|
--
|
||||||
-- >>> addFallbackAppName ver "postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass"
|
-- addFallbackAppName ver "postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass"
|
||||||
-- "postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
-- "postgresql://?dbname=postgres&host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||||
--
|
--
|
||||||
-- >>> addFallbackAppName ver "postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass"
|
-- addFallbackAppName ver "postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass"
|
||||||
-- "postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
-- "postgresql:///postgres?host=/run/user/1000/postgrest/postgrest-with-postgresql-16-BuR/socket&user=some_protected_user&password=invalid_pass&fallback_application_name=PostgREST%2011.1.0%20%285a04ec7%29"
|
||||||
addFallbackAppName :: ByteString -> Text -> Text
|
addFallbackAppName :: ByteString -> Text -> Text
|
||||||
addFallbackAppName version dbUri = addConnStringOption dbUri "fallback_application_name" pgrstVer
|
addFallbackAppName version dbUri = addConnStringOption dbUri "fallback_application_name" pgrstVer
|
||||||
@@ -651,12 +603,6 @@ addFallbackAppName version dbUri = addConnStringOption dbUri "fallback_applicati
|
|||||||
addTargetSessionAttrs :: Text -> Text
|
addTargetSessionAttrs :: Text -> Text
|
||||||
addTargetSessionAttrs dbUri = addConnStringOption dbUri "target_session_attrs" "read-write"
|
addTargetSessionAttrs dbUri = addConnStringOption dbUri "target_session_attrs" "read-write"
|
||||||
|
|
||||||
toConnectionSettings :: (Text -> Text) -> AppConfig -> [SQL.Setting]
|
|
||||||
toConnectionSettings transformUri AppConfig{configDbUri, configDbPreparedStatements} =
|
|
||||||
[ SQL.connection $ SQL.string $ transformUri . addFallbackAppName prettyVersion $ configDbUri
|
|
||||||
, SQL.usePreparedStatements configDbPreparedStatements
|
|
||||||
]
|
|
||||||
|
|
||||||
addConnStringOption :: Text -> Text -> Text -> Text
|
addConnStringOption :: Text -> Text -> Text -> Text
|
||||||
addConnStringOption dbUri key val = dbUri <>
|
addConnStringOption dbUri key val = dbUri <>
|
||||||
case pgConnString dbUri of
|
case pgConnString dbUri of
|
||||||
@@ -677,9 +623,6 @@ exampleConfigFile = S.unlines
|
|||||||
[ "## Admin server used for checks. It's disabled by default unless a port is specified."
|
[ "## Admin server used for checks. It's disabled by default unless a port is specified."
|
||||||
, "# admin-server-port = 3001"
|
, "# admin-server-port = 3001"
|
||||||
, ""
|
, ""
|
||||||
, "# PostgREST error json verbosity config"
|
|
||||||
, "# client-error-verbosity = \"verbose\""
|
|
||||||
, ""
|
|
||||||
, "## The database role to use when no client authentication is provided"
|
, "## The database role to use when no client authentication is provided"
|
||||||
, "# db-anon-role = \"anon\""
|
, "# db-anon-role = \"anon\""
|
||||||
, ""
|
, ""
|
||||||
@@ -729,19 +672,16 @@ exampleConfigFile = S.unlines
|
|||||||
, "## The name of which database schema to expose to REST clients"
|
, "## The name of which database schema to expose to REST clients"
|
||||||
, "db-schemas = \"public\""
|
, "db-schemas = \"public\""
|
||||||
, ""
|
, ""
|
||||||
, "## Enable quering pg_timezone_names from db"
|
|
||||||
, "# db-timezone-enabled = true"
|
|
||||||
, ""
|
|
||||||
, "## How to terminate database transactions"
|
, "## How to terminate database transactions"
|
||||||
, "## Possible values are:"
|
, "## Possible values are:"
|
||||||
, "## commit (default)"
|
, "## commit (default)"
|
||||||
, "## Transaction is always committed, this can not be overridden"
|
, "## Transaction is always committed, this can not be overriden"
|
||||||
, "## commit-allow-override"
|
, "## commit-allow-override"
|
||||||
, "## Transaction is committed, but can be overridden with Prefer tx=rollback header"
|
, "## Transaction is committed, but can be overriden with Prefer tx=rollback header"
|
||||||
, "## rollback"
|
, "## rollback"
|
||||||
, "## Transaction is always rolled back, this can not be overridden"
|
, "## Transaction is always rolled back, this can not be overriden"
|
||||||
, "## rollback-allow-override"
|
, "## rollback-allow-override"
|
||||||
, "## Transaction is rolled back, but can be overridden with Prefer tx=commit header"
|
, "## Transaction is rolled back, but can be overriden with Prefer tx=commit header"
|
||||||
, "db-tx-end = \"commit\""
|
, "db-tx-end = \"commit\""
|
||||||
, ""
|
, ""
|
||||||
, "## The standard connection URI format, documented at"
|
, "## The standard connection URI format, documented at"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import Control.Arrow ((***))
|
|||||||
import PostgREST.Config.PgVersion (PgVersion (..), pgVersion150)
|
import PostgREST.Config.PgVersion (PgVersion (..), pgVersion150)
|
||||||
|
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
import qualified Hasql.Encoders as HE
|
import qualified Hasql.Encoders as HE
|
||||||
@@ -32,8 +33,8 @@ type RoleSettings = (HM.HashMap ByteString (HM.HashMap ByteString ByteString
|
|||||||
type RoleIsolationLvl = HM.HashMap ByteString SQL.IsolationLevel
|
type RoleIsolationLvl = HM.HashMap ByteString SQL.IsolationLevel
|
||||||
type TimezoneNames = Set Text -- cache timezone names for prefer timezone=
|
type TimezoneNames = Set Text -- cache timezone names for prefer timezone=
|
||||||
|
|
||||||
toIsolationLevel :: (Eq a, IsString a) => a -> SQL.IsolationLevel
|
toIsolationLevel :: Text -> SQL.IsolationLevel
|
||||||
toIsolationLevel a = case a of
|
toIsolationLevel a = case T.toLower a of
|
||||||
"repeatable read" -> SQL.RepeatableRead
|
"repeatable read" -> SQL.RepeatableRead
|
||||||
"serializable" -> SQL.Serializable
|
"serializable" -> SQL.Serializable
|
||||||
_ -> SQL.ReadCommitted
|
_ -> SQL.ReadCommitted
|
||||||
@@ -46,7 +47,6 @@ dbSettingsNames :: [Text]
|
|||||||
dbSettingsNames =
|
dbSettingsNames =
|
||||||
(prefix <>) <$>
|
(prefix <>) <$>
|
||||||
["db_aggregates_enabled"
|
["db_aggregates_enabled"
|
||||||
,"client_error_verbosity"
|
|
||||||
,"db_anon_role"
|
,"db_anon_role"
|
||||||
,"db_pre_config"
|
,"db_pre_config"
|
||||||
,"db_extra_search_path"
|
,"db_extra_search_path"
|
||||||
@@ -56,7 +56,6 @@ dbSettingsNames =
|
|||||||
,"db_prepared_statements"
|
,"db_prepared_statements"
|
||||||
,"db_root_spec"
|
,"db_root_spec"
|
||||||
,"db_schemas"
|
,"db_schemas"
|
||||||
,"db_timezone_enabled"
|
|
||||||
,"db_tx_end"
|
,"db_tx_end"
|
||||||
,"db_hoisted_tx_settings"
|
,"db_hoisted_tx_settings"
|
||||||
,"jwt_aud"
|
,"jwt_aud"
|
||||||
@@ -72,8 +71,8 @@ dbSettingsNames =
|
|||||||
,"server_timing_enabled"
|
,"server_timing_enabled"
|
||||||
]
|
]
|
||||||
|
|
||||||
queryPgVersion :: Session PgVersion
|
queryPgVersion :: Bool -> Session PgVersion
|
||||||
queryPgVersion = statement mempty $ pgVersionStatement False
|
queryPgVersion prepared = statement mempty $ pgVersionStatement prepared
|
||||||
|
|
||||||
pgVersionStatement :: Bool -> SQL.Statement () PgVersion
|
pgVersionStatement :: Bool -> SQL.Statement () PgVersion
|
||||||
pgVersionStatement = SQL.Statement sql HE.noParams versionRow
|
pgVersionStatement = SQL.Statement sql HE.noParams versionRow
|
||||||
@@ -92,9 +91,10 @@ pgVersionStatement = SQL.Statement sql HE.noParams versionRow
|
|||||||
--
|
--
|
||||||
-- The example above will result in <prefix>jwt_aud = 'val'
|
-- The example above will result in <prefix>jwt_aud = 'val'
|
||||||
-- A setting on the database only will have no effect: ALTER DATABASE postgres SET <prefix>jwt_aud = 'xx'
|
-- A setting on the database only will have no effect: ALTER DATABASE postgres SET <prefix>jwt_aud = 'xx'
|
||||||
queryDbSettings :: Maybe Text -> Session [(Text, Text)]
|
queryDbSettings :: Maybe Text -> Bool -> Session [(Text, Text)]
|
||||||
queryDbSettings preConfFunc =
|
queryDbSettings preConfFunc prepared =
|
||||||
SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings True
|
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
||||||
|
transaction SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings prepared
|
||||||
where
|
where
|
||||||
sql = encodeUtf8 [trimming|
|
sql = encodeUtf8 [trimming|
|
||||||
WITH
|
WITH
|
||||||
@@ -132,9 +132,10 @@ queryDbSettings preConfFunc =
|
|||||||
|]::Text
|
|]::Text
|
||||||
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
||||||
|
|
||||||
queryRoleSettings :: PgVersion -> Session (RoleSettings, RoleIsolationLvl)
|
queryRoleSettings :: PgVersion -> Bool -> Session (RoleSettings, RoleIsolationLvl)
|
||||||
queryRoleSettings pgVer =
|
queryRoleSettings pgVer prepared =
|
||||||
SQL.transactionNoRetry SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) True
|
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
|
where
|
||||||
sql = encodeUtf8 [trimming|
|
sql = encodeUtf8 [trimming|
|
||||||
with
|
with
|
||||||
@@ -148,7 +149,7 @@ queryRoleSettings pgVer =
|
|||||||
SELECT
|
SELECT
|
||||||
rolname,
|
rolname,
|
||||||
substr(setting, 1, strpos(setting, '=') - 1) as key,
|
substr(setting, 1, strpos(setting, '=') - 1) as key,
|
||||||
lower(substr(setting, strpos(setting, '=') + 1)) as value
|
substr(setting, strpos(setting, '=') + 1) as value
|
||||||
FROM role_setting
|
FROM role_setting
|
||||||
),
|
),
|
||||||
iso_setting AS (
|
iso_setting AS (
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
|
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
|
||||||
module PostgREST.Config.JSPath
|
module PostgREST.Config.JSPath
|
||||||
( JSPath
|
( JSPath
|
||||||
, JSPathExp(..)
|
, JSPathExp(..)
|
||||||
, FilterExp(..)
|
, FilterExp(..)
|
||||||
, dumpJSPath
|
, dumpJSPath
|
||||||
, pRoleClaimKey
|
, pRoleClaimKey
|
||||||
, walkJSPath
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
|
||||||
import qualified Data.Aeson.Key as K
|
|
||||||
import qualified Data.Aeson.KeyMap as KM
|
|
||||||
import qualified Data.Text as T
|
|
||||||
import qualified Data.Vector as V
|
|
||||||
import qualified Text.ParserCombinators.Parsec as P
|
import qualified Text.ParserCombinators.Parsec as P
|
||||||
|
|
||||||
import Data.Either.Combinators (mapLeft)
|
import Data.Either.Combinators (mapLeft)
|
||||||
@@ -29,10 +22,9 @@ type JSPath = [JSPathExp]
|
|||||||
-- NOTE: We only accept one JSPFilter expr (at the end of input)
|
-- NOTE: We only accept one JSPFilter expr (at the end of input)
|
||||||
-- | jspath expression
|
-- | jspath expression
|
||||||
data JSPathExp
|
data JSPathExp
|
||||||
= JSPKey Text -- .property or ."property-dash"
|
= JSPKey Text -- .property or ."property-dash"
|
||||||
| JSPIdx Int -- [0]
|
| JSPIdx Int -- [0]
|
||||||
| JSPSlice (Maybe Int) (Maybe Int) -- [0:5] or [0:] or [:5] or [:]
|
| JSPFilter FilterExp -- [?(@ == "match")]
|
||||||
| JSPFilter FilterExp -- [?(@ == "match")]
|
|
||||||
|
|
||||||
data FilterExp
|
data FilterExp
|
||||||
= EqualsCond Text
|
= EqualsCond Text
|
||||||
@@ -45,7 +37,6 @@ dumpJSPath :: JSPathExp -> Text
|
|||||||
-- TODO: this needs to be quoted properly for special chars
|
-- TODO: this needs to be quoted properly for special chars
|
||||||
dumpJSPath (JSPKey k) = "." <> show k
|
dumpJSPath (JSPKey k) = "." <> show k
|
||||||
dumpJSPath (JSPIdx i) = "[" <> show i <> "]"
|
dumpJSPath (JSPIdx i) = "[" <> show i <> "]"
|
||||||
dumpJSPath (JSPSlice s e) = "[" <> maybe "" show s <> ":" <> maybe "" show e <> "]"
|
|
||||||
dumpJSPath (JSPFilter cond) = "[?(@" <> expr <> ")]"
|
dumpJSPath (JSPFilter cond) = "[?(@" <> expr <> ")]"
|
||||||
where
|
where
|
||||||
expr =
|
expr =
|
||||||
@@ -56,35 +47,6 @@ dumpJSPath (JSPFilter cond) = "[?(@" <> expr <> ")]"
|
|||||||
EndsWithCond text -> " ==^ " <> show text
|
EndsWithCond text -> " ==^ " <> show text
|
||||||
ContainsCond text -> " *== " <> show text
|
ContainsCond text -> " *== " <> show text
|
||||||
|
|
||||||
-- | Evaluate JSPath on a JSON
|
|
||||||
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
|
|
||||||
walkJSPath x [] = x
|
|
||||||
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest
|
|
||||||
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
|
|
||||||
walkJSPath (Just (JSON.String str)) (JSPSlice start end:rest) =
|
|
||||||
let
|
|
||||||
len = T.length str
|
|
||||||
|
|
||||||
norm :: Maybe Int -> Maybe Int -- Normalize negative indices to positive
|
|
||||||
norm = fmap (\i -> max 0 $ min len $ if i < 0 then len + i else i)
|
|
||||||
|
|
||||||
s = fromMaybe 0 $ norm start -- normalized start index
|
|
||||||
e = fromMaybe len $ norm end -- normalized end index
|
|
||||||
slicedString = if s >= e then T.empty else T.take (e-s) $ T.drop s str
|
|
||||||
in
|
|
||||||
walkJSPath (Just $ JSON.String slicedString) rest
|
|
||||||
|
|
||||||
walkJSPath (Just (JSON.Array ar)) (JSPFilter jspFilter:rest) = case jspFilter of
|
|
||||||
EqualsCond txt -> walkJSPath (findFirstMatch (==) txt ar) rest
|
|
||||||
NotEqualsCond txt -> walkJSPath (findFirstMatch (/=) txt ar) rest
|
|
||||||
StartsWithCond txt -> walkJSPath (findFirstMatch T.isPrefixOf txt ar) rest
|
|
||||||
EndsWithCond txt -> walkJSPath (findFirstMatch T.isSuffixOf txt ar) rest
|
|
||||||
ContainsCond txt -> walkJSPath (findFirstMatch T.isInfixOf txt ar) rest
|
|
||||||
where
|
|
||||||
findFirstMatch matchWith pattern = find (\case
|
|
||||||
JSON.String txt -> pattern `matchWith` txt
|
|
||||||
_ -> False)
|
|
||||||
walkJSPath _ _ = Nothing
|
|
||||||
|
|
||||||
-- Used for the config value "role-claim-key"
|
-- Used for the config value "role-claim-key"
|
||||||
pRoleClaimKey :: Text -> Either Text JSPath
|
pRoleClaimKey :: Text -> Either Text JSPath
|
||||||
@@ -95,7 +57,7 @@ pJSPath :: P.Parser JSPath
|
|||||||
pJSPath = P.many1 pJSPathExp <* P.eof
|
pJSPath = P.many1 pJSPathExp <* P.eof
|
||||||
|
|
||||||
pJSPathExp :: P.Parser JSPathExp
|
pJSPathExp :: P.Parser JSPathExp
|
||||||
pJSPathExp = P.try pJSPKey <|> P.try pJSPFilter <|> P.try pJSPIdx <|> pJSPSlice
|
pJSPathExp = pJSPKey <|> pJSPFilter <|> pJSPIdx
|
||||||
|
|
||||||
pJSPKey :: P.Parser JSPathExp
|
pJSPKey :: P.Parser JSPathExp
|
||||||
pJSPKey = do
|
pJSPKey = do
|
||||||
@@ -110,25 +72,13 @@ pJSPIdx = do
|
|||||||
P.char ']'
|
P.char ']'
|
||||||
return (JSPIdx num) <?> "pJSPIdx: JSPath array index"
|
return (JSPIdx num) <?> "pJSPIdx: JSPath array index"
|
||||||
|
|
||||||
pJSPSlice :: P.Parser JSPathExp
|
|
||||||
pJSPSlice = do
|
|
||||||
P.char '['
|
|
||||||
startSign <- P.optionMaybe $ P.char '-'
|
|
||||||
startIndex <- P.optionMaybe (read <$> P.many1 P.digit)
|
|
||||||
P.char ':'
|
|
||||||
endSign <- P.optionMaybe $ P.char '-'
|
|
||||||
endIndex <- P.optionMaybe (read <$> P.many1 P.digit)
|
|
||||||
P.char ']'
|
|
||||||
let start' = if isJust startSign then ((-1) *) <$> startIndex else startIndex
|
|
||||||
end' = if isJust endSign then ((-1) *) <$> endIndex else endIndex
|
|
||||||
return (JSPSlice start' end') <?> "pJSPSlice: JSPath string slice"
|
|
||||||
|
|
||||||
pJSPFilter :: P.Parser JSPathExp
|
pJSPFilter :: P.Parser JSPathExp
|
||||||
pJSPFilter = do
|
pJSPFilter = do
|
||||||
P.try $ P.string "[?("
|
P.try $ P.string "[?("
|
||||||
condition <- pFilterConditionParser
|
condition <- pFilterConditionParser
|
||||||
P.char ')'
|
P.char ')'
|
||||||
P.char ']'
|
P.char ']'
|
||||||
|
P.eof -- this should be the last jspath expression
|
||||||
return (JSPFilter condition) <?> "pJSPFilter: JSPath filter exp"
|
return (JSPFilter condition) <?> "pJSPFilter: JSPath filter exp"
|
||||||
|
|
||||||
pFilterConditionParser :: P.Parser FilterExp
|
pFilterConditionParser :: P.Parser FilterExp
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
module PostgREST.Config.PgVersion
|
module PostgREST.Config.PgVersion
|
||||||
( PgVersion(..)
|
( PgVersion(..)
|
||||||
, minimumPgVersion
|
, minimumPgVersion
|
||||||
|
, pgVersion140
|
||||||
, pgVersion150
|
, pgVersion150
|
||||||
, pgVersion170
|
, pgVersion170
|
||||||
, pgVersion180
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
@@ -25,7 +25,10 @@ instance Ord PgVersion where
|
|||||||
|
|
||||||
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
||||||
minimumPgVersion :: PgVersion
|
minimumPgVersion :: PgVersion
|
||||||
minimumPgVersion = pgVersion140
|
minimumPgVersion = pgVersion130
|
||||||
|
|
||||||
|
pgVersion130 :: PgVersion
|
||||||
|
pgVersion130 = PgVersion 130000 "13.0" "13.0"
|
||||||
|
|
||||||
pgVersion140 :: PgVersion
|
pgVersion140 :: PgVersion
|
||||||
pgVersion140 = PgVersion 140000 "14.0" "14.0"
|
pgVersion140 = PgVersion 140000 "14.0" "14.0"
|
||||||
@@ -35,6 +38,3 @@ pgVersion150 = PgVersion 150000 "15.0" "15.0"
|
|||||||
|
|
||||||
pgVersion170 :: PgVersion
|
pgVersion170 :: PgVersion
|
||||||
pgVersion170 = PgVersion 170000 "17.0" "17.0"
|
pgVersion170 = PgVersion 170000 "17.0" "17.0"
|
||||||
|
|
||||||
pgVersion180 :: PgVersion
|
|
||||||
pgVersion180 = PgVersion 180000 "18.0" "18.0"
|
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
module PostgREST.Debounce
|
|
||||||
( makeDebouncer) where
|
|
||||||
|
|
||||||
import Protolude
|
|
||||||
|
|
||||||
-- | Make a new debouncer action. An internal "worker" thread runs forever
|
|
||||||
-- ensuring "action" runs when the "trigger" is called. The "action" is only
|
|
||||||
-- executed once over a burst of calls.
|
|
||||||
makeDebouncer :: IO () -> IO (IO ())
|
|
||||||
makeDebouncer action = do
|
|
||||||
flag <- newEmptyMVar
|
|
||||||
|
|
||||||
let worker = forever $ do
|
|
||||||
takeMVar flag
|
|
||||||
action
|
|
||||||
trigger = void $ tryPutMVar flag ()
|
|
||||||
|
|
||||||
void $ forkIO worker
|
|
||||||
pure trigger
|
|
||||||
+192
-73
@@ -42,7 +42,6 @@ import Network.HTTP.Types.Header (Header)
|
|||||||
import PostgREST.MediaType (MediaType (..))
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import qualified PostgREST.MediaType as MediaType
|
import qualified PostgREST.MediaType as MediaType
|
||||||
|
|
||||||
import PostgREST.Config (Verbosity (..))
|
|
||||||
import PostgREST.SchemaCache (SchemaCache (SchemaCache, dbTablesFuzzyIndex))
|
import PostgREST.SchemaCache (SchemaCache (SchemaCache, dbTablesFuzzyIndex))
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
@@ -52,40 +51,22 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
|||||||
RelationshipsMap)
|
RelationshipsMap)
|
||||||
import PostgREST.SchemaCache.Routine (Routine (..),
|
import PostgREST.SchemaCache.Routine (Routine (..),
|
||||||
RoutineParam (..))
|
RoutineParam (..))
|
||||||
|
|
||||||
import PostgREST.Error.Types
|
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
-- | Encode Error to ByteString
|
class (ErrorBody a, JSON.ToJSON a) => PgrstError a where
|
||||||
errorPayload :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> LByteString
|
status :: a -> HTTP.Status
|
||||||
errorPayload verb = JSON.encode . toJsonPgrstError verb
|
headers :: a -> [Header]
|
||||||
where
|
|
||||||
toJsonPgrstError :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> JSON.Value
|
|
||||||
toJsonPgrstError Verbose err = JSON.object [
|
|
||||||
"code" .= code err
|
|
||||||
, "message" .= message err
|
|
||||||
, "details" .= details err
|
|
||||||
, "hint" .= hint err
|
|
||||||
]
|
|
||||||
toJsonPgrstError Minimal err = JSON.object [
|
|
||||||
"code" .= code err
|
|
||||||
, "message" .= message err
|
|
||||||
]
|
|
||||||
|
|
||||||
-- | Create HTTP response from Error
|
errorPayload :: a -> LByteString
|
||||||
errorResponseFor :: (ErrorBody a, ErrorHeaders a) => Verbosity -> a -> Response
|
errorPayload = JSON.encode
|
||||||
errorResponseFor verb err =
|
|
||||||
let
|
|
||||||
baseHeader = MediaType.toContentType MTApplicationJSON
|
|
||||||
cLHeader body = (,) "Content-Length" (show $ LBS.length body) :: Header
|
|
||||||
pSHeader code' = ("Proxy-Status", "PostgREST; error=" <> T.encodeUtf8 code')
|
|
||||||
in
|
|
||||||
responseLBS (status err) (baseHeader : cLHeader (errorPayload verb err) : pSHeader (code err) : headers err) $ errorPayload verb err
|
|
||||||
|
|
||||||
class ErrorHeaders a where
|
errorResponseFor :: a -> Response
|
||||||
status :: a -> HTTP.Status
|
errorResponseFor err =
|
||||||
headers :: a -> [Header]
|
let
|
||||||
|
baseHeader = MediaType.toContentType MTApplicationJSON
|
||||||
|
cLHeader body = (,) "Content-Length" (show $ LBS.length body) :: Header
|
||||||
|
in
|
||||||
|
responseLBS (status err) (baseHeader : cLHeader (errorPayload err) : headers err) $ errorPayload err
|
||||||
|
|
||||||
class ErrorBody a where
|
class ErrorBody a where
|
||||||
code :: a -> Text
|
code :: a -> Text
|
||||||
@@ -93,7 +74,49 @@ class ErrorBody a where
|
|||||||
details :: a -> Maybe JSON.Value
|
details :: a -> Maybe JSON.Value
|
||||||
hint :: a -> Maybe JSON.Value
|
hint :: a -> Maybe JSON.Value
|
||||||
|
|
||||||
instance ErrorHeaders ApiRequestError where
|
data ApiRequestError
|
||||||
|
= AggregatesNotAllowed
|
||||||
|
| MediaTypeError [ByteString]
|
||||||
|
| InvalidBody ByteString
|
||||||
|
| InvalidFilters
|
||||||
|
| InvalidPreferences [ByteString]
|
||||||
|
| InvalidRange RangeError
|
||||||
|
| InvalidRpcMethod ByteString
|
||||||
|
| NotEmbedded Text
|
||||||
|
| NotImplemented Text
|
||||||
|
| PutLimitNotAllowedError
|
||||||
|
| QueryParamError QPError
|
||||||
|
| RelatedOrderNotToOne Text Text
|
||||||
|
| UnacceptableFilter Text
|
||||||
|
| UnacceptableSchema Text [Text]
|
||||||
|
| UnsupportedMethod ByteString
|
||||||
|
| GucHeadersError
|
||||||
|
| GucStatusError
|
||||||
|
| PutMatchingPkError
|
||||||
|
| SingularityError Integer
|
||||||
|
| PGRSTParseError RaiseError
|
||||||
|
| MaxAffectedViolationError Integer
|
||||||
|
| InvalidResourcePath
|
||||||
|
| OpenAPIDisabled
|
||||||
|
| MaxAffectedRpcViolation
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
data QPError = QPError Text Text
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
data RaiseError
|
||||||
|
= MsgParseError ByteString
|
||||||
|
| DetParseError ByteString
|
||||||
|
| NoDetail
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
data RangeError
|
||||||
|
= NegativeLimit
|
||||||
|
| LowerGTUpper
|
||||||
|
| OutOfBounds Text Text
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
instance PgrstError ApiRequestError where
|
||||||
status AggregatesNotAllowed{} = HTTP.status400
|
status AggregatesNotAllowed{} = HTTP.status400
|
||||||
status MediaTypeError{} = HTTP.status406
|
status MediaTypeError{} = HTTP.status406
|
||||||
status InvalidBody{} = HTTP.status400
|
status InvalidBody{} = HTTP.status400
|
||||||
@@ -217,7 +240,20 @@ instance ErrorBody ApiRequestError where
|
|||||||
|
|
||||||
hint _ = Nothing
|
hint _ = Nothing
|
||||||
|
|
||||||
instance ErrorHeaders SchemaCacheError where
|
instance JSON.ToJSON ApiRequestError where
|
||||||
|
toJSON err = toJsonPgrstError
|
||||||
|
(code err) (message err) (details err) (hint err)
|
||||||
|
|
||||||
|
data SchemaCacheError
|
||||||
|
= AmbiguousRelBetween Text Text [Relationship]
|
||||||
|
| AmbiguousRpc [Routine]
|
||||||
|
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||||
|
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
|
||||||
|
| ColumnNotFound Text Text
|
||||||
|
| TableNotFound Text Text SchemaCache
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
instance PgrstError SchemaCacheError where
|
||||||
status AmbiguousRelBetween{} = HTTP.status300
|
status AmbiguousRelBetween{} = HTTP.status300
|
||||||
status AmbiguousRpc{} = HTTP.status300
|
status AmbiguousRpc{} = HTTP.status300
|
||||||
status NoRelBetween{} = HTTP.status400
|
status NoRelBetween{} = HTTP.status400
|
||||||
@@ -281,6 +317,18 @@ instance ErrorBody SchemaCacheError where
|
|||||||
|
|
||||||
hint _ = Nothing
|
hint _ = Nothing
|
||||||
|
|
||||||
|
instance JSON.ToJSON SchemaCacheError where
|
||||||
|
toJSON err = toJsonPgrstError
|
||||||
|
(code err) (message err) (details err) (hint err)
|
||||||
|
|
||||||
|
toJsonPgrstError :: Text -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value
|
||||||
|
toJsonPgrstError code' message' details' hint' = JSON.object [
|
||||||
|
"code" .= code'
|
||||||
|
, "message" .= message'
|
||||||
|
, "details" .= details'
|
||||||
|
, "hint" .= hint'
|
||||||
|
]
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- If no relationship is found then:
|
-- If no relationship is found then:
|
||||||
--
|
--
|
||||||
@@ -299,6 +347,9 @@ instance ErrorBody SchemaCacheError where
|
|||||||
-- >>> noRelBetweenHint "films" "role" "api" rels
|
-- >>> noRelBetweenHint "films" "role" "api" rels
|
||||||
-- Just "Perhaps you meant 'roles' instead of 'role'."
|
-- Just "Perhaps you meant 'roles' instead of 'role'."
|
||||||
--
|
--
|
||||||
|
-- >>> noRelBetweenHint "films" "role" "api" rels
|
||||||
|
-- Just "Perhaps you meant 'roles' instead of 'role'."
|
||||||
|
--
|
||||||
-- >>> noRelBetweenHint "films" "actors" "api" rels
|
-- >>> noRelBetweenHint "films" "actors" "api" rels
|
||||||
-- Nothing
|
-- Nothing
|
||||||
--
|
--
|
||||||
@@ -448,7 +499,12 @@ pgrstParseErrorHint err = case err of
|
|||||||
MsgParseError _ -> "MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and optional keys: 'details', 'hint'."
|
MsgParseError _ -> "MESSAGE must be a JSON object with obligatory keys: 'code', 'message' and optional keys: 'details', 'hint'."
|
||||||
_ -> "DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'."
|
_ -> "DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'."
|
||||||
|
|
||||||
instance ErrorHeaders PgError where
|
data PgError = PgError Authenticated SQL.UsageError
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
type Authenticated = Bool
|
||||||
|
|
||||||
|
instance PgrstError PgError where
|
||||||
status (PgError authed usageError) = pgErrorStatus authed usageError
|
status (PgError authed usageError) = pgErrorStatus authed usageError
|
||||||
|
|
||||||
headers (PgError _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p))))) =
|
headers (PgError _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p))))) =
|
||||||
@@ -463,33 +519,44 @@ instance ErrorHeaders PgError where
|
|||||||
then [("WWW-Authenticate", "Bearer") :: Header]
|
then [("WWW-Authenticate", "Bearer") :: Header]
|
||||||
else mempty
|
else mempty
|
||||||
|
|
||||||
|
proxyStatusHeader :: Text -> Header
|
||||||
|
proxyStatusHeader code' = ("Proxy-Status", "PostgREST; error=" <> T.encodeUtf8 code')
|
||||||
|
|
||||||
|
instance JSON.ToJSON PgError where
|
||||||
|
toJSON (PgError _ usageError) = toJsonPgrstError
|
||||||
|
(code usageError) (message usageError) (details usageError) (hint usageError)
|
||||||
|
|
||||||
instance ErrorBody PgError where
|
instance ErrorBody PgError where
|
||||||
code (PgError _ usageError) = code usageError
|
code (PgError _ usageError) = code usageError
|
||||||
message (PgError _ usageError) = message usageError
|
message (PgError _ usageError) = message usageError
|
||||||
details (PgError _ usageError) = details usageError
|
details (PgError _ usageError) = details usageError
|
||||||
hint (PgError _ usageError) = hint usageError
|
hint (PgError _ usageError) = hint usageError
|
||||||
|
|
||||||
|
instance JSON.ToJSON SQL.UsageError where
|
||||||
|
toJSON err = toJsonPgrstError
|
||||||
|
(code err) (message err) (details err) (hint err)
|
||||||
|
|
||||||
instance ErrorBody SQL.UsageError where
|
instance ErrorBody SQL.UsageError where
|
||||||
code (SQL.ConnectionUsageError _) = "PGRST000"
|
code (SQL.ConnectionUsageError _) = "PGRST000"
|
||||||
code (SQL.SessionUsageError (SQL.PipelineError e)) = code e
|
|
||||||
code (SQL.SessionUsageError (SQL.QueryError _ _ e)) = code e
|
code (SQL.SessionUsageError (SQL.QueryError _ _ e)) = code e
|
||||||
code SQL.AcquisitionTimeoutUsageError = "PGRST003"
|
code SQL.AcquisitionTimeoutUsageError = "PGRST003"
|
||||||
|
|
||||||
message (SQL.ConnectionUsageError _) = "Database connection error. Retrying the connection."
|
message (SQL.ConnectionUsageError _) = "Database connection error."
|
||||||
message (SQL.SessionUsageError (SQL.PipelineError e)) = message e
|
|
||||||
message (SQL.SessionUsageError (SQL.QueryError _ _ e)) = message e
|
message (SQL.SessionUsageError (SQL.QueryError _ _ e)) = message e
|
||||||
message SQL.AcquisitionTimeoutUsageError = "Timed out acquiring connection from connection pool."
|
message SQL.AcquisitionTimeoutUsageError = "Timed out acquiring connection from connection pool."
|
||||||
|
|
||||||
details (SQL.ConnectionUsageError e) = JSON.String . T.decodeUtf8 <$> e
|
details (SQL.ConnectionUsageError e) = JSON.String . T.decodeUtf8 <$> e
|
||||||
details (SQL.SessionUsageError (SQL.PipelineError e)) = details e
|
|
||||||
details (SQL.SessionUsageError (SQL.QueryError _ _ e)) = details e
|
details (SQL.SessionUsageError (SQL.QueryError _ _ e)) = details e
|
||||||
details SQL.AcquisitionTimeoutUsageError = Nothing
|
details SQL.AcquisitionTimeoutUsageError = Nothing
|
||||||
|
|
||||||
hint (SQL.ConnectionUsageError _) = Nothing
|
hint (SQL.ConnectionUsageError _) = Nothing
|
||||||
hint (SQL.SessionUsageError (SQL.PipelineError e)) = hint e
|
|
||||||
hint (SQL.SessionUsageError (SQL.QueryError _ _ e)) = hint e
|
hint (SQL.SessionUsageError (SQL.QueryError _ _ e)) = hint e
|
||||||
hint SQL.AcquisitionTimeoutUsageError = Nothing
|
hint SQL.AcquisitionTimeoutUsageError = Nothing
|
||||||
|
|
||||||
|
instance JSON.ToJSON SQL.CommandError where
|
||||||
|
toJSON err = toJsonPgrstError
|
||||||
|
(code err) (message err) (details err) (hint err)
|
||||||
|
|
||||||
instance ErrorBody SQL.CommandError where
|
instance ErrorBody SQL.CommandError where
|
||||||
-- Special error raised with code PGRST, to allow full response control
|
-- Special error raised with code PGRST, to allow full response control
|
||||||
code (SQL.ResultError (SQL.ServerError "PGRST" m d _ _)) =
|
code (SQL.ResultError (SQL.ServerError "PGRST" m d _ _)) =
|
||||||
@@ -531,13 +598,8 @@ instance ErrorBody SQL.CommandError where
|
|||||||
pgErrorStatus :: Bool -> SQL.UsageError -> HTTP.Status
|
pgErrorStatus :: Bool -> SQL.UsageError -> HTTP.Status
|
||||||
pgErrorStatus _ (SQL.ConnectionUsageError _) = HTTP.status503
|
pgErrorStatus _ (SQL.ConnectionUsageError _) = HTTP.status503
|
||||||
pgErrorStatus _ SQL.AcquisitionTimeoutUsageError = HTTP.status504
|
pgErrorStatus _ SQL.AcquisitionTimeoutUsageError = HTTP.status504
|
||||||
pgErrorStatus _ (SQL.SessionUsageError (SQL.PipelineError (SQL.ClientError _))) = HTTP.status503
|
|
||||||
pgErrorStatus _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) = HTTP.status503
|
pgErrorStatus _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) = HTTP.status503
|
||||||
pgErrorStatus authed (SQL.SessionUsageError (SQL.PipelineError (SQL.ResultError rError))) = mapSQLtoHTTP authed rError
|
pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError rError))) =
|
||||||
pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError rError))) = mapSQLtoHTTP authed rError
|
|
||||||
|
|
||||||
mapSQLtoHTTP :: Bool -> SQL.ResultError -> HTTP.Status
|
|
||||||
mapSQLtoHTTP authed rError =
|
|
||||||
case rError of
|
case rError of
|
||||||
(SQL.ServerError c m d _ _) ->
|
(SQL.ServerError c m d _ _) ->
|
||||||
case BS.unpack c of
|
case BS.unpack c of
|
||||||
@@ -591,45 +653,86 @@ mapSQLtoHTTP authed rError =
|
|||||||
_ -> HTTP.status500
|
_ -> HTTP.status500
|
||||||
|
|
||||||
|
|
||||||
instance ErrorHeaders Error where
|
data Error
|
||||||
status (ApiRequestErr err) = status err
|
= ApiRequestError ApiRequestError
|
||||||
status (SchemaCacheErr err) = status err
|
| SchemaCacheErr SchemaCacheError
|
||||||
status (JwtErr err) = status err
|
| JwtErr JwtError
|
||||||
status NoSchemaCacheError = HTTP.status503
|
| NoSchemaCacheError
|
||||||
status (PgErr err) = status err
|
| PgErr PgError
|
||||||
|
deriving Show
|
||||||
|
|
||||||
headers (ApiRequestErr err) = headers err
|
data JwtError
|
||||||
headers (SchemaCacheErr err) = headers err
|
= JwtDecodeErr JwtDecodeError
|
||||||
headers (JwtErr err) = headers err
|
| JwtSecretMissing
|
||||||
headers (PgErr err) = headers err
|
| JwtTokenRequired
|
||||||
headers NoSchemaCacheError = mempty
|
| JwtClaimsErr JwtClaimsError
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
data JwtDecodeError
|
||||||
|
= EmptyAuthHeader
|
||||||
|
| UnexpectedParts Int
|
||||||
|
| KeyError Text
|
||||||
|
| BadAlgorithm Text
|
||||||
|
| BadCrypto
|
||||||
|
| UnsupportedTokenType
|
||||||
|
| UnreachableDecodeError
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
data JwtClaimsError
|
||||||
|
= JWTExpired
|
||||||
|
| JWTNotYetValid
|
||||||
|
| JWTIssuedAtFuture
|
||||||
|
| JWTNotInAudience
|
||||||
|
| ParsingClaimsFailed
|
||||||
|
| ExpClaimNotNumber
|
||||||
|
| NbfClaimNotNumber
|
||||||
|
| IatClaimNotNumber
|
||||||
|
| AudClaimNotStringOrArray
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
instance PgrstError Error where
|
||||||
|
status (ApiRequestError err) = status err
|
||||||
|
status (SchemaCacheErr err) = status err
|
||||||
|
status (JwtErr err) = status err
|
||||||
|
status NoSchemaCacheError = HTTP.status503
|
||||||
|
status (PgErr err) = status err
|
||||||
|
|
||||||
|
headers (ApiRequestError err) = proxyStatusHeader (code err) : headers err
|
||||||
|
headers (SchemaCacheErr err) = proxyStatusHeader (code err) : headers err
|
||||||
|
headers (JwtErr err) = proxyStatusHeader (code err) : headers err
|
||||||
|
headers (PgErr err) = proxyStatusHeader (code err) : headers err
|
||||||
|
headers err@NoSchemaCacheError = proxyStatusHeader (code err) : mempty
|
||||||
|
|
||||||
|
instance JSON.ToJSON Error where
|
||||||
|
toJSON err = toJsonPgrstError
|
||||||
|
(code err) (message err) (details err) (hint err)
|
||||||
|
|
||||||
instance ErrorBody Error where
|
instance ErrorBody Error where
|
||||||
code (ApiRequestErr err) = code err
|
code (ApiRequestError err) = code err
|
||||||
code (SchemaCacheErr err) = code err
|
code (SchemaCacheErr err) = code err
|
||||||
code (JwtErr err) = code err
|
code (JwtErr err) = code err
|
||||||
code NoSchemaCacheError = "PGRST002"
|
code NoSchemaCacheError = "PGRST002"
|
||||||
code (PgErr err) = code err
|
code (PgErr err) = code err
|
||||||
|
|
||||||
message (ApiRequestErr err) = message err
|
message (ApiRequestError err) = message err
|
||||||
message (SchemaCacheErr err) = message err
|
message (SchemaCacheErr err) = message err
|
||||||
message (JwtErr err) = message err
|
message (JwtErr err) = message err
|
||||||
message NoSchemaCacheError = "Could not query the database for the schema cache. Retrying."
|
message NoSchemaCacheError = "Could not query the database for the schema cache. Retrying."
|
||||||
message (PgErr err) = message err
|
message (PgErr err) = message err
|
||||||
|
|
||||||
details (ApiRequestErr err) = details err
|
details (ApiRequestError err) = details err
|
||||||
details (SchemaCacheErr err) = details err
|
details (SchemaCacheErr err) = details err
|
||||||
details (JwtErr err) = details err
|
details (JwtErr err) = details err
|
||||||
details NoSchemaCacheError = Nothing
|
details NoSchemaCacheError = Nothing
|
||||||
details (PgErr err) = details err
|
details (PgErr err) = details err
|
||||||
|
|
||||||
hint (ApiRequestErr err) = hint err
|
hint (ApiRequestError err) = hint err
|
||||||
hint (SchemaCacheErr err) = hint err
|
hint (SchemaCacheErr err) = hint err
|
||||||
hint (JwtErr err) = hint err
|
hint (JwtErr err) = hint err
|
||||||
hint NoSchemaCacheError = Nothing
|
hint NoSchemaCacheError = Nothing
|
||||||
hint (PgErr err) = hint err
|
hint (PgErr err) = hint err
|
||||||
|
|
||||||
instance ErrorHeaders JwtError where
|
instance PgrstError JwtError where
|
||||||
status JwtDecodeErr{} = HTTP.unauthorized401
|
status JwtDecodeErr{} = HTTP.unauthorized401
|
||||||
status JwtSecretMissing = HTTP.status500
|
status JwtSecretMissing = HTTP.status500
|
||||||
status JwtTokenRequired = HTTP.unauthorized401
|
status JwtTokenRequired = HTTP.unauthorized401
|
||||||
@@ -640,6 +743,10 @@ instance ErrorHeaders JwtError where
|
|||||||
headers e@(JwtClaimsErr _) = [invalidTokenHeader $ message e]
|
headers e@(JwtClaimsErr _) = [invalidTokenHeader $ message e]
|
||||||
headers _ = mempty
|
headers _ = mempty
|
||||||
|
|
||||||
|
instance JSON.ToJSON JwtError where
|
||||||
|
toJSON err = toJsonPgrstError
|
||||||
|
(code err) (message err) (details err) (hint err)
|
||||||
|
|
||||||
instance ErrorBody JwtError where
|
instance ErrorBody JwtError where
|
||||||
code JwtSecretMissing = "PGRST300"
|
code JwtSecretMissing = "PGRST300"
|
||||||
code (JwtDecodeErr _) = "PGRST301"
|
code (JwtDecodeErr _) = "PGRST301"
|
||||||
@@ -683,6 +790,18 @@ requiredTokenHeader :: Header
|
|||||||
requiredTokenHeader = ("WWW-Authenticate", "Bearer")
|
requiredTokenHeader = ("WWW-Authenticate", "Bearer")
|
||||||
|
|
||||||
-- For parsing byteString to JSON Object, used for allowing full response control
|
-- 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
|
instance JSON.FromJSON PgRaiseErrMessage where
|
||||||
parseJSON (JSON.Object m) =
|
parseJSON (JSON.Object m) =
|
||||||
|
|||||||
@@ -1,138 +0,0 @@
|
|||||||
{-|
|
|
||||||
Module : PostgREST.Error.Types
|
|
||||||
Description : PostgREST Error Data Types
|
|
||||||
-}
|
|
||||||
module PostgREST.Error.Types
|
|
||||||
( ApiRequestError(..)
|
|
||||||
, QPError(..)
|
|
||||||
, RangeError(..)
|
|
||||||
, RaiseError(..)
|
|
||||||
, SchemaCacheError(..)
|
|
||||||
, PgError(..)
|
|
||||||
, Error(..)
|
|
||||||
, JwtError (..)
|
|
||||||
, JwtDecodeError(..)
|
|
||||||
, JwtClaimsError(..)
|
|
||||||
, PgRaiseErrMessage(..)
|
|
||||||
, PgRaiseErrDetails(..)
|
|
||||||
) where
|
|
||||||
|
|
||||||
import qualified Hasql.Pool as SQL
|
|
||||||
|
|
||||||
import PostgREST.MediaType (MediaType (..))
|
|
||||||
import PostgREST.SchemaCache (SchemaCache (..))
|
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
|
||||||
import PostgREST.SchemaCache.Relationship (Relationship (..),
|
|
||||||
RelationshipsMap)
|
|
||||||
import PostgREST.SchemaCache.Routine (Routine (..))
|
|
||||||
import Protolude
|
|
||||||
|
|
||||||
data Error
|
|
||||||
= ApiRequestErr ApiRequestError
|
|
||||||
| SchemaCacheErr SchemaCacheError
|
|
||||||
| JwtErr JwtError
|
|
||||||
| NoSchemaCacheError
|
|
||||||
| PgErr PgError
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
-- API REQUEST ERRORS: PGRST1XX
|
|
||||||
data ApiRequestError
|
|
||||||
= AggregatesNotAllowed
|
|
||||||
| MediaTypeError [ByteString]
|
|
||||||
| InvalidBody ByteString
|
|
||||||
| InvalidFilters
|
|
||||||
| InvalidPreferences [ByteString]
|
|
||||||
| InvalidRange RangeError
|
|
||||||
| InvalidRpcMethod ByteString
|
|
||||||
| NotEmbedded Text
|
|
||||||
| NotImplemented Text
|
|
||||||
| PutLimitNotAllowedError
|
|
||||||
| QueryParamError QPError
|
|
||||||
| RelatedOrderNotToOne Text Text
|
|
||||||
| UnacceptableFilter Text
|
|
||||||
| UnacceptableSchema Text [Text]
|
|
||||||
| UnsupportedMethod ByteString
|
|
||||||
| GucHeadersError
|
|
||||||
| GucStatusError
|
|
||||||
| PutMatchingPkError
|
|
||||||
| SingularityError Integer
|
|
||||||
| PGRSTParseError RaiseError
|
|
||||||
| MaxAffectedViolationError Integer
|
|
||||||
| InvalidResourcePath
|
|
||||||
| OpenAPIDisabled
|
|
||||||
| MaxAffectedRpcViolation
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
data QPError = QPError Text Text
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
data RaiseError
|
|
||||||
= MsgParseError ByteString
|
|
||||||
| DetParseError ByteString
|
|
||||||
| NoDetail
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
data RangeError
|
|
||||||
= NegativeLimit
|
|
||||||
| LowerGTUpper
|
|
||||||
| OutOfBounds Text Text
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
-- SCHEMA CACHE ERRORS: PGRST2XX
|
|
||||||
data SchemaCacheError
|
|
||||||
= AmbiguousRelBetween Text Text [Relationship]
|
|
||||||
| AmbiguousRpc [Routine]
|
|
||||||
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
|
||||||
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
|
|
||||||
| ColumnNotFound Text Text
|
|
||||||
| TableNotFound Text Text SchemaCache
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
-- JWT ERRORS: PGRST3XX
|
|
||||||
data JwtError
|
|
||||||
= JwtDecodeErr JwtDecodeError
|
|
||||||
| JwtSecretMissing
|
|
||||||
| JwtTokenRequired
|
|
||||||
| JwtClaimsErr JwtClaimsError
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
data JwtDecodeError
|
|
||||||
= EmptyAuthHeader
|
|
||||||
| UnexpectedParts Int
|
|
||||||
| KeyError Text
|
|
||||||
| BadAlgorithm Text
|
|
||||||
| BadCrypto
|
|
||||||
| UnsupportedTokenType
|
|
||||||
| UnreachableDecodeError
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
data JwtClaimsError
|
|
||||||
= JWTExpired
|
|
||||||
| JWTNotYetValid
|
|
||||||
| JWTIssuedAtFuture
|
|
||||||
| JWTNotInAudience
|
|
||||||
| ParsingClaimsFailed
|
|
||||||
| ExpClaimNotNumber
|
|
||||||
| NbfClaimNotNumber
|
|
||||||
| IatClaimNotNumber
|
|
||||||
| AudClaimNotStringOrArray
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
-- PG ERRORS
|
|
||||||
type Authenticated = Bool
|
|
||||||
data PgError = PgError Authenticated SQL.UsageError
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
-- For parsing byteString to JSON Object, used for allowing full response control
|
|
||||||
data PgRaiseErrMessage = PgRaiseErrMessage {
|
|
||||||
getCode :: Text,
|
|
||||||
getMessage :: Text,
|
|
||||||
getDetails :: Maybe Text,
|
|
||||||
getHint :: Maybe Text
|
|
||||||
}
|
|
||||||
|
|
||||||
data PgRaiseErrDetails = PgRaiseErrDetails {
|
|
||||||
getStatus :: Int,
|
|
||||||
getStatusText :: Maybe Text,
|
|
||||||
getHeaders :: Map Text Text
|
|
||||||
}
|
|
||||||
+16
-10
@@ -11,6 +11,7 @@ import qualified Hasql.Notifications as SQL
|
|||||||
import PostgREST.AppState (AppState, getConfig)
|
import PostgREST.AppState (AppState, getConfig)
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..))
|
||||||
import PostgREST.Observation (Observation (..))
|
import PostgREST.Observation (Observation (..))
|
||||||
|
import PostgREST.Version (prettyVersion)
|
||||||
|
|
||||||
import qualified PostgREST.AppState as AppState
|
import qualified PostgREST.AppState as AppState
|
||||||
import qualified PostgREST.Config as Config
|
import qualified PostgREST.Config as Config
|
||||||
@@ -30,20 +31,20 @@ runListener :: AppState -> IO ()
|
|||||||
runListener appState = do
|
runListener appState = do
|
||||||
AppConfig{..} <- getConfig appState
|
AppConfig{..} <- getConfig appState
|
||||||
when configDbChannelEnabled $
|
when configDbChannelEnabled $
|
||||||
void . forkIO . void $ retryingListen appState
|
void . forkIO . void $ retryingListen appState False
|
||||||
|
|
||||||
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
|
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
|
||||||
-- | This function never returns (but can throw) and return type enforces that.
|
-- | This function never returns (but can throw) and return type enforces that.
|
||||||
retryingListen :: AppState -> IO Void
|
retryingListen :: AppState -> Bool -> IO Void
|
||||||
retryingListen appState = do
|
retryingListen appState hasDbListenerBug = do
|
||||||
cfg@AppConfig{..} <- AppState.getConfig appState
|
AppConfig{..} <- AppState.getConfig appState
|
||||||
let
|
let
|
||||||
dbChannel = toS configDbChannel
|
dbChannel = toS configDbChannel
|
||||||
onError err = do
|
onError err = do
|
||||||
AppState.putIsListenerOn appState False
|
AppState.putIsListenerOn appState False
|
||||||
observer $ DBListenFail dbChannel (Right err)
|
observer $ DBListenFail dbChannel (Right err)
|
||||||
when (isDbListenerBug err) $
|
when (isDbListenerBug err) $
|
||||||
observer DBListenBugHint
|
observer DBListenBugCallQueryFix
|
||||||
unless configDbPoolAutomaticRecovery $
|
unless configDbPoolAutomaticRecovery $
|
||||||
killThread mainThreadId
|
killThread mainThreadId
|
||||||
|
|
||||||
@@ -54,23 +55,23 @@ retryingListen appState = do
|
|||||||
unless (delay == maxDelay) $
|
unless (delay == maxDelay) $
|
||||||
AppState.putNextListenerDelay appState (delay * 2)
|
AppState.putNextListenerDelay appState (delay * 2)
|
||||||
-- loop running the listener
|
-- loop running the listener
|
||||||
retryingListen appState
|
retryingListen appState (isDbListenerBug err)
|
||||||
|
|
||||||
-- Execute the listener with with error handling
|
-- Execute the listener with with error handling
|
||||||
handle onError $ do
|
handle onError $ do
|
||||||
-- Make sure we don't leak connections on errors
|
-- Make sure we don't leak connections on errors
|
||||||
bracket
|
bracket
|
||||||
-- acquire connection
|
-- acquire connection
|
||||||
(SQL.acquire $
|
(SQL.acquire $ toUtf8 (Config.addTargetSessionAttrs $ Config.addFallbackAppName prettyVersion configDbUri))
|
||||||
Config.toConnectionSettings Config.addTargetSessionAttrs cfg)
|
|
||||||
-- release connection
|
-- release connection
|
||||||
(`whenRight` releaseConnection) $
|
(`whenRight` releaseConnection) $
|
||||||
-- use connection
|
-- use connection
|
||||||
\case
|
\case
|
||||||
Right db -> do
|
Right db -> do
|
||||||
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
|
||||||
(pqHost, pqPort) <- SQL.withLibPQConnection db $ bisequence . (LibPQ.host &&& LibPQ.port)
|
(pqHost, pqPort) <- SQL.withLibPQConnection db $ bisequence . (LibPQ.host &&& LibPQ.port)
|
||||||
pgFullName <- SQL.run queryPgVersion db >>= either throwIO (pure . pgvFullName)
|
pgFullName <- SQL.run (queryPgVersion False) db >>= either throwIO (pure . pgvFullName)
|
||||||
|
when hasDbListenerBug $ SQL.run callNotifQueryUsage db >>= either throwIO pure
|
||||||
|
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
||||||
|
|
||||||
AppState.putIsListenerOn appState True
|
AppState.putIsListenerOn appState True
|
||||||
|
|
||||||
@@ -108,3 +109,8 @@ retryingListen appState = do
|
|||||||
releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail) . SQL.release
|
releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail) . SQL.release
|
||||||
|
|
||||||
isDbListenerBug e = "could not access status of transaction" `T.isInfixOf` show e
|
isDbListenerBug e = "could not access status of transaction" `T.isInfixOf` show e
|
||||||
|
|
||||||
|
-- Used to fix a Postgres bug in the listener, see: https://github.com/PostgREST/postgrest/issues/3147#issuecomment-3494591361
|
||||||
|
-- This query advances the async notification query tail, which solves this issue.
|
||||||
|
callNotifQueryUsage :: SQL.Session ()
|
||||||
|
callNotifQueryUsage = SQL.sql "SELECT pg_notification_queue_usage();"
|
||||||
|
|||||||
+101
-92
@@ -1,6 +1,5 @@
|
|||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
{-# LANGUAGE RecursiveDo #-}
|
|
||||||
{-|
|
{-|
|
||||||
Module : PostgREST.Logger
|
Module : PostgREST.Logger
|
||||||
Description : Logging based on the Observation.hs module. Access logs get sent to stdout and server diagnostic get sent to stderr.
|
Description : Logging based on the Observation.hs module. Access logs get sent to stdout and server diagnostic get sent to stderr.
|
||||||
@@ -16,6 +15,7 @@ module PostgREST.Logger
|
|||||||
import Control.AutoUpdate (defaultUpdateSettings,
|
import Control.AutoUpdate (defaultUpdateSettings,
|
||||||
mkAutoUpdate,
|
mkAutoUpdate,
|
||||||
updateAction)
|
updateAction)
|
||||||
|
import Control.Debounce
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.Text.Encoding as T
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
@@ -32,16 +32,13 @@ import qualified Network.Wai.Middleware.RequestLogger as Wai
|
|||||||
import Network.HTTP.Types.Status (Status, status400, status500)
|
import Network.HTTP.Types.Status (Status, status400, status500)
|
||||||
import System.IO.Unsafe (unsafePerformIO)
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
|
|
||||||
import PostgREST.Config (LogLevel (..), Verbosity (..))
|
import PostgREST.Config (LogLevel (..))
|
||||||
import PostgREST.Debounce (makeDebouncer)
|
|
||||||
import PostgREST.Observation
|
import PostgREST.Observation
|
||||||
import PostgREST.Query (MainQuery (..))
|
import PostgREST.Query (MainQuery (..))
|
||||||
import PostgREST.SchemaCache (queryTimingsWLabels)
|
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Hasql.Connection as SQL
|
import qualified Hasql.Connection as SQL
|
||||||
import qualified Hasql.Pool as SQL
|
|
||||||
import qualified Hasql.Pool.Observation as SQL
|
import qualified Hasql.Pool.Observation as SQL
|
||||||
import Numeric (showFFloat)
|
import Numeric (showFFloat)
|
||||||
import PostgREST.Config.PgVersion (pgvName)
|
import PostgREST.Config.PgVersion (pgvName)
|
||||||
@@ -50,18 +47,29 @@ import Protolude
|
|||||||
|
|
||||||
data LoggerState = LoggerState
|
data LoggerState = LoggerState
|
||||||
{ stateGetZTime :: IO ZonedTime -- ^ Time with time zone used for logs
|
{ stateGetZTime :: IO ZonedTime -- ^ Time with time zone used for logs
|
||||||
, stateLogDebouncePoolTimeout :: IO () -- ^ Logs with a debounce
|
, stateLogDebouncePoolTimeout :: MVar (IO ()) -- ^ Logs with a debounce
|
||||||
}
|
}
|
||||||
|
|
||||||
init :: IO LoggerState
|
init :: IO LoggerState
|
||||||
init = mdo
|
init = do
|
||||||
let
|
|
||||||
oneSecond = 1000000
|
|
||||||
loggerState = LoggerState zTime debouncePoolTimeout
|
|
||||||
zTime <- mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
zTime <- mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
||||||
debouncePoolTimeout <- makeDebouncer $
|
LoggerState zTime <$> newEmptyMVar
|
||||||
logWithZTime loggerState (observationMessages PoolAcqTimeoutObs) *> threadDelay (5 * oneSecond)
|
|
||||||
pure loggerState
|
logWithDebounce :: LoggerState -> IO () -> IO ()
|
||||||
|
logWithDebounce loggerState action = do
|
||||||
|
debouncer <- tryReadMVar $ stateLogDebouncePoolTimeout loggerState
|
||||||
|
case debouncer of
|
||||||
|
Just d -> d
|
||||||
|
Nothing -> do
|
||||||
|
newDebouncer <-
|
||||||
|
let oneSecond = 1000000 in
|
||||||
|
mkDebounce defaultDebounceSettings
|
||||||
|
{ debounceAction = action
|
||||||
|
, debounceFreq = 5*oneSecond
|
||||||
|
, debounceEdge = leadingEdge -- logs at the start and the end
|
||||||
|
}
|
||||||
|
putMVar (stateLogDebouncePoolTimeout loggerState) newDebouncer
|
||||||
|
newDebouncer
|
||||||
|
|
||||||
-- TODO stop using this middleware to reuse the same "observer" pattern for all our logs
|
-- TODO stop using this middleware to reuse the same "observer" pattern for all our logs
|
||||||
middleware :: LogLevel -> (Wai.Request -> Maybe BS.ByteString) -> Wai.Middleware
|
middleware :: LogLevel -> (Wai.Request -> Maybe BS.ByteString) -> Wai.Middleware
|
||||||
@@ -88,166 +96,167 @@ shouldLogResponse logLevel = case logLevel of
|
|||||||
-- All observations are logged except some that depend on the log-level
|
-- All observations are logged except some that depend on the log-level
|
||||||
observationLogger :: LoggerState -> LogLevel -> ObservationHandler
|
observationLogger :: LoggerState -> LogLevel -> ObservationHandler
|
||||||
observationLogger loggerState logLevel obs = case obs of
|
observationLogger loggerState logLevel obs = case obs of
|
||||||
PoolAcqTimeoutObs -> do
|
o@(PoolAcqTimeoutObs _) -> do
|
||||||
when (logLevel >= LogError) $
|
when (logLevel >= LogError) $ do
|
||||||
stateLogDebouncePoolTimeout loggerState
|
logWithDebounce loggerState $
|
||||||
|
logWithZTime loggerState $ observationMessage o
|
||||||
o@(QueryErrorCodeHighObs _) -> do
|
o@(QueryErrorCodeHighObs _) -> do
|
||||||
when (logLevel >= LogError) $ do
|
when (logLevel >= LogError) $ do
|
||||||
logWithZTime loggerState $ observationMessages o
|
logWithZTime loggerState $ observationMessage o
|
||||||
o@SchemaCacheEmptyObs ->
|
o@SchemaCacheEmptyObs ->
|
||||||
when (logLevel >= LogError) $ do
|
when (logLevel >= LogError) $ do
|
||||||
logWithZTime loggerState $ observationMessages o
|
logWithZTime loggerState $ observationMessage o
|
||||||
o@(HasqlPoolObs _) -> do
|
o@(HasqlPoolObs _) -> do
|
||||||
when (logLevel >= LogDebug) $ do
|
when (logLevel >= LogDebug) $ do
|
||||||
logWithZTime loggerState $ observationMessages o
|
logWithZTime loggerState $ observationMessage o
|
||||||
o@(QueryObs _ status) -> do
|
QueryObs gq status -> do
|
||||||
when (shouldLogResponse logLevel status) $
|
when (shouldLogResponse logLevel status) $
|
||||||
logWithZTime loggerState $ observationMessages o
|
logMainQ loggerState gq
|
||||||
o@PoolRequest ->
|
o@PoolRequest ->
|
||||||
when (logLevel >= LogDebug) $ do
|
when (logLevel >= LogDebug) $ do
|
||||||
logWithZTime loggerState $ observationMessages o
|
logWithZTime loggerState $ observationMessage o
|
||||||
o@PoolRequestFullfilled ->
|
o@PoolRequestFullfilled ->
|
||||||
when (logLevel >= LogDebug) $ do
|
when (logLevel >= LogDebug) $ do
|
||||||
logWithZTime loggerState $ observationMessages o
|
logWithZTime loggerState $ observationMessage o
|
||||||
o@PoolFlushed ->
|
o@PoolFlushed ->
|
||||||
when (logLevel >= LogDebug) $ do
|
when (logLevel >= LogDebug) $ do
|
||||||
logWithZTime loggerState $ observationMessages o
|
logWithZTime loggerState $ observationMessage o
|
||||||
o@JwtCacheEviction ->
|
o@JwtCacheEviction ->
|
||||||
when (logLevel >= LogDebug) $ do
|
when (logLevel >= LogDebug) $ do
|
||||||
logWithZTime loggerState $ observationMessages o
|
logWithZTime loggerState $ observationMessage o
|
||||||
o@(JwtCacheLookup _) ->
|
o@(JwtCacheLookup _) ->
|
||||||
when (logLevel >= LogDebug) $ do
|
when (logLevel >= LogDebug) $ do
|
||||||
logWithZTime loggerState $ observationMessages o
|
logWithZTime loggerState $ observationMessage o
|
||||||
o@(WarpServerObs _) ->
|
o@(WarpServerObs _) ->
|
||||||
when (logLevel >= LogDebug) $ do
|
when (logLevel >= LogDebug) $ do
|
||||||
logWithZTime loggerState $ observationMessages o
|
logWithZTime loggerState $ observationMessage o
|
||||||
o ->
|
o ->
|
||||||
logWithZTime loggerState $ observationMessages o
|
logWithZTime loggerState $ observationMessage o
|
||||||
|
|
||||||
logWithZTime :: LoggerState -> [Text] -> IO ()
|
logWithZTime :: LoggerState -> Text -> IO ()
|
||||||
logWithZTime loggerState txts = do
|
logWithZTime loggerState txt = do
|
||||||
zTime <- stateGetZTime loggerState
|
zTime <- stateGetZTime loggerState
|
||||||
traverse_ (hPutStrLn stderr . (toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <>)) txts
|
hPutStrLn stderr $ toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <> txt
|
||||||
|
|
||||||
|
logMainQ :: LoggerState -> MainQuery -> IO ()
|
||||||
|
logMainQ loggerState MainQuery{mqOpenAPI=(x, y, z),..} =
|
||||||
|
let snipts = renderSnippet <$> [mqTxVars, fromMaybe mempty mqPreReq, mqMain, x, y, z, fromMaybe mempty mqExplain]
|
||||||
|
-- Does not log SQL when it's empty (happens on OPTIONS requests and when the openapi queries are not generated)
|
||||||
|
logQ q = when (q /= mempty) $ logWithZTime loggerState $ showOnSingleLine '\n' $ T.decodeUtf8 q in
|
||||||
|
mapM_ logQ snipts
|
||||||
|
|
||||||
-- TODO: maybe patch upstream hasql-dynamic-statements so we have a less hackish way to convert
|
-- TODO: maybe patch upstream hasql-dynamic-statements so we have a less hackish way to convert
|
||||||
-- the SQL.Snippet or maybe don't use hasql-dynamic-statements and resort to plain strings for the queries and use regular hasql
|
-- the SQL.Snippet or maybe don't use hasql-dynamic-statements and resort to plain strings for the queries and use regular hasql
|
||||||
renderSnippet :: SQL.Snippet -> ByteString
|
renderSnippet :: SQL.Snippet -> ByteString
|
||||||
renderSnippet snippet =
|
renderSnippet snippet =
|
||||||
let SQL.Statement sql _ _ _ = SQL.dynamicallyParameterized snippet decoder False
|
let SQL.Statement sql _ _ _ = SQL.dynamicallyParameterized snippet decoder prepared
|
||||||
decoder = HD.noResult -- unused
|
decoder = HD.noResult -- unused
|
||||||
|
prepared = False -- unused
|
||||||
in
|
in
|
||||||
sql
|
sql
|
||||||
|
|
||||||
observationMessages :: Observation -> [Text]
|
|
||||||
observationMessages = \case
|
observationMessage :: Observation -> Text
|
||||||
|
observationMessage = \case
|
||||||
AdminStartObs address ->
|
AdminStartObs address ->
|
||||||
pure $ "Admin server listening on " <> address
|
"Admin server listening on " <> address
|
||||||
AppStartObs ver ->
|
AppStartObs ver ->
|
||||||
pure $ "Starting PostgREST " <> T.decodeUtf8 ver <> "..."
|
"Starting PostgREST " <> T.decodeUtf8 ver <> "..."
|
||||||
AppServerAddressObs address ->
|
AppServerAddressObs address ->
|
||||||
pure $ "API server listening on " <> address
|
"API server listening on " <> address
|
||||||
DBConnectedObs ver ->
|
DBConnectedObs ver ->
|
||||||
pure $ "Successfully connected to " <> ver
|
"Successfully connected to " <> ver
|
||||||
ExitUnsupportedPgVersion pgVer minPgVer ->
|
ExitUnsupportedPgVersion pgVer minPgVer ->
|
||||||
pure $ "Cannot run in this PostgreSQL version (" <> pgvName pgVer <> "), PostgREST needs at least " <> pgvName minPgVer
|
"Cannot run in this PostgreSQL version (" <> pgvName pgVer <> "), PostgREST needs at least " <> pgvName minPgVer
|
||||||
ExitDBNoRecoveryObs ->
|
ExitDBNoRecoveryObs ->
|
||||||
pure "Automatic recovery disabled, exiting."
|
"Automatic recovery disabled, exiting."
|
||||||
ExitDBFatalError ServerAuthError usageErr ->
|
ExitDBFatalError ServerAuthError usageErr ->
|
||||||
pure $ "Failed to establish a connection. " <> jsonMessage usageErr
|
"Failed to establish a connection. " <> jsonMessage usageErr
|
||||||
ExitDBFatalError ServerPgrstBug usageErr ->
|
ExitDBFatalError ServerPgrstBug usageErr ->
|
||||||
pure $ "This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. " <> jsonMessage usageErr
|
"This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues. " <> jsonMessage usageErr
|
||||||
ExitDBFatalError ServerError42P05 usageErr ->
|
ExitDBFatalError ServerError42P05 usageErr ->
|
||||||
pure $ "If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
|
"If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
|
||||||
ExitDBFatalError ServerError08P01 usageErr ->
|
ExitDBFatalError ServerError08P01 usageErr ->
|
||||||
pure $ "Connection poolers in statement mode are not supported." <> jsonMessage usageErr
|
"Connection poolers in statement mode are not supported." <> jsonMessage usageErr
|
||||||
SchemaCacheEmptyObs ->
|
SchemaCacheEmptyObs ->
|
||||||
pure $ T.decodeUtf8 . LBS.toStrict . Error.errorPayload Verbose $ Error.NoSchemaCacheError
|
T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.NoSchemaCacheError
|
||||||
SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
|
SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
|
||||||
pure $ "Failed to load the schema cache using "
|
"Failed to load the schema cache using "
|
||||||
<> "db-schemas=" <> T.intercalate "," (toList dbSchemas)
|
<> "db-schemas=" <> T.intercalate "," (toList dbSchemas)
|
||||||
<> " and "
|
<> " and "
|
||||||
<> "db-extra-search-path=" <> T.intercalate "," extraPaths
|
<> "db-extra-search-path=" <> T.intercalate "," extraPaths <> ". " <> jsonMessage usageErr
|
||||||
<> ". " <> jsonMessage usageErr
|
SchemaCacheQueriedObs resultTime ->
|
||||||
SchemaCacheQueriedObs resultTime timings ->
|
"Schema cache queried in " <> showMillis resultTime <> " milliseconds"
|
||||||
[ "Schema cache queried in " <> showMillis resultTime <> " milliseconds " ] <>
|
SchemaCacheSummaryObs summary ->
|
||||||
let showTimings qt = [ T.intercalate ", " $ (\(l, v) -> T.decodeUtf8 l <> ": " <> v <> " ms") <$> queryTimingsWLabels qt ] in
|
"Schema cache loaded " <> summary
|
||||||
maybe mempty showTimings timings
|
SchemaCacheLoadedObs resultTime ->
|
||||||
SchemaCacheLoadedObs resultTime summary ->
|
"Schema cache loaded in " <> showMillis resultTime <> " milliseconds"
|
||||||
[
|
|
||||||
"Schema cache loaded " <> summary
|
|
||||||
, "Schema cache loaded in " <> showMillis resultTime <> " milliseconds"
|
|
||||||
]
|
|
||||||
ConnectionRetryObs delay ->
|
ConnectionRetryObs delay ->
|
||||||
pure $ "Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
|
"Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
|
||||||
QueryPgVersionError usageErr ->
|
QueryPgVersionError usageErr ->
|
||||||
pure $ "Failed to query the PostgreSQL version. " <> jsonMessage usageErr
|
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr
|
||||||
DBListenStart host port fullName channel -> do
|
DBListenStart host port fullName channel -> do
|
||||||
pure $ "Listener connected to " <> fullName <> " on " <> show (fold $ host <> fmap (":" <>) port) <> " and listening for database notifications on the " <> show channel <> " channel"
|
"Listener connected to " <> fullName <> " on " <> show (fold $ host <> fmap (":" <>) port) <> " and listening for database notifications on the " <> show channel <> " channel"
|
||||||
DBListenFail channel listenErr ->
|
DBListenFail channel listenErr ->
|
||||||
pure $ "Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
||||||
either showListenerConnError showListenerException listenErr
|
either showListenerConnError showListenerException listenErr
|
||||||
DBListenRetry delay ->
|
DBListenRetry delay ->
|
||||||
pure $ "Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
||||||
DBListenBugHint ->
|
DBListenBugCallQueryFix ->
|
||||||
pure "HINT: This is likely a bug in the notification queue, try executing the following to solve it: select pg_notification_queue_usage();"
|
"This is likely a PostgreSQL bug in the notification queue, executing the following to try to solve it: SELECT pg_notification_queue_usage();"
|
||||||
DBListenerGotSCacheMsg channel ->
|
DBListenerGotSCacheMsg channel ->
|
||||||
pure $ "Received a schema cache reload message on the " <> show channel <> " channel"
|
"Received a schema cache reload message on the " <> show channel <> " channel"
|
||||||
DBListenerGotConfigMsg channel ->
|
DBListenerGotConfigMsg channel ->
|
||||||
pure $ "Received a config reload message on the " <> show channel <> " channel"
|
"Received a config reload message on the " <> show channel <> " channel"
|
||||||
DBListenerConnectionCleanupFail ex ->
|
DBListenerConnectionCleanupFail ex ->
|
||||||
pure $ "Failed during listener connection cleanup: " <> showOnSingleLine '\t' (show ex)
|
"Failed during listener connection cleanup: " <> showOnSingleLine '\t' (show ex)
|
||||||
(QueryObs MainQuery{mqOpenAPI=(x, y, z),..} _) ->
|
QueryObs{} ->
|
||||||
let snipts = renderSnippet <$> [mqTxVars, fromMaybe mempty mqPreReq, mqMain, x, y, z, fromMaybe mempty mqExplain]
|
mempty -- TODO pending refactor: The logic for printing the query cannot be done here. Join the observationMessage function into observationLogger to avoid this mempty.
|
||||||
in
|
|
||||||
showOnSingleLine '\n' . T.decodeUtf8 <$> filter (/= mempty) snipts
|
|
||||||
ConfigReadErrorObs usageErr ->
|
ConfigReadErrorObs usageErr ->
|
||||||
pure $ "Failed to query database settings for the config parameters." <> jsonMessage usageErr
|
"Failed to query database settings for the config parameters." <> jsonMessage usageErr
|
||||||
QueryRoleSettingsErrorObs usageErr ->
|
QueryRoleSettingsErrorObs usageErr ->
|
||||||
pure $ "Failed to query the role settings. " <> jsonMessage usageErr
|
"Failed to query the role settings. " <> jsonMessage usageErr
|
||||||
QueryErrorCodeHighObs usageErr ->
|
QueryErrorCodeHighObs usageErr ->
|
||||||
pure $ jsonMessage usageErr
|
jsonMessage usageErr
|
||||||
ConfigInvalidObs err ->
|
ConfigInvalidObs err ->
|
||||||
pure $ "Failed reloading config: " <> err
|
"Failed reloading config: " <> err
|
||||||
ConfigSucceededObs ->
|
ConfigSucceededObs ->
|
||||||
pure "Config reloaded"
|
"Config reloaded"
|
||||||
PoolInit poolSize ->
|
PoolInit poolSize ->
|
||||||
pure $ "Connection Pool initialized with a maximum size of " <> show poolSize <> " connections"
|
"Connection Pool initialized with a maximum size of " <> show poolSize <> " connections"
|
||||||
PoolAcqTimeoutObs -> pure $ jsonMessage SQL.AcquisitionTimeoutUsageError
|
PoolAcqTimeoutObs usageErr ->
|
||||||
|
jsonMessage usageErr
|
||||||
HasqlPoolObs (SQL.ConnectionObservation uuid status) ->
|
HasqlPoolObs (SQL.ConnectionObservation uuid status) ->
|
||||||
pure $ "Connection " <> show uuid <> (
|
"Connection " <> show uuid <> (
|
||||||
case status of
|
case status of
|
||||||
SQL.ConnectingConnectionStatus -> " is being established"
|
SQL.ConnectingConnectionStatus -> " is being established"
|
||||||
SQL.ReadyForUseConnectionStatus reason -> " is available due to " <> case reason of
|
SQL.ReadyForUseConnectionStatus -> " is available"
|
||||||
SQL.EstablishedConnectionReadyForUseReason -> "connection establishment"
|
|
||||||
SQL.SessionFailedConnectionReadyForUseReason _ -> "session failure"
|
|
||||||
SQL.SessionSucceededConnectionReadyForUseReason -> "session success"
|
|
||||||
SQL.InUseConnectionStatus -> " is used"
|
SQL.InUseConnectionStatus -> " is used"
|
||||||
SQL.TerminatedConnectionStatus reason -> " is terminated due to " <> case reason of
|
SQL.TerminatedConnectionStatus reason -> " is terminated due to " <> case reason of
|
||||||
SQL.AgingConnectionTerminationReason -> "max lifetime"
|
SQL.AgingConnectionTerminationReason -> "max lifetime"
|
||||||
SQL.IdlenessConnectionTerminationReason -> "max idletime"
|
SQL.IdlenessConnectionTerminationReason -> "max idletime"
|
||||||
SQL.ReleaseConnectionTerminationReason -> "release"
|
SQL.ReleaseConnectionTerminationReason -> "release"
|
||||||
SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message.
|
SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message.
|
||||||
SQL.InitializationErrorTerminationReason _ -> "init failure"
|
|
||||||
)
|
)
|
||||||
PoolRequest ->
|
PoolRequest ->
|
||||||
pure "Trying to borrow a connection from pool"
|
"Trying to borrow a connection from pool"
|
||||||
PoolRequestFullfilled ->
|
PoolRequestFullfilled ->
|
||||||
pure "Borrowed a connection from the pool"
|
"Borrowed a connection from the pool"
|
||||||
PoolFlushed ->
|
PoolFlushed ->
|
||||||
pure "Database connection pool flushed"
|
"Database connection pool flushed"
|
||||||
JwtCacheLookup _ ->
|
JwtCacheLookup _ ->
|
||||||
pure "Looked up a JWT in JWT cache"
|
"Looked up a JWT in JWT cache"
|
||||||
JwtCacheEviction ->
|
JwtCacheEviction ->
|
||||||
pure "Evicted entry from JWT cache"
|
"Evicted entry from JWT cache"
|
||||||
TerminationUnixSignalObs signal ->
|
TerminationUnixSignalObs signal ->
|
||||||
pure $ "Received termination unix signal " <> signal
|
"Received termination unix signal " <> signal
|
||||||
WarpServerObs txt ->
|
WarpServerObs txt ->
|
||||||
pure $ "Warp server: " <> txt
|
"Warp server: " <> txt
|
||||||
where
|
where
|
||||||
showMillis :: Double -> Text
|
showMillis :: Double -> Text
|
||||||
showMillis x = toS $ showFFloat (Just 1) x ""
|
showMillis x = toS $ showFFloat (Just 1) x ""
|
||||||
|
|
||||||
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload Verbose $ Error.PgError False err
|
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
|
||||||
|
|
||||||
|
|
||||||
showListenerConnError :: SQL.ConnectionError -> Text
|
showListenerConnError :: SQL.ConnectionError -> Text
|
||||||
|
|||||||
@@ -96,8 +96,9 @@ data ResultSet
|
|||||||
mainTx :: MainQuery -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> MainTx
|
mainTx :: MainQuery -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> MainTx
|
||||||
mainTx _ _ _ _ (NoDb x) _ = NoDbTx $ NoDbResult x
|
mainTx _ _ _ _ (NoDb x) _ = NoDbTx $ NoDbResult x
|
||||||
mainTx genQ@MainQuery{..} conf@AppConfig{..} AuthResult{..} apiReq (Db plan) sCache =
|
mainTx genQ@MainQuery{..} conf@AppConfig{..} AuthResult{..} apiReq (Db plan) sCache =
|
||||||
DbTx isoLvl txMode dbHandler SQL.transactionNoRetry
|
DbTx isoLvl txMode dbHandler transaction
|
||||||
where
|
where
|
||||||
|
transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction
|
||||||
isoLvl = planIsoLvl conf authRole plan
|
isoLvl = planIsoLvl conf authRole plan
|
||||||
txMode = planTxMode plan
|
txMode = planTxMode plan
|
||||||
dbHandler = do
|
dbHandler = do
|
||||||
@@ -221,7 +222,7 @@ failPut :: ResultSet -> DbHandler ()
|
|||||||
failPut RSStandard{rsQueryTotal=queryTotal} =
|
failPut RSStandard{rsQueryTotal=queryTotal} =
|
||||||
when (queryTotal /= 1) $ do
|
when (queryTotal /= 1) $ do
|
||||||
lift SQL.condemn
|
lift SQL.condemn
|
||||||
throwError $ Error.ApiRequestErr Error.PutMatchingPkError
|
throwError $ Error.ApiRequestError Error.PutMatchingPkError
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- Fail a response if a single JSON object was requested and not exactly one
|
-- Fail a response if a single JSON object was requested and not exactly one
|
||||||
@@ -230,13 +231,13 @@ failNotSingular :: MediaType -> ResultSet -> DbHandler ()
|
|||||||
failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =
|
failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =
|
||||||
when (elem mediaType [MTVndSingularJSON True, MTVndSingularJSON False] && queryTotal /= 1) $ do
|
when (elem mediaType [MTVndSingularJSON True, MTVndSingularJSON False] && queryTotal /= 1) $ do
|
||||||
lift SQL.condemn
|
lift SQL.condemn
|
||||||
throwError $ Error.ApiRequestErr . Error.SingularityError $ toInteger queryTotal
|
throwError $ Error.ApiRequestError . Error.SingularityError $ toInteger queryTotal
|
||||||
|
|
||||||
failExceedsMaxAffectedPref :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> ResultSet -> DbHandler ()
|
failExceedsMaxAffectedPref :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> ResultSet -> DbHandler ()
|
||||||
failExceedsMaxAffectedPref (Nothing,_) _ = pure ()
|
failExceedsMaxAffectedPref (Nothing,_) _ = pure ()
|
||||||
failExceedsMaxAffectedPref (Just (PreferMaxAffected n), handling) RSStandard{rsQueryTotal=queryTotal} = when ((queryTotal > n) && (handling == Just Strict)) $ do
|
failExceedsMaxAffectedPref (Just (PreferMaxAffected n), handling) RSStandard{rsQueryTotal=queryTotal} = when ((queryTotal > n) && (handling == Just Strict)) $ do
|
||||||
lift SQL.condemn
|
lift SQL.condemn
|
||||||
throwError $ Error.ApiRequestErr . Error.MaxAffectedViolationError $ toInteger queryTotal
|
throwError $ Error.ApiRequestError . Error.MaxAffectedViolationError $ toInteger queryTotal
|
||||||
|
|
||||||
-- | Set a transaction to roll back if requested
|
-- | Set a transaction to roll back if requested
|
||||||
optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
|
optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
|
||||||
|
|||||||
+55
-13
@@ -5,7 +5,10 @@ Description : Metrics based on the Observation module. See Observation.hs.
|
|||||||
-}
|
-}
|
||||||
module PostgREST.Metrics
|
module PostgREST.Metrics
|
||||||
( init
|
( init
|
||||||
|
, ConnTrack
|
||||||
|
, ConnStats (..)
|
||||||
, MetricsState (..)
|
, MetricsState (..)
|
||||||
|
, connectionCounts
|
||||||
, observationMetrics
|
, observationMetrics
|
||||||
, metricsToText
|
, metricsToText
|
||||||
) where
|
) where
|
||||||
@@ -17,12 +20,18 @@ import Prometheus
|
|||||||
|
|
||||||
import PostgREST.Observation
|
import PostgREST.Observation
|
||||||
|
|
||||||
import Protolude
|
import Control.Arrow ((&&&))
|
||||||
|
import Data.Bitraversable (bisequenceA)
|
||||||
|
import Data.Tuple.Extra (both)
|
||||||
|
import Data.UUID (UUID)
|
||||||
|
import qualified Focus
|
||||||
|
import Protolude
|
||||||
|
import qualified StmHamt.SizedHamt as SH
|
||||||
|
|
||||||
data MetricsState =
|
data MetricsState =
|
||||||
MetricsState {
|
MetricsState {
|
||||||
poolTimeouts :: Counter,
|
poolTimeouts :: Counter,
|
||||||
poolAvailable :: Gauge,
|
connTrack :: ConnTrack,
|
||||||
poolWaiting :: Gauge,
|
poolWaiting :: Gauge,
|
||||||
poolMaxSize :: Gauge,
|
poolMaxSize :: Gauge,
|
||||||
schemaCacheLoads :: Vector Label1 Counter,
|
schemaCacheLoads :: Vector Label1 Counter,
|
||||||
@@ -36,7 +45,7 @@ init :: Int -> IO MetricsState
|
|||||||
init configDbPoolSize = do
|
init configDbPoolSize = do
|
||||||
metricState <- MetricsState <$>
|
metricState <- MetricsState <$>
|
||||||
register (counter (Info "pgrst_db_pool_timeouts_total" "The total number of pool connection timeouts")) <*>
|
register (counter (Info "pgrst_db_pool_timeouts_total" "The total number of pool connection timeouts")) <*>
|
||||||
register (gauge (Info "pgrst_db_pool_available" "Available connections in the pool")) <*>
|
register (Metric ((identity &&& dbPoolAvailable) <$> connectionTracker)) <*>
|
||||||
register (gauge (Info "pgrst_db_pool_waiting" "Requests waiting to acquire a pool connection")) <*>
|
register (gauge (Info "pgrst_db_pool_waiting" "Requests waiting to acquire a pool connection")) <*>
|
||||||
register (gauge (Info "pgrst_db_pool_max" "Max pool connections")) <*>
|
register (gauge (Info "pgrst_db_pool_max" "Max pool connections")) <*>
|
||||||
register (vector "status" $ counter (Info "pgrst_schema_cache_loads_total" "The total number of times the schema cache was loaded")) <*>
|
register (vector "status" $ counter (Info "pgrst_schema_cache_loads_total" "The total number of times the schema cache was loaded")) <*>
|
||||||
@@ -46,25 +55,33 @@ init configDbPoolSize = do
|
|||||||
register (counter (Info "pgrst_jwt_cache_evictions_total" "The total number of JWT cache evictions"))
|
register (counter (Info "pgrst_jwt_cache_evictions_total" "The total number of JWT cache evictions"))
|
||||||
setGauge (poolMaxSize metricState) (fromIntegral configDbPoolSize)
|
setGauge (poolMaxSize metricState) (fromIntegral configDbPoolSize)
|
||||||
pure metricState
|
pure metricState
|
||||||
|
where
|
||||||
|
dbPoolAvailable = (pure . noLabelsGroup (Info "pgrst_db_pool_available" "Available connections in the pool") GaugeType . calcAvailable <$>) . connectionCounts
|
||||||
|
where
|
||||||
|
calcAvailable = liftA2 (-) connected inUse
|
||||||
|
toSample name labels = Sample name labels . encodeUtf8 . show
|
||||||
|
noLabelsGroup info sampleType = SampleGroup info sampleType . pure . toSample (metricName info) mempty
|
||||||
|
|
||||||
-- Only some observations are used as metrics
|
-- Only some observations are used as metrics
|
||||||
observationMetrics :: MetricsState -> ObservationHandler
|
observationMetrics :: MetricsState -> ObservationHandler
|
||||||
observationMetrics MetricsState{..} obs = case obs of
|
observationMetrics MetricsState{..} obs = case obs of
|
||||||
PoolAcqTimeoutObs -> do
|
(PoolAcqTimeoutObs _) -> do
|
||||||
incCounter poolTimeouts
|
incCounter poolTimeouts
|
||||||
(HasqlPoolObs (SQL.ConnectionObservation _ status)) -> case status of
|
-- Handle pool observations with connection tracking
|
||||||
SQL.ReadyForUseConnectionStatus _ -> do
|
-- this is necessary because it is not possible
|
||||||
incGauge poolAvailable
|
-- to accurately maintain open/in use conneciton counts
|
||||||
SQL.InUseConnectionStatus -> do
|
-- statelessly based only on pool observation events.
|
||||||
decGauge poolAvailable
|
-- The reason is that hasql-pool emits TerminatedConnectionStatus
|
||||||
SQL.TerminatedConnectionStatus _ -> do
|
-- both for connections successfully established and failed when connecting.
|
||||||
decGauge poolAvailable
|
-- When receiving TerminatedConnectionStatus we have to find out
|
||||||
SQL.ConnectingConnectionStatus -> pure ()
|
-- if we can decrement established connection count. To do that we have to track
|
||||||
|
-- established connections.
|
||||||
|
(HasqlPoolObs sqlObs) -> trackConnections connTrack sqlObs
|
||||||
PoolRequest ->
|
PoolRequest ->
|
||||||
incGauge poolWaiting
|
incGauge poolWaiting
|
||||||
PoolRequestFullfilled ->
|
PoolRequestFullfilled ->
|
||||||
decGauge poolWaiting
|
decGauge poolWaiting
|
||||||
SchemaCacheLoadedObs resTime _ -> do
|
SchemaCacheLoadedObs resTime -> do
|
||||||
withLabel schemaCacheLoads "SUCCESS" incCounter
|
withLabel schemaCacheLoads "SUCCESS" incCounter
|
||||||
setGauge schemaCacheQueryTime resTime
|
setGauge schemaCacheQueryTime resTime
|
||||||
SchemaCacheErrorObs{} -> do
|
SchemaCacheErrorObs{} -> do
|
||||||
@@ -77,3 +94,28 @@ observationMetrics MetricsState{..} obs = case obs of
|
|||||||
|
|
||||||
metricsToText :: IO LBS.ByteString
|
metricsToText :: IO LBS.ByteString
|
||||||
metricsToText = exportMetricsAsText
|
metricsToText = exportMetricsAsText
|
||||||
|
|
||||||
|
data ConnStats = ConnStats {
|
||||||
|
connected :: Int,
|
||||||
|
inUse :: Int
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
data ConnTrack = ConnTrack { connTrackConnected :: SH.SizedHamt UUID, connTrackInUse :: SH.SizedHamt UUID }
|
||||||
|
|
||||||
|
connectionTracker :: IO ConnTrack
|
||||||
|
connectionTracker = ConnTrack <$> SH.newIO <*> SH.newIO
|
||||||
|
|
||||||
|
trackConnections :: ConnTrack -> SQL.Observation -> IO ()
|
||||||
|
trackConnections ConnTrack{..} (SQL.ConnectionObservation uuid status) = case status of
|
||||||
|
SQL.ReadyForUseConnectionStatus -> atomically $
|
||||||
|
SH.insert identity uuid connTrackConnected *>
|
||||||
|
SH.focus Focus.delete identity uuid connTrackInUse
|
||||||
|
SQL.TerminatedConnectionStatus _ -> atomically $
|
||||||
|
SH.focus Focus.delete identity uuid connTrackConnected *>
|
||||||
|
SH.focus Focus.delete identity uuid connTrackInUse
|
||||||
|
SQL.InUseConnectionStatus -> atomically $
|
||||||
|
SH.insert identity uuid connTrackInUse
|
||||||
|
_ -> mempty
|
||||||
|
|
||||||
|
connectionCounts :: ConnTrack -> IO ConnStats
|
||||||
|
connectionCounts = atomically . fmap (uncurry ConnStats) . bisequenceA . both SH.size . (connTrackConnected &&& connTrackInUse)
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import qualified Hasql.Pool.Observation as SQL
|
|||||||
import Network.HTTP.Types.Status (Status)
|
import Network.HTTP.Types.Status (Status)
|
||||||
import PostgREST.Config.PgVersion
|
import PostgREST.Config.PgVersion
|
||||||
import PostgREST.Query (MainQuery)
|
import PostgREST.Query (MainQuery)
|
||||||
import PostgREST.SchemaCache (QueryTimings)
|
|
||||||
|
|
||||||
import Protolude hiding (toList)
|
import Protolude hiding (toList)
|
||||||
|
|
||||||
@@ -32,13 +31,14 @@ data Observation
|
|||||||
| DBConnectedObs Text
|
| DBConnectedObs Text
|
||||||
| SchemaCacheEmptyObs
|
| SchemaCacheEmptyObs
|
||||||
| SchemaCacheErrorObs (NonEmpty Text) [Text] SQL.UsageError
|
| SchemaCacheErrorObs (NonEmpty Text) [Text] SQL.UsageError
|
||||||
| SchemaCacheQueriedObs Double (Maybe QueryTimings)
|
| SchemaCacheQueriedObs Double
|
||||||
| SchemaCacheLoadedObs Double Text
|
| SchemaCacheSummaryObs Text
|
||||||
|
| SchemaCacheLoadedObs Double
|
||||||
| ConnectionRetryObs Int
|
| ConnectionRetryObs Int
|
||||||
| DBListenStart (Maybe ByteString) (Maybe ByteString) Text Text -- host, port, version string, channel
|
| DBListenStart (Maybe ByteString) (Maybe ByteString) Text Text -- host, port, version string, channel
|
||||||
| DBListenFail Text (Either SQL.ConnectionError SomeException)
|
| DBListenFail Text (Either SQL.ConnectionError SomeException)
|
||||||
| DBListenRetry Int
|
| DBListenRetry Int
|
||||||
| DBListenBugHint -- https://github.com/PostgREST/postgrest/issues/3147
|
| DBListenBugCallQueryFix
|
||||||
| DBListenerGotSCacheMsg ByteString
|
| DBListenerGotSCacheMsg ByteString
|
||||||
| DBListenerGotConfigMsg ByteString
|
| DBListenerGotConfigMsg ByteString
|
||||||
| DBListenerConnectionCleanupFail SomeException
|
| DBListenerConnectionCleanupFail SomeException
|
||||||
@@ -50,7 +50,7 @@ data Observation
|
|||||||
| QueryErrorCodeHighObs SQL.UsageError
|
| QueryErrorCodeHighObs SQL.UsageError
|
||||||
| QueryPgVersionError SQL.UsageError
|
| QueryPgVersionError SQL.UsageError
|
||||||
| PoolInit Int
|
| PoolInit Int
|
||||||
| PoolAcqTimeoutObs
|
| PoolAcqTimeoutObs SQL.UsageError
|
||||||
| HasqlPoolObs SQL.Observation
|
| HasqlPoolObs SQL.Observation
|
||||||
| PoolRequest
|
| PoolRequest
|
||||||
| PoolRequestFullfilled
|
| PoolRequestFullfilled
|
||||||
|
|||||||
+51
-17
@@ -43,7 +43,6 @@ import PostgREST.Error (ApiRequestError (..),
|
|||||||
Error (..),
|
Error (..),
|
||||||
SchemaCacheError (..))
|
SchemaCacheError (..))
|
||||||
import PostgREST.MediaType (MediaType (..))
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import PostgREST.Plan.Negotiate (negotiateContent)
|
|
||||||
import PostgREST.Query.SqlFragment (sourceCTEName)
|
import PostgREST.Query.SqlFragment (sourceCTEName)
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
convertToLimitZeroRange,
|
convertToLimitZeroRange,
|
||||||
@@ -51,6 +50,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
|
|||||||
import PostgREST.SchemaCache (SchemaCache (..))
|
import PostgREST.SchemaCache (SchemaCache (..))
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
|
RelIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
@@ -60,6 +60,8 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
|||||||
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
|
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
|
||||||
RepresentationsMap)
|
RepresentationsMap)
|
||||||
import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
||||||
|
MediaHandlerMap,
|
||||||
|
ResolvedHandler,
|
||||||
Routine (..),
|
Routine (..),
|
||||||
RoutineMap,
|
RoutineMap,
|
||||||
RoutineParam (..),
|
RoutineParam (..),
|
||||||
@@ -172,8 +174,8 @@ wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest
|
|||||||
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do
|
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do
|
||||||
qi <- findTable identifier sCache
|
qi <- findTable identifier sCache
|
||||||
rPlan <- readPlan qi conf sCache apiRequest
|
rPlan <- readPlan qi conf sCache apiRequest
|
||||||
(handler, mediaType) <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()
|
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||||
return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly qi
|
return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly qi
|
||||||
|
|
||||||
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan
|
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan
|
||||||
@@ -181,8 +183,8 @@ mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..}
|
|||||||
qi <- findTable identifier sCache
|
qi <- findTable identifier sCache
|
||||||
rPlan <- readPlan qi conf sCache apiRequest
|
rPlan <- readPlan qi conf sCache apiRequest
|
||||||
mPlan <- mutatePlan mutation qi apiRequest sCache rPlan
|
mPlan <- mutatePlan mutation qi apiRequest sCache rPlan
|
||||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()
|
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||||
(handler, mediaType) <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||||
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi
|
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi
|
||||||
|
|
||||||
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CrudPlan
|
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CrudPlan
|
||||||
@@ -204,15 +206,15 @@ callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferenc
|
|||||||
(Inv, Routine.Immutable) -> SQL.Read
|
(Inv, Routine.Immutable) -> SQL.Read
|
||||||
(Inv, Routine.Volatile) -> SQL.Write
|
(Inv, Routine.Volatile) -> SQL.Write
|
||||||
cPlan = callPlan proc apiRequest paramKeys args rPlan
|
cPlan = callPlan proc apiRequest paramKeys args rPlan
|
||||||
(handler, mediaType) <- mapLeft ApiRequestErr $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestErr $ InvalidPreferences invalidPrefs else Right ()
|
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||||
failMaxAffectedRpcReturnsSingle (preferMaxAffected, preferHandling) proc
|
failMaxAffectedRpcReturnsSingle (preferMaxAffected, preferHandling) proc
|
||||||
return $ CallReadPlan rPlan cPlan txMode proc handler mediaType invMethod identifier
|
return $ CallReadPlan rPlan cPlan txMode proc handler mediaType invMethod identifier
|
||||||
where
|
where
|
||||||
qsParams' = QueryParams.qsParams iQueryParams
|
qsParams' = QueryParams.qsParams iQueryParams
|
||||||
|
|
||||||
failMaxAffectedRpcReturnsSingle :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> Routine -> Either Error ()
|
failMaxAffectedRpcReturnsSingle :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> Routine -> Either Error ()
|
||||||
failMaxAffectedRpcReturnsSingle (Just (PreferMaxAffected _), Just Strict) rout = if funcReturnsSingle rout then Left $ ApiRequestErr MaxAffectedRpcViolation else Right ()
|
failMaxAffectedRpcReturnsSingle (Just (PreferMaxAffected _), Just Strict) rout = if funcReturnsSingle rout then Left $ ApiRequestError MaxAffectedRpcViolation else Right ()
|
||||||
failMaxAffectedRpcReturnsSingle _ _ = Right ()
|
failMaxAffectedRpcReturnsSingle _ _ = Right ()
|
||||||
|
|
||||||
hasDefaultSelect :: ReadPlanTree -> Bool
|
hasDefaultSelect :: ReadPlanTree -> Bool
|
||||||
@@ -225,7 +227,7 @@ inspectPlan apiRequest headersOnly schema = do
|
|||||||
accepts = iAcceptMediaType apiRequest
|
accepts = iAcceptMediaType apiRequest
|
||||||
mediaType <- if not . null $ L.intersect accepts producedMTs
|
mediaType <- if not . null $ L.intersect accepts producedMTs
|
||||||
then Right MTOpenAPI
|
then Right MTOpenAPI
|
||||||
else Left . ApiRequestErr . MediaTypeError $ MediaType.toMime <$> accepts
|
else Left . ApiRequestError . MediaTypeError $ MediaType.toMime <$> accepts
|
||||||
return $ InspectPlan mediaType SQL.Read headersOnly schema
|
return $ InspectPlan mediaType SQL.Read headersOnly schema
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
@@ -784,7 +786,7 @@ hoistIntoRelSelectFields _ r = r
|
|||||||
-- to order once it's aggregated if it's not selected in the inner query beforehand.
|
-- to order once it's aggregated if it's not selected in the inner query beforehand.
|
||||||
addToManyOrderSelects :: ReadPlanTree -> Either Error ReadPlanTree
|
addToManyOrderSelects :: ReadPlanTree -> Either Error ReadPlanTree
|
||||||
addToManyOrderSelects (Node rp@ReadPlan{order, select, relAggAlias, relSelect, relSpread = Just ToManySpread {}} forest)
|
addToManyOrderSelects (Node rp@ReadPlan{order, select, relAggAlias, relSelect, relSpread = Just ToManySpread {}} forest)
|
||||||
| anyAggSel || anyAggRelSel = Left $ ApiRequestErr $ NotImplemented "Aggregates are not implemented for one-to-many or many-to-many spreads."
|
| anyAggSel || anyAggRelSel = Left $ ApiRequestError $ NotImplemented "Aggregates are not implemented for one-to-many or many-to-many spreads."
|
||||||
| otherwise = Node rp { order = [], relSpread = newRelSpread } <$> addToManyOrderSelects `traverse` forest
|
| otherwise = Node rp { order = [], relSpread = newRelSpread } <$> addToManyOrderSelects `traverse` forest
|
||||||
where
|
where
|
||||||
newRelSpread = Just ToManySpread { stExtraSelect = addSprExtraSelects, stOrder = addSprOrder}
|
newRelSpread = Just ToManySpread { stExtraSelect = addSprExtraSelects, stOrder = addSprOrder}
|
||||||
@@ -806,7 +808,7 @@ addToManyOrderSelects (Node rp forest) = Node rp <$> addToManyOrderSelects `trav
|
|||||||
|
|
||||||
validateAggFunctions :: Bool -> ReadPlanTree -> Either Error ReadPlanTree
|
validateAggFunctions :: Bool -> ReadPlanTree -> Either Error ReadPlanTree
|
||||||
validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
|
validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
|
||||||
| not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left $ ApiRequestErr AggregatesNotAllowed
|
| not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left $ ApiRequestError AggregatesNotAllowed
|
||||||
| otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
|
| otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
|
||||||
|
|
||||||
-- | Lookup table in the schema cache before creating read plan
|
-- | Lookup table in the schema cache before creating read plan
|
||||||
@@ -860,9 +862,9 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
name = fromMaybe relName relAlias in
|
name = fromMaybe relName relAlias in
|
||||||
if isToOne == Just True
|
if isToOne == Just True
|
||||||
then Right $ cot{coRelation=relAggAlias}
|
then Right $ cot{coRelation=relAggAlias}
|
||||||
else Left $ ApiRequestErr $ RelatedOrderNotToOne (qiName from) name
|
else Left $ ApiRequestError $ RelatedOrderNotToOne (qiName from) name
|
||||||
Nothing ->
|
Nothing ->
|
||||||
Left $ ApiRequestErr $ NotEmbedded coRelation
|
Left $ ApiRequestError $ NotEmbedded coRelation
|
||||||
|
|
||||||
-- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null`
|
-- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null`
|
||||||
--
|
--
|
||||||
@@ -956,7 +958,7 @@ addRanges ApiRequest{..} rReq =
|
|||||||
_ -> foldr addRangeToNode (Right rReq) =<< ranges
|
_ -> foldr addRangeToNode (Right rReq) =<< ranges
|
||||||
where
|
where
|
||||||
ranges :: Either Error [(EmbedPath, NonnegRange)]
|
ranges :: Either Error [(EmbedPath, NonnegRange)]
|
||||||
ranges = first (ApiRequestErr . QueryParamError) $ QueryParams.pRequestRange `traverse` HM.toList iRange
|
ranges = first (ApiRequestError . QueryParamError) $ QueryParams.pRequestRange `traverse` HM.toList iRange
|
||||||
|
|
||||||
addRangeToNode :: (EmbedPath, NonnegRange) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
addRangeToNode :: (EmbedPath, NonnegRange) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
||||||
addRangeToNode = updateNode (\r (Node q f) -> Node q{range_=r} f)
|
addRangeToNode = updateNode (\r (Node q f) -> Node q{range_=r} f)
|
||||||
@@ -990,13 +992,13 @@ updateNode f ([], a) rr = f a <$> rr
|
|||||||
updateNode _ _ (Left e) = Left e
|
updateNode _ _ (Left e) = Left e
|
||||||
updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
|
updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
|
||||||
case findNode of
|
case findNode of
|
||||||
Nothing -> Left $ ApiRequestErr $ NotEmbedded targetNodeName
|
Nothing -> Left $ ApiRequestError $ NotEmbedded targetNodeName
|
||||||
Just target ->
|
Just target ->
|
||||||
(\node -> Node rootNode $ node : delete target forest) <$>
|
(\node -> Node rootNode $ node : delete target forest) <$>
|
||||||
updateNode f (remainingPath, a) (Right target)
|
updateNode f (remainingPath, a) (Right target)
|
||||||
where
|
where
|
||||||
findNode :: Maybe ReadPlanTree
|
findNode :: Maybe ReadPlanTree
|
||||||
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> fromMaybe relName relAlias == targetNodeName) forest
|
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> relName == targetNodeName || relAlias == Just targetNodeName) forest
|
||||||
|
|
||||||
mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan
|
mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan
|
||||||
mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq =
|
mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq =
|
||||||
@@ -1014,7 +1016,7 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
|
|||||||
_ -> False) qsFiltersRoot
|
_ -> False) qsFiltersRoot
|
||||||
then mapRight (\typedColumns -> Insert qi typedColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty False) typedColumnsOrError
|
then mapRight (\typedColumns -> Insert qi typedColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty False) typedColumnsOrError
|
||||||
else
|
else
|
||||||
Left $ ApiRequestErr InvalidFilters
|
Left $ ApiRequestError InvalidFilters
|
||||||
MutationDelete -> Right $ Delete qi combinedLogic returnings
|
MutationDelete -> Right $ Delete qi combinedLogic returnings
|
||||||
where
|
where
|
||||||
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
||||||
@@ -1121,3 +1123,35 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
|||||||
-- they are later concatenated with AND in the QueryBuilder
|
-- they are later concatenated with AND in the QueryBuilder
|
||||||
addFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleLogicTree]
|
addFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleLogicTree]
|
||||||
addFilterToLogicForest flt lf = CoercibleStmnt flt : lf
|
addFilterToLogicForest flt lf = CoercibleStmnt flt : lf
|
||||||
|
|
||||||
|
-- | Do content negotiation. i.e. choose a media type based on the intersection of accepted/produced media types.
|
||||||
|
negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Bool -> Either ApiRequestError ResolvedHandler
|
||||||
|
negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces defaultSelect =
|
||||||
|
case (act, firstAcceptedPick) of
|
||||||
|
(_, Nothing) -> Left . MediaTypeError $ map MediaType.toMime accepts
|
||||||
|
(ActDb (ActRelationMut _ _), Just (x, mt)) -> Right (if rep == Just Full then x else NoAgg, mt)
|
||||||
|
-- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849
|
||||||
|
-- TODO: despite no aggregate, these are responding with a Content-Type, which is not correct.
|
||||||
|
(ActDb (ActRelationRead _ True), Just (_, mt)) -> Right (NoAgg, mt)
|
||||||
|
(ActDb (ActRoutine _ (InvRead True)), Just (_, mt)) -> Right (NoAgg, mt)
|
||||||
|
(_, Just (x, mt)) -> Right (x, mt)
|
||||||
|
where
|
||||||
|
firstAcceptedPick = listToMaybe $ mapMaybe matchMT accepts -- If there are multiple accepted media types, pick the first. This is usual in content negotiation.
|
||||||
|
matchMT mt = case mt of
|
||||||
|
-- all the vendored media types have special handling as they have media type parameters, they cannot be overridden
|
||||||
|
m@(MTVndSingularJSON strip) -> Just (BuiltinAggSingleJson strip, m)
|
||||||
|
m@MTVndArrayJSONStrip -> Just (BuiltinAggArrayJsonStrip, m)
|
||||||
|
m@(MTVndPlan (MTVndSingularJSON strip) _ _) -> mtPlanToNothing $ Just (BuiltinAggSingleJson strip, m)
|
||||||
|
m@(MTVndPlan MTVndArrayJSONStrip _ _) -> mtPlanToNothing $ Just (BuiltinAggArrayJsonStrip, m)
|
||||||
|
-- TODO the plan should have its own MediaHandler instead of relying on MediaType
|
||||||
|
m@(MTVndPlan mType _ _) -> mtPlanToNothing $ ((,) . fst <$> lookupHandler mType) <*> pure m
|
||||||
|
-- all the other media types can be overridden
|
||||||
|
x -> lookupHandler x
|
||||||
|
mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed
|
||||||
|
lookupHandler mt =
|
||||||
|
when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) <|> -- lookup for identifier and `*/*`
|
||||||
|
when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|> -- lookup for identifier and a particular media type
|
||||||
|
HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type
|
||||||
|
when' :: Bool -> Maybe a -> Maybe a
|
||||||
|
when' True (Just a) = Just a
|
||||||
|
when' _ _ = Nothing
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
{-|
|
|
||||||
Module : PostgREST.Plan.Negotiate
|
|
||||||
Description : PostgREST Content Negotiation
|
|
||||||
|
|
||||||
This module contains logic for content negotiation.
|
|
||||||
RFC: https://datatracker.ietf.org/doc/html/rfc7231#section-3.4
|
|
||||||
-}
|
|
||||||
|
|
||||||
module PostgREST.Plan.Negotiate
|
|
||||||
( negotiateContent
|
|
||||||
) where
|
|
||||||
|
|
||||||
import qualified Data.HashMap.Strict as HM
|
|
||||||
|
|
||||||
import PostgREST.ApiRequest (ApiRequest (..))
|
|
||||||
import PostgREST.Config (AppConfig (..))
|
|
||||||
import PostgREST.Error (ApiRequestError (..))
|
|
||||||
import PostgREST.MediaType (MediaType (..))
|
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
|
||||||
RelIdentifier (..))
|
|
||||||
import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
|
||||||
MediaHandlerMap,
|
|
||||||
ResolvedHandler)
|
|
||||||
|
|
||||||
import PostgREST.ApiRequest.Preferences
|
|
||||||
import PostgREST.ApiRequest.Types
|
|
||||||
import qualified PostgREST.MediaType as MediaType
|
|
||||||
|
|
||||||
import Protolude hiding (from)
|
|
||||||
|
|
||||||
-- We have two general cases of return values from database objects
|
|
||||||
-- (tables/views/functions):
|
|
||||||
--
|
|
||||||
-- 1. "un-mime-typed" values, in most of the cases this is a composite/row
|
|
||||||
-- value, for example for tables or views, but also often for functions.
|
|
||||||
-- It can be simple integer values or text or bytea as well.
|
|
||||||
--
|
|
||||||
-- For this, we need handlers to transform the "non-mime-typed" values
|
|
||||||
-- into "mimetypes". We have a default builtin handler that does
|
|
||||||
-- "application/json". We can add more handlers via aggregates.
|
|
||||||
--
|
|
||||||
-- 2. "mime-typed" values, which specifically return a domain type that is
|
|
||||||
-- associated to a certain mimetype. e.g, a function returning only
|
|
||||||
-- "image/png".
|
|
||||||
--
|
|
||||||
-- FIXME:
|
|
||||||
-- If the function returns a domain type - let's say image/png, we should
|
|
||||||
-- accept */*, image/*, and image/png.
|
|
||||||
-- Related issue: https://github.com/PostgREST/postgrest/issues/3391
|
|
||||||
|
|
||||||
-- | Do content negotiation. i.e. choose a media type based on the
|
|
||||||
-- intersection of accepted/produced media types.
|
|
||||||
negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Bool -> Either ApiRequestError ResolvedHandler
|
|
||||||
negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces defaultSelect =
|
|
||||||
case (act, firstAcceptedPick) of
|
|
||||||
(_, Nothing) -> Left . MediaTypeError $ map MediaType.toMime accepts
|
|
||||||
(ActDb (ActRelationMut _ _), Just (x, mt)) -> Right (if rep == Just Full then x else NoAgg, mt)
|
|
||||||
-- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849
|
|
||||||
-- TODO: despite no aggregate, these are responding with a Content-Type, which is not correct.
|
|
||||||
(ActDb (ActRelationRead _ True), Just (_, mt)) -> Right (NoAgg, mt)
|
|
||||||
(ActDb (ActRoutine _ (InvRead True)), Just (_, mt)) -> Right (NoAgg, mt)
|
|
||||||
(_, Just (x, mt)) -> Right (x, mt)
|
|
||||||
where
|
|
||||||
firstAcceptedPick = listToMaybe $ mapMaybe matchMT accepts -- If there are multiple accepted media types, pick the first. This is usual in content negotiation.
|
|
||||||
matchMT mt = case mt of
|
|
||||||
-- all the vendored media types have special handling as they have media type parameters, they cannot be overridden
|
|
||||||
m@(MTVndSingularJSON strip) -> Just (BuiltinAggSingleJson strip, m)
|
|
||||||
m@MTVndArrayJSONStrip -> Just (BuiltinAggArrayJsonStrip, m)
|
|
||||||
m@(MTVndPlan (MTVndSingularJSON strip) _ _) -> mtPlanToNothing $ Just (BuiltinAggSingleJson strip, m)
|
|
||||||
m@(MTVndPlan MTVndArrayJSONStrip _ _) -> mtPlanToNothing $ Just (BuiltinAggArrayJsonStrip, m)
|
|
||||||
-- TODO the plan should have its own MediaHandler instead of relying on MediaType
|
|
||||||
m@(MTVndPlan mType _ _) -> mtPlanToNothing $ ((,) . fst <$> lookupHandler mType) <*> pure m
|
|
||||||
-- all the other media types can be overridden
|
|
||||||
x -> lookupHandler x
|
|
||||||
mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed
|
|
||||||
lookupHandler mt =
|
|
||||||
when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) <|> -- lookup for identifier and `*/*`
|
|
||||||
when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|> -- lookup for identifier and a particular media type
|
|
||||||
HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type
|
|
||||||
when' :: Bool -> Maybe a -> Maybe a
|
|
||||||
when' True (Just a) = Just a
|
|
||||||
when' _ _ = Nothing
|
|
||||||
@@ -47,7 +47,7 @@ data CoercibleField = CoercibleField
|
|||||||
, cfBaseType :: Text -- ^ The base type of the field in case of domains, or just the type otherwise (without modifiers in case of pg_catalog types)
|
, cfBaseType :: Text -- ^ The base type of the field in case of domains, or just the type otherwise (without modifiers in case of pg_catalog types)
|
||||||
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
||||||
, cfDefault :: Maybe Text
|
, cfDefault :: Maybe Text
|
||||||
, cfFullRow :: Bool -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spread resources.
|
, cfFullRow :: Bool -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spreaded resources.
|
||||||
} deriving (Eq, Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
unknownField :: FieldName -> JsonPath -> CoercibleField
|
unknownField :: FieldName -> JsonPath -> CoercibleField
|
||||||
|
|||||||
@@ -43,16 +43,16 @@ data MainQuery = MainQuery
|
|||||||
|
|
||||||
mainQuery :: ActionPlan -> AppConfig -> ApiRequest -> AuthResult -> Maybe QualifiedIdentifier -> MainQuery
|
mainQuery :: ActionPlan -> AppConfig -> ApiRequest -> AuthResult -> Maybe QualifiedIdentifier -> MainQuery
|
||||||
mainQuery (NoDb _) _ _ _ _ = MainQuery mempty Nothing mempty (mempty, mempty, mempty) mempty
|
mainQuery (NoDb _) _ _ _ _ = MainQuery mempty Nothing mempty (mempty, mempty, mempty) mempty
|
||||||
mainQuery (Db plan) conf@AppConfig{..} apiReq@ApiRequest{iTopLevelRange=range, iPreferences=Preferences{..}} authRes preReq =
|
mainQuery (Db plan) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} authRes preReq =
|
||||||
let genQ = MainQuery (PreQuery.txVarQuery plan conf authRes apiReq) (PreQuery.preReqQuery <$> preReq) in
|
let genQ = MainQuery (PreQuery.txVarQuery plan conf authRes apiReq) (PreQuery.preReqQuery <$> preReq) in
|
||||||
case plan of
|
case plan of
|
||||||
DbCrud _ WrappedReadPlan{..} ->
|
DbCrud _ WrappedReadPlan{..} ->
|
||||||
let countQuery = QueryBuilder.readPlanToCountQuery wrReadPlan in
|
let countQuery = QueryBuilder.readPlanToCountQuery wrReadPlan in
|
||||||
genQ (Statements.mainRead wrReadPlan countQuery preferCount configDbMaxRows range pMedia wrHandler) (mempty, mempty, mempty)
|
genQ (Statements.mainRead wrReadPlan countQuery preferCount configDbMaxRows pMedia wrHandler) (mempty, mempty, mempty)
|
||||||
(if shouldExplainCount preferCount then Just (Statements.postExplain countQuery) else Nothing)
|
(if shouldExplainCount preferCount then Just (Statements.postExplain countQuery) else Nothing)
|
||||||
DbCrud _ MutateReadPlan{..} ->
|
DbCrud _ MutateReadPlan{..} ->
|
||||||
genQ (Statements.mainWrite mrReadPlan mrMutatePlan pMedia mrHandler preferRepresentation preferResolution) (mempty, mempty, mempty) mempty
|
genQ (Statements.mainWrite mrReadPlan mrMutatePlan pMedia mrHandler preferRepresentation preferResolution) (mempty, mempty, mempty) mempty
|
||||||
DbCrud _ CallReadPlan{..} ->
|
DbCrud _ CallReadPlan{..} ->
|
||||||
genQ (Statements.mainCall crProc crCallPlan crReadPlan preferCount configDbMaxRows range pMedia crHandler) (mempty, mempty, mempty) mempty
|
genQ (Statements.mainCall crProc crCallPlan crReadPlan preferCount pMedia crHandler) (mempty, mempty, mempty) mempty
|
||||||
MayUseDb InspectPlan{ipSchema=tSchema} ->
|
MayUseDb InspectPlan{ipSchema=tSchema} ->
|
||||||
genQ mempty (SqlFragment.accessibleTables tSchema, SqlFragment.accessibleFuncs tSchema, SqlFragment.schemaDescription tSchema) mempty
|
genQ mempty (SqlFragment.accessibleTables tSchema, SqlFragment.accessibleFuncs tSchema, SqlFragment.schemaDescription tSchema) mempty
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ module PostgREST.Query.PreQuery
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.Aeson.KeyMap as KM
|
|
||||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Hasql.DynamicStatements.Snippet as SQL hiding (sql)
|
import qualified Hasql.DynamicStatements.Snippet as SQL hiding (sql)
|
||||||
@@ -47,10 +46,7 @@ txVarQuery dbActPlan AppConfig{..} AuthResult{..} ApiRequest{..} =
|
|||||||
pathSql = setConfigWithConstantName ("request.path", iPath)
|
pathSql = setConfigWithConstantName ("request.path", iPath)
|
||||||
headersSql = setConfigWithConstantNameJSON "request.headers" iHeaders
|
headersSql = setConfigWithConstantNameJSON "request.headers" iHeaders
|
||||||
cookiesSql = setConfigWithConstantNameJSON "request.cookies" iCookies
|
cookiesSql = setConfigWithConstantNameJSON "request.cookies" iCookies
|
||||||
claimsSql = [setConfigWithConstantName ("request.jwt.claims", LBS.toStrict $ JSON.encode claims)]
|
claimsSql = [setConfigWithConstantName ("request.jwt.claims", LBS.toStrict $ JSON.encode authClaims)]
|
||||||
where
|
|
||||||
claims = authClaims & KM.insert "role" (JSON.String $ decodeUtf8 authRole) -- insert "role" to claims as well
|
|
||||||
|
|
||||||
roleSql = [setConfigWithConstantName ("role", authRole)]
|
roleSql = [setConfigWithConstantName ("role", authRole)]
|
||||||
roleSettingsSql = setConfigWithDynamicName <$> HM.toList (fromMaybe mempty $ HM.lookup authRole configRoleSettings)
|
roleSettingsSql = setConfigWithDynamicName <$> HM.toList (fromMaybe mempty $ HM.lookup authRole configRoleSettings)
|
||||||
appSettingsSql = setConfigWithDynamicName . join bimap toUtf8 <$> configAppSettings
|
appSettingsSql = setConfigWithDynamicName . join bimap toUtf8 <$> configAppSettings
|
||||||
|
|||||||
@@ -264,7 +264,7 @@ readPlanToCountQuery (Node ReadPlan{from=mainQi, fromAlias=tblAlias, where_=logi
|
|||||||
limitedQuery :: SQL.Snippet -> Maybe Integer -> SQL.Snippet
|
limitedQuery :: SQL.Snippet -> Maybe Integer -> SQL.Snippet
|
||||||
limitedQuery query maxRows = query <> SQL.sql (maybe mempty (\x -> " LIMIT " <> BS.pack (show x)) maxRows)
|
limitedQuery query maxRows = query <> SQL.sql (maybe mempty (\x -> " LIMIT " <> BS.pack (show x)) maxRows)
|
||||||
|
|
||||||
-- TODO refactor so this function is unneeded and ComputedRelationship QualifiedIdentifier comes from the ReadPlan type
|
-- TODO refactor so this function is uneeded and ComputedRelationship QualifiedIdentifier comes from the ReadPlan type
|
||||||
getQualifiedIdentifier :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> QualifiedIdentifier
|
getQualifiedIdentifier :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> QualifiedIdentifier
|
||||||
getQualifiedIdentifier rel mainQi tblAlias = case rel of
|
getQualifiedIdentifier rel mainQi tblAlias = case rel of
|
||||||
Just ComputedRelationship{relFunction} -> QualifiedIdentifier mempty $ fromMaybe (qiName relFunction) tblAlias
|
Just ComputedRelationship{relFunction} -> QualifiedIdentifier mempty $ fromMaybe (qiName relFunction) tblAlias
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ module PostgREST.Query.SqlFragment
|
|||||||
, locationF
|
, locationF
|
||||||
, noLocationF
|
, noLocationF
|
||||||
, orderF
|
, orderF
|
||||||
, pageCountSelectF
|
|
||||||
, pgFmtColumn
|
, pgFmtColumn
|
||||||
, pgFmtFilter
|
, pgFmtFilter
|
||||||
, pgFmtIdent
|
, pgFmtIdent
|
||||||
@@ -97,7 +96,6 @@ import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
|||||||
Routine (..),
|
Routine (..),
|
||||||
funcReturnsScalar,
|
funcReturnsScalar,
|
||||||
funcReturnsSetOfScalar,
|
funcReturnsSetOfScalar,
|
||||||
funcReturnsSingle,
|
|
||||||
funcReturnsSingleComposite)
|
funcReturnsSingleComposite)
|
||||||
|
|
||||||
import Protolude hiding (Sum, cast)
|
import Protolude hiding (Sum, cast)
|
||||||
@@ -487,21 +485,15 @@ pgFmtGroup _ CoercibleSelectField{csAggFunction=Just _} = Nothing
|
|||||||
pgFmtGroup _ CoercibleSelectField{csAlias=Just alias, csAggFunction=Nothing} = Just $ pgFmtIdent alias
|
pgFmtGroup _ CoercibleSelectField{csAlias=Just alias, csAggFunction=Nothing} = Just $ pgFmtIdent alias
|
||||||
pgFmtGroup qi CoercibleSelectField{csField=fld, csAlias=Nothing, csAggFunction=Nothing} = Just $ pgFmtField qi fld
|
pgFmtGroup qi CoercibleSelectField{csField=fld, csAlias=Nothing, csAggFunction=Nothing} = Just $ pgFmtField qi fld
|
||||||
|
|
||||||
countF :: SQL.Snippet -> SQL.Snippet -> Bool -> Maybe Integer -> NonnegRange -> (SQL.Snippet, SQL.Snippet)
|
countF :: SQL.Snippet -> Bool -> (SQL.Snippet, SQL.Snippet)
|
||||||
countF countQuery pageCountSelect shouldCount maxRows range
|
countF countQuery shouldCount =
|
||||||
| shouldCount = if isJust maxRows || range /= allRange
|
if shouldCount
|
||||||
then ( ", pgrst_source_count AS (" <> countQuery <> ")"
|
then (
|
||||||
, "(SELECT pg_catalog.count(*) FROM pgrst_source_count)" )
|
", pgrst_source_count AS (" <> countQuery <> ")"
|
||||||
-- When there are no db-max-rows and limits/offsets, the total count will be the same as the page count,
|
, "(SELECT pg_catalog.count(*) FROM pgrst_source_count)" )
|
||||||
-- so we use the same page count here to avoid doing a separate aggregated count.
|
else (
|
||||||
else ( mempty, pageCountSelect )
|
mempty
|
||||||
| otherwise = ( mempty, "null::bigint" )
|
, "null::bigint")
|
||||||
|
|
||||||
pageCountSelectF :: Maybe Routine -> SQL.Snippet
|
|
||||||
pageCountSelectF rout =
|
|
||||||
if maybe False funcReturnsSingle rout
|
|
||||||
then "1"
|
|
||||||
else "pg_catalog.count(_postgrest_t)"
|
|
||||||
|
|
||||||
returningF :: QualifiedIdentifier -> [FieldName] -> SQL.Snippet
|
returningF :: QualifiedIdentifier -> [FieldName] -> SQL.Snippet
|
||||||
returningF qi returnings =
|
returningF qi returnings =
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ import PostgREST.Plan.MutatePlan as MTPlan
|
|||||||
import PostgREST.Plan.ReadPlan
|
import PostgREST.Plan.ReadPlan
|
||||||
import PostgREST.Query.QueryBuilder
|
import PostgREST.Query.QueryBuilder
|
||||||
import PostgREST.Query.SqlFragment
|
import PostgREST.Query.SqlFragment
|
||||||
import PostgREST.RangeQuery (NonnegRange)
|
import PostgREST.SchemaCache.Routine (MediaHandler (..), Routine,
|
||||||
import PostgREST.SchemaCache.Routine (MediaHandler (..), Routine)
|
funcReturnsSingle)
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -64,24 +64,23 @@ mainWrite rPlan mtplan mt handler rep resolution = mtSnippet mt snippet
|
|||||||
_ -> (False,False, mempty);
|
_ -> (False,False, mempty);
|
||||||
|
|
||||||
mainRead :: ReadPlanTree -> SQL.Snippet -> Maybe PreferCount -> Maybe Integer ->
|
mainRead :: ReadPlanTree -> SQL.Snippet -> Maybe PreferCount -> Maybe Integer ->
|
||||||
NonnegRange -> MediaType -> MediaHandler -> SQL.Snippet
|
MediaType -> MediaHandler -> SQL.Snippet
|
||||||
mainRead rPlan countQuery pCount maxRows range mt handler = mtSnippet mt snippet
|
mainRead rPlan countQuery pCount maxRows mt handler = mtSnippet mt snippet
|
||||||
where
|
where
|
||||||
snippet =
|
snippet =
|
||||||
"WITH " <> sourceCTE <> " AS ( " <> selectQuery <> " ) " <>
|
"WITH " <> sourceCTE <> " AS ( " <> selectQuery <> " ) " <>
|
||||||
countCTEF <> " " <>
|
countCTEF <> " " <>
|
||||||
"SELECT " <>
|
"SELECT " <>
|
||||||
countResultF <> " AS total_result_set, " <>
|
countResultF <> " AS total_result_set, " <>
|
||||||
pageCountSelect <> " AS page_total, " <>
|
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
||||||
handlerF Nothing handler <> " AS body, " <>
|
handlerF Nothing handler <> " AS body, " <>
|
||||||
responseHeadersF <> " AS response_headers, " <>
|
responseHeadersF <> " AS response_headers, " <>
|
||||||
responseStatusF <> " AS response_status, " <>
|
responseStatusF <> " AS response_status, " <>
|
||||||
"''" <> " AS response_inserted " <>
|
"''" <> " AS response_inserted " <>
|
||||||
"FROM ( SELECT * FROM " <> sourceCTE <> " ) _postgrest_t"
|
"FROM ( SELECT * FROM " <> sourceCTE <> " ) _postgrest_t"
|
||||||
|
|
||||||
(countCTEF, countResultF) = countF countQ pageCountSelect (shouldCount pCount) maxRows range
|
(countCTEF, countResultF) = countF countQ $ shouldCount pCount
|
||||||
selectQuery = readPlanToQuery rPlan
|
selectQuery = readPlanToQuery rPlan
|
||||||
pageCountSelect = pageCountSelectF Nothing
|
|
||||||
countQ =
|
countQ =
|
||||||
if pCount == Just EstimatedCount then
|
if pCount == Just EstimatedCount then
|
||||||
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
||||||
@@ -89,27 +88,28 @@ mainRead rPlan countQuery pCount maxRows range mt handler = mtSnippet mt snippet
|
|||||||
else
|
else
|
||||||
countQuery
|
countQuery
|
||||||
|
|
||||||
mainCall :: Routine -> CallPlan -> ReadPlanTree -> Maybe PreferCount -> Maybe Integer ->
|
mainCall :: Routine -> CallPlan -> ReadPlanTree -> Maybe PreferCount ->
|
||||||
NonnegRange-> MediaType -> MediaHandler -> SQL.Snippet
|
MediaType -> MediaHandler -> SQL.Snippet
|
||||||
mainCall rout cPlan rPlan pCount maxRows range mt handler = mtSnippet mt snippet
|
mainCall rout cPlan rPlan pCount mt handler = mtSnippet mt snippet
|
||||||
where
|
where
|
||||||
snippet =
|
snippet =
|
||||||
"WITH " <> sourceCTE <> " AS (" <> callProcQuery <> ") " <>
|
"WITH " <> sourceCTE <> " AS (" <> callProcQuery <> ") " <>
|
||||||
countCTEF <>
|
countCTEF <>
|
||||||
"SELECT " <>
|
"SELECT " <>
|
||||||
countResultF <> " AS total_result_set, " <>
|
countResultF <> " AS total_result_set, " <>
|
||||||
pageCountSelect <> " AS page_total, " <>
|
(if funcReturnsSingle rout
|
||||||
|
then "1"
|
||||||
|
else "pg_catalog.count(_postgrest_t)") <> " AS page_total, " <>
|
||||||
handlerF (Just rout) handler <> " AS body, " <>
|
handlerF (Just rout) handler <> " AS body, " <>
|
||||||
responseHeadersF <> " AS response_headers, " <>
|
responseHeadersF <> " AS response_headers, " <>
|
||||||
responseStatusF <> " AS response_status, " <>
|
responseStatusF <> " AS response_status, " <>
|
||||||
"''" <> " AS response_inserted " <>
|
"''" <> " AS response_inserted " <>
|
||||||
"FROM (" <> selectQuery <> ") _postgrest_t"
|
"FROM (" <> selectQuery <> ") _postgrest_t"
|
||||||
|
|
||||||
(countCTEF, countResultF) = countF countQuery pageCountSelect (shouldCount pCount) maxRows range
|
(countCTEF, countResultF) = countF countQuery $ shouldCount pCount
|
||||||
selectQuery = readPlanToQuery rPlan
|
selectQuery = readPlanToQuery rPlan
|
||||||
callProcQuery = callPlanToQuery cPlan
|
callProcQuery = callPlanToQuery cPlan
|
||||||
countQuery = readPlanToCountQuery rPlan
|
countQuery = readPlanToCountQuery rPlan
|
||||||
pageCountSelect = pageCountSelectF (Just rout)
|
|
||||||
|
|
||||||
-- This occurs after the main query runs, that's why it's prefixed with "post"
|
-- This occurs after the main query runs, that's why it's prefixed with "post"
|
||||||
postExplain :: SQL.Snippet -> SQL.Snippet
|
postExplain :: SQL.Snippet -> SQL.Snippet
|
||||||
|
|||||||
+17
-17
@@ -60,9 +60,9 @@ data PgrstResponse = PgrstResponse {
|
|||||||
, pgrstBody :: LBS.ByteString
|
, pgrstBody :: LBS.ByteString
|
||||||
}
|
}
|
||||||
|
|
||||||
actionResponse :: DbResult -> ApiRequest -> (Text, Text) -> AppConfig -> SchemaCache -> Either Error.Error PgrstResponse
|
actionResponse :: DbResult -> ApiRequest -> (Text, Text) -> AppConfig -> SchemaCache -> Schema -> Bool -> Either Error.Error PgrstResponse
|
||||||
|
|
||||||
actionResponse (DbCrudResult plan@WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly, crudQi=identifier} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ AppConfig{..} _ = do
|
actionResponse (DbCrudResult plan@WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly, crudQi=identifier} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ _ _ = do
|
||||||
let
|
let
|
||||||
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
||||||
cLHeader = if headersOnly then mempty else [ contentLengthHeader bod ]
|
cLHeader = if headersOnly then mempty else [ contentLengthHeader bod ]
|
||||||
@@ -79,7 +79,7 @@ actionResponse (DbCrudResult plan@WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly
|
|||||||
++ cLHeader
|
++ cLHeader
|
||||||
++ contentTypeHeaders pMedia ctxApiRequest
|
++ contentTypeHeaders pMedia ctxApiRequest
|
||||||
++ prefHeader
|
++ prefHeader
|
||||||
bod | status == HTTP.status416 = Error.errorPayload configClientErrorVerbosity $ Error.ApiRequestErr $ Error.InvalidRange $
|
bod | status == HTTP.status416 = Error.errorPayload $ Error.ApiRequestError $ Error.InvalidRange $
|
||||||
Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
||||||
| headersOnly = mempty
|
| headersOnly = mempty
|
||||||
| otherwise = LBS.fromStrict rsBody
|
| otherwise = LBS.fromStrict rsBody
|
||||||
@@ -88,7 +88,7 @@ actionResponse (DbCrudResult plan@WrappedReadPlan{pMedia, wrHdrsOnly=headersOnly
|
|||||||
|
|
||||||
Right $ PgrstResponse ovStatus ovHeaders bod
|
Right $ PgrstResponse ovStatus ovHeaders bod
|
||||||
|
|
||||||
actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationCreate, pMedia, crudQi=QualifiedIdentifier{..}} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ = do
|
actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationCreate, pMedia, crudQi=QualifiedIdentifier{..}} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ _ _ = do
|
||||||
let
|
let
|
||||||
prefHeader = prefAppliedHeader $ responsePreferences plan ctxApiRequest
|
prefHeader = prefAppliedHeader $ responsePreferences plan ctxApiRequest
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationCreate, pMed
|
|||||||
|
|
||||||
Right $ PgrstResponse ovStatus ovHeaders bod
|
Right $ PgrstResponse ovStatus ovHeaders bod
|
||||||
|
|
||||||
actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationUpdate, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ = do
|
actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationUpdate, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ _ _ = do
|
||||||
let
|
let
|
||||||
contentRangeHeader =
|
contentRangeHeader =
|
||||||
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
|
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
|
||||||
@@ -144,7 +144,7 @@ actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationUpdate, pMed
|
|||||||
|
|
||||||
Right $ PgrstResponse ovStatus ovHeaders body
|
Right $ PgrstResponse ovStatus ovHeaders body
|
||||||
|
|
||||||
actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationSingleUpsert, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ = do
|
actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationSingleUpsert, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ _ _ = do
|
||||||
let
|
let
|
||||||
prefHeader = maybeToList . prefAppliedHeader $ responsePreferences plan ctxApiRequest
|
prefHeader = maybeToList . prefAppliedHeader $ responsePreferences plan ctxApiRequest
|
||||||
lbsBody = LBS.fromStrict rsBody
|
lbsBody = LBS.fromStrict rsBody
|
||||||
@@ -162,7 +162,7 @@ actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationSingleUpsert
|
|||||||
|
|
||||||
Right $ PgrstResponse ovStatus ovHeaders body
|
Right $ PgrstResponse ovStatus ovHeaders body
|
||||||
|
|
||||||
actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationDelete, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ = do
|
actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationDelete, pMedia} RSStandard{..}) ctxApiRequest@ApiRequest{..} _ _ _ _ _ = do
|
||||||
let
|
let
|
||||||
contentRangeHeader = RangeQuery.contentRangeH 1 0 $ if shouldCount (preferCount iPreferences) then Just rsQueryTotal else Nothing
|
contentRangeHeader = RangeQuery.contentRangeH 1 0 $ if shouldCount (preferCount iPreferences) then Just rsQueryTotal else Nothing
|
||||||
prefHeader = maybeToList . prefAppliedHeader $ responsePreferences plan ctxApiRequest
|
prefHeader = maybeToList . prefAppliedHeader $ responsePreferences plan ctxApiRequest
|
||||||
@@ -178,12 +178,12 @@ actionResponse (DbCrudResult plan@MutateReadPlan{mrMutation=MutationDelete, pMed
|
|||||||
|
|
||||||
Right $ PgrstResponse ovStatus ovHeaders body
|
Right $ PgrstResponse ovStatus ovHeaders body
|
||||||
|
|
||||||
actionResponse (DbCrudResult plan@CallReadPlan{pMedia, crInvMthd=invMethod, crProc=proc} RSStandard {..}) ctxApiRequest@ApiRequest{..} _ AppConfig{..} _ = do
|
actionResponse (DbCrudResult plan@CallReadPlan{pMedia, crInvMthd=invMethod, crProc=proc} RSStandard {..}) ctxApiRequest@ApiRequest{..} _ _ _ _ _ = do
|
||||||
let
|
let
|
||||||
(status, contentRange) =
|
(status, contentRange) =
|
||||||
RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
||||||
rsOrErrBody = if status == HTTP.status416
|
rsOrErrBody = if status == HTTP.status416
|
||||||
then Error.errorPayload configClientErrorVerbosity $ Error.ApiRequestErr $ Error.InvalidRange
|
then Error.errorPayload $ Error.ApiRequestError $ Error.InvalidRange
|
||||||
$ Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
$ Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
||||||
else LBS.fromStrict rsBody
|
else LBS.fromStrict rsBody
|
||||||
isHeadMethod = invMethod == InvRead True
|
isHeadMethod = invMethod == InvRead True
|
||||||
@@ -202,18 +202,18 @@ actionResponse (DbCrudResult plan@CallReadPlan{pMedia, crInvMthd=invMethod, crPr
|
|||||||
|
|
||||||
Right $ PgrstResponse ovStatus ovHeaders body
|
Right $ PgrstResponse ovStatus ovHeaders body
|
||||||
|
|
||||||
actionResponse (DbPlanResult media plan) ctxApiRequest _ _ _ =
|
actionResponse (DbPlanResult media plan) ctxApiRequest _ _ _ _ _ =
|
||||||
let body = LBS.fromStrict plan in
|
let body = LBS.fromStrict plan in
|
||||||
Right $ PgrstResponse HTTP.status200 (contentLengthHeader body : contentTypeHeaders media ctxApiRequest) body
|
Right $ PgrstResponse HTTP.status200 (contentLengthHeader body : contentTypeHeaders media ctxApiRequest) body
|
||||||
|
|
||||||
actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) ApiRequest{..} versions conf sCache =
|
actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versions conf sCache schema negotiatedByProfile =
|
||||||
let
|
let
|
||||||
rsBody = maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body
|
rsBody = maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body
|
||||||
cLHeader = if headersOnly then mempty else [contentLengthHeader rsBody]
|
cLHeader = if headersOnly then mempty else [contentLengthHeader rsBody]
|
||||||
in
|
in
|
||||||
Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader iSchema iNegotiatedByProfile)) rsBody
|
Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader schema negotiatedByProfile)) rsBody
|
||||||
|
|
||||||
actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ sc@SchemaCache{dbTables} =
|
actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ sc@SchemaCache{dbTables} _ _ =
|
||||||
case HM.lookup qi dbTables of
|
case HM.lookup qi dbTables of
|
||||||
Just tbl -> respondInfo $ allowH tbl
|
Just tbl -> respondInfo $ allowH tbl
|
||||||
Nothing -> Left $ Error.SchemaCacheErr $ Error.TableNotFound qiSchema qiName sc
|
Nothing -> Left $ Error.SchemaCacheErr $ Error.TableNotFound qiSchema qiName sc
|
||||||
@@ -227,11 +227,11 @@ actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ sc@Sc
|
|||||||
["PATCH" | tableUpdatable table] ++
|
["PATCH" | tableUpdatable table] ++
|
||||||
["DELETE" | tableDeletable table]
|
["DELETE" | tableDeletable table]
|
||||||
|
|
||||||
actionResponse (NoDbResult (RoutineInfoPlan proc)) _ _ _ _
|
actionResponse (NoDbResult (RoutineInfoPlan proc)) _ _ _ _ _ _
|
||||||
| pdVolatility proc == Volatile = respondInfo "OPTIONS,POST"
|
| pdVolatility proc == Volatile = respondInfo "OPTIONS,POST"
|
||||||
| otherwise = respondInfo "OPTIONS,GET,HEAD,POST"
|
| otherwise = respondInfo "OPTIONS,GET,HEAD,POST"
|
||||||
|
|
||||||
actionResponse (NoDbResult SchemaInfoPlan) _ _ _ _ = respondInfo "OPTIONS,GET,HEAD"
|
actionResponse (NoDbResult SchemaInfoPlan) _ _ _ _ _ _ = respondInfo "OPTIONS,GET,HEAD"
|
||||||
|
|
||||||
respondInfo :: ByteString -> Either Error.Error PgrstResponse
|
respondInfo :: ByteString -> Either Error.Error PgrstResponse
|
||||||
respondInfo allowHeader =
|
respondInfo allowHeader =
|
||||||
@@ -247,11 +247,11 @@ overrideStatusHeaders rsGucStatus rsGucHeaders pgrstStatus pgrstHeaders = do
|
|||||||
|
|
||||||
decodeGucHeaders :: Maybe BS.ByteString -> Either Error.Error [GucHeader]
|
decodeGucHeaders :: Maybe BS.ByteString -> Either Error.Error [GucHeader]
|
||||||
decodeGucHeaders =
|
decodeGucHeaders =
|
||||||
maybe (Right []) $ first (const . Error.ApiRequestErr $ Error.GucHeadersError) . JSON.eitherDecode . LBS.fromStrict
|
maybe (Right []) $ first (const . Error.ApiRequestError $ Error.GucHeadersError) . JSON.eitherDecode . LBS.fromStrict
|
||||||
|
|
||||||
decodeGucStatus :: Maybe Text -> Either Error.Error (Maybe HTTP.Status)
|
decodeGucStatus :: Maybe Text -> Either Error.Error (Maybe HTTP.Status)
|
||||||
decodeGucStatus =
|
decodeGucStatus =
|
||||||
maybe (Right Nothing) $ first (const . Error.ApiRequestErr $ Error.GucStatusError) . fmap (Just . toEnum . fst) . decimal
|
maybe (Right Nothing) $ first (const . Error.ApiRequestError $ Error.GucStatusError) . fmap (Just . toEnum . fst) . decimal
|
||||||
|
|
||||||
contentLengthHeader :: LBS.ByteString -> HTTP.Header
|
contentLengthHeader :: LBS.ByteString -> HTTP.Header
|
||||||
contentLengthHeader body = ("Content-Length", show (LBS.length body))
|
contentLengthHeader body = ("Content-Length", show (LBS.length body))
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user