Compare commits
@@ -1,5 +1,5 @@
|
||||
freebsd_instance:
|
||||
image_family: freebsd-14-2
|
||||
image_family: freebsd-14-3
|
||||
|
||||
build_task:
|
||||
# Don't change this name without adjusting .github/workflows/build.yaml
|
||||
@@ -35,7 +35,7 @@ build_task:
|
||||
- find main src -type f -iname '*.hs' -exec md5sum "{}" +
|
||||
|
||||
build_script: |
|
||||
stack build -j 1 --local-bin-path . --copy-bins --stack-yaml stack-21.7.yaml
|
||||
stack build -j 1 --local-bin-path . --copy-bins
|
||||
strip postgrest
|
||||
|
||||
bin_artifacts:
|
||||
|
||||
@@ -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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ${{ inputs.upload }}
|
||||
path: ${{ steps.download.outputs.artifacts }}
|
||||
if-no-files-found: error
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Cache on main
|
||||
|
||||
description: Stores caches on main and release branches only, but restores them on all branches.
|
||||
|
||||
inputs:
|
||||
path:
|
||||
description: Path(s) to cache
|
||||
required: true
|
||||
save-prs:
|
||||
description: Whether to additionally store the cache in a pull request, too. Should only be used for very small caches.
|
||||
type: boolean
|
||||
prefix:
|
||||
description: Cache key prefix to be used in both primary key and restore-keys.
|
||||
required: true
|
||||
suffix:
|
||||
description: Cache key suffix to be used only in primary key.
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ inputs.prefix }}-
|
||||
- uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
||||
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
|
||||
with:
|
||||
path: ${{ inputs.path }}
|
||||
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ inputs.prefix }}-
|
||||
@@ -11,12 +11,12 @@ inputs:
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: nixbuild/nix-quick-install-action@5bb6a3b3abe66fd09bbf250dce8ada94f856a703 # v30
|
||||
- uses: nixbuild/nix-quick-install-action@9f63be77f412a248c9d9a65a4c82cf066cdf8f0c # v35
|
||||
with:
|
||||
nix_conf: |-
|
||||
always-allow-substitutes = true
|
||||
max-jobs = auto
|
||||
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16
|
||||
- uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
|
||||
with:
|
||||
name: postgrest
|
||||
authToken: ${{ inputs.authToken }}
|
||||
|
||||
@@ -1,210 +0,0 @@
|
||||
name: Build
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
CACHIX_AUTH_TOKEN:
|
||||
required: false
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- v[0-9]+
|
||||
paths:
|
||||
- .github/workflows/build.yaml
|
||||
- .github/actions/**
|
||||
- .github/scripts/**
|
||||
- .github/*
|
||||
- '*.nix'
|
||||
- nix/**
|
||||
- .cirrus.yml
|
||||
- cabal.project*
|
||||
- postgrest.cabal
|
||||
- stack.yaml*
|
||||
- '**.hs'
|
||||
- '!**.md'
|
||||
|
||||
concurrency:
|
||||
# Terminate all previous runs of the same workflow for pull requests
|
||||
group: build-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
static:
|
||||
name: Nix - Linux x86-64 static
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
|
||||
- name: Build static executable
|
||||
run: nix-build -A postgrestStatic
|
||||
- name: Save built executable as artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: postgrest-linux-static-x86-64
|
||||
path: result/bin/postgrest
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Build Docker image
|
||||
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
|
||||
- name: Save built Docker image as artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: postgrest-docker-x86-64
|
||||
path: postgrest-docker.tar.gz
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
macos:
|
||||
name: Nix - MacOS
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
- name: Install gnu sed
|
||||
run: brew install gnu-sed
|
||||
|
||||
- name: Build everything
|
||||
run: |
|
||||
# The --dry-run will give us a list of derivations to download from cachix and
|
||||
# derivations to build. We only take those that would have to be built and then build
|
||||
# those explicitly. This has the advantage that pure verification will not include
|
||||
# a download anymore, making it much faster. If something needs to be built, only
|
||||
# the dependencies required to do so will be downloaded, but not everything.
|
||||
nix-build --dry-run 2>&1 \
|
||||
| gsed -e '1,/derivations will be built:$/d' -e '/paths will be fetched/Q' \
|
||||
| xargs nix-build
|
||||
|
||||
|
||||
stack:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux aarch64
|
||||
runs-on: ubuntu-24.04-arm
|
||||
cache: |
|
||||
~/.stack/pantry
|
||||
~/.stack/snapshots
|
||||
~/.stack/stack.sqlite3
|
||||
artifact: postgrest-ubuntu-aarch64
|
||||
deps: sudo apt-get update && sudo apt-get install libpq-dev
|
||||
|
||||
- name: MacOS aarch64
|
||||
runs-on: macos-14
|
||||
cache: |
|
||||
~/.stack/pantry
|
||||
~/.stack/snapshots
|
||||
~/.stack/stack.sqlite3
|
||||
artifact: postgrest-macos-aarch64
|
||||
deps: brew link --force libpq
|
||||
|
||||
- name: MacOS x86-64
|
||||
runs-on: macos-13
|
||||
cache: |
|
||||
~/.stack/pantry
|
||||
~/.stack/snapshots
|
||||
~/.stack/stack.sqlite3
|
||||
artifact: postgrest-macos-x86-64
|
||||
deps: brew link --force libpq
|
||||
|
||||
- name: Windows
|
||||
runs-on: windows-2022
|
||||
cache: |
|
||||
C:\sr\pantry
|
||||
C:\sr\snapshots
|
||||
C:\sr\stack.sqlite3
|
||||
deps: Add-Content $env:GITHUB_PATH $env:PGBIN
|
||||
artifact: postgrest-windows-x86-64
|
||||
|
||||
name: Stack - ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: haskell-actions/setup@64445b6b5dd545faf5f8e2acee8253eb5c2b29aa # v2.7.11
|
||||
with:
|
||||
# This must match the version in stack.yaml's resolver
|
||||
ghc-version: 9.6.6
|
||||
enable-stack: true
|
||||
stack-no-global: true
|
||||
stack-setup-ghc: true
|
||||
- name: Cache ~/.stack
|
||||
uses: ./.github/actions/cache-on-main
|
||||
with:
|
||||
path: ${{ matrix.cache }}
|
||||
prefix: stack
|
||||
suffix: ${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
|
||||
- name: Cache .stack-work
|
||||
uses: ./.github/actions/cache-on-main
|
||||
with:
|
||||
path: .stack-work
|
||||
save-prs: true
|
||||
prefix: stack-work-${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
|
||||
suffix: ${{ hashFiles('main/**/*.hs', 'src/**/*.hs') }}
|
||||
- name: Install dependencies
|
||||
if: matrix.deps
|
||||
run: ${{ matrix.deps }}
|
||||
- name: Build with Stack
|
||||
run: stack build --lock-file error-on-write --local-bin-path result --copy-bins
|
||||
- name: Strip Executable
|
||||
run: strip result/postgrest*
|
||||
- name: Save built executable as artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: ${{ matrix.artifact }}
|
||||
path: |
|
||||
result/postgrest
|
||||
result/postgrest.exe
|
||||
if-no-files-found: error
|
||||
|
||||
|
||||
freebsd:
|
||||
name: Stack - FreeBSD from CirrusCI
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: ./.github/actions/artifact-from-cirrus
|
||||
with:
|
||||
token: ${{ github.token }}
|
||||
task: Build FreeBSD (Stack)
|
||||
download: bin
|
||||
upload: postgrest-freebsd-x86-64
|
||||
|
||||
|
||||
cabal:
|
||||
strategy:
|
||||
matrix:
|
||||
ghc: ['9.6.6', '9.8.2']
|
||||
fail-fast: false
|
||||
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: haskell-actions/setup@64445b6b5dd545faf5f8e2acee8253eb5c2b29aa # v2.7.11
|
||||
with:
|
||||
ghc-version: ${{ matrix.ghc }}
|
||||
- name: Cache .cabal
|
||||
uses: ./.github/actions/cache-on-main
|
||||
with:
|
||||
path: |
|
||||
~/.cabal/packages
|
||||
~/.cabal/store
|
||||
prefix: cabal-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}
|
||||
suffix: ${{ hashFiles('postgrest.cabal', 'cabal.project') }}
|
||||
- name: Cache dist-newstyle
|
||||
uses: ./.github/actions/cache-on-main
|
||||
with:
|
||||
path: dist-newstyle
|
||||
save-prs: true
|
||||
prefix: cabal-${{ matrix.ghc }}-dist-newstyle-${{ hashFiles('postgrest.cabal', 'cabal.project', 'cabal.project.freeze') }}
|
||||
suffix: ${{ hashFiles('**/*.hs') }}
|
||||
- name: Install dependencies
|
||||
run: cabal build --only-dependencies --enable-tests --enable-benchmarks
|
||||
- name: Build
|
||||
run: cabal build --enable-tests --enable-benchmarks all
|
||||
@@ -1,32 +0,0 @@
|
||||
name: Check
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
CACHIX_AUTH_TOKEN:
|
||||
required: false
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- v[0-9]+
|
||||
|
||||
concurrency:
|
||||
# Terminate all previous runs of the same workflow for pull requests
|
||||
group: style-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
lint-style:
|
||||
name: Lint & Style
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
tools: style.lint.bin style.styleCheck.bin
|
||||
- name: Run linter (check locally with `nix-shell --run postgrest-lint`)
|
||||
run: postgrest-lint
|
||||
- name: Run style check (auto-format with `nix-shell --run postgrest-style`)
|
||||
run: postgrest-style-check
|
||||
@@ -1,70 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v[0-9]+
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check
|
||||
uses: ./.github/workflows/check.yaml
|
||||
secrets:
|
||||
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
|
||||
|
||||
docs:
|
||||
name: Docs
|
||||
uses: ./.github/workflows/docs.yaml
|
||||
secrets:
|
||||
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
|
||||
|
||||
test:
|
||||
name: Test
|
||||
uses: ./.github/workflows/test.yaml
|
||||
secrets:
|
||||
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
|
||||
build:
|
||||
name: Build
|
||||
uses: ./.github/workflows/build.yaml
|
||||
secrets:
|
||||
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
|
||||
|
||||
tag:
|
||||
name: Tag
|
||||
concurrency:
|
||||
# Never tag outdated commits on the main branch by skipping superseded commits
|
||||
group: ci-tag-${{ (github.ref == 'refs/heads/main' && github.ref) || github.run_id }}
|
||||
# TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved
|
||||
cancel-in-progress: false
|
||||
if: vars.RELEASE_ENABLED
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- docs
|
||||
- test
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
|
||||
- name: Tag latest commit
|
||||
run: |
|
||||
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||
|
||||
if [[ "$cabal_version" == *.*.* ]]; then
|
||||
git fetch --tags
|
||||
|
||||
if [ -z "$(git tag --list "v$cabal_version")" ]; then
|
||||
git tag "v$cabal_version"
|
||||
git push origin "v$cabal_version"
|
||||
fi
|
||||
else
|
||||
git tag -f "devel"
|
||||
git push -f origin "devel"
|
||||
fi
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
name: Build
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
name: Spellcheck
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
name: Linkcheck
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '1 2 * * 3'
|
||||
|
||||
jobs:
|
||||
linkcheck:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
tools: docs.linkcheck.bin
|
||||
- run: postgrest-docs-linkcheck
|
||||
@@ -1,205 +0,0 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- devel
|
||||
- v*
|
||||
|
||||
concurrency:
|
||||
# Terminate all previous runs of the same workflow for the same tag.
|
||||
group: release-${{ github.ref }}
|
||||
# TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
uses: ./.github/workflows/build.yaml
|
||||
secrets:
|
||||
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
|
||||
|
||||
|
||||
prepare:
|
||||
name: Prepare
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- build
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Check the version to be released
|
||||
run: |
|
||||
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||
|
||||
if [ "${GITHUB_REF_NAME}" != "devel" ] && [ "${GITHUB_REF_NAME}" != "v$cabal_version" ]; then
|
||||
echo "Tagged version ($GITHUB_REF_NAME) does not match the one in postgrest.cabal (v$cabal_version). Aborting release..."
|
||||
exit 1
|
||||
fi
|
||||
- name: Identify changes from CHANGELOG.md
|
||||
run: |
|
||||
if [ "${GITHUB_REF_NAME}" == "devel" ]; then
|
||||
echo "Getting unreleased changes..."
|
||||
sed -n "1,/## Unreleased/d;/## \[/q;p" CHANGELOG.md > CHANGES.md
|
||||
else
|
||||
version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||
echo "Propper release, getting changes for version $version ..."
|
||||
sed -n "1,/## \[$version\]/d;/## \[/q;p" CHANGELOG.md > CHANGES.md
|
||||
fi
|
||||
|
||||
echo "Relevant extract from CHANGELOG.md:"
|
||||
cat CHANGES.md
|
||||
- name: Save CHANGES.md as artifact
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
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@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
path: artifacts
|
||||
- name: Create release bundle with archives for all builds
|
||||
run: |
|
||||
find artifacts -type f -iname postgrest -exec chmod +x {} \;
|
||||
|
||||
mkdir -p release-bundle
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-x86-64.tar.xz" \
|
||||
-C artifacts/postgrest-linux-static-x86-64 postgrest
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-aarch64.tar.xz" \
|
||||
-C artifacts/postgrest-macos-aarch64 postgrest
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-x86-64.tar.xz" \
|
||||
-C artifacts/postgrest-macos-x86-64 postgrest
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \
|
||||
-C artifacts/postgrest-freebsd-x86-64 postgrest
|
||||
|
||||
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-ubuntu-aarch64.tar.xz" \
|
||||
-C artifacts/postgrest-ubuntu-aarch64 postgrest
|
||||
|
||||
zip --junk-paths "release-bundle/postgrest-${GITHUB_REF_NAME}-windows-x86-64.zip" \
|
||||
artifacts/postgrest-windows-x86-64/postgrest.exe
|
||||
|
||||
- name: Save release bundle
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: release-bundle
|
||||
path: release-bundle
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Publish release on GitHub
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
echo "Releasing version ${GITHUB_REF_NAME} on GitHub..."
|
||||
|
||||
if [ "${GITHUB_REF_NAME}" == "devel" ]; then
|
||||
# To replace the existing release, we must first delete the old assets,
|
||||
# then modify the release, then add the new assets.
|
||||
gh release view devel --json assets \
|
||||
| jq -r '.assets[] | .name' \
|
||||
| xargs -rn1 \
|
||||
gh release delete-asset -y devel
|
||||
gh release edit devel \
|
||||
-t devel \
|
||||
--verify-tag \
|
||||
-F artifacts/release-changes/CHANGES.md \
|
||||
--prerelease
|
||||
gh release upload --clobber devel release-bundle/*
|
||||
else
|
||||
gh release create "${GITHUB_REF_NAME}" \
|
||||
-t "${GITHUB_REF_NAME}" \
|
||||
--verify-tag \
|
||||
-F artifacts/release-changes/CHANGES.md \
|
||||
release-bundle/*
|
||||
fi
|
||||
|
||||
|
||||
docker:
|
||||
name: Docker Hub
|
||||
runs-on: ubuntu-24.04-arm
|
||||
needs:
|
||||
- prepare
|
||||
if: |
|
||||
vars.DOCKER_REPO && vars.DOCKER_USER
|
||||
env:
|
||||
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Download x86-64 Docker image
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: postgrest-docker-x86-64
|
||||
- name: Download aarch64 binary
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: postgrest-ubuntu-aarch64
|
||||
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||
with:
|
||||
username: ${{ vars.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
- name: Build aarch64 Docker image
|
||||
run: |
|
||||
# This only pushes the image via digest, not a tag. This will not appear
|
||||
# in the image list on Docker Hub, yet. It will be later added to the main
|
||||
# tag's manifest.
|
||||
docker buildx build \
|
||||
-t "$DOCKER_REPO/postgrest" \
|
||||
--platform linux/arm64 \
|
||||
--output push-by-digest=true,type=image,push=true \
|
||||
--metadata-file metadata.json \
|
||||
.
|
||||
echo "SHA256_ARM=$(jq -r '."containerimage.digest"' metadata.json)" >> "$GITHUB_ENV"
|
||||
- name: Publish images on Docker Hub
|
||||
run: |
|
||||
docker load -i postgrest-docker.tar.gz
|
||||
|
||||
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
|
||||
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
|
||||
docker buildx imagetools create --append \
|
||||
-t "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}" \
|
||||
"$DOCKER_REPO/postgrest@$SHA256_ARM"
|
||||
|
||||
# Only tag 'latest' for full releases
|
||||
if [ "${GITHUB_REF_NAME}" != "devel" ]; then
|
||||
echo "Pushing to 'latest' tag for full release of ${GITHUB_REF_NAME} ..."
|
||||
docker tag postgrest:latest "$DOCKER_REPO"/postgrest:latest
|
||||
docker push "$DOCKER_REPO"/postgrest:latest
|
||||
docker buildx imagetools create --append \
|
||||
-t "$DOCKER_REPO/postgrest:latest" \
|
||||
"$DOCKER_REPO/postgrest@$SHA256_ARM"
|
||||
else
|
||||
echo "Skipping push to 'latest' tag for pre-release..."
|
||||
fi
|
||||
|
||||
|
||||
docker-description:
|
||||
name: Docker Hub Description
|
||||
runs-on: ubuntu-24.04
|
||||
if: |
|
||||
vars.DOCKER_REPO && vars.DOCKER_USER &&
|
||||
github.ref == 'refs/tags/devel'
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
|
||||
with:
|
||||
username: ${{ vars.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASS }}
|
||||
repository: ${{ vars.DOCKER_REPO }}/postgrest
|
||||
short-description: ${{ github.event.repository.description }}
|
||||
readme-filepath: ./docker-hub-readme.md
|
||||
|
||||
@@ -1,158 +0,0 @@
|
||||
name: Test
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
CACHIX_AUTH_TOKEN:
|
||||
required: false
|
||||
CODECOV_TOKEN:
|
||||
required: false
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- v[0-9]+
|
||||
paths:
|
||||
- .github/workflows/test.yaml
|
||||
- .github/workflows/report.yaml
|
||||
- .github/actions/setup-nix/**
|
||||
- default.nix
|
||||
- nix/**
|
||||
- .stylish-haskell.yaml
|
||||
- cabal.project
|
||||
- postgrest.cabal
|
||||
- '**.hs'
|
||||
- test/**
|
||||
- '!**.md'
|
||||
|
||||
concurrency:
|
||||
# Terminate all previous runs of the same workflow for pull requests
|
||||
group: test-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
# Hack for enabling color output, see:
|
||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
tools: tests.coverage.bin tests.testDoctests.bin tests.testSpecIdempotence.bin
|
||||
|
||||
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
||||
run: postgrest-coverage
|
||||
- name: Upload coverage to codecov
|
||||
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2
|
||||
with:
|
||||
files: ./coverage/codecov.json
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: Run doctests
|
||||
if: always()
|
||||
run: postgrest-test-doctests
|
||||
|
||||
- name: Check the spec tests for idempotence
|
||||
if: always()
|
||||
run: postgrest-test-spec-idempotence
|
||||
|
||||
|
||||
postgres:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
pgVersion: [12, 13, 14, 15, 16, 17]
|
||||
name: PG ${{ matrix.pgVersion }}
|
||||
runs-on: ubuntu-24.04
|
||||
defaults:
|
||||
run:
|
||||
# Hack for enabling color output, see:
|
||||
# https://github.com/actions/runner/issues/241#issuecomment-842566950
|
||||
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
tools: tests.testSpec.bin tests.testIO.bin tests.testBigSchema.bin withTools.postgresql-${{ matrix.pgVersion }}.bin
|
||||
|
||||
- name: Run spec tests
|
||||
if: always()
|
||||
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-spec
|
||||
|
||||
- name: Run IO tests
|
||||
if: always()
|
||||
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-io -vv
|
||||
|
||||
- name: Run IO tests on a big schema
|
||||
if: always()
|
||||
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-big-schema -vv
|
||||
|
||||
|
||||
memory:
|
||||
name: Memory
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
tools: tests.testMemory.bin
|
||||
- name: Run memory tests
|
||||
run: postgrest-test-memory
|
||||
|
||||
|
||||
loadtest:
|
||||
strategy:
|
||||
matrix:
|
||||
kind: ['mixed', 'jwt']
|
||||
name: Loadtest
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
tools: loadtest.loadtestAgainst.bin loadtest.report.bin
|
||||
- uses: WyriHaximus/github-action-get-previous-tag@04e8485ecb6487243907e330d522ff60f02283ce # v1.4.0
|
||||
id: get-latest-tag
|
||||
with:
|
||||
prefix: v
|
||||
- name: Run loadtest
|
||||
run: |
|
||||
postgrest-loadtest-against -k ${{ matrix.kind }} main ${{ steps.get-latest-tag.outputs.tag }}
|
||||
postgrest-loadtest-report >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
flake:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
runs-on:
|
||||
- macos-13 # x86_64-darwin
|
||||
- macos-14 # aarch64-darwin
|
||||
- ubuntu-24.04 # x86_64-linux
|
||||
- ubuntu-24.04-arm # aarch64-linux
|
||||
name: Flake Check
|
||||
runs-on: ${{ matrix.runs-on }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup Nix Environment
|
||||
uses: ./.github/actions/setup-nix
|
||||
with:
|
||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||
- name: Run flake check
|
||||
run: |
|
||||
nix flake check
|
||||
@@ -18,21 +18,21 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/code-build.png">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/tembo.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/tembo.png">
|
||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||
<img width="296px" src="static/euronodes.svg">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
|
||||
<img width="296px" src="static/supabase.png">
|
||||
<img width="296px" src="static/supabase.svg">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -89,8 +89,8 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="222px" src="static/gnuhost.png">
|
||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="222px" src="static/code-build.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -5,6 +5,67 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## Unreleased
|
||||
|
||||
## [13.0.8] - 2025-10-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix loading utf-8 config files with `ASCII` locale set by @taimoorzaeem in #4386
|
||||
|
||||
## [13.0.7] - 2025-09-14
|
||||
|
||||
### Added
|
||||
|
||||
- Improve the `PGRST106` error when the requested schema is invalid by @laurenceisla in #4089
|
||||
+ It now shows the invalid schema in the `message` field.
|
||||
+ The exposed schemas are now listed in the `hint` instead of the `message` field.
|
||||
- Improve error details of `PGRST301` error by @taimoorzaeem in #4051
|
||||
|
||||
## [13.0.6] - 2025-08-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix logging the Haskell type instead of the listener error message directly by @laurenceisla in #3588
|
||||
- Fix format of `IPv6` address logged at PostgREST startup by @taimoorzaeem in #4291
|
||||
- Fix empty enum in `preferParams` OpenAPI parameter by @laurenceisla in #4292
|
||||
|
||||
## [13.0.5] - 2025-08-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix OpenAPI broken docs link by @taimoorzaeem in #4048
|
||||
- Fix OpenAPI specification incorrectly exposing GET methods for volatile functions by @joelonsql in #4174
|
||||
- Fix empty spread embeddings return unexpected SQL error by @taimoorzaeem in #3887
|
||||
- Fix `/metrics` endpoint not responding with `Content-Type` header by @taimoorzaeem in #4271
|
||||
|
||||
## [13.0.4] - 2025-06-17
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix regression that makes full-text search not work on domain types based on `tsvector` by @laurenceisla in #4135
|
||||
- Fix `jwt-aud` config not failing when set to an invalid URI by @taimoorzaeem in #4132
|
||||
|
||||
## [13.0.3] - 2025-06-16
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix `max-affected` preference not failing with RPC when `handling=strict` by @taimoorzaeem in #4100
|
||||
- Fix a property definition's type in OpenAPI not showing the correct base type of a recursive domain by @laurenceisla in #4136
|
||||
|
||||
## [13.0.2] - 2025-06-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix regression that makes `ORDER BY` with nulls-order not work alongside limits by @laurenceisla in #4109
|
||||
|
||||
## [13.0.1] - 2025-06-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix jwt error returning HTTP status `400` for invalid role by @taimoorzaeem in #3601
|
||||
- Fix `db-extra-search-path` cannot be set to nothing by @taimoorzaeem in #4074
|
||||
+ It can now be disabled by setting it to empty string.
|
||||
+ Schema Cache load error is now logged including `db-schemas` and `db-extra-search-path` config values.
|
||||
|
||||
## [13.0.0] - 2025-05-08
|
||||
|
||||
### Added
|
||||
@@ -47,8 +108,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- #2052, Dropped support for PostgreSQL 11 - @wolfgangwalther
|
||||
- #3508, PostgREST now fails to start when `server-port` and `admin-server-port` config options are the same - @develop7
|
||||
- #3607, PostgREST now fails to start when the JWT secret is less than 32 characters long - @laurenceisla
|
||||
- #3644, Fail schema cache lookup with invalid db-schemas config - @wolfgangwalther
|
||||
- #3644, Fail schema cache lookup with invalid `db-schemas` or `db-extra-search-path` config - @wolfgangwalther
|
||||
- Previously, this would silently return 200 - OK on the root endpoint, but don't provide any usable endpoints.
|
||||
- Note: This also applies when deleting the `public` schema - both config options default to that.
|
||||
- #3757, Remove support for `Prefer: params=single-object` - @joelonsql
|
||||
+ This preference was deprecated in favor of Functions with an array of JSON objects
|
||||
- #3013, Drop support for Limited updates/deletes
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
# PostgREST Docker Hub image for aarch64.
|
||||
# The x86-64 is a single-static-binary image built via Nix, see:
|
||||
# nix/tools/docker/README.md
|
||||
|
||||
FROM ubuntu:noble@sha256:6015f66923d7afbc53558d7ccffd325d43b4e249f41a6e93eef074c9505d2233 AS postgrest
|
||||
|
||||
RUN apt-get update -y \
|
||||
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY postgrest /usr/bin/postgrest
|
||||
RUN chmod +x /usr/bin/postgrest
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
USER 1000
|
||||
|
||||
# Use the array form to avoid running the command using bash, which does not handle `SIGTERM` properly.
|
||||
# See https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop
|
||||
CMD ["postgrest"]
|
||||
@@ -27,21 +27,21 @@ API than you are likely to write from scratch.
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/code-build.png">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/tembo.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="static/tembo.png">
|
||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||
<img width="296px" src="static/euronodes.svg">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
|
||||
<img width="296px" src="static/supabase.png">
|
||||
<img width="296px" src="static/supabase.svg">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1 +1 @@
|
||||
index-state: hackage.haskell.org 2025-02-01T14:59:33Z
|
||||
index-state: hackage.haskell.org 2025-10-13T04:53:27Z
|
||||
|
||||
@@ -24,21 +24,21 @@ write from scratch.
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/code-build.png">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/tembo.png">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr></tr>
|
||||
<tr>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/tembo.png">
|
||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||
<img width="296px" src="static/euronodes.svg">
|
||||
</a>
|
||||
</td>
|
||||
<td align="center" valign="middle">
|
||||
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
|
||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.svg">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -82,8 +82,8 @@ url of Authorization is [[../explanations/db_authz.html]]
|
||||
url of CLI is [[../references/cli.html#cli]]
|
||||
url of "Connection Pool" is [[../references/connection_pool.html]]
|
||||
url of Config is [[../references/configuration.html#configuration]]
|
||||
url of HTTPADMIN is [[https://aosabook.org/en/posa/warp.html]]
|
||||
url of HTTPAPI is [[https://aosabook.org/en/posa/warp.html]]
|
||||
url of HTTPADMIN is [[../explanations/architecture.html#http]]
|
||||
url of HTTPAPI is [[../explanations/architecture.html#http]]
|
||||
url of Listener is [[../references/listener.html#listener]]
|
||||
url of Proxy is [[../explanations/nginx.html]]
|
||||
url of "Schema Cache" is [[../references/schema_cache.html#schema-cache]]
|
||||
|
||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
@@ -114,7 +114,7 @@ html_theme = "sphinx_rtd_theme"
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
html_theme_options = {"display_version": False}
|
||||
html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
@@ -302,6 +302,7 @@ linkcheck_ignore = [
|
||||
r"https://blog.frankel.ch/poor-man-api",
|
||||
# Odd SSL error
|
||||
r"https://www.dripdepot.com",
|
||||
r"https://www.euronodes.com",
|
||||
# New GitHub UI delays comment load, so anchor fails
|
||||
r"https://github.com/.*#issuecomment",
|
||||
# Random 500 Internal Server Error
|
||||
|
||||
@@ -6,7 +6,7 @@ Community Tutorials
|
||||
* `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ -
|
||||
In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.
|
||||
|
||||
* `PostgREST + Auth0: Create REST API in mintutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
|
||||
* `PostgREST + Auth0: Create REST API in minutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest-1-introduction/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
|
||||
|
||||
* `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ -
|
||||
A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service).
|
||||
@@ -34,7 +34,7 @@ Templates
|
||||
Example Apps
|
||||
------------
|
||||
|
||||
* `archtika <https://github.com/archtika/archtika>`_ - self‑hosted CMS
|
||||
* `archtika <https://github.com/thiloho/archtika>`_ - self‑hosted CMS
|
||||
* `delibrium-postgrest <https://gitlab.com/delibrium/delibrium-postgrest/>`_ - example school API and front-end in Vue.js
|
||||
* `ETH-transactions-storage <https://github.com/Adamant-im/ETH-transactions-storage>`_ - indexer for Ethereum to get transaction list by ETH address
|
||||
* `general <https://github.com/PierreRochard/general>`_ - example auth back-end
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.. _external_jwt:
|
||||
.. _external_auth:
|
||||
|
||||
External JWT Generation
|
||||
External Authentication
|
||||
-----------------------
|
||||
|
||||
JWT from Auth0
|
||||
@@ -9,21 +9,3 @@ JWT from Auth0
|
||||
An external service like `Auth0 <https://auth0.com/>`_ can do the hard work transforming OAuth from Github, Twitter, Google etc into a JWT suitable for PostgREST. Auth0 can also handle email signup and password reset flows.
|
||||
|
||||
To use Auth0, create `an application <https://auth0.com/docs/get-started/applications>`_ for your app and `an API <https://auth0.com/docs/get-started/apis>`_ for your PostgREST server. Auth0 supports both HS256 and RS256 scheme for the issued tokens for APIs. For simplicity, you may first try HS256 scheme while creating your API on Auth0. Your application should use your PostgREST API's `API identifier <https://auth0.com/docs/get-started/apis/api-settings>`_ by setting it with the `audience parameter <https://auth0.com/docs/secure/tokens/access-tokens/get-access-tokens#control-access-token-audience>`_ during the authorization request. This will ensure that Auth0 will issue an access token for your PostgREST API. For PostgREST to verify the access token, you will need to set ``jwt-secret`` on PostgREST config file with your API's signing secret.
|
||||
|
||||
JWT using OpenSSL
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
To manually generate a JWT using ``openssl`` commands, you can use the following script. This may be useful for testing JWT related features of PostgREST.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
TEST_JWT_SECRET='test_secret_that_is_at_least_32_characters_long'
|
||||
_base64 () { openssl base64 -e -A | tr '+/' '-_' | tr -d '='; }
|
||||
header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | _base64)
|
||||
exp=$(( EPOCHSECONDS + 60*60 )) # 1 hour
|
||||
payload=$(echo -n "{\"role\":\"test_role\",\"exp\":$exp}" | _base64)
|
||||
signature=$(echo -n "$header.$payload" | openssl dgst -sha256 -hmac "$TEST_JWT_SECRET" -binary | _base64)
|
||||
echo -n "$header.$payload.$signature"
|
||||
@@ -41,46 +41,43 @@ Sponsors
|
||||
.. container:: img-dark
|
||||
|
||||
.. image:: ../static/neon-dark.jpg
|
||||
:target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
|
||||
.. container:: img-light
|
||||
|
||||
.. image:: ../static/neon.jpg
|
||||
:target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||
|
||||
.. container:: img-dark
|
||||
|
||||
.. image:: ../static/code-build-dark.png
|
||||
:target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
|
||||
|
||||
.. container:: img-light
|
||||
|
||||
.. image:: ../static/code-build.png
|
||||
:target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
|
||||
.. image:: ../static/tembo.png
|
||||
:target: https://www.tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
||||
|
||||
|
|
||||
|
||||
.. image:: ../static/tembo.png
|
||||
:target: https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
||||
.. container:: img-dark
|
||||
|
||||
.. image:: ../static/euronodes.svg
|
||||
:target: https://www.euronodes.com/postgrest
|
||||
|
||||
.. container:: img-light
|
||||
|
||||
.. image:: ../static/euronodes.svg
|
||||
:target: https://www.euronodes.com/postgrest
|
||||
|
||||
.. container:: img-dark
|
||||
|
||||
.. image:: ../static/supabase-dark.png
|
||||
.. image:: ../static/supabase-dark.svg
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
|
||||
.. container:: img-light
|
||||
|
||||
.. image:: ../static/supabase.png
|
||||
.. image:: ../static/supabase.svg
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
|
||||
.. image:: _static/empty.png
|
||||
:target: #sponsors
|
||||
|
||||
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
|
||||
to create space and center the logos. It's not easy to layout with restructuredText.
|
||||
|
||||
.. .. image:: _static/empty.png
|
||||
:target: #sponsors
|
||||
.. image:: _static/empty.png
|
||||
:target: #sponsors
|
||||
|
||||
|
|
||||
|
||||
@@ -209,20 +206,14 @@ In Production
|
||||
Here are some companies that use PostgREST in production.
|
||||
|
||||
* `Catarse <https://www.catarse.me>`_
|
||||
* `Datrium <https://www.datrium.com>`_
|
||||
* `Drip Depot <https://www.dripdepot.com>`_
|
||||
* `Image-charts <https://www.image-charts.com>`_
|
||||
* `Moat <https://www.oracle.com/advertising/>`_
|
||||
* `Netwo <https://www.netwo.io>`_
|
||||
* `Nimbus <https://www.nimbusfacility.com/sg/home>`_
|
||||
- See how Nimbus uses PostgREST in `Paul Copplestone's blog post <https://paul.copplest.one/blog/nimbus-tech-2019-04.html>`_.
|
||||
* `OpenBooking <https://openbooking.ch>`_
|
||||
* `Supabase <https://supabase.com>`_
|
||||
|
||||
.. Failing links
|
||||
* `eGull <http://www.egull.co>`_
|
||||
* `MotionDynamic - Fast highly dynamic video generation at scale <https://motiondynamic.tech>`_
|
||||
|
||||
Testimonials
|
||||
------------
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
Greenplum
|
||||
#########
|
||||
|
||||
`Greenplum <https://blogs.vmware.com/tanzu/tanzu-greenplum/>`_ has been reported to work by adding ``LOGIN`` to the :ref:`anonymous and user roles <roles>`.
|
||||
|
||||
For more details, see https://github.com/PostgREST/postgrest/issues/2021.
|
||||
@@ -46,7 +46,6 @@ Github
|
||||
Google
|
||||
grantor
|
||||
GraphQL
|
||||
Greenplum
|
||||
gte
|
||||
GUC
|
||||
Haskell
|
||||
@@ -104,7 +103,6 @@ Observability
|
||||
Okta
|
||||
OpenAPI
|
||||
openapi
|
||||
OpenSSL
|
||||
ov
|
||||
parametrized
|
||||
passphrase
|
||||
@@ -176,6 +174,7 @@ unikernel
|
||||
unix
|
||||
updatable
|
||||
unfulfillable
|
||||
unselected
|
||||
Untyped
|
||||
UPSERT
|
||||
Upsert
|
||||
|
||||
@@ -12,7 +12,7 @@ Health Check
|
||||
|
||||
You can enable a health check to verify if PostgREST is available for client requests. Also to check the status of its internal state.
|
||||
|
||||
Two endpoints ``live`` and ``ready`` will then be available.
|
||||
Two endpoints ``live`` and ``ready`` will then be available. Both these endpoints reply with a status code and empty response body.
|
||||
|
||||
.. important::
|
||||
|
||||
|
||||
@@ -294,6 +294,23 @@ Let's get its :ref:`explain_plan` when calling it with filters applied:
|
||||
|
||||
Notice there's no "Function Scan" node in the plan, which tells us it has been inlined.
|
||||
|
||||
Horizontal Filtering
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Table-valued functions support horizontal filtering on selected and unselected columns.
|
||||
|
||||
For example, the following RPC with filter on unselected column returns:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/getallprojects?select=id,client_id&name=like.OSX"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{ "id": 4, "client_id": 2 }
|
||||
]
|
||||
|
||||
.. _scalar_functions:
|
||||
|
||||
Scalar functions
|
||||
|
||||
@@ -242,7 +242,7 @@ Will result in:
|
||||
Max Affected
|
||||
============
|
||||
|
||||
You can set a limit to the amount of resources affected in a request by sending ``max-affected`` preference. This feature works in combination with ``handling=strict`` preference. ``max-affected`` would be ignored with lenient handling. The "affected resources" are the number of rows returned by ``DELETE`` and ``PATCH`` requests. This is also supported through ``RPC`` calls.
|
||||
You can set a limit to the amount of resources affected in a request by sending ``max-affected`` preference. This feature works in combination with ``handling=strict`` preference. ``max-affected`` would be ignored with lenient handling. The "affected resources" are the number of rows returned by ``DELETE`` and ``PATCH`` requests.
|
||||
|
||||
To illustrate the use of this preference, consider the following scenario where the ``items`` table contains 14 rows.
|
||||
|
||||
@@ -264,3 +264,35 @@ To illustrate the use of this preference, consider the following scenario where
|
||||
"details": "The query affects 14 rows",
|
||||
"hint": null
|
||||
}
|
||||
|
||||
With :ref:`RPC <functions>`, the preference is honored completely on the basis of the number of rows returned in the result set of the function. This can be useful for complex mutation queries using `data-modifying statements <https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-MODIFYING>`_. A simple example:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE FUNCTION test.delete_items()
|
||||
RETURNS SETOF items AS $$
|
||||
DELETE FROM items WHERE id < 15 RETURNING *;
|
||||
$$ LANGUAGE SQL;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/rpc/delete_items" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: handling=strict, max-affected=10"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"code": "PGRST124",
|
||||
"message": "Query result exceeds max-affected preference constraint",
|
||||
"details": "The query affects 14 rows",
|
||||
"hint": null
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
It is important for functions to return ``SETOF`` or ``TABLE`` when called with ``max-affected`` preference. A violation of this would cause a :ref:`PGRST128 <pgrst128>` error.
|
||||
|
||||
@@ -1150,27 +1150,19 @@ For example, to arrange the films in descending order using the director's last
|
||||
Spread embedded resource
|
||||
========================
|
||||
|
||||
The ``...`` operator lets you "spread" an embedded resource.
|
||||
That is, it removes the surrounding JSON object for the embedded resource columns.
|
||||
|
||||
.. note::
|
||||
|
||||
The spread operator ``...`` is borrowed from the Javascript `spread syntax <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax>`_.
|
||||
You can modify the shape of the embedded resources by using the spread syntax (``...``).
|
||||
|
||||
.. _spread_to_one_embed:
|
||||
|
||||
Spread To-One relationships
|
||||
---------------------------
|
||||
|
||||
This applies to :ref:`one-to-one <one-to-one>` and :ref:`many-to-one <many-to-one>` relationships.
|
||||
Take the following example:
|
||||
Spread on resources forming :ref:`one-to-one <one-to-one>` and :ref:`many-to-one <many-to-one>` relationships, will lift the embedded columns to the top object.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# curl "http://localhost:3000/films?select=title,...directors(director_last_name:last_name)&title=like.*Workers*"
|
||||
|
||||
curl --get "http://localhost:3000/films" \
|
||||
-d "select=title,...directors(director_last_name:last_name)" \
|
||||
-d "select=title,...directors(director_first_name:first_name, director_last_name:last_name)" \
|
||||
-d "title=like.*Workers*"
|
||||
|
||||
.. code-block:: json
|
||||
@@ -1178,48 +1170,22 @@ Take the following example:
|
||||
[
|
||||
{
|
||||
"title": "Workers Leaving The Lumière Factory In Lyon",
|
||||
"director_first_name": "Louis",
|
||||
"director_last_name": "Lumière"
|
||||
}
|
||||
]
|
||||
|
||||
Note that there is no ``"directors"`` object. Also the embed columns can be aliased normally.
|
||||
|
||||
You can use this to get the columns of a join table in a many-to-many relationship. For instance, to get films and its actors, but including the ``character`` column from the roles table:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# curl "http://localhost:3000/films?select=title,actors:roles(character,...actors(first_name,last_name))&title=like.*Lighthouse*"
|
||||
|
||||
curl --get "http://localhost:3000/films" \
|
||||
-d "select=title,actors:roles(character,...actors(first_name,last_name))" \
|
||||
-d "title=like.*Lighthouse*"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"title": "The Lighthouse",
|
||||
"actors": [
|
||||
{
|
||||
"character": "Thomas Wake",
|
||||
"first_name": "Willem",
|
||||
"last_name": "Dafoe"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
Note that there is no wrapping ``"directors"`` object, unlike regularly embedding :ref:`many-to-one <many-to-one>` relationships. Also note that embedded columns can be aliased normally.
|
||||
|
||||
.. _spread_to_many_embed:
|
||||
|
||||
Spread To-Many relationships
|
||||
----------------------------
|
||||
|
||||
The spread columns in :ref:`one-to-many <one-to-many>` or :ref:`many-to-many <many-to-many>` relationships will show the data in arrays.
|
||||
Spread on resources forming :ref:`one-to-many <one-to-many>` and :ref:`many-to-many <many-to-many>` relationships, will convert the embedded columns into correlated arrays.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# curl -g "http://localhost:3000/directors?select=first_name,...films(film_titles:title,film_years:year)&first_name=like.Quentin*"
|
||||
|
||||
curl --get "http://localhost:3000/directors" \
|
||||
-d "select=first_name,...films(film_titles:title,film_years:year)" \
|
||||
-d "first_name=like.Quentin*"
|
||||
@@ -1240,16 +1206,17 @@ The spread columns in :ref:`one-to-many <one-to-many>` or :ref:`many-to-many <ma
|
||||
}
|
||||
]
|
||||
|
||||
Note that there is no ``films`` array of objects.
|
||||
Note that ``films`` is no longer an array of objects, unlike regularly embedding :ref:`one-to-many`. The embedded columns become arrays and they're correlated—in the above result, we can say that "Pulp Fiction" premiered in 1994 and "Reservoir Dogs" in 1992.
|
||||
|
||||
By default, the order of the values inside the resulting array is unspecified but `it is safe to assume <https://www.postgresql.org/message-id/15950.1491843689%40sss.pgh.pa.us>`_ that all the columns return the values in the same unspecified order.
|
||||
From the previous result, we can say that "Pulp Fiction" premiered in 1994 and "Reservoir Dogs" in 1992.
|
||||
You can still order all the resulting arrays explicitly. For example, to order by the release year:
|
||||
Order in spread to-many
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
In the above example, the order of the values inside the correlated arrays is unspecified, but all the values are guaranteed to be in the same unspecified order.
|
||||
|
||||
You can order the correlated arrays explicitly. For example, to order by the film year:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# curl -g "http://localhost:3000/directors?select=first_name,...films(film_titles:title,film_years:year)&first_name=like.Quentin*&films.order=year"
|
||||
|
||||
curl --get "http://localhost:3000/directors" \
|
||||
-d "select=first_name,...films(film_titles:title,film_years:year)" \
|
||||
-d "first_name=like.Quentin*" \
|
||||
@@ -1271,15 +1238,38 @@ You can still order all the resulting arrays explicitly. For example, to order b
|
||||
}
|
||||
]
|
||||
|
||||
Nesting Spreads
|
||||
~~~~~~~~~~~~~~~
|
||||
.. warning::
|
||||
|
||||
For example, let's nest ``...technical_specs`` (one-to-one) and ``...roles`` (one-to-many) inside ``...films``:
|
||||
Aliasing spreaded columns is recommended since JSON allows duplicate keys. Example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl --get "localhost:3000/projects" \
|
||||
-d "select=id,name,...clients(id,name)"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[{"id":1,"name":"Windows 7","id":1,"name":"Microsoft"},
|
||||
{"id":2,"name":"Windows 10","id":1,"name":"Microsoft"},
|
||||
{"id":3,"name":"IOS","id":2,"name":"Apple"},
|
||||
{"id":4,"name":"OSX","id":2,"name":"Apple"},
|
||||
{"id":5,"name":"Orphan","id":null,"name":null}]
|
||||
|
||||
This can be a problem in Javascript objects, since only the last duplicated key will be considered. To solve it do:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl --get "localhost:3000/projects" \
|
||||
-d "select=id,name,...clients(client_id:id,client_name:name)"
|
||||
|
||||
|
||||
Multiple Spreads
|
||||
----------------
|
||||
|
||||
You can use multiple spreads at any level. For example, let's spread ``technical_specs`` and ``roles`` into ``films`` and then spread ``films`` into ``directors``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# curl -g "http://localhost:3000/directors?select=first_name,...films(film_titles:title,film_years:year,...technical_specs(film_runtimes:runtime),...roles(film_characters:character))&first_name=like.Quentin*&films.order=year&films.roles.order=character"
|
||||
|
||||
curl --get "http://localhost:3000/directors" \
|
||||
-d "select=first_name,...films(film_titles:title,film_years:year,...technical_specs(film_runtimes:runtime),...roles(film_characters:character))" \
|
||||
-d "first_name=like.Quentin*" \
|
||||
@@ -1310,6 +1300,36 @@ For example, let's nest ``...technical_specs`` (one-to-one) and ``...roles`` (on
|
||||
}
|
||||
]
|
||||
|
||||
All the elements inside ``films`` are selected in the same order, including both nested resources.
|
||||
For example, we can say that "Reservoir Dogs" premiered in 1992, its runtime is 1:39:00 and it has the following characters: ``[ "Mr. Pink", "Mr. White" ]``.
|
||||
Note that the data inside to-many nested resources can also be ordered (``roles`` by the ``character`` name in our example).
|
||||
Note that:
|
||||
|
||||
- All the ``film_*`` arrays are correlated—"Reservoir Dogs" premiered in 1992, its runtime is 1:39:00 and it has the following characters: ``[ "Mr. Pink", "Mr. White" ]``.
|
||||
- The ``film_*`` arrays are ordered by ``year`` (due to ``films.order=year``).
|
||||
- The bottom level array ``film_characters`` is ordered (due to ``films.roles.order=character``).
|
||||
|
||||
Spread a join table
|
||||
-------------------
|
||||
|
||||
Spread can be used to move the columns of a join table in a :ref:`many-to-many <many-to-many>` to the top object. For instance, to get the ``character`` column of the ``roles`` join table into ``actors``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl --get "http://localhost:3000/films" \
|
||||
-d "select=title,actors:roles(character,...actors(first_name,last_name))" \
|
||||
-d "title=like.*Lighthouse*"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"title": "The Lighthouse",
|
||||
"actors": [
|
||||
{
|
||||
"character": "Thomas Wake",
|
||||
"first_name": "Willem",
|
||||
"last_name": "Dafoe"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -175,23 +175,29 @@ To ensure best performance on larger data sets, an `appropriate index <https://w
|
||||
Full-Text Search
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The :code:`fts` filter mentioned above has a number of options to support flexible textual queries, namely the choice of plain vs phrase search and the language used for stemming. Suppose that :code:`tsearch` is a table with column :code:`my_tsv`, of type `tsvector <https://www.postgresql.org/docs/current/datatype-textsearch.html>`_. The following examples illustrate the possibilities.
|
||||
The :code:`fts` operator has a number of options to support flexible textual queries, namely the choice of plain vs phrase search and the language used for stemming.
|
||||
|
||||
The following examples illustrate the possibilities, assuming column :code:`my_tsv` is of type `tsvector <https://www.postgresql.org/docs/current/datatype-textsearch.html>`_.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_tsv=fts(french).amusant"
|
||||
curl --get "http://localhost:3000/people" \
|
||||
-d "my_tsv=fts(french).amusant"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_tsv=plfts.The%20Fat%20Cats"
|
||||
curl --get "http://localhost:3000/people" \
|
||||
-d "my_tsv=plfts.The%20Fat%20Cats"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats"
|
||||
curl --get "http://localhost:3000/people" \
|
||||
-d "my_tsv=not.phfts(english).The%20Fat%20Cats"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_tsv=not.wfts(french).amusant"
|
||||
curl --get "http://localhost:3000/people" \
|
||||
-d "my_tsv=not.wfts(french).amusant"
|
||||
|
||||
.. _fts_to_tsvector:
|
||||
|
||||
@@ -199,15 +205,17 @@ Automatic ``tsvector`` conversion
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If the filtered column is not of type ``tsvector``, then it will be automatically converted using `to_tsvector() <https://www.postgresql.org/docs/current/functions-textsearch.html#TEXTSEARCH-FUNCTIONS-TABLE>`_.
|
||||
This allows using ``fts`` on ``text`` and ``json`` types out of the box, for example.
|
||||
This allows using the ``fts`` operator on ``text`` and ``json`` types out of the box.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_text_column=fts(french).amusant"
|
||||
curl --get "http://localhost:3000/people" \
|
||||
-d "my_text_column=fts(french).amusant"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_json_column=not.phfts(english).The%20Fat%20Cats"
|
||||
curl --get "http://localhost:3000/people" \
|
||||
-d "my_json_column=not.phfts(english).The%20Fat%20Cats"
|
||||
|
||||
.. _v_filter:
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ If the client included no JWT (or one without a role claim) then PostgREST switc
|
||||
JWT Generation
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
You can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_jwt`).
|
||||
You can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_auth`).
|
||||
|
||||
.. _client_auth:
|
||||
|
||||
@@ -156,6 +156,19 @@ You can specify the literal value as we saw earlier, or reference a filename to
|
||||
|
||||
jwt-secret = "@rsa.jwk.pub"
|
||||
|
||||
JWK ``kid`` validation
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
PostgREST has built-in validation of the `key ID parameter <https://www.rfc-editor.org/rfc/rfc7517#section-4.5>`_, useful when working with a JWK Set.
|
||||
It goes as follows:
|
||||
|
||||
- If the JWT contains a ``kid`` parameter, then PostgREST will look for the JWK in the :ref:`jwt-secret`.
|
||||
|
||||
+ If no JWK matches the same ``kid`` value (or if they do not have a ``kid``), then the token will be rejected with a :ref:`401 Unauthorized <pgrst301>` error.
|
||||
+ If a JWK matches the ``kid`` value then it will validate the token against that JWK accordingly.
|
||||
|
||||
- If the JWT does not have a ``kid`` parameter, then PostgREST will validate the token against each JWK in the :ref:`jwt-secret`.
|
||||
|
||||
.. _jwt_claims_validation:
|
||||
|
||||
JWT Claims Validation
|
||||
@@ -188,12 +201,12 @@ It works this way:
|
||||
+ If the match fails or if the ``aud`` value is not a string or array of strings, then the token will be rejected with a :ref:`401 Unauthorized <pgrst303>` error.
|
||||
+ If the ``aud`` key **is not present** or if its value is ``null`` or ``[]``, PostgREST will interpret this token as allowed for all audiences and will complete the request.
|
||||
|
||||
.. _jwt_role_claim_key_extract:
|
||||
.. _jwt_role_extract:
|
||||
|
||||
JWT Role Claim Key Extraction
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
JWT Role Extraction
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. This can be used to consume a JWT provided by a third party service like Auth0, Okta, Microsoft Entra or Keycloak.
|
||||
A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. It's configured by :ref:`jwt-role-claim-key`. This can be used to consume a JWT provided by a third party service like Auth0, Okta, Microsoft Entra or Keycloak.
|
||||
|
||||
The DSL follows the `JSONPath <https://goessner.net/articles/JsonPath/>`_ expression grammar with extended string comparison operators. Supported operators are:
|
||||
|
||||
@@ -224,6 +237,9 @@ Usage examples:
|
||||
jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]"
|
||||
jwt-role-claim-key = ".postgrest.roles[?(@ *== \"utho\")]"
|
||||
|
||||
.. note::
|
||||
|
||||
The string comparison operators are implemented as a custom extension to the JSPath and does not strictly follow the `RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_.
|
||||
|
||||
JWT Security
|
||||
~~~~~~~~~~~~
|
||||
|
||||
@@ -315,6 +315,10 @@ db-extra-search-path
|
||||
|
||||
Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``.
|
||||
|
||||
.. important::
|
||||
|
||||
We default this config to ``public`` because it is the most common schema used to install PostgreSQL extensions such as :ref:`PostGIS <ww_postgis>`. You can disable this by setting this config to ``""``.
|
||||
|
||||
.. _db-hoisted-tx-settings:
|
||||
|
||||
db-hoisted-tx-settings
|
||||
@@ -616,7 +620,7 @@ jwt-role-claim-key
|
||||
|
||||
*For backwards compatibility, this config parameter is also available without prefix as "role-claim-key".*
|
||||
|
||||
See :ref:`jwt_role_claim_key_extract` on how to specify key paths and usage examples.
|
||||
See :ref:`jwt_role_extract` on how to specify key paths and usage examples.
|
||||
|
||||
.. _jwt-secret:
|
||||
|
||||
@@ -718,7 +722,7 @@ log-query
|
||||
=============== =================================
|
||||
|
||||
Logs the SQL query for the corresponding request at the current :ref:`log-level`.
|
||||
See :ref:``sql_query_logs``.
|
||||
See :ref:`sql_query_logs`.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
@@ -890,7 +894,7 @@ server-timing-enabled
|
||||
**In-Database** pgrst.server_timing_enabled
|
||||
=============== =================================
|
||||
|
||||
Enables the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>`_ header.
|
||||
Enables the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing>`_ header.
|
||||
See :ref:`server-timing_header`.
|
||||
|
||||
.. _server-unix-socket:
|
||||
|
||||
@@ -267,6 +267,10 @@ Related to the HTTP request elements.
|
||||
| | | implemented. |
|
||||
| PGRST127 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst128: | 400 | ``max-affected`` preference is violated with ``RPC`` call. |
|
||||
| | | See :ref:`prefer_max_affected`. |
|
||||
| PGRST128 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
|
||||
|
||||
.. _pgrst2**:
|
||||
|
||||
@@ -122,12 +122,17 @@ Restart the database and watch the log file in real-time to understand how HTTP
|
||||
Metrics
|
||||
=======
|
||||
|
||||
The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in `Prometheus text format <https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format>`_.
|
||||
The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in `Prometheus text format <https://prometheus.io/docs/instrumenting/exposition_formats/#prometheus-text-format>`_.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3001/metrics"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
# HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load
|
||||
# TYPE pgrst_schema_cache_query_time_seconds gauge
|
||||
pgrst_schema_cache_query_time_seconds 1.5937927e-2
|
||||
@@ -246,7 +251,7 @@ See :ref:`proxy-status_header`.
|
||||
Server-Timing Header
|
||||
--------------------
|
||||
|
||||
You can enable the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>`_ header by setting :ref:`server-timing-enabled` on.
|
||||
You can enable the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing>`_ header by setting :ref:`server-timing-enabled` on.
|
||||
This header communicates metrics of the different phases in the request-response cycle.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
sphinx==7.4.7
|
||||
sphinx-copybutton==0.5.2
|
||||
sphinx-rtd-dark-mode==1.3.0
|
||||
sphinx-rtd-theme==2.0.0
|
||||
sphinx-rtd-theme==3.0.2
|
||||
sphinx-tabs==3.4.7
|
||||
sphinxext-opengraph==0.9.1
|
||||
@@ -52,17 +52,31 @@ Check that the :code:`tutorial.conf` (created in the previous tutorial) has the
|
||||
|
||||
If the PostgREST server is still running from the previous tutorial, restart it to load the updated configuration file.
|
||||
|
||||
.. _tut1_step3:
|
||||
|
||||
Step 3. Sign a Token
|
||||
--------------------
|
||||
|
||||
Ordinarily your own code in the database or in another server will create and sign authentication tokens, but for this tutorial we will make one "by hand." Go to `jwt.io <https://jwt.io/#debugger-io>`_ and fill in the fields like this:
|
||||
Ordinarily your own code in the database or in another server will create and sign authentication tokens, but for this tutorial we will make one "by hand" using ``bash`` and ``openssl``.
|
||||
|
||||
.. figure:: ../_static/tuts/tut1-jwt-io.png
|
||||
:alt: jwt.io interface
|
||||
.. code:: bash
|
||||
|
||||
How to create a token at https://jwt.io
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
**Remember to fill in the secret you generated rather than the word "secret".** After you have filled in the secret and payload, the encoded data on the left will update. Copy the encoded token.
|
||||
JWT_SECRET='test_secret_that_is_at_least_32_characters_long'
|
||||
|
||||
_base64 () { openssl base64 -e -A | tr '+/' '-_' | tr -d '='; }
|
||||
|
||||
header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | _base64)
|
||||
|
||||
payload=$(echo -n "{\"role\":\"todo_user\"}" | _base64)
|
||||
|
||||
signature=$(echo -n "$header.$payload" | openssl dgst -sha256 -hmac "$JWT_SECRET" -binary | _base64)
|
||||
|
||||
echo -n "$header.$payload.$signature"
|
||||
|
||||
**Remember to fill in the secret you generated rather than keeping the "test_secret_that_is_at_least_32_characters_long".** After you have filled in the secret and payload, the encoded data on the left will update. Copy the encoded token.
|
||||
|
||||
.. note::
|
||||
|
||||
@@ -145,14 +159,22 @@ To observe expiration in action, we'll add an :code:`exp` claim of five minutes
|
||||
|
||||
select extract(epoch from now() + '5 minutes'::interval) :: integer;
|
||||
|
||||
Go back to jwt.io and change the payload to
|
||||
Or in ``bash``:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"role": "todo_user",
|
||||
"exp": 123456789
|
||||
}
|
||||
.. code-block:: bash
|
||||
|
||||
exp=$(( EPOCHSECONDS + 5*60 )) # five minutes
|
||||
|
||||
echo $exp
|
||||
|
||||
Go back to :ref:`tut1_step3` and change the payload to
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
payload=$(echo -n "{\"role\":\"todo_user\",\"exp\":123456789}" | _base64)
|
||||
|
||||
echo -n "$header.$payload.$signature"
|
||||
|
||||
**NOTE**: Don't forget to change the dummy epoch value :code:`123456789` in the snippet above to the epoch value returned by the :code:`psql` command.
|
||||
|
||||
|
||||
@@ -50,6 +50,16 @@ let
|
||||
# jailbreak, because hspec limit for tests
|
||||
fuzzyset = prev.fuzzyset_0_2_4;
|
||||
|
||||
# TODO: Remove once available in nixpkgs haskellPackages
|
||||
configurator-pg =
|
||||
prev.callHackageDirect
|
||||
{
|
||||
pkg = "configurator-pg";
|
||||
ver = "0.2.11";
|
||||
sha256 = "sha256-mtGtNawDJgz2ZIEVca+IYXVu4oNw9xsfJiYWAqAbbgc=";
|
||||
}
|
||||
{ };
|
||||
|
||||
hasql-pool = lib.dontCheck (prev.callHackageDirect
|
||||
{
|
||||
pkg = "hasql-pool";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
, aspellDicts
|
||||
, buildToolbox
|
||||
, checkedShellScript
|
||||
, fetchPypi
|
||||
, lib
|
||||
, plantuml
|
||||
, python3
|
||||
@@ -10,14 +11,26 @@
|
||||
, writers
|
||||
}:
|
||||
let
|
||||
selectPythonPackages = ps: [
|
||||
ps.sphinx
|
||||
ps.sphinx-copybutton
|
||||
ps.sphinx-rtd-dark-mode
|
||||
ps.sphinx-rtd-theme
|
||||
ps.sphinx-tabs
|
||||
ps.sphinxext-opengraph
|
||||
];
|
||||
selectPythonPackages = ps:
|
||||
let
|
||||
# TODO: Remove with next nixpkgs update
|
||||
sphinx-rtd-theme = assert ps.sphinx-rtd-theme.version == "2.0.0"; ps.sphinx-rtd-theme.overrideAttrs rec {
|
||||
version = "3.0.2";
|
||||
src = fetchPypi {
|
||||
pname = "sphinx_rtd_theme";
|
||||
inherit version;
|
||||
hash = "sha256-t0V7wl3acjsgsIamcLmVPIWeq2CioD7o6yuyPhduX4U=";
|
||||
};
|
||||
};
|
||||
in
|
||||
[
|
||||
ps.sphinx
|
||||
ps.sphinx-copybutton
|
||||
(ps.sphinx-rtd-dark-mode.override { inherit sphinx-rtd-theme; })
|
||||
sphinx-rtd-theme
|
||||
ps.sphinx-tabs
|
||||
ps.sphinxext-opengraph
|
||||
];
|
||||
|
||||
requirements = writeTextFile {
|
||||
name = "requirements.txt";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: postgrest
|
||||
version: 13.0.0
|
||||
version: 13.0.8
|
||||
synopsis: REST API for any Postgres database
|
||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||
for tables, views, and functions, supporting all HTTP methods that security
|
||||
@@ -16,15 +16,13 @@ extra-source-files: CHANGELOG.md
|
||||
cabal-version: >= 1.10
|
||||
|
||||
tested-with:
|
||||
-- stack on FreeBSD
|
||||
GHC == 9.4.5
|
||||
-- nix, cabal on Ubuntu (arm)
|
||||
, GHC == 9.4.8
|
||||
-- nix
|
||||
GHC == 9.4.8
|
||||
-- cabal on Ubuntu
|
||||
-- stack on MacOS, Ubuntu, Windows
|
||||
, GHC == 9.6.6
|
||||
-- stack on FreeBSD, MacOS, Ubuntu, Windows
|
||||
, GHC == 9.6.7
|
||||
-- cabal on Ubuntu
|
||||
, GHC == 9.8.2
|
||||
, GHC == 9.8.4
|
||||
|
||||
source-repository head
|
||||
type: git
|
||||
@@ -91,21 +89,20 @@ library
|
||||
PostgREST.Response.GucHeader
|
||||
PostgREST.Response.Performance
|
||||
PostgREST.Version
|
||||
other-modules: Paths_postgrest
|
||||
build-depends: base >= 4.9 && < 4.20
|
||||
, HTTP >= 4000.3.7 && < 4000.5
|
||||
, Ranged-sets >= 0.3 && < 0.5
|
||||
, aeson >= 2.0.3 && < 2.3
|
||||
, auto-update >= 0.1.4 && < 0.2
|
||||
, auto-update >= 0.1.4 && < 0.3
|
||||
, base64-bytestring >= 1 && < 1.3
|
||||
, bytestring >= 0.10.8 && < 0.13
|
||||
, cache >= 0.1.3 && < 0.2.0
|
||||
, case-insensitive >= 1.2 && < 1.3
|
||||
, cassava >= 0.4.5 && < 0.6
|
||||
, clock >= 0.8.3 && < 0.9.0
|
||||
, configurator-pg >= 0.2 && < 0.3
|
||||
, configurator-pg >= 0.2.11 && < 0.3
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
, cookie >= 0.4.2 && < 0.5
|
||||
, cookie >= 0.4.2 && < 0.6
|
||||
, directory >= 1.2.6 && < 1.4
|
||||
, either >= 4.4.1 && < 5.1
|
||||
, extra >= 1.7.0 && < 2.0
|
||||
@@ -114,17 +111,17 @@ library
|
||||
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||
, hasql-notifications >= 0.2.2.2 && < 0.2.3
|
||||
, hasql-pool >= 1.0.1 && < 1.1
|
||||
, hasql-transaction >= 1.0.1 && < 1.1
|
||||
, hasql-transaction >= 1.0.1 && < 1.2
|
||||
, heredoc >= 0.2 && < 0.3
|
||||
, http-types >= 0.12.2 && < 0.13
|
||||
, insert-ordered-containers >= 0.2.2 && < 0.3
|
||||
, iproute >= 1.7.0 && < 1.8
|
||||
, jose-jwt >= 0.9.6 && < 0.11
|
||||
, lens >= 4.14 && < 5.3
|
||||
, lens >= 4.14 && < 5.4
|
||||
, lens-aeson >= 1.0.1 && < 1.3
|
||||
, mtl >= 2.2.2 && < 2.4
|
||||
, neat-interpolation >= 0.5 && < 0.6
|
||||
, network >= 2.6 && < 3.2
|
||||
, network >= 2.6 && < 3.3
|
||||
, network-uri >= 2.6.1 && < 2.8
|
||||
, optparse-applicative >= 0.13 && < 0.19
|
||||
, parsec >= 3.1.11 && < 3.2
|
||||
@@ -152,7 +149,7 @@ library
|
||||
-- for unix sockets; this is tested in test/io/test_io.py. See
|
||||
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
|
||||
, wai-logger >= 2.4.0
|
||||
, warp >= 3.3.19 && < 3.4
|
||||
, warp >= 3.3.19 && < 3.5
|
||||
-- -fno-spec-constr may help keep compile time memory use in check,
|
||||
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
||||
-- -optP-Wno-nonportable-include-path
|
||||
@@ -261,14 +258,14 @@ test-suite spec
|
||||
, case-insensitive >= 1.2 && < 1.3
|
||||
, containers >= 0.5.7 && < 0.7
|
||||
, hasql-pool >= 1.0.1 && < 1.1
|
||||
, hasql-transaction >= 1.0.1 && < 1.1
|
||||
, hasql-transaction >= 1.0.1 && < 1.2
|
||||
, heredoc >= 0.2 && < 0.3
|
||||
, hspec >= 2.3 && < 2.12
|
||||
, hspec-wai >= 0.10 && < 0.12
|
||||
, hspec-wai-json >= 0.10 && < 0.12
|
||||
, http-types >= 0.12.3 && < 0.13
|
||||
, jose-jwt >= 0.9.6 && < 0.11
|
||||
, lens >= 4.14 && < 5.3
|
||||
, lens >= 4.14 && < 5.4
|
||||
, lens-aeson >= 1.0.1 && < 1.3
|
||||
, monad-control >= 1.0.1 && < 1.1
|
||||
, postgrest
|
||||
|
||||
@@ -16,6 +16,7 @@ import Network.Socket.ByteString
|
||||
|
||||
import PostgREST.AppState (AppState)
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.MediaType (MediaType (..), toContentType)
|
||||
import PostgREST.Metrics (metricsToText)
|
||||
import PostgREST.Network (resolveHost)
|
||||
import PostgREST.Observation (Observation (..))
|
||||
@@ -58,7 +59,7 @@ admin appState req respond = do
|
||||
respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache)
|
||||
["metrics"] -> do
|
||||
mets <- metricsToText
|
||||
respond $ Wai.responseLBS HTTP.status200 [] mets
|
||||
respond $ Wai.responseLBS HTTP.status200 [toContentType MTTextPlain] mets -- Content-Type is required for prometheus compliance
|
||||
_ ->
|
||||
respond $ Wai.responseLBS HTTP.status404 [] mempty
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ getAction resource schema method =
|
||||
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
|
||||
getSchema AppConfig{configDbSchemas} hdrs method = do
|
||||
case profile of
|
||||
Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema $ toList configDbSchemas
|
||||
Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema p $ toList configDbSchemas
|
||||
| otherwise -> Right (p, True)
|
||||
Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated
|
||||
where
|
||||
|
||||
@@ -407,7 +407,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
||||
Left e -> do
|
||||
putSCacheStatus appState SCPending
|
||||
putSchemaCache appState Nothing
|
||||
observer $ SchemaCacheErrorObs e
|
||||
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
|
||||
return Nothing
|
||||
|
||||
Right sCache -> do
|
||||
|
||||
@@ -50,7 +50,8 @@ import PostgREST.Auth.JwtCache (lookupJwtCache)
|
||||
import PostgREST.Auth.Types (AuthResult (..))
|
||||
import PostgREST.Config (AppConfig (..), FilterExp (..),
|
||||
JSPath, JSPathExp (..))
|
||||
import PostgREST.Error (Error (..), JwtError (..))
|
||||
import PostgREST.Error (Error (..), JwtClaimsError (..),
|
||||
JwtDecodeError (..), JwtError (..))
|
||||
|
||||
import Protolude
|
||||
|
||||
@@ -58,7 +59,7 @@ import Protolude
|
||||
-- JSON object of JWT claims.
|
||||
parseToken :: AppConfig -> Maybe ByteString -> UTCTime -> ExceptT Error IO JSON.Value
|
||||
parseToken _ Nothing _ = return JSON.emptyObject
|
||||
parseToken _ (Just "") _ = throwE . JwtErr $ JwtDecodeError "Empty JWT is sent in Authorization header"
|
||||
parseToken _ (Just "") _ = throwE . JwtErr $ JwtDecodeErr EmptyAuthHeader
|
||||
parseToken AppConfig{..} (Just tkn) time = do
|
||||
secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
|
||||
tknWith3Parts <- liftEither $ hasThreeParts tkn
|
||||
@@ -69,33 +70,33 @@ parseToken AppConfig{..} (Just tkn) time = do
|
||||
hasThreeParts :: ByteString -> Either Error ByteString
|
||||
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
|
||||
3 -> Right token
|
||||
n -> Left $ JwtErr $ JwtDecodeError ("Expected 3 parts in JWT; got " <> show n)
|
||||
n -> Left $ JwtErr $ JwtDecodeErr $ UnexpectedParts n
|
||||
jwtDecodeError :: JWT.JwtError -> JwtError
|
||||
-- The only errors we can get from JWT.decode function are:
|
||||
-- BadAlgorithm
|
||||
-- KeyError
|
||||
-- BadCrypto
|
||||
jwtDecodeError (JWT.KeyError _) = JwtDecodeError "No suitable key or wrong key type"
|
||||
jwtDecodeError (JWT.BadAlgorithm _) = JwtDecodeError "Wrong or unsupported encoding algorithm"
|
||||
jwtDecodeError JWT.BadCrypto = JwtDecodeError "JWT cryptographic operation failed"
|
||||
jwtDecodeError (JWT.KeyError m) = JwtDecodeErr $ KeyError m
|
||||
jwtDecodeError (JWT.BadAlgorithm m) = JwtDecodeErr $ BadAlgorithm m
|
||||
jwtDecodeError JWT.BadCrypto = JwtDecodeErr BadCrypto
|
||||
-- Control never reaches here, the decode function only returns the above three
|
||||
jwtDecodeError _ = JwtDecodeError "JWT couldn't be decoded"
|
||||
jwtDecodeError _ = JwtDecodeErr UnreachableDecodeError
|
||||
|
||||
verifyClaims :: JWT.JwtContent -> Either JwtError JSON.Value
|
||||
verifyClaims (JWT.Jws (_, claims)) = case JSON.decodeStrict claims of
|
||||
Just jclaims@(JSON.Object mclaims) ->
|
||||
verifyClaim mclaims "exp" isValidExpClaim "JWT expired" >>
|
||||
verifyClaim mclaims "nbf" isValidNbfClaim "JWT not yet valid" >>
|
||||
verifyClaim mclaims "iat" isValidIatClaim "JWT issued at future" >>
|
||||
verifyClaim mclaims "aud" isValidAudClaim "JWT not in audience" >>
|
||||
verifyClaim mclaims "exp" isValidExpClaim JWTExpired >>
|
||||
verifyClaim mclaims "nbf" isValidNbfClaim JWTNotYetValid >>
|
||||
verifyClaim mclaims "iat" isValidIatClaim JWTIssuedAtFuture >>
|
||||
verifyClaim mclaims "aud" isValidAudClaim JWTNotInAudience >>
|
||||
return jclaims
|
||||
_ -> Left $ JwtClaimsError "Parsing claims failed"
|
||||
_ -> Left $ JwtClaimsErr ParsingClaimsFailed
|
||||
-- TODO: We could enable JWE support here (encrypted tokens)
|
||||
verifyClaims _ = Left $ JwtDecodeError "Unsupported token type"
|
||||
verifyClaims _ = Left $ JwtDecodeErr UnsupportedTokenType
|
||||
|
||||
verifyClaim mclaims claim func err = do
|
||||
isValid <- maybe (Right True) func (KM.lookup claim mclaims)
|
||||
unless isValid $ Left $ JwtClaimsError err
|
||||
unless isValid $ Left $ JwtClaimsErr err
|
||||
|
||||
allowedSkewSeconds = 30 :: Int64
|
||||
now = floor . nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds time
|
||||
@@ -104,15 +105,15 @@ parseToken AppConfig{..} (Just tkn) time = do
|
||||
|
||||
isValidExpClaim :: JSON.Value -> Either JwtError Bool
|
||||
isValidExpClaim (JSON.Number secs) = Right $ now <= (sciToInt secs + allowedSkewSeconds)
|
||||
isValidExpClaim _ = Left $ JwtClaimsError "The JWT 'exp' claim must be a number"
|
||||
isValidExpClaim _ = Left $ JwtClaimsErr ExpClaimNotNumber
|
||||
|
||||
isValidNbfClaim :: JSON.Value -> Either JwtError Bool
|
||||
isValidNbfClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
||||
isValidNbfClaim _ = Left $ JwtClaimsError "The JWT 'nbf' claim must be a number"
|
||||
isValidNbfClaim _ = Left $ JwtClaimsErr NbfClaimNotNumber
|
||||
|
||||
isValidIatClaim :: JSON.Value -> Either JwtError Bool
|
||||
isValidIatClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
||||
isValidIatClaim _ = Left $ JwtClaimsError "The JWT 'iat' claim must be a number"
|
||||
isValidIatClaim _ = Left $ JwtClaimsErr IatClaimNotNumber
|
||||
|
||||
isValidAudClaim :: JSON.Value -> Either JwtError Bool
|
||||
isValidAudClaim JSON.Null = Right True -- {"aud": null} is valid for all audiences
|
||||
@@ -120,7 +121,7 @@ parseToken AppConfig{..} (Just tkn) time = do
|
||||
isValidAudClaim (JSON.Array arr)
|
||||
| null arr = Right True -- {"aud": []} is valid for all audiences
|
||||
| allStrings arr = Right $ maybe True (\a -> JSON.String a `elem` arr) configJwtAudience
|
||||
isValidAudClaim _ = Left $ JwtClaimsError "The JWT 'aud' claim must be a string or an array of strings"
|
||||
isValidAudClaim _ = Left $ JwtClaimsErr AudClaimNotStringOrArray
|
||||
|
||||
parseClaims :: Monad m =>
|
||||
AppConfig -> JSON.Value -> ExceptT Error m AuthResult
|
||||
|
||||
@@ -60,7 +60,7 @@ dumpSchema appState = do
|
||||
case result of
|
||||
Left e -> do
|
||||
let observer = AppState.getObserver appState
|
||||
observer $ SchemaCacheErrorObs e
|
||||
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
|
||||
exitFailure
|
||||
Right sCache -> return $ JSON.encode sCache
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ import Data.List.NonEmpty (fromList, toList)
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.Scientific (floatingOrInteger)
|
||||
import Jose.Jwk (Jwk, JwkSet)
|
||||
import Network.URI (escapeURIString,
|
||||
import Network.URI (escapeURIString, isURI,
|
||||
isUnescapedInURIComponent)
|
||||
import Numeric (readOct, showOct)
|
||||
import System.Environment (getEnvironment)
|
||||
@@ -256,8 +256,8 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
|
||||
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
||||
<*> (fromMaybe True <$> optBool "db-channel-enabled")
|
||||
<*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
|
||||
<*> (maybe defaultHoistedAllowList splitOnCommas <$> optValue "db-hoisted-tx-settings")
|
||||
<*> (maybe ["public"] splitOnCommasEmptyable <$> optStringEmptyable "db-extra-search-path")
|
||||
<*> (maybe defaultHoistedAllowList splitOnCommas <$> optString "db-hoisted-tx-settings")
|
||||
<*> optWithAlias (optInt "db-max-rows")
|
||||
(optInt "max-rows")
|
||||
<*> (fromMaybe False <$> optBool "db-plan-enabled")
|
||||
@@ -272,8 +272,8 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
||||
<*> (fmap toQi <$> optWithAlias (optString "db-root-spec")
|
||||
(optString "root-spec"))
|
||||
<*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optValue "db-schemas")
|
||||
(optValue "db-schema"))
|
||||
<*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optString "db-schemas")
|
||||
(optString "db-schema"))
|
||||
<*> (fromMaybe True <$> optBool "db-config")
|
||||
<*> (fmap toQi <$> optString "db-pre-config")
|
||||
<*> parseTxEnd "db-tx-end" snd
|
||||
@@ -281,7 +281,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
|
||||
<*> pure optPath
|
||||
<*> pure Nothing
|
||||
<*> optString "jwt-aud"
|
||||
<*> optStringOrURI "jwt-aud"
|
||||
<*> parseRoleClaimKey "jwt-role-claim-key" "role-claim-key"
|
||||
<*> (fmap encodeUtf8 <$> optString "jwt-secret")
|
||||
<*> (fromMaybe False <$> optWithAlias
|
||||
@@ -404,8 +404,22 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
optString :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||
optString k = mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
||||
|
||||
optValue :: C.Key -> C.Parser C.Config (Maybe C.Value)
|
||||
optValue k = overrideFromDbOrEnvironment C.optional k identity
|
||||
optStringEmptyable :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||
optStringEmptyable k = overrideFromDbOrEnvironment C.optional k coerceText
|
||||
|
||||
optStringOrURI :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||
optStringOrURI k = do
|
||||
stringOrURI <- mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
||||
-- If the string contains ':' then it should
|
||||
-- be a valid URI according to RFC 3986
|
||||
case stringOrURI of
|
||||
Just s -> if T.isInfixOf ":" s then validateURI s else return (Just s)
|
||||
Nothing -> return Nothing
|
||||
where
|
||||
validateURI :: Text -> C.Parser C.Config (Maybe Text)
|
||||
validateURI s = if isURI (T.unpack s)
|
||||
then return $ Just s
|
||||
else fail "jwt-aud should be a string or a valid URI"
|
||||
|
||||
optInt :: (Read i, Integral i) => C.Key -> C.Parser C.Config (Maybe i)
|
||||
optInt k = join <$> overrideFromDbOrEnvironment C.optional k coerceInt
|
||||
@@ -445,9 +459,12 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||
Nothing -> (> 0) <$> (readMaybe s :: Maybe Integer)
|
||||
coerceBool _ = Nothing
|
||||
|
||||
splitOnCommas :: C.Value -> [Text]
|
||||
splitOnCommas (C.String s) = T.strip <$> T.splitOn "," s
|
||||
splitOnCommas _ = []
|
||||
splitOnCommas :: Text -> [Text]
|
||||
splitOnCommas s = T.strip <$> T.splitOn "," s
|
||||
|
||||
splitOnCommasEmptyable :: Text -> [Text]
|
||||
splitOnCommasEmptyable "" = []
|
||||
splitOnCommasEmptyable s = T.strip <$> T.splitOn "," s
|
||||
|
||||
defaultHoistedAllowList = ["statement_timeout","plan_filter.statement_cost_limit","default_transaction_isolation"]
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ module PostgREST.Error
|
||||
, PgError(..)
|
||||
, Error(..)
|
||||
, JwtError (..)
|
||||
, JwtDecodeError(..)
|
||||
, JwtClaimsError(..)
|
||||
, errorPayload
|
||||
, status
|
||||
) where
|
||||
@@ -86,7 +88,7 @@ data ApiRequestError
|
||||
| QueryParamError QPError
|
||||
| RelatedOrderNotToOne Text Text
|
||||
| UnacceptableFilter Text
|
||||
| UnacceptableSchema [Text]
|
||||
| UnacceptableSchema Text [Text]
|
||||
| UnsupportedMethod ByteString
|
||||
| GucHeadersError
|
||||
| GucStatusError
|
||||
@@ -96,6 +98,7 @@ data ApiRequestError
|
||||
| MaxAffectedViolationError Integer
|
||||
| InvalidResourcePath
|
||||
| OpenAPIDisabled
|
||||
| MaxAffectedRpcViolation
|
||||
deriving Show
|
||||
|
||||
data QPError = QPError Text Text
|
||||
@@ -138,6 +141,7 @@ instance PgrstError ApiRequestError where
|
||||
status MaxAffectedViolationError{} = HTTP.status400
|
||||
status InvalidResourcePath = HTTP.status404
|
||||
status OpenAPIDisabled = HTTP.status404
|
||||
status MaxAffectedRpcViolation = HTTP.status400
|
||||
|
||||
headers _ = mempty
|
||||
|
||||
@@ -184,6 +188,7 @@ instance ErrorBody ApiRequestError where
|
||||
code InvalidResourcePath = "PGRST125"
|
||||
code OpenAPIDisabled = "PGRST126"
|
||||
code NotImplemented{} = "PGRST127"
|
||||
code MaxAffectedRpcViolation = "PGRST128"
|
||||
|
||||
-- MESSAGE: Text
|
||||
message (QueryParamError (QPError msg _)) = msg
|
||||
@@ -191,7 +196,7 @@ instance ErrorBody ApiRequestError where
|
||||
message (InvalidBody errorMessage) = T.decodeUtf8 errorMessage
|
||||
message (InvalidRange _) = "Requested range not satisfiable"
|
||||
message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators"
|
||||
message (UnacceptableSchema schemas) = "The schema must be one of the following: " <> T.intercalate ", " schemas
|
||||
message (UnacceptableSchema sch _) = "Invalid schema: " <> sch
|
||||
message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)
|
||||
message (NotEmbedded resource) = "'" <> resource <> "' is not an embedded resource in this request"
|
||||
message GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value"
|
||||
@@ -209,6 +214,7 @@ instance ErrorBody ApiRequestError where
|
||||
message InvalidResourcePath = "Invalid path specified in request URL"
|
||||
message OpenAPIDisabled = "Root endpoint metadata is disabled"
|
||||
message (NotImplemented _) = "Feature not implemented"
|
||||
message MaxAffectedRpcViolation = "Function must return SETOF or TABLE when max-affected preference is used with handling=strict"
|
||||
|
||||
-- DETAILS: Maybe JSON.Value
|
||||
details (QueryParamError (QPError _ dets)) = Just $ JSON.String dets
|
||||
@@ -230,6 +236,7 @@ instance ErrorBody ApiRequestError where
|
||||
-- HINT: Maybe JSON.Value
|
||||
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
|
||||
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
|
||||
hint (UnacceptableSchema _ schemas) = Just $ JSON.String $ "Only the following schemas are exposed: " <> T.intercalate ", " schemas
|
||||
|
||||
hint _ = Nothing
|
||||
|
||||
@@ -595,6 +602,10 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
|
||||
if BS.isSuffixOf "requires a WHERE clause" m
|
||||
then HTTP.status400 -- special case for pg-safeupdate, which we consider as client error
|
||||
else HTTP.status500 -- generic function or view server error, e.g. "more than one row returned by a subquery used as an expression"
|
||||
"22023" -> -- invalid_parameter_value. Catch nonexistent role error, see https://github.com/PostgREST/postgrest/issues/3601
|
||||
if BS.isPrefixOf "role" m && BS.isSuffixOf "does not exist" m
|
||||
then HTTP.status401 -- role in jwt does not exist
|
||||
else HTTP.status400
|
||||
'2':'5':_ -> HTTP.status500 -- invalid tx state
|
||||
'2':'8':_ -> HTTP.status403 -- invalid auth specification
|
||||
'2':'D':_ -> HTTP.status500 -- invalid tx termination
|
||||
@@ -639,10 +650,32 @@ data Error
|
||||
deriving Show
|
||||
|
||||
data JwtError
|
||||
= JwtDecodeError Text
|
||||
= JwtDecodeErr JwtDecodeError
|
||||
| JwtSecretMissing
|
||||
| JwtTokenRequired
|
||||
| JwtClaimsError Text
|
||||
| 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
|
||||
@@ -688,14 +721,14 @@ instance ErrorBody Error where
|
||||
hint (PgErr err) = hint err
|
||||
|
||||
instance PgrstError JwtError where
|
||||
status JwtDecodeError{} = HTTP.unauthorized401
|
||||
status JwtDecodeErr{} = HTTP.unauthorized401
|
||||
status JwtSecretMissing = HTTP.status500
|
||||
status JwtTokenRequired = HTTP.unauthorized401
|
||||
status JwtClaimsError{} = HTTP.unauthorized401
|
||||
status JwtClaimsErr{} = HTTP.unauthorized401
|
||||
|
||||
headers (JwtDecodeError m) = [invalidTokenHeader m]
|
||||
headers e@(JwtDecodeErr _) = [invalidTokenHeader $ message e]
|
||||
headers JwtTokenRequired = [requiredTokenHeader]
|
||||
headers (JwtClaimsError m) = [invalidTokenHeader m]
|
||||
headers e@(JwtClaimsErr _) = [invalidTokenHeader $ message e]
|
||||
headers _ = mempty
|
||||
|
||||
instance JSON.ToJSON JwtError where
|
||||
@@ -703,16 +736,36 @@ instance JSON.ToJSON JwtError where
|
||||
(code err) (message err) (details err) (hint err)
|
||||
|
||||
instance ErrorBody JwtError where
|
||||
code JwtSecretMissing = "PGRST300"
|
||||
code (JwtDecodeError _) = "PGRST301"
|
||||
code JwtTokenRequired = "PGRST302"
|
||||
code (JwtClaimsError _) = "PGRST303"
|
||||
code JwtSecretMissing = "PGRST300"
|
||||
code (JwtDecodeErr _) = "PGRST301"
|
||||
code JwtTokenRequired = "PGRST302"
|
||||
code (JwtClaimsErr _) = "PGRST303"
|
||||
|
||||
message JwtSecretMissing = "Server lacks JWT secret"
|
||||
message (JwtDecodeError msg) = msg
|
||||
message JwtTokenRequired = "Anonymous access is disabled"
|
||||
message (JwtClaimsError msg) = msg
|
||||
message JwtSecretMissing = "Server lacks JWT secret"
|
||||
message (JwtDecodeErr e) = case e of
|
||||
EmptyAuthHeader -> "Empty JWT is sent in Authorization header"
|
||||
UnexpectedParts n -> "Expected 3 parts in JWT; got " <> show n
|
||||
KeyError _ -> "No suitable key or wrong key type"
|
||||
BadAlgorithm _ -> "Wrong or unsupported encoding algorithm"
|
||||
BadCrypto -> "JWT cryptographic operation failed"
|
||||
UnsupportedTokenType -> "Unsupported token type"
|
||||
UnreachableDecodeError -> "JWT couldn't be decoded"
|
||||
message JwtTokenRequired = "Anonymous access is disabled"
|
||||
message (JwtClaimsErr e) = case e of
|
||||
JWTExpired -> "JWT expired"
|
||||
JWTNotYetValid -> "JWT not yet valid"
|
||||
JWTIssuedAtFuture -> "JWT issued at future"
|
||||
JWTNotInAudience -> "JWT not in audience"
|
||||
ParsingClaimsFailed -> "Parsing claims failed"
|
||||
ExpClaimNotNumber -> "The JWT 'exp' claim must be a number"
|
||||
NbfClaimNotNumber -> "The JWT 'nbf' claim must be a number"
|
||||
IatClaimNotNumber -> "The JWT 'iat' claim must be a number"
|
||||
AudClaimNotStringOrArray -> "The JWT 'aud' claim must be a string or an array of strings"
|
||||
|
||||
details (JwtDecodeErr jde) = case jde of
|
||||
KeyError dets -> Just $ JSON.String dets
|
||||
BadAlgorithm dets -> Just $ JSON.String dets
|
||||
_ -> Nothing
|
||||
details _ = Nothing
|
||||
|
||||
hint _ = Nothing
|
||||
|
||||
@@ -52,7 +52,7 @@ observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schema
|
||||
SchemaCacheLoadedObs resTime -> do
|
||||
withLabel schemaCacheLoads "SUCCESS" incCounter
|
||||
setGauge schemaCacheQueryTime resTime
|
||||
SchemaCacheErrorObs _ -> do
|
||||
SchemaCacheErrorObs{} -> do
|
||||
withLabel schemaCacheLoads "FAIL" incCounter
|
||||
_ ->
|
||||
pure ()
|
||||
|
||||
@@ -13,5 +13,9 @@ resolveHost sock = do
|
||||
sn <- NS.getSocketName sock
|
||||
case sn of
|
||||
NS.SockAddrInet _ hostAddr -> pure $ Just $ fromString $ show $ fromHostAddress hostAddr
|
||||
NS.SockAddrInet6 _ _ hostAddr6 _ -> pure $ Just $ fromString $ show $ fromHostAddress6 hostAddr6
|
||||
-- The IPv6 addresses are wrapped in [] brackets. This is done in accordance
|
||||
-- to RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).
|
||||
-- In short, we did this to have a clear separation between the port and host
|
||||
-- because the components of an IPv6 are separated with the ':' character.
|
||||
NS.SockAddrInet6 _ _ hostAddr6 _ -> pure $ Just $ fromString $ "[" ++ show (fromHostAddress6 hostAddr6) ++ "]"
|
||||
_ -> pure Nothing
|
||||
|
||||
@@ -14,6 +14,7 @@ module PostgREST.Observation
|
||||
) where
|
||||
|
||||
import qualified Data.ByteString.Lazy as LBS
|
||||
import Data.List.NonEmpty (toList)
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.Text.Encoding as T
|
||||
import qualified Hasql.Connection as SQL
|
||||
@@ -25,7 +26,7 @@ import Numeric (showFFloat)
|
||||
import PostgREST.Config.PgVersion
|
||||
import qualified PostgREST.Error as Error
|
||||
|
||||
import Protolude
|
||||
import Protolude hiding (toList)
|
||||
import Protolude.Partial (fromJust)
|
||||
|
||||
data Observation
|
||||
@@ -37,7 +38,7 @@ data Observation
|
||||
| ExitDBNoRecoveryObs
|
||||
| ExitDBFatalError ObsFatalError SQL.UsageError
|
||||
| DBConnectedObs Text
|
||||
| SchemaCacheErrorObs SQL.UsageError
|
||||
| SchemaCacheErrorObs (NonEmpty Text) [Text] SQL.UsageError
|
||||
| SchemaCacheQueriedObs Double
|
||||
| SchemaCacheSummaryObs Text
|
||||
| SchemaCacheLoadedObs Double
|
||||
@@ -88,8 +89,12 @@ observationMessage = \case
|
||||
"If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
|
||||
ExitDBFatalError ServerError08P01 usageErr ->
|
||||
"Connection poolers in statement mode are not supported." <> jsonMessage usageErr
|
||||
SchemaCacheErrorObs usageErr ->
|
||||
"Failed to load the schema cache. " <> jsonMessage usageErr
|
||||
SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
|
||||
"Failed to load the schema cache using "
|
||||
<> "db-schemas=" <> T.intercalate "," (toList dbSchemas)
|
||||
<> " and "
|
||||
<> "db-extra-search-path=" <> T.intercalate "," extraPaths
|
||||
<> ". " <> jsonMessage usageErr
|
||||
SchemaCacheQueriedObs resultTime ->
|
||||
"Schema cache queried in " <> showMillis resultTime <> " milliseconds"
|
||||
SchemaCacheSummaryObs summary ->
|
||||
@@ -103,11 +108,8 @@ observationMessage = \case
|
||||
DBListenStart channel -> do
|
||||
"Listening for database notifications on the " <> show channel <> " channel"
|
||||
DBListenFail channel listenErr ->
|
||||
"Failed listening for database notifications on the " <> show channel <> " channel. " <> (
|
||||
case listenErr of
|
||||
Left err -> show err
|
||||
Right err -> showListenerError err
|
||||
)
|
||||
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
||||
either showListenerConnError showListenerException listenErr
|
||||
DBListenRetry delay ->
|
||||
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
||||
DBListenerGotSCacheMsg channel ->
|
||||
@@ -152,8 +154,11 @@ observationMessage = \case
|
||||
|
||||
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
|
||||
|
||||
showListenerError :: Either SomeException () -> Text
|
||||
showListenerError (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
|
||||
showListenerError (Left e) =
|
||||
let showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt in -- the errors from hasql-notifications come intercalated with "\t\n"
|
||||
showOnSingleLine $ show e
|
||||
showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
|
||||
|
||||
showListenerConnError :: SQL.ConnectionError -> Text
|
||||
showListenerConnError = maybe "Connection error" (showOnSingleLine . T.decodeUtf8)
|
||||
|
||||
showListenerException :: Either SomeException () -> Text
|
||||
showListenerException (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
|
||||
showListenerException (Left e) = showOnSingleLine $ show e
|
||||
|
||||
@@ -72,7 +72,8 @@ import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
||||
RoutineParam (..),
|
||||
funcReturnsCompositeAlias,
|
||||
funcReturnsScalar,
|
||||
funcReturnsSetOfScalar)
|
||||
funcReturnsSetOfScalar,
|
||||
funcReturnsSingle)
|
||||
import PostgREST.SchemaCache.Table (Column (..), Table (..),
|
||||
TablesMap,
|
||||
tableColumnsList,
|
||||
@@ -172,7 +173,7 @@ mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..}
|
||||
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi
|
||||
|
||||
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
|
||||
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{preferHandling, invalidPrefs},..} invMethod = do
|
||||
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{preferHandling, invalidPrefs, preferMaxAffected},..} invMethod = do
|
||||
let paramKeys = case invMethod of
|
||||
InvRead _ -> S.fromList $ fst <$> qsParams'
|
||||
Inv -> iColumns
|
||||
@@ -192,10 +193,15 @@ callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferenc
|
||||
cPlan = callPlan proc apiRequest paramKeys args rPlan
|
||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||
failMaxAffectedRpcReturnsSingle (preferMaxAffected, preferHandling) proc
|
||||
return $ CallReadPlan rPlan cPlan txMode proc handler mediaType invMethod identifier
|
||||
where
|
||||
qsParams' = QueryParams.qsParams iQueryParams
|
||||
|
||||
failMaxAffectedRpcReturnsSingle :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> Routine -> Either Error ()
|
||||
failMaxAffectedRpcReturnsSingle (Just (PreferMaxAffected _), Just Strict) rout = if funcReturnsSingle rout then Left $ ApiRequestError MaxAffectedRpcViolation else Right ()
|
||||
failMaxAffectedRpcReturnsSingle _ _ = Right ()
|
||||
|
||||
hasDefaultSelect :: ReadPlanTree -> Bool
|
||||
hasDefaultSelect (Node ReadPlan{select=[CoercibleSelectField{csField=CoercibleField{cfName}}]} []) = cfName == "*"
|
||||
hasDefaultSelect _ = False
|
||||
@@ -272,7 +278,7 @@ data ResolverContext = ResolverContext
|
||||
}
|
||||
|
||||
resolveColumnField :: Column -> Maybe ToTsVector -> CoercibleField
|
||||
resolveColumnField col toTsV = CoercibleField (colName col) mempty False toTsV (colNominalType col) Nothing (colDefault col) False
|
||||
resolveColumnField col toTsV = CoercibleField (colName col) mempty False toTsV (colNominalType col) (colType col) Nothing (colDefault col) False
|
||||
|
||||
resolveTableFieldName :: Table -> FieldName -> Maybe ToTsVector -> CoercibleField
|
||||
resolveTableFieldName table fieldName toTsV=
|
||||
@@ -285,12 +291,12 @@ resolveTypeOrUnknown ResolverContext{..} (fn, jp) toTsV =
|
||||
case res of
|
||||
-- types that are already json/jsonb don't need to be converted with `to_jsonb` for using arrow operators `data->attr`
|
||||
-- this prevents indexes not applying https://github.com/PostgREST/postgrest/issues/2594
|
||||
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp, cfToJson=False}
|
||||
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp, cfToJson=False}
|
||||
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp, cfToJson=False}
|
||||
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp, cfToJson=False}
|
||||
-- Do not apply to_tsvector to tsvector types
|
||||
cf@CoercibleField{cfIRType="tsvector"} -> cf{cfJsonPath=jp, cfToJson=True, cfToTsVector=Nothing}
|
||||
cf@CoercibleField{cfBaseType="tsvector"} -> cf{cfJsonPath=jp, cfToJson=True, cfToTsVector=Nothing}
|
||||
-- other types will get converted `to_jsonb(col)->attr`, even unknown types
|
||||
cf -> cf{cfJsonPath=jp, cfToJson=True}
|
||||
cf -> cf{cfJsonPath=jp, cfToJson=True}
|
||||
where
|
||||
res = fromMaybe (unknownField fn jp) $ HM.lookup qi tables >>=
|
||||
Just . (\t -> resolveTableFieldName t fn toTsV)
|
||||
@@ -885,7 +891,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
-- where_ = [
|
||||
-- CoercibleStmnt (
|
||||
-- CoercibleFilter {
|
||||
-- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson=False, cfToTsVector = Nothing, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False},
|
||||
-- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson=False, cfToTsVector = Nothing, cfIRType = "", cfBaseType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False},
|
||||
-- opExpr = op
|
||||
-- }
|
||||
-- )
|
||||
@@ -901,7 +907,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
|
||||
--
|
||||
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
|
||||
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfToTsVector = Nothing, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is IsNull)})]
|
||||
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfToTsVector = Nothing, cfIRType = "", cfBaseType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is IsNull)})]
|
||||
--
|
||||
-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.
|
||||
--
|
||||
@@ -920,7 +926,7 @@ addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
|
||||
newNullFilters rPlans = \case
|
||||
(CoercibleExpr b lOp trees) ->
|
||||
CoercibleExpr b lOp <$> (newNullFilters rPlans `traverse` trees)
|
||||
flt@(CoercibleStmnt (CoercibleFilter (CoercibleField fld [] _ _ _ _ _ _) opExpr)) ->
|
||||
flt@(CoercibleStmnt (CoercibleFilter CoercibleField{cfName=fld, cfJsonPath=[]} opExpr)) ->
|
||||
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
|
||||
case (foundRP, opExpr) of
|
||||
(Just ReadPlan{relAggAlias}, OpExpr b (Is IsNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
|
||||
|
||||
@@ -44,13 +44,14 @@ data CoercibleField = CoercibleField
|
||||
, cfToJson :: Bool
|
||||
, cfToTsVector :: Maybe ToTsVector -- ^ If the field should be converted using to_tsvector(<language>, <field>)
|
||||
, cfIRType :: Text -- ^ The native Postgres type of the field, the intermediate (IR) type before mapping.
|
||||
, cfBaseType :: Text -- ^ The base type of the field in case of domains, or just the type otherwise (without modifiers in case of pg_catalog types)
|
||||
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
||||
, cfDefault :: Maybe Text
|
||||
, cfFullRow :: Bool -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spreaded resources.
|
||||
} deriving (Eq, Show)
|
||||
|
||||
unknownField :: FieldName -> JsonPath -> CoercibleField
|
||||
unknownField name path = CoercibleField name path False Nothing "" Nothing Nothing False
|
||||
unknownField name path = CoercibleField name path False Nothing "" "" Nothing Nothing False
|
||||
|
||||
-- | Like an API request LogicTree, but with coercible field information.
|
||||
data CoercibleLogicTree
|
||||
|
||||
@@ -53,8 +53,8 @@ readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicFor
|
||||
(if null logicForest && null relJoinConds
|
||||
then mempty
|
||||
else " WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition relJoinConds)) <> " " <>
|
||||
groupF qi select relSelect <>
|
||||
orderF qi order <>
|
||||
groupF qi select relSelect <> " " <>
|
||||
orderF qi order <> " " <>
|
||||
limitOffsetF readRange
|
||||
where
|
||||
fromFrag = fromF relToParent mainQi fromAlias
|
||||
@@ -70,21 +70,21 @@ readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicFor
|
||||
|
||||
getJoinSelects :: ReadPlanTree -> [SQL.Snippet]
|
||||
getJoinSelects (Node ReadPlan{relSelect} _) =
|
||||
mapMaybe relSelectToSnippet relSelect
|
||||
join $ map relSelectToSnippet relSelect
|
||||
where
|
||||
relSelectToSnippet :: RelSelectField -> Maybe SQL.Snippet
|
||||
relSelectToSnippet :: RelSelectField -> [SQL.Snippet]
|
||||
relSelectToSnippet fld =
|
||||
let aggAlias = pgFmtIdent $ rsAggAlias fld
|
||||
in
|
||||
case fld of
|
||||
JsonEmbed{rsEmptyEmbed = True} ->
|
||||
Nothing
|
||||
[]
|
||||
JsonEmbed{rsSelName, rsEmbedMode = JsonObject} ->
|
||||
Just $ "row_to_json(" <> aggAlias <> ".*)::jsonb AS " <> pgFmtIdent rsSelName
|
||||
["row_to_json(" <> aggAlias <> ".*)::jsonb AS " <> pgFmtIdent rsSelName]
|
||||
JsonEmbed{rsSelName, rsEmbedMode = JsonArray} ->
|
||||
Just $ "COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> pgFmtIdent rsSelName
|
||||
["COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> pgFmtIdent rsSelName]
|
||||
Spread{rsSpreadSel, rsAggAlias} ->
|
||||
Just $ intercalateSnippet ", " (pgFmtSpreadSelectItem rsAggAlias <$> rsSpreadSel)
|
||||
pgFmtSpreadSelectItem rsAggAlias <$> rsSpreadSel
|
||||
|
||||
getJoins :: ReadPlanTree -> [SQL.Snippet]
|
||||
getJoins (Node _ []) = []
|
||||
@@ -182,7 +182,7 @@ callPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScal
|
||||
KeyParams [] -> "FROM " <> callIt mempty
|
||||
KeyParams prms -> case arguments of
|
||||
DirectArgs args -> "FROM " <> callIt (fmtArgs prms args)
|
||||
JsonArgs json -> fromJsonBodyF json ((\p -> CoercibleField (ppName p) mempty False Nothing (ppTypeMaxLength p) Nothing Nothing False) <$> prms) False True False <> ", " <>
|
||||
JsonArgs json -> fromJsonBodyF json ((\p -> CoercibleField (ppName p) mempty False Nothing (ppTypeMaxLength p) mempty Nothing Nothing False) <$> prms) False True False <> ", " <>
|
||||
"LATERAL " <> callIt (fmtParams prms)
|
||||
|
||||
callIt :: SQL.Snippet -> SQL.Snippet
|
||||
|
||||
@@ -30,7 +30,8 @@ import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||
Relationship (..),
|
||||
RelationshipsMap)
|
||||
import PostgREST.SchemaCache.Routine (Routine (..),
|
||||
import PostgREST.SchemaCache.Routine (FuncVolatility (..),
|
||||
Routine (..),
|
||||
RoutineParam (..))
|
||||
import PostgREST.SchemaCache.Table (Column (..), Table (..),
|
||||
TablesMap,
|
||||
@@ -170,8 +171,9 @@ makePreferParam ts =
|
||||
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
|
||||
& in_ .~ ParamHeader
|
||||
& type_ ?~ SwaggerString
|
||||
& enum_ .~ JSON.decode (JSON.encode $ foldl (<>) [] (val <$> ts)))
|
||||
& enum_ .~ if null enu then Nothing else JSON.decode (JSON.encode enu))
|
||||
where
|
||||
enu = foldl (<>) [] (val <$> ts)
|
||||
val :: Text -> [Text]
|
||||
val = \case
|
||||
"count" -> ["count=none"]
|
||||
@@ -355,9 +357,9 @@ makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
|
||||
& parameters .~ makeProcGetParams (pdParams pd)
|
||||
postOp = procOp
|
||||
& parameters .~ makeProcPostParams pd
|
||||
pe = (mempty :: PathItem)
|
||||
& get ?~ getOp
|
||||
& post ?~ postOp
|
||||
pe = case pdVolatility pd of
|
||||
Volatile -> (mempty :: PathItem) & post ?~ postOp
|
||||
_ -> (mempty :: PathItem) & get ?~ getOp & post ?~ postOp
|
||||
|
||||
makeRootPathItem :: (FilePath, PathItem)
|
||||
makeRootPathItem = ("/", p)
|
||||
|
||||
@@ -375,32 +375,40 @@ accessibleFuncs = SQL.Statement sql params decodeFuncs
|
||||
(snd >$< arrayParam HE.text)
|
||||
sql = funcsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')"
|
||||
|
||||
funcsSqlQuery :: SqlQuery
|
||||
funcsSqlQuery = encodeUtf8 [trimming|
|
||||
-- Recursively get the base types of domains
|
||||
WITH
|
||||
baseTypesCte :: Text
|
||||
baseTypesCte = [trimming|
|
||||
-- Recursively get the base types of domains
|
||||
base_types AS (
|
||||
WITH RECURSIVE
|
||||
recurse AS (
|
||||
SELECT
|
||||
oid,
|
||||
typbasetype,
|
||||
COALESCE(NULLIF(typbasetype, 0), oid) AS base
|
||||
typnamespace AS base_namespace,
|
||||
COALESCE(NULLIF(typbasetype, 0), oid) AS base_type
|
||||
FROM pg_type
|
||||
UNION
|
||||
SELECT
|
||||
t.oid,
|
||||
b.typbasetype,
|
||||
COALESCE(NULLIF(b.typbasetype, 0), b.oid) AS base
|
||||
b.typnamespace AS base_namespace,
|
||||
COALESCE(NULLIF(b.typbasetype, 0), b.oid) AS base_type
|
||||
FROM recurse t
|
||||
JOIN pg_type b ON t.typbasetype = b.oid
|
||||
)
|
||||
SELECT
|
||||
oid,
|
||||
base
|
||||
base_namespace,
|
||||
base_type
|
||||
FROM recurse
|
||||
WHERE typbasetype = 0
|
||||
),
|
||||
)
|
||||
|]
|
||||
|
||||
funcsSqlQuery :: SqlQuery
|
||||
funcsSqlQuery = encodeUtf8 [trimming|
|
||||
WITH
|
||||
$baseTypesCte,
|
||||
arguments AS (
|
||||
SELECT
|
||||
oid,
|
||||
@@ -440,7 +448,7 @@ funcsSqlQuery = encodeUtf8 [trimming|
|
||||
-- if any TABLE, INOUT or OUT arguments present, treat as composite
|
||||
or COALESCE(proargmodes::text[] && '{t,b,o}', false)
|
||||
) AS rettype_is_composite,
|
||||
bt.oid <> bt.base as rettype_is_composite_alias,
|
||||
bt.oid <> bt.base_type as rettype_is_composite_alias,
|
||||
p.provolatile,
|
||||
p.provariadic > 0 as hasvariadic,
|
||||
lower((regexp_split_to_array((regexp_split_to_array(iso_config, '='))[2], ','))[1]) AS transaction_isolation_level,
|
||||
@@ -449,7 +457,7 @@ funcsSqlQuery = encodeUtf8 [trimming|
|
||||
LEFT JOIN arguments a ON a.oid = p.oid
|
||||
JOIN pg_namespace pn ON pn.oid = p.pronamespace
|
||||
JOIN base_types bt ON bt.oid = p.prorettype
|
||||
JOIN pg_type t ON t.oid = bt.base
|
||||
JOIN pg_type t ON t.oid = bt.base_type
|
||||
JOIN pg_namespace tn ON tn.oid = t.typnamespace
|
||||
LEFT JOIN pg_class comp ON comp.oid = t.typrelid
|
||||
LEFT JOIN pg_description as d ON d.objoid = p.oid AND d.classoid = 'pg_proc'::regclass
|
||||
@@ -615,6 +623,7 @@ tablesSqlQuery =
|
||||
-- generated columns are only available from pg >= 10 but the query is agnostic to versions. dep.deptype = 'i' is done because there are other 'a' dependencies on PKs
|
||||
encodeUtf8 [trimming|
|
||||
WITH
|
||||
$baseTypesCte,
|
||||
columns AS (
|
||||
SELECT
|
||||
c.oid AS relid,
|
||||
@@ -631,7 +640,7 @@ tablesSqlQuery =
|
||||
CASE
|
||||
WHEN t.typtype = 'd' THEN
|
||||
CASE
|
||||
WHEN bt.typnamespace = 'pg_catalog'::regnamespace THEN format_type(t.typbasetype, NULL::integer)
|
||||
WHEN bt.base_namespace = 'pg_catalog'::regnamespace THEN format_type(bt.base_type, NULL::integer)
|
||||
ELSE format_type(a.atttypid, a.atttypmod)
|
||||
END
|
||||
ELSE
|
||||
@@ -645,7 +654,7 @@ tablesSqlQuery =
|
||||
information_schema._pg_truetypid(a.*, t.*),
|
||||
information_schema._pg_truetypmod(a.*, t.*)
|
||||
)::integer AS character_maximum_length,
|
||||
COALESCE(bt.oid, t.oid) AS base_type,
|
||||
bt.base_type,
|
||||
a.attnum::integer AS position
|
||||
FROM pg_attribute a
|
||||
LEFT JOIN pg_description AS d
|
||||
@@ -656,8 +665,8 @@ tablesSqlQuery =
|
||||
ON a.attrelid = c.oid
|
||||
JOIN pg_type t
|
||||
ON a.atttypid = t.oid
|
||||
LEFT JOIN pg_type bt
|
||||
ON t.typtype = 'd' AND t.typbasetype = bt.oid
|
||||
LEFT JOIN base_types bt
|
||||
ON t.oid = bt.oid
|
||||
LEFT JOIN pg_depend seq
|
||||
ON seq.refobjid = a.attrelid and seq.refobjsubid = a.attnum and seq.deptype = 'i'
|
||||
WHERE
|
||||
|
||||
@@ -26,7 +26,7 @@ prettyVersion =
|
||||
docsVersion :: Text
|
||||
docsVersion
|
||||
| isPreRelease = "latest"
|
||||
| otherwise = "v" <> (T.intercalate "." . map show . take 1 $ version)
|
||||
| otherwise = "v" <> T.intercalate "." (take 1 version)
|
||||
|
||||
|
||||
-- | Versions with two components (e.g., '1.1') are treated as pre-releases.
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# We're keeping this at 21.7 for freebsd right now, because:
|
||||
# Error: [S-9443]
|
||||
# No setup information found for ghc-9.6.4 on your platform. This probably means a GHC binary
|
||||
# distribution has not yet been added for OS key freebsd64-ino64. Supported versions:
|
||||
# ghc-8.4.3, ghc-8.4.4, ghc-8.6.2, ghc-8.6.3, ghc-8.6.4, ghc-8.6.5, ghc-8.8.1, ghc-8.8.2,
|
||||
# ghc-8.8.3, ghc-8.8.4, ghc-8.10.3, ghc-8.10.4, ghc-8.10.6, ghc-8.10.7, ghc-9.0.2, ghc-9.2.5,
|
||||
# ghc-9.2.6, ghc-9.2.7, ghc-9.2.8 and ghc-9.4.5.
|
||||
resolver: lts-21.7 # 2023-08-14, GHC 9.4.5
|
||||
|
||||
nix:
|
||||
packages:
|
||||
- pcre
|
||||
- pkgconfig
|
||||
- postgresql
|
||||
- zlib
|
||||
# disable pure by default so that the test environment can be passed
|
||||
pure: false
|
||||
|
||||
extra-deps:
|
||||
- configurator-pg-0.2.10
|
||||
- fuzzyset-0.2.4
|
||||
- hasql-notifications-0.2.2.2
|
||||
- hasql-pool-1.0.1
|
||||
- postgresql-libpq-0.10.1.0
|
||||
@@ -1,47 +0,0 @@
|
||||
# This file was autogenerated by Stack.
|
||||
# You should not edit this file by hand.
|
||||
# For more information, please see the documentation at:
|
||||
# https://docs.haskellstack.org/en/stable/lock_files
|
||||
|
||||
packages:
|
||||
- completed:
|
||||
hackage: configurator-pg-0.2.10@sha256:dbb9381c4f491b838214289c59d465eff99f6b569334a98ccf836548970e0f49,2786
|
||||
pantry-tree:
|
||||
sha256: a6dc0197c8c8515877ff579c088511edcf2d1090d9a73ac618d884101d6cbed8
|
||||
size: 2463
|
||||
original:
|
||||
hackage: configurator-pg-0.2.10
|
||||
- completed:
|
||||
hackage: fuzzyset-0.2.4@sha256:f1b6de8bf33277bf6255207541d65028f1f1ea93af5541b654c86b5674995485,1618
|
||||
pantry-tree:
|
||||
sha256: cee68e8d88f530e9e0588b81b260236936fe3318ef9a66e9f43f680b4cd5f76e
|
||||
size: 574
|
||||
original:
|
||||
hackage: fuzzyset-0.2.4
|
||||
- completed:
|
||||
hackage: hasql-notifications-0.2.2.2@sha256:d1d6bc0d3ee5e418fc12ea023b78739e0decba6c34e2b43bec55b89e18bd4412,2025
|
||||
pantry-tree:
|
||||
sha256: 83a9cbb179b1efd0b2acd6509583c7afcdbe63469ab033d8581d48d675a80b44
|
||||
size: 452
|
||||
original:
|
||||
hackage: hasql-notifications-0.2.2.2
|
||||
- completed:
|
||||
hackage: hasql-pool-1.0.1@sha256:3cfb4c7153a6c536ac7e126c17723e6d26ee03794954deed2d72bcc826d05a40,2302
|
||||
pantry-tree:
|
||||
sha256: d98e1269bdd60989b0eb0b84e1d5357eaa9f92821439d9f206663b7251ee95b2
|
||||
size: 799
|
||||
original:
|
||||
hackage: hasql-pool-1.0.1
|
||||
- completed:
|
||||
hackage: postgresql-libpq-0.10.1.0@sha256:6b580c9d5068e78eecc13e655b2885c8e79cdacfca513c5d1e5a6b9dc61d9758,3166
|
||||
pantry-tree:
|
||||
sha256: ae81e7628a8f3d1ef33ace71fa0845c073c003ca7f1150cc9d9ba1e55fc84236
|
||||
size: 1096
|
||||
original:
|
||||
hackage: postgresql-libpq-0.10.1.0
|
||||
snapshots:
|
||||
- completed:
|
||||
sha256: 23bb9bb355bfdb1635252e120a29b712f0d5e8a6c6a65c5ab5bd6692f46c438e
|
||||
size: 640457
|
||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/21/7.yaml
|
||||
original: lts-21.7
|
||||
@@ -1,17 +1,16 @@
|
||||
resolver: lts-22.41 # 2024-11-10, GHC 9.6.6
|
||||
resolver: lts-22.44 # 2025-05-02, GHC 9.6.7
|
||||
|
||||
nix:
|
||||
packages:
|
||||
- pcre
|
||||
- pkgconfig
|
||||
- postgresql
|
||||
- libpq
|
||||
- pkg-config
|
||||
- zlib
|
||||
# disable pure by default so that the test environment can be passed
|
||||
pure: false
|
||||
|
||||
extra-deps:
|
||||
- configurator-pg-0.2.11
|
||||
- fuzzyset-0.2.4
|
||||
- hasql-pool-1.0.1
|
||||
- jose-jwt-0.10.0
|
||||
- postgresql-libpq-0.10.1.0
|
||||
- hasql-notifications-0.2.2.2
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
# This file was autogenerated by Stack.
|
||||
# You should not edit this file by hand.
|
||||
# For more information, please see the documentation at:
|
||||
# https://docs.haskellstack.org/en/stable/lock_files
|
||||
# https://docs.haskellstack.org/en/stable/topics/lock_files
|
||||
|
||||
packages:
|
||||
- completed:
|
||||
hackage: configurator-pg-0.2.11@sha256:de0c56386591e85159436b0af04a8f15a4f4e156354e99709676c2c2ee959505,2850
|
||||
pantry-tree:
|
||||
sha256: 7b4daa4ea970335414f26b313a168cd12182b7b34ea8af2616c83d38699c4301
|
||||
size: 2527
|
||||
original:
|
||||
hackage: configurator-pg-0.2.11
|
||||
- completed:
|
||||
hackage: fuzzyset-0.2.4@sha256:f1b6de8bf33277bf6255207541d65028f1f1ea93af5541b654c86b5674995485,1618
|
||||
pantry-tree:
|
||||
@@ -32,16 +39,9 @@ packages:
|
||||
size: 1096
|
||||
original:
|
||||
hackage: postgresql-libpq-0.10.1.0
|
||||
- completed:
|
||||
hackage: hasql-notifications-0.2.2.2@sha256:d1d6bc0d3ee5e418fc12ea023b78739e0decba6c34e2b43bec55b89e18bd4412,2025
|
||||
pantry-tree:
|
||||
sha256: 83a9cbb179b1efd0b2acd6509583c7afcdbe63469ab033d8581d48d675a80b44
|
||||
size: 452
|
||||
original:
|
||||
hackage: hasql-notifications-0.2.2.2
|
||||
snapshots:
|
||||
- completed:
|
||||
sha256: 1e32b51d9082fdf6f3bd92accc9dfffd4ddaf406404427fb10bf76d2bc03cbbb
|
||||
size: 720263
|
||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/41.yaml
|
||||
original: lts-22.41
|
||||
sha256: 238fa745b64f91184f9aa518fe04bdde6552533d169b0da5256670df83a0f1a9
|
||||
size: 721141
|
||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/22/44.yaml
|
||||
original: lts-22.44
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<svg width="917" height="146" viewBox="0 0 917 146" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.5462 82.5408H83.3862C80.9862 64.1408 69.1462 52.6208 52.0262 52.6208C35.3862 52.6208 22.1062 64.9408 19.5462 82.5408ZM102.906 92.6208V97.5808H19.7062C23.0662 115.341 37.1462 127.181 55.3862 127.181C67.8662 127.181 79.3862 121.261 86.2662 112.941L98.7462 125.261C88.5062 138.381 73.1462 145.101 54.5862 145.101C23.0662 145.101 0.186172 122.701 0.186172 89.9008C0.186172 58.3808 22.5862 34.7008 52.5062 34.7008C83.3862 34.7008 102.906 57.2608 102.906 92.6208ZM129.367 35.5008V98.7008C129.367 118.701 139.447 127.821 155.927 127.821C173.687 127.821 187.927 112.941 187.927 92.7808V35.5008H206.327V144.301H189.527L187.927 123.181V122.061C182.647 132.301 171.447 145.101 148.407 145.101C126.167 145.101 110.967 131.181 110.967 105.901V35.5008H129.367ZM217.72 144.301V35.5008H234.52L236.12 56.1408C241.88 42.2208 253.24 34.7008 268.6 34.7008C272.44 34.7008 275.96 35.0208 279.16 35.5008L277.72 53.5808C274.68 52.7808 271.16 52.4608 266.36 52.4608C247 52.4608 236.12 68.3008 236.12 91.6608V144.301H217.72ZM278.311 89.9008C278.311 56.7808 300.071 34.7008 332.551 34.7008C365.191 34.7008 386.791 56.7808 386.791 89.9008C386.791 123.021 365.191 145.101 332.551 145.101C300.071 145.101 278.311 123.021 278.311 89.9008ZM297.031 89.7408C297.031 112.141 311.591 127.021 332.551 127.021C353.671 127.021 368.071 112.141 368.071 89.7408C368.071 67.6608 353.671 52.7808 332.551 52.7808C311.591 52.7808 297.031 67.6608 297.031 89.7408Z" fill="#FF0831"/>
|
||||
<path d="M471.711 144.301V86.7008C471.711 64.1408 461.311 51.9808 443.711 51.9808C426.911 51.9808 413.151 69.1008 413.151 88.9408V144.301H394.751V35.5008H411.551L413.151 56.6208V59.5008C418.271 48.3008 428.671 34.7008 449.631 34.7008C473.631 34.7008 490.111 51.6608 490.111 76.9408V144.301H471.711ZM498.467 89.9008C498.467 56.7808 520.227 34.7008 552.707 34.7008C585.347 34.7008 606.947 56.7808 606.947 89.9008C606.947 123.021 585.347 145.101 552.707 145.101C520.227 145.101 498.467 123.021 498.467 89.9008ZM517.187 89.7408C517.187 112.141 531.747 127.021 552.707 127.021C573.827 127.021 588.227 112.141 588.227 89.7408C588.227 67.6608 573.827 52.7808 552.707 52.7808C531.747 52.7808 517.187 67.6608 517.187 89.7408ZM697.147 97.5808V85.5808C697.147 68.1408 682.907 52.4608 664.987 52.4608C647.067 52.4608 632.027 67.6608 632.027 89.7408C632.027 112.141 646.427 127.341 665.307 127.341C684.027 127.341 697.147 115.341 697.147 97.5808ZM698.747 144.301L697.307 127.501C691.707 136.301 681.307 145.101 662.427 145.101C634.107 145.101 613.307 123.021 613.307 89.9008C613.307 56.7808 634.427 34.7008 661.307 34.7008C680.187 34.7008 690.907 44.1408 697.147 53.9008V0.300776H715.547V144.301H698.747ZM744.702 82.5408H808.542C806.142 64.1408 794.302 52.6208 777.182 52.6208C760.542 52.6208 747.262 64.9408 744.702 82.5408ZM828.062 92.6208V97.5808H744.862C748.222 115.341 762.302 127.181 780.542 127.181C793.022 127.181 804.542 121.261 811.422 112.941L823.902 125.261C813.662 138.381 798.302 145.101 779.742 145.101C748.222 145.101 725.342 122.701 725.342 89.9008C725.342 58.3808 747.742 34.7008 777.662 34.7008C808.542 34.7008 828.062 57.2608 828.062 92.6208ZM916.161 115.181C916.161 132.781 900.481 145.101 876.641 145.101C858.721 145.101 844.321 137.581 834.561 127.021L843.841 112.781C851.361 120.781 861.761 128.141 876.801 128.141C889.761 128.141 897.441 122.221 897.441 114.221C897.441 90.5408 836.801 104.941 836.801 67.1808C836.801 47.9808 853.121 34.7008 874.721 34.7008C892.001 34.7008 905.761 42.0608 914.881 53.9008L902.721 65.4208C895.841 56.6208 884.961 52.1408 873.761 52.1408C862.721 52.1408 855.041 58.7008 855.041 67.0208C855.041 89.1008 916.161 75.5008 916.161 115.181Z" fill="black"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,23 @@
|
||||
<svg width="581" height="113" viewBox="0 0 581 113" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M151.397 66.7608C151.996 72.3621 157.091 81.9642 171.877 81.9642C184.764 81.9642 190.959 73.7624 190.959 65.7607C190.959 58.559 186.063 52.6577 176.373 50.6571L169.379 49.1569C166.682 48.6568 164.884 47.1565 164.884 44.7559C164.884 41.9552 167.681 39.8549 171.178 39.8549C176.772 39.8549 178.87 43.5556 179.27 46.4564L190.359 43.9558C189.76 38.6546 185.064 29.7527 171.078 29.7527C160.488 29.7527 152.696 37.0543 152.696 45.8561C152.696 52.7576 156.991 58.4591 166.482 60.5594L172.976 62.0598C176.772 62.8599 178.271 64.6605 178.271 66.8609C178.271 69.4615 176.173 71.762 171.777 71.762C165.983 71.762 163.085 68.1611 162.786 64.2602L151.397 66.7608Z" fill="white"/>
|
||||
<path d="M233.421 80.4639H246.109C245.909 78.7635 245.609 75.3628 245.609 71.5618V31.2529H232.321V59.8592C232.321 65.5606 228.925 69.5614 223.031 69.5614C216.837 69.5614 214.039 65.1604 214.039 59.6592V31.2529H200.752V62.3599C200.752 73.0622 207.545 81.7642 219.434 81.7642C224.628 81.7642 230.325 79.7638 233.022 75.1627C233.022 77.1631 233.221 79.4636 233.421 80.4639Z" fill="white"/>
|
||||
<path d="M273.076 99.4682V75.663C275.473 78.9636 280.469 81.6644 287.263 81.6644C301.149 81.6644 310.439 70.6617 310.439 55.7584C310.439 41.1553 302.148 30.1528 287.762 30.1528C280.37 30.1528 274.875 33.4534 272.677 37.2544V31.253H259.79V99.4682H273.076ZM297.352 55.8585C297.352 64.6606 291.958 69.7616 285.164 69.7616C278.372 69.7616 272.877 64.5605 272.877 55.8585C272.877 47.1566 278.372 42.0554 285.164 42.0554C291.958 42.0554 297.352 47.1566 297.352 55.8585Z" fill="white"/>
|
||||
<path d="M317.964 67.0609C317.964 74.7627 324.357 81.8643 334.848 81.8643C342.139 81.8643 346.835 78.4634 349.332 74.5625C349.332 76.463 349.532 79.1635 349.832 80.4639H362.02C361.72 78.7635 361.422 75.2627 361.422 72.6622V48.4567C361.422 38.5545 355.627 29.7527 340.043 29.7527C326.855 29.7527 319.761 38.2544 318.963 45.9562L330.751 48.4567C331.151 44.1558 334.348 40.455 340.141 40.455C345.737 40.455 348.434 43.3556 348.434 46.8564C348.434 48.5568 347.536 49.9572 344.738 50.3572L332.65 52.1576C324.458 53.3579 317.964 58.2589 317.964 67.0609ZM337.644 71.962C333.349 71.962 331.25 69.1614 331.25 66.2608C331.25 62.4599 333.947 60.5594 337.345 60.0594L348.434 58.359V60.5594C348.434 69.2615 343.239 71.962 337.644 71.962Z" fill="white"/>
|
||||
<path d="M387.703 80.4641V74.4627C390.299 78.6637 395.494 81.6644 402.288 81.6644C416.276 81.6644 425.467 70.5618 425.467 55.6585C425.467 41.0552 417.174 29.9528 402.788 29.9528C395.494 29.9528 390.1 33.1535 387.902 36.6541V8.04785H374.815V80.4641H387.703ZM412.178 55.7584C412.178 64.7605 406.784 69.7616 399.99 69.7616C393.297 69.7616 387.703 64.6606 387.703 55.7584C387.703 46.7564 393.297 41.8554 399.99 41.8554C406.784 41.8554 412.178 46.7564 412.178 55.7584Z" fill="white"/>
|
||||
<path d="M432.99 67.0609C432.99 74.7627 439.383 81.8643 449.873 81.8643C457.165 81.8643 461.862 78.4634 464.358 74.5625C464.358 76.463 464.559 79.1635 464.858 80.4639H477.046C476.748 78.7635 476.448 75.2627 476.448 72.6622V48.4567C476.448 38.5545 470.653 29.7527 455.068 29.7527C441.881 29.7527 434.788 38.2544 433.989 45.9562L445.776 48.4567C446.177 44.1558 449.374 40.455 455.167 40.455C460.763 40.455 463.46 43.3556 463.46 46.8564C463.46 48.5568 462.561 49.9572 459.763 50.3572L447.676 52.1576C439.484 53.3579 432.99 58.2589 432.99 67.0609ZM452.671 71.962C448.375 71.962 446.276 69.1614 446.276 66.2608C446.276 62.4599 448.973 60.5594 452.371 60.0594L463.46 58.359V60.5594C463.46 69.2615 458.265 71.962 452.671 71.962Z" fill="white"/>
|
||||
<path d="M485.645 66.7608C486.243 72.3621 491.339 81.9642 506.124 81.9642C519.012 81.9642 525.205 73.7624 525.205 65.7607C525.205 58.559 520.311 52.6577 510.62 50.6571L503.626 49.1569C500.929 48.6568 499.132 47.1565 499.132 44.7559C499.132 41.9552 501.928 39.8549 505.425 39.8549C511.021 39.8549 513.118 43.5556 513.519 46.4564L524.607 43.9558C524.007 38.6546 519.312 29.7527 505.326 29.7527C494.735 29.7527 486.944 37.0543 486.944 45.8561C486.944 52.7576 491.238 58.4591 500.73 60.5594L507.224 62.0598C511.021 62.8599 512.519 64.6605 512.519 66.8609C512.519 69.4615 510.421 71.762 506.025 71.762C500.23 71.762 497.334 68.1611 497.034 64.2602L485.645 66.7608Z" fill="white"/>
|
||||
<path d="M545.385 50.2571C545.685 45.7562 549.482 40.5549 556.375 40.5549C563.967 40.5549 567.165 45.3561 567.365 50.2571H545.385ZM568.664 63.0601C567.065 67.4609 563.668 70.5617 557.474 70.5617C550.88 70.5617 545.385 65.8606 545.087 59.3593H580.252C580.252 59.159 580.451 57.1587 580.451 55.2582C580.451 39.4547 571.361 29.7527 556.175 29.7527C543.588 29.7527 531.998 39.9548 531.998 55.6584C531.998 72.262 543.886 81.9642 557.374 81.9642C569.462 81.9642 577.255 74.8626 579.753 66.3607L568.664 63.0601Z" fill="white"/>
|
||||
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" fill-opacity="0.2"/>
|
||||
<path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#249361"/>
|
||||
<stop offset="1" stop-color="#3ECF8E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 4.4 KiB |
@@ -0,0 +1,23 @@
|
||||
<svg width="581" height="113" viewBox="0 0 581 113" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M151.397 66.7608C151.996 72.3621 157.091 81.9642 171.877 81.9642C184.764 81.9642 190.959 73.7624 190.959 65.7607C190.959 58.559 186.063 52.6577 176.373 50.6571L169.379 49.1569C166.682 48.6568 164.884 47.1565 164.884 44.7559C164.884 41.9552 167.681 39.8549 171.178 39.8549C176.772 39.8549 178.87 43.5556 179.27 46.4564L190.359 43.9558C189.76 38.6546 185.064 29.7527 171.078 29.7527C160.488 29.7527 152.696 37.0543 152.696 45.8561C152.696 52.7576 156.991 58.4591 166.482 60.5594L172.976 62.0598C176.772 62.8599 178.271 64.6605 178.271 66.8609C178.271 69.4615 176.173 71.762 171.777 71.762C165.983 71.762 163.085 68.1611 162.786 64.2602L151.397 66.7608Z" fill="#1F1F1F"/>
|
||||
<path d="M233.421 80.4639H246.109C245.909 78.7635 245.609 75.3628 245.609 71.5618V31.2529H232.321V59.8592C232.321 65.5606 228.925 69.5614 223.031 69.5614C216.837 69.5614 214.039 65.1604 214.039 59.6592V31.2529H200.752V62.3599C200.752 73.0622 207.545 81.7642 219.434 81.7642C224.628 81.7642 230.325 79.7638 233.022 75.1627C233.022 77.1631 233.221 79.4636 233.421 80.4639Z" fill="#1F1F1F"/>
|
||||
<path d="M273.076 99.4682V75.663C275.473 78.9636 280.469 81.6644 287.263 81.6644C301.149 81.6644 310.439 70.6617 310.439 55.7584C310.439 41.1553 302.148 30.1528 287.762 30.1528C280.37 30.1528 274.875 33.4534 272.677 37.2544V31.253H259.79V99.4682H273.076ZM297.352 55.8585C297.352 64.6606 291.958 69.7616 285.164 69.7616C278.372 69.7616 272.877 64.5605 272.877 55.8585C272.877 47.1566 278.372 42.0554 285.164 42.0554C291.958 42.0554 297.352 47.1566 297.352 55.8585Z" fill="#1F1F1F"/>
|
||||
<path d="M317.964 67.0609C317.964 74.7627 324.357 81.8643 334.848 81.8643C342.139 81.8643 346.835 78.4634 349.332 74.5625C349.332 76.463 349.532 79.1635 349.832 80.4639H362.02C361.72 78.7635 361.422 75.2627 361.422 72.6622V48.4567C361.422 38.5545 355.627 29.7527 340.043 29.7527C326.855 29.7527 319.761 38.2544 318.963 45.9562L330.751 48.4567C331.151 44.1558 334.348 40.455 340.141 40.455C345.737 40.455 348.434 43.3556 348.434 46.8564C348.434 48.5568 347.536 49.9572 344.738 50.3572L332.65 52.1576C324.458 53.3579 317.964 58.2589 317.964 67.0609ZM337.644 71.962C333.349 71.962 331.25 69.1614 331.25 66.2608C331.25 62.4599 333.947 60.5594 337.345 60.0594L348.434 58.359V60.5594C348.434 69.2615 343.239 71.962 337.644 71.962Z" fill="#1F1F1F"/>
|
||||
<path d="M387.703 80.4641V74.4627C390.299 78.6637 395.494 81.6644 402.288 81.6644C416.276 81.6644 425.467 70.5618 425.467 55.6585C425.467 41.0552 417.174 29.9528 402.788 29.9528C395.494 29.9528 390.1 33.1535 387.902 36.6541V8.04785H374.815V80.4641H387.703ZM412.178 55.7584C412.178 64.7605 406.784 69.7616 399.99 69.7616C393.297 69.7616 387.703 64.6606 387.703 55.7584C387.703 46.7564 393.297 41.8554 399.99 41.8554C406.784 41.8554 412.178 46.7564 412.178 55.7584Z" fill="#1F1F1F"/>
|
||||
<path d="M432.99 67.0609C432.99 74.7627 439.383 81.8643 449.873 81.8643C457.165 81.8643 461.862 78.4634 464.358 74.5625C464.358 76.463 464.559 79.1635 464.858 80.4639H477.046C476.748 78.7635 476.448 75.2627 476.448 72.6622V48.4567C476.448 38.5545 470.653 29.7527 455.068 29.7527C441.881 29.7527 434.788 38.2544 433.989 45.9562L445.776 48.4567C446.177 44.1558 449.374 40.455 455.167 40.455C460.763 40.455 463.46 43.3556 463.46 46.8564C463.46 48.5568 462.561 49.9572 459.763 50.3572L447.676 52.1576C439.484 53.3579 432.99 58.2589 432.99 67.0609ZM452.671 71.962C448.375 71.962 446.276 69.1614 446.276 66.2608C446.276 62.4599 448.973 60.5594 452.371 60.0594L463.46 58.359V60.5594C463.46 69.2615 458.265 71.962 452.671 71.962Z" fill="#1F1F1F"/>
|
||||
<path d="M485.645 66.7608C486.243 72.3621 491.339 81.9642 506.124 81.9642C519.012 81.9642 525.205 73.7624 525.205 65.7607C525.205 58.559 520.311 52.6577 510.62 50.6571L503.626 49.1569C500.929 48.6568 499.132 47.1565 499.132 44.7559C499.132 41.9552 501.928 39.8549 505.425 39.8549C511.021 39.8549 513.118 43.5556 513.519 46.4564L524.607 43.9558C524.007 38.6546 519.312 29.7527 505.326 29.7527C494.735 29.7527 486.944 37.0543 486.944 45.8561C486.944 52.7576 491.238 58.4591 500.73 60.5594L507.224 62.0598C511.021 62.8599 512.519 64.6605 512.519 66.8609C512.519 69.4615 510.421 71.762 506.025 71.762C500.23 71.762 497.334 68.1611 497.034 64.2602L485.645 66.7608Z" fill="#1F1F1F"/>
|
||||
<path d="M545.385 50.2571C545.685 45.7562 549.482 40.5549 556.375 40.5549C563.967 40.5549 567.165 45.3561 567.365 50.2571H545.385ZM568.664 63.0601C567.065 67.4609 563.668 70.5617 557.474 70.5617C550.88 70.5617 545.385 65.8606 545.087 59.3593H580.252C580.252 59.159 580.451 57.1587 580.451 55.2582C580.451 39.4547 571.361 29.7527 556.175 29.7527C543.588 29.7527 531.998 39.9548 531.998 55.6584C531.998 72.262 543.886 81.9642 557.374 81.9642C569.462 81.9642 577.255 74.8626 579.753 66.3607L568.664 63.0601Z" fill="#1F1F1F"/>
|
||||
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint0_linear)"/>
|
||||
<path d="M63.7076 110.284C60.8481 113.885 55.0502 111.912 54.9813 107.314L53.9738 40.0627L99.1935 40.0627C107.384 40.0627 111.952 49.5228 106.859 55.9374L63.7076 110.284Z" fill="url(#paint1_linear)" fill-opacity="0.2"/>
|
||||
<path d="M45.317 2.07103C48.1765 -1.53037 53.9745 0.442937 54.0434 5.041L54.4849 72.2922H9.83113C1.64038 72.2922 -2.92775 62.8321 2.1655 56.4175L45.317 2.07103Z" fill="#3ECF8E"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear" x1="53.9738" y1="54.974" x2="94.1635" y2="71.8295" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#249361"/>
|
||||
<stop offset="1" stop-color="#3ECF8E"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear" x1="36.1558" y1="30.578" x2="54.4844" y2="65.0806" gradientUnits="userSpaceOnUse">
|
||||
<stop/>
|
||||
<stop offset="1" stop-opacity="0"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1,40 @@
|
||||
db-aggregates-enabled = false
|
||||
db-anon-role = ""
|
||||
db-channel = "pgrst"
|
||||
db-channel-enabled = true
|
||||
db-extra-search-path = "public"
|
||||
db-hoisted-tx-settings = "statement_timeout,plan_filter.statement_cost_limit,default_transaction_isolation"
|
||||
db-max-rows = ""
|
||||
db-plan-enabled = false
|
||||
db-pool = 10
|
||||
db-pool-acquisition-timeout = 10
|
||||
db-pool-max-lifetime = 1800
|
||||
db-pool-max-idletime = 30
|
||||
db-pool-automatic-recovery = true
|
||||
db-pre-request = ""
|
||||
db-prepared-statements = true
|
||||
db-root-spec = ""
|
||||
db-schemas = "public"
|
||||
db-config = true
|
||||
db-pre-config = ""
|
||||
db-tx-end = "commit"
|
||||
db-uri = "postgresql://"
|
||||
jwt-aud = ""
|
||||
jwt-role-claim-key = ".\"role\""
|
||||
jwt-secret = ""
|
||||
jwt-secret-is-base64 = false
|
||||
jwt-cache-max-lifetime = 0
|
||||
log-level = "crit"
|
||||
log-query = "disabled"
|
||||
openapi-mode = "follow-privileges"
|
||||
openapi-security-active = false
|
||||
openapi-server-proxy-uri = ""
|
||||
server-cors-allowed-origins = ""
|
||||
server-host = "!4"
|
||||
server-port = 3000
|
||||
server-trace-header = ""
|
||||
server-timing-enabled = false
|
||||
server-unix-socket = ""
|
||||
server-unix-socket-mode = "660"
|
||||
admin-server-host = "!4"
|
||||
admin-server-port = ""
|
||||
@@ -0,0 +1,2 @@
|
||||
# Commènt utf-8 chàrs
|
||||
log-level = "crit"
|
||||
@@ -41,12 +41,11 @@ cli:
|
||||
use_defaultenv: true
|
||||
env:
|
||||
PGRST_SERVER_UNIX_SOCKET_MODE: '778'
|
||||
# TODO: Bug needs to be fixed
|
||||
# - name: invalid jwt-aud
|
||||
# expect: error
|
||||
# use_defaultenv: true
|
||||
# env:
|
||||
# PGRST_JWT_AUD: 'htp:/@@localhorst.invalid'
|
||||
- name: invalid jwt-aud
|
||||
expect: error
|
||||
use_defaultenv: true
|
||||
env:
|
||||
PGRST_JWT_AUD: 'http://%%localhorst.invalid'
|
||||
- name: invalid log-level
|
||||
expect: error
|
||||
use_defaultenv: true
|
||||
@@ -196,6 +195,24 @@ roleclaims:
|
||||
- obj_key: obj_value
|
||||
expected_status: 401 # fails because it compares an object with a string
|
||||
|
||||
jwtaudroleclaims:
|
||||
- key: '.aud'
|
||||
data:
|
||||
aud: postgrest_test_author
|
||||
expected_status: 200
|
||||
- key: '.aud'
|
||||
data:
|
||||
aud: postgrest_test_invalid
|
||||
expected_status: 401
|
||||
- key: '.aud[0]'
|
||||
data:
|
||||
aud: [postgrest_test_author]
|
||||
expected_status: 200
|
||||
- key: '.aud[1]' # succeeds the aud claims check, but fail when hits the db
|
||||
data:
|
||||
aud: [postgrest_test_author, postgrest_test_invalid]
|
||||
expected_status: 401
|
||||
|
||||
invalidroleclaimkeys:
|
||||
- 'role.other'
|
||||
- '.role##'
|
||||
|
||||
@@ -96,7 +96,9 @@ def run(
|
||||
if port:
|
||||
env["PGRST_SERVER_PORT"] = str(port)
|
||||
env["PGRST_SERVER_HOST"] = host or "localhost"
|
||||
baseurl = f"http://localhost:{port}"
|
||||
# When constructing IPv6 address, host address should be bracketed like [host]
|
||||
apihost = f"[{host}]" if host and is_ipv6(host) else "localhost"
|
||||
baseurl = f"http://{apihost}:{port}"
|
||||
else:
|
||||
socketfile = pathlib.Path(tmpdir) / "postgrest.sock"
|
||||
env["PGRST_SERVER_UNIX_SOCKET"] = str(socketfile)
|
||||
@@ -104,7 +106,8 @@ def run(
|
||||
|
||||
adminport = freeport(port)
|
||||
env["PGRST_ADMIN_SERVER_PORT"] = str(adminport)
|
||||
adminurl = f"http://localhost:{adminport}"
|
||||
adminhost = f"[{host}]" if host and is_ipv6(host) else "localhost"
|
||||
adminurl = f"http://{adminhost}:{adminport}"
|
||||
|
||||
command = [POSTGREST_BIN]
|
||||
env["HPCTIXFILE"] = hpctixfile()
|
||||
@@ -218,3 +221,11 @@ def sleep_pool_connection(url, seconds):
|
||||
session.get(url + f"/rpc/sleep?seconds={seconds}", timeout=0.1)
|
||||
except requests.exceptions.ReadTimeout:
|
||||
pass
|
||||
|
||||
|
||||
def is_ipv6(addr):
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, addr)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
@@ -278,3 +278,15 @@ def test_schema_cache_snapshot(baseenv, key, snapshot_yaml):
|
||||
Dumper=yaml.SafeDumper if key == "dbTimezones" else ExtraNewLinesDumper,
|
||||
)
|
||||
assert formatted == snapshot_yaml
|
||||
|
||||
|
||||
def test_jwt_aud_config_set_to_invalid_uri(defaultenv):
|
||||
"PostgREST should exit with an error message in output if jwt-aud config is set to an invalid URI"
|
||||
env = {
|
||||
**defaultenv,
|
||||
"PGRST_JWT_AUD": "foo://%%$$^^.com",
|
||||
}
|
||||
|
||||
with pytest.raises(PostgrestError):
|
||||
dump = cli(["--dump-config"], env=env).split("\n")
|
||||
assert "jwt-aud should be a string or a valid URI" in dump
|
||||
|
||||
@@ -93,27 +93,29 @@ def test_jwt_errors(defaultenv):
|
||||
response = postgrest.session.get("/", headers=headers)
|
||||
assert response.status_code == 401
|
||||
assert response.json()["message"] == "No suitable key or wrong key type"
|
||||
assert (
|
||||
response.json()["details"] == "None of the keys was able to decode the JWT"
|
||||
)
|
||||
|
||||
headers = jwtauthheader({"role": "not_existing"}, SECRET)
|
||||
response = postgrest.session.get("/", headers=headers)
|
||||
# TODO: Should this return 401?
|
||||
assert response.status_code == 400
|
||||
assert response.status_code == 401
|
||||
assert response.json()["message"] == 'role "not_existing" does not exist'
|
||||
|
||||
# -31 seconds, because we allow clock skew of 30 seconds
|
||||
headers = jwtauthheader({"exp": relativeSeconds(-31)}, SECRET)
|
||||
# -35 seconds, because we allow clock skew of 30 seconds
|
||||
headers = jwtauthheader({"exp": relativeSeconds(-35)}, SECRET)
|
||||
response = postgrest.session.get("/", headers=headers)
|
||||
assert response.status_code == 401
|
||||
assert response.json()["message"] == "JWT expired"
|
||||
|
||||
# 31 seconds, because we allow clock skew of 30 seconds
|
||||
headers = jwtauthheader({"nbf": relativeSeconds(31)}, SECRET)
|
||||
# 35 seconds, because we allow clock skew of 30 seconds
|
||||
headers = jwtauthheader({"nbf": relativeSeconds(35)}, SECRET)
|
||||
response = postgrest.session.get("/", headers=headers)
|
||||
assert response.status_code == 401
|
||||
assert response.json()["message"] == "JWT not yet valid"
|
||||
|
||||
# 31 seconds, because we allow clock skew of 30 seconds
|
||||
headers = jwtauthheader({"iat": relativeSeconds(31)}, SECRET)
|
||||
# 35 seconds, because we allow clock skew of 35 seconds
|
||||
headers = jwtauthheader({"iat": relativeSeconds(35)}, SECRET)
|
||||
response = postgrest.session.get("/", headers=headers)
|
||||
assert response.status_code == 401
|
||||
assert response.json()["message"] == "JWT issued at future"
|
||||
@@ -142,6 +144,10 @@ def test_jwt_errors(defaultenv):
|
||||
response = postgrest.session.get("/", headers=headers)
|
||||
assert response.status_code == 401
|
||||
assert response.json()["message"] == "Wrong or unsupported encoding algorithm"
|
||||
assert (
|
||||
response.json()["details"]
|
||||
== "JWT is unsecured but expected 'alg' was not 'none'"
|
||||
)
|
||||
|
||||
env = {
|
||||
**defaultenv,
|
||||
@@ -234,6 +240,28 @@ def test_role_claim_key(roleclaim, defaultenv):
|
||||
assert response.status_code == roleclaim["expected_status"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"jwtaudroleclaim",
|
||||
FIXTURES["jwtaudroleclaims"],
|
||||
ids=lambda claim: claim["key"] + "_" + str(claim["expected_status"]),
|
||||
)
|
||||
def test_jwt_aud_in_role_claim_key(jwtaudroleclaim, defaultenv):
|
||||
"Allows authorization with JWT aud claim in role-claim-key"
|
||||
|
||||
env = {
|
||||
**defaultenv,
|
||||
"PGRST_JWT_AUD": "postgrest_test_author",
|
||||
"PGRST_JWT_ROLE_CLAIM_KEY": jwtaudroleclaim["key"],
|
||||
"PGRST_JWT_SECRET": SECRET,
|
||||
}
|
||||
|
||||
headers = jwtauthheader(jwtaudroleclaim["data"], SECRET)
|
||||
|
||||
with run(env=env) as postgrest:
|
||||
response = postgrest.session.get("/authors_only", headers=headers)
|
||||
assert response.status_code == jwtaudroleclaim["expected_status"]
|
||||
|
||||
|
||||
def test_iat_claim(defaultenv):
|
||||
"""
|
||||
A claim with an 'iat' (issued at) attribute should be successful.
|
||||
@@ -1334,9 +1362,9 @@ def test_log_postgrest_version(defaultenv):
|
||||
assert "Starting PostgREST %s..." % version in output[0]
|
||||
|
||||
|
||||
def test_log_postgrest_host_and_port(defaultenv):
|
||||
@pytest.mark.parametrize("host", ["127.0.0.1", "::1"])
|
||||
def test_log_postgrest_host_and_port(host, defaultenv):
|
||||
"PostgREST should output the host and port it is bound to."
|
||||
host = "127.0.0.1"
|
||||
port = freeport()
|
||||
|
||||
with run(
|
||||
@@ -1344,7 +1372,10 @@ def test_log_postgrest_host_and_port(defaultenv):
|
||||
) as postgrest:
|
||||
output = postgrest.read_stdout(nlines=10)
|
||||
|
||||
assert f"API server listening on {host}:{port}" in output[2] # output-sensitive
|
||||
if is_ipv6(host): # IPv6
|
||||
assert f"API server listening on [{host}]:{port}" in output[2]
|
||||
else: # IPv4
|
||||
assert f"API server listening on {host}:{port}" in output[2]
|
||||
|
||||
|
||||
def test_succeed_w_role_having_superuser_settings(defaultenv):
|
||||
@@ -1702,6 +1733,7 @@ def test_admin_metrics(defaultenv):
|
||||
with run(env=defaultenv, port=freeport()) as postgrest:
|
||||
response = postgrest.admin.get("/metrics")
|
||||
assert response.status_code == 200
|
||||
assert response.headers["Content-Type"] == "text/plain; charset=utf-8"
|
||||
assert "pgrst_schema_cache_query_time_seconds" in response.text
|
||||
assert 'pgrst_schema_cache_loads_total{status="SUCCESS"}' in response.text
|
||||
assert "pgrst_db_pool_max" in response.text
|
||||
@@ -1875,3 +1907,54 @@ def test_invalidate_jwt_cache_when_secret_changes(tmp_path, defaultenv):
|
||||
# now the request should fail because the cached token is removed
|
||||
response = postgrest.session.get("/authors_only", headers=headers)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_allow_configs_to_be_set_to_empty(defaultenv):
|
||||
'configs that are explicitly set to empty (= "<empty>") should not throw parse error'
|
||||
|
||||
env = {
|
||||
**defaultenv,
|
||||
"PGRST_DB_EXTRA_SEARCH_PATH": "",
|
||||
}
|
||||
|
||||
with run(env=env) as postgrest:
|
||||
response = postgrest.session.get("/projects")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_schema_cache_error_observation(defaultenv):
|
||||
"schema cache error observation should be logged with invalid db-schemas or db-extra-search-path"
|
||||
|
||||
env = {
|
||||
**defaultenv,
|
||||
"PGRST_DB_EXTRA_SEARCH_PATH": "x",
|
||||
}
|
||||
|
||||
with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:
|
||||
# TODO: postgrest should exit here, instead it keeps retrying
|
||||
# exitCode = wait_until_exit(postgrest)
|
||||
# assert exitCode == 1
|
||||
|
||||
output = postgrest.read_stdout(nlines=9)
|
||||
assert (
|
||||
"Failed to load the schema cache using db-schemas=public and db-extra-search-path=x"
|
||||
in output[7]
|
||||
)
|
||||
|
||||
|
||||
def test_log_listener_connection_errors(defaultenv):
|
||||
"The logs should show the listener connection error message in a single line"
|
||||
|
||||
env = {
|
||||
**defaultenv,
|
||||
"PGHOST": "no_host",
|
||||
"PGRST_DB_CHANNEL_ENABLED": "true",
|
||||
}
|
||||
|
||||
with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:
|
||||
output = postgrest.read_stdout(nlines=5)
|
||||
assert any(
|
||||
'Failed listening for database notifications on the "pgrst" channel. could not translate host name "no_host" to address:'
|
||||
in line
|
||||
for line in output
|
||||
)
|
||||
|
||||
@@ -1058,6 +1058,40 @@ spec = describe "OpenAPI" $ do
|
||||
}
|
||||
|]
|
||||
|
||||
it "only includes POST method for volatile functions" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
let volatileGet = r ^? key "paths" . key "/rpc/reset_table" . key "get"
|
||||
volatilePost = r ^? key "paths" . key "/rpc/reset_table" . key "post"
|
||||
|
||||
liftIO $ do
|
||||
volatileGet `shouldBe` Nothing
|
||||
volatilePost `shouldNotBe` Nothing
|
||||
|
||||
it "includes GET and POST methods for stable functions" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
let stableGet = r ^? key "paths" . key "/rpc/getallusers" . key "get"
|
||||
stablePost = r ^? key "paths" . key "/rpc/getallusers" . key "post"
|
||||
|
||||
liftIO $ do
|
||||
stableGet `shouldNotBe` Nothing
|
||||
stablePost `shouldNotBe` Nothing
|
||||
|
||||
it "includes GET and POST methods for immutable functions" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
let immutableGet = r ^? key "paths" . key "/rpc/jwt_test" . key "get"
|
||||
immutablePost = r ^? key "paths" . key "/rpc/jwt_test" . key "post"
|
||||
|
||||
liftIO $ do
|
||||
immutableGet `shouldNotBe` Nothing
|
||||
immutablePost `shouldNotBe` Nothing
|
||||
|
||||
it "does not include empty enum in the preferParams parameter" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
let preferParams = r ^? key "parameters" . key "preferParams" . key "enum"
|
||||
|
||||
liftIO $ do
|
||||
preferParams `shouldBe` Nothing
|
||||
|
||||
describe "Security" $
|
||||
it "does not include security or security definitions by default" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
|
||||
@@ -42,10 +42,10 @@ pgErrorCodeMapping = do
|
||||
it "works with SchemaCache error" $
|
||||
get "/non_existent_table"
|
||||
`shouldRespondWith`
|
||||
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.json_table'","message":"Could not find the table 'test.non_existent_table' in the schema cache"} |]
|
||||
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.collision_test_table'","message":"Could not find the table 'test.non_existent_table' in the schema cache"} |]
|
||||
{ matchStatus = 404
|
||||
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST205"
|
||||
, "Content-Length" <:> "172" ]
|
||||
, "Content-Length" <:> "182" ]
|
||||
}
|
||||
|
||||
it "works with Jwt error" $ do
|
||||
|
||||
@@ -72,9 +72,9 @@ spec =
|
||||
, matchHeaders = []
|
||||
}
|
||||
|
||||
it "fails trying to read table from unkown schema" $
|
||||
request methodGet "/parents" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
|
||||
[json|{"message":"The schema must be one of the following: v1, v2, SPECIAL \"@/\\#~_-","code":"PGRST106","details":null,"hint":null}|]
|
||||
it "fails trying to read table from unknown schema" $
|
||||
request methodGet "/parents" [("Accept-Profile", "unknown")] "" `shouldRespondWith`
|
||||
[json|{"message":"Invalid schema: unknown","code":"PGRST106","details":null,"hint":"Only the following schemas are exposed: v1, v2, SPECIAL \"@/\\#~_-"}|]
|
||||
{
|
||||
matchStatus = 406
|
||||
}
|
||||
@@ -151,7 +151,7 @@ spec =
|
||||
request methodPost "/children" [("Content-Profile", "unknown")]
|
||||
[json|{"name": "child 4", "parent_id": 4}|]
|
||||
`shouldRespondWith`
|
||||
[json|{"message":"The schema must be one of the following: v1, v2, SPECIAL \"@/\\#~_-","code":"PGRST106","details":null,"hint":null}|]
|
||||
[json|{"message":"Invalid schema: unknown","code":"PGRST106","details":null,"hint":"Only the following schemas are exposed: v1, v2, SPECIAL \"@/\\#~_-"}|]
|
||||
{
|
||||
matchStatus = 406
|
||||
}
|
||||
@@ -389,9 +389,9 @@ spec =
|
||||
let def = simpleBody r ^? key "definitions" . key "another_table"
|
||||
def `shouldBe` Nothing
|
||||
|
||||
it "fails trying to read definitions from unkown schema" $
|
||||
request methodGet "/" [("Accept-Profile", "unkown")] "" `shouldRespondWith`
|
||||
[json|{"message":"The schema must be one of the following: v1, v2, SPECIAL \"@/\\#~_-","code":"PGRST106","details":null,"hint":null}|]
|
||||
it "fails trying to read definitions from unknown schema" $
|
||||
request methodGet "/" [("Accept-Profile", "unknown")] "" `shouldRespondWith`
|
||||
[json|{"message":"Invalid schema: unknown","code":"PGRST106","details":null,"hint":"Only the following schemas are exposed: v1, v2, SPECIAL \"@/\\#~_-"}|]
|
||||
{
|
||||
matchStatus = 406
|
||||
}
|
||||
|
||||
@@ -191,3 +191,48 @@ spec =
|
||||
""
|
||||
{ matchStatus = 204
|
||||
, matchHeaders = ["Preference-Applied" <:> "handling=lenient"]}
|
||||
|
||||
context "test Prefer: max-affected with rpc" $ do
|
||||
it "should fail with rpc when deleting rows more than prefered with returns setof" $
|
||||
request methodPost "/rpc/delete_items_returns_setof"
|
||||
[("Prefer", "handling=strict, max-affected=10")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json| {"code":"PGRST124","details":"The query affects 15 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"} |]
|
||||
{ matchStatus = 400 }
|
||||
|
||||
it "should fail with rpc when deleting rows more than prefered with returns table" $
|
||||
request methodPost "/rpc/delete_items_returns_table"
|
||||
[("Prefer", "handling=strict, max-affected=10")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json| {"code":"PGRST124","details":"The query affects 15 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"} |]
|
||||
{ matchStatus = 400 }
|
||||
|
||||
it "should succeed with rpc deleting rows less than prefered with returns setof" $
|
||||
request methodPost "/rpc/delete_items_returns_setof"
|
||||
[("Prefer", "handling=strict, max-affected=20")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},
|
||||
{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},
|
||||
{"id":14},{"id":15}]|]
|
||||
{ matchStatus = 200 }
|
||||
|
||||
it "should succeed with rpc deleting rows less than prefered with returns table" $
|
||||
request methodPost "/rpc/delete_items_returns_table"
|
||||
[("Prefer", "handling=strict, max-affected=20")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},
|
||||
{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},
|
||||
{"id":14},{"id":15}]|]
|
||||
{ matchStatus = 200 }
|
||||
|
||||
it "should fail with rpc when returns void with handling=strict" $
|
||||
request methodPost "/rpc/delete_items_returns_void"
|
||||
[("Prefer", "handling=strict, max-affected=20")]
|
||||
""
|
||||
`shouldRespondWith`
|
||||
[json| {"code":"PGRST128","details":null,"hint":null,"message":"Function must return SETOF or TABLE when max-affected preference is used with handling=strict"} |]
|
||||
{ matchStatus = 400 }
|
||||
|
||||
@@ -294,6 +294,20 @@ spec = do
|
||||
]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when the column type is a tsvector domain" $ do
|
||||
get "tsearch_to_tsvector?select=text_search_domain&text_search_domain=fts(simple).of" `shouldRespondWith`
|
||||
[json| [
|
||||
{"text_search_domain":"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6"}
|
||||
]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when the column type is a recursive tsvector domain" $ do
|
||||
get "tsearch_to_tsvector?select=text_search_rec_domain&text_search_rec_domain=fts(simple).of" `shouldRespondWith`
|
||||
[json| [
|
||||
{"text_search_rec_domain":"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6"}
|
||||
]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "text and json columns" $ do
|
||||
it "finds matches with to_tsquery" $ do
|
||||
get "/tsearch_to_tsvector?select=text_search&text_search=fts.impossible" `shouldRespondWith`
|
||||
|
||||
@@ -223,6 +223,14 @@ spec = do
|
||||
, "Content-Range" <:> "2-4/*" ]
|
||||
}
|
||||
|
||||
it "works alongside order by with nulls order" $
|
||||
get "/clients?select=id,projects(id,tasks(id))&order=id.asc.nullslast&limit=1&projects.order=id.asc.nullsfirst&projects.limit=2"
|
||||
`shouldRespondWith`
|
||||
[json|[{"id":1,"projects":[{"id": 1, "tasks": [{"id": 1}, {"id": 2}]}, {"id": 2, "tasks": [{"id": 3}, {"id": 4}]}]}]|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||
}
|
||||
|
||||
context "succeeds if offset equals 0 as a no-op" $ do
|
||||
it "no items" $ do
|
||||
get "/items?offset=0&id=eq.0"
|
||||
|
||||
@@ -998,6 +998,22 @@ spec =
|
||||
|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "should work with filters that use the fts operator when the column type is a tsvector domain" $
|
||||
get "/rpc/get_tsearch_to_tsvector?select=text_search_domain&text_search_domain=fts(simple).impossible" `shouldRespondWith`
|
||||
[json|[
|
||||
{"text_search_domain":"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6"},
|
||||
{"text_search_domain":"'amusant':5 'c':1 'de':6 'est':2 'faire':7 'impossible':9 'l':8 'peu':4 'un':3"}]
|
||||
|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "should work with filters that use the fts operator when the column type is a recursive tsvector domain" $
|
||||
get "/rpc/get_tsearch_to_tsvector?select=text_search_rec_domain&text_search_rec_domain=fts(simple).impossible" `shouldRespondWith`
|
||||
[json|[
|
||||
{"text_search_rec_domain":"'do':7 'fun':5 'impossible':9 'it':1 'kind':3 'of':4 's':2 'the':8 'to':6"},
|
||||
{"text_search_rec_domain":"'amusant':5 'c':1 'de':6 'est':2 'faire':7 'impossible':9 'l':8 'peu':4 'un':3"}]
|
||||
|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "should work with the phraseto_tsquery function" $
|
||||
get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith`
|
||||
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
|
||||
@@ -1450,3 +1466,11 @@ spec =
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
context "schema cache duplicate definitions when two entries in pg_description have the same OID" $
|
||||
it "doesn't err with 300 Multiple Choices" $
|
||||
request methodGet "/rpc/collision_test_func?id=1"
|
||||
[] ""
|
||||
`shouldRespondWith`
|
||||
[json| 1 |]
|
||||
{ matchStatus = 200 }
|
||||
|
||||
@@ -600,3 +600,37 @@ spec =
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
context "empty spreads embeds" $
|
||||
it "should work return the same as empty embeddings" $ do
|
||||
get "/actors?select=*,...films()"
|
||||
`shouldRespondWith`
|
||||
[json| [{"id":1,"name":"john"}, {"id":2,"name":"mary"}] |]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
get "/grandchild_entities?select=name,...child_entities(parent_name:name,...entities())"
|
||||
`shouldRespondWith`
|
||||
[json|
|
||||
[{"name":"grandchild entity 1","parent_name":"child entity 1"},
|
||||
{"name":"grandchild entity 2","parent_name":"child entity 1"},
|
||||
{"name":"grandchild entity 3","parent_name":"child entity 2"},
|
||||
{"name":"(grandchild,entity,4)","parent_name":"child entity 2"},
|
||||
{"name":"(grandchild,entity,5)","parent_name":"child entity 2"}]
|
||||
|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
get "/factories?select=factory:name,...processes()"
|
||||
`shouldRespondWith`
|
||||
[json|
|
||||
[{"factory":"Factory A"},
|
||||
{"factory":"Factory B"},
|
||||
{"factory":"Factory C"},
|
||||
{"factory":"Factory D"}]
|
||||
|]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = [matchContentTypeJson]
|
||||
}
|
||||
|
||||
@@ -954,7 +954,8 @@ INSERT INTO tsearch_to_tsvector(text_search) VALUES ('C''est un peu amusant de f
|
||||
INSERT INTO tsearch_to_tsvector(text_search) VALUES ('Es ist eine Art Spaß, das Unmögliche zu machen');
|
||||
|
||||
UPDATE tsearch_to_tsvector SET jsonb_search = jsonb_build_object('text_search', text_search);
|
||||
|
||||
UPDATE tsearch_to_tsvector SET text_search_domain = to_tsvector('simple', text_search);
|
||||
UPDATE tsearch_to_tsvector SET text_search_rec_domain = to_tsvector('simple', text_search);
|
||||
|
||||
TRUNCATE TABLE artists CASCADE;
|
||||
INSERT INTO artists
|
||||
|
||||
@@ -3750,9 +3750,17 @@ create table surr_gen_default_upsert (
|
||||
extra text
|
||||
);
|
||||
|
||||
create domain tsvector_not_null as tsvector
|
||||
constraint "tsvector is required" check (value is not null);
|
||||
|
||||
create domain tsvector_not_empty as tsvector_not_null
|
||||
constraint "tsvector is required and not empty" check (value <> '');
|
||||
|
||||
create table tsearch_to_tsvector (
|
||||
text_search text,
|
||||
jsonb_search jsonb
|
||||
jsonb_search jsonb,
|
||||
text_search_domain tsvector_not_null default '',
|
||||
text_search_rec_domain tsvector_not_empty default '.'
|
||||
);
|
||||
|
||||
create function test.get_tsearch_to_tsvector() returns setof test.tsearch_to_tsvector AS $$
|
||||
@@ -3800,3 +3808,31 @@ create table factory_buildings (
|
||||
factory_id int references factories(id),
|
||||
inspections jsonb
|
||||
);
|
||||
|
||||
-- collision test as occured in https://github.com/PostgREST/postgrest/issues/4052
|
||||
create table test.collision_test_table (id integer);
|
||||
comment on table collision_test_table is 'foobarbaz';
|
||||
|
||||
create function test.collision_test_func(id integer)
|
||||
returns int language sql as $$
|
||||
select 1;
|
||||
$$;
|
||||
|
||||
update pg_proc
|
||||
set oid = 'test.collision_test_table'::regclass::oid
|
||||
where oid = 'test.collision_test_func'::regproc::oid;
|
||||
|
||||
comment on function test.collision_test_func(id integer) is 'fizzbuzz';
|
||||
|
||||
|
||||
create or replace function test.delete_items_returns_setof() returns setof items as $$
|
||||
delete from items where id <= 15 returning *; -- deletes 15 items, then return them
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.delete_items_returns_table() returns table(id bigint) as $$
|
||||
delete from items where id <= 15 returning *;
|
||||
$$ language sql;
|
||||
|
||||
create or replace function test.delete_items_returns_void() returns void as $$
|
||||
delete from items;
|
||||
$$ language sql;
|
||||
|
||||