Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9f017c632 | ||
|
|
132ecd8d74 | ||
|
|
19c3ee2143 | ||
|
|
4fb0d1279c | ||
|
|
81f9bf290c | ||
|
|
dbef11869c | ||
|
|
b38564e623 | ||
|
|
2a2a4bdeac | ||
|
|
a65ac9b0f6 | ||
|
|
7ed8b10776 | ||
|
|
2f239b9048 | ||
|
|
c3854e7c74 | ||
|
|
b7dcc63e37 | ||
|
|
62243852b6 | ||
|
|
1b48531369 | ||
|
|
1981160536 | ||
|
|
f3eb8a317d | ||
|
|
7aedebecee | ||
|
|
d5afdc7c53 | ||
|
|
b7c261f520 | ||
|
|
942391c508 | ||
|
|
f333305491 | ||
|
|
88283801cb | ||
|
|
4e07846c5d | ||
|
|
bf58a3600a | ||
|
|
bae5afbad6 | ||
|
|
91689e28f0 | ||
|
|
041f73cae8 | ||
|
|
43c8bde8ce | ||
|
|
82dd1b732d | ||
|
|
41ac0d5d93 | ||
|
|
60db026dcc | ||
|
|
47b6f04a66 | ||
|
|
f228cccbae | ||
|
|
3820590649 | ||
|
|
8cc8531e49 | ||
|
|
65062364c4 | ||
|
|
ebe66cb985 | ||
|
|
9c172db534 | ||
|
|
de531b6b41 | ||
|
|
71cb8afc0d | ||
|
|
36dbd09459 | ||
|
|
fa4df95f55 | ||
|
|
4104f5fb75 | ||
|
|
abb6167caa | ||
|
|
15ea7dfe09 | ||
|
|
a2c2544271 | ||
|
|
2b22f889bd | ||
|
|
cd3013569e | ||
|
|
5dc37fc8e8 | ||
|
|
038d84b62d | ||
|
|
40f9a6068a | ||
|
|
5cd7d35966 | ||
|
|
71262fbc5c | ||
|
|
d16a4a9a2b | ||
|
|
2357961f48 | ||
|
|
162c4e38af | ||
|
|
0f95900822 | ||
|
|
8ac14447ef | ||
|
|
a1b3bca7fc | ||
|
|
8cde90b7fe | ||
|
|
3b83f536ca | ||
|
|
42d7cf8ae5 | ||
|
|
627c3c34b7 | ||
|
|
ee56dd5db1 | ||
|
|
bf91187e63 | ||
|
|
2982c5de2e | ||
|
|
add95c32ca | ||
|
|
243e4f0408 | ||
|
|
885ff60138 | ||
|
|
72a28a145d | ||
|
|
4a594d5e95 | ||
|
|
0ba133a0bd | ||
|
|
b05898d17f | ||
|
|
d4c6abbaec | ||
|
|
caaa34b5de | ||
|
|
c9a60373f6 | ||
|
|
ed5072f4b1 | ||
|
|
ed98ac9d7e | ||
|
|
d9f1ae6a46 | ||
|
|
5874482f03 | ||
|
|
0511e99dd4 | ||
|
|
4e4548d702 | ||
|
|
b3899e7aa4 | ||
|
|
6d9fcfa09f | ||
|
|
38e5b76996 | ||
|
|
ca4539e78d | ||
|
|
a54a61d00e | ||
|
|
142fc06b15 | ||
|
|
c7b1cda3ed | ||
|
|
fb2adea5e9 | ||
|
|
d9b15adb35 |
@@ -1,276 +0,0 @@
|
|||||||
version: 2
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Make sure that there are no outstanding linting hints and that
|
|
||||||
# auto-formatting does not result in any changes.
|
|
||||||
style-check:
|
|
||||||
docker:
|
|
||||||
- image: nixos/nix:2.3
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run:
|
|
||||||
name: Install linting and styling scripts
|
|
||||||
command: nix-env -f default.nix -iA style
|
|
||||||
- run:
|
|
||||||
name: Run linter
|
|
||||||
command: |
|
|
||||||
# Note: For checking this locally, use `nix-shell --run postgrest-lint`
|
|
||||||
postgrest-lint
|
|
||||||
- run:
|
|
||||||
name: Run style check
|
|
||||||
command: |
|
|
||||||
# 'Note: For checking this locally, use `nix-shell --run postgrest-style`
|
|
||||||
postgrest-style-check
|
|
||||||
|
|
||||||
# Run tests based on stack and docker against the oldest PostgreSQL version
|
|
||||||
# that we support.
|
|
||||||
stack-test:
|
|
||||||
docker:
|
|
||||||
- image: cimg/base:2021.03
|
|
||||||
environment:
|
|
||||||
- PGHOST=localhost
|
|
||||||
- image: circleci/postgres:9.5
|
|
||||||
environment:
|
|
||||||
- POSTGRES_USER=circleci
|
|
||||||
- POSTGRES_DB=circleci
|
|
||||||
- POSTGRES_HOST_AUTH_METHOD=trust
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
keys:
|
|
||||||
- v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
|
||||||
- run:
|
|
||||||
name: install stack & dependencies
|
|
||||||
command: |
|
|
||||||
curl -L https://github.com/commercialhaskell/stack/releases/download/v2.3.1/stack-2.3.1-linux-x86_64.tar.gz | tar zx -C /tmp
|
|
||||||
sudo mv /tmp/stack-2.3.1-linux-x86_64/stack /usr/bin
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install -y libgmp-dev postgresql-client
|
|
||||||
sudo apt-get install -y --only-upgrade binutils
|
|
||||||
stack setup
|
|
||||||
- run:
|
|
||||||
name: build src and tests dependencies
|
|
||||||
command: |
|
|
||||||
stack build --fast -j1 --only-dependencies
|
|
||||||
stack build --fast --test --no-run-tests --only-dependencies
|
|
||||||
- save_cache:
|
|
||||||
paths:
|
|
||||||
- "~/.stack"
|
|
||||||
- ".stack-work"
|
|
||||||
key: v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
|
||||||
- run:
|
|
||||||
name: build src and tests
|
|
||||||
command: |
|
|
||||||
stack build --fast -j1
|
|
||||||
stack build --fast --test --no-run-tests
|
|
||||||
- run:
|
|
||||||
name: run spec tests
|
|
||||||
command: |
|
|
||||||
test/create_test_db "postgres://circleci@localhost" postgrest_test stack test
|
|
||||||
- store_artifacts:
|
|
||||||
path: /tmp/postgrest
|
|
||||||
|
|
||||||
# Publish a new release. This only runs when a release is tagged (see
|
|
||||||
# workflow below).
|
|
||||||
release:
|
|
||||||
machine: true
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run:
|
|
||||||
name: Install Nix
|
|
||||||
command: |
|
|
||||||
curl -L https://nixos.org/nix/install | sh
|
|
||||||
echo "source $HOME/.nix-profile/etc/profile.d/nix.sh" >> $BASH_ENV
|
|
||||||
- run:
|
|
||||||
name: Change postgrest.cabal if nightly
|
|
||||||
command: |
|
|
||||||
if test "$CIRCLE_TAG" = "nightly"
|
|
||||||
then
|
|
||||||
cabal_nightly_version=$(git show -s --format='%cd' --date='format:%Y%m%d')
|
|
||||||
sed -i "s/^version:.*/version:$cabal_nightly_version/" postgrest.cabal
|
|
||||||
fi
|
|
||||||
- run:
|
|
||||||
name: Install and use the Cachix binary cache
|
|
||||||
command: |
|
|
||||||
nix-env -iA cachix -f https://cachix.org/api/v1/install
|
|
||||||
cachix use postgrest
|
|
||||||
- run:
|
|
||||||
name: Install release scripts
|
|
||||||
command: nix-env -f default.nix -iA release
|
|
||||||
- run:
|
|
||||||
name: Publish GitHub release
|
|
||||||
command: |
|
|
||||||
export GITHUB_USERNAME="$CIRCLE_PROJECT_USERNAME"
|
|
||||||
export GITHUB_REPONAME="$CIRCLE_PROJECT_REPONAME"
|
|
||||||
postgrest-release-github $CIRCLE_TAG
|
|
||||||
- run:
|
|
||||||
name: Publish Docker images
|
|
||||||
command: |
|
|
||||||
export DOCKER_REPO=postgrest
|
|
||||||
postgrest-release-docker-login
|
|
||||||
postgrest-release-dockerhub $CIRCLE_TAG
|
|
||||||
if test "$CIRCLE_TAG" != "nightly"
|
|
||||||
then
|
|
||||||
postgrest-release-dockerhub-description
|
|
||||||
fi
|
|
||||||
- store_artifacts:
|
|
||||||
path: /tmp/postgrest
|
|
||||||
|
|
||||||
# Build everything in default.nix and push to the Cachix binary cache if running on main
|
|
||||||
nix-build:
|
|
||||||
machine: true
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run:
|
|
||||||
name: Install Nix
|
|
||||||
command: |
|
|
||||||
curl -L https://nixos.org/nix/install | sh
|
|
||||||
echo "source $HOME/.nix-profile/etc/profile.d/nix.sh" >> $BASH_ENV
|
|
||||||
- run:
|
|
||||||
name: Install and use the Cachix binary cache
|
|
||||||
command: |
|
|
||||||
nix-env -iA cachix -f https://cachix.org/api/v1/install
|
|
||||||
cachix use postgrest
|
|
||||||
- run:
|
|
||||||
name: Change postgrest.cabal if nightly
|
|
||||||
command: |
|
|
||||||
if test "$CIRCLE_TAG" = "nightly"
|
|
||||||
then
|
|
||||||
cabal_nightly_version=$(git show -s --format='%cd' --date='format:%Y%m%d')
|
|
||||||
sed -i "s/^version:.*/version:$cabal_nightly_version/" postgrest.cabal
|
|
||||||
fi
|
|
||||||
- run:
|
|
||||||
name: Build all derivations from default.nix and push results to Cachix
|
|
||||||
command: |
|
|
||||||
# Only push to the cache when CircleCI makes the CACHIX_SIGNING_KEY
|
|
||||||
# available (e.g. not for pull requests).
|
|
||||||
if [ -n "${CACHIX_AUTH_TOKEN:-""}" ]; then
|
|
||||||
echo "Building and caching all derivations..."
|
|
||||||
cachix authtoken "$CACHIX_AUTH_TOKEN"
|
|
||||||
|
|
||||||
# Push new builds as we go
|
|
||||||
nix-build | cachix push postgrest
|
|
||||||
|
|
||||||
# Make sure that everything, including .drv files, is pushed
|
|
||||||
nix-env -f default.nix -iA devTools
|
|
||||||
postgrest-push-cachix
|
|
||||||
else
|
|
||||||
echo "Building all derivations (caching skipped for outside pull requests)..."
|
|
||||||
nix-build
|
|
||||||
fi
|
|
||||||
- store_artifacts:
|
|
||||||
path: /tmp/postgrest
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
nix-test:
|
|
||||||
machine: true
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- run:
|
|
||||||
name: Install Nix
|
|
||||||
command: |
|
|
||||||
curl -L https://nixos.org/nix/install | sh
|
|
||||||
echo "source $HOME/.nix-profile/etc/profile.d/nix.sh" >> $BASH_ENV
|
|
||||||
- run:
|
|
||||||
name: Install and use the Cachix binary cache
|
|
||||||
command: |
|
|
||||||
nix-env -iA cachix -f https://cachix.org/api/v1/install
|
|
||||||
cachix use postgrest
|
|
||||||
- run:
|
|
||||||
name: Install testing scripts
|
|
||||||
command: nix-env -f default.nix -iA tests memory withTools
|
|
||||||
- run:
|
|
||||||
name: Run coverage (io tests and spec tests against PostgreSQL 13)
|
|
||||||
command: postgrest-coverage
|
|
||||||
when: always
|
|
||||||
- run:
|
|
||||||
name: Skip tests on build or primary test failure
|
|
||||||
command: circleci-agent step halt
|
|
||||||
when: on_fail
|
|
||||||
- run:
|
|
||||||
name: Upload coverage to codecov
|
|
||||||
command: |
|
|
||||||
# Modified from:
|
|
||||||
# https://docs.codecov.io/docs/about-the-codecov-bash-uploader#validating-the-bash-script
|
|
||||||
curl -s https://codecov.io/bash > codecov;
|
|
||||||
VERSION=$(grep 'VERSION=\".*\"' codecov | cut -d'"' -f2);
|
|
||||||
shasum -a 512 -c <(curl -s https://raw.githubusercontent.com/codecov/codecov-bash/${VERSION}/SHA512SUM | grep codecov)
|
|
||||||
bash codecov -f coverage/codecov.json
|
|
||||||
- run:
|
|
||||||
name: Run the spec tests against PostgreSQL 12
|
|
||||||
command: postgrest-with-postgresql-12 postgrest-test-spec
|
|
||||||
when: always
|
|
||||||
- run:
|
|
||||||
name: Run the spec tests against PostgreSQL 11
|
|
||||||
command: postgrest-with-postgresql-11 postgrest-test-spec
|
|
||||||
when: always
|
|
||||||
- run:
|
|
||||||
name: Run the spec tests against PostgreSQL 10
|
|
||||||
command: postgrest-with-postgresql-10 postgrest-test-spec
|
|
||||||
when: always
|
|
||||||
- run:
|
|
||||||
name: Run the spec tests against PostgreSQL 9.6
|
|
||||||
command: postgrest-with-postgresql-9.6 postgrest-test-spec
|
|
||||||
when: always
|
|
||||||
- run:
|
|
||||||
name: Run the spec tests against PostgreSQL 9.5
|
|
||||||
command: postgrest-with-postgresql-9.5 postgrest-test-spec
|
|
||||||
when: always
|
|
||||||
- run:
|
|
||||||
name: Check the spec tests for idempotence
|
|
||||||
command: postgrest-test-spec-idempotence
|
|
||||||
when: always
|
|
||||||
- run:
|
|
||||||
name: Run memory tests
|
|
||||||
command: postgrest-test-memory
|
|
||||||
when: always
|
|
||||||
- store_artifacts:
|
|
||||||
path: /tmp/postgrest
|
|
||||||
|
|
||||||
workflows:
|
|
||||||
version: 2
|
|
||||||
build-test-release:
|
|
||||||
jobs:
|
|
||||||
- style-check:
|
|
||||||
# Make sure that this job also runs when releases are tagged.
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only:
|
|
||||||
- /v[0-9]+(\.[0-9]+)*/
|
|
||||||
- nightly
|
|
||||||
- stack-test:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only:
|
|
||||||
- /v[0-9]+(\.[0-9]+)*/
|
|
||||||
- nightly
|
|
||||||
- nix-build:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only:
|
|
||||||
- /v[0-9]+(\.[0-9]+)*/
|
|
||||||
- nightly
|
|
||||||
context:
|
|
||||||
- cachix
|
|
||||||
- nix-test:
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only:
|
|
||||||
- /v[0-9]+(\.[0-9]+)*/
|
|
||||||
- nightly
|
|
||||||
- release:
|
|
||||||
requires:
|
|
||||||
- style-check
|
|
||||||
- stack-test
|
|
||||||
- nix-build
|
|
||||||
- nix-test
|
|
||||||
filters:
|
|
||||||
tags:
|
|
||||||
only:
|
|
||||||
- /v[0-9]+(\.[0-9]+)*/
|
|
||||||
- nightly
|
|
||||||
branches:
|
|
||||||
ignore: /.*/
|
|
||||||
context:
|
|
||||||
- docker
|
|
||||||
- github
|
|
||||||
+9
-65
@@ -1,71 +1,15 @@
|
|||||||
freebsd_instance:
|
freebsd_instance:
|
||||||
image: freebsd-12-2-release-amd64
|
image_family: freebsd-13-0
|
||||||
|
|
||||||
build_task:
|
build_task:
|
||||||
env:
|
name: Build FreeBSD (Stack)
|
||||||
GITHUB_TOKEN: ENCRYPTED[!1ecc3020fe8c6463c06ebc22153533239e132ee56e4faad95ce336bd2ee2bde6aa89c0352e89faaa2c10f4a5bac9b7fc!]
|
install_script: pkg install -y postgresql13-client hs-stack
|
||||||
# caches the freebsd package downloads
|
|
||||||
# saves probably just a couple of seconds, but hey...
|
|
||||||
pkg_cache:
|
|
||||||
folder: /var/cache/pkg
|
|
||||||
|
|
||||||
install_script:
|
stack_cache:
|
||||||
# - pkg update
|
folders: /.stack
|
||||||
- pkg install -y postgresql12-client ghc hs-cabal-install jq git
|
fingerprint_script: cat postgrest.cabal stack.yaml.lock
|
||||||
|
|
||||||
# cache the hackage index file and downloads which are
|
|
||||||
# cabal v2-update downloads an incremental update, so we don't need to keep this up2date
|
|
||||||
packages_cache:
|
|
||||||
# warning: don't use ~/.cabal here, this will break the cache
|
|
||||||
folder: /.cabal/packages
|
|
||||||
reupload_on_changes: false
|
reupload_on_changes: false
|
||||||
|
|
||||||
# cache the dependencies built by cabal
|
build_script: stack build -j 1 --local-bin-path . --copy-bins
|
||||||
# they have to be uploaded on every change to make the next build fast
|
bin_artifacts:
|
||||||
store_cache:
|
path: postgrest
|
||||||
# warning: don't use ~/.cabal here, this will break the cache
|
|
||||||
folder: /.cabal/store
|
|
||||||
fingerprint_script: cat postgrest.cabal
|
|
||||||
reupload_on_changes: true
|
|
||||||
|
|
||||||
build_script:
|
|
||||||
- cabal v2-update
|
|
||||||
- |
|
|
||||||
if test "$CIRRUS_TAG" = "nightly"
|
|
||||||
then
|
|
||||||
cabal_nightly_version=$(git show -s --format='%cd' --date='format:%Y%m%d')
|
|
||||||
sed -i '' "s/^version:.*/version:$cabal_nightly_version/" postgrest.cabal
|
|
||||||
fi
|
|
||||||
## compile for 30 minutes tops
|
|
||||||
- timeout 1800 cabal v2-build -j1 || test "$?" = "124"
|
|
||||||
|
|
||||||
publish_script:
|
|
||||||
- |
|
|
||||||
if test ! "$CIRRUS_TAG"
|
|
||||||
then
|
|
||||||
echo 'No tag pushed. Skip release.'
|
|
||||||
else
|
|
||||||
cabal v2-install
|
|
||||||
|
|
||||||
bin_name=""
|
|
||||||
|
|
||||||
if test $CIRRUS_TAG = "nightly"
|
|
||||||
then
|
|
||||||
suffix=$(git show -s --format="%cd-%h" --date="format:%Y-%m-%d-%H-%M")
|
|
||||||
bin_name=postgrest-nightly-$suffix-freebsd.tar.xz
|
|
||||||
else
|
|
||||||
bin_name=postgrest-$CIRRUS_TAG-freebsd.tar.xz
|
|
||||||
fi
|
|
||||||
|
|
||||||
release_id=$(curl -s https://api.github.com/repos/$CIRRUS_REPO_FULL_NAME/releases/tags/$CIRRUS_TAG | jq .id)
|
|
||||||
|
|
||||||
echo "Uploading $bin_name to gh release: $release_id"
|
|
||||||
|
|
||||||
tar cvJf $bin_name --dereference -C /.cabal/bin postgrest
|
|
||||||
|
|
||||||
## We don't use ghr here because it doesn't provide freebsd binaries: https://github.com/tcnksm/ghr/issues/127
|
|
||||||
curl -X POST --data-binary @$bin_name \
|
|
||||||
-H "Authorization:token $GITHUB_TOKEN" \
|
|
||||||
-H "Content-Type:application/octet-stream" \
|
|
||||||
"https://uploads.github.com/repos/$CIRRUS_REPO_FULL_NAME/releases/$release_id/assets?name=$bin_name"
|
|
||||||
fi
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: Setup Nix
|
||||||
|
|
||||||
|
description: Installs nix, sets up cachix and installs a subset of tooling.
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
authToken:
|
||||||
|
description: Token to pass to cachix
|
||||||
|
tools:
|
||||||
|
description: Tools to install with nix-env -iA <tools>
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- uses: cachix/install-nix-action@v16
|
||||||
|
- uses: cachix/cachix-action@v10
|
||||||
|
with:
|
||||||
|
name: postgrest
|
||||||
|
authToken: ${{ inputs.authToken }}
|
||||||
|
- if: ${{ inputs.tools }}
|
||||||
|
run: nix-env -f default.nix -iA ${{ inputs.tools }}
|
||||||
|
shell: bash
|
||||||
@@ -9,10 +9,10 @@ coverage:
|
|||||||
project:
|
project:
|
||||||
default:
|
default:
|
||||||
target: auto
|
target: auto
|
||||||
threshold: 0%
|
threshold: 1%
|
||||||
only_pulls: false
|
only_pulls: false
|
||||||
patch:
|
patch:
|
||||||
default:
|
default:
|
||||||
target: auto
|
target: auto
|
||||||
threshold: 0%
|
threshold: 1%
|
||||||
only_pulls: true
|
only_pulls: true
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Get the FreeBSD PostgREST executable built by CirrusCI for the given GITHUB_COMMIT in GITHUB_REPOSITORY
|
||||||
|
|
||||||
|
# We use the GitHub API for 'check suites' to find the corresponding CirrusCI job, see:
|
||||||
|
# https://docs.github.com/en/rest/reference/checks#list-check-suites-for-a-git-reference
|
||||||
|
|
||||||
|
cirrus_artifact_name=bin
|
||||||
|
gh_accept_header="Accept: application/vnd.github.v3+json"
|
||||||
|
|
||||||
|
get_gh_check_runs_url() {
|
||||||
|
gh_checks_list_url="https://api.github.com/repos/$GITHUB_REPOSITORY/commits/$GITHUB_COMMIT/check-suites"
|
||||||
|
>&2 echo "Getting list of check-suites from $gh_checks_list_url ..."
|
||||||
|
curl --fail -H "$gh_accept_header" "$gh_checks_list_url" \
|
||||||
|
| jq -r '.check_suites[] | select(.app.slug == "cirrus-ci") | .check_runs_url'
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_cirrusci() {
|
||||||
|
gh_check_runs_url="$(get_gh_check_runs_url)"
|
||||||
|
>&2 echo "Waiting to CirrusCI run to complete (two hours maximum)..."
|
||||||
|
for _ in $(seq 1 120); do
|
||||||
|
echo "Checking for CirrusCI task status at $gh_check_runs_url ..."
|
||||||
|
status=$(curl --fail "$gh_check_runs_url" | jq -r '.check_runs[] | .status')
|
||||||
|
if [ "$status" == "completed" ]; then
|
||||||
|
break
|
||||||
|
else
|
||||||
|
echo "CirrusCI task is still $status, waiting..."
|
||||||
|
sleep 60
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# The CirrusCI taskid can change if a new check run is started for the same commit,
|
||||||
|
# e.g. when pushing both a branch and tag. We make sure that we have the very
|
||||||
|
# latest taskid by re-loading the 'gh_check_runs_url' and the 'check run' itself.
|
||||||
|
get_cirrus_taskid() {
|
||||||
|
gh_check_runs_url="$(get_gh_check_runs_url)"
|
||||||
|
>&2 echo "Getting the CirrusCI task id from $gh_check_runs_url ..."
|
||||||
|
curl --fail -H "$gh_accept_header" "$gh_check_runs_url" \
|
||||||
|
| jq -r '.check_runs[] | .external_id'
|
||||||
|
}
|
||||||
|
|
||||||
|
download_artifact() {
|
||||||
|
cirrus_task_id="$(get_cirrus_taskid)"
|
||||||
|
cirrus_artifact_url="https://api.cirrus-ci.com/v1/artifact/task/$cirrus_task_id/$cirrus_artifact_name.zip"
|
||||||
|
>&2 echo "Attemping to download the CirrusCI artifact from $cirrus_artifact_url ..."
|
||||||
|
curl --fail "$cirrus_artifact_url" -o freebsd.zip
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_cirrusci
|
||||||
|
download_artifact
|
||||||
|
|
||||||
|
echo "Unpacking executable..."
|
||||||
|
unzip freebsd.zip -d .
|
||||||
|
rm -rf freebsd.zip
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Tag a release that will be built with Github Actions. The version is
|
||||||
|
# read from 'postgrest.cabal'.
|
||||||
|
|
||||||
|
version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||||
|
|
||||||
|
echo "Tagging version v$version"
|
||||||
|
git tag -f "v$version"
|
||||||
|
|
||||||
|
echo "Pushing tag..."
|
||||||
|
git push -f origin "refs/tags/v$version"
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Lint-Style:
|
||||||
|
name: Lint & check code style
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2.4.0
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
with:
|
||||||
|
tools: style
|
||||||
|
- name: Run linter (check locally with `nix-shell --run postgrest-lint`)
|
||||||
|
run: postgrest-lint
|
||||||
|
- name: Run style check (auto-format with `nix-shell --run postgrest-style`)
|
||||||
|
run: postgrest-style-check
|
||||||
|
|
||||||
|
|
||||||
|
Test-Nix:
|
||||||
|
name: Test (Nix)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
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@v2.4.0
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
with:
|
||||||
|
tools: tests withTools
|
||||||
|
|
||||||
|
- name: Run coverage (IO tests and Spec tests against PostgreSQL 14)
|
||||||
|
run: postgrest-coverage
|
||||||
|
- name: Upload coverage to codecov
|
||||||
|
uses: codecov/codecov-action@v2.1.0
|
||||||
|
with:
|
||||||
|
files: ./coverage/codecov.json
|
||||||
|
|
||||||
|
- name: Run the spec tests against PostgreSQL 13
|
||||||
|
if: always()
|
||||||
|
run: postgrest-with-postgresql-13 postgrest-test-spec
|
||||||
|
- name: Run the spec tests against PostgreSQL 12
|
||||||
|
if: always()
|
||||||
|
run: postgrest-with-postgresql-12 postgrest-test-spec
|
||||||
|
- name: Run the spec tests against PostgreSQL 11
|
||||||
|
if: always()
|
||||||
|
run: postgrest-with-postgresql-11 postgrest-test-spec
|
||||||
|
- name: Run the spec tests against PostgreSQL 10
|
||||||
|
if: always()
|
||||||
|
run: postgrest-with-postgresql-10 postgrest-test-spec
|
||||||
|
- name: Run the spec tests against PostgreSQL 9.6
|
||||||
|
if: always()
|
||||||
|
run: postgrest-with-postgresql-9.6 postgrest-test-spec
|
||||||
|
|
||||||
|
- name: Run query cost tests against all PostgreSQL versions
|
||||||
|
if: always()
|
||||||
|
run: postgrest-with-all postgrest-test-querycost
|
||||||
|
|
||||||
|
- name: Run doctests
|
||||||
|
if: always()
|
||||||
|
run: nix-shell --run postgrest-test-doctests
|
||||||
|
|
||||||
|
- name: Check the spec tests for idempotence
|
||||||
|
if: always()
|
||||||
|
run: postgrest-test-spec-idempotence
|
||||||
|
|
||||||
|
|
||||||
|
Test-Memory-Nix:
|
||||||
|
name: Test memory (Nix)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2.4.0
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
with:
|
||||||
|
tools: memory
|
||||||
|
- name: Run memory tests
|
||||||
|
run: postgrest-test-memory
|
||||||
|
|
||||||
|
|
||||||
|
Build-Nix:
|
||||||
|
name: Build Linux static (Nix)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2.4.0
|
||||||
|
- 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@v2.2.4
|
||||||
|
with:
|
||||||
|
name: postgrest-linux-static-x64
|
||||||
|
path: result/bin/postgrest
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
|
||||||
|
- name: Save built Docker image as artifact
|
||||||
|
uses: actions/upload-artifact@v2.2.4
|
||||||
|
with:
|
||||||
|
name: postgrest-docker-x64
|
||||||
|
path: postgrest-docker.tar.gz
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Build and push everything to Cachix (main branch only)
|
||||||
|
if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
run: |
|
||||||
|
nix-build
|
||||||
|
nix-env -f default.nix -iA devTools
|
||||||
|
postgrest-push-cachix
|
||||||
|
|
||||||
|
|
||||||
|
Build-Stack:
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- name: Linux & test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
cache: |
|
||||||
|
~/.stack
|
||||||
|
.stack-work
|
||||||
|
test: true
|
||||||
|
pgdir: /usr/lib/postgresql
|
||||||
|
artifact: postgrest-ubuntu-x64
|
||||||
|
|
||||||
|
- name: MacOS & test
|
||||||
|
runs-on: macos-latest
|
||||||
|
cache: |
|
||||||
|
~/.stack
|
||||||
|
.stack-work
|
||||||
|
test: true
|
||||||
|
pgdir: /usr/local/Cellar/postgresql
|
||||||
|
artifact: postgrest-macos-x64
|
||||||
|
|
||||||
|
- name: Windows
|
||||||
|
runs-on: windows-latest
|
||||||
|
cache: |
|
||||||
|
~\AppData\Roaming\stack
|
||||||
|
~\AppData\Local\Programs\stack
|
||||||
|
.stack-work
|
||||||
|
deps: |
|
||||||
|
stack exec -- pacman -S mingw64/mingw-w64-x86_64-postgresql --noconfirm
|
||||||
|
# We'd need to make test/with_tmp_db run on Windows first
|
||||||
|
# test: true
|
||||||
|
artifact: postgrest-windows-x64
|
||||||
|
|
||||||
|
name: Build ${{ matrix.name }} (Stack)
|
||||||
|
runs-on: ${{ matrix.runs-on }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2.4.0
|
||||||
|
- name: Stack working files cache
|
||||||
|
uses: actions/cache@v2.1.7
|
||||||
|
with:
|
||||||
|
path: ${{ matrix.cache }}
|
||||||
|
key: ${{ runner.os }}-${{ hashFiles('stack.yaml.lock') }}
|
||||||
|
- name: Install dependencies
|
||||||
|
if: ${{ matrix.deps }}
|
||||||
|
run: ${{ matrix.deps }}
|
||||||
|
- name: Build with Stack
|
||||||
|
run: stack build --local-bin-path result --copy-bins
|
||||||
|
- name: Run Spec tests with Stack
|
||||||
|
if: ${{ matrix.test }}
|
||||||
|
run: |
|
||||||
|
postgresql_bin="$(find ${{ matrix.pgdir }} -maxdepth 2 -type d -name bin | head -n 1)"
|
||||||
|
echo "Using PostgreSQL binaries at $postgresql_bin ..."
|
||||||
|
PATH="$postgresql_bin:$PATH" test/with_tmp_db stack test
|
||||||
|
- name: Save built executable as artifact
|
||||||
|
uses: actions/upload-artifact@v2.2.4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.artifact }}
|
||||||
|
path: |
|
||||||
|
result/postgrest
|
||||||
|
result/postgrest.exe
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
|
Get-FreeBSD-CirrusCI:
|
||||||
|
name: Get FreeBSD build from CirrusCI
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2.4.0
|
||||||
|
- name: Get FreeBSD executable from CirrusCI
|
||||||
|
env:
|
||||||
|
# GITHUB_SHA does weird things for pull request, so we roll our own:
|
||||||
|
GITHUB_COMMIT: ${{github.event.pull_request.head.sha || github.sha}}
|
||||||
|
run: .github/get_cirrusci_freebsd
|
||||||
|
- name: Save executable as artifact
|
||||||
|
uses: actions/upload-artifact@v2.2.4
|
||||||
|
with:
|
||||||
|
name: postgrest-freebsd-x64
|
||||||
|
path: postgrest
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
|
Prepare-Release:
|
||||||
|
name: Prepare release
|
||||||
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs:
|
||||||
|
- Lint-Style
|
||||||
|
- Test-Nix
|
||||||
|
- Test-Memory-Nix
|
||||||
|
- Build-Nix
|
||||||
|
- Build-Stack
|
||||||
|
- Get-FreeBSD-CirrusCI
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.Identify-Version.outputs.version }}
|
||||||
|
isprerelease: ${{ steps.Identify-Version.outputs.isprerelease }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2.4.0
|
||||||
|
- id: Identify-Version
|
||||||
|
name: Identify the version to be released
|
||||||
|
run: |
|
||||||
|
tag_version="${GITHUB_REF##*/}"
|
||||||
|
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
|
||||||
|
|
||||||
|
if [ "$tag_version" != "v$cabal_version" ]; then
|
||||||
|
echo "Tagged version ($tag_version) does not match the one in postgrest.cabal (v$cabal_version). Aborting release..."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Version to be released is $cabal_version"
|
||||||
|
echo "::set-output name=version::$cabal_version"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$cabal_version" != *.*.*.* ]]; then
|
||||||
|
echo "Version is for a full release (version does not have four components)"
|
||||||
|
else
|
||||||
|
echo "Version is for a pre-release (version has four components, e.g., 1.1.1.1)"
|
||||||
|
echo "::set-output name=isprerelease::1"
|
||||||
|
fi
|
||||||
|
- name: Identify changes from CHANGELOG.md
|
||||||
|
run: |
|
||||||
|
version="${{ steps.Identify-Version.outputs.version }}"
|
||||||
|
isprerelease="${{ steps.Identify-Version.outputs.isprerelease }}"
|
||||||
|
|
||||||
|
if [ -n "$isprerelease" ]; then
|
||||||
|
echo "Getting unreleased changes..."
|
||||||
|
sed -n "1,/## Unreleased/d;/## \[/q;p" CHANGELOG.md > CHANGES.md
|
||||||
|
else
|
||||||
|
echo "Full release (no '-' in version), 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@v2.2.4
|
||||||
|
with:
|
||||||
|
name: release-changes
|
||||||
|
path: CHANGES.md
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
|
Release-GitHub:
|
||||||
|
name: Release on GitHub
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: Prepare-Release
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.Prepare-Release.outputs.version }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2.4.0
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v2.0.10
|
||||||
|
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-v$VERSION-linux-static-x64.tar.xz" \
|
||||||
|
-C artifacts/postgrest-linux-static-x64 postgrest
|
||||||
|
|
||||||
|
# No need to release Ubuntu, as the static Linux binary built with Nix
|
||||||
|
# covers all Linux use-cases
|
||||||
|
#tar cfJv "release-bundle/postgrest-v$VERSION-ubuntu-x64.tar.xz" \
|
||||||
|
# -C artifacts/postgrest-ubuntu-x64 postgrest
|
||||||
|
|
||||||
|
tar cJvf "release-bundle/postgrest-v$VERSION-macos-x64.tar.xz" \
|
||||||
|
-C artifacts/postgrest-macos-x64 postgrest
|
||||||
|
|
||||||
|
tar cJvf "release-bundle/postgrest-v$VERSION-freebsd-x64.tar.xz" \
|
||||||
|
-C artifacts/postgrest-freebsd-x64 postgrest
|
||||||
|
|
||||||
|
zip "release-bundle/postgrest-v$VERSION-windows-x64.zip" \
|
||||||
|
artifacts/postgrest-windows-x64/postgrest.exe
|
||||||
|
|
||||||
|
- name: Save release bundle
|
||||||
|
uses: actions/upload-artifact@v2.2.4
|
||||||
|
with:
|
||||||
|
name: release-bundle
|
||||||
|
path: release-bundle
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Publish release on GitHub
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
isprerelease="${{ needs.Prepare-Release.outputs.isprerelease }}"
|
||||||
|
echo "Releasing version v$VERSION on GitHub (isprerelease=$isprerelease)..."
|
||||||
|
|
||||||
|
gh release delete "v$VERSION" || true
|
||||||
|
gh release create "v$VERSION" \
|
||||||
|
-F artifacts/release-changes/CHANGES.md \
|
||||||
|
${isprerelease:+"--prerelease"} \
|
||||||
|
release-bundle/*
|
||||||
|
|
||||||
|
|
||||||
|
Release-Docker:
|
||||||
|
name: Release on Docker Hub
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: Prepare-Release
|
||||||
|
env:
|
||||||
|
DOCKER_REPO: postgrest
|
||||||
|
DOCKER_USER: stevechavez
|
||||||
|
VERSION: ${{ needs.Prepare-Release.outputs.version }}
|
||||||
|
ISPRERELEASE: ${{ needs.Prepare-Release.outputs.isprerelease }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2.4.0
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
with:
|
||||||
|
tools: release
|
||||||
|
- name: Download Docker image
|
||||||
|
uses: actions/download-artifact@v2.0.10
|
||||||
|
with:
|
||||||
|
name: postgrest-docker-x64
|
||||||
|
- name: Publish images on Docker Hub
|
||||||
|
run: |
|
||||||
|
docker login -u "$DOCKER_USER" -p "${{ secrets.DOCKER_PASS }}"
|
||||||
|
docker load -i postgrest-docker.tar.gz
|
||||||
|
|
||||||
|
docker tag postgrest:latest "$DOCKER_REPO/postgrest:v$VERSION"
|
||||||
|
docker push "$DOCKER_REPO/postgrest:v$VERSION"
|
||||||
|
|
||||||
|
# Only tag 'latest' for full releases
|
||||||
|
if [[ -z "$ISPRERELEASE" ]]; then
|
||||||
|
echo "Pushing to 'latest' tag for full release of v$VERSION ..."
|
||||||
|
docker tag postgrest:latest "$DOCKER_REPO"/postgrest:latest
|
||||||
|
docker push "$DOCKER_REPO"/postgrest:latest
|
||||||
|
else
|
||||||
|
echo "Skipping pushing to 'latest' tag for v$VERSION pre-release..."
|
||||||
|
fi
|
||||||
|
- name: Update descriptions on Docker Hub
|
||||||
|
env:
|
||||||
|
DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
|
||||||
|
run: |
|
||||||
|
if [[ -z "$ISPRERELEASE" ]]; then
|
||||||
|
echo "Updating description on Docker Hub..."
|
||||||
|
postgrest-release-dockerhub-description
|
||||||
|
else
|
||||||
|
echo "Skipping updating description for pre-release..."
|
||||||
|
fi
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: Upload Reports
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Loadtest"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
upload:
|
||||||
|
name: Loadtest
|
||||||
|
permissions:
|
||||||
|
checks: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
steps:
|
||||||
|
- name: Download from Artifacts
|
||||||
|
uses: dawidd6/action-download-artifact@v2
|
||||||
|
with:
|
||||||
|
workflow: ${{ github.event.workflow.name }}
|
||||||
|
run_id: ${{github.event.workflow_run.id }}
|
||||||
|
name: loadtest.md
|
||||||
|
path: artifacts
|
||||||
|
- name: Upload to GitHub Checks
|
||||||
|
uses: LouisBrunner/checks-action@v1.2.0
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
sha: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
name: Loadtest Results
|
||||||
|
conclusion: neutral
|
||||||
|
output: |
|
||||||
|
{"summary":""}
|
||||||
|
output_text_description_file: artifacts/loadtest.md
|
||||||
-80
@@ -1,80 +0,0 @@
|
|||||||
language: generic
|
|
||||||
|
|
||||||
sudo: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
include:
|
|
||||||
- name: Build OSX Binary
|
|
||||||
os: osx
|
|
||||||
cache:
|
|
||||||
timeout: 1000
|
|
||||||
directories:
|
|
||||||
- $HOME/.stack
|
|
||||||
- $HOME/.local/bin
|
|
||||||
before_install:
|
|
||||||
- mkdir -p "$HOME/.local/bin"
|
|
||||||
- export PATH="$PATH:$HOME/.local/bin"
|
|
||||||
install:
|
|
||||||
- |
|
|
||||||
if test -f "$HOME/.local/bin/stack"
|
|
||||||
then
|
|
||||||
echo 'Stack is already installed.'
|
|
||||||
else
|
|
||||||
echo "Installing Stack..."
|
|
||||||
travis_retry curl -L https://www.stackage.org/stack/osx-x86_64 > stack.tar.gz
|
|
||||||
gunzip stack.tar.gz
|
|
||||||
tar -x -f stack.tar --strip-components 1
|
|
||||||
mv stack "$HOME/.local/bin/"
|
|
||||||
rm stack.tar
|
|
||||||
fi
|
|
||||||
- |
|
|
||||||
if test -f "$HOME/.local/bin/ghr"
|
|
||||||
then
|
|
||||||
echo 'ghr is already installed.'
|
|
||||||
else
|
|
||||||
echo "Installing ghr..."
|
|
||||||
travis_retry curl -L https://github.com/tcnksm/ghr/releases/download/v0.5.4/ghr_v0.5.4_darwin_386.zip > ghr.zip
|
|
||||||
unzip ghr.zip -d "$HOME/.local/bin"
|
|
||||||
rm ghr.zip
|
|
||||||
fi
|
|
||||||
script:
|
|
||||||
- |
|
|
||||||
if test "$TRAVIS_TAG" = "nightly"
|
|
||||||
then
|
|
||||||
cabal_nightly_version=$(git show -s --format='%cd' --date='format:%Y%m%d')
|
|
||||||
sed -i '' "s/^version:.*/version:$cabal_nightly_version/" postgrest.cabal
|
|
||||||
fi
|
|
||||||
## Building the whole project can take longer than 50 minutes. Since Travis has a global timeout of 50 minutes
|
|
||||||
## we compile for 30 minutes tops(`gtimeout 1800`) and quit compiling with no error.
|
|
||||||
## Since we CACHE the compile results we can continue compiling from where we left off
|
|
||||||
## on the next commit.
|
|
||||||
- gtimeout 1800 stack build --no-terminal --only-snapshot --install-ghc || (($?==124))
|
|
||||||
- |
|
|
||||||
if test ! "$TRAVIS_TAG"
|
|
||||||
then
|
|
||||||
echo 'No tag pushed. Skip building binary.'
|
|
||||||
else
|
|
||||||
stack build --no-terminal --copy-bins --local-bin-path .
|
|
||||||
fi
|
|
||||||
- |
|
|
||||||
if test ! "$TRAVIS_TAG"
|
|
||||||
then
|
|
||||||
echo 'No tag pushed. Skipping release.'
|
|
||||||
else
|
|
||||||
owner="$(echo "$TRAVIS_REPO_SLUG" | cut -f1 -d/)"
|
|
||||||
repo="$(echo "$TRAVIS_REPO_SLUG" | cut -f2 -d/)"
|
|
||||||
if test $TRAVIS_TAG = "nightly"
|
|
||||||
then
|
|
||||||
suffix=$(git show -s --format="%cd-%h" --date="format:%Y-%m-%d-%H-%M")
|
|
||||||
strip postgrest
|
|
||||||
tar cJf postgrest-nightly-$suffix-osx.tar.xz postgrest
|
|
||||||
ghr -t $GITHUB_TOKEN -u $owner -r $repo --replace nightly postgrest-nightly-$suffix-osx.tar.xz
|
|
||||||
else
|
|
||||||
start=$TRAVIS_TAG
|
|
||||||
end='## \['
|
|
||||||
body=$(sed -n "1,/$start/d;/$end/q;p" CHANGELOG.md)
|
|
||||||
strip postgrest
|
|
||||||
tar cJf postgrest-$TRAVIS_TAG-osx.tar.xz postgrest
|
|
||||||
ghr -t $GITHUB_TOKEN -u $owner -r $repo -b "$body"--replace $TRAVIS_TAG postgrest-$TRAVIS_TAG-osx.tar.xz
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
@@ -9,6 +9,37 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
## [9.0.0] - 2021-11-25
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- #1783, Include partitioned tables into the schema cache. Allows embedding, UPSERT, INSERT with Location response, OPTIONS request and OpenAPI support for partitioned tables - @laurenceisla
|
||||||
|
- #1878, Add Retry-After hint header when in recovery mode - @gautam1168
|
||||||
|
- #1735, Allow calling function with single unnamed param through RPC POST. - @steve-chavez
|
||||||
|
+ Enables calling a function with a single json parameter without using `Prefer: params=single-object`
|
||||||
|
+ Enables uploading bytea to a function with `Content-Type: application/octet-stream`
|
||||||
|
+ Enables uploading raw text to a function with `Content-Type: text/plain`
|
||||||
|
- #1938, Allow escaping inside double quotes with a backslash, e.g. `?col=in.("Double\"Quote")`, `?col=in.("Back\\slash")` - @steve-chavez
|
||||||
|
- #1075, Allow filtering top-level resource based on embedded resources filters. This is enabled by adding `!inner` to the embedded resource, e.g. `/projects?select=*,clients!inner(*)&clients.id=eq.12`- @steve-chavez, @Iced-Sun
|
||||||
|
- #1857, Make GUC names for headers, cookies and jwt claims compatible with PostgreSQL v14 - @laurenceisla, @robertsosinski
|
||||||
|
+ Getting the value for a header GUC on PostgreSQL 14 is done using `current_setting('request.headers')::json->>'name-of-header'` and in a similar way for `request.cookies` and `request.jwt.claims`
|
||||||
|
+ PostgreSQL versions below 14 can opt in to the new JSON GUCs by setting the `db-use-legacy-gucs` config option to false (true by default)
|
||||||
|
- #1988, Allow specifying `unknown` for the `is` operator - @steve-chavez
|
||||||
|
- #2031, Improve error message for ambiguous embedding and add a relevant hint that includes unambiguous embedding suggestions - @laurenceisla
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- #1871, Fix OpenAPI missing default values for String types and identify Array types as "array" instead of "string" - @laurenceisla
|
||||||
|
- #1930, Fix RPC return type handling for `RETURNS TABLE` with a single column. Regression of #1615. - @wolfgangwalther
|
||||||
|
- #1938, Fix using single double quotes(`"`) and backslashes(`/`) as values on the "in" operator - @steve-chavez
|
||||||
|
- #1992, Fix schema cache query failing with standard_conforming_strings = off - @wolfgangwalther
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- #1949, Drop support for embedding hints used with '.'(`select=projects.client_id(*)`), '!' should be used instead(`select=projects!client_id(*)`) - @steve-chavez
|
||||||
|
- #1783, Partitions (created using `PARTITION OF`) are no longer included in the schema cache. - @laurenceisla
|
||||||
|
- #2038, Dropped support for PostgreSQL 9.5 - @wolfgangwalther
|
||||||
|
|
||||||
## [8.0.0] - 2021-07-25
|
## [8.0.0] - 2021-07-25
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
[](https://gitter.im/begriffs/postgrest)
|
[](https://gitter.im/begriffs/postgrest)
|
||||||
[](http://postgrest.org)
|
[](http://postgrest.org)
|
||||||
[](https://hub.docker.com/r/postgrest/postgrest/)
|
[](https://hub.docker.com/r/postgrest/postgrest/)
|
||||||
[](https://circleci.com/gh/PostgREST/postgrest/tree/main)
|
[](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)
|
||||||
[](https://app.codecov.io/gh/PostgREST/postgrest)
|
[](https://app.codecov.io/gh/PostgREST/postgrest)
|
||||||
[](http://hackage.haskell.org/package/postgrest)
|
[](http://hackage.haskell.org/package/postgrest)
|
||||||
|
|
||||||
@@ -112,15 +112,6 @@ the connection cannot do anything the user themselves couldn't. Other
|
|||||||
forms of authentication can be built on top of the JWT primitive. See
|
forms of authentication can be built on top of the JWT primitive. See
|
||||||
the docs for more information.
|
the docs for more information.
|
||||||
|
|
||||||
Since PostgreSQL 9.5 supports true [row-level
|
|
||||||
security](http://www.postgresql.org/docs/9.5/static/ddl-rowsecurity.html).
|
|
||||||
In previous versions it can be simulated with triggers and
|
|
||||||
security-barrier views. Because the possible queries to the database
|
|
||||||
are limited to certain templates using
|
|
||||||
[leakproof](http://blog.2ndquadrant.com/how-do-postgresql-security_barrier-views-work/)
|
|
||||||
functions, the trigger workaround does not compromise row-level
|
|
||||||
security.
|
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
A robust long-lived API needs the freedom to exist in multiple
|
A robust long-lived API needs the freedom to exist in multiple
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
## AppVeyor is only used for building a Windows binary, no tests are run here.
|
|
||||||
platform: x64
|
|
||||||
image: Visual Studio 2015
|
|
||||||
|
|
||||||
cache:
|
|
||||||
- "c:\\sr"
|
|
||||||
- .stack-work
|
|
||||||
- "c:\\Users\\appveyor\\AppData\\Local\\Programs\\stack"
|
|
||||||
|
|
||||||
environment:
|
|
||||||
global:
|
|
||||||
STACK_ROOT: "c:\\sr"
|
|
||||||
GOPATH: c:\gopath
|
|
||||||
TMP: "c:\\tmp"
|
|
||||||
|
|
||||||
test: off
|
|
||||||
|
|
||||||
install:
|
|
||||||
- set PATH=C:\Program Files\PostgreSQL\9.6\bin\;%PATH%
|
|
||||||
- curl -sS -ostack.zip -L --insecure http://www.stackage.org/stack/windows-x86_64
|
|
||||||
- 7z x stack.zip stack.exe
|
|
||||||
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
|
|
||||||
- go get -u github.com/tcnksm/ghr
|
|
||||||
|
|
||||||
build_script:
|
|
||||||
- ps: $env:cabal_nightly_version=(git show -s --format='%cd' --date='format:%Y%m%d')
|
|
||||||
- IF "%APPVEYOR_REPO_TAG_NAME%"=="nightly" bash -lc "sed -i -r \"s/^(version:\s+)\S+$/\1$cabal_nightly_version/\" postgrest.cabal"
|
|
||||||
- stack setup --no-terminal > nul
|
|
||||||
# Appveyor has a timeout of 60 mins, building can take longer, limit the time and make sure this succeeds,
|
|
||||||
# previous work will get cached and finish on next commit
|
|
||||||
- bash -lc "timeout 2700 'C:\projects\postgrest\stack.exe' build -j1 --copy-bins --local-bin-path . || (($?==124))"
|
|
||||||
|
|
||||||
artifacts:
|
|
||||||
- path: postgrest.exe
|
|
||||||
|
|
||||||
deploy_script:
|
|
||||||
## Use powershell(ps) for this because CMD commands having "%" don't work(even by escaping with "%%"). See https://github.com/appveyor/ci/issues/246.
|
|
||||||
- ps: $env:suffix=(git show -s --format="%cd-%h" --date="format:%Y-%m-%d-%H-%M")
|
|
||||||
- IF DEFINED APPVEYOR_REPO_TAG_NAME (
|
|
||||||
IF "%APPVEYOR_REPO_TAG_NAME%"=="nightly" (
|
|
||||||
7z a -tzip postgrest-nightly-%suffix%-windows-x64.zip postgrest.exe &&
|
|
||||||
bash -lc "exec 0</dev/null && cd $APPVEYOR_BUILD_FOLDER && ghr -t $GITHUB_TOKEN -u $APPVEYOR_ACCOUNT_NAME -r $APPVEYOR_PROJECT_NAME --replace nightly postgrest-nightly-$suffix-windows-x64.zip"
|
|
||||||
) ELSE (
|
|
||||||
7z a -tzip postgrest-%APPVEYOR_REPO_TAG_NAME%-windows-x64.zip postgrest.exe &&
|
|
||||||
bash -lc "exec 0</dev/null && cd $APPVEYOR_BUILD_FOLDER && ghr -t $GITHUB_TOKEN -u $APPVEYOR_ACCOUNT_NAME -r $APPVEYOR_PROJECT_NAME -b \"`sed -n \"1,/$APPVEYOR_REPO_TAG_NAME/d;/## \[/q;p\" CHANGELOG.md`\" --replace $APPVEYOR_REPO_TAG_NAME postgrest-$APPVEYOR_REPO_TAG_NAME-windows-x64.zip"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
+10
-15
@@ -3,7 +3,7 @@ let
|
|||||||
"postgrest";
|
"postgrest";
|
||||||
|
|
||||||
compiler =
|
compiler =
|
||||||
"ghc8104";
|
"ghc8107";
|
||||||
|
|
||||||
# PostgREST source files, filtered based on the rules in the .gitignore files
|
# PostgREST source files, filtered based on the rules in the .gitignore files
|
||||||
# and file extensions. We want to include as litte as possible, as the files
|
# and file extensions. We want to include as litte as possible, as the files
|
||||||
@@ -33,10 +33,10 @@ let
|
|||||||
[
|
[
|
||||||
allOverlays.build-toolbox
|
allOverlays.build-toolbox
|
||||||
allOverlays.checked-shell-script
|
allOverlays.checked-shell-script
|
||||||
allOverlays.ghr
|
|
||||||
allOverlays.gitignore
|
allOverlays.gitignore
|
||||||
allOverlays.postgresql-default
|
allOverlays.postgresql-default
|
||||||
allOverlays.postgresql-legacy
|
allOverlays.postgresql-legacy
|
||||||
|
allOverlays.postgresql-future
|
||||||
(allOverlays.haskell-packages { inherit compiler; })
|
(allOverlays.haskell-packages { inherit compiler; })
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -46,12 +46,12 @@ let
|
|||||||
|
|
||||||
postgresqlVersions =
|
postgresqlVersions =
|
||||||
[
|
[
|
||||||
|
{ name = "postgresql-14"; postgresql = pkgs.postgresql_14; }
|
||||||
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13; }
|
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13; }
|
||||||
{ name = "postgresql-12"; postgresql = pkgs.postgresql_12; }
|
{ name = "postgresql-12"; postgresql = pkgs.postgresql_12; }
|
||||||
{ name = "postgresql-11"; postgresql = pkgs.postgresql_11; }
|
{ name = "postgresql-11"; postgresql = pkgs.postgresql_11; }
|
||||||
{ name = "postgresql-10"; postgresql = pkgs.postgresql_10; }
|
{ name = "postgresql-10"; postgresql = pkgs.postgresql_10; }
|
||||||
{ name = "postgresql-9.6"; postgresql = pkgs.postgresql_9_6; }
|
{ name = "postgresql-9.6"; postgresql = pkgs.postgresql_9_6; }
|
||||||
{ name = "postgresql-9.5"; postgresql = pkgs.postgresql_9_5; }
|
|
||||||
];
|
];
|
||||||
|
|
||||||
patches =
|
patches =
|
||||||
@@ -79,8 +79,7 @@ let
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
lib =
|
inherit (pkgs.haskell) lib;
|
||||||
pkgs.haskell.lib;
|
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
inherit nixpkgs pkgs;
|
inherit nixpkgs pkgs;
|
||||||
@@ -103,13 +102,12 @@ rec {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
env =
|
inherit (postgrest) env;
|
||||||
postgrest.env;
|
|
||||||
|
|
||||||
# Tooling for analyzing Haskell imports and exports.
|
# Tooling for analyzing Haskell imports and exports.
|
||||||
hsie =
|
hsie =
|
||||||
pkgs.callPackage nix/hsie {
|
pkgs.callPackage nix/hsie {
|
||||||
ghcWithPackages = pkgs.haskell.packages.ghc884.ghcWithPackages;
|
inherit (pkgs.haskell.packages."${compiler}") ghcWithPackages;
|
||||||
};
|
};
|
||||||
|
|
||||||
### Tools
|
### Tools
|
||||||
@@ -119,7 +117,7 @@ rec {
|
|||||||
|
|
||||||
# Development tools.
|
# Development tools.
|
||||||
devTools =
|
devTools =
|
||||||
pkgs.callPackage nix/tools/devTools.nix { inherit tests style devCabalOptions hsie; };
|
pkgs.callPackage nix/tools/devTools.nix { inherit tests style devCabalOptions hsie withTools; };
|
||||||
|
|
||||||
# Docker images and loading script.
|
# Docker images and loading script.
|
||||||
docker =
|
docker =
|
||||||
@@ -135,21 +133,18 @@ rec {
|
|||||||
|
|
||||||
# Scripts for publishing new releases.
|
# Scripts for publishing new releases.
|
||||||
release =
|
release =
|
||||||
pkgs.callPackage nix/tools/release {
|
pkgs.callPackage nix/tools/release { };
|
||||||
inherit docker;
|
|
||||||
postgrest = postgrestStatic;
|
|
||||||
};
|
|
||||||
|
|
||||||
# Linting and styling tools.
|
# Linting and styling tools.
|
||||||
style =
|
style =
|
||||||
pkgs.callPackage nix/tools/style.nix { };
|
pkgs.callPackage nix/tools/style.nix { inherit hsie; };
|
||||||
|
|
||||||
# Scripts for running tests.
|
# Scripts for running tests.
|
||||||
tests =
|
tests =
|
||||||
pkgs.callPackage nix/tools/tests.nix {
|
pkgs.callPackage nix/tools/tests.nix {
|
||||||
inherit postgrest devCabalOptions withTools;
|
inherit postgrest devCabalOptions withTools;
|
||||||
ghc = pkgs.haskell.compiler."${compiler}";
|
ghc = pkgs.haskell.compiler."${compiler}";
|
||||||
hpc-codecov = pkgs.haskell.packages."${compiler}".hpc-codecov;
|
inherit (pkgs.haskell.packages."${compiler}") hpc-codecov;
|
||||||
};
|
};
|
||||||
|
|
||||||
withTools =
|
withTools =
|
||||||
|
|||||||
+4
-4
@@ -80,8 +80,8 @@ postgrest-coverage postgrest-with-postgresql-10
|
|||||||
postgrest-lint postgrest-with-postgresql-11
|
postgrest-lint postgrest-with-postgresql-11
|
||||||
postgrest-run postgrest-with-postgresql-12
|
postgrest-run postgrest-with-postgresql-12
|
||||||
postgrest-style postgrest-with-postgresql-13
|
postgrest-style postgrest-with-postgresql-13
|
||||||
postgrest-style-check postgrest-with-postgresql-9.5
|
postgrest-style-check postgrest-with-postgresql-9.6
|
||||||
postgrest-test-io postgrest-with-postgresql-9.6
|
postgrest-test-io
|
||||||
...
|
...
|
||||||
|
|
||||||
[nix-shell]$
|
[nix-shell]$
|
||||||
@@ -104,8 +104,8 @@ postgrest-coverage postgrest-with-postgresql-10
|
|||||||
postgrest-lint postgrest-with-postgresql-11
|
postgrest-lint postgrest-with-postgresql-11
|
||||||
postgrest-run postgrest-with-postgresql-12
|
postgrest-run postgrest-with-postgresql-12
|
||||||
postgrest-style postgrest-with-postgresql-13
|
postgrest-style postgrest-with-postgresql-13
|
||||||
postgrest-style-check postgrest-with-postgresql-9.5
|
postgrest-style-check postgrest-with-postgresql-9.6
|
||||||
postgrest-test-io postgrest-with-postgresql-9.6
|
postgrest-test-io
|
||||||
postgrest-test-memory
|
postgrest-test-memory
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|||||||
+7
-4
@@ -17,6 +17,7 @@ module Main (main) where
|
|||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString.Lazy.Char8 as LBS8
|
import qualified Data.ByteString.Lazy.Char8 as LBS8
|
||||||
import qualified Data.Csv as Csv
|
import qualified Data.Csv as Csv
|
||||||
|
import qualified Data.List as List
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as Set
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
@@ -27,13 +28,14 @@ import qualified Language.Haskell.GHC.ExactPrint.Parsers as ExactPrint
|
|||||||
import qualified Options.Applicative as O
|
import qualified Options.Applicative as O
|
||||||
import qualified System.FilePath as FP
|
import qualified System.FilePath as FP
|
||||||
|
|
||||||
|
import Bag (bagToList)
|
||||||
import Data.Aeson.Encode.Pretty (encodePretty)
|
import Data.Aeson.Encode.Pretty (encodePretty)
|
||||||
import Data.Function ((&))
|
import Data.Function ((&))
|
||||||
import Data.List (intercalate)
|
import Data.List (intercalate)
|
||||||
import Data.Maybe (catMaybes, mapMaybe)
|
import Data.Maybe (catMaybes, mapMaybe)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import GHC.Generics (Generic)
|
import GHC.Generics (Generic)
|
||||||
import HsExtension (GhcPs)
|
import GHC.Hs.Extension (GhcPs)
|
||||||
import Module (moduleNameString)
|
import Module (moduleNameString)
|
||||||
import OccName (occNameString)
|
import OccName (occNameString)
|
||||||
import RdrName (rdrNameOcc)
|
import RdrName (rdrNameOcc)
|
||||||
@@ -201,8 +203,9 @@ parseModule filepath = do
|
|||||||
case result of
|
case result of
|
||||||
Right (_, hsmod) ->
|
Right (_, hsmod) ->
|
||||||
return $ GHC.unLoc hsmod
|
return $ GHC.unLoc hsmod
|
||||||
Left (loc, err) ->
|
Left errs ->
|
||||||
fail $ "Error with " <> show filepath <> " at " <> show loc <> ": " <> err
|
fail $ "Errors with " <> show filepath <> ":\n "
|
||||||
|
<> List.intercalate "\n " (show <$> bagToList errs)
|
||||||
|
|
||||||
-- | Symbols imported in an import declaration.
|
-- | Symbols imported in an import declaration.
|
||||||
--
|
--
|
||||||
@@ -223,7 +226,7 @@ importSymbols source filepath GHC.ImportDecl{..} =
|
|||||||
, impSource = source
|
, impSource = source
|
||||||
, impFromModule = T.pack $ moduleFromPath filepath
|
, impFromModule = T.pack $ moduleFromPath filepath
|
||||||
, impModule = T.pack . moduleNameString . GHC.unLoc $ ideclName
|
, impModule = T.pack . moduleNameString . GHC.unLoc $ ideclName
|
||||||
, impQualified = if ideclQualified then Qualified else NotQualified
|
, impQualified = if ideclQualified /= GHC.NotQualified then Qualified else NotQualified
|
||||||
, impAlias = T.pack . moduleNameString . GHC.unLoc <$> ideclAs
|
, impAlias = T.pack . moduleNameString . GHC.unLoc <$> ideclAs
|
||||||
, impInternal = External
|
, impInternal = External
|
||||||
, impType = hiding
|
, impType = hiding
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Pinned version of Nixpkgs, generated with postgrest-nixpkgs-upgrade.
|
# Pinned version of Nixpkgs, generated with postgrest-nixpkgs-upgrade.
|
||||||
{
|
{
|
||||||
date = "2021-07-17";
|
date = "2021-11-02";
|
||||||
rev = "d00b5a5fa6fe8bdf7005abb06c46ae0245aec8b5";
|
rev = "7053541084bf5ce2921ef307e5585d39d7ba8b3f";
|
||||||
tarballHash = "08497wbpnf3w5dalcasqzymw3fmcn8qrnbkf8rxxwwvyjdnczxdv";
|
tarballHash = "1flhh5d4zy43x6060hvzjb5hi5cmc51ivc0nwmija9n8d35kcc4x";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
, inRootDir ? false
|
, inRootDir ? false
|
||||||
, redirectTixFiles ? true
|
, redirectTixFiles ? true
|
||||||
, withEnv ? null
|
, withEnv ? null
|
||||||
|
, withPath ? [ ]
|
||||||
, withTmpDir ? false
|
, withTmpDir ? false
|
||||||
}: text:
|
}: text:
|
||||||
let
|
let
|
||||||
@@ -114,6 +115,10 @@ let
|
|||||||
export PATH="$env/bin:$PATH"
|
export PATH="$env/bin:$PATH"
|
||||||
''
|
''
|
||||||
|
|
||||||
|
+ lib.optionalString (lib.length withPath > 0) ''
|
||||||
|
export PATH="${lib.concatMapStrings (p: p + "/bin:") withPath}$PATH"
|
||||||
|
''
|
||||||
|
|
||||||
+ "(${text})"
|
+ "(${text})"
|
||||||
|
|
||||||
+ lib.optionalString withTmpDir ''
|
+ lib.optionalString withTmpDir ''
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
build-toolbox = import ./build-toolbox;
|
build-toolbox = import ./build-toolbox;
|
||||||
checked-shell-script = import ./checked-shell-script;
|
checked-shell-script = import ./checked-shell-script;
|
||||||
ghr = import ./ghr;
|
|
||||||
gitignore = import ./gitignore.nix;
|
gitignore = import ./gitignore.nix;
|
||||||
haskell-packages = import ./haskell-packages.nix;
|
haskell-packages = import ./haskell-packages.nix;
|
||||||
postgresql-default = import ./postgresql-default.nix;
|
postgresql-default = import ./postgresql-default.nix;
|
||||||
postgresql-legacy = import ./postgresql-legacy.nix;
|
postgresql-legacy = import ./postgresql-legacy.nix;
|
||||||
|
postgresql-future = import ./postgresql-future.nix;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
self: super:
|
|
||||||
# Overlay that adds `ghr`: Upload multiple artifacts to GitHub Release in
|
|
||||||
# parallel, http://tcnksm.github.io/ghr/
|
|
||||||
|
|
||||||
{
|
|
||||||
ghr = super.callPackage ./ghr.nix { };
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{ buildGoModule, fetchFromGitHub }:
|
|
||||||
|
|
||||||
buildGoModule rec {
|
|
||||||
pname = "ghr";
|
|
||||||
version = "0.14.0";
|
|
||||||
|
|
||||||
src = fetchFromGitHub {
|
|
||||||
rev = "v${version}";
|
|
||||||
owner = "tcnksm";
|
|
||||||
repo = "ghr";
|
|
||||||
sha256 = "1jjc3bwmyw831r1ayic1f1ysh5ggm88aszbndm0swg8byhz56pd4";
|
|
||||||
};
|
|
||||||
|
|
||||||
vendorSha256 = "06cbhsnxv4gisnwrhw61af7rpv2a9slf9z2wbn79r91xzkh51vzr";
|
|
||||||
|
|
||||||
# Disabling tests, as they require a GitHub API token
|
|
||||||
doCheck = false;
|
|
||||||
}
|
|
||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
self: super:
|
self: super:
|
||||||
let
|
let
|
||||||
lib =
|
inherit (self.haskell) lib;
|
||||||
self.haskell.lib;
|
|
||||||
|
|
||||||
overrides =
|
overrides =
|
||||||
final: prev:
|
final: prev:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
self: super:
|
self: super:
|
||||||
# Overlay that sets the default version of PostgreSQL.
|
# Overlay that sets the default version of PostgreSQL.
|
||||||
{
|
{
|
||||||
postgresql = super.postgresql_13;
|
postgresql = super.postgresql_14;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
self: super:
|
||||||
|
# Overlay that adds future versions of PostgreSQL that are supported by
|
||||||
|
# PostgREST.
|
||||||
|
{
|
||||||
|
## Example for including a postgresql version from a specific nixpks commit:
|
||||||
|
##
|
||||||
|
# postgresql_14 =
|
||||||
|
# let
|
||||||
|
# rev = "76b1e16c6659ccef7187ca69b287525fea133244";
|
||||||
|
# tarballHash = "1vsahpcx80k2bgslspb0sa6j4bmhdx77sw6la455drqcrqhdqj6a";
|
||||||
|
#
|
||||||
|
# pinnedPkgs =
|
||||||
|
# builtins.fetchTarball {
|
||||||
|
# url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||||
|
# sha256 = tarballHash;
|
||||||
|
# };
|
||||||
|
# in
|
||||||
|
# (import pinnedPkgs { }).pkgs.postgresql_14;
|
||||||
|
}
|
||||||
@@ -5,16 +5,16 @@ self: super:
|
|||||||
# PostgreSQL 9.5 was removed from Nixpkgs with
|
# PostgreSQL 9.5 was removed from Nixpkgs with
|
||||||
# https://github.com/NixOS/nixpkgs/commit/72ab382fb6b729b0d654f2c03f5eb25b39f11fbb
|
# https://github.com/NixOS/nixpkgs/commit/72ab382fb6b729b0d654f2c03f5eb25b39f11fbb
|
||||||
# We pin its parent commit to get the last version that was available.
|
# We pin its parent commit to get the last version that was available.
|
||||||
postgresql_9_5 =
|
# postgresql_9_5 =
|
||||||
let
|
# let
|
||||||
rev = "55ac7d4580c9ab67848c98cb9519317a1cc399c8";
|
# rev = "55ac7d4580c9ab67848c98cb9519317a1cc399c8";
|
||||||
tarballHash = "02ffj9f8s1hwhmxj85nx04sv64qb6jm7w0122a1dz9n32fymgklj";
|
# tarballHash = "02ffj9f8s1hwhmxj85nx04sv64qb6jm7w0122a1dz9n32fymgklj";
|
||||||
|
#
|
||||||
pinnedPkgs =
|
# pinnedPkgs =
|
||||||
builtins.fetchTarball {
|
# builtins.fetchTarball {
|
||||||
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
# url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||||
sha256 = tarballHash;
|
# sha256 = tarballHash;
|
||||||
};
|
# };
|
||||||
in
|
# in
|
||||||
(import pinnedPkgs { }).pkgs.postgresql_9_5;
|
# (import pinnedPkgs { }).pkgs.postgresql_9_5;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,16 +39,29 @@ let
|
|||||||
|
|
||||||
overlays =
|
overlays =
|
||||||
[
|
[
|
||||||
|
allOverlays.postgresql-future
|
||||||
|
allOverlays.postgresql-default
|
||||||
(allOverlays.haskell-packages { inherit compiler extraOverrides; })
|
(allOverlays.haskell-packages { inherit compiler extraOverrides; })
|
||||||
|
# Disable failing tests for postgresql on musl that should have no impact
|
||||||
|
# on the libpq that we need (collate.icu.utf8 and foreign regression
|
||||||
|
# tests)
|
||||||
|
(self: super:
|
||||||
|
{ postgresql = super.postgresql.overrideAttrs (_: { doCheck = false; }); }
|
||||||
|
)
|
||||||
];
|
];
|
||||||
|
|
||||||
# Apply our overlay to the given pkgs.
|
# Apply our overlay to the given pkgs.
|
||||||
normalPkgs =
|
normalPkgs =
|
||||||
import patchedNixpkgs { inherit overlays; };
|
import patchedNixpkgs { inherit overlays; };
|
||||||
|
|
||||||
|
defaultCabalPackageVersionComingWithGhc =
|
||||||
|
{
|
||||||
|
ghc8107 = "Cabal_3_2_1_0";
|
||||||
|
}."${compiler}";
|
||||||
|
|
||||||
# The static-haskell-nix 'survey' derives a full static set of Haskell
|
# The static-haskell-nix 'survey' derives a full static set of Haskell
|
||||||
# packages, applying fixes where necessary.
|
# packages, applying fixes where necessary.
|
||||||
survey =
|
survey =
|
||||||
import "${patched-static-haskell-nix}/survey" { inherit normalPkgs compiler; };
|
import "${patched-static-haskell-nix}/survey" { inherit normalPkgs compiler defaultCabalPackageVersionComingWithGhc; };
|
||||||
in
|
in
|
||||||
survey.haskellPackages."${name}"
|
survey.haskellPackages."${name}"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
, silver-searcher
|
, silver-searcher
|
||||||
, style
|
, style
|
||||||
, tests
|
, tests
|
||||||
|
, withTools
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
watch =
|
watch =
|
||||||
@@ -71,7 +72,9 @@ let
|
|||||||
inRootDir = true;
|
inRootDir = true;
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
${tests}/bin/postgrest-with-all ${tests}/bin/postgrest-test-spec
|
${withTools}/bin/postgrest-with-all ${tests}/bin/postgrest-test-spec
|
||||||
|
${withTools}/bin/postgrest-with-all ${tests}/bin/postgrest-test-querycost
|
||||||
|
${tests}/bin/postgrest-test-doctests
|
||||||
${tests}/bin/postgrest-test-spec-idempotence
|
${tests}/bin/postgrest-test-spec-idempotence
|
||||||
${tests}/bin/postgrest-test-io
|
${tests}/bin/postgrest-test-io
|
||||||
${style}/bin/postgrest-lint
|
${style}/bin/postgrest-lint
|
||||||
|
|||||||
@@ -14,10 +14,9 @@ let
|
|||||||
name = "postgrest-test-memory";
|
name = "postgrest-test-memory";
|
||||||
docs = "Run the memory tests.";
|
docs = "Run the memory tests.";
|
||||||
inRootDir = true;
|
inRootDir = true;
|
||||||
|
withPath = [ postgrestProfiled curl ];
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
export PATH="${postgrestProfiled}/bin:${curl}/bin:$PATH"
|
|
||||||
|
|
||||||
${withTools.latest} test/memory-tests.sh
|
${withTools.latest} test/memory-tests.sh
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|||||||
@@ -14,13 +14,13 @@ let
|
|||||||
"postgrest-nixpkgs-upgrade";
|
"postgrest-nixpkgs-upgrade";
|
||||||
|
|
||||||
refUrl =
|
refUrl =
|
||||||
https://api.github.com/repos/nixos/nixpkgs/git/ref/heads/nixpkgs-unstable;
|
"https://api.github.com/repos/nixos/nixpkgs/git/ref/heads/nixpkgs-unstable";
|
||||||
|
|
||||||
githubV3Header =
|
githubV3Header =
|
||||||
"Accept: application/vnd.github.v3+json";
|
"Accept: application/vnd.github.v3+json";
|
||||||
|
|
||||||
tarballUrlBase =
|
tarballUrlBase =
|
||||||
https://github.com/nixos/nixpkgs/archive/;
|
"https://github.com/nixos/nixpkgs/archive/";
|
||||||
|
|
||||||
upgrade =
|
upgrade =
|
||||||
checkedShellScript
|
checkedShellScript
|
||||||
|
|||||||
+13
-133
@@ -1,124 +1,9 @@
|
|||||||
{ buildToolbox
|
{ buildToolbox
|
||||||
, checkedShellScript
|
, checkedShellScript
|
||||||
, curl
|
, curl
|
||||||
, docker
|
|
||||||
, envsubst
|
|
||||||
, ghr
|
|
||||||
, git
|
|
||||||
, jq
|
, jq
|
||||||
, postgrest
|
|
||||||
, runCommand
|
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
github =
|
|
||||||
checkedShellScript
|
|
||||||
{
|
|
||||||
name = "postgrest-release-github";
|
|
||||||
docs = "Push a new release to GitHub.";
|
|
||||||
args = [
|
|
||||||
"ARG_POSITIONAL_SINGLE([version], [git version tag to make release for])"
|
|
||||||
"ARG_USE_ENV([GITHUB_TOKEN], [], [GitHub token])"
|
|
||||||
"ARG_USE_ENV([GITHUB_USERNAME], [], [GitHub user name])"
|
|
||||||
"ARG_USE_ENV([GITHUB_REPONAME], [], [GitHub repository name])"
|
|
||||||
];
|
|
||||||
inRootDir = true;
|
|
||||||
}
|
|
||||||
''
|
|
||||||
# ARG_USE_ENV only adds defaults or docs for environment variables
|
|
||||||
# We manually implement a required check here
|
|
||||||
# See also: https://github.com/matejak/argbash/issues/80
|
|
||||||
GITHUB_TOKEN="''${GITHUB_TOKEN:?GITHUB_TOKEN is required}"
|
|
||||||
GITHUB_USERNAME="''${GITHUB_USERNAME:?GITHUB_USERNAME is required}"
|
|
||||||
GITHUB_REPONAME="''${GITHUB_REPONAME:?GITHUB_REPONAME is required}"
|
|
||||||
|
|
||||||
if test "$_arg_version" = "nightly"
|
|
||||||
then
|
|
||||||
suffix=$(${git}/bin/git show -s --format="%cd-%h" --date="format:%Y-%m-%d-%H-%M")
|
|
||||||
tar cvJf "postgrest-nightly-$suffix-linux-x64-static.tar.xz" \
|
|
||||||
-C ${postgrest}/bin postgrest
|
|
||||||
|
|
||||||
${ghr}/bin/ghr \
|
|
||||||
-t "$GITHUB_TOKEN" \
|
|
||||||
-u "$GITHUB_USERNAME" \
|
|
||||||
-r "$GITHUB_REPONAME" \
|
|
||||||
--replace nightly \
|
|
||||||
"postgrest-nightly-$suffix-linux-x64-static.tar.xz"
|
|
||||||
else
|
|
||||||
changes="$(sed -n "1,/$_arg_version/d;/## \[/q;p" ${../../../CHANGELOG.md})"
|
|
||||||
|
|
||||||
tar cvJf "postgrest-$_arg_version-linux-x64-static.tar.xz" \
|
|
||||||
-C ${postgrest}/bin postgrest
|
|
||||||
|
|
||||||
${ghr}/bin/ghr \
|
|
||||||
-t "$GITHUB_TOKEN" \
|
|
||||||
-u "$GITHUB_USERNAME" \
|
|
||||||
-r "$GITHUB_REPONAME" \
|
|
||||||
-b "$changes" \
|
|
||||||
--replace "$_arg_version" \
|
|
||||||
"postgrest-$_arg_version-linux-x64-static.tar.xz"
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
|
|
||||||
dockerLogin =
|
|
||||||
checkedShellScript
|
|
||||||
{
|
|
||||||
name = "postgrest-release-docker-login";
|
|
||||||
docs =
|
|
||||||
''
|
|
||||||
Log in to Docker Hub using the DOCKER_USER and DOCKER_PASS env vars.
|
|
||||||
|
|
||||||
Those env vars are usually provided by CircleCI. The DOCKER_USER is
|
|
||||||
not the same as DOCKER_REPO because we use the
|
|
||||||
https://hub.docker.com/u/postgrestbot account for uploading to dockerhub.
|
|
||||||
'';
|
|
||||||
args = [
|
|
||||||
"ARG_USE_ENV([DOCKER_USER], [], [DockerHub user name])"
|
|
||||||
"ARG_USE_ENV([DOCKER_PASS], [], [DockerHub password])"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
''
|
|
||||||
# ARG_USE_ENV only adds defaults or docs for environment variables
|
|
||||||
# We manually implement a required check here
|
|
||||||
# See also: https://github.com/matejak/argbash/issues/80
|
|
||||||
DOCKER_USER="''${DOCKER_USER:?DOCKER_USER is required}"
|
|
||||||
DOCKER_PASS="''${DOCKER_PASS:?DOCKER_PASS is required}"
|
|
||||||
|
|
||||||
docker login -u "$DOCKER_USER" -p "$DOCKER_PASS"
|
|
||||||
'';
|
|
||||||
|
|
||||||
dockerHub =
|
|
||||||
checkedShellScript
|
|
||||||
{
|
|
||||||
name = "postgrest-release-dockerhub";
|
|
||||||
docs = "Push a new release to Docker Hub";
|
|
||||||
args = [
|
|
||||||
"ARG_POSITIONAL_SINGLE([version], [git version tag to tag image with])"
|
|
||||||
"ARG_USE_ENV([DOCKER_REPO], [], [DockerHub repository])"
|
|
||||||
];
|
|
||||||
}
|
|
||||||
''
|
|
||||||
# ARG_USE_ENV only adds defaults or docs for environment variables
|
|
||||||
# We manually implement a required check here
|
|
||||||
# See also: https://github.com/matejak/argbash/issues/80
|
|
||||||
DOCKER_REPO="''${DOCKER_REPO:?DOCKER_REPO is required}"
|
|
||||||
|
|
||||||
docker load -i ${docker.image}
|
|
||||||
|
|
||||||
if test "$_arg_version" = "nightly"
|
|
||||||
then
|
|
||||||
suffix=$(${git}/bin/git show -s --format="%cd-%h" --date="format:%Y-%m-%d-%H-%M")
|
|
||||||
|
|
||||||
docker tag postgrest:latest "$DOCKER_REPO/postgrest:nightly-$suffix"
|
|
||||||
docker push "$DOCKER_REPO/postgrest:nightly-$suffix"
|
|
||||||
else
|
|
||||||
docker tag postgrest:latest "$DOCKER_REPO"/postgrest:latest
|
|
||||||
docker tag postgrest:latest "$DOCKER_REPO/postgrest:$_arg_version"
|
|
||||||
|
|
||||||
docker push "$DOCKER_REPO"/postgrest:latest
|
|
||||||
docker push "$DOCKER_REPO/postgrest:$_arg_version"
|
|
||||||
fi
|
|
||||||
'';
|
|
||||||
|
|
||||||
dockerHubDescription =
|
dockerHubDescription =
|
||||||
let
|
let
|
||||||
description =
|
description =
|
||||||
@@ -141,34 +26,29 @@ let
|
|||||||
# ARG_USE_ENV only adds defaults or docs for environment variables
|
# ARG_USE_ENV only adds defaults or docs for environment variables
|
||||||
# We manually implement a required check here
|
# We manually implement a required check here
|
||||||
# See also: https://github.com/matejak/argbash/issues/80
|
# See also: https://github.com/matejak/argbash/issues/80
|
||||||
DOCKER_USER="''${DOCKER_USER:?DOCKER_USER is required}"
|
: "''${DOCKER_USER:?DOCKER_USER is required}"
|
||||||
DOCKER_PASS="''${DOCKER_PASS:?DOCKER_PASS is required}"
|
: "''${DOCKER_PASS:?DOCKER_PASS is required}"
|
||||||
DOCKER_REPO="''${DOCKER_REPO:?DOCKER_REPO is required}"
|
: "''${DOCKER_REPO:?DOCKER_REPO is required}"
|
||||||
|
|
||||||
# Login to Docker Hub and get a token.
|
echo "Logging in to Docker Hub to get an auth token..."
|
||||||
token="$(
|
token="$(
|
||||||
${curl}/bin/curl -s \
|
${curl}/bin/curl --fail -s \
|
||||||
--data-urlencode "username=$DOCKER_USER" \
|
--data-urlencode "username=$DOCKER_USER" \
|
||||||
--data-urlencode "password=$DOCKER_PASS" \
|
--data-urlencode "password=$DOCKER_PASS" \
|
||||||
"https://hub.docker.com/v2/users/login/" \
|
"https://hub.docker.com/v2/users/login/" \
|
||||||
| ${jq}/bin/jq -r .token
|
| ${jq}/bin/jq -r .token
|
||||||
)"
|
)"
|
||||||
|
|
||||||
# Patch both descriptions.
|
repo_url="https://hub.docker.com/v2/repositories/$DOCKER_REPO/postgrest/"
|
||||||
responseCode="$(
|
echo "Patching both descriptions at $repo_url ..."
|
||||||
${curl}/bin/curl -s --write-out "%{response_code}" \
|
${curl}/bin/curl --fail -X PATCH "$repo_url" \
|
||||||
--output /dev/null -H "Authorization: JWT $token" -X PATCH \
|
-H "Authorization: JWT $token" \
|
||||||
--data-urlencode description@${description} \
|
--data-urlencode description@${description} \
|
||||||
--data-urlencode full_description@${fullDescription} \
|
--data-urlencode full_description@${fullDescription}
|
||||||
"https://hub.docker.com/v2/repositories/$DOCKER_REPO/postgrest/"
|
|
||||||
)"
|
|
||||||
|
|
||||||
[ "$responseCode" -eq 200 ]
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
in
|
in
|
||||||
buildToolbox
|
buildToolbox
|
||||||
{
|
{
|
||||||
name = "postgrest-release";
|
name = "postgrest-release";
|
||||||
tools = [ github dockerLogin dockerHub dockerHubDescription ];
|
tools = [ dockerHubDescription ];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
[](https://www.patreon.com/postgrest)
|
[](https://www.patreon.com/postgrest)
|
||||||
[](https://www.paypal.me/postgrest)
|
[](https://www.paypal.me/postgrest)
|
||||||
[](http://postgrest.org)
|
[](http://postgrest.org)
|
||||||
[](https://circleci.com/gh/PostgREST/postgrest/tree/main)
|
[](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)
|
||||||
|
|
||||||
PostgREST serves a fully RESTful API from any existing PostgreSQL database. It
|
PostgREST serves a fully RESTful API from any existing PostgreSQL database. It
|
||||||
provides a cleaner, more standards-compliant, faster API than you are likely to
|
provides a cleaner, more standards-compliant, faster API than you are likely to
|
||||||
|
|||||||
+19
-5
@@ -1,11 +1,14 @@
|
|||||||
{ black
|
{ actionlint
|
||||||
|
, black
|
||||||
, buildToolbox
|
, buildToolbox
|
||||||
, checkedShellScript
|
, checkedShellScript
|
||||||
, git
|
, git
|
||||||
, hlint
|
, hlint
|
||||||
|
, hsie
|
||||||
, nixpkgs-fmt
|
, nixpkgs-fmt
|
||||||
, shellcheck
|
, shellcheck
|
||||||
, silver-searcher
|
, silver-searcher
|
||||||
|
, statix
|
||||||
, stylish-haskell
|
, stylish-haskell
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -18,6 +21,7 @@ let
|
|||||||
}
|
}
|
||||||
''
|
''
|
||||||
# Format Nix files
|
# Format Nix files
|
||||||
|
${statix}/bin/statix fix
|
||||||
${nixpkgs-fmt}/bin/nixpkgs-fmt . > /dev/null 2> /dev/null
|
${nixpkgs-fmt}/bin/nixpkgs-fmt . > /dev/null 2> /dev/null
|
||||||
|
|
||||||
# Format Haskell files
|
# Format Haskell files
|
||||||
@@ -47,17 +51,27 @@ let
|
|||||||
checkedShellScript
|
checkedShellScript
|
||||||
{
|
{
|
||||||
name = "postgrest-lint";
|
name = "postgrest-lint";
|
||||||
docs = "Lint all Haskell files and bash scripts.";
|
docs = "Lint all Haskell files, bash scripts and github workflows.";
|
||||||
inRootDir = true;
|
inRootDir = true;
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
# Lint Haskell files
|
echo "Checking consistency of import aliases in Haskell code..."
|
||||||
|
${hsie} check-aliases main src
|
||||||
|
|
||||||
|
|
||||||
|
echo "Linting Haskell files..."
|
||||||
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
|
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
|
||||||
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \
|
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \
|
||||||
| xargs ${hlint}/bin/hlint -X QuasiQuotes -X NoPatternSynonyms
|
| xargs ${hlint}/bin/hlint -X QuasiQuotes -X NoPatternSynonyms
|
||||||
|
|
||||||
# Lint bash scripts
|
echo "Linting bash scripts..."
|
||||||
${shellcheck}/bin/shellcheck test/create_test_db test/memory-tests.sh
|
${shellcheck}/bin/shellcheck \
|
||||||
|
.github/get_cirrusci_freebsd \
|
||||||
|
.github/release \
|
||||||
|
test/with_tmp_db
|
||||||
|
|
||||||
|
echo "Linting workflows..."
|
||||||
|
${actionlint}/bin/actionlint
|
||||||
'';
|
'';
|
||||||
|
|
||||||
in
|
in
|
||||||
|
|||||||
+46
-9
@@ -24,7 +24,37 @@ let
|
|||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
${withTools.latest} ${cabal-install}/bin/cabal v2-test ${devCabalOptions}
|
${withTools.latest} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec
|
||||||
|
'';
|
||||||
|
|
||||||
|
testQuerycost =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-test-querycost";
|
||||||
|
docs = "Run the Haskell test suite for query costs";
|
||||||
|
inRootDir = true;
|
||||||
|
withEnv = postgrest.env;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${withTools.latest} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:querycost
|
||||||
|
'';
|
||||||
|
|
||||||
|
testDoctests =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-test-doctests";
|
||||||
|
docs = "Run the Haskell doctest test suite";
|
||||||
|
inRootDir = true;
|
||||||
|
withEnv = postgrest.env;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
# For unknown reasons, doctests uses the wrong GHC package database outside
|
||||||
|
# nix-shell and fails, so we set the package path explicitly
|
||||||
|
#ghcWithPackages="$(cat ${postgrest.env})"
|
||||||
|
#ghcVersion="$(ls "$ghcWithPackages/lib")"
|
||||||
|
#export GHC_PACKAGE_PATH="$ghcWithPackages/lib/$ghcVersion/package.conf.d/"
|
||||||
|
|
||||||
|
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:doctests
|
||||||
'';
|
'';
|
||||||
|
|
||||||
testSpecIdempotence =
|
testSpecIdempotence =
|
||||||
@@ -37,8 +67,8 @@ let
|
|||||||
}
|
}
|
||||||
''
|
''
|
||||||
${withTools.latest} ${runtimeShell} -c " \
|
${withTools.latest} ${runtimeShell} -c " \
|
||||||
${cabal-install}/bin/cabal v2-test ${devCabalOptions} && \
|
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec && \
|
||||||
${cabal-install}/bin/cabal v2-test ${devCabalOptions}"
|
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
ioTestPython =
|
ioTestPython =
|
||||||
@@ -73,10 +103,9 @@ let
|
|||||||
docs = "Dump the loaded schema's DbStructure as a yaml file.";
|
docs = "Dump the loaded schema's DbStructure as a yaml file.";
|
||||||
inRootDir = true;
|
inRootDir = true;
|
||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
|
withPath = [ jq ];
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
export PATH="${jq}/bin:$PATH"
|
|
||||||
|
|
||||||
${withTools.latest} \
|
${withTools.latest} \
|
||||||
${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
|
${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
|
||||||
postgrest --dump-schema \
|
postgrest --dump-schema \
|
||||||
@@ -102,18 +131,24 @@ let
|
|||||||
rm -rf coverage/*
|
rm -rf coverage/*
|
||||||
|
|
||||||
# build once before running all the tests
|
# build once before running all the tests
|
||||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec test:spec-querycost
|
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec test:querycost
|
||||||
|
|
||||||
# collect all tests
|
# collect all tests
|
||||||
HPCTIXFILE="$tmpdir"/io.tix \
|
HPCTIXFILE="$tmpdir"/io.tix \
|
||||||
${withTools.latest} ${cabal-install}/bin/cabal v2-exec ${devCabalOptions} \
|
${withTools.latest} ${cabal-install}/bin/cabal v2-exec ${devCabalOptions} \
|
||||||
${ioTestPython}/bin/pytest -- -v test/io-tests
|
${ioTestPython}/bin/pytest -- -v test/io-tests
|
||||||
|
|
||||||
HPCTIXFILE="$tmpdir"/spec.tix \
|
HPCTIXFILE="$tmpdir"/spec.tix \
|
||||||
${withTools.latest} ${cabal-install}/bin/cabal v2-test ${devCabalOptions}
|
${withTools.latest} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec
|
||||||
|
|
||||||
|
HPCTIXFILE="$tmpdir"/querycost.tix \
|
||||||
|
${withTools.latest} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:querycost
|
||||||
|
|
||||||
|
# Note: No coverage for doctests, as doctests leverage GHCi and GHCi does not support hpc
|
||||||
|
|
||||||
# collect all the tix files
|
# collect all the tix files
|
||||||
${ghc}/bin/hpc sum --union --exclude=Paths_postgrest --output="$tmpdir"/tests.tix "$tmpdir"/io*.tix "$tmpdir"/spec.tix
|
${ghc}/bin/hpc sum --union --exclude=Paths_postgrest --output="$tmpdir"/tests.tix \
|
||||||
|
"$tmpdir"/io*.tix "$tmpdir"/spec.tix "$tmpdir"/querycost.tix
|
||||||
|
|
||||||
# prepare the overlay
|
# prepare the overlay
|
||||||
${ghc}/bin/hpc overlay --output="$tmpdir"/overlay.tix test/coverage.overlay
|
${ghc}/bin/hpc overlay --output="$tmpdir"/overlay.tix test/coverage.overlay
|
||||||
@@ -164,6 +199,8 @@ buildToolbox
|
|||||||
tools =
|
tools =
|
||||||
[
|
[
|
||||||
testSpec
|
testSpec
|
||||||
|
testQuerycost
|
||||||
|
testDoctests
|
||||||
testSpecIdempotence
|
testSpecIdempotence
|
||||||
testIO
|
testIO
|
||||||
dumpSchema
|
dumpSchema
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ let
|
|||||||
addCommandCompletion = true;
|
addCommandCompletion = true;
|
||||||
inRootDir = true;
|
inRootDir = true;
|
||||||
redirectTixFiles = false;
|
redirectTixFiles = false;
|
||||||
|
withPath = [ postgresql ];
|
||||||
withTmpDir = true;
|
withTmpDir = true;
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
@@ -32,8 +33,7 @@ let
|
|||||||
if test -v PGRST_DB_URI; then
|
if test -v PGRST_DB_URI; then
|
||||||
exec "$@"
|
exec "$@"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
export PATH=${postgresql}/bin:"$PATH"
|
|
||||||
setuplog="$tmpdir/setup.log"
|
setuplog="$tmpdir/setup.log"
|
||||||
|
|
||||||
log () {
|
log () {
|
||||||
@@ -60,18 +60,13 @@ let
|
|||||||
|
|
||||||
log "Starting the database cluster..."
|
log "Starting the database cluster..."
|
||||||
# Instead of listening on a local port, we will listen on a unix domain socket.
|
# Instead of listening on a local port, we will listen on a unix domain socket.
|
||||||
pg_ctl -l "$tmpdir/db.log" start -o "-F -c listen_addresses=\"\" -k $PGHOST" \
|
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -k $PGHOST" \
|
||||||
>> "$setuplog"
|
>> "$setuplog"
|
||||||
|
|
||||||
log "Waiting for the database cluster to be ready..."
|
|
||||||
# Waiting is required for older versions of Postgres (< 10).
|
|
||||||
until pg_isready >> "$setuplog"; do
|
|
||||||
sleep 0.1
|
|
||||||
done
|
|
||||||
|
|
||||||
stop () {
|
stop () {
|
||||||
log "Stopping the database cluster..."
|
log "Stopping the database cluster..."
|
||||||
pg_ctl stop -m i >> "$setuplog"
|
pg_ctl stop -m i >> "$setuplog"
|
||||||
|
rm -rf "$tmpdir/db"
|
||||||
}
|
}
|
||||||
trap stop EXIT
|
trap stop EXIT
|
||||||
|
|
||||||
|
|||||||
+26
-9
@@ -1,5 +1,5 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 8.0.0
|
version: 9.0.0
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for the tables and views, supporting all HTTP verbs that security
|
for the tables and views, supporting all HTTP verbs that security
|
||||||
@@ -65,7 +65,7 @@ library
|
|||||||
PostgREST.Version
|
PostgREST.Version
|
||||||
PostgREST.Workers
|
PostgREST.Workers
|
||||||
other-modules: Paths_postgrest
|
other-modules: Paths_postgrest
|
||||||
build-depends: base >= 4.9 && < 4.15
|
build-depends: base >= 4.9 && < 4.16
|
||||||
, HTTP >= 4000.3.7 && < 4000.4
|
, HTTP >= 4000.3.7 && < 4000.4
|
||||||
, Ranged-sets >= 0.3 && < 0.5
|
, Ranged-sets >= 0.3 && < 0.5
|
||||||
, aeson >= 1.4.7 && < 1.6
|
, aeson >= 1.4.7 && < 1.6
|
||||||
@@ -101,7 +101,7 @@ library
|
|||||||
, parsec >= 3.1.11 && < 3.2
|
, parsec >= 3.1.11 && < 3.2
|
||||||
, protolude >= 0.3 && < 0.4
|
, protolude >= 0.3 && < 0.4
|
||||||
, regex-tdfa >= 1.2.2 && < 1.4
|
, regex-tdfa >= 1.2.2 && < 1.4
|
||||||
, retry >= 0.7.4 && < 0.9
|
, retry >= 0.7.4 && < 0.10
|
||||||
, scientific >= 0.3.4 && < 0.4
|
, scientific >= 0.3.4 && < 0.4
|
||||||
, swagger2 >= 2.4 && < 2.7
|
, swagger2 >= 2.4 && < 2.7
|
||||||
, text >= 1.2.2 && < 1.3
|
, text >= 1.2.2 && < 1.3
|
||||||
@@ -143,7 +143,7 @@ executable postgrest
|
|||||||
NoImplicitPrelude
|
NoImplicitPrelude
|
||||||
hs-source-dirs: main
|
hs-source-dirs: main
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
build-depends: base >= 4.9 && < 4.15
|
build-depends: base >= 4.9 && < 4.16
|
||||||
, containers >= 0.5.7 && < 0.7
|
, containers >= 0.5.7 && < 0.7
|
||||||
, postgrest
|
, postgrest
|
||||||
, protolude >= 0.3 && < 0.4
|
, protolude >= 0.3 && < 0.4
|
||||||
@@ -176,11 +176,13 @@ test-suite spec
|
|||||||
Feature.DeleteSpec
|
Feature.DeleteSpec
|
||||||
Feature.DisabledOpenApiSpec
|
Feature.DisabledOpenApiSpec
|
||||||
Feature.EmbedDisambiguationSpec
|
Feature.EmbedDisambiguationSpec
|
||||||
|
Feature.EmbedInnerJoinSpec
|
||||||
Feature.ExtraSearchPathSpec
|
Feature.ExtraSearchPathSpec
|
||||||
Feature.HtmlRawOutputSpec
|
Feature.HtmlRawOutputSpec
|
||||||
Feature.InsertSpec
|
Feature.InsertSpec
|
||||||
Feature.IgnorePrivOpenApiSpec
|
Feature.IgnorePrivOpenApiSpec
|
||||||
Feature.JsonOperatorSpec
|
Feature.JsonOperatorSpec
|
||||||
|
Feature.LegacyGucsSpec
|
||||||
Feature.MultipleSchemaSpec
|
Feature.MultipleSchemaSpec
|
||||||
Feature.NoJwtSpec
|
Feature.NoJwtSpec
|
||||||
Feature.NonexistentSchemaSpec
|
Feature.NonexistentSchemaSpec
|
||||||
@@ -201,7 +203,7 @@ test-suite spec
|
|||||||
Feature.UpsertSpec
|
Feature.UpsertSpec
|
||||||
SpecHelper
|
SpecHelper
|
||||||
TestTypes
|
TestTypes
|
||||||
build-depends: base >= 4.9 && < 4.15
|
build-depends: base >= 4.9 && < 4.16
|
||||||
, aeson >= 1.4.7 && < 1.6
|
, aeson >= 1.4.7 && < 1.6
|
||||||
, aeson-qq >= 0.8.1 && < 0.9
|
, aeson-qq >= 0.8.1 && < 0.9
|
||||||
, async >= 2.1.1 && < 2.3
|
, async >= 2.1.1 && < 2.3
|
||||||
@@ -216,7 +218,7 @@ test-suite spec
|
|||||||
, hasql-pool >= 0.5 && < 0.6
|
, hasql-pool >= 0.5 && < 0.6
|
||||||
, hasql-transaction >= 1.0.1 && < 1.1
|
, hasql-transaction >= 1.0.1 && < 1.1
|
||||||
, heredoc >= 0.2 && < 0.3
|
, heredoc >= 0.2 && < 0.3
|
||||||
, hspec >= 2.3 && < 2.8
|
, hspec >= 2.3 && < 2.9
|
||||||
, hspec-wai >= 0.10 && < 0.12
|
, hspec-wai >= 0.10 && < 0.12
|
||||||
, hspec-wai-json >= 0.10 && < 0.12
|
, hspec-wai-json >= 0.10 && < 0.12
|
||||||
, http-types >= 0.12.3 && < 0.13
|
, http-types >= 0.12.3 && < 0.13
|
||||||
@@ -236,7 +238,7 @@ test-suite spec
|
|||||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||||
-fno-warn-missing-signatures
|
-fno-warn-missing-signatures
|
||||||
|
|
||||||
test-suite spec-querycost
|
test-suite querycost
|
||||||
type: exitcode-stdio-1.0
|
type: exitcode-stdio-1.0
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
default-extensions: OverloadedStrings
|
default-extensions: OverloadedStrings
|
||||||
@@ -245,7 +247,7 @@ test-suite spec-querycost
|
|||||||
hs-source-dirs: test
|
hs-source-dirs: test
|
||||||
main-is: QueryCost.hs
|
main-is: QueryCost.hs
|
||||||
other-modules: SpecHelper
|
other-modules: SpecHelper
|
||||||
build-depends: base >= 4.9 && < 4.15
|
build-depends: base >= 4.9 && < 4.16
|
||||||
, aeson >= 1.4.7 && < 1.6
|
, aeson >= 1.4.7 && < 1.6
|
||||||
, aeson-qq >= 0.8.1 && < 0.9
|
, aeson-qq >= 0.8.1 && < 0.9
|
||||||
, async >= 2.1.1 && < 2.3
|
, async >= 2.1.1 && < 2.3
|
||||||
@@ -261,7 +263,7 @@ test-suite spec-querycost
|
|||||||
, hasql-pool >= 0.5 && < 0.6
|
, hasql-pool >= 0.5 && < 0.6
|
||||||
, hasql-transaction >= 1.0.1 && < 1.1
|
, hasql-transaction >= 1.0.1 && < 1.1
|
||||||
, heredoc >= 0.2 && < 0.3
|
, heredoc >= 0.2 && < 0.3
|
||||||
, hspec >= 2.3 && < 2.8
|
, hspec >= 2.3 && < 2.9
|
||||||
, hspec-wai >= 0.10 && < 0.12
|
, hspec-wai >= 0.10 && < 0.12
|
||||||
, hspec-wai-json >= 0.10 && < 0.12
|
, hspec-wai-json >= 0.10 && < 0.12
|
||||||
, http-types >= 0.12.3 && < 0.13
|
, http-types >= 0.12.3 && < 0.13
|
||||||
@@ -279,3 +281,18 @@ test-suite spec-querycost
|
|||||||
, wai-extra >= 3.0.19 && < 3.2
|
, wai-extra >= 3.0.19 && < 3.2
|
||||||
ghc-options: -O0 -Werror -Wall -fwarn-identities
|
ghc-options: -O0 -Werror -Wall -fwarn-identities
|
||||||
-fno-spec-constr -optP-Wno-nonportable-include-path
|
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||||
|
|
||||||
|
test-suite doctests
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
default-language: Haskell2010
|
||||||
|
default-extensions: OverloadedStrings
|
||||||
|
NoImplicitPrelude
|
||||||
|
hs-source-dirs: test/doctests
|
||||||
|
main-is: Main.hs
|
||||||
|
build-depends: base >= 4.9 && < 4.16
|
||||||
|
, doctest >= 0.8
|
||||||
|
, postgrest
|
||||||
|
, pretty-simple
|
||||||
|
, protolude >= 0.3 && < 0.4
|
||||||
|
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
|
||||||
|
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||||
|
|||||||
@@ -2,26 +2,20 @@
|
|||||||
# disabled by default. You can activate them by passing arguments to nix-shell,
|
# disabled by default. You can activate them by passing arguments to nix-shell,
|
||||||
# e.g.:
|
# e.g.:
|
||||||
#
|
#
|
||||||
# nix-shell --arg release true
|
# nix-shell --arg docker true
|
||||||
#
|
|
||||||
# This will provide you with a shell where the `postgrest-release-*` scripts
|
|
||||||
# are available.
|
|
||||||
#
|
#
|
||||||
# We highly recommend that use the PostgREST binary cache by installing cachix
|
# We highly recommend that use the PostgREST binary cache by installing cachix
|
||||||
# (https://app.cachix.org/) and running `cachix use postgrest`.
|
# (https://app.cachix.org/) and running `cachix use postgrest`.
|
||||||
{ docker ? false
|
{ docker ? false
|
||||||
, memory ? false
|
, memory ? false
|
||||||
, release ? false
|
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
postgrest =
|
postgrest =
|
||||||
import ./default.nix;
|
import ./default.nix;
|
||||||
|
|
||||||
pkgs =
|
inherit (postgrest) pkgs;
|
||||||
postgrest.pkgs;
|
|
||||||
|
|
||||||
lib =
|
inherit (pkgs) lib;
|
||||||
pkgs.lib;
|
|
||||||
|
|
||||||
toolboxes =
|
toolboxes =
|
||||||
[
|
[
|
||||||
@@ -31,10 +25,10 @@ let
|
|||||||
postgrest.style
|
postgrest.style
|
||||||
postgrest.tests
|
postgrest.tests
|
||||||
postgrest.withTools
|
postgrest.withTools
|
||||||
|
postgrest.release
|
||||||
]
|
]
|
||||||
++ lib.optional docker postgrest.docker
|
++ lib.optional docker postgrest.docker
|
||||||
++ lib.optional memory postgrest.memory
|
++ lib.optional memory postgrest.memory;
|
||||||
++ lib.optional release postgrest.release;
|
|
||||||
|
|
||||||
in
|
in
|
||||||
lib.overrideDerivation postgrest.env (
|
lib.overrideDerivation postgrest.env (
|
||||||
|
|||||||
+42
-47
@@ -26,10 +26,10 @@ import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
|||||||
setServerName)
|
setServerName)
|
||||||
import System.Posix.Types (FileMode)
|
import System.Posix.Types (FileMode)
|
||||||
|
|
||||||
import qualified Data.ByteString.Char8 as BS8
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.HashMap.Strict as Map
|
import qualified Data.HashMap.Strict as M
|
||||||
import qualified Data.Set as Set
|
import qualified Data.Set as S
|
||||||
import qualified Hasql.DynamicStatements.Snippet as SQL
|
import qualified Hasql.DynamicStatements.Snippet as SQL
|
||||||
import qualified Hasql.Pool as SQL
|
import qualified Hasql.Pool as SQL
|
||||||
import qualified Hasql.Transaction as SQL
|
import qualified Hasql.Transaction as SQL
|
||||||
@@ -76,7 +76,8 @@ import PostgREST.Request.ApiRequest (Action (..),
|
|||||||
Target (..))
|
Target (..))
|
||||||
import PostgREST.Request.Preferences (PreferCount (..),
|
import PostgREST.Request.Preferences (PreferCount (..),
|
||||||
PreferParameters (..),
|
PreferParameters (..),
|
||||||
PreferRepresentation (..))
|
PreferRepresentation (..),
|
||||||
|
toAppliedHeader)
|
||||||
import PostgREST.Request.Types (ReadRequest, fstFieldNames)
|
import PostgREST.Request.Types (ReadRequest, fstFieldNames)
|
||||||
import PostgREST.Version (prettyVersion)
|
import PostgREST.Version (prettyVersion)
|
||||||
import PostgREST.Workers (connectionWorker, listener)
|
import PostgREST.Workers (connectionWorker, listener)
|
||||||
@@ -84,8 +85,7 @@ import PostgREST.Workers (connectionWorker, listener)
|
|||||||
import qualified PostgREST.ContentType as ContentType
|
import qualified PostgREST.ContentType as ContentType
|
||||||
import qualified PostgREST.DbStructure.Proc as Proc
|
import qualified PostgREST.DbStructure.Proc as Proc
|
||||||
|
|
||||||
import Protolude hiding (Handler, toS)
|
import Protolude hiding (Handler)
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
|
|
||||||
data RequestContext = RequestContext
|
data RequestContext = RequestContext
|
||||||
@@ -133,7 +133,7 @@ serverSettings AppConfig{..} =
|
|||||||
defaultSettings
|
defaultSettings
|
||||||
& setHost (fromString $ toS configServerHost)
|
& setHost (fromString $ toS configServerHost)
|
||||||
& setPort configServerPort
|
& setPort configServerPort
|
||||||
& setServerName (toS $ "postgrest/" <> prettyVersion)
|
& setServerName ("postgrest/" <> prettyVersion)
|
||||||
|
|
||||||
-- | PostgREST application
|
-- | PostgREST application
|
||||||
postgrest :: LogLevel -> AppState.AppState -> IO () -> Wai.Application
|
postgrest :: LogLevel -> AppState.AppState -> IO () -> Wai.Application
|
||||||
@@ -152,13 +152,19 @@ postgrest logLev appState connWorker =
|
|||||||
runExceptT $ postgrestResponse conf maybeDbStructure jsonDbS pgVer (AppState.getPool appState) time req
|
runExceptT $ postgrestResponse conf maybeDbStructure jsonDbS pgVer (AppState.getPool appState) time req
|
||||||
|
|
||||||
response <- either Error.errorResponseFor identity <$> eitherResponse
|
response <- either Error.errorResponseFor identity <$> eitherResponse
|
||||||
|
|
||||||
-- Launch the connWorker when the connection is down. The postgrest
|
-- Launch the connWorker when the connection is down. The postgrest
|
||||||
-- function can respond successfully (with a stale schema cache) before
|
-- function can respond successfully (with a stale schema cache) before
|
||||||
-- the connWorker is done.
|
-- the connWorker is done.
|
||||||
when (Wai.responseStatus response == HTTP.status503) connWorker
|
let isPGAway = Wai.responseStatus response == HTTP.status503
|
||||||
|
when isPGAway connWorker
|
||||||
|
resp <- addRetryHint isPGAway appState response
|
||||||
|
respond resp
|
||||||
|
|
||||||
respond response
|
addRetryHint :: Bool -> AppState -> Wai.Response -> IO Wai.Response
|
||||||
|
addRetryHint shouldAdd appState response = do
|
||||||
|
delay <- AppState.getRetryNextIn appState
|
||||||
|
let h = ("Retry-After", BS.pack $ show delay)
|
||||||
|
return $ Wai.mapResponseHeaders (\hs -> if shouldAdd then h:hs else hs) response
|
||||||
|
|
||||||
postgrestResponse
|
postgrestResponse
|
||||||
:: AppConfig
|
:: AppConfig
|
||||||
@@ -184,7 +190,7 @@ postgrestResponse conf maybeDbStructure jsonDbS pgVer pool time req = do
|
|||||||
ApiRequest.userApiRequest conf dbStructure req body
|
ApiRequest.userApiRequest conf dbStructure req body
|
||||||
|
|
||||||
-- The JWT must be checked before touching the db
|
-- The JWT must be checked before touching the db
|
||||||
jwtClaims <- Auth.jwtClaims conf (toS iJWT) time
|
jwtClaims <- Auth.jwtClaims conf (toUtf8Lazy iJWT) time
|
||||||
|
|
||||||
let
|
let
|
||||||
handleReq apiReq =
|
handleReq apiReq =
|
||||||
@@ -192,7 +198,7 @@ postgrestResponse conf maybeDbStructure jsonDbS pgVer pool time req = do
|
|||||||
|
|
||||||
runDbHandler pool (txMode apiRequest) jwtClaims (configDbPreparedStatements conf) .
|
runDbHandler pool (txMode apiRequest) jwtClaims (configDbPreparedStatements conf) .
|
||||||
Middleware.optionalRollback conf apiRequest $
|
Middleware.optionalRollback conf apiRequest $
|
||||||
Middleware.runPgLocals conf jwtClaims handleReq apiRequest jsonDbS
|
Middleware.runPgLocals conf jwtClaims handleReq apiRequest jsonDbS pgVer
|
||||||
|
|
||||||
runDbHandler :: SQL.Pool -> SQL.Mode -> Auth.JWTClaims -> Bool -> DbHandler a -> Handler IO a
|
runDbHandler :: SQL.Pool -> SQL.Mode -> Auth.JWTClaims -> Bool -> DbHandler a -> Handler IO a
|
||||||
runDbHandler pool mode jwtClaims prepared handler = do
|
runDbHandler pool mode jwtClaims prepared handler = do
|
||||||
@@ -252,7 +258,6 @@ handleRead headersOnly identifier context@RequestContext{..} = do
|
|||||||
(shouldCount iPreferCount)
|
(shouldCount iPreferCount)
|
||||||
(iAcceptContentType == CTTextCSV)
|
(iAcceptContentType == CTTextCSV)
|
||||||
bField
|
bField
|
||||||
ctxPgVersion
|
|
||||||
configDbPreparedStatements
|
configDbPreparedStatements
|
||||||
|
|
||||||
total <- readTotal ctxConfig ctxApiRequest tableTotal countQuery
|
total <- readTotal ctxConfig ctxApiRequest tableTotal countQuery
|
||||||
@@ -264,14 +269,14 @@ handleRead headersOnly identifier context@RequestContext{..} = do
|
|||||||
[ contentRange
|
[ contentRange
|
||||||
, ( "Content-Location"
|
, ( "Content-Location"
|
||||||
, "/"
|
, "/"
|
||||||
<> toS (qiName identifier)
|
<> toUtf8 (qiName identifier)
|
||||||
<> if BS8.null iCanonicalQS then mempty else "?" <> toS iCanonicalQS
|
<> if BS.null iCanonicalQS then mempty else "?" <> iCanonicalQS
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
++ contentTypeHeaders context
|
++ contentTypeHeaders context
|
||||||
|
|
||||||
failNotSingular iAcceptContentType queryTotal . response status headers $
|
failNotSingular iAcceptContentType queryTotal . response status headers $
|
||||||
if headersOnly then mempty else toS body
|
if headersOnly then mempty else LBS.fromStrict body
|
||||||
|
|
||||||
readTotal :: AppConfig -> ApiRequest -> Maybe Int64 -> SQL.Snippet -> DbHandler (Maybe Int64)
|
readTotal :: AppConfig -> ApiRequest -> Maybe Int64 -> SQL.Snippet -> DbHandler (Maybe Int64)
|
||||||
readTotal AppConfig{..} ApiRequest{..} tableTotal countQuery =
|
readTotal AppConfig{..} ApiRequest{..} tableTotal countQuery =
|
||||||
@@ -308,7 +313,7 @@ handleCreate identifier@QualifiedIdentifier{..} context@RequestContext{..} = do
|
|||||||
Just
|
Just
|
||||||
( HTTP.hLocation
|
( HTTP.hLocation
|
||||||
, "/"
|
, "/"
|
||||||
<> toS qiName
|
<> toUtf8 qiName
|
||||||
<> HTTP.renderSimpleQuery True (splitKeyValue <$> resFields)
|
<> HTTP.renderSimpleQuery True (splitKeyValue <$> resFields)
|
||||||
)
|
)
|
||||||
, Just . RangeQuery.contentRangeH 1 0 $
|
, Just . RangeQuery.contentRangeH 1 0 $
|
||||||
@@ -316,12 +321,12 @@ handleCreate identifier@QualifiedIdentifier{..} context@RequestContext{..} = do
|
|||||||
, if null pkCols && isNothing iOnConflict then
|
, if null pkCols && isNothing iOnConflict then
|
||||||
Nothing
|
Nothing
|
||||||
else
|
else
|
||||||
(\x -> ("Preference-Applied", BS8.pack $ show x)) <$> iPreferResolution
|
toAppliedHeader <$> iPreferResolution
|
||||||
]
|
]
|
||||||
|
|
||||||
failNotSingular iAcceptContentType resQueryTotal $
|
failNotSingular iAcceptContentType resQueryTotal $
|
||||||
if iPreferRepresentation == Full then
|
if iPreferRepresentation == Full then
|
||||||
response HTTP.status201 (headers ++ contentTypeHeaders context) (toS resBody)
|
response HTTP.status201 (headers ++ contentTypeHeaders context) (LBS.fromStrict resBody)
|
||||||
else
|
else
|
||||||
response HTTP.status201 headers mempty
|
response HTTP.status201 headers mempty
|
||||||
|
|
||||||
@@ -332,7 +337,7 @@ handleUpdate identifier context@(RequestContext _ _ ApiRequest{..} _) = do
|
|||||||
let
|
let
|
||||||
response = gucResponse resGucStatus resGucHeaders
|
response = gucResponse resGucStatus resGucHeaders
|
||||||
fullRepr = iPreferRepresentation == Full
|
fullRepr = iPreferRepresentation == Full
|
||||||
updateIsNoOp = Set.null iColumns
|
updateIsNoOp = S.null iColumns
|
||||||
status
|
status
|
||||||
| resQueryTotal == 0 && not updateIsNoOp = HTTP.status404
|
| resQueryTotal == 0 && not updateIsNoOp = HTTP.status404
|
||||||
| fullRepr = HTTP.status200
|
| fullRepr = HTTP.status200
|
||||||
@@ -343,7 +348,7 @@ handleUpdate identifier context@(RequestContext _ _ ApiRequest{..} _) = do
|
|||||||
|
|
||||||
failNotSingular iAcceptContentType resQueryTotal $
|
failNotSingular iAcceptContentType resQueryTotal $
|
||||||
if fullRepr then
|
if fullRepr then
|
||||||
response status (contentTypeHeaders context ++ [contentRangeHeader]) (toS resBody)
|
response status (contentTypeHeaders context ++ [contentRangeHeader]) (LBS.fromStrict resBody)
|
||||||
else
|
else
|
||||||
response status [contentRangeHeader] mempty
|
response status [contentRangeHeader] mempty
|
||||||
|
|
||||||
@@ -367,7 +372,7 @@ handleSingleUpsert identifier context@(RequestContext _ _ ApiRequest{..} _) = do
|
|||||||
|
|
||||||
return $
|
return $
|
||||||
if iPreferRepresentation == Full then
|
if iPreferRepresentation == Full then
|
||||||
response HTTP.status200 (contentTypeHeaders context) (toS resBody)
|
response HTTP.status200 (contentTypeHeaders context) (LBS.fromStrict resBody)
|
||||||
else
|
else
|
||||||
response HTTP.status204 (contentTypeHeaders context) mempty
|
response HTTP.status204 (contentTypeHeaders context) mempty
|
||||||
|
|
||||||
@@ -385,7 +390,7 @@ handleDelete identifier context@(RequestContext _ _ ApiRequest{..} _) = do
|
|||||||
if iPreferRepresentation == Full then
|
if iPreferRepresentation == Full then
|
||||||
response HTTP.status200
|
response HTTP.status200
|
||||||
(contentTypeHeaders context ++ [contentRangeHeader])
|
(contentTypeHeaders context ++ [contentRangeHeader])
|
||||||
(toS resBody)
|
(LBS.fromStrict resBody)
|
||||||
else
|
else
|
||||||
response HTTP.status204 [contentRangeHeader] mempty
|
response HTTP.status204 [contentRangeHeader] mempty
|
||||||
|
|
||||||
@@ -400,7 +405,7 @@ handleInfo identifier RequestContext{..} =
|
|||||||
allOrigins = ("Access-Control-Allow-Origin", "*")
|
allOrigins = ("Access-Control-Allow-Origin", "*")
|
||||||
allowH table =
|
allowH table =
|
||||||
( HTTP.hAllow
|
( HTTP.hAllow
|
||||||
, BS8.intercalate "," $
|
, BS.intercalate "," $
|
||||||
["OPTIONS,GET,HEAD"]
|
["OPTIONS,GET,HEAD"]
|
||||||
++ ["POST" | tableInsertable table]
|
++ ["POST" | tableInsertable table]
|
||||||
++ ["PUT" | tableInsertable table && tableUpdatable table && hasPK]
|
++ ["PUT" | tableInsertable table && tableUpdatable table && hasPK]
|
||||||
@@ -423,25 +428,17 @@ handleInvoke invMethod proc context@RequestContext{..} = do
|
|||||||
(pdSchema proc)
|
(pdSchema proc)
|
||||||
(fromMaybe (pdName proc) $ Proc.procTableName proc)
|
(fromMaybe (pdName proc) $ Proc.procTableName proc)
|
||||||
|
|
||||||
returnsSingle (ApiRequest.TargetProc target _) = Proc.procReturnsSingle target
|
|
||||||
returnsSingle _ = False
|
|
||||||
|
|
||||||
req <- readRequest identifier context
|
req <- readRequest identifier context
|
||||||
bField <- binaryField context req
|
bField <- binaryField context req
|
||||||
|
|
||||||
|
let callReq = ReqBuilder.callRequest proc ctxApiRequest req
|
||||||
|
|
||||||
(tableTotal, queryTotal, body, gucHeaders, gucStatus) <-
|
(tableTotal, queryTotal, body, gucHeaders, gucStatus) <-
|
||||||
lift . SQL.statement mempty $
|
lift . SQL.statement mempty $
|
||||||
Statements.callProcStatement
|
Statements.callProcStatement
|
||||||
(returnsScalar iTarget)
|
(Proc.procReturnsScalar proc)
|
||||||
(returnsSingle iTarget)
|
(Proc.procReturnsSingle proc)
|
||||||
(QueryBuilder.requestToCallProcQuery
|
(QueryBuilder.requestToCallProcQuery callReq)
|
||||||
(QualifiedIdentifier (pdSchema proc) (pdName proc))
|
|
||||||
(Proc.specifiedProcArgs iColumns proc)
|
|
||||||
iPayload
|
|
||||||
(returnsScalar iTarget)
|
|
||||||
iPreferParameters
|
|
||||||
(ReqBuilder.returningCols req [])
|
|
||||||
)
|
|
||||||
(QueryBuilder.readRequestToQuery req)
|
(QueryBuilder.readRequestToQuery req)
|
||||||
(QueryBuilder.readRequestToCountQuery req)
|
(QueryBuilder.readRequestToCountQuery req)
|
||||||
(shouldCount iPreferCount)
|
(shouldCount iPreferCount)
|
||||||
@@ -449,7 +446,6 @@ handleInvoke invMethod proc context@RequestContext{..} = do
|
|||||||
(iAcceptContentType == CTTextCSV)
|
(iAcceptContentType == CTTextCSV)
|
||||||
(iPreferParameters == Just MultipleObjects)
|
(iPreferParameters == Just MultipleObjects)
|
||||||
bField
|
bField
|
||||||
ctxPgVersion
|
|
||||||
(configDbPreparedStatements ctxConfig)
|
(configDbPreparedStatements ctxConfig)
|
||||||
|
|
||||||
response <- liftEither $ gucResponse <$> gucStatus <*> gucHeaders
|
response <- liftEither $ gucResponse <$> gucStatus <*> gucHeaders
|
||||||
@@ -461,21 +457,21 @@ handleInvoke invMethod proc context@RequestContext{..} = do
|
|||||||
failNotSingular iAcceptContentType queryTotal $
|
failNotSingular iAcceptContentType queryTotal $
|
||||||
response status
|
response status
|
||||||
(contentTypeHeaders context ++ [contentRange])
|
(contentTypeHeaders context ++ [contentRange])
|
||||||
(if invMethod == InvHead then mempty else toS body)
|
(if invMethod == InvHead then mempty else LBS.fromStrict body)
|
||||||
|
|
||||||
handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response
|
handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response
|
||||||
handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure apiRequest _) = do
|
handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure apiRequest ctxPgVersion) = do
|
||||||
body <-
|
body <-
|
||||||
lift $ case configOpenApiMode of
|
lift $ case configOpenApiMode of
|
||||||
OAFollowPriv ->
|
OAFollowPriv ->
|
||||||
OpenAPI.encode conf dbStructure
|
OpenAPI.encode conf dbStructure
|
||||||
<$> SQL.statement tSchema (DbStructure.accessibleTables configDbPreparedStatements)
|
<$> SQL.statement tSchema (DbStructure.accessibleTables ctxPgVersion configDbPreparedStatements)
|
||||||
<*> SQL.statement tSchema (DbStructure.accessibleProcs configDbPreparedStatements)
|
<*> SQL.statement tSchema (DbStructure.accessibleProcs configDbPreparedStatements)
|
||||||
<*> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements)
|
<*> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements)
|
||||||
OAIgnorePriv ->
|
OAIgnorePriv ->
|
||||||
OpenAPI.encode conf dbStructure
|
OpenAPI.encode conf dbStructure
|
||||||
(filter (\x -> tableSchema x == tSchema) $ DbStructure.dbTables dbStructure)
|
(filter (\x -> tableSchema x == tSchema) $ DbStructure.dbTables dbStructure)
|
||||||
(Map.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ DbStructure.dbProcs dbStructure)
|
(M.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ DbStructure.dbProcs dbStructure)
|
||||||
<$> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements)
|
<$> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements)
|
||||||
OADisabled ->
|
OADisabled ->
|
||||||
pure mempty
|
pure mempty
|
||||||
@@ -483,7 +479,7 @@ handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure
|
|||||||
return $
|
return $
|
||||||
Wai.responseLBS HTTP.status200
|
Wai.responseLBS HTTP.status200
|
||||||
(ContentType.toHeader CTOpenAPI : maybeToList (profileHeader apiRequest))
|
(ContentType.toHeader CTOpenAPI : maybeToList (profileHeader apiRequest))
|
||||||
(if headersOnly then mempty else toS body)
|
(if headersOnly then mempty else body)
|
||||||
|
|
||||||
txMode :: ApiRequest -> SQL.Mode
|
txMode :: ApiRequest -> SQL.Mode
|
||||||
txMode ApiRequest{..} =
|
txMode ApiRequest{..} =
|
||||||
@@ -534,7 +530,6 @@ writeQuery identifier@QualifiedIdentifier{..} isInsert pkCols context@RequestCon
|
|||||||
(iAcceptContentType ctxApiRequest == CTTextCSV)
|
(iAcceptContentType ctxApiRequest == CTTextCSV)
|
||||||
(iPreferRepresentation ctxApiRequest)
|
(iPreferRepresentation ctxApiRequest)
|
||||||
pkCols
|
pkCols
|
||||||
ctxPgVersion
|
|
||||||
(configDbPreparedStatements ctxConfig)
|
(configDbPreparedStatements ctxConfig)
|
||||||
|
|
||||||
liftEither $ WriteQueryResult queryTotal fields body <$> gucStatus <*> gucHeaders
|
liftEither $ WriteQueryResult queryTotal fields body <$> gucStatus <*> gucHeaders
|
||||||
@@ -607,10 +602,10 @@ rawContentTypes AppConfig{..} =
|
|||||||
|
|
||||||
profileHeader :: ApiRequest -> Maybe HTTP.Header
|
profileHeader :: ApiRequest -> Maybe HTTP.Header
|
||||||
profileHeader ApiRequest{..} =
|
profileHeader ApiRequest{..} =
|
||||||
(,) "Content-Profile" <$> (toS <$> iProfile)
|
(,) "Content-Profile" <$> (toUtf8 <$> iProfile)
|
||||||
|
|
||||||
splitKeyValue :: ByteString -> (ByteString, ByteString)
|
splitKeyValue :: ByteString -> (ByteString, ByteString)
|
||||||
splitKeyValue kv =
|
splitKeyValue kv =
|
||||||
(k, BS8.tail v)
|
(k, BS.tail v)
|
||||||
where
|
where
|
||||||
(k, v) = BS8.break (== '=') kv
|
(k, v) = BS.break (== '=') kv
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ module PostgREST.AppState
|
|||||||
, getPgVersion
|
, getPgVersion
|
||||||
, getPool
|
, getPool
|
||||||
, getTime
|
, getTime
|
||||||
|
, getRetryNextIn
|
||||||
, init
|
, init
|
||||||
, initWithPool
|
, initWithPool
|
||||||
, logWithZTime
|
, logWithZTime
|
||||||
@@ -18,12 +19,13 @@ module PostgREST.AppState
|
|||||||
, putIsWorkerOn
|
, putIsWorkerOn
|
||||||
, putJsonDbS
|
, putJsonDbS
|
||||||
, putPgVersion
|
, putPgVersion
|
||||||
|
, putRetryNextIn
|
||||||
, releasePool
|
, releasePool
|
||||||
, signalListener
|
, signalListener
|
||||||
, waitListener
|
, waitListener
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Hasql.Pool as P
|
import qualified Hasql.Pool as SQL
|
||||||
|
|
||||||
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
||||||
updateAction)
|
updateAction)
|
||||||
@@ -37,12 +39,11 @@ import PostgREST.Config (AppConfig (..))
|
|||||||
import PostgREST.Config.PgVersion (PgVersion (..), minimumPgVersion)
|
import PostgREST.Config.PgVersion (PgVersion (..), minimumPgVersion)
|
||||||
import PostgREST.DbStructure (DbStructure)
|
import PostgREST.DbStructure (DbStructure)
|
||||||
|
|
||||||
import Protolude hiding (toS)
|
import Protolude
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
|
|
||||||
data AppState = AppState
|
data AppState = AppState
|
||||||
{ statePool :: P.Pool -- | Connection pool, either a 'Connection' or a 'ConnectionError'
|
{ statePool :: SQL.Pool -- | Connection pool, either a 'Connection' or a 'ConnectionError'
|
||||||
, statePgVersion :: IORef PgVersion
|
, statePgVersion :: IORef PgVersion
|
||||||
-- | No schema cache at the start. Will be filled in by the connectionWorker
|
-- | No schema cache at the start. Will be filled in by the connectionWorker
|
||||||
, stateDbStructure :: IORef (Maybe DbStructure)
|
, stateDbStructure :: IORef (Maybe DbStructure)
|
||||||
@@ -60,6 +61,8 @@ data AppState = AppState
|
|||||||
, stateGetZTime :: IO ZonedTime
|
, stateGetZTime :: IO ZonedTime
|
||||||
-- | Used for killing the main thread in case a subthread fails
|
-- | Used for killing the main thread in case a subthread fails
|
||||||
, stateMainThreadId :: ThreadId
|
, stateMainThreadId :: ThreadId
|
||||||
|
-- | Keeps track of when the next retry for connecting to database is scheduled
|
||||||
|
, stateRetryNextIn :: IORef Int
|
||||||
}
|
}
|
||||||
|
|
||||||
init :: AppConfig -> IO AppState
|
init :: AppConfig -> IO AppState
|
||||||
@@ -67,7 +70,7 @@ init conf = do
|
|||||||
newPool <- initPool conf
|
newPool <- initPool conf
|
||||||
initWithPool newPool conf
|
initWithPool newPool conf
|
||||||
|
|
||||||
initWithPool :: P.Pool -> AppConfig -> IO AppState
|
initWithPool :: SQL.Pool -> AppConfig -> IO AppState
|
||||||
initWithPool newPool conf =
|
initWithPool newPool conf =
|
||||||
AppState newPool
|
AppState newPool
|
||||||
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
||||||
@@ -79,16 +82,17 @@ initWithPool newPool conf =
|
|||||||
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
|
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
|
||||||
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
||||||
<*> myThreadId
|
<*> myThreadId
|
||||||
|
<*> newIORef 0
|
||||||
|
|
||||||
initPool :: AppConfig -> IO P.Pool
|
initPool :: AppConfig -> IO SQL.Pool
|
||||||
initPool AppConfig{..} =
|
initPool AppConfig{..} =
|
||||||
P.acquire (configDbPoolSize, configDbPoolTimeout, toS configDbUri)
|
SQL.acquire (configDbPoolSize, configDbPoolTimeout, toUtf8 configDbUri)
|
||||||
|
|
||||||
getPool :: AppState -> P.Pool
|
getPool :: AppState -> SQL.Pool
|
||||||
getPool = statePool
|
getPool = statePool
|
||||||
|
|
||||||
releasePool :: AppState -> IO ()
|
releasePool :: AppState -> IO ()
|
||||||
releasePool AppState{..} = P.release statePool >> throwTo stateMainThreadId UserInterrupt
|
releasePool AppState{..} = SQL.release statePool >> throwTo stateMainThreadId UserInterrupt
|
||||||
|
|
||||||
getPgVersion :: AppState -> IO PgVersion
|
getPgVersion :: AppState -> IO PgVersion
|
||||||
getPgVersion = readIORef . statePgVersion
|
getPgVersion = readIORef . statePgVersion
|
||||||
@@ -115,6 +119,12 @@ getIsWorkerOn = readIORef . stateIsWorkerOn
|
|||||||
putIsWorkerOn :: AppState -> Bool -> IO ()
|
putIsWorkerOn :: AppState -> Bool -> IO ()
|
||||||
putIsWorkerOn = atomicWriteIORef . stateIsWorkerOn
|
putIsWorkerOn = atomicWriteIORef . stateIsWorkerOn
|
||||||
|
|
||||||
|
getRetryNextIn :: AppState -> IO Int
|
||||||
|
getRetryNextIn = readIORef . stateRetryNextIn
|
||||||
|
|
||||||
|
putRetryNextIn :: AppState -> Int -> IO ()
|
||||||
|
putRetryNextIn = atomicWriteIORef . stateRetryNextIn
|
||||||
|
|
||||||
getConfig :: AppState -> IO AppConfig
|
getConfig :: AppState -> IO AppConfig
|
||||||
getConfig = readIORef . stateConf
|
getConfig = readIORef . stateConf
|
||||||
|
|
||||||
|
|||||||
+16
-10
@@ -8,12 +8,12 @@ module PostgREST.CLI
|
|||||||
, readCLIShowHelp
|
, readCLIShowHelp
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as Aeson
|
import qualified Data.Aeson as JSON
|
||||||
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Hasql.Pool as P
|
import qualified Hasql.Pool as SQL
|
||||||
import qualified Hasql.Transaction.Sessions as HT
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
import qualified Options.Applicative as O
|
import qualified Options.Applicative as O
|
||||||
import qualified Protolude.Conv as Conv
|
|
||||||
|
|
||||||
import Data.Text.IO (hPutStrLn)
|
import Data.Text.IO (hPutStrLn)
|
||||||
import Text.Heredoc (str)
|
import Text.Heredoc (str)
|
||||||
@@ -53,20 +53,22 @@ main installSignalHandlers runAppWithSocket CLI{cliCommand, cliPath} = do
|
|||||||
dumpSchema :: AppState -> IO LBS.ByteString
|
dumpSchema :: AppState -> IO LBS.ByteString
|
||||||
dumpSchema appState = do
|
dumpSchema appState = do
|
||||||
AppConfig{..} <- AppState.getConfig appState
|
AppConfig{..} <- AppState.getConfig appState
|
||||||
|
actualPgVersion <- AppState.getPgVersion appState
|
||||||
result <-
|
result <-
|
||||||
let transaction = if configDbPreparedStatements then HT.transaction else HT.unpreparedTransaction in
|
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
||||||
P.use (AppState.getPool appState) $
|
SQL.use (AppState.getPool appState) $
|
||||||
transaction HT.ReadCommitted HT.Read $
|
transaction SQL.ReadCommitted SQL.Read $
|
||||||
queryDbStructure
|
queryDbStructure
|
||||||
(toList configDbSchemas)
|
(toList configDbSchemas)
|
||||||
configDbExtraSearchPath
|
configDbExtraSearchPath
|
||||||
|
actualPgVersion
|
||||||
configDbPreparedStatements
|
configDbPreparedStatements
|
||||||
P.release $ AppState.getPool appState
|
SQL.release $ AppState.getPool appState
|
||||||
case result of
|
case result of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
hPutStrLn stderr $ "An error ocurred when loading the schema cache:\n" <> show e
|
hPutStrLn stderr $ "An error ocurred when loading the schema cache:\n" <> show e
|
||||||
exitFailure
|
exitFailure
|
||||||
Right dbStructure -> return $ Aeson.encode dbStructure
|
Right dbStructure -> return $ JSON.encode dbStructure
|
||||||
|
|
||||||
-- | Command line interface options
|
-- | Command line interface options
|
||||||
data CLI = CLI
|
data CLI = CLI
|
||||||
@@ -91,7 +93,7 @@ readCLIShowHelp hasEnvironment =
|
|||||||
progDesc =
|
progDesc =
|
||||||
O.progDesc $
|
O.progDesc $
|
||||||
"PostgREST "
|
"PostgREST "
|
||||||
<> Conv.toS prettyVersion
|
<> BS.unpack prettyVersion
|
||||||
<> " / create a REST API to an existing Postgres database"
|
<> " / create a REST API to an existing Postgres database"
|
||||||
|
|
||||||
footer =
|
footer =
|
||||||
@@ -166,6 +168,10 @@ exampleConfigFile =
|
|||||||
|## Enable in-database configuration
|
|## Enable in-database configuration
|
||||||
|db-config = true
|
|db-config = true
|
||||||
|
|
|
|
||||||
|
|## Determine if GUC request settings for headers, cookies and jwt claims use the legacy names (string with dashes, invalid starting from PostgreSQL v14) with text values instead of the new names (string without dashes, valid on all PostgreSQL versions) with json values.
|
||||||
|
|## For PostgreSQL v14 and up, this setting will be ignored.
|
||||||
|
|db-use-legacy-gucs = true
|
||||||
|
|
|
||||||
|## how to terminate database transactions
|
|## how to terminate database transactions
|
||||||
|## possible values are:
|
|## possible values are:
|
||||||
|## commit (default)
|
|## commit (default)
|
||||||
|
|||||||
+35
-31
@@ -29,19 +29,18 @@ module PostgREST.Config
|
|||||||
import qualified Crypto.JOSE.Types as JOSE
|
import qualified Crypto.JOSE.Types as JOSE
|
||||||
import qualified Crypto.JWT as JWT
|
import qualified Crypto.JWT as JWT
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString as B
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.ByteString.Base64 as B64
|
import qualified Data.ByteString.Base64 as B64
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.Configurator as C
|
import qualified Data.Configurator as C
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified GHC.Show (show)
|
|
||||||
|
|
||||||
import Control.Lens (preview)
|
import Control.Lens (preview)
|
||||||
import Control.Monad (fail)
|
import Control.Monad (fail)
|
||||||
import Crypto.JWT (JWK, JWKSet, StringOrURI, stringOrUri)
|
import Crypto.JWT (JWK, JWKSet, StringOrURI, stringOrUri)
|
||||||
import Data.Aeson (encode, toJSON)
|
import Data.Aeson (toJSON)
|
||||||
import Data.Either.Combinators (mapLeft)
|
import Data.Either.Combinators (mapLeft)
|
||||||
import Data.List (lookup)
|
import Data.List (lookup)
|
||||||
import Data.List.NonEmpty (fromList, toList)
|
import Data.List.NonEmpty (fromList, toList)
|
||||||
@@ -53,13 +52,13 @@ import System.Environment (getEnvironment)
|
|||||||
import System.Posix.Types (FileMode)
|
import System.Posix.Types (FileMode)
|
||||||
|
|
||||||
import PostgREST.Config.JSPath (JSPath, JSPathExp (..),
|
import PostgREST.Config.JSPath (JSPath, JSPathExp (..),
|
||||||
pRoleClaimKey)
|
dumpJSPath, pRoleClaimKey)
|
||||||
import PostgREST.Config.Proxy (Proxy (..),
|
import PostgREST.Config.Proxy (Proxy (..),
|
||||||
isMalformedProxyUri, toURI)
|
isMalformedProxyUri, toURI)
|
||||||
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier, toQi)
|
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier, dumpQi,
|
||||||
|
toQi)
|
||||||
|
|
||||||
import Protolude hiding (Proxy, toList, toS)
|
import Protolude hiding (Proxy, toList)
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
|
|
||||||
data AppConfig = AppConfig
|
data AppConfig = AppConfig
|
||||||
@@ -79,16 +78,17 @@ data AppConfig = AppConfig
|
|||||||
, configDbTxAllowOverride :: Bool
|
, configDbTxAllowOverride :: Bool
|
||||||
, configDbTxRollbackAll :: Bool
|
, configDbTxRollbackAll :: Bool
|
||||||
, configDbUri :: Text
|
, configDbUri :: Text
|
||||||
|
, configDbUseLegacyGucs :: Bool
|
||||||
, configFilePath :: Maybe FilePath
|
, configFilePath :: Maybe FilePath
|
||||||
, configJWKS :: Maybe JWKSet
|
, configJWKS :: Maybe JWKSet
|
||||||
, configJwtAudience :: Maybe StringOrURI
|
, configJwtAudience :: Maybe StringOrURI
|
||||||
, configJwtRoleClaimKey :: JSPath
|
, configJwtRoleClaimKey :: JSPath
|
||||||
, configJwtSecret :: Maybe B.ByteString
|
, configJwtSecret :: Maybe BS.ByteString
|
||||||
, configJwtSecretIsBase64 :: Bool
|
, configJwtSecretIsBase64 :: Bool
|
||||||
, configLogLevel :: LogLevel
|
, configLogLevel :: LogLevel
|
||||||
, configOpenApiMode :: OpenAPIMode
|
, configOpenApiMode :: OpenAPIMode
|
||||||
, configOpenApiServerProxyUri :: Maybe Text
|
, configOpenApiServerProxyUri :: Maybe Text
|
||||||
, configRawMediaTypes :: [B.ByteString]
|
, configRawMediaTypes :: [BS.ByteString]
|
||||||
, configServerHost :: Text
|
, configServerHost :: Text
|
||||||
, configServerPort :: Int
|
, configServerPort :: Int
|
||||||
, configServerUnixSocket :: Maybe FilePath
|
, configServerUnixSocket :: Maybe FilePath
|
||||||
@@ -97,19 +97,21 @@ data AppConfig = AppConfig
|
|||||||
|
|
||||||
data LogLevel = LogCrit | LogError | LogWarn | LogInfo
|
data LogLevel = LogCrit | LogError | LogWarn | LogInfo
|
||||||
|
|
||||||
instance Show LogLevel where
|
dumpLogLevel :: LogLevel -> Text
|
||||||
show LogCrit = "crit"
|
dumpLogLevel = \case
|
||||||
show LogError = "error"
|
LogCrit -> "crit"
|
||||||
show LogWarn = "warn"
|
LogError -> "error"
|
||||||
show LogInfo = "info"
|
LogWarn -> "warn"
|
||||||
|
LogInfo -> "info"
|
||||||
|
|
||||||
data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
|
data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
instance Show OpenAPIMode where
|
dumpOpenApiMode :: OpenAPIMode -> Text
|
||||||
show OAFollowPriv = "follow-privileges"
|
dumpOpenApiMode = \case
|
||||||
show OAIgnorePriv = "ignore-privileges"
|
OAFollowPriv -> "follow-privileges"
|
||||||
show OADisabled = "disabled"
|
OAIgnorePriv -> "ignore-privileges"
|
||||||
|
OADisabled -> "disabled"
|
||||||
|
|
||||||
-- | Dump the config
|
-- | Dump the config
|
||||||
toText :: AppConfig -> Text
|
toText :: AppConfig -> Text
|
||||||
@@ -125,21 +127,22 @@ toText conf =
|
|||||||
,("db-max-rows", maybe "\"\"" show . configDbMaxRows)
|
,("db-max-rows", maybe "\"\"" show . configDbMaxRows)
|
||||||
,("db-pool", show . configDbPoolSize)
|
,("db-pool", show . configDbPoolSize)
|
||||||
,("db-pool-timeout", show . floor . configDbPoolTimeout)
|
,("db-pool-timeout", show . floor . configDbPoolTimeout)
|
||||||
,("db-pre-request", q . maybe mempty show . configDbPreRequest)
|
,("db-pre-request", q . maybe mempty dumpQi . configDbPreRequest)
|
||||||
,("db-prepared-statements", T.toLower . show . configDbPreparedStatements)
|
,("db-prepared-statements", T.toLower . show . configDbPreparedStatements)
|
||||||
,("db-root-spec", q . maybe mempty show . configDbRootSpec)
|
,("db-root-spec", q . maybe mempty dumpQi . configDbRootSpec)
|
||||||
,("db-schemas", q . T.intercalate "," . toList . configDbSchemas)
|
,("db-schemas", q . T.intercalate "," . toList . configDbSchemas)
|
||||||
,("db-config", q . T.toLower . show . configDbConfig)
|
,("db-config", q . T.toLower . show . configDbConfig)
|
||||||
,("db-tx-end", q . showTxEnd)
|
,("db-tx-end", q . showTxEnd)
|
||||||
,("db-uri", q . configDbUri)
|
,("db-uri", q . configDbUri)
|
||||||
,("jwt-aud", toS . encode . maybe "" toJSON . configJwtAudience)
|
,("db-use-legacy-gucs", T.toLower . show . configDbUseLegacyGucs)
|
||||||
,("jwt-role-claim-key", q . T.intercalate mempty . fmap show . configJwtRoleClaimKey)
|
,("jwt-aud", T.decodeUtf8 . LBS.toStrict . JSON.encode . maybe "" toJSON . configJwtAudience)
|
||||||
,("jwt-secret", q . toS . showJwtSecret)
|
,("jwt-role-claim-key", q . T.intercalate mempty . fmap dumpJSPath . configJwtRoleClaimKey)
|
||||||
|
,("jwt-secret", q . T.decodeUtf8 . showJwtSecret)
|
||||||
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
|
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
|
||||||
,("log-level", q . show . configLogLevel)
|
,("log-level", q . dumpLogLevel . configLogLevel)
|
||||||
,("openapi-mode", q . show . configOpenApiMode)
|
,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode)
|
||||||
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
|
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
|
||||||
,("raw-media-types", q . toS . B.intercalate "," . configRawMediaTypes)
|
,("raw-media-types", q . T.decodeUtf8 . BS.intercalate "," . configRawMediaTypes)
|
||||||
,("server-host", q . configServerHost)
|
,("server-host", q . configServerHost)
|
||||||
,("server-port", show . configServerPort)
|
,("server-port", show . configServerPort)
|
||||||
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
|
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
|
||||||
@@ -159,7 +162,7 @@ toText conf =
|
|||||||
( True , True ) -> "rollback-allow-override"
|
( True , True ) -> "rollback-allow-override"
|
||||||
showJwtSecret c
|
showJwtSecret c
|
||||||
| configJwtSecretIsBase64 c = B64.encode secret
|
| configJwtSecretIsBase64 c = B64.encode secret
|
||||||
| otherwise = toS secret
|
| otherwise = secret
|
||||||
where
|
where
|
||||||
secret = fromMaybe mempty $ configJwtSecret c
|
secret = fromMaybe mempty $ configJwtSecret c
|
||||||
showSocketMode c = showOct (configServerUnixSocketMode c) mempty
|
showSocketMode c = showOct (configServerUnixSocketMode c) mempty
|
||||||
@@ -222,6 +225,7 @@ parser optPath env dbSettings =
|
|||||||
<*> parseTxEnd "db-tx-end" snd
|
<*> parseTxEnd "db-tx-end" snd
|
||||||
<*> parseTxEnd "db-tx-end" fst
|
<*> parseTxEnd "db-tx-end" fst
|
||||||
<*> reqString "db-uri"
|
<*> reqString "db-uri"
|
||||||
|
<*> (fromMaybe True <$> optBool "db-use-legacy-gucs")
|
||||||
<*> pure optPath
|
<*> pure optPath
|
||||||
<*> pure Nothing
|
<*> pure Nothing
|
||||||
<*> parseJwtAudience "jwt-aud"
|
<*> parseJwtAudience "jwt-aud"
|
||||||
@@ -420,8 +424,8 @@ parseSecret bytes =
|
|||||||
fromMaybe (maybe secret (\jwk' -> JWT.JWKSet [jwk']) maybeJWK)
|
fromMaybe (maybe secret (\jwk' -> JWT.JWKSet [jwk']) maybeJWK)
|
||||||
maybeJWKSet
|
maybeJWKSet
|
||||||
where
|
where
|
||||||
maybeJWKSet = JSON.decode (toS bytes) :: Maybe JWKSet
|
maybeJWKSet = JSON.decodeStrict bytes :: Maybe JWKSet
|
||||||
maybeJWK = JSON.decode (toS bytes) :: Maybe JWK
|
maybeJWK = JSON.decodeStrict bytes :: Maybe JWK
|
||||||
secret = JWT.JWKSet [JWT.fromKeyMaterial keyMaterial]
|
secret = JWT.JWKSet [JWT.fromKeyMaterial keyMaterial]
|
||||||
keyMaterial = JWT.OctKeyMaterial . JWT.OctKeyParameters $ JOSE.Base64Octets bytes
|
keyMaterial = JWT.OctKeyMaterial . JWT.OctKeyParameters $ JOSE.Base64Octets bytes
|
||||||
|
|
||||||
|
|||||||
@@ -9,31 +9,31 @@ import PostgREST.Config.PgVersion (PgVersion (..))
|
|||||||
|
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
import qualified Hasql.Encoders as HE
|
import qualified Hasql.Encoders as HE
|
||||||
import qualified Hasql.Pool as P
|
import qualified Hasql.Pool as SQL
|
||||||
import qualified Hasql.Session as H
|
import Hasql.Session (Session, statement)
|
||||||
import qualified Hasql.Statement as H
|
import qualified Hasql.Statement as SQL
|
||||||
import qualified Hasql.Transaction as HT
|
import qualified Hasql.Transaction as SQL
|
||||||
import qualified Hasql.Transaction.Sessions as HT
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
|
|
||||||
import Text.InterpolatedString.Perl6 (q)
|
import Text.InterpolatedString.Perl6 (q)
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
queryPgVersion :: H.Session PgVersion
|
queryPgVersion :: Session PgVersion
|
||||||
queryPgVersion = H.statement mempty $ H.Statement sql HE.noParams versionRow False
|
queryPgVersion = statement mempty $ SQL.Statement sql HE.noParams versionRow False
|
||||||
where
|
where
|
||||||
sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')"
|
sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')"
|
||||||
versionRow = HD.singleRow $ PgVersion <$> column HD.int4 <*> column HD.text
|
versionRow = HD.singleRow $ PgVersion <$> column HD.int4 <*> column HD.text
|
||||||
|
|
||||||
queryDbSettings :: P.Pool -> Bool -> IO (Either P.UsageError [(Text, Text)])
|
queryDbSettings :: SQL.Pool -> Bool -> IO (Either SQL.UsageError [(Text, Text)])
|
||||||
queryDbSettings pool prepared =
|
queryDbSettings pool prepared =
|
||||||
let transaction = if prepared then HT.transaction else HT.unpreparedTransaction in
|
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
||||||
P.use pool . transaction HT.ReadCommitted HT.Read $
|
SQL.use pool . transaction SQL.ReadCommitted SQL.Read $
|
||||||
HT.statement mempty dbSettingsStatement
|
SQL.statement mempty dbSettingsStatement
|
||||||
|
|
||||||
-- | Get db settings from the connection role. Global settings will be overridden by database specific settings.
|
-- | Get db settings from the connection role. Global settings will be overridden by database specific settings.
|
||||||
dbSettingsStatement :: H.Statement () [(Text, Text)]
|
dbSettingsStatement :: SQL.Statement () [(Text, Text)]
|
||||||
dbSettingsStatement = H.Statement sql HE.noParams decodeSettings False
|
dbSettingsStatement = SQL.Statement sql HE.noParams decodeSettings False
|
||||||
where
|
where
|
||||||
sql = [q|
|
sql = [q|
|
||||||
with
|
with
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
{-|
|
|
||||||
Module : PostgREST.Types
|
|
||||||
Description : PostgREST common types and functions used by the rest of the modules
|
|
||||||
-}
|
|
||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
|
||||||
|
|
||||||
module PostgREST.Config.JSPath
|
module PostgREST.Config.JSPath
|
||||||
( JSPath
|
( JSPath
|
||||||
, JSPathExp(..)
|
, JSPathExp(..)
|
||||||
|
, dumpJSPath
|
||||||
, pRoleClaimKey
|
, pRoleClaimKey
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@@ -16,10 +11,7 @@ import Data.Either.Combinators (mapLeft)
|
|||||||
import Text.ParserCombinators.Parsec ((<?>))
|
import Text.ParserCombinators.Parsec ((<?>))
|
||||||
import Text.Read (read)
|
import Text.Read (read)
|
||||||
|
|
||||||
import qualified GHC.Show (show)
|
import Protolude
|
||||||
|
|
||||||
import Protolude hiding (toS)
|
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
|
|
||||||
-- | full jspath, e.g. .property[0].attr.detail
|
-- | full jspath, e.g. .property[0].attr.detail
|
||||||
@@ -30,10 +22,10 @@ data JSPathExp
|
|||||||
= JSPKey Text
|
= JSPKey Text
|
||||||
| JSPIdx Int
|
| JSPIdx Int
|
||||||
|
|
||||||
instance Show JSPathExp where
|
dumpJSPath :: JSPathExp -> Text
|
||||||
-- TODO: this needs to be quoted properly for special chars
|
-- TODO: this needs to be quoted properly for special chars
|
||||||
show (JSPKey k) = "." <> show k
|
dumpJSPath (JSPKey k) = "." <> show k
|
||||||
show (JSPIdx i) = "[" <> show i <> "]"
|
dumpJSPath (JSPIdx i) = "[" <> show i <> "]"
|
||||||
|
|
||||||
-- Used for the config value "role-claim-key"
|
-- Used for the config value "role-claim-key"
|
||||||
pRoleClaimKey :: Text -> Either Text JSPath
|
pRoleClaimKey :: Text -> Either Text JSPath
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
module PostgREST.Config.PgVersion
|
module PostgREST.Config.PgVersion
|
||||||
( PgVersion(..)
|
( PgVersion(..)
|
||||||
, minimumPgVersion
|
, minimumPgVersion
|
||||||
, pgVersion95
|
|
||||||
, pgVersion96
|
, pgVersion96
|
||||||
, pgVersion100
|
, pgVersion100
|
||||||
, pgVersion109
|
, pgVersion109
|
||||||
@@ -12,6 +11,7 @@ module PostgREST.Config.PgVersion
|
|||||||
, pgVersion114
|
, pgVersion114
|
||||||
, pgVersion121
|
, pgVersion121
|
||||||
, pgVersion130
|
, pgVersion130
|
||||||
|
, pgVersion140
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
@@ -30,10 +30,7 @@ instance Ord PgVersion where
|
|||||||
|
|
||||||
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
||||||
minimumPgVersion :: PgVersion
|
minimumPgVersion :: PgVersion
|
||||||
minimumPgVersion = pgVersion95
|
minimumPgVersion = pgVersion96
|
||||||
|
|
||||||
pgVersion95 :: PgVersion
|
|
||||||
pgVersion95 = PgVersion 90500 "9.5"
|
|
||||||
|
|
||||||
pgVersion96 :: PgVersion
|
pgVersion96 :: PgVersion
|
||||||
pgVersion96 = PgVersion 90600 "9.6"
|
pgVersion96 = PgVersion 90600 "9.6"
|
||||||
@@ -58,3 +55,6 @@ pgVersion121 = PgVersion 120001 "12.1"
|
|||||||
|
|
||||||
pgVersion130 :: PgVersion
|
pgVersion130 :: PgVersion
|
||||||
pgVersion130 = PgVersion 130000 "13.0"
|
pgVersion130 = PgVersion 130000 "13.0"
|
||||||
|
|
||||||
|
pgVersion140 :: PgVersion
|
||||||
|
pgVersion140 = PgVersion 140000 "14.0"
|
||||||
|
|||||||
@@ -8,13 +8,12 @@ module PostgREST.Config.Proxy
|
|||||||
, toURI
|
, toURI
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Text (pack, toLower)
|
|
||||||
import Network.URI (URI (..), URIAuth (..), isAbsoluteURI, parseURI)
|
import Network.URI (URI (..), URIAuth (..), isAbsoluteURI, parseURI)
|
||||||
|
|
||||||
import Protolude hiding (Proxy, dropWhile, get, intercalate,
|
import Protolude hiding (Proxy)
|
||||||
toLower, toS, (&))
|
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
data Proxy = Proxy
|
data Proxy = Proxy
|
||||||
{ proxyScheme :: Text
|
{ proxyScheme :: Text
|
||||||
@@ -48,8 +47,8 @@ fAnd fs x = all ($ x) fs
|
|||||||
|
|
||||||
isSchemeValid :: URI -> Bool
|
isSchemeValid :: URI -> Bool
|
||||||
isSchemeValid URI {uriScheme = s}
|
isSchemeValid URI {uriScheme = s}
|
||||||
| toLower (pack s) == "https:" = True
|
| T.toLower (T.pack s) == "https:" = True
|
||||||
| toLower (pack s) == "http:" = True
|
| T.toLower (T.pack s) == "http:" = True
|
||||||
| otherwise = False
|
| otherwise = False
|
||||||
|
|
||||||
isQueryValid :: URI -> Bool
|
isQueryValid :: URI -> Bool
|
||||||
|
|||||||
+74
-107
@@ -33,18 +33,20 @@ import qualified Data.HashMap.Strict as M
|
|||||||
import qualified Data.List as L
|
import qualified Data.List as L
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
import qualified Hasql.Encoders as HE
|
import qualified Hasql.Encoders as HE
|
||||||
import qualified Hasql.Statement as H
|
import qualified Hasql.Statement as SQL
|
||||||
import qualified Hasql.Transaction as HT
|
import qualified Hasql.Transaction as SQL
|
||||||
|
|
||||||
import Contravariant.Extras (contrazip2)
|
import Contravariant.Extras (contrazip2)
|
||||||
import Data.Set as S (fromList)
|
import Data.Set as S (fromList)
|
||||||
import Data.Text (split)
|
import Data.Text (split)
|
||||||
import Text.InterpolatedString.Perl6 (q)
|
import Text.InterpolatedString.Perl6 (q)
|
||||||
|
|
||||||
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion100)
|
||||||
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..),
|
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..),
|
||||||
Schema, TableName)
|
Schema, TableName)
|
||||||
import PostgREST.DbStructure.Proc (PgArg (..), PgType (..),
|
import PostgREST.DbStructure.Proc (PgType (..),
|
||||||
ProcDescription (..),
|
ProcDescription (..),
|
||||||
|
ProcParam (..),
|
||||||
ProcVolatility (..),
|
ProcVolatility (..),
|
||||||
ProcsMap, RetType (..))
|
ProcsMap, RetType (..))
|
||||||
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
||||||
@@ -53,8 +55,7 @@ import PostgREST.DbStructure.Relationship (Cardinality (..),
|
|||||||
Relationship (..))
|
Relationship (..))
|
||||||
import PostgREST.DbStructure.Table (Column (..), Table (..))
|
import PostgREST.DbStructure.Table (Column (..), Table (..))
|
||||||
|
|
||||||
import Protolude hiding (toS)
|
import Protolude
|
||||||
import Protolude.Conv (toS)
|
|
||||||
import Protolude.Unsafe (unsafeHead)
|
import Protolude.Unsafe (unsafeHead)
|
||||||
|
|
||||||
|
|
||||||
@@ -82,15 +83,15 @@ type ViewColumn = Column
|
|||||||
-- | A SQL query that can be executed independently
|
-- | A SQL query that can be executed independently
|
||||||
type SqlQuery = ByteString
|
type SqlQuery = ByteString
|
||||||
|
|
||||||
queryDbStructure :: [Schema] -> [Schema] -> Bool -> HT.Transaction DbStructure
|
queryDbStructure :: [Schema] -> [Schema] -> PgVersion -> Bool -> SQL.Transaction DbStructure
|
||||||
queryDbStructure schemas extraSearchPath prepared = do
|
queryDbStructure schemas extraSearchPath pgVer prepared = do
|
||||||
HT.sql "set local schema ''" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object
|
SQL.sql "set local schema ''" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object
|
||||||
tabs <- HT.statement mempty $ allTables prepared
|
tabs <- SQL.statement mempty $ allTables pgVer prepared
|
||||||
cols <- HT.statement schemas $ allColumns tabs prepared
|
cols <- SQL.statement schemas $ allColumns tabs prepared
|
||||||
srcCols <- HT.statement (schemas, extraSearchPath) $ pfkSourceColumns cols prepared
|
srcCols <- SQL.statement (schemas, extraSearchPath) $ pfkSourceColumns cols prepared
|
||||||
m2oRels <- HT.statement mempty $ allM2ORels tabs cols prepared
|
m2oRels <- SQL.statement mempty $ allM2ORels tabs cols prepared
|
||||||
keys <- HT.statement mempty $ allPrimaryKeys tabs prepared
|
keys <- SQL.statement mempty $ allPrimaryKeys tabs prepared
|
||||||
procs <- HT.statement schemas $ allProcs prepared
|
procs <- SQL.statement schemas $ allProcs prepared
|
||||||
|
|
||||||
let rels = addO2MRels . addM2MRels $ addViewM2ORels srcCols m2oRels
|
let rels = addO2MRels . addM2MRels $ addViewM2ORels srcCols m2oRels
|
||||||
keys' = addViewPrimaryKeys srcCols keys
|
keys' = addViewPrimaryKeys srcCols keys
|
||||||
@@ -192,7 +193,7 @@ decodeProcs =
|
|||||||
<*> column HD.text
|
<*> column HD.text
|
||||||
<*> nullableColumn HD.text
|
<*> nullableColumn HD.text
|
||||||
<*> compositeArrayColumn
|
<*> compositeArrayColumn
|
||||||
(PgArg
|
(ProcParam
|
||||||
<$> compositeField HD.text
|
<$> compositeField HD.text
|
||||||
<*> compositeField HD.text
|
<*> compositeField HD.text
|
||||||
<*> compositeField HD.bool
|
<*> compositeField HD.bool
|
||||||
@@ -223,13 +224,13 @@ decodeProcs =
|
|||||||
| v == 's' = Stable
|
| v == 's' = Stable
|
||||||
| otherwise = Volatile -- only 'v' can happen here
|
| otherwise = Volatile -- only 'v' can happen here
|
||||||
|
|
||||||
allProcs :: Bool -> H.Statement [Schema] ProcsMap
|
allProcs :: Bool -> SQL.Statement [Schema] ProcsMap
|
||||||
allProcs = H.Statement (toS sql) (arrayParam HE.text) decodeProcs
|
allProcs = SQL.Statement sql (arrayParam HE.text) decodeProcs
|
||||||
where
|
where
|
||||||
sql = procsSqlQuery <> " WHERE pn.nspname = ANY($1)"
|
sql = procsSqlQuery <> " WHERE pn.nspname = ANY($1)"
|
||||||
|
|
||||||
accessibleProcs :: Bool -> H.Statement Schema ProcsMap
|
accessibleProcs :: Bool -> SQL.Statement Schema ProcsMap
|
||||||
accessibleProcs = H.Statement (toS sql) (param HE.text) decodeProcs
|
accessibleProcs = SQL.Statement sql (param HE.text) decodeProcs
|
||||||
where
|
where
|
||||||
sql = procsSqlQuery <> " WHERE pn.nspname = $1 AND has_function_privilege(p.oid, 'execute')"
|
sql = procsSqlQuery <> " WHERE pn.nspname = $1 AND has_function_privilege(p.oid, 'execute')"
|
||||||
|
|
||||||
@@ -283,10 +284,8 @@ procsSqlQuery = [q|
|
|||||||
COALESCE(comp.relname, t.typname) AS name,
|
COALESCE(comp.relname, t.typname) AS name,
|
||||||
p.proretset AS rettype_is_setof,
|
p.proretset AS rettype_is_setof,
|
||||||
(t.typtype = 'c'
|
(t.typtype = 'c'
|
||||||
-- Only pg pseudo type that is a row type is 'record'
|
-- if any TABLE, INOUT or OUT arguments present, treat as composite
|
||||||
or t.typtype = 'p' and t.typname = 'record'
|
or COALESCE(proargmodes::text[] && '{t,b,o}', false)
|
||||||
-- if any INOUT or OUT arguments present, treat as composite
|
|
||||||
or COALESCE(proargmodes::text[] && '{b,o}', false)
|
|
||||||
) AS rettype_is_composite,
|
) AS rettype_is_composite,
|
||||||
p.provolatile,
|
p.provolatile,
|
||||||
p.provariadic > 0 as hasvariadic
|
p.provariadic > 0 as hasvariadic
|
||||||
@@ -300,9 +299,9 @@ procsSqlQuery = [q|
|
|||||||
LEFT JOIN pg_catalog.pg_description as d ON d.objoid = p.oid
|
LEFT JOIN pg_catalog.pg_description as d ON d.objoid = p.oid
|
||||||
|]
|
|]
|
||||||
|
|
||||||
schemaDescription :: Bool -> H.Statement Schema (Maybe Text)
|
schemaDescription :: Bool -> SQL.Statement Schema (Maybe Text)
|
||||||
schemaDescription =
|
schemaDescription =
|
||||||
H.Statement sql (param HE.text) (join <$> HD.rowMaybe (nullableColumn HD.text))
|
SQL.Statement sql (param HE.text) (join <$> HD.rowMaybe (nullableColumn HD.text))
|
||||||
where
|
where
|
||||||
sql = [q|
|
sql = [q|
|
||||||
select
|
select
|
||||||
@@ -313,9 +312,9 @@ schemaDescription =
|
|||||||
where
|
where
|
||||||
n.nspname = $1 |]
|
n.nspname = $1 |]
|
||||||
|
|
||||||
accessibleTables :: Bool -> H.Statement Schema [Table]
|
accessibleTables :: PgVersion -> Bool -> SQL.Statement Schema [Table]
|
||||||
accessibleTables =
|
accessibleTables pgVer =
|
||||||
H.Statement sql (param HE.text) decodeTables
|
SQL.Statement sql (param HE.text) decodeTables
|
||||||
where
|
where
|
||||||
sql = [q|
|
sql = [q|
|
||||||
select
|
select
|
||||||
@@ -323,38 +322,27 @@ accessibleTables =
|
|||||||
relname as table_name,
|
relname as table_name,
|
||||||
d.description as table_description,
|
d.description as table_description,
|
||||||
(
|
(
|
||||||
c.relkind IN ('r', 'v','f')
|
c.relkind IN ('r','p')
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8
|
OR (
|
||||||
OR EXISTS (
|
c.relkind IN ('v','f')
|
||||||
SELECT 1
|
-- CMD_INSERT - see allTables query below for explanation
|
||||||
FROM pg_trigger
|
AND (pg_relation_is_updatable(c.oid::regclass, TRUE) & 8) = 8
|
||||||
WHERE
|
|
||||||
pg_trigger.tgrelid = c.oid
|
|
||||||
AND (pg_trigger.tgtype::integer & 69) = 69
|
|
||||||
)
|
)
|
||||||
) AS insertable,
|
) AS insertable,
|
||||||
(
|
(
|
||||||
c.relkind IN ('r', 'v','f')
|
c.relkind IN ('r','p')
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 4) = 4
|
OR (
|
||||||
-- CMD_UPDATE
|
c.relkind IN ('v','f')
|
||||||
OR EXISTS (
|
-- CMD_UPDATE
|
||||||
SELECT 1
|
AND (pg_relation_is_updatable(c.oid::regclass, TRUE) & 4) = 4
|
||||||
FROM pg_trigger
|
|
||||||
WHERE
|
|
||||||
pg_trigger.tgrelid = c.oid
|
|
||||||
and (pg_trigger.tgtype::integer & 81) = 81
|
|
||||||
)
|
)
|
||||||
) as updatable,
|
) as updatable,
|
||||||
(
|
(
|
||||||
c.relkind IN ('r', 'v','f')
|
c.relkind IN ('r','p')
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 16) = 16
|
OR (
|
||||||
-- CMD_DELETE
|
c.relkind IN ('v','f')
|
||||||
OR EXISTS (
|
-- CMD_DELETE
|
||||||
SELECT 1
|
AND (pg_relation_is_updatable(c.oid::regclass, TRUE) & 16) = 16
|
||||||
FROM pg_trigger
|
|
||||||
WHERE
|
|
||||||
pg_trigger.tgrelid = c.oid
|
|
||||||
and (pg_trigger.tgtype::integer & 73) = 73
|
|
||||||
)
|
)
|
||||||
) as deletable
|
) as deletable
|
||||||
from
|
from
|
||||||
@@ -362,8 +350,9 @@ accessibleTables =
|
|||||||
join pg_namespace n on n.oid = c.relnamespace
|
join pg_namespace n on n.oid = c.relnamespace
|
||||||
left join pg_catalog.pg_description as d on d.objoid = c.oid and d.objsubid = 0
|
left join pg_catalog.pg_description as d on d.objoid = c.oid and d.objsubid = 0
|
||||||
where
|
where
|
||||||
c.relkind in ('v', 'r', 'm', 'f')
|
c.relkind in ('v','r','m','f','p')
|
||||||
and n.nspname = $1
|
and n.nspname = $1 |]
|
||||||
|
<> relIsNotPartition pgVer <> [q|
|
||||||
and (
|
and (
|
||||||
pg_has_role(c.relowner, 'USAGE')
|
pg_has_role(c.relowner, 'USAGE')
|
||||||
or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER')
|
or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER')
|
||||||
@@ -458,9 +447,9 @@ addViewPrimaryKeys srcCols = concatMap (\pk ->
|
|||||||
filter (\(col, _) -> colTable col == pkTable pk && colName col == pkName pk) srcCols in
|
filter (\(col, _) -> colTable col == pkTable pk && colName col == pkName pk) srcCols in
|
||||||
pk : viewPks)
|
pk : viewPks)
|
||||||
|
|
||||||
allTables :: Bool -> H.Statement () [Table]
|
allTables :: PgVersion -> Bool -> SQL.Statement () [Table]
|
||||||
allTables =
|
allTables pgVer =
|
||||||
H.Statement sql HE.noParams decodeTables
|
SQL.Statement sql HE.noParams decodeTables
|
||||||
where
|
where
|
||||||
sql = [q|
|
sql = [q|
|
||||||
SELECT
|
SELECT
|
||||||
@@ -468,67 +457,45 @@ allTables =
|
|||||||
c.relname AS table_name,
|
c.relname AS table_name,
|
||||||
d.description AS table_description,
|
d.description AS table_description,
|
||||||
(
|
(
|
||||||
c.relkind = 'r'
|
c.relkind IN ('r','p')
|
||||||
OR (
|
OR (
|
||||||
c.relkind in ('v','f')
|
c.relkind in ('v','f')
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8
|
|
||||||
-- The function `pg_relation_is_updateable` returns a bitmask where 8
|
-- The function `pg_relation_is_updateable` returns a bitmask where 8
|
||||||
-- corresponds to `1 << CMD_INSERT` in the PostgreSQL source code, i.e.
|
-- corresponds to `1 << CMD_INSERT` in the PostgreSQL source code, i.e.
|
||||||
-- it's possible to insert into the relation.
|
-- it's possible to insert into the relation.
|
||||||
OR EXISTS (
|
AND (pg_relation_is_updatable(c.oid::regclass, TRUE) & 8) = 8
|
||||||
SELECT 1
|
|
||||||
FROM pg_trigger
|
|
||||||
WHERE
|
|
||||||
pg_trigger.tgrelid = c.oid
|
|
||||||
AND (pg_trigger.tgtype::integer & 69) = 69
|
|
||||||
-- The trigger type `tgtype` is a bitmask where 69 corresponds to
|
|
||||||
-- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_INSERT
|
|
||||||
-- in the PostgreSQL source code.
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
) AS insertable,
|
) AS insertable,
|
||||||
(
|
(
|
||||||
c.relkind = 'r'
|
c.relkind IN ('r','p')
|
||||||
OR (
|
OR (
|
||||||
c.relkind in ('v','f')
|
c.relkind in ('v','f')
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 4) = 4
|
|
||||||
-- CMD_UPDATE
|
-- CMD_UPDATE
|
||||||
OR EXISTS (
|
AND (pg_relation_is_updatable(c.oid::regclass, TRUE) & 4) = 4
|
||||||
SELECT 1
|
|
||||||
FROM pg_trigger
|
|
||||||
WHERE
|
|
||||||
pg_trigger.tgrelid = c.oid
|
|
||||||
and (pg_trigger.tgtype::integer & 81) = 81
|
|
||||||
-- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_UPDATE
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
) AS updatable,
|
) AS updatable,
|
||||||
(
|
(
|
||||||
c.relkind = 'r'
|
c.relkind IN ('r','p')
|
||||||
OR (
|
OR (
|
||||||
c.relkind in ('v','f')
|
c.relkind in ('v','f')
|
||||||
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 16) = 16
|
|
||||||
-- CMD_DELETE
|
-- CMD_DELETE
|
||||||
OR EXISTS (
|
AND (pg_relation_is_updatable(c.oid::regclass, TRUE) & 16) = 16
|
||||||
SELECT 1
|
|
||||||
FROM pg_trigger
|
|
||||||
WHERE
|
|
||||||
pg_trigger.tgrelid = c.oid
|
|
||||||
and (pg_trigger.tgtype::integer & 73) = 73
|
|
||||||
-- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_DELETE
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
) AS deletable
|
) AS deletable
|
||||||
FROM pg_class c
|
FROM pg_class c
|
||||||
JOIN pg_namespace n ON n.oid = c.relnamespace
|
JOIN pg_namespace n ON n.oid = c.relnamespace
|
||||||
LEFT JOIN pg_catalog.pg_description as d on d.objoid = c.oid and d.objsubid = 0
|
LEFT JOIN pg_catalog.pg_description as d on d.objoid = c.oid and d.objsubid = 0
|
||||||
WHERE c.relkind IN ('v','r','m','f')
|
WHERE c.relkind IN ('v','r','m','f','p')
|
||||||
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
AND n.nspname NOT IN ('pg_catalog', 'information_schema') |]
|
||||||
|
<> relIsNotPartition pgVer <> [q|
|
||||||
ORDER BY table_schema, table_name |]
|
ORDER BY table_schema, table_name |]
|
||||||
|
|
||||||
allColumns :: [Table] -> Bool -> H.Statement [Schema] [Column]
|
relIsNotPartition :: PgVersion -> SqlQuery
|
||||||
|
relIsNotPartition pgVer = if pgVer >= pgVersion100 then " AND not c.relispartition " else mempty
|
||||||
|
|
||||||
|
allColumns :: [Table] -> Bool -> SQL.Statement [Schema] [Column]
|
||||||
allColumns tabs =
|
allColumns tabs =
|
||||||
H.Statement sql (arrayParam HE.text) (decodeColumns tabs)
|
SQL.Statement sql (arrayParam HE.text) (decodeColumns tabs)
|
||||||
where
|
where
|
||||||
sql = [q|
|
sql = [q|
|
||||||
SELECT DISTINCT
|
SELECT DISTINCT
|
||||||
@@ -559,7 +526,7 @@ allColumns tabs =
|
|||||||
pg_catalog.pg_namespace n
|
pg_catalog.pg_namespace n
|
||||||
WHERE
|
WHERE
|
||||||
r.contype IN ('f', 'p', 'u')
|
r.contype IN ('f', 'p', 'u')
|
||||||
AND c.relkind IN ('r', 'v', 'f', 'm')
|
AND c.relkind IN ('r', 'v', 'f', 'm', 'p')
|
||||||
AND r.conrelid = c.oid
|
AND r.conrelid = c.oid
|
||||||
AND c.relnamespace = n.oid
|
AND c.relnamespace = n.oid
|
||||||
AND n.nspname <> ANY (ARRAY['pg_catalog', 'information_schema'] || $1)
|
AND n.nspname <> ANY (ARRAY['pg_catalog', 'information_schema'] || $1)
|
||||||
@@ -617,7 +584,7 @@ allColumns tabs =
|
|||||||
NOT pg_is_other_temp_schema(nc.oid)
|
NOT pg_is_other_temp_schema(nc.oid)
|
||||||
AND a.attnum > 0
|
AND a.attnum > 0
|
||||||
AND NOT a.attisdropped
|
AND NOT a.attisdropped
|
||||||
AND c.relkind in ('r', 'v', 'f', 'm')
|
AND c.relkind in ('r', 'v', 'f', 'm', 'p')
|
||||||
-- Filter only columns that are FK/PK or in the api schema:
|
-- Filter only columns that are FK/PK or in the api schema:
|
||||||
AND (nc.nspname = ANY ($1) OR kc.r_oid IS NOT NULL)
|
AND (nc.nspname = ANY ($1) OR kc.r_oid IS NOT NULL)
|
||||||
)
|
)
|
||||||
@@ -659,9 +626,9 @@ columnFromRow tabs (s, t, n, desc, nul, typ, l, d, e) = buildColumn <$> table
|
|||||||
parseEnum :: Maybe Text -> [Text]
|
parseEnum :: Maybe Text -> [Text]
|
||||||
parseEnum = maybe [] (split (==','))
|
parseEnum = maybe [] (split (==','))
|
||||||
|
|
||||||
allM2ORels :: [Table] -> [Column] -> Bool -> H.Statement () [Relationship]
|
allM2ORels :: [Table] -> [Column] -> Bool -> SQL.Statement () [Relationship]
|
||||||
allM2ORels tabs cols =
|
allM2ORels tabs cols =
|
||||||
H.Statement sql HE.noParams (decodeRels tabs cols)
|
SQL.Statement sql HE.noParams (decodeRels tabs cols)
|
||||||
where
|
where
|
||||||
sql = [q|
|
sql = [q|
|
||||||
SELECT ns1.nspname AS table_schema,
|
SELECT ns1.nspname AS table_schema,
|
||||||
@@ -697,9 +664,9 @@ relFromRow allTabs allCols (rs, rt, cn, rcs, frs, frt, frcs) =
|
|||||||
cols = mapM (findCol rs rt) rcs
|
cols = mapM (findCol rs rt) rcs
|
||||||
colsF = mapM (findCol frs frt) frcs
|
colsF = mapM (findCol frs frt) frcs
|
||||||
|
|
||||||
allPrimaryKeys :: [Table] -> Bool -> H.Statement () [PrimaryKey]
|
allPrimaryKeys :: [Table] -> Bool -> SQL.Statement () [PrimaryKey]
|
||||||
allPrimaryKeys tabs =
|
allPrimaryKeys tabs =
|
||||||
H.Statement sql HE.noParams (decodePks tabs)
|
SQL.Statement sql HE.noParams (decodePks tabs)
|
||||||
where
|
where
|
||||||
sql = [q|
|
sql = [q|
|
||||||
-- CTE to replace information_schema.table_constraints to remove owner limit
|
-- CTE to replace information_schema.table_constraints to remove owner limit
|
||||||
@@ -716,7 +683,7 @@ allPrimaryKeys tabs =
|
|||||||
nc.oid = c.connamespace
|
nc.oid = c.connamespace
|
||||||
AND nr.oid = r.relnamespace
|
AND nr.oid = r.relnamespace
|
||||||
AND c.conrelid = r.oid
|
AND c.conrelid = r.oid
|
||||||
AND r.relkind = 'r'
|
AND r.relkind IN ('r', 'p')
|
||||||
AND NOT pg_is_other_temp_schema(nr.oid)
|
AND NOT pg_is_other_temp_schema(nr.oid)
|
||||||
AND c.contype = 'p'
|
AND c.contype = 'p'
|
||||||
),
|
),
|
||||||
@@ -753,7 +720,7 @@ allPrimaryKeys tabs =
|
|||||||
AND r.oid = c.conrelid
|
AND r.oid = c.conrelid
|
||||||
AND nc.oid = c.connamespace
|
AND nc.oid = c.connamespace
|
||||||
AND c.contype in ('p', 'u', 'f')
|
AND c.contype in ('p', 'u', 'f')
|
||||||
AND r.relkind = 'r'
|
AND r.relkind IN ('r', 'p')
|
||||||
AND NOT pg_is_other_temp_schema(nr.oid)
|
AND NOT pg_is_other_temp_schema(nr.oid)
|
||||||
) ss
|
) ss
|
||||||
WHERE
|
WHERE
|
||||||
@@ -778,9 +745,9 @@ pkFromRow tabs (s, t, n) = PrimaryKey <$> table <*> pure n
|
|||||||
where table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs
|
where table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs
|
||||||
|
|
||||||
-- returns all the primary and foreign key columns which are referenced in views
|
-- returns all the primary and foreign key columns which are referenced in views
|
||||||
pfkSourceColumns :: [Column] -> Bool -> H.Statement ([Schema], [Schema]) [SourceColumn]
|
pfkSourceColumns :: [Column] -> Bool -> SQL.Statement ([Schema], [Schema]) [SourceColumn]
|
||||||
pfkSourceColumns cols =
|
pfkSourceColumns cols =
|
||||||
H.Statement sql (contrazip2 (arrayParam HE.text) (arrayParam HE.text)) (decodeSourceColumns cols)
|
SQL.Statement sql (contrazip2 (arrayParam HE.text) (arrayParam HE.text)) (decodeSourceColumns cols)
|
||||||
-- query explanation at:
|
-- query explanation at:
|
||||||
-- * rationale: https://gist.github.com/wolfgangwalther/5425d64e7b0d20aad71f6f68474d9f19
|
-- * rationale: https://gist.github.com/wolfgangwalther/5425d64e7b0d20aad71f6f68474d9f19
|
||||||
-- * json transformation: https://gist.github.com/wolfgangwalther/3a8939da680c24ad767e93ad2c183089
|
-- * json transformation: https://gist.github.com/wolfgangwalther/3a8939da680c24ad767e93ad2c183089
|
||||||
@@ -852,8 +819,8 @@ pfkSourceColumns cols =
|
|||||||
',' , ''
|
',' , ''
|
||||||
-- The same applies for `{` and `}`, although those are used a lot in pg_node_tree.
|
-- The same applies for `{` and `}`, although those are used a lot in pg_node_tree.
|
||||||
-- We remove the escaped ones, which might be part of column names again.
|
-- We remove the escaped ones, which might be part of column names again.
|
||||||
), '\{' , ''
|
), E'\\{' , ''
|
||||||
), '\}' , ''
|
), E'\\}' , ''
|
||||||
-- The fields we need are formatted as json manually to protect them from the regex.
|
-- The fields we need are formatted as json manually to protect them from the regex.
|
||||||
), ' :targetList ' , ',"targetList":'
|
), ' :targetList ' , ',"targetList":'
|
||||||
), ' :resno ' , ',"resno":'
|
), ' :resno ' , ',"resno":'
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ module PostgREST.DbStructure.Identifiers
|
|||||||
, Schema
|
, Schema
|
||||||
, TableName
|
, TableName
|
||||||
, FieldName
|
, FieldName
|
||||||
|
, dumpQi
|
||||||
, toQi
|
, toQi
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified GHC.Show
|
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -26,9 +26,9 @@ data QualifiedIdentifier = QualifiedIdentifier
|
|||||||
|
|
||||||
instance Hashable QualifiedIdentifier
|
instance Hashable QualifiedIdentifier
|
||||||
|
|
||||||
instance Show QualifiedIdentifier where
|
dumpQi :: QualifiedIdentifier -> Text
|
||||||
show (QualifiedIdentifier s i) =
|
dumpQi (QualifiedIdentifier s i) =
|
||||||
(if T.null s then mempty else toS s <> ".") <> toS i
|
(if T.null s then mempty else s <> ".") <> i
|
||||||
|
|
||||||
-- TODO: Handle a case where the QI comes like this: "my.fav.schema"."my.identifier"
|
-- TODO: Handle a case where the QI comes like this: "my.fav.schema"."my.identifier"
|
||||||
-- Right now it only handles the schema.identifier case
|
-- Right now it only handles the schema.identifier case
|
||||||
|
|||||||
@@ -2,37 +2,25 @@
|
|||||||
{-# LANGUAGE DeriveGeneric #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
|
||||||
module PostgREST.DbStructure.Proc
|
module PostgREST.DbStructure.Proc
|
||||||
( PgArg(..)
|
( PgType(..)
|
||||||
, PgType(..)
|
|
||||||
, ProcDescription(..)
|
, ProcDescription(..)
|
||||||
|
, ProcParam(..)
|
||||||
, ProcVolatility(..)
|
, ProcVolatility(..)
|
||||||
, ProcsMap
|
, ProcsMap
|
||||||
, RetType(..)
|
, RetType(..)
|
||||||
, procReturnsScalar
|
, procReturnsScalar
|
||||||
, procReturnsSingle
|
, procReturnsSingle
|
||||||
, procTableName
|
, procTableName
|
||||||
, specifiedProcArgs
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import qualified Data.Set as S
|
|
||||||
|
|
||||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..),
|
||||||
QualifiedIdentifier (..),
|
|
||||||
Schema, TableName)
|
Schema, TableName)
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
data PgArg = PgArg
|
|
||||||
{ pgaName :: Text
|
|
||||||
, pgaType :: Text
|
|
||||||
, pgaReq :: Bool
|
|
||||||
, pgaVar :: Bool
|
|
||||||
}
|
|
||||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
data PgType
|
data PgType
|
||||||
= Scalar
|
= Scalar
|
||||||
| Composite QualifiedIdentifier
|
| Composite QualifiedIdentifier
|
||||||
@@ -53,32 +41,32 @@ data ProcDescription = ProcDescription
|
|||||||
{ pdSchema :: Schema
|
{ pdSchema :: Schema
|
||||||
, pdName :: Text
|
, pdName :: Text
|
||||||
, pdDescription :: Maybe Text
|
, pdDescription :: Maybe Text
|
||||||
, pdArgs :: [PgArg]
|
, pdParams :: [ProcParam]
|
||||||
, pdReturnType :: RetType
|
, pdReturnType :: RetType
|
||||||
, pdVolatility :: ProcVolatility
|
, pdVolatility :: ProcVolatility
|
||||||
, pdHasVariadic :: Bool
|
, pdHasVariadic :: Bool
|
||||||
}
|
}
|
||||||
deriving (Eq, Generic, JSON.ToJSON)
|
deriving (Eq, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
-- Order by least number of args in the case of overloaded functions
|
data ProcParam = ProcParam
|
||||||
|
{ ppName :: Text
|
||||||
|
, ppType :: Text
|
||||||
|
, ppReq :: Bool
|
||||||
|
, ppVar :: Bool
|
||||||
|
}
|
||||||
|
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
-- Order by least number of params in the case of overloaded functions
|
||||||
instance Ord ProcDescription where
|
instance Ord ProcDescription where
|
||||||
ProcDescription schema1 name1 des1 args1 rt1 vol1 hasVar1 `compare` ProcDescription schema2 name2 des2 args2 rt2 vol2 hasVar2
|
ProcDescription schema1 name1 des1 prms1 rt1 vol1 hasVar1 `compare` ProcDescription schema2 name2 des2 prms2 rt2 vol2 hasVar2
|
||||||
| schema1 == schema2 && name1 == name2 && length args1 < length args2 = LT
|
| schema1 == schema2 && name1 == name2 && length prms1 < length prms2 = LT
|
||||||
| schema2 == schema2 && name1 == name2 && length args1 > length args2 = GT
|
| schema2 == schema2 && name1 == name2 && length prms1 > length prms2 = GT
|
||||||
| otherwise = (schema1, name1, des1, args1, rt1, vol1, hasVar1) `compare` (schema2, name2, des2, args2, rt2, vol2, hasVar2)
|
| otherwise = (schema1, name1, des1, prms1, rt1, vol1, hasVar1) `compare` (schema2, name2, des2, prms2, rt2, vol2, hasVar2)
|
||||||
|
|
||||||
-- | A map of all procs, all of which can be overloaded(one entry will have more than one ProcDescription).
|
-- | A map of all procs, all of which can be overloaded(one entry will have more than one ProcDescription).
|
||||||
-- | It uses a HashMap for a faster lookup.
|
-- | It uses a HashMap for a faster lookup.
|
||||||
type ProcsMap = M.HashMap QualifiedIdentifier [ProcDescription]
|
type ProcsMap = M.HashMap QualifiedIdentifier [ProcDescription]
|
||||||
|
|
||||||
{-|
|
|
||||||
Search the procedure parameters by matching them with the specified keys.
|
|
||||||
If the key doesn't match a parameter, a parameter with a default type "text" is assumed.
|
|
||||||
-}
|
|
||||||
specifiedProcArgs :: S.Set FieldName -> ProcDescription -> [PgArg]
|
|
||||||
specifiedProcArgs keys proc =
|
|
||||||
(\k -> fromMaybe (PgArg k "text" True False) (find ((==) k . pgaName) (pdArgs proc))) <$> S.toList keys
|
|
||||||
|
|
||||||
procReturnsScalar :: ProcDescription -> Bool
|
procReturnsScalar :: ProcDescription -> Bool
|
||||||
procReturnsScalar proc = case proc of
|
procReturnsScalar proc = case proc of
|
||||||
ProcDescription{pdReturnType = (Single Scalar)} -> True
|
ProcDescription{pdReturnType = (Single Scalar)} -> True
|
||||||
|
|||||||
+143
-125
@@ -16,10 +16,13 @@ module PostgREST.Error
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Hasql.Pool as P
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Hasql.Session as H
|
import qualified Data.Text.Encoding.Error as T
|
||||||
import qualified Network.HTTP.Types.Status as HT
|
import qualified Hasql.Pool as SQL
|
||||||
|
import qualified Hasql.Session as SQL
|
||||||
|
import qualified Network.HTTP.Types.Status as HTTP
|
||||||
|
|
||||||
import Data.Aeson ((.=))
|
import Data.Aeson ((.=))
|
||||||
import Network.Wai (Response, responseLBS)
|
import Network.Wai (Response, responseLBS)
|
||||||
@@ -29,19 +32,18 @@ import Network.HTTP.Types.Header (Header)
|
|||||||
import PostgREST.ContentType (ContentType (..))
|
import PostgREST.ContentType (ContentType (..))
|
||||||
import qualified PostgREST.ContentType as ContentType
|
import qualified PostgREST.ContentType as ContentType
|
||||||
|
|
||||||
import PostgREST.DbStructure.Proc (PgArg (..),
|
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
||||||
ProcDescription (..))
|
ProcParam (..))
|
||||||
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
Relationship (..))
|
Relationship (..))
|
||||||
import PostgREST.DbStructure.Table (Column (..), Table (..))
|
import PostgREST.DbStructure.Table (Column (..), Table (..))
|
||||||
|
|
||||||
import Protolude hiding (toS)
|
import Protolude
|
||||||
import Protolude.Conv (toS, toSL)
|
|
||||||
|
|
||||||
|
|
||||||
class (JSON.ToJSON a) => PgrstError a where
|
class (JSON.ToJSON a) => PgrstError a where
|
||||||
status :: a -> HT.Status
|
status :: a -> HTTP.Status
|
||||||
headers :: a -> [Header]
|
headers :: a -> [Header]
|
||||||
|
|
||||||
errorPayload :: a -> LByteString
|
errorPayload :: a -> LByteString
|
||||||
@@ -60,25 +62,25 @@ data ApiRequestError
|
|||||||
| NoRelBetween Text Text
|
| NoRelBetween Text Text
|
||||||
| AmbiguousRelBetween Text Text [Relationship]
|
| AmbiguousRelBetween Text Text [Relationship]
|
||||||
| AmbiguousRpc [ProcDescription]
|
| AmbiguousRpc [ProcDescription]
|
||||||
| NoRpc Text Text [Text] Bool
|
| NoRpc Text Text [Text] Bool ContentType Bool
|
||||||
| InvalidFilters
|
| InvalidFilters
|
||||||
| UnacceptableSchema [Text]
|
| UnacceptableSchema [Text]
|
||||||
| ContentTypeError [ByteString]
|
| ContentTypeError [ByteString]
|
||||||
| UnsupportedVerb -- Unreachable?
|
| UnsupportedVerb -- Unreachable?
|
||||||
|
|
||||||
instance PgrstError ApiRequestError where
|
instance PgrstError ApiRequestError where
|
||||||
status InvalidRange = HT.status416
|
status InvalidRange = HTTP.status416
|
||||||
status InvalidFilters = HT.status405
|
status InvalidFilters = HTTP.status405
|
||||||
status (InvalidBody _) = HT.status400
|
status (InvalidBody _) = HTTP.status400
|
||||||
status UnsupportedVerb = HT.status405
|
status UnsupportedVerb = HTTP.status405
|
||||||
status ActionInappropriate = HT.status405
|
status ActionInappropriate = HTTP.status405
|
||||||
status (ParseRequestError _ _) = HT.status400
|
status (ParseRequestError _ _) = HTTP.status400
|
||||||
status (NoRelBetween _ _) = HT.status400
|
status (NoRelBetween _ _) = HTTP.status400
|
||||||
status AmbiguousRelBetween{} = HT.status300
|
status AmbiguousRelBetween{} = HTTP.status300
|
||||||
status (AmbiguousRpc _) = HT.status300
|
status (AmbiguousRpc _) = HTTP.status300
|
||||||
status NoRpc{} = HT.status404
|
status NoRpc{} = HTTP.status404
|
||||||
status (UnacceptableSchema _) = HT.status406
|
status (UnacceptableSchema _) = HTTP.status406
|
||||||
status (ContentTypeError _) = HT.status415
|
status (ContentTypeError _) = HTTP.status415
|
||||||
|
|
||||||
headers _ = [ContentType.toHeader CTApplicationJSON]
|
headers _ = [ContentType.toHeader CTApplicationJSON]
|
||||||
|
|
||||||
@@ -88,22 +90,30 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
toJSON ActionInappropriate = JSON.object [
|
toJSON ActionInappropriate = JSON.object [
|
||||||
"message" .= ("Bad Request" :: Text)]
|
"message" .= ("Bad Request" :: Text)]
|
||||||
toJSON (InvalidBody errorMessage) = JSON.object [
|
toJSON (InvalidBody errorMessage) = JSON.object [
|
||||||
"message" .= (toS errorMessage :: Text)]
|
"message" .= T.decodeUtf8 errorMessage]
|
||||||
toJSON InvalidRange = JSON.object [
|
toJSON InvalidRange = JSON.object [
|
||||||
"message" .= ("HTTP Range error" :: Text)]
|
"message" .= ("HTTP Range error" :: Text)]
|
||||||
toJSON (NoRelBetween parent child) = JSON.object [
|
toJSON (NoRelBetween parent child) = JSON.object [
|
||||||
"hint" .= ("If a new foreign key between these entities was created in the database, try reloading the schema cache." :: Text),
|
"hint" .= ("If a new foreign key between these entities was created in the database, try reloading the schema cache." :: Text),
|
||||||
"message" .= ("Could not find a relationship between " <> parent <> " and " <> child <> " in the schema cache" :: Text)]
|
"message" .= ("Could not find a relationship between " <> parent <> " and " <> child <> " in the schema cache" :: Text)]
|
||||||
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
|
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
|
||||||
"hint" .= ("By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)" :: Text),
|
"hint" .= ("Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key." :: Text),
|
||||||
"message" .= ("More than one relationship was found for " <> parent <> " and " <> child :: Text),
|
"message" .= ("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'" :: Text),
|
||||||
"details" .= (compressedRel <$> rels) ]
|
"details" .= (compressedRel <$> rels) ]
|
||||||
toJSON (AmbiguousRpc procs) = JSON.object [
|
toJSON (AmbiguousRpc procs) = JSON.object [
|
||||||
"hint" .= ("Overloaded functions with the same argument name but different types are not supported" :: Text),
|
"hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text),
|
||||||
"message" .= ("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [pgaName a <> " => " <> pgaType a | a <- pdArgs p] <> ")" | p <- procs])]
|
"message" .= ("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs])]
|
||||||
toJSON (NoRpc schema procName payloadKeys hasPreferSingleObject) = JSON.object [
|
toJSON (NoRpc schema procName argumentKeys hasPreferSingleObject contentType isInvPost) =
|
||||||
"hint" .= ("If a new function was created in the database with this name and arguments, try reloading the schema cache." :: Text),
|
let prms = "(" <> T.intercalate ", " argumentKeys <> ")" in JSON.object [
|
||||||
"message" .= ("Could not find the " <> schema <> "." <> procName <> (if hasPreferSingleObject then " function with a single json or jsonb argument" else "(" <> T.intercalate ", " payloadKeys <> ")" <> " function") <> " in the schema cache")]
|
"hint" .= ("If a new function was created in the database with this name and parameters, try reloading the schema cache." :: Text),
|
||||||
|
"message" .= ("Could not find the " <> schema <> "." <> procName <>
|
||||||
|
(case (hasPreferSingleObject, isInvPost, contentType) of
|
||||||
|
(True, _, _) -> " function with a single json or jsonb parameter"
|
||||||
|
(_, True, CTTextPlain) -> " function with a single unnamed text parameter"
|
||||||
|
(_, True, CTOctetStream) -> " function with a single unnamed bytea parameter"
|
||||||
|
(_, True, CTApplicationJSON) -> prms <> " function or the " <> schema <> "." <> procName <>" function with a single unnamed json or jsonb parameter"
|
||||||
|
_ -> prms <> " function") <>
|
||||||
|
" in the schema cache")]
|
||||||
toJSON UnsupportedVerb = JSON.object [
|
toJSON UnsupportedVerb = JSON.object [
|
||||||
"message" .= ("Unsupported HTTP verb" :: Text)]
|
"message" .= ("Unsupported HTTP verb" :: Text)]
|
||||||
toJSON InvalidFilters = JSON.object [
|
toJSON InvalidFilters = JSON.object [
|
||||||
@@ -111,7 +121,7 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
toJSON (UnacceptableSchema schemas) = JSON.object [
|
toJSON (UnacceptableSchema schemas) = JSON.object [
|
||||||
"message" .= ("The schema must be one of the following: " <> T.intercalate ", " schemas)]
|
"message" .= ("The schema must be one of the following: " <> T.intercalate ", " schemas)]
|
||||||
toJSON (ContentTypeError cts) = JSON.object [
|
toJSON (ContentTypeError cts) = JSON.object [
|
||||||
"message" .= ("None of these Content-Types are available: " <> (toS . intercalate ", " . map toS) cts :: Text)]
|
"message" .= ("None of these Content-Types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts))]
|
||||||
|
|
||||||
compressedRel :: Relationship -> JSON.Value
|
compressedRel :: Relationship -> JSON.Value
|
||||||
compressedRel Relationship{..} =
|
compressedRel Relationship{..} =
|
||||||
@@ -119,140 +129,148 @@ compressedRel Relationship{..} =
|
|||||||
fmtTbl Table{..} = tableSchema <> "." <> tableName
|
fmtTbl Table{..} = tableSchema <> "." <> tableName
|
||||||
fmtEls els = "[" <> T.intercalate ", " els <> "]"
|
fmtEls els = "[" <> T.intercalate ", " els <> "]"
|
||||||
in
|
in
|
||||||
JSON.object $ [
|
JSON.object $
|
||||||
"origin" .= fmtTbl relTable
|
("embedding" .= (tableName relTable <> " with " <> tableName relForeignTable :: Text))
|
||||||
, "target" .= fmtTbl relForeignTable
|
: case relCardinality of
|
||||||
] ++
|
M2M Junction{..} -> [
|
||||||
case relCardinality of
|
"cardinality" .= ("many-to-many" :: Text)
|
||||||
M2M Junction{..} -> [
|
, "relationship" .= (fmtTbl junTable <> fmtEls [junConstraint1] <> fmtEls [junConstraint2])
|
||||||
"cardinality" .= ("m2m" :: Text)
|
]
|
||||||
, "relationship" .= (fmtTbl junTable <> fmtEls [junConstraint1] <> fmtEls [junConstraint2])
|
M2O cons -> [
|
||||||
]
|
"cardinality" .= ("many-to-one" :: Text)
|
||||||
M2O cons -> [
|
, "relationship" .= (cons <> fmtEls (colName <$> relColumns) <> fmtEls (colName <$> relForeignColumns))
|
||||||
"cardinality" .= ("m2o" :: Text)
|
]
|
||||||
, "relationship" .= (cons <> fmtEls (colName <$> relColumns) <> fmtEls (colName <$> relForeignColumns))
|
O2M cons -> [
|
||||||
]
|
"cardinality" .= ("one-to-many" :: Text)
|
||||||
O2M cons -> [
|
, "relationship" .= (cons <> fmtEls (colName <$> relColumns) <> fmtEls (colName <$> relForeignColumns))
|
||||||
"cardinality" .= ("o2m" :: Text)
|
]
|
||||||
, "relationship" .= (cons <> fmtEls (colName <$> relColumns) <> fmtEls (colName <$> relForeignColumns))
|
|
||||||
]
|
|
||||||
|
|
||||||
data PgError = PgError Authenticated P.UsageError
|
relHint :: [Relationship] -> Text
|
||||||
|
relHint rels = T.intercalate ", " (hintList <$> rels)
|
||||||
|
where
|
||||||
|
hintList Relationship{..} =
|
||||||
|
let buildHint rel = "'" <> tableName relForeignTable <> "!" <> rel <> "'" in
|
||||||
|
case relCardinality of
|
||||||
|
M2M Junction{..} -> buildHint (tableName junTable)
|
||||||
|
M2O cons -> buildHint cons
|
||||||
|
O2M cons -> buildHint cons
|
||||||
|
|
||||||
|
data PgError = PgError Authenticated SQL.UsageError
|
||||||
type Authenticated = Bool
|
type Authenticated = Bool
|
||||||
|
|
||||||
instance PgrstError PgError where
|
instance PgrstError PgError where
|
||||||
status (PgError authed usageError) = pgErrorStatus authed usageError
|
status (PgError authed usageError) = pgErrorStatus authed usageError
|
||||||
|
|
||||||
headers err =
|
headers err =
|
||||||
if status err == HT.status401
|
if status err == HTTP.status401
|
||||||
then [ContentType.toHeader CTApplicationJSON, ("WWW-Authenticate", "Bearer") :: Header]
|
then [ContentType.toHeader CTApplicationJSON, ("WWW-Authenticate", "Bearer") :: Header]
|
||||||
else [ContentType.toHeader CTApplicationJSON]
|
else [ContentType.toHeader CTApplicationJSON]
|
||||||
|
|
||||||
instance JSON.ToJSON PgError where
|
instance JSON.ToJSON PgError where
|
||||||
toJSON (PgError _ usageError) = JSON.toJSON usageError
|
toJSON (PgError _ usageError) = JSON.toJSON usageError
|
||||||
|
|
||||||
instance JSON.ToJSON P.UsageError where
|
instance JSON.ToJSON SQL.UsageError where
|
||||||
toJSON (P.ConnectionError e) = JSON.object [
|
toJSON (SQL.ConnectionError e) = JSON.object [
|
||||||
"code" .= ("" :: Text),
|
"code" .= ("" :: Text),
|
||||||
"message" .= ("Database connection error. Retrying the connection." :: Text),
|
"message" .= ("Database connection error. Retrying the connection." :: Text),
|
||||||
"details" .= (toSL $ fromMaybe "" e :: Text)]
|
"details" .= (T.decodeUtf8With T.lenientDecode $ fromMaybe "" e :: Text)]
|
||||||
toJSON (P.SessionError e) = JSON.toJSON e -- H.Error
|
toJSON (SQL.SessionError e) = JSON.toJSON e -- SQL.Error
|
||||||
|
|
||||||
instance JSON.ToJSON H.QueryError where
|
instance JSON.ToJSON SQL.QueryError where
|
||||||
toJSON (H.QueryError _ _ e) = JSON.toJSON e
|
toJSON (SQL.QueryError _ _ e) = JSON.toJSON e
|
||||||
|
|
||||||
instance JSON.ToJSON H.CommandError where
|
instance JSON.ToJSON SQL.CommandError where
|
||||||
toJSON (H.ResultError (H.ServerError c m d h)) = case toS c of
|
toJSON (SQL.ResultError (SQL.ServerError c m d h)) = case BS.unpack c of
|
||||||
'P':'T':_ -> JSON.object [
|
'P':'T':_ -> JSON.object [
|
||||||
"details" .= (fmap toS d :: Maybe Text),
|
"details" .= fmap T.decodeUtf8 d,
|
||||||
"hint" .= (fmap toS h :: Maybe Text)]
|
"hint" .= fmap T.decodeUtf8 h]
|
||||||
|
|
||||||
_ -> JSON.object [
|
_ -> JSON.object [
|
||||||
"code" .= (toS c :: Text),
|
"code" .= (T.decodeUtf8 c :: Text),
|
||||||
"message" .= (toS m :: Text),
|
"message" .= (T.decodeUtf8 m :: Text),
|
||||||
"details" .= (fmap toS d :: Maybe Text),
|
"details" .= (fmap T.decodeUtf8 d :: Maybe Text),
|
||||||
"hint" .= (fmap toS h :: Maybe Text)]
|
"hint" .= (fmap T.decodeUtf8 h :: Maybe Text)]
|
||||||
|
|
||||||
toJSON (H.ResultError (H.UnexpectedResult m)) = JSON.object [
|
toJSON (SQL.ResultError (SQL.UnexpectedResult m)) = JSON.object [
|
||||||
"message" .= (m :: Text)]
|
"message" .= (m :: Text)]
|
||||||
toJSON (H.ResultError (H.RowError i H.EndOfInput)) = JSON.object [
|
toJSON (SQL.ResultError (SQL.RowError i SQL.EndOfInput)) = JSON.object [
|
||||||
"message" .= ("Row error: end of input" :: Text),
|
"message" .= ("Row error: end of input" :: Text),
|
||||||
"details" .= ("Attempt to parse more columns than there are in the result" :: Text),
|
"details" .= ("Attempt to parse more columns than there are in the result" :: Text),
|
||||||
"hint" .= (("Row number " <> show i) :: Text)]
|
"hint" .= (("Row number " <> show i) :: Text)]
|
||||||
toJSON (H.ResultError (H.RowError i H.UnexpectedNull)) = JSON.object [
|
toJSON (SQL.ResultError (SQL.RowError i SQL.UnexpectedNull)) = JSON.object [
|
||||||
"message" .= ("Row error: unexpected null" :: Text),
|
"message" .= ("Row error: unexpected null" :: Text),
|
||||||
"details" .= ("Attempt to parse a NULL as some value." :: Text),
|
"details" .= ("Attempt to parse a NULL as some value." :: Text),
|
||||||
"hint" .= (("Row number " <> show i) :: Text)]
|
"hint" .= (("Row number " <> show i) :: Text)]
|
||||||
toJSON (H.ResultError (H.RowError i (H.ValueError d))) = JSON.object [
|
toJSON (SQL.ResultError (SQL.RowError i (SQL.ValueError d))) = JSON.object [
|
||||||
"message" .= ("Row error: Wrong value parser used" :: Text),
|
"message" .= ("Row error: Wrong value parser used" :: Text),
|
||||||
"details" .= d,
|
"details" .= d,
|
||||||
"hint" .= (("Row number " <> show i) :: Text)]
|
"hint" .= (("Row number " <> show i) :: Text)]
|
||||||
toJSON (H.ResultError (H.UnexpectedAmountOfRows i)) = JSON.object [
|
toJSON (SQL.ResultError (SQL.UnexpectedAmountOfRows i)) = JSON.object [
|
||||||
"message" .= ("Unexpected amount of rows" :: Text),
|
"message" .= ("Unexpected amount of rows" :: Text),
|
||||||
"details" .= i]
|
"details" .= i]
|
||||||
toJSON (H.ClientError d) = JSON.object [
|
toJSON (SQL.ClientError d) = JSON.object [
|
||||||
"message" .= ("Database client error. Retrying the connection." :: Text),
|
"message" .= ("Database client error. Retrying the connection." :: Text),
|
||||||
"details" .= (fmap toS d :: Maybe Text)]
|
"details" .= (fmap T.decodeUtf8 d :: Maybe Text)]
|
||||||
|
|
||||||
pgErrorStatus :: Bool -> P.UsageError -> HT.Status
|
pgErrorStatus :: Bool -> SQL.UsageError -> HTTP.Status
|
||||||
pgErrorStatus _ (P.ConnectionError _) = HT.status503
|
pgErrorStatus _ (SQL.ConnectionError _) = HTTP.status503
|
||||||
pgErrorStatus _ (P.SessionError (H.QueryError _ _ (H.ClientError _))) = HT.status503
|
pgErrorStatus _ (SQL.SessionError (SQL.QueryError _ _ (SQL.ClientError _))) = HTTP.status503
|
||||||
pgErrorStatus authed (P.SessionError (H.QueryError _ _ (H.ResultError rError))) =
|
pgErrorStatus authed (SQL.SessionError (SQL.QueryError _ _ (SQL.ResultError rError))) =
|
||||||
case rError of
|
case rError of
|
||||||
(H.ServerError c m _ _) ->
|
(SQL.ServerError c m _ _) ->
|
||||||
case toS c of
|
case BS.unpack c of
|
||||||
'0':'8':_ -> HT.status503 -- pg connection err
|
'0':'8':_ -> HTTP.status503 -- pg connection err
|
||||||
'0':'9':_ -> HT.status500 -- triggered action exception
|
'0':'9':_ -> HTTP.status500 -- triggered action exception
|
||||||
'0':'L':_ -> HT.status403 -- invalid grantor
|
'0':'L':_ -> HTTP.status403 -- invalid grantor
|
||||||
'0':'P':_ -> HT.status403 -- invalid role specification
|
'0':'P':_ -> HTTP.status403 -- invalid role specification
|
||||||
"23503" -> HT.status409 -- foreign_key_violation
|
"23503" -> HTTP.status409 -- foreign_key_violation
|
||||||
"23505" -> HT.status409 -- unique_violation
|
"23505" -> HTTP.status409 -- unique_violation
|
||||||
"25006" -> HT.status405 -- read_only_sql_transaction
|
"25006" -> HTTP.status405 -- read_only_sql_transaction
|
||||||
'2':'5':_ -> HT.status500 -- invalid tx state
|
'2':'5':_ -> HTTP.status500 -- invalid tx state
|
||||||
'2':'8':_ -> HT.status403 -- invalid auth specification
|
'2':'8':_ -> HTTP.status403 -- invalid auth specification
|
||||||
'2':'D':_ -> HT.status500 -- invalid tx termination
|
'2':'D':_ -> HTTP.status500 -- invalid tx termination
|
||||||
'3':'8':_ -> HT.status500 -- external routine exception
|
'3':'8':_ -> HTTP.status500 -- external routine exception
|
||||||
'3':'9':_ -> HT.status500 -- external routine invocation
|
'3':'9':_ -> HTTP.status500 -- external routine invocation
|
||||||
'3':'B':_ -> HT.status500 -- savepoint exception
|
'3':'B':_ -> HTTP.status500 -- savepoint exception
|
||||||
'4':'0':_ -> HT.status500 -- tx rollback
|
'4':'0':_ -> HTTP.status500 -- tx rollback
|
||||||
'5':'3':_ -> HT.status503 -- insufficient resources
|
'5':'3':_ -> HTTP.status503 -- insufficient resources
|
||||||
'5':'4':_ -> HT.status413 -- too complex
|
'5':'4':_ -> HTTP.status413 -- too complex
|
||||||
'5':'5':_ -> HT.status500 -- obj not on prereq state
|
'5':'5':_ -> HTTP.status500 -- obj not on prereq state
|
||||||
'5':'7':_ -> HT.status500 -- operator intervention
|
'5':'7':_ -> HTTP.status500 -- operator intervention
|
||||||
'5':'8':_ -> HT.status500 -- system error
|
'5':'8':_ -> HTTP.status500 -- system error
|
||||||
'F':'0':_ -> HT.status500 -- conf file error
|
'F':'0':_ -> HTTP.status500 -- conf file error
|
||||||
'H':'V':_ -> HT.status500 -- foreign data wrapper error
|
'H':'V':_ -> HTTP.status500 -- foreign data wrapper error
|
||||||
"P0001" -> HT.status400 -- default code for "raise"
|
"P0001" -> HTTP.status400 -- default code for "raise"
|
||||||
'P':'0':_ -> HT.status500 -- PL/pgSQL Error
|
'P':'0':_ -> HTTP.status500 -- PL/pgSQL Error
|
||||||
'X':'X':_ -> HT.status500 -- internal Error
|
'X':'X':_ -> HTTP.status500 -- internal Error
|
||||||
"42883" -> HT.status404 -- undefined function
|
"42883" -> HTTP.status404 -- undefined function
|
||||||
"42P01" -> HT.status404 -- undefined table
|
"42P01" -> HTTP.status404 -- undefined table
|
||||||
"42501" -> if authed then HT.status403 else HT.status401 -- insufficient privilege
|
"42501" -> if authed then HTTP.status403 else HTTP.status401 -- insufficient privilege
|
||||||
'P':'T':n -> fromMaybe HT.status500 (HT.mkStatus <$> readMaybe n <*> pure m)
|
'P':'T':n -> fromMaybe HTTP.status500 (HTTP.mkStatus <$> readMaybe n <*> pure m)
|
||||||
_ -> HT.status400
|
_ -> HTTP.status400
|
||||||
|
|
||||||
_ -> HT.status500
|
_ -> HTTP.status500
|
||||||
|
|
||||||
checkIsFatal :: PgError -> Maybe Text
|
checkIsFatal :: PgError -> Maybe Text
|
||||||
checkIsFatal (PgError _ (P.ConnectionError e))
|
checkIsFatal (PgError _ (SQL.ConnectionError e))
|
||||||
| isAuthFailureMessage = Just $ toS failureMessage
|
| isAuthFailureMessage = Just $ toS failureMessage
|
||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
where isAuthFailureMessage = "FATAL: password authentication failed" `isPrefixOf` toS failureMessage
|
where isAuthFailureMessage = "FATAL: password authentication failed" `isPrefixOf` failureMessage
|
||||||
failureMessage = fromMaybe mempty e
|
failureMessage = BS.unpack $ fromMaybe mempty e
|
||||||
checkIsFatal (PgError _ (P.SessionError (H.QueryError _ _ (H.ResultError serverError))))
|
checkIsFatal (PgError _ (SQL.SessionError (SQL.QueryError _ _ (SQL.ResultError serverError))))
|
||||||
= case serverError of
|
= case serverError of
|
||||||
-- Check for a syntax error (42601 is the pg code). This would mean the error is on our part somehow, so we treat it as fatal.
|
-- Check for a syntax error (42601 is the pg code). This would mean the error is on our part somehow, so we treat it as fatal.
|
||||||
H.ServerError "42601" _ _ _
|
SQL.ServerError "42601" _ _ _
|
||||||
-> Just "Hint: This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues"
|
-> Just "Hint: This is probably a bug in PostgREST, please report it at https://github.com/PostgREST/postgrest/issues"
|
||||||
-- Check for a "prepared statement <name> already exists" error (Code 42P05: duplicate_prepared_statement).
|
-- Check for a "prepared statement <name> already exists" error (Code 42P05: duplicate_prepared_statement).
|
||||||
-- This would mean that a connection pooler in transaction mode is being used
|
-- This would mean that a connection pooler in transaction mode is being used
|
||||||
-- while prepared statements are enabled in the PostgREST configuration,
|
-- while prepared statements are enabled in the PostgREST configuration,
|
||||||
-- both of which are incompatible with each other.
|
-- both of which are incompatible with each other.
|
||||||
H.ServerError "42P05" _ _ _
|
SQL.ServerError "42P05" _ _ _
|
||||||
-> Just "Hint: If you are using connection poolers in transaction mode, try setting db-prepared-statements to false."
|
-> Just "Hint: If you are using connection poolers in transaction mode, try setting db-prepared-statements to false."
|
||||||
-- Check for a "transaction blocks not allowed in statement pooling mode" error (Code 08P01: protocol_violation).
|
-- Check for a "transaction blocks not allowed in statement pooling mode" error (Code 08P01: protocol_violation).
|
||||||
-- This would mean that a connection pooler in statement mode is being used which is not supported in PostgREST.
|
-- This would mean that a connection pooler in statement mode is being used which is not supported in PostgREST.
|
||||||
H.ServerError "08P01" "transaction blocks not allowed in statement pooling mode" _ _
|
SQL.ServerError "08P01" "transaction blocks not allowed in statement pooling mode" _ _
|
||||||
-> Just "Hint: Connection poolers in statement mode are not supported."
|
-> Just "Hint: Connection poolers in statement mode are not supported."
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
checkIsFatal _ = Nothing
|
checkIsFatal _ = Nothing
|
||||||
@@ -273,16 +291,16 @@ data Error
|
|||||||
| PgErr PgError
|
| PgErr PgError
|
||||||
|
|
||||||
instance PgrstError Error where
|
instance PgrstError Error where
|
||||||
status GucHeadersError = HT.status500
|
status GucHeadersError = HTTP.status500
|
||||||
status GucStatusError = HT.status500
|
status GucStatusError = HTTP.status500
|
||||||
status (BinaryFieldError _) = HT.status406
|
status (BinaryFieldError _) = HTTP.status406
|
||||||
status ConnectionLostError = HT.status503
|
status ConnectionLostError = HTTP.status503
|
||||||
status PutMatchingPkError = HT.status400
|
status PutMatchingPkError = HTTP.status400
|
||||||
status PutRangeNotAllowedError = HT.status400
|
status PutRangeNotAllowedError = HTTP.status400
|
||||||
status JwtTokenMissing = HT.status500
|
status JwtTokenMissing = HTTP.status500
|
||||||
status (JwtTokenInvalid _) = HT.unauthorized401
|
status (JwtTokenInvalid _) = HTTP.unauthorized401
|
||||||
status (SingularityError _) = HT.status406
|
status (SingularityError _) = HTTP.status406
|
||||||
status NotFound = HT.status404
|
status NotFound = HTTP.status404
|
||||||
status (PgErr err) = status err
|
status (PgErr err) = status err
|
||||||
status (ApiRequestError err) = status err
|
status (ApiRequestError err) = status err
|
||||||
|
|
||||||
@@ -298,7 +316,7 @@ instance JSON.ToJSON Error where
|
|||||||
toJSON GucStatusError = JSON.object [
|
toJSON GucStatusError = JSON.object [
|
||||||
"message" .= ("response.status guc must be a valid status code" :: Text)]
|
"message" .= ("response.status guc must be a valid status code" :: Text)]
|
||||||
toJSON (BinaryFieldError ct) = JSON.object [
|
toJSON (BinaryFieldError ct) = JSON.object [
|
||||||
"message" .= ((toS (ContentType.toMime ct) <> " requested but more than one column was selected") :: Text)]
|
"message" .= ((T.decodeUtf8 (ContentType.toMime ct) <> " requested but more than one column was selected") :: Text)]
|
||||||
toJSON ConnectionLostError = JSON.object [
|
toJSON ConnectionLostError = JSON.object [
|
||||||
"message" .= ("Database connection lost. Retrying the connection." :: Text)]
|
"message" .= ("Database connection lost. Retrying the connection." :: Text)]
|
||||||
|
|
||||||
@@ -309,7 +327,7 @@ instance JSON.ToJSON Error where
|
|||||||
|
|
||||||
toJSON (SingularityError n) = JSON.object [
|
toJSON (SingularityError n) = JSON.object [
|
||||||
"message" .= ("JSON object requested, multiple (or no) rows returned" :: Text),
|
"message" .= ("JSON object requested, multiple (or no) rows returned" :: Text),
|
||||||
"details" .= T.unwords ["Results contain", show n, "rows,", toS (ContentType.toMime CTSingularJSON), "requires 1 row"]]
|
"details" .= T.unwords ["Results contain", show n, "rows,", T.decodeUtf8 (ContentType.toMime CTSingularJSON), "requires 1 row"]]
|
||||||
|
|
||||||
toJSON JwtTokenMissing = JSON.object [
|
toJSON JwtTokenMissing = JSON.object [
|
||||||
"message" .= ("Server lacks JWT secret" :: Text)]
|
"message" .= ("Server lacks JWT secret" :: Text)]
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import qualified Data.HashMap.Strict as M
|
|||||||
|
|
||||||
import Network.HTTP.Types.Header (Header)
|
import Network.HTTP.Types.Header (Header)
|
||||||
|
|
||||||
import Protolude hiding (toS)
|
import Protolude
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
@@ -21,11 +20,11 @@ import Protolude.Conv (toS)
|
|||||||
newtype GucHeader = GucHeader (CI.CI ByteString, ByteString)
|
newtype GucHeader = GucHeader (CI.CI ByteString, ByteString)
|
||||||
|
|
||||||
instance JSON.FromJSON GucHeader where
|
instance JSON.FromJSON GucHeader where
|
||||||
parseJSON (JSON.Object o) = case headMay (M.toList o) of
|
parseJSON (JSON.Object o) =
|
||||||
Just (k, JSON.String s) | M.size o == 1 -> pure $ GucHeader (CI.mk $ toS k, toS s)
|
case M.toList o of
|
||||||
| otherwise -> mzero
|
[(k, JSON.String s)] -> pure $ GucHeader (CI.mk $ toUtf8 k, toUtf8 s)
|
||||||
_ -> mzero
|
_ -> mzero
|
||||||
parseJSON _ = mzero
|
parseJSON _ = mzero
|
||||||
|
|
||||||
unwrapGucHeader :: GucHeader -> Header
|
unwrapGucHeader :: GucHeader -> Header
|
||||||
unwrapGucHeader (GucHeader (k, v)) = (k, v)
|
unwrapGucHeader (GucHeader (k, v)) = (k, v)
|
||||||
|
|||||||
+51
-30
@@ -14,15 +14,16 @@ module PostgREST.Middleware
|
|||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
import qualified Hasql.DynamicStatements.Snippet as H hiding
|
import qualified Hasql.DynamicStatements.Snippet as SQL hiding
|
||||||
(sql)
|
(sql)
|
||||||
import qualified Hasql.DynamicStatements.Statement as H
|
import qualified Hasql.DynamicStatements.Statement as SQL
|
||||||
import qualified Hasql.Transaction as H
|
import qualified Hasql.Transaction as SQL
|
||||||
import qualified Network.HTTP.Types.Header as HTTP
|
|
||||||
import qualified Network.Wai as Wai
|
import qualified Network.Wai as Wai
|
||||||
import qualified Network.Wai.Logger as Wai
|
import qualified Network.Wai.Logger as Wai
|
||||||
import qualified Network.Wai.Middleware.Cors as Wai
|
import qualified Network.Wai.Middleware.Cors as Wai
|
||||||
@@ -30,6 +31,8 @@ import qualified Network.Wai.Middleware.Gzip as Wai
|
|||||||
import qualified Network.Wai.Middleware.RequestLogger as Wai
|
import qualified Network.Wai.Middleware.RequestLogger as Wai
|
||||||
import qualified Network.Wai.Middleware.Static as Wai
|
import qualified Network.Wai.Middleware.Static as Wai
|
||||||
|
|
||||||
|
import Control.Arrow ((***))
|
||||||
|
|
||||||
import Data.Function (id)
|
import Data.Function (id)
|
||||||
import Data.List (lookup)
|
import Data.List (lookup)
|
||||||
import Data.Scientific (FPFormat (..), formatScientific,
|
import Data.Scientific (FPFormat (..), formatScientific,
|
||||||
@@ -40,6 +43,7 @@ import System.IO.Unsafe (unsafePerformIO)
|
|||||||
import System.Log.FastLogger (toLogStr)
|
import System.Log.FastLogger (toLogStr)
|
||||||
|
|
||||||
import PostgREST.Config (AppConfig (..), LogLevel (..))
|
import PostgREST.Config (AppConfig (..), LogLevel (..))
|
||||||
|
import PostgREST.Config.PgVersion (PgVersion (..), pgVersion140)
|
||||||
import PostgREST.Error (Error, errorResponseFor)
|
import PostgREST.Error (Error, errorResponseFor)
|
||||||
import PostgREST.GucHeader (addHeadersIfNotIncluded)
|
import PostgREST.GucHeader (addHeadersIfNotIncluded)
|
||||||
import PostgREST.Query.SqlFragment (fromQi, intercalateSnippet,
|
import PostgREST.Query.SqlFragment (fromQi, intercalateSnippet,
|
||||||
@@ -48,41 +52,43 @@ import PostgREST.Request.ApiRequest (ApiRequest (..), Target (..))
|
|||||||
|
|
||||||
import PostgREST.Request.Preferences
|
import PostgREST.Request.Preferences
|
||||||
|
|
||||||
import Protolude hiding (head, toS)
|
import Protolude
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
-- | Runs local(transaction scoped) GUCs for every request, plus the pre-request function
|
-- | Runs local(transaction scoped) GUCs for every request, plus the pre-request function
|
||||||
runPgLocals :: AppConfig -> M.HashMap Text JSON.Value ->
|
runPgLocals :: AppConfig -> M.HashMap Text JSON.Value ->
|
||||||
(ApiRequest -> ExceptT Error H.Transaction Wai.Response) ->
|
(ApiRequest -> ExceptT Error SQL.Transaction Wai.Response) ->
|
||||||
ApiRequest -> ByteString -> ExceptT Error H.Transaction Wai.Response
|
ApiRequest -> ByteString -> PgVersion -> ExceptT Error SQL.Transaction Wai.Response
|
||||||
runPgLocals conf claims app req jsonDbS = do
|
runPgLocals conf claims app req jsonDbS actualPgVersion = do
|
||||||
lift $ H.statement mempty $ H.dynamicallyParameterized
|
lift $ SQL.statement mempty $ SQL.dynamicallyParameterized
|
||||||
("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql ++ specSql))
|
("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql ++ specSql))
|
||||||
HD.noResult (configDbPreparedStatements conf)
|
HD.noResult (configDbPreparedStatements conf)
|
||||||
lift $ traverse_ H.sql preReqSql
|
lift $ traverse_ SQL.sql preReqSql
|
||||||
app req
|
app req
|
||||||
where
|
where
|
||||||
methodSql = setConfigLocal mempty ("request.method", iMethod req)
|
methodSql = setConfigLocal mempty ("request.method", iMethod req)
|
||||||
pathSql = setConfigLocal mempty ("request.path", iPath req)
|
pathSql = setConfigLocal mempty ("request.path", iPath req)
|
||||||
headersSql = setConfigLocal "request.header." <$> iHeaders req
|
headersSql = if usesLegacyGucs
|
||||||
cookiesSql = setConfigLocal "request.cookie." <$> iCookies req
|
then setConfigLocal "request.header." <$> iHeaders req
|
||||||
|
else setConfigLocalJson "request.headers" (iHeaders req)
|
||||||
|
cookiesSql = if usesLegacyGucs
|
||||||
|
then setConfigLocal "request.cookie." <$> iCookies req
|
||||||
|
else setConfigLocalJson "request.cookies" (iCookies req)
|
||||||
claimsWithRole =
|
claimsWithRole =
|
||||||
let anon = JSON.String . toS $ configDbAnonRole conf in -- role claim defaults to anon if not specified in jwt
|
let anon = JSON.String . toS $ configDbAnonRole conf in -- role claim defaults to anon if not specified in jwt
|
||||||
M.union claims (M.singleton "role" anon)
|
M.union claims (M.singleton "role" anon)
|
||||||
claimsSql = setConfigLocal "request.jwt.claim." <$> [(toS c, toS $ unquoted v) | (c,v) <- M.toList claimsWithRole]
|
claimsSql = if usesLegacyGucs
|
||||||
roleSql = maybeToList $ (\x -> setConfigLocal mempty ("role", toS $ unquoted x)) <$> M.lookup "role" claimsWithRole
|
then setConfigLocal "request.jwt.claim." <$> [(toUtf8 c, toUtf8 $ unquoted v) | (c,v) <- M.toList claimsWithRole]
|
||||||
appSettingsSql = setConfigLocal mempty <$> (join bimap toS <$> configAppSettings conf)
|
else [setConfigLocal mempty ("request.jwt.claims", LBS.toStrict $ JSON.encode claimsWithRole)]
|
||||||
|
roleSql = maybeToList $ (\x -> setConfigLocal mempty ("role", toUtf8 $ unquoted x)) <$> M.lookup "role" claimsWithRole
|
||||||
|
appSettingsSql = setConfigLocal mempty <$> (join bimap toUtf8 <$> configAppSettings conf)
|
||||||
searchPathSql =
|
searchPathSql =
|
||||||
let schemas = T.intercalate ", " (iSchema req : configDbExtraSearchPath conf) in
|
let schemas = T.intercalate ", " (iSchema req : configDbExtraSearchPath conf) in
|
||||||
setConfigLocal mempty ("search_path", toS schemas)
|
setConfigLocal mempty ("search_path", toUtf8 schemas)
|
||||||
preReqSql = (\f -> "select " <> fromQi f <> "();") <$> configDbPreRequest conf
|
preReqSql = (\f -> "select " <> fromQi f <> "();") <$> configDbPreRequest conf
|
||||||
specSql = case iTarget req of
|
specSql = case iTarget req of
|
||||||
TargetProc{tpIsRootSpec=True} -> [setConfigLocal mempty ("request.spec", jsonDbS)]
|
TargetProc{tpIsRootSpec=True} -> [setConfigLocal mempty ("request.spec", jsonDbS)]
|
||||||
_ -> mempty
|
_ -> mempty
|
||||||
-- | Do a pg set_config(setting, value, true) call. This is equivalent to a SET LOCAL.
|
usesLegacyGucs = configDbUseLegacyGucs conf && actualPgVersion < pgVersion140
|
||||||
setConfigLocal :: ByteString -> (ByteString, ByteString) -> H.Snippet
|
|
||||||
setConfigLocal prefix (k, v) =
|
|
||||||
"set_config(" <> unknownEncoder (prefix <> k) <> ", " <> unknownEncoder v <> ", true)"
|
|
||||||
|
|
||||||
-- | Log in apache format. Only requests that have a status greater than minStatus are logged.
|
-- | Log in apache format. Only requests that have a status greater than minStatus are logged.
|
||||||
-- | There's no way to filter logs in the apache format on wai-extra: https://hackage.haskell.org/package/wai-extra-3.0.29.2/docs/Network-Wai-Middleware-RequestLogger.html#t:OutputFormat.
|
-- | There's no way to filter logs in the apache format on wai-extra: https://hackage.haskell.org/package/wai-extra-3.0.29.2/docs/Network-Wai-Middleware-RequestLogger.html#t:OutputFormat.
|
||||||
@@ -145,27 +151,27 @@ corsPolicy req = case lookup "origin" headers of
|
|||||||
where
|
where
|
||||||
headers = Wai.requestHeaders req
|
headers = Wai.requestHeaders req
|
||||||
accHeaders = case lookup "access-control-request-headers" headers of
|
accHeaders = case lookup "access-control-request-headers" headers of
|
||||||
Just hdrs -> map (CI.mk . toS . T.strip . toS) $ BS.split ',' hdrs
|
Just hdrs -> map (CI.mk . BS.strip) $ BS.split ',' hdrs
|
||||||
Nothing -> []
|
Nothing -> []
|
||||||
|
|
||||||
unquoted :: JSON.Value -> Text
|
unquoted :: JSON.Value -> Text
|
||||||
unquoted (JSON.String t) = t
|
unquoted (JSON.String t) = t
|
||||||
unquoted (JSON.Number n) =
|
unquoted (JSON.Number n) =
|
||||||
toS $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
|
toS $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
|
||||||
unquoted (JSON.Bool b) = show b
|
unquoted (JSON.Bool b) = show b
|
||||||
unquoted v = toS $ JSON.encode v
|
unquoted v = T.decodeUtf8 . LBS.toStrict $ JSON.encode v
|
||||||
|
|
||||||
-- | Set a transaction to eventually roll back if requested and set respective
|
-- | Set a transaction to eventually roll back if requested and set respective
|
||||||
-- headers on the response.
|
-- headers on the response.
|
||||||
optionalRollback
|
optionalRollback
|
||||||
:: AppConfig
|
:: AppConfig
|
||||||
-> ApiRequest
|
-> ApiRequest
|
||||||
-> ExceptT Error H.Transaction Wai.Response
|
-> ExceptT Error SQL.Transaction Wai.Response
|
||||||
-> ExceptT Error H.Transaction Wai.Response
|
-> ExceptT Error SQL.Transaction Wai.Response
|
||||||
optionalRollback AppConfig{..} ApiRequest{..} transaction = do
|
optionalRollback AppConfig{..} ApiRequest{..} transaction = do
|
||||||
resp <- catchError transaction $ return . errorResponseFor
|
resp <- catchError transaction $ return . errorResponseFor
|
||||||
when (shouldRollback || (configDbTxRollbackAll && not shouldCommit))
|
when (shouldRollback || (configDbTxRollbackAll && not shouldCommit))
|
||||||
(lift H.condemn)
|
(lift SQL.condemn)
|
||||||
return $ Wai.mapResponseHeaders preferenceApplied resp
|
return $ Wai.mapResponseHeaders preferenceApplied resp
|
||||||
where
|
where
|
||||||
shouldCommit =
|
shouldCommit =
|
||||||
@@ -175,9 +181,24 @@ optionalRollback AppConfig{..} ApiRequest{..} transaction = do
|
|||||||
preferenceApplied
|
preferenceApplied
|
||||||
| shouldCommit =
|
| shouldCommit =
|
||||||
addHeadersIfNotIncluded
|
addHeadersIfNotIncluded
|
||||||
[(HTTP.hPreferenceApplied, BS.pack (show Commit))]
|
[toAppliedHeader Commit]
|
||||||
| shouldRollback =
|
| shouldRollback =
|
||||||
addHeadersIfNotIncluded
|
addHeadersIfNotIncluded
|
||||||
[(HTTP.hPreferenceApplied, BS.pack (show Rollback))]
|
[toAppliedHeader Rollback]
|
||||||
| otherwise =
|
| otherwise =
|
||||||
identity
|
identity
|
||||||
|
|
||||||
|
-- | Do a pg set_config(setting, value, true) call. This is equivalent to a SET LOCAL.
|
||||||
|
setConfigLocal :: ByteString -> (ByteString, ByteString) -> SQL.Snippet
|
||||||
|
setConfigLocal prefix (k, v) =
|
||||||
|
"set_config(" <> unknownEncoder (prefix <> k) <> ", " <> unknownEncoder v <> ", true)"
|
||||||
|
|
||||||
|
-- | Starting from PostgreSQL v14, some characters are not allowed for config names (mostly affecting headers with "-").
|
||||||
|
-- | A JSON format string is used to avoid this problem. See https://github.com/PostgREST/postgrest/issues/1857
|
||||||
|
setConfigLocalJson :: ByteString -> [(ByteString, ByteString)] -> [SQL.Snippet]
|
||||||
|
setConfigLocalJson prefix keyVals = [setConfigLocal mempty (prefix, gucJsonVal keyVals)]
|
||||||
|
where
|
||||||
|
gucJsonVal :: [(ByteString, ByteString)] -> ByteString
|
||||||
|
gucJsonVal = LBS.toStrict . JSON.encode . M.fromList . arrayByteStringToText
|
||||||
|
arrayByteStringToText :: [(ByteString, ByteString)] -> [(Text,Text)]
|
||||||
|
arrayByteStringToText keyVal = (T.decodeUtf8 *** T.decodeUtf8) <$> keyVal
|
||||||
|
|||||||
+30
-18
@@ -7,11 +7,13 @@ Description : Generates the OpenAPI output
|
|||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
module PostgREST.OpenAPI (encode) where
|
module PostgREST.OpenAPI (encode) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.HashMap.Strict as HashMap
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.HashSet.InsOrd as Set
|
import qualified Data.HashMap.Strict as M
|
||||||
import qualified Data.Text as T
|
import qualified Data.HashSet.InsOrd as Set
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Encoding as T
|
||||||
|
|
||||||
import Control.Arrow ((&&&))
|
import Control.Arrow ((&&&))
|
||||||
import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList)
|
import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList)
|
||||||
@@ -27,8 +29,8 @@ import PostgREST.Config (AppConfig (..), Proxy (..),
|
|||||||
isMalformedProxyUri, toURI)
|
isMalformedProxyUri, toURI)
|
||||||
import PostgREST.DbStructure (DbStructure (..),
|
import PostgREST.DbStructure (DbStructure (..),
|
||||||
tableCols, tablePKCols)
|
tableCols, tablePKCols)
|
||||||
import PostgREST.DbStructure.Proc (PgArg (..),
|
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
||||||
ProcDescription (..))
|
ProcParam (..))
|
||||||
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
||||||
PrimaryKey (..),
|
PrimaryKey (..),
|
||||||
Relationship (..))
|
Relationship (..))
|
||||||
@@ -37,22 +39,21 @@ import PostgREST.Version (docsVersion, prettyVersion)
|
|||||||
|
|
||||||
import PostgREST.ContentType
|
import PostgREST.ContentType
|
||||||
|
|
||||||
import Protolude hiding (Proxy, get, toS)
|
import Protolude hiding (Proxy, get)
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
encode :: AppConfig -> DbStructure -> [Table] -> HashMap.HashMap k [ProcDescription] -> Maybe Text -> LBS.ByteString
|
encode :: AppConfig -> DbStructure -> [Table] -> M.HashMap k [ProcDescription] -> Maybe Text -> LBS.ByteString
|
||||||
encode conf dbStructure tables procs schemaDescription =
|
encode conf dbStructure tables procs schemaDescription =
|
||||||
JSON.encode $
|
JSON.encode $
|
||||||
postgrestSpec
|
postgrestSpec
|
||||||
(dbRelationships dbStructure)
|
(dbRelationships dbStructure)
|
||||||
(concat $ HashMap.elems procs)
|
(concat $ M.elems procs)
|
||||||
(openApiTableInfo dbStructure <$> tables)
|
(openApiTableInfo dbStructure <$> tables)
|
||||||
(proxyUri conf)
|
(proxyUri conf)
|
||||||
schemaDescription
|
schemaDescription
|
||||||
(dbPrimaryKeys dbStructure)
|
(dbPrimaryKeys dbStructure)
|
||||||
|
|
||||||
makeMimeList :: [ContentType] -> MimeList
|
makeMimeList :: [ContentType] -> MimeList
|
||||||
makeMimeList cs = MimeList $ fmap (fromString . toS . toMime) cs
|
makeMimeList cs = MimeList $ fmap (fromString . BS.unpack . toMime) cs
|
||||||
|
|
||||||
toSwaggerType :: Text -> SwaggerType t
|
toSwaggerType :: Text -> SwaggerType t
|
||||||
toSwaggerType "character varying" = SwaggerString
|
toSwaggerType "character varying" = SwaggerString
|
||||||
@@ -65,8 +66,19 @@ toSwaggerType "bigint" = SwaggerInteger
|
|||||||
toSwaggerType "numeric" = SwaggerNumber
|
toSwaggerType "numeric" = SwaggerNumber
|
||||||
toSwaggerType "real" = SwaggerNumber
|
toSwaggerType "real" = SwaggerNumber
|
||||||
toSwaggerType "double precision" = SwaggerNumber
|
toSwaggerType "double precision" = SwaggerNumber
|
||||||
|
toSwaggerType "ARRAY" = SwaggerArray
|
||||||
toSwaggerType _ = SwaggerString
|
toSwaggerType _ = SwaggerString
|
||||||
|
|
||||||
|
parseDefault :: Text -> Text -> Text
|
||||||
|
parseDefault colType colDefault =
|
||||||
|
case toSwaggerType colType of
|
||||||
|
SwaggerString -> wrapInQuotations $ case T.stripSuffix ("::" <> colType) colDefault of
|
||||||
|
Just def -> T.dropAround (=='\'') def
|
||||||
|
Nothing -> colDefault
|
||||||
|
_ -> colDefault
|
||||||
|
where
|
||||||
|
wrapInQuotations text = "\"" <> text <> "\""
|
||||||
|
|
||||||
makeTableDef :: [Relationship] -> [PrimaryKey] -> (Table, [Column], [Text]) -> (Text, Schema)
|
makeTableDef :: [Relationship] -> [PrimaryKey] -> (Table, [Column], [Text]) -> (Text, Schema)
|
||||||
makeTableDef rels pks (t, cs, _) =
|
makeTableDef rels pks (t, cs, _) =
|
||||||
let tn = tableName t in
|
let tn = tableName t in
|
||||||
@@ -107,7 +119,7 @@ makeProperty rels pks c = (colName c, Inline s)
|
|||||||
colDescription c
|
colDescription c
|
||||||
s =
|
s =
|
||||||
(mempty :: Schema)
|
(mempty :: Schema)
|
||||||
& default_ .~ (JSON.decode . toS =<< colDefault c)
|
& default_ .~ (JSON.decode . toUtf8Lazy . parseDefault (colType c) =<< colDefault c)
|
||||||
& description .~ d
|
& description .~ d
|
||||||
& enum_ .~ e
|
& enum_ .~ e
|
||||||
& format ?~ colType c
|
& format ?~ colType c
|
||||||
@@ -119,11 +131,11 @@ makeProcSchema pd =
|
|||||||
(mempty :: Schema)
|
(mempty :: Schema)
|
||||||
& description .~ pdDescription pd
|
& description .~ pdDescription pd
|
||||||
& type_ ?~ SwaggerObject
|
& type_ ?~ SwaggerObject
|
||||||
& properties .~ fromList (fmap makeProcProperty (pdArgs pd))
|
& properties .~ fromList (fmap makeProcProperty (pdParams pd))
|
||||||
& required .~ fmap pgaName (filter pgaReq (pdArgs pd))
|
& required .~ fmap ppName (filter ppReq (pdParams pd))
|
||||||
|
|
||||||
makeProcProperty :: PgArg -> (Text, Referenced Schema)
|
makeProcProperty :: ProcParam -> (Text, Referenced Schema)
|
||||||
makeProcProperty (PgArg n t _ _) = (n, Inline s)
|
makeProcProperty (ProcParam n t _ _) = (n, Inline s)
|
||||||
where
|
where
|
||||||
s = (mempty :: Schema)
|
s = (mempty :: Schema)
|
||||||
& type_ ?~ toSwaggerType t
|
& type_ ?~ toSwaggerType t
|
||||||
@@ -313,7 +325,7 @@ postgrestSpec rels pds ti (s, h, p, b) sd pks = (mempty :: Swagger)
|
|||||||
& basePath ?~ T.unpack b
|
& basePath ?~ T.unpack b
|
||||||
& schemes ?~ [s']
|
& schemes ?~ [s']
|
||||||
& info .~ ((mempty :: Info)
|
& info .~ ((mempty :: Info)
|
||||||
& version .~ prettyVersion
|
& version .~ T.decodeUtf8 prettyVersion
|
||||||
& title .~ "PostgREST API"
|
& title .~ "PostgREST API"
|
||||||
& description ?~ d)
|
& description ?~ d)
|
||||||
& externalDocs ?~ ((mempty :: ExternalDocs)
|
& externalDocs ?~ ((mempty :: ExternalDocs)
|
||||||
|
|||||||
@@ -17,30 +17,27 @@ module PostgREST.Query.QueryBuilder
|
|||||||
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
import qualified Hasql.DynamicStatements.Snippet as H
|
import qualified Hasql.DynamicStatements.Snippet as SQL
|
||||||
|
|
||||||
import Data.Tree (Tree (..))
|
import Data.Tree (Tree (..))
|
||||||
|
|
||||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..))
|
||||||
QualifiedIdentifier (..))
|
import PostgREST.DbStructure.Proc (ProcParam (..))
|
||||||
import PostgREST.DbStructure.Proc (PgArg (..))
|
|
||||||
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
||||||
Relationship (..))
|
Relationship (..))
|
||||||
import PostgREST.DbStructure.Table (Table (..))
|
import PostgREST.DbStructure.Table (Table (..))
|
||||||
import PostgREST.Request.ApiRequest (PayloadJSON (..))
|
import PostgREST.Request.Preferences (PreferResolution (..))
|
||||||
import PostgREST.Request.Preferences (PreferParameters (..),
|
|
||||||
PreferResolution (..))
|
|
||||||
|
|
||||||
import PostgREST.Query.SqlFragment
|
import PostgREST.Query.SqlFragment
|
||||||
import PostgREST.Request.Types
|
import PostgREST.Request.Types
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
readRequestToQuery :: ReadRequest -> H.Snippet
|
readRequestToQuery :: ReadRequest -> SQL.Snippet
|
||||||
readRequestToQuery (Node (Select colSelects mainQi tblAlias implJoins logicForest joinConditions_ ordts range, _) forest) =
|
readRequestToQuery (Node (Select colSelects mainQi tblAlias implJoins logicForest joinConditions_ ordts range, _) forest) =
|
||||||
"SELECT " <>
|
"SELECT " <>
|
||||||
intercalateSnippet ", " ((pgFmtSelectItem qi <$> colSelects) ++ selects) <>
|
intercalateSnippet ", " ((pgFmtSelectItem qi <$> colSelects) ++ selects) <>
|
||||||
"FROM " <> H.sql (BS.intercalate ", " (tabl : implJs)) <> " " <>
|
"FROM " <> SQL.sql (BS.intercalate ", " (tabl : implJs)) <> " " <>
|
||||||
intercalateSnippet " " joins <> " " <>
|
intercalateSnippet " " joins <> " " <>
|
||||||
(if null logicForest && null joinConditions_ then mempty else "WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition joinConditions_))
|
(if null logicForest && null joinConditions_ then mempty else "WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition joinConditions_))
|
||||||
<> " " <>
|
<> " " <>
|
||||||
@@ -52,33 +49,46 @@ readRequestToQuery (Node (Select colSelects mainQi tblAlias implJoins logicFores
|
|||||||
qi = maybe mainQi (QualifiedIdentifier mempty) tblAlias
|
qi = maybe mainQi (QualifiedIdentifier mempty) tblAlias
|
||||||
(joins, selects) = foldr getJoinsSelects ([],[]) forest
|
(joins, selects) = foldr getJoinsSelects ([],[]) forest
|
||||||
|
|
||||||
getJoinsSelects :: ReadRequest -> ([H.Snippet], [H.Snippet]) -> ([H.Snippet], [H.Snippet])
|
getJoinsSelects :: ReadRequest -> ([SQL.Snippet], [SQL.Snippet]) -> ([SQL.Snippet], [SQL.Snippet])
|
||||||
getJoinsSelects rr@(Node (_, (name, Just Relationship{relCardinality=card,relTable=Table{tableName=table}}, alias, _, _)) _) (j,s) =
|
getJoinsSelects rr@(Node (_, (name, Just Relationship{relCardinality=card,relTable=Table{tableName=table}}, alias, _, joinType, _)) _) (joins,selects) =
|
||||||
let subquery = readRequestToQuery rr in
|
let subquery = readRequestToQuery rr in
|
||||||
case card of
|
case card of
|
||||||
M2O _ ->
|
M2O _ ->
|
||||||
let aliasOrName = fromMaybe name alias
|
let aliasOrName = fromMaybe name alias
|
||||||
localTableName = pgFmtIdent $ table <> "_" <> aliasOrName
|
localTableName = pgFmtIdent $ table <> "_" <> aliasOrName
|
||||||
sel = H.sql ("row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName)
|
sel = SQL.sql ("row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName)
|
||||||
joi = " LEFT JOIN LATERAL( " <> subquery <> " ) AS " <> H.sql localTableName <> " ON TRUE " in
|
joi = (if joinType == Just JTInner then " INNER" else " LEFT")
|
||||||
(joi:j,sel:s)
|
<> " JOIN LATERAL( " <> subquery <> " ) AS " <> SQL.sql localTableName <> " ON TRUE " in
|
||||||
_ ->
|
(joi:joins,sel:selects)
|
||||||
let sel = "COALESCE (("
|
_ -> case joinType of
|
||||||
<> "SELECT json_agg(" <> H.sql (pgFmtIdent table) <> ".*) "
|
Just JTInner ->
|
||||||
<> "FROM (" <> subquery <> ") " <> H.sql (pgFmtIdent table) <> " "
|
let aliasOrName = fromMaybe name alias
|
||||||
<> "), '[]') AS " <> H.sql (pgFmtIdent (fromMaybe name alias)) in
|
locTblName = table <> "_" <> aliasOrName
|
||||||
(j,sel:s)
|
localTableName = pgFmtIdent locTblName
|
||||||
getJoinsSelects (Node (_, (_, Nothing, _, _, _)) _) _ = ([], [])
|
internalTableName = pgFmtIdent $ "_" <> locTblName
|
||||||
|
sel = SQL.sql $ localTableName <> "." <> internalTableName <> " AS " <> pgFmtIdent aliasOrName
|
||||||
|
joi = "INNER JOIN LATERAL(" <>
|
||||||
|
"SELECT json_agg(" <> SQL.sql internalTableName <> ") AS " <> SQL.sql internalTableName <>
|
||||||
|
"FROM (" <> subquery <> " ) AS " <> SQL.sql internalTableName <>
|
||||||
|
") AS " <> SQL.sql localTableName <> " ON " <> SQL.sql localTableName <> "IS NOT NULL" in
|
||||||
|
(joi:joins,sel:selects)
|
||||||
|
_ ->
|
||||||
|
let sel = "COALESCE (("
|
||||||
|
<> "SELECT json_agg(" <> SQL.sql (pgFmtIdent table) <> ".*) "
|
||||||
|
<> "FROM (" <> subquery <> ") " <> SQL.sql (pgFmtIdent table) <> " "
|
||||||
|
<> "), '[]') AS " <> SQL.sql (pgFmtIdent (fromMaybe name alias)) in
|
||||||
|
(joins,sel:selects)
|
||||||
|
getJoinsSelects (Node (_, (_, Nothing, _, _, _, _)) _) _ = ([], [])
|
||||||
|
|
||||||
mutateRequestToQuery :: MutateRequest -> H.Snippet
|
mutateRequestToQuery :: MutateRequest -> SQL.Snippet
|
||||||
mutateRequestToQuery (Insert mainQi iCols body onConflct putConditions returnings) =
|
mutateRequestToQuery (Insert mainQi iCols body onConflct putConditions returnings) =
|
||||||
"WITH " <> normalizedBody body <> " " <>
|
"WITH " <> normalizedBody body <> " " <>
|
||||||
"INSERT INTO " <> H.sql (fromQi mainQi) <> H.sql (if S.null iCols then " " else "(" <> cols <> ") ") <>
|
"INSERT INTO " <> SQL.sql (fromQi mainQi) <> SQL.sql (if S.null iCols then " " else "(" <> cols <> ") ") <>
|
||||||
"SELECT " <> H.sql cols <> " " <>
|
"SELECT " <> SQL.sql cols <> " " <>
|
||||||
H.sql ("FROM json_populate_recordset (null::" <> fromQi mainQi <> ", " <> selectBody <> ") _ ") <>
|
SQL.sql ("FROM json_populate_recordset (null::" <> fromQi mainQi <> ", " <> selectBody <> ") _ ") <>
|
||||||
-- Only used for PUT
|
-- Only used for PUT
|
||||||
(if null putConditions then mempty else "WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree (QualifiedIdentifier mempty "_") <$> putConditions)) <>
|
(if null putConditions then mempty else "WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree (QualifiedIdentifier mempty "_") <$> putConditions)) <>
|
||||||
H.sql (BS.unwords [
|
SQL.sql (BS.unwords [
|
||||||
maybe "" (\(oncDo, oncCols) ->
|
maybe "" (\(oncDo, oncCols) ->
|
||||||
if null oncCols then
|
if null oncCols then
|
||||||
mempty
|
mempty
|
||||||
@@ -100,13 +110,13 @@ mutateRequestToQuery (Update mainQi uCols body logicForest returnings) =
|
|||||||
-- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax
|
-- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax
|
||||||
-- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select=
|
-- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select=
|
||||||
-- the select has to be based on "returnings" to make computed overloaded functions not throw
|
-- the select has to be based on "returnings" to make computed overloaded functions not throw
|
||||||
then H.sql ("SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false")
|
then SQL.sql ("SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false")
|
||||||
else
|
else
|
||||||
"WITH " <> normalizedBody body <> " " <>
|
"WITH " <> normalizedBody body <> " " <>
|
||||||
"UPDATE " <> H.sql (fromQi mainQi) <> " SET " <> H.sql cols <> " " <>
|
"UPDATE " <> SQL.sql (fromQi mainQi) <> " SET " <> SQL.sql cols <> " " <>
|
||||||
"FROM (SELECT * FROM json_populate_recordset (null::" <> H.sql (fromQi mainQi) <> " , " <> H.sql selectBody <> " )) _ " <>
|
"FROM (SELECT * FROM json_populate_recordset (null::" <> SQL.sql (fromQi mainQi) <> " , " <> SQL.sql selectBody <> " )) _ " <>
|
||||||
(if null logicForest then mempty else "WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)) <> " " <>
|
(if null logicForest then mempty else "WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)) <> " " <>
|
||||||
H.sql (returningF mainQi returnings)
|
SQL.sql (returningF mainQi returnings)
|
||||||
where
|
where
|
||||||
cols = BS.intercalate ", " (pgFmtIdent <> const " = _." <> pgFmtIdent <$> S.toList uCols)
|
cols = BS.intercalate ", " (pgFmtIdent <> const " = _." <> pgFmtIdent <$> S.toList uCols)
|
||||||
emptyBodyReturnedColumns :: SqlFragment
|
emptyBodyReturnedColumns :: SqlFragment
|
||||||
@@ -114,42 +124,39 @@ mutateRequestToQuery (Update mainQi uCols body logicForest returnings) =
|
|||||||
| null returnings = "NULL"
|
| null returnings = "NULL"
|
||||||
| otherwise = BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)
|
| otherwise = BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)
|
||||||
mutateRequestToQuery (Delete mainQi logicForest returnings) =
|
mutateRequestToQuery (Delete mainQi logicForest returnings) =
|
||||||
"DELETE FROM " <> H.sql (fromQi mainQi) <> " " <>
|
"DELETE FROM " <> SQL.sql (fromQi mainQi) <> " " <>
|
||||||
(if null logicForest then mempty else "WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree mainQi) logicForest)) <> " " <>
|
(if null logicForest then mempty else "WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree mainQi) logicForest)) <> " " <>
|
||||||
H.sql (returningF mainQi returnings)
|
SQL.sql (returningF mainQi returnings)
|
||||||
|
|
||||||
requestToCallProcQuery :: QualifiedIdentifier -> [PgArg] -> Maybe PayloadJSON -> Bool -> Maybe PreferParameters -> [FieldName] -> H.Snippet
|
requestToCallProcQuery :: CallRequest -> SQL.Snippet
|
||||||
requestToCallProcQuery qi pgArgs pj returnsScalar preferParams returnings =
|
requestToCallProcQuery (FunctionCall qi params args returnsScalar multipleCall returnings) =
|
||||||
argsCTE <> sourceBody
|
prmsCTE <> argsBody
|
||||||
where
|
where
|
||||||
body = pjRaw <$> pj
|
(prmsCTE, argFrag) = case params of
|
||||||
paramsAsSingleObject = preferParams == Just SingleObject
|
OnePosParam prm -> ("WITH pgrst_args AS (SELECT NULL)", singleParameter args (encodeUtf8 $ ppType prm))
|
||||||
paramsAsMultipleObjects = preferParams == Just MultipleObjects
|
KeyParams [] -> (mempty, mempty)
|
||||||
|
KeyParams prms -> (
|
||||||
(argsCTE, args)
|
"WITH " <> normalizedBody args <> ", " <>
|
||||||
| null pgArgs = (mempty, mempty)
|
SQL.sql (
|
||||||
| paramsAsSingleObject = ("WITH pgrst_args AS (SELECT NULL)", jsonPlaceHolder body)
|
|
||||||
| otherwise = (
|
|
||||||
"WITH " <> normalizedBody body <> ", " <>
|
|
||||||
H.sql (
|
|
||||||
BS.unwords [
|
BS.unwords [
|
||||||
"pgrst_args AS (",
|
"pgrst_args AS (",
|
||||||
"SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <> fmtArgs (const mempty) (\a -> " " <> encodeUtf8 (pgaType a)) <> ")",
|
"SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <> fmtParams prms (const mempty) (\a -> " " <> encodeUtf8 (ppType a)) <> ")",
|
||||||
")"])
|
")"])
|
||||||
, H.sql $ if paramsAsMultipleObjects
|
, SQL.sql $ if multipleCall
|
||||||
then fmtArgs varadicPrefix (\a -> " := pgrst_args." <> pgFmtIdent (pgaName a))
|
then fmtParams prms varadicPrefix (\a -> " := pgrst_args." <> pgFmtIdent (ppName a))
|
||||||
else fmtArgs varadicPrefix (\a -> " := (SELECT " <> pgFmtIdent (pgaName a) <> " FROM pgrst_args LIMIT 1)")
|
else fmtParams prms varadicPrefix (\a -> " := (SELECT " <> pgFmtIdent (ppName a) <> " FROM pgrst_args LIMIT 1)")
|
||||||
)
|
)
|
||||||
|
|
||||||
fmtArgs :: (PgArg -> SqlFragment) -> (PgArg -> SqlFragment) -> SqlFragment
|
fmtParams :: [ProcParam] -> (ProcParam -> SqlFragment) -> (ProcParam -> SqlFragment) -> SqlFragment
|
||||||
fmtArgs argFragPre argFragSuf = BS.intercalate ", " ((\a -> argFragPre a <> pgFmtIdent (pgaName a) <> argFragSuf a) <$> pgArgs)
|
fmtParams prms prmFragPre prmFragSuf = BS.intercalate ", "
|
||||||
|
((\a -> prmFragPre a <> pgFmtIdent (ppName a) <> prmFragSuf a) <$> prms)
|
||||||
|
|
||||||
varadicPrefix :: PgArg -> SqlFragment
|
varadicPrefix :: ProcParam -> SqlFragment
|
||||||
varadicPrefix a = if pgaVar a then "VARIADIC " else mempty
|
varadicPrefix a = if ppVar a then "VARIADIC " else mempty
|
||||||
|
|
||||||
sourceBody :: H.Snippet
|
argsBody :: SQL.Snippet
|
||||||
sourceBody
|
argsBody
|
||||||
| paramsAsMultipleObjects =
|
| multipleCall =
|
||||||
if returnsScalar
|
if returnsScalar
|
||||||
then "SELECT " <> callIt <> " AS pgrst_scalar FROM pgrst_args"
|
then "SELECT " <> callIt <> " AS pgrst_scalar FROM pgrst_args"
|
||||||
else "SELECT pgrst_lat_args.* FROM pgrst_args, " <>
|
else "SELECT pgrst_lat_args.* FROM pgrst_args, " <>
|
||||||
@@ -159,23 +166,41 @@ requestToCallProcQuery qi pgArgs pj returnsScalar preferParams returnings =
|
|||||||
then "SELECT " <> callIt <> " AS pgrst_scalar"
|
then "SELECT " <> callIt <> " AS pgrst_scalar"
|
||||||
else "SELECT " <> returnedColumns <> " FROM " <> callIt
|
else "SELECT " <> returnedColumns <> " FROM " <> callIt
|
||||||
|
|
||||||
callIt :: H.Snippet
|
callIt :: SQL.Snippet
|
||||||
callIt = H.sql (fromQi qi) <> "(" <> args <> ")"
|
callIt = SQL.sql (fromQi qi) <> "(" <> argFrag <> ")"
|
||||||
|
|
||||||
returnedColumns :: H.Snippet
|
returnedColumns :: SQL.Snippet
|
||||||
returnedColumns
|
returnedColumns
|
||||||
| null returnings = "*"
|
| null returnings = "*"
|
||||||
| otherwise = H.sql $ BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName qi) <$> returnings)
|
| otherwise = SQL.sql $ BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName qi) <$> returnings)
|
||||||
|
|
||||||
|
|
||||||
-- | SQL query meant for COUNTing the root node of the Tree.
|
-- | SQL query meant for COUNTing the root node of the Tree.
|
||||||
-- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT.
|
-- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT.
|
||||||
-- SELECT 1 is done instead of SELECT * to prevent doing expensive operations(like functions based on the columns)
|
-- SELECT 1 is done instead of SELECT * to prevent doing expensive operations(like functions based on the columns)
|
||||||
-- inside the FROM target.
|
-- inside the FROM target.
|
||||||
readRequestToCountQuery :: ReadRequest -> H.Snippet
|
-- If the request contains INNER JOINs, then the COUNT of the root node will change.
|
||||||
readRequestToCountQuery (Node (Select{from=qi, where_=logicForest}, _) _) =
|
-- For this case, we use a WHERE EXISTS instead of an INNER JOIN on the count query.
|
||||||
"SELECT 1 " <> "FROM " <> H.sql (fromQi qi) <> " " <>
|
-- See https://github.com/PostgREST/postgrest/issues/2009#issuecomment-977473031
|
||||||
if null logicForest then mempty else "WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest)
|
-- Only for the nodes that have an INNER JOIN linked to the root level.
|
||||||
|
readRequestToCountQuery :: ReadRequest -> SQL.Snippet
|
||||||
|
readRequestToCountQuery (Node (Select{from=qi, implicitJoins=implJoins, where_=logicForest, joinConditions=joinConditions_}, _) forest) =
|
||||||
|
"SELECT 1 FROM " <> SQL.sql (BS.intercalate ", " (fromQi qi:(fromQi <$> implJoins))) <>
|
||||||
|
(if null logicForest && null joinConditions_ && null subQueries
|
||||||
|
then mempty
|
||||||
|
else " WHERE " ) <>
|
||||||
|
intercalateSnippet " AND " (
|
||||||
|
map (pgFmtLogicTree qi) logicForest ++
|
||||||
|
map pgFmtJoinCondition joinConditions_ ++
|
||||||
|
subQueries
|
||||||
|
)
|
||||||
|
where
|
||||||
|
subQueries = foldr existsSubquery [] forest
|
||||||
|
existsSubquery :: ReadRequest -> [SQL.Snippet] -> [SQL.Snippet]
|
||||||
|
existsSubquery readReq@(Node (_, (_, _, _, _, joinType, _)) _) rest =
|
||||||
|
if joinType == Just JTInner
|
||||||
|
then ("EXISTS (" <> readRequestToCountQuery readReq <> " )"):rest
|
||||||
|
else mempty
|
||||||
|
|
||||||
limitedQuery :: H.Snippet -> Maybe Integer -> H.Snippet
|
limitedQuery :: SQL.Snippet -> Maybe Integer -> SQL.Snippet
|
||||||
limitedQuery query maxRows = query <> H.sql (maybe mempty (\x -> " LIMIT " <> BS.pack (show x)) maxRows)
|
limitedQuery query maxRows = query <> SQL.sql (maybe mempty (\x -> " LIMIT " <> BS.pack (show x)) maxRows)
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ module PostgREST.Query.SqlFragment
|
|||||||
, countF
|
, countF
|
||||||
, fromQi
|
, fromQi
|
||||||
, ftsOperators
|
, ftsOperators
|
||||||
, jsonPlaceHolder
|
|
||||||
, limitOffsetF
|
, limitOffsetF
|
||||||
, locationF
|
, locationF
|
||||||
, normalizedBody
|
, normalizedBody
|
||||||
@@ -31,22 +30,22 @@ module PostgREST.Query.SqlFragment
|
|||||||
, responseStatusF
|
, responseStatusF
|
||||||
, returningF
|
, returningF
|
||||||
, selectBody
|
, selectBody
|
||||||
|
, singleParameter
|
||||||
, sourceCTEName
|
, sourceCTEName
|
||||||
, unknownEncoder
|
, unknownEncoder
|
||||||
, intercalateSnippet
|
, intercalateSnippet
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as M
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Hasql.DynamicStatements.Snippet as H
|
import qualified Hasql.DynamicStatements.Snippet as SQL
|
||||||
import qualified Hasql.Encoders as HE
|
import qualified Hasql.Encoders as HE
|
||||||
|
|
||||||
import Data.Foldable (foldr1)
|
import Data.Foldable (foldr1)
|
||||||
import Text.InterpolatedString.Perl6 (qc)
|
import Text.InterpolatedString.Perl6 (qc)
|
||||||
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion96)
|
|
||||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..))
|
QualifiedIdentifier (..))
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
@@ -55,12 +54,16 @@ import PostgREST.Request.Types (Alias, Field, Filter (..),
|
|||||||
JoinCondition (..),
|
JoinCondition (..),
|
||||||
JsonOperand (..),
|
JsonOperand (..),
|
||||||
JsonOperation (..),
|
JsonOperation (..),
|
||||||
JsonPath, LogicTree (..),
|
JsonPath,
|
||||||
OpExpr (..), Operation (..),
|
LogicOperator (..),
|
||||||
OrderTerm (..), SelectItem)
|
LogicTree (..), OpExpr (..),
|
||||||
|
Operation (..),
|
||||||
|
OrderDirection (..),
|
||||||
|
OrderNulls (..),
|
||||||
|
OrderTerm (..), SelectItem,
|
||||||
|
TrileanVal (..))
|
||||||
|
|
||||||
import Protolude hiding (cast, toS)
|
import Protolude hiding (cast)
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
|
|
||||||
-- | A part of a SQL query that cannot be executed independently
|
-- | A part of a SQL query that cannot be executed independently
|
||||||
@@ -72,8 +75,8 @@ noLocationF = "array[]::text[]"
|
|||||||
sourceCTEName :: SqlFragment
|
sourceCTEName :: SqlFragment
|
||||||
sourceCTEName = "pgrst_source"
|
sourceCTEName = "pgrst_source"
|
||||||
|
|
||||||
operators :: HM.HashMap Text SqlFragment
|
operators :: M.HashMap Text SqlFragment
|
||||||
operators = HM.union (HM.fromList [
|
operators = M.union (M.fromList [
|
||||||
("eq", "="),
|
("eq", "="),
|
||||||
("gte", ">="),
|
("gte", ">="),
|
||||||
("gt", ">"),
|
("gt", ">"),
|
||||||
@@ -93,8 +96,8 @@ operators = HM.union (HM.fromList [
|
|||||||
("nxl", "&>"),
|
("nxl", "&>"),
|
||||||
("adj", "-|-")]) ftsOperators
|
("adj", "-|-")]) ftsOperators
|
||||||
|
|
||||||
ftsOperators :: HM.HashMap Text SqlFragment
|
ftsOperators :: M.HashMap Text SqlFragment
|
||||||
ftsOperators = HM.fromList [
|
ftsOperators = M.fromList [
|
||||||
("fts", "@@ to_tsquery"),
|
("fts", "@@ to_tsquery"),
|
||||||
("plfts", "@@ plainto_tsquery"),
|
("plfts", "@@ plainto_tsquery"),
|
||||||
("phfts", "@@ phraseto_tsquery"),
|
("phfts", "@@ phraseto_tsquery"),
|
||||||
@@ -105,10 +108,11 @@ ftsOperators = HM.fromList [
|
|||||||
-- These CTEs convert a json object into a json array, this way we can use json_populate_recordset for all json payloads
|
-- These CTEs convert a json object into a json array, this way we can use json_populate_recordset for all json payloads
|
||||||
-- Otherwise we'd have to use json_populate_record for json objects and json_populate_recordset for json arrays
|
-- Otherwise we'd have to use json_populate_record for json objects and json_populate_recordset for json arrays
|
||||||
-- We do this in SQL to avoid processing the JSON in application code
|
-- We do this in SQL to avoid processing the JSON in application code
|
||||||
normalizedBody :: Maybe BL.ByteString -> H.Snippet
|
-- TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body
|
||||||
|
normalizedBody :: Maybe LBS.ByteString -> SQL.Snippet
|
||||||
normalizedBody body =
|
normalizedBody body =
|
||||||
"pgrst_payload AS (SELECT " <> jsonPlaceHolder body <> " AS json_data), " <>
|
"pgrst_payload AS (SELECT " <> jsonPlaceHolder <> " AS json_data), " <>
|
||||||
H.sql (BS.unwords [
|
SQL.sql (BS.unwords [
|
||||||
"pgrst_body AS (",
|
"pgrst_body AS (",
|
||||||
"SELECT",
|
"SELECT",
|
||||||
"CASE WHEN json_typeof(json_data) = 'array'",
|
"CASE WHEN json_typeof(json_data) = 'array'",
|
||||||
@@ -116,24 +120,31 @@ normalizedBody body =
|
|||||||
"ELSE json_build_array(json_data)",
|
"ELSE json_build_array(json_data)",
|
||||||
"END AS val",
|
"END AS val",
|
||||||
"FROM pgrst_payload)"])
|
"FROM pgrst_payload)"])
|
||||||
|
where
|
||||||
|
jsonPlaceHolder = SQL.encoderAndParam (HE.nullable HE.unknown) (LBS.toStrict <$> body) <> "::json"
|
||||||
|
|
||||||
-- | Equivalent to "$1::json"
|
singleParameter :: Maybe LBS.ByteString -> ByteString -> SQL.Snippet
|
||||||
-- | TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body
|
singleParameter body typ =
|
||||||
jsonPlaceHolder :: Maybe BL.ByteString -> H.Snippet
|
if typ == "bytea"
|
||||||
jsonPlaceHolder body =
|
-- TODO: Hasql fails when using HE.unknown with bytea(pg tries to utf8 encode).
|
||||||
H.encoderAndParam (HE.nullable HE.unknown) (toS <$> body) <> "::json"
|
then SQL.encoderAndParam (HE.nullable HE.bytea) (LBS.toStrict <$> body)
|
||||||
|
else SQL.encoderAndParam (HE.nullable HE.unknown) (LBS.toStrict <$> body) <> "::" <> SQL.sql typ
|
||||||
|
|
||||||
selectBody :: SqlFragment
|
selectBody :: SqlFragment
|
||||||
selectBody = "(SELECT val FROM pgrst_body)"
|
selectBody = "(SELECT val FROM pgrst_body)"
|
||||||
|
|
||||||
pgFmtLit :: Text -> SqlFragment
|
-- Here we build the pg array literal, e.g '{"Hebdon, John","Other","Another"}', manually.
|
||||||
pgFmtLit x =
|
-- This is necessary to pass an "unknown" array and let pg infer the type.
|
||||||
let trimmed = trimNullChars x
|
-- There are backslashes here, but since this value is parametrized and is not a string constant
|
||||||
escaped = "'" <> T.replace "'" "''" trimmed <> "'"
|
-- https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS
|
||||||
slashed = T.replace "\\" "\\\\" escaped in
|
-- we don't need to use the E'string' form for C-style escapes
|
||||||
encodeUtf8 $ if "\\" `T.isInfixOf` escaped
|
-- https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE
|
||||||
then "E" <> slashed
|
pgBuildArrayLiteral :: [Text] -> Text
|
||||||
else slashed
|
pgBuildArrayLiteral vals =
|
||||||
|
let trimmed = trimNullChars
|
||||||
|
slashed = T.replace "\\" "\\\\" . trimmed
|
||||||
|
escaped x = "\"" <> T.replace "\"" "\\\"" (slashed x) <> "\"" in
|
||||||
|
"{" <> T.intercalate "," (escaped <$> vals) <> "}"
|
||||||
|
|
||||||
-- TODO: refactor by following https://github.com/PostgREST/postgrest/pull/1631#issuecomment-711070833
|
-- TODO: refactor by following https://github.com/PostgREST/postgrest/pull/1631#issuecomment-711070833
|
||||||
pgFmtIdent :: Text -> SqlFragment
|
pgFmtIdent :: Text -> SqlFragment
|
||||||
@@ -189,67 +200,77 @@ pgFmtColumn :: QualifiedIdentifier -> Text -> SqlFragment
|
|||||||
pgFmtColumn table "*" = fromQi table <> ".*"
|
pgFmtColumn table "*" = fromQi table <> ".*"
|
||||||
pgFmtColumn table c = fromQi table <> "." <> pgFmtIdent c
|
pgFmtColumn table c = fromQi table <> "." <> pgFmtIdent c
|
||||||
|
|
||||||
pgFmtField :: QualifiedIdentifier -> Field -> H.Snippet
|
pgFmtField :: QualifiedIdentifier -> Field -> SQL.Snippet
|
||||||
pgFmtField table (c, jp) = H.sql (pgFmtColumn table c) <> pgFmtJsonPath jp
|
pgFmtField table (c, jp) = SQL.sql (pgFmtColumn table c) <> pgFmtJsonPath jp
|
||||||
|
|
||||||
pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> H.Snippet
|
pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> SQL.Snippet
|
||||||
pgFmtSelectItem table (f@(fName, jp), Nothing, alias, _) = pgFmtField table f <> H.sql (pgFmtAs fName jp alias)
|
pgFmtSelectItem table (f@(fName, jp), Nothing, alias, _, _) = pgFmtField table f <> SQL.sql (pgFmtAs fName jp alias)
|
||||||
-- Ideally we'd quote the cast with "pgFmtIdent cast". However, that would invalidate common casts such as "int", "bigint", etc.
|
-- Ideally we'd quote the cast with "pgFmtIdent cast". However, that would invalidate common casts such as "int", "bigint", etc.
|
||||||
-- Try doing: `select 1::"bigint"` - it'll err, using "int8" will work though. There's some parser magic that pg does that's invalidated when quoting.
|
-- Try doing: `select 1::"bigint"` - it'll err, using "int8" will work though. There's some parser magic that pg does that's invalidated when quoting.
|
||||||
-- Not quoting should be fine, we validate the input on Parsers.
|
-- Not quoting should be fine, we validate the input on Parsers.
|
||||||
pgFmtSelectItem table (f@(fName, jp), Just cast, alias, _) = "CAST (" <> pgFmtField table f <> " AS " <> H.sql (encodeUtf8 cast) <> " )" <> H.sql (pgFmtAs fName jp alias)
|
pgFmtSelectItem table (f@(fName, jp), Just cast, alias, _, _) = "CAST (" <> pgFmtField table f <> " AS " <> SQL.sql (encodeUtf8 cast) <> " )" <> SQL.sql (pgFmtAs fName jp alias)
|
||||||
|
|
||||||
pgFmtOrderTerm :: QualifiedIdentifier -> OrderTerm -> H.Snippet
|
pgFmtOrderTerm :: QualifiedIdentifier -> OrderTerm -> SQL.Snippet
|
||||||
pgFmtOrderTerm qi ot =
|
pgFmtOrderTerm qi ot =
|
||||||
pgFmtField qi (otTerm ot) <> " " <>
|
pgFmtField qi (otTerm ot) <> " " <>
|
||||||
H.sql (BS.unwords [
|
SQL.sql (BS.unwords [
|
||||||
BS.pack $ maybe mempty show $ otDirection ot,
|
maybe mempty direction $ otDirection ot,
|
||||||
BS.pack $ maybe mempty show $ otNullOrder ot])
|
maybe mempty nullOrder $ otNullOrder ot])
|
||||||
|
where
|
||||||
|
direction OrderAsc = "ASC"
|
||||||
|
direction OrderDesc = "DESC"
|
||||||
|
|
||||||
pgFmtFilter :: QualifiedIdentifier -> Filter -> H.Snippet
|
nullOrder OrderNullsFirst = "NULLS FIRST"
|
||||||
|
nullOrder OrderNullsLast = "NULLS LAST"
|
||||||
|
|
||||||
|
|
||||||
|
pgFmtFilter :: QualifiedIdentifier -> Filter -> SQL.Snippet
|
||||||
pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper of
|
pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper of
|
||||||
Op op val -> pgFmtFieldOp op <> " " <> case op of
|
Op op val -> pgFmtFieldOp op <> " " <> case op of
|
||||||
"like" -> unknownLiteral (T.map star val)
|
"like" -> unknownLiteral (T.map star val)
|
||||||
"ilike" -> unknownLiteral (T.map star val)
|
"ilike" -> unknownLiteral (T.map star val)
|
||||||
"is" -> isAllowed val
|
|
||||||
_ -> unknownLiteral val
|
_ -> unknownLiteral val
|
||||||
|
|
||||||
|
-- IS cannot be prepared. `PREPARE boolplan AS SELECT * FROM projects where id IS $1` will give a syntax error.
|
||||||
|
-- The above can be fixed by using `PREPARE boolplan AS SELECT * FROM projects where id IS NOT DISTINCT FROM $1;`
|
||||||
|
-- However that would not accept the TRUE/FALSE/NULL/UNKNOWN keywords. See: https://stackoverflow.com/questions/6133525/proper-way-to-set-preparedstatement-parameter-to-null-under-postgres.
|
||||||
|
-- This is why `IS` operands are whitelisted at the Parsers.hs level
|
||||||
|
Is triVal -> pgFmtField table fld <> " IS " <> case triVal of
|
||||||
|
TriTrue -> "TRUE"
|
||||||
|
TriFalse -> "FALSE"
|
||||||
|
TriNull -> "NULL"
|
||||||
|
TriUnknown -> "UNKNOWN"
|
||||||
|
|
||||||
-- We don't use "IN", we use "= ANY". IN has the following disadvantages:
|
-- We don't use "IN", we use "= ANY". IN has the following disadvantages:
|
||||||
-- + No way to use an empty value on IN: "col IN ()" is invalid syntax. With ANY we can do "= ANY('{}')"
|
-- + No way to use an empty value on IN: "col IN ()" is invalid syntax. With ANY we can do "= ANY('{}')"
|
||||||
-- + Can invalidate prepared statements: multiple parameters on an IN($1, $2, $3) will lead to using different prepared statements and not take advantage of caching.
|
-- + Can invalidate prepared statements: multiple parameters on an IN($1, $2, $3) will lead to using different prepared statements and not take advantage of caching.
|
||||||
In vals -> pgFmtField table fld <> " " <>
|
In vals -> pgFmtField table fld <> " " <> case vals of
|
||||||
case vals of
|
|
||||||
[""] -> "= ANY('{}') "
|
[""] -> "= ANY('{}') "
|
||||||
-- Here we build the pg array, e.g '{"Hebdon, John","Other","Another"}', manually. We quote the values to prevent the "," being treated as an element separator.
|
_ -> "= ANY (" <> unknownLiteral (pgBuildArrayLiteral vals) <> ") "
|
||||||
-- TODO: Ideally this would be done on Hasql with an encoder, but the "array unknown" is not working(Hasql doesn't pass any value).
|
|
||||||
_ -> "= ANY (" <> unknownLiteral ("{" <> T.intercalate "," ((\x -> "\"" <> x <> "\"") <$> vals) <> "}") <> ")"
|
|
||||||
|
|
||||||
Fts op lang val ->
|
Fts op lang val ->
|
||||||
pgFmtFieldOp op <> "(" <> ftsLang lang <> unknownLiteral val <> ") "
|
pgFmtFieldOp op <> "(" <> ftsLang lang <> unknownLiteral val <> ") "
|
||||||
where
|
where
|
||||||
ftsLang = maybe mempty (\l -> unknownLiteral l <> ", ")
|
ftsLang = maybe mempty (\l -> unknownLiteral l <> ", ")
|
||||||
pgFmtFieldOp op = pgFmtField table fld <> " " <> sqlOperator op
|
pgFmtFieldOp op = pgFmtField table fld <> " " <> sqlOperator op
|
||||||
sqlOperator o = H.sql $ HM.lookupDefault "=" o operators
|
sqlOperator o = SQL.sql $ M.lookupDefault "=" o operators
|
||||||
notOp = if hasNot then "NOT" else mempty
|
notOp = if hasNot then "NOT" else mempty
|
||||||
star c = if c == '*' then '%' else c
|
star c = if c == '*' then '%' else c
|
||||||
-- IS cannot be prepared. `PREPARE boolplan AS SELECT * FROM projects where id IS $1` will give a syntax error.
|
|
||||||
-- The above can be fixed by using `PREPARE boolplan AS SELECT * FROM projects where id IS NOT DISTINCT FROM $1;`
|
|
||||||
-- However that would not accept the TRUE/FALSE/NULL keywords. See: https://stackoverflow.com/questions/6133525/proper-way-to-set-preparedstatement-parameter-to-null-under-postgres.
|
|
||||||
isAllowed :: Text -> H.Snippet
|
|
||||||
isAllowed v = H.sql $ maybe
|
|
||||||
(pgFmtLit v <> "::unknown") encodeUtf8
|
|
||||||
(find ((==) . T.toLower $ v) ["null","true","false"])
|
|
||||||
|
|
||||||
pgFmtJoinCondition :: JoinCondition -> H.Snippet
|
pgFmtJoinCondition :: JoinCondition -> SQL.Snippet
|
||||||
pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
|
pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
|
||||||
H.sql $ pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2
|
SQL.sql $ pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2
|
||||||
|
|
||||||
pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> H.Snippet
|
pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SQL.Snippet
|
||||||
pgFmtLogicTree qi (Expr hasNot op forest) = H.sql notOp <> " (" <> intercalateSnippet (" " <> BS.pack (show op) <> " ") (pgFmtLogicTree qi <$> forest) <> ")"
|
pgFmtLogicTree qi (Expr hasNot op forest) = SQL.sql notOp <> " (" <> intercalateSnippet (opSql op) (pgFmtLogicTree qi <$> forest) <> ")"
|
||||||
where notOp = if hasNot then "NOT" else mempty
|
where
|
||||||
|
notOp = if hasNot then "NOT" else mempty
|
||||||
|
|
||||||
|
opSql And = " AND "
|
||||||
|
opSql Or = " OR "
|
||||||
pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt
|
pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt
|
||||||
|
|
||||||
pgFmtJsonPath :: JsonPath -> H.Snippet
|
pgFmtJsonPath :: JsonPath -> SQL.Snippet
|
||||||
pgFmtJsonPath = \case
|
pgFmtJsonPath = \case
|
||||||
[] -> mempty
|
[] -> mempty
|
||||||
(JArrow x:xs) -> "->" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
|
(JArrow x:xs) -> "->" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
|
||||||
@@ -270,7 +291,7 @@ pgFmtAs fName jp Nothing = case jOp <$> lastMay jp of
|
|||||||
Nothing -> mempty
|
Nothing -> mempty
|
||||||
pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias
|
pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias
|
||||||
|
|
||||||
countF :: H.Snippet -> Bool -> (H.Snippet, SqlFragment)
|
countF :: SQL.Snippet -> Bool -> (SQL.Snippet, SqlFragment)
|
||||||
countF countQuery shouldCount =
|
countF countQuery shouldCount =
|
||||||
if shouldCount
|
if shouldCount
|
||||||
then (
|
then (
|
||||||
@@ -286,37 +307,31 @@ returningF qi returnings =
|
|||||||
then "RETURNING 1" -- For mutation cases where there's no ?select, we return 1 to know how many rows were modified
|
then "RETURNING 1" -- For mutation cases where there's no ?select, we return 1 to know how many rows were modified
|
||||||
else "RETURNING " <> BS.intercalate ", " (pgFmtColumn qi <$> returnings)
|
else "RETURNING " <> BS.intercalate ", " (pgFmtColumn qi <$> returnings)
|
||||||
|
|
||||||
limitOffsetF :: NonnegRange -> H.Snippet
|
limitOffsetF :: NonnegRange -> SQL.Snippet
|
||||||
limitOffsetF range =
|
limitOffsetF range =
|
||||||
if range == allRange then mempty else "LIMIT " <> limit <> " OFFSET " <> offset
|
if range == allRange then mempty else "LIMIT " <> limit <> " OFFSET " <> offset
|
||||||
where
|
where
|
||||||
limit = maybe "ALL" (\l -> unknownEncoder (BS.pack $ show l)) $ rangeLimit range
|
limit = maybe "ALL" (\l -> unknownEncoder (BS.pack $ show l)) $ rangeLimit range
|
||||||
offset = unknownEncoder (BS.pack . show $ rangeOffset range)
|
offset = unknownEncoder (BS.pack . show $ rangeOffset range)
|
||||||
|
|
||||||
responseHeadersF :: PgVersion -> SqlFragment
|
responseHeadersF :: SqlFragment
|
||||||
responseHeadersF pgVer =
|
responseHeadersF = currentSettingF "response.headers"
|
||||||
if pgVer >= pgVersion96
|
|
||||||
then currentSettingF "response.headers"
|
|
||||||
else "null"
|
|
||||||
|
|
||||||
responseStatusF :: PgVersion -> SqlFragment
|
responseStatusF :: SqlFragment
|
||||||
responseStatusF pgVer =
|
responseStatusF = currentSettingF "response.status"
|
||||||
if pgVer >= pgVersion96
|
|
||||||
then currentSettingF "response.status"
|
|
||||||
else "null"
|
|
||||||
|
|
||||||
currentSettingF :: Text -> SqlFragment
|
currentSettingF :: SqlFragment -> SqlFragment
|
||||||
currentSettingF setting =
|
currentSettingF setting =
|
||||||
-- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
|
-- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
|
||||||
"nullif(current_setting(" <> pgFmtLit setting <> ", true), '')"
|
"nullif(current_setting('" <> setting <> "', true), '')"
|
||||||
|
|
||||||
-- Hasql Snippet utilities
|
-- Hasql Snippet utilities
|
||||||
unknownEncoder :: ByteString -> H.Snippet
|
unknownEncoder :: ByteString -> SQL.Snippet
|
||||||
unknownEncoder = H.encoderAndParam (HE.nonNullable HE.unknown)
|
unknownEncoder = SQL.encoderAndParam (HE.nonNullable HE.unknown)
|
||||||
|
|
||||||
unknownLiteral :: Text -> H.Snippet
|
unknownLiteral :: Text -> SQL.Snippet
|
||||||
unknownLiteral = unknownEncoder . encodeUtf8
|
unknownLiteral = unknownEncoder . encodeUtf8
|
||||||
|
|
||||||
intercalateSnippet :: ByteString -> [H.Snippet] -> H.Snippet
|
intercalateSnippet :: ByteString -> [SQL.Snippet] -> SQL.Snippet
|
||||||
intercalateSnippet _ [] = mempty
|
intercalateSnippet _ [] = mempty
|
||||||
intercalateSnippet frag snippets = foldr1 (\a b -> a <> H.sql frag <> b) snippets
|
intercalateSnippet frag snippets = foldr1 (\a b -> a <> SQL.sql frag <> b) snippets
|
||||||
|
|||||||
@@ -19,26 +19,25 @@ module PostgREST.Query.Statements
|
|||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.Aeson.Lens as L
|
import qualified Data.Aeson.Lens as L
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
import qualified Hasql.DynamicStatements.Snippet as H
|
import qualified Hasql.DynamicStatements.Snippet as SQL
|
||||||
import qualified Hasql.DynamicStatements.Statement as H
|
import qualified Hasql.DynamicStatements.Statement as SQL
|
||||||
import qualified Hasql.Statement as H
|
import qualified Hasql.Statement as SQL
|
||||||
|
|
||||||
import Control.Lens ((^?))
|
import Control.Lens ((^?))
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Text.Read (decimal)
|
import Data.Text.Read (decimal)
|
||||||
import Network.HTTP.Types.Status (Status)
|
import Network.HTTP.Types.Status (Status)
|
||||||
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion)
|
import PostgREST.Error (Error (..))
|
||||||
import PostgREST.Error (Error (..))
|
import PostgREST.GucHeader (GucHeader)
|
||||||
import PostgREST.GucHeader (GucHeader)
|
|
||||||
|
|
||||||
import PostgREST.DbStructure.Identifiers (FieldName)
|
import PostgREST.DbStructure.Identifiers (FieldName)
|
||||||
import PostgREST.Query.SqlFragment
|
import PostgREST.Query.SqlFragment
|
||||||
import PostgREST.Request.Preferences
|
import PostgREST.Request.Preferences
|
||||||
|
|
||||||
import Protolude hiding (toS)
|
import Protolude
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
{-| The generic query result format used by API responses. The location header
|
{-| The generic query result format used by API responses. The location header
|
||||||
is represented as a list of strings containing variable bindings like
|
is represented as a list of strings containing variable bindings like
|
||||||
@@ -46,22 +45,22 @@ import Protolude.Conv (toS)
|
|||||||
-}
|
-}
|
||||||
type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString, Either Error [GucHeader], Either Error (Maybe Status))
|
type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString, Either Error [GucHeader], Either Error (Maybe Status))
|
||||||
|
|
||||||
createWriteStatement :: H.Snippet -> H.Snippet -> Bool -> Bool -> Bool ->
|
createWriteStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool ->
|
||||||
PreferRepresentation -> [Text] -> PgVersion -> Bool ->
|
PreferRepresentation -> [Text] -> Bool ->
|
||||||
H.Statement () ResultsWithCount
|
SQL.Statement () ResultsWithCount
|
||||||
createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys pgVer =
|
createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys =
|
||||||
H.dynamicallyParameterized snippet decodeStandard
|
SQL.dynamicallyParameterized snippet decodeStandard
|
||||||
where
|
where
|
||||||
snippet =
|
snippet =
|
||||||
"WITH " <> H.sql sourceCTEName <> " AS (" <> mutateQuery <> ") " <>
|
"WITH " <> SQL.sql sourceCTEName <> " AS (" <> mutateQuery <> ") " <>
|
||||||
H.sql (
|
SQL.sql (
|
||||||
"SELECT " <>
|
"SELECT " <>
|
||||||
"'' AS total_result_set, " <>
|
"'' AS total_result_set, " <>
|
||||||
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
||||||
locF <> " AS header, " <>
|
locF <> " AS header, " <>
|
||||||
bodyF <> " AS body, " <>
|
bodyF <> " AS body, " <>
|
||||||
responseHeadersF pgVer <> " AS response_headers, " <>
|
responseHeadersF <> " AS response_headers, " <>
|
||||||
responseStatusF pgVer <> " AS response_status "
|
responseStatusF <> " AS response_status "
|
||||||
) <>
|
) <>
|
||||||
"FROM (" <> selectF <> ") _postgrest_t"
|
"FROM (" <> selectF <> ") _postgrest_t"
|
||||||
|
|
||||||
@@ -82,29 +81,29 @@ createWriteStatement selectQuery mutateQuery wantSingle isInsert asCsv rep pKeys
|
|||||||
|
|
||||||
selectF
|
selectF
|
||||||
-- prevent using any of the column names in ?select= when no response is returned from the CTE
|
-- prevent using any of the column names in ?select= when no response is returned from the CTE
|
||||||
| rep `elem` [None, HeadersOnly] = H.sql ("SELECT * FROM " <> sourceCTEName)
|
| rep `elem` [None, HeadersOnly] = SQL.sql ("SELECT * FROM " <> sourceCTEName)
|
||||||
| otherwise = selectQuery
|
| otherwise = selectQuery
|
||||||
|
|
||||||
decodeStandard :: HD.Result ResultsWithCount
|
decodeStandard :: HD.Result ResultsWithCount
|
||||||
decodeStandard =
|
decodeStandard =
|
||||||
fromMaybe (Nothing, 0, [], mempty, Right [], Right Nothing) <$> HD.rowMaybe standardRow
|
fromMaybe (Nothing, 0, [], mempty, Right [], Right Nothing) <$> HD.rowMaybe standardRow
|
||||||
|
|
||||||
createReadStatement :: H.Snippet -> H.Snippet -> Bool -> Bool -> Bool -> Maybe FieldName -> PgVersion -> Bool ->
|
createReadStatement :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool ->
|
||||||
H.Statement () ResultsWithCount
|
SQL.Statement () ResultsWithCount
|
||||||
createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField pgVer =
|
createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField =
|
||||||
H.dynamicallyParameterized snippet decodeStandard
|
SQL.dynamicallyParameterized snippet decodeStandard
|
||||||
where
|
where
|
||||||
snippet =
|
snippet =
|
||||||
"WITH " <>
|
"WITH " <>
|
||||||
H.sql sourceCTEName <> " AS ( " <> selectQuery <> " ) " <>
|
SQL.sql sourceCTEName <> " AS ( " <> selectQuery <> " ) " <>
|
||||||
countCTEF <> " " <>
|
countCTEF <> " " <>
|
||||||
H.sql ("SELECT " <>
|
SQL.sql ("SELECT " <>
|
||||||
countResultF <> " AS total_result_set, " <>
|
countResultF <> " AS total_result_set, " <>
|
||||||
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
||||||
noLocationF <> " AS header, " <>
|
noLocationF <> " AS header, " <>
|
||||||
bodyF <> " AS body, " <>
|
bodyF <> " AS body, " <>
|
||||||
responseHeadersF pgVer <> " AS response_headers, " <>
|
responseHeadersF <> " AS response_headers, " <>
|
||||||
responseStatusF pgVer <> " AS response_status " <>
|
responseStatusF <> " AS response_status " <>
|
||||||
"FROM ( SELECT * FROM " <> sourceCTEName <> " ) _postgrest_t")
|
"FROM ( SELECT * FROM " <> sourceCTEName <> " ) _postgrest_t")
|
||||||
|
|
||||||
(countCTEF, countResultF) = countF countQuery countTotal
|
(countCTEF, countResultF) = countF countQuery countTotal
|
||||||
@@ -131,22 +130,22 @@ standardRow = (,,,,,) <$> nullableColumn HD.int8 <*> column HD.int8
|
|||||||
|
|
||||||
type ProcResults = (Maybe Int64, Int64, ByteString, Either Error [GucHeader], Either Error (Maybe Status))
|
type ProcResults = (Maybe Int64, Int64, ByteString, Either Error [GucHeader], Either Error (Maybe Status))
|
||||||
|
|
||||||
callProcStatement :: Bool -> Bool -> H.Snippet -> H.Snippet -> H.Snippet -> Bool ->
|
callProcStatement :: Bool -> Bool -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
|
||||||
Bool -> Bool -> Bool -> Maybe FieldName -> PgVersion -> Bool ->
|
Bool -> Bool -> Bool -> Maybe FieldName -> Bool ->
|
||||||
H.Statement () ProcResults
|
SQL.Statement () ProcResults
|
||||||
callProcStatement returnsScalar returnsSingle callProcQuery selectQuery countQuery countTotal asSingle asCsv multObjects binaryField pgVer =
|
callProcStatement returnsScalar returnsSingle callProcQuery selectQuery countQuery countTotal asSingle asCsv multObjects binaryField =
|
||||||
H.dynamicallyParameterized snippet decodeProc
|
SQL.dynamicallyParameterized snippet decodeProc
|
||||||
where
|
where
|
||||||
snippet =
|
snippet =
|
||||||
"WITH " <> H.sql sourceCTEName <> " AS (" <> callProcQuery <> ") " <>
|
"WITH " <> SQL.sql sourceCTEName <> " AS (" <> callProcQuery <> ") " <>
|
||||||
countCTEF <>
|
countCTEF <>
|
||||||
H.sql (
|
SQL.sql (
|
||||||
"SELECT " <>
|
"SELECT " <>
|
||||||
countResultF <> " AS total_result_set, " <>
|
countResultF <> " AS total_result_set, " <>
|
||||||
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
"pg_catalog.count(_postgrest_t) AS page_total, " <>
|
||||||
bodyF <> " AS body, " <>
|
bodyF <> " AS body, " <>
|
||||||
responseHeadersF pgVer <> " AS response_headers, " <>
|
responseHeadersF <> " AS response_headers, " <>
|
||||||
responseStatusF pgVer <> " AS response_status ") <>
|
responseStatusF <> " AS response_status ") <>
|
||||||
"FROM (" <> selectQuery <> ") _postgrest_t"
|
"FROM (" <> selectQuery <> ") _postgrest_t"
|
||||||
|
|
||||||
(countCTEF, countResultF) = countF countQuery countTotal
|
(countCTEF, countResultF) = countF countQuery countTotal
|
||||||
@@ -170,9 +169,9 @@ callProcStatement returnsScalar returnsSingle callProcQuery selectQuery countQue
|
|||||||
<*> (fromMaybe defGucHeaders <$> nullableColumn decodeGucHeaders)
|
<*> (fromMaybe defGucHeaders <$> nullableColumn decodeGucHeaders)
|
||||||
<*> (fromMaybe defGucStatus <$> nullableColumn decodeGucStatus)
|
<*> (fromMaybe defGucStatus <$> nullableColumn decodeGucStatus)
|
||||||
|
|
||||||
createExplainStatement :: H.Snippet -> Bool -> H.Statement () (Maybe Int64)
|
createExplainStatement :: SQL.Snippet -> Bool -> SQL.Statement () (Maybe Int64)
|
||||||
createExplainStatement countQuery =
|
createExplainStatement countQuery =
|
||||||
H.dynamicallyParameterized snippet decodeExplain
|
SQL.dynamicallyParameterized snippet decodeExplain
|
||||||
where
|
where
|
||||||
snippet = "EXPLAIN (FORMAT JSON) " <> countQuery
|
snippet = "EXPLAIN (FORMAT JSON) " <> countQuery
|
||||||
-- |
|
-- |
|
||||||
@@ -189,7 +188,7 @@ createExplainStatement countQuery =
|
|||||||
(^? L.nth 0 . L.key "Plan" . L.key "Plan Rows" . L._Integral) <$> row
|
(^? L.nth 0 . L.key "Plan" . L.key "Plan Rows" . L._Integral) <$> row
|
||||||
|
|
||||||
decodeGucHeaders :: HD.Value (Either Error [GucHeader])
|
decodeGucHeaders :: HD.Value (Either Error [GucHeader])
|
||||||
decodeGucHeaders = first (const GucHeadersError) . JSON.eitherDecode . toS <$> HD.bytea
|
decodeGucHeaders = first (const GucHeadersError) . JSON.eitherDecode . LBS.fromStrict <$> HD.bytea
|
||||||
|
|
||||||
decodeGucStatus :: HD.Value (Either Error (Maybe Status))
|
decodeGucStatus :: HD.Value (Either Error (Maybe Status))
|
||||||
decodeGucStatus = first (const GucStatusError) . fmap (Just . toEnum . fst) . decimal <$> HD.text
|
decodeGucStatus = first (const GucStatusError) . fmap (Just . toEnum . fst) . decimal <$> HD.text
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ import Data.Ranged.Ranges
|
|||||||
import Network.HTTP.Types.Header
|
import Network.HTTP.Types.Header
|
||||||
import Network.HTTP.Types.Status
|
import Network.HTTP.Types.Status
|
||||||
|
|
||||||
import Protolude hiding (toS)
|
import Protolude
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
type NonnegRange = Range Integer
|
type NonnegRange = Range Integer
|
||||||
|
|
||||||
@@ -37,7 +36,7 @@ rangeParse range = do
|
|||||||
|
|
||||||
case listToMaybe (range =~ rangeRegex :: [[BS.ByteString]]) of
|
case listToMaybe (range =~ rangeRegex :: [[BS.ByteString]]) of
|
||||||
Just parsedRange ->
|
Just parsedRange ->
|
||||||
let [_, mLower, mUpper] = readMaybe . toS <$> parsedRange
|
let [_, mLower, mUpper] = readMaybe . BS.unpack <$> parsedRange
|
||||||
lower = maybe emptyRange rangeGeq mLower
|
lower = maybe emptyRange rangeGeq mLower
|
||||||
upper = maybe allRange rangeLeq mUpper in
|
upper = maybe allRange rangeLeq mUpper in
|
||||||
rangeIntersection lower upper
|
rangeIntersection lower upper
|
||||||
|
|||||||
+115
-113
@@ -3,7 +3,6 @@ Module : PostgREST.Request.ApiRequest
|
|||||||
Description : PostgREST functions to translate HTTP request to a domain type called ApiRequest.
|
Description : PostgREST functions to translate HTTP request to a domain type called ApiRequest.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE MultiWayIf #-}
|
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
@@ -13,25 +12,26 @@ module PostgREST.Request.ApiRequest
|
|||||||
, ContentType(..)
|
, ContentType(..)
|
||||||
, Action(..)
|
, Action(..)
|
||||||
, Target(..)
|
, Target(..)
|
||||||
, PayloadJSON(..)
|
, Payload(..)
|
||||||
, userApiRequest
|
, userApiRequest
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
import qualified Data.Csv as CSV
|
import qualified Data.Csv as CSV
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import qualified Data.List as L
|
import qualified Data.List as L
|
||||||
import qualified Data.Set as S
|
import qualified Data.List.NonEmpty as NonEmptyList
|
||||||
import qualified Data.Text as T
|
import qualified Data.Set as S
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Encoding as T
|
||||||
|
import qualified Data.Vector as V
|
||||||
|
|
||||||
import Control.Arrow ((***))
|
import Control.Arrow ((***))
|
||||||
import Data.Aeson.Types (emptyArray, emptyObject)
|
import Data.Aeson.Types (emptyArray, emptyObject)
|
||||||
import Data.List (last, lookup, partition, union)
|
import Data.List (last, lookup, partition, union)
|
||||||
import Data.List.NonEmpty (head)
|
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Ranged.Boundaries (Boundary (..))
|
import Data.Ranged.Boundaries (Boundary (..))
|
||||||
import Data.Ranged.Ranges (Range (..), emptyRange,
|
import Data.Ranged.Ranges (Range (..), emptyRange,
|
||||||
@@ -51,9 +51,8 @@ import PostgREST.DbStructure (DbStructure (..))
|
|||||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
import PostgREST.DbStructure.Proc (PgArg (..),
|
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
||||||
ProcDescription (..),
|
ProcParam (..), ProcsMap)
|
||||||
ProcsMap)
|
|
||||||
import PostgREST.Error (ApiRequestError (..))
|
import PostgREST.Error (ApiRequestError (..))
|
||||||
import PostgREST.Query.SqlFragment (ftsOperators, operators)
|
import PostgREST.Query.SqlFragment (ftsOperators, operators)
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
@@ -67,26 +66,27 @@ import PostgREST.Request.Preferences (PreferCount (..),
|
|||||||
PreferResolution (..),
|
PreferResolution (..),
|
||||||
PreferTransaction (..))
|
PreferTransaction (..))
|
||||||
|
|
||||||
import qualified PostgREST.ContentType as ContentType
|
import qualified PostgREST.ContentType as ContentType
|
||||||
|
import qualified PostgREST.Request.Preferences as Preferences
|
||||||
|
|
||||||
import Protolude hiding (head, toS)
|
import Protolude
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
|
|
||||||
type RequestBody = BL.ByteString
|
type RequestBody = LBS.ByteString
|
||||||
|
|
||||||
data PayloadJSON
|
data Payload
|
||||||
= ProcessedJSON -- ^ Cached attributes of a JSON payload
|
= ProcessedJSON -- ^ Cached attributes of a JSON payload
|
||||||
{ pjRaw :: BL.ByteString
|
{ payRaw :: LBS.ByteString
|
||||||
-- ^ This is the raw ByteString that comes from the request body. We
|
-- ^ This is the raw ByteString that comes from the request body. We
|
||||||
-- cache this instead of an Aeson Value because it was detected that for
|
-- cache this instead of an Aeson Value because it was detected that for
|
||||||
-- large payloads the encoding had high memory usage, see
|
-- large payloads the encoding had high memory usage, see
|
||||||
-- https://github.com/PostgREST/postgrest/pull/1005 for more details
|
-- https://github.com/PostgREST/postgrest/pull/1005 for more details
|
||||||
, pjKeys :: S.Set Text
|
, payKeys :: S.Set Text
|
||||||
-- ^ Keys of the object or if it's an array these keys are guaranteed to
|
-- ^ Keys of the object or if it's an array these keys are guaranteed to
|
||||||
-- be the same across all its objects
|
-- be the same across all its objects
|
||||||
}
|
}
|
||||||
| RawJSON { pjRaw :: BL.ByteString }
|
| RawJSON { payRaw :: LBS.ByteString }
|
||||||
|
| RawPay { payRaw :: LBS.ByteString }
|
||||||
|
|
||||||
data InvokeMethod = InvHead | InvGet | InvPost deriving Eq
|
data InvokeMethod = InvHead | InvGet | InvPost deriving Eq
|
||||||
-- | Types of things a user wants to do to tables/views/procs
|
-- | Types of things a user wants to do to tables/views/procs
|
||||||
@@ -119,15 +119,15 @@ instance JSON.ToJSON RpcParamValue where
|
|||||||
toJSON (Variadic v) = JSON.toJSON v
|
toJSON (Variadic v) = JSON.toJSON v
|
||||||
|
|
||||||
toRpcParamValue :: ProcDescription -> (Text, Text) -> (Text, RpcParamValue)
|
toRpcParamValue :: ProcDescription -> (Text, Text) -> (Text, RpcParamValue)
|
||||||
toRpcParamValue proc (k, v) | argIsVariadic k = (k, Variadic [v])
|
toRpcParamValue proc (k, v) | prmIsVariadic k = (k, Variadic [v])
|
||||||
| otherwise = (k, Fixed v)
|
| otherwise = (k, Fixed v)
|
||||||
where
|
where
|
||||||
argIsVariadic arg = isJust $ find (\PgArg{pgaName, pgaVar} -> pgaName == arg && pgaVar) $ pdArgs proc
|
prmIsVariadic prm = isJust $ find (\ProcParam{ppName, ppVar} -> ppName == prm && ppVar) $ pdParams proc
|
||||||
|
|
||||||
-- | Convert rpc params `/rpc/func?a=val1&b=val2` to json `{"a": "val1", "b": "val2"}
|
-- | Convert rpc params `/rpc/func?a=val1&b=val2` to json `{"a": "val1", "b": "val2"}
|
||||||
jsonRpcParams :: ProcDescription -> [(Text, Text)] -> PayloadJSON
|
jsonRpcParams :: ProcDescription -> [(Text, Text)] -> Payload
|
||||||
jsonRpcParams proc prms =
|
jsonRpcParams proc prms =
|
||||||
if not $ pdHasVariadic proc then -- if proc has no variadic arg, save steps and directly convert to json
|
if not $ pdHasVariadic proc then -- if proc has no variadic param, save steps and directly convert to json
|
||||||
ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> prms) (S.fromList $ fst <$> prms)
|
ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> prms) (S.fromList $ fst <$> prms)
|
||||||
else
|
else
|
||||||
let paramsMap = M.fromListWith mergeParams $ toRpcParamValue proc <$> prms in
|
let paramsMap = M.fromListWith mergeParams $ toRpcParamValue proc <$> prms in
|
||||||
@@ -135,9 +135,9 @@ jsonRpcParams proc prms =
|
|||||||
where
|
where
|
||||||
mergeParams :: RpcParamValue -> RpcParamValue -> RpcParamValue
|
mergeParams :: RpcParamValue -> RpcParamValue -> RpcParamValue
|
||||||
mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a
|
mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a
|
||||||
mergeParams v _ = v -- repeated params for non-variadic arguments are not merged
|
mergeParams v _ = v -- repeated params for non-variadic parameters are not merged
|
||||||
|
|
||||||
targetToJsonRpcParams :: Maybe Target -> [(Text, Text)] -> Maybe PayloadJSON
|
targetToJsonRpcParams :: Maybe Target -> [(Text, Text)] -> Maybe Payload
|
||||||
targetToJsonRpcParams target params =
|
targetToJsonRpcParams target params =
|
||||||
case target of
|
case target of
|
||||||
Just TargetProc{tProc} -> Just $ jsonRpcParams tProc params
|
Just TargetProc{tProc} -> Just $ jsonRpcParams tProc params
|
||||||
@@ -152,10 +152,10 @@ targetToJsonRpcParams target params =
|
|||||||
-}
|
-}
|
||||||
data ApiRequest = ApiRequest {
|
data ApiRequest = ApiRequest {
|
||||||
iAction :: Action -- ^ Similar but not identical to HTTP verb, e.g. Create/Invoke both POST
|
iAction :: Action -- ^ Similar but not identical to HTTP verb, e.g. Create/Invoke both POST
|
||||||
, iRange :: M.HashMap ByteString NonnegRange -- ^ Requested range of rows within response
|
, iRange :: M.HashMap Text NonnegRange -- ^ Requested range of rows within response
|
||||||
, iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level
|
, iTopLevelRange :: NonnegRange -- ^ Requested range of rows from the top level
|
||||||
, iTarget :: Target -- ^ The target, be it calling a proc or accessing a table
|
, iTarget :: Target -- ^ The target, be it calling a proc or accessing a table
|
||||||
, iPayload :: Maybe PayloadJSON -- ^ Data sent by client and used for mutation actions
|
, iPayload :: Maybe Payload -- ^ Data sent by client and used for mutation actions
|
||||||
, iPreferRepresentation :: PreferRepresentation -- ^ If client wants created items echoed back
|
, iPreferRepresentation :: PreferRepresentation -- ^ If client wants created items echoed back
|
||||||
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
|
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
|
||||||
, iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count
|
, iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count
|
||||||
@@ -184,7 +184,7 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
|||||||
| isJust profile && fromJust profile `notElem` configDbSchemas = Left $ UnacceptableSchema $ toList configDbSchemas
|
| isJust profile && fromJust profile `notElem` configDbSchemas = Left $ UnacceptableSchema $ toList configDbSchemas
|
||||||
| isTargetingProc && method `notElem` ["HEAD", "GET", "POST"] = Left ActionInappropriate
|
| isTargetingProc && method `notElem` ["HEAD", "GET", "POST"] = Left ActionInappropriate
|
||||||
| topLevelRange == emptyRange = Left InvalidRange
|
| topLevelRange == emptyRange = Left InvalidRange
|
||||||
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) witness payload
|
| shouldParsePayload && isLeft payload = either (Left . InvalidBody) witness payload
|
||||||
| isLeft parsedColumns = either Left witness parsedColumns
|
| isLeft parsedColumns = either Left witness parsedColumns
|
||||||
| otherwise = do
|
| otherwise = do
|
||||||
acceptContentType <- findAcceptContentType conf action path accepts
|
acceptContentType <- findAcceptContentType conf action path accepts
|
||||||
@@ -195,29 +195,20 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
|||||||
, iRange = ranges
|
, iRange = ranges
|
||||||
, iTopLevelRange = topLevelRange
|
, iTopLevelRange = topLevelRange
|
||||||
, iPayload = relevantPayload
|
, iPayload = relevantPayload
|
||||||
, iPreferRepresentation = representation
|
, iPreferRepresentation = fromMaybe None preferRepresentation
|
||||||
, iPreferParameters = if | hasPrefer (show SingleObject) -> Just SingleObject
|
, iPreferParameters = preferParameters
|
||||||
| hasPrefer (show MultipleObjects) -> Just MultipleObjects
|
, iPreferCount = preferCount
|
||||||
| otherwise -> Nothing
|
, iPreferResolution = preferResolution
|
||||||
, iPreferCount = if | hasPrefer (show ExactCount) -> Just ExactCount
|
, iPreferTransaction = preferTransaction
|
||||||
| hasPrefer (show PlannedCount) -> Just PlannedCount
|
|
||||||
| hasPrefer (show EstimatedCount) -> Just EstimatedCount
|
|
||||||
| otherwise -> Nothing
|
|
||||||
, iPreferResolution = if | hasPrefer (show MergeDuplicates) -> Just MergeDuplicates
|
|
||||||
| hasPrefer (show IgnoreDuplicates) -> Just IgnoreDuplicates
|
|
||||||
| otherwise -> Nothing
|
|
||||||
, iPreferTransaction = if | hasPrefer (show Commit) -> Just Commit
|
|
||||||
| hasPrefer (show Rollback) -> Just Rollback
|
|
||||||
| otherwise -> Nothing
|
|
||||||
, iFilters = filters
|
, iFilters = filters
|
||||||
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
|
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
|
||||||
, iSelect = toS <$> join (lookup "select" qParams)
|
, iSelect = toS <$> join (lookup "select" qParams)
|
||||||
, iOnConflict = toS <$> join (lookup "on_conflict" qParams)
|
, iOnConflict = toS <$> join (lookup "on_conflict" qParams)
|
||||||
, iColumns = payloadColumns
|
, iColumns = payloadColumns
|
||||||
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
|
||||||
, iCanonicalQS = toS $ urlEncodeVars
|
, iCanonicalQS = BS.pack $ urlEncodeVars
|
||||||
. L.sortOn fst
|
. L.sortOn fst
|
||||||
. map (join (***) toS . second (fromMaybe BS.empty))
|
. map (join (***) BS.unpack . second (fromMaybe mempty))
|
||||||
$ qString
|
$ qString
|
||||||
, iJWT = tokenStr
|
, iJWT = tokenStr
|
||||||
, iHeaders = [ (CI.foldedCase k, v) | (k,v) <- hdrs, k /= hCookie]
|
, iHeaders = [ (CI.foldedCase k, v) | (k,v) <- hdrs, k /= hCookie]
|
||||||
@@ -254,7 +245,7 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
|||||||
isTargetingDefaultSpec = case path of
|
isTargetingDefaultSpec = case path of
|
||||||
PathInfo{pIsDefaultSpec=True} -> True
|
PathInfo{pIsDefaultSpec=True} -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
contentType = ContentType.decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type"
|
contentType = maybe CTApplicationJSON ContentType.decodeContentType $ lookupHeader "content-type"
|
||||||
columns
|
columns
|
||||||
| action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams)
|
| action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams)
|
||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
@@ -263,27 +254,30 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
|||||||
case (contentType, action) of
|
case (contentType, action) of
|
||||||
(_, ActionInvoke InvGet) -> S.fromList $ fst <$> rpcQParams
|
(_, ActionInvoke InvGet) -> S.fromList $ fst <$> rpcQParams
|
||||||
(_, ActionInvoke InvHead) -> S.fromList $ fst <$> rpcQParams
|
(_, ActionInvoke InvHead) -> S.fromList $ fst <$> rpcQParams
|
||||||
(CTUrlEncoded, _) -> S.fromList $ map (toS . fst) $ parseSimpleQuery $ toS reqBody
|
(CTUrlEncoded, _) -> S.fromList $ map (T.decodeUtf8 . fst) $ parseSimpleQuery $ LBS.toStrict reqBody
|
||||||
_ -> case (relevantPayload, fromRight Nothing parsedColumns) of
|
_ -> case (relevantPayload, fromRight Nothing parsedColumns) of
|
||||||
(Just ProcessedJSON{pjKeys}, _) -> pjKeys
|
(Just ProcessedJSON{payKeys}, _) -> payKeys
|
||||||
(Just RawJSON{}, Just cls) -> cls
|
(Just RawJSON{}, Just cls) -> cls
|
||||||
_ -> S.empty
|
_ -> S.empty
|
||||||
|
payload :: Either ByteString Payload
|
||||||
payload = case contentType of
|
payload = case contentType of
|
||||||
CTApplicationJSON ->
|
CTApplicationJSON ->
|
||||||
if isJust columns
|
if isJust columns
|
||||||
then Right $ RawJSON reqBody
|
then Right $ RawJSON reqBody
|
||||||
else note "All object keys must match" . payloadAttributes reqBody
|
else note "All object keys must match" . payloadAttributes reqBody
|
||||||
=<< if BL.null reqBody && isTargetingProc
|
=<< if LBS.null reqBody && isTargetingProc
|
||||||
then Right emptyObject
|
then Right emptyObject
|
||||||
else JSON.eitherDecode reqBody
|
else first BS.pack $ JSON.eitherDecode reqBody
|
||||||
CTTextCSV -> do
|
CTTextCSV -> do
|
||||||
json <- csvToJson <$> CSV.decodeByName reqBody
|
json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)
|
||||||
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
|
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
|
||||||
CTUrlEncoded ->
|
CTUrlEncoded ->
|
||||||
let paramsMap = M.fromList $ (toS *** JSON.String . toS) <$> parseSimpleQuery (toS reqBody) in
|
let paramsMap = M.fromList $ (T.decodeUtf8 *** JSON.String . T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody) in
|
||||||
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (M.keys paramsMap)
|
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (M.keys paramsMap)
|
||||||
ct ->
|
ct ->
|
||||||
Left $ toS $ "Content-Type not acceptable: " <> ContentType.toMime ct
|
if isTargetingProc && ct `elem` [CTTextPlain, CTOctetStream]
|
||||||
|
then Right $ RawPay reqBody
|
||||||
|
else Left $ "Content-Type not acceptable: " <> ContentType.toMime ct
|
||||||
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
||||||
action =
|
action =
|
||||||
case method of
|
case method of
|
||||||
@@ -304,7 +298,7 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
|||||||
"OPTIONS" -> ActionInfo
|
"OPTIONS" -> ActionInfo
|
||||||
_ -> ActionInspect{isHead=False}
|
_ -> ActionInspect{isHead=False}
|
||||||
|
|
||||||
defaultSchema = head configDbSchemas
|
defaultSchema = NonEmptyList.head configDbSchemas
|
||||||
profile
|
profile
|
||||||
| length configDbSchemas <= 1 -- only enable content negotiation by profile when there are multiple schemas specified in the config
|
| length configDbSchemas <= 1 -- only enable content negotiation by profile when there are multiple schemas specified in the config
|
||||||
= Nothing
|
= Nothing
|
||||||
@@ -317,12 +311,14 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
|||||||
ActionInvoke InvPost -> contentProfile
|
ActionInvoke InvPost -> contentProfile
|
||||||
_ -> acceptProfile
|
_ -> acceptProfile
|
||||||
where
|
where
|
||||||
contentProfile = Just $ maybe defaultSchema toS $ lookupHeader "Content-Profile"
|
contentProfile = Just $ maybe defaultSchema T.decodeUtf8 $ lookupHeader "Content-Profile"
|
||||||
acceptProfile = Just $ maybe defaultSchema toS $ lookupHeader "Accept-Profile"
|
acceptProfile = Just $ maybe defaultSchema T.decodeUtf8 $ lookupHeader "Accept-Profile"
|
||||||
schema = fromMaybe defaultSchema profile
|
schema = fromMaybe defaultSchema profile
|
||||||
target =
|
target =
|
||||||
let
|
let
|
||||||
callFindProc procSch procNam = findProc (QualifiedIdentifier procSch procNam) payloadColumns (hasPrefer (show SingleObject)) $ dbProcs dbStructure
|
callFindProc procSch procNam = findProc
|
||||||
|
(QualifiedIdentifier procSch procNam) payloadColumns (preferParameters == Just SingleObject) (dbProcs dbStructure)
|
||||||
|
contentType (action == ActionInvoke InvPost)
|
||||||
in
|
in
|
||||||
case path of
|
case path of
|
||||||
PathInfo{pSchema, pName, pHasRpc, pIsRootSpec, pIsDefaultSpec}
|
PathInfo{pSchema, pName, pHasRpc, pIsRootSpec, pIsDefaultSpec}
|
||||||
@@ -339,7 +335,7 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
|||||||
-- to store the query string arguments to the function.
|
-- to store the query string arguments to the function.
|
||||||
(_, ActionInvoke InvGet) -> targetToJsonRpcParams (rightToMaybe target) rpcQParams
|
(_, ActionInvoke InvGet) -> targetToJsonRpcParams (rightToMaybe target) rpcQParams
|
||||||
(_, ActionInvoke InvHead) -> targetToJsonRpcParams (rightToMaybe target) rpcQParams
|
(_, ActionInvoke InvHead) -> targetToJsonRpcParams (rightToMaybe target) rpcQParams
|
||||||
(CTUrlEncoded, ActionInvoke InvPost) -> targetToJsonRpcParams (rightToMaybe target) $ (toS *** toS) <$> parseSimpleQuery (toS reqBody)
|
(CTUrlEncoded, ActionInvoke InvPost) -> targetToJsonRpcParams (rightToMaybe target) $ (T.decodeUtf8 *** T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody)
|
||||||
_ | shouldParsePayload -> rightToMaybe payload
|
_ | shouldParsePayload -> rightToMaybe payload
|
||||||
| otherwise -> Nothing
|
| otherwise -> Nothing
|
||||||
path =
|
path =
|
||||||
@@ -353,20 +349,11 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
|||||||
_ -> PathUnknown
|
_ -> PathUnknown
|
||||||
method = requestMethod req
|
method = requestMethod req
|
||||||
hdrs = requestHeaders req
|
hdrs = requestHeaders req
|
||||||
qParams = [(toS k, v)|(k,v) <- qString]
|
qParams = [(T.decodeUtf8 k, T.decodeUtf8 <$> v)|(k,v) <- qString]
|
||||||
lookupHeader = flip lookup hdrs
|
lookupHeader = flip lookup hdrs
|
||||||
hasPrefer :: Text -> Bool
|
Preferences.Preferences{..} = Preferences.fromHeaders hdrs
|
||||||
hasPrefer val = any (\(h,v) -> h == "Prefer" && val `elem` split v) hdrs
|
|
||||||
where
|
|
||||||
split :: BS.ByteString -> [Text]
|
|
||||||
split = map T.strip . T.split (==',') . toS
|
|
||||||
representation
|
|
||||||
| hasPrefer (show Full) = Full
|
|
||||||
| hasPrefer (show None) = None
|
|
||||||
| hasPrefer (show HeadersOnly) = HeadersOnly
|
|
||||||
| otherwise = None
|
|
||||||
auth = fromMaybe "" $ lookupHeader hAuthorization
|
auth = fromMaybe "" $ lookupHeader hAuthorization
|
||||||
tokenStr = case T.split (== ' ') (toS auth) of
|
tokenStr = case T.split (== ' ') (T.decodeUtf8 auth) of
|
||||||
("Bearer" : t : _) -> t
|
("Bearer" : t : _) -> t
|
||||||
("bearer" : t : _) -> t
|
("bearer" : t : _) -> t
|
||||||
_ -> ""
|
_ -> ""
|
||||||
@@ -376,9 +363,9 @@ userApiRequest conf@AppConfig{..} dbStructure req reqBody
|
|||||||
|
|
||||||
headerRange = rangeRequested hdrs
|
headerRange = rangeRequested hdrs
|
||||||
replaceLast x s = T.intercalate "." $ L.init (T.split (=='.') s) ++ [x]
|
replaceLast x s = T.intercalate "." $ L.init (T.split (=='.') s) ++ [x]
|
||||||
limitParams :: M.HashMap ByteString NonnegRange
|
limitParams :: M.HashMap Text NonnegRange
|
||||||
limitParams = M.fromList [(toS (replaceLast "limit" k), restrictRange (readMaybe . toS =<< v) allRange) | (k,v) <- qParams, isJust v, endingIn ["limit"] k]
|
limitParams = M.fromList [(toS (replaceLast "limit" k), restrictRange (readMaybe . toS =<< v) allRange) | (k,v) <- qParams, isJust v, endingIn ["limit"] k]
|
||||||
offsetParams :: M.HashMap ByteString NonnegRange
|
offsetParams :: M.HashMap Text NonnegRange
|
||||||
offsetParams = M.fromList [(toS (replaceLast "limit" k), maybe allRange rangeGeq (readMaybe . toS =<< v)) | (k,v) <- qParams, isJust v, endingIn ["offset"] k]
|
offsetParams = M.fromList [(toS (replaceLast "limit" k), maybe allRange rangeGeq (readMaybe . toS =<< v)) | (k,v) <- qParams, isJust v, endingIn ["offset"] k]
|
||||||
|
|
||||||
urlRange = M.unionWith f limitParams offsetParams
|
urlRange = M.unionWith f limitParams offsetParams
|
||||||
@@ -402,7 +389,7 @@ mutuallyAgreeable sProduces cAccepts =
|
|||||||
then listToMaybe sProduces
|
then listToMaybe sProduces
|
||||||
else exact
|
else exact
|
||||||
|
|
||||||
type CsvData = V.Vector (M.HashMap Text BL.ByteString)
|
type CsvData = V.Vector (M.HashMap Text LBS.ByteString)
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Converts CSV like
|
Converts CSV like
|
||||||
@@ -424,10 +411,10 @@ csvToJson (_, vals) =
|
|||||||
M.map (\str ->
|
M.map (\str ->
|
||||||
if str == "NULL"
|
if str == "NULL"
|
||||||
then JSON.Null
|
then JSON.Null
|
||||||
else JSON.String $ toS str
|
else JSON.String . T.decodeUtf8 $ LBS.toStrict str
|
||||||
)
|
)
|
||||||
|
|
||||||
payloadAttributes :: RequestBody -> JSON.Value -> Maybe PayloadJSON
|
payloadAttributes :: RequestBody -> JSON.Value -> Maybe Payload
|
||||||
payloadAttributes raw json =
|
payloadAttributes raw json =
|
||||||
-- Test that Array contains only Objects having the same keys
|
-- Test that Array contains only Objects having the same keys
|
||||||
case json of
|
case json of
|
||||||
@@ -480,37 +467,52 @@ rawContentTypes AppConfig{..} =
|
|||||||
(ContentType.decodeContentType <$> configRawMediaTypes) `union` [CTOctetStream, CTTextPlain]
|
(ContentType.decodeContentType <$> configRawMediaTypes) `union` [CTOctetStream, CTTextPlain]
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Search a pg procedure by its parameters. Since a function can be overloaded, the name is not enough to find it.
|
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
|
||||||
An overloaded function can have a different volatility or even a different return type.
|
the name is not enough to find it. An overloaded function can have a different volatility or even a different return type.
|
||||||
-}
|
-}
|
||||||
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> Either ApiRequestError ProcDescription
|
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> ContentType -> Bool -> Either ApiRequestError ProcDescription
|
||||||
findProc qi payloadKeys paramsAsSingleObject allProcs =
|
findProc qi argumentsKeys paramsAsSingleObject allProcs contentType isInvPost =
|
||||||
case bestMatch of
|
case matchProc of
|
||||||
[] -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList payloadKeys) paramsAsSingleObject
|
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentType isInvPost
|
||||||
[proc] -> Right proc
|
-- If there are no functions with named arguments, fallback to the single unnamed argument function
|
||||||
procs -> Left $ AmbiguousRpc (toList procs)
|
([], [proc]) -> Right proc
|
||||||
|
([], procs) -> Left $ AmbiguousRpc (toList procs)
|
||||||
|
-- Matches the functions with named arguments
|
||||||
|
([proc], _) -> Right proc
|
||||||
|
(procs, _) -> Left $ AmbiguousRpc (toList procs)
|
||||||
where
|
where
|
||||||
bestMatch =
|
matchProc = overloadedProcPartition $ M.lookupDefault mempty qi allProcs -- first find the proc by name
|
||||||
case M.lookup qi allProcs of
|
-- The partition obtained has the form (overloadedProcs,fallbackProcs)
|
||||||
Nothing -> []
|
-- where fallbackProcs are functions with a single unnamed parameter
|
||||||
Just [proc] -> [proc | matches proc]
|
overloadedProcPartition procs = foldr select ([],[]) procs
|
||||||
Just procs -> filter matches procs
|
select proc ~(ts,fs)
|
||||||
-- Find the exact arguments match
|
| matchesParams proc = (proc:ts,fs)
|
||||||
matches proc
|
| hasSingleUnnamedParam proc = (ts,proc:fs)
|
||||||
| paramsAsSingleObject = case pdArgs proc of
|
| otherwise = (ts,fs)
|
||||||
[arg] -> pgaType arg `elem` ["json", "jsonb"]
|
-- If the function is called with post and has a single unnamed parameter
|
||||||
_ -> False
|
-- it can be called depending on content type and the parameter type
|
||||||
| otherwise = case pdArgs proc of
|
hasSingleUnnamedParam proc = isInvPost && case pdParams proc of
|
||||||
[] -> null payloadKeys
|
[ProcParam "" ppType _ _]
|
||||||
args -> matchesArg args
|
| contentType == CTApplicationJSON -> ppType `elem` ["json", "jsonb"]
|
||||||
matchesArg args =
|
| contentType == CTTextPlain -> ppType == "text"
|
||||||
-- The function's required arguments are separated from the ones with a default value assigned.
|
| contentType == CTOctetStream -> ppType == "bytea"
|
||||||
-- The set of names of those arguments is compared to the set of keys supplied by the client
|
| otherwise -> False
|
||||||
-- 1. If only required arguments are found, the keys must be exactly the same as those arguments
|
_ -> False
|
||||||
-- 2. If only optional arguments are found, the keys must be a subset of those arguments
|
matchesParams proc =
|
||||||
-- 3. If both required and optional arguments are found, the result of taking away the optional arguments
|
let params = pdParams proc in
|
||||||
-- from the keys must be exactly the same as the required arguments
|
-- exceptional case for Prefer: params=single-object
|
||||||
case L.partition pgaReq args of
|
if paramsAsSingleObject
|
||||||
(reqArgs, []) -> payloadKeys == S.fromList (pgaName <$> reqArgs)
|
then length params == 1 && (ppType <$> headMay params) `elem` [Just "json", Just "jsonb"]
|
||||||
([], defArgs) -> payloadKeys `S.isSubsetOf` S.fromList (pgaName <$> defArgs)
|
-- If the function has no parameters, the arguments keys must be empty as well
|
||||||
(reqArgs, defArgs) -> payloadKeys `S.difference` S.fromList (pgaName <$> defArgs) == S.fromList (pgaName <$> reqArgs)
|
else if null params
|
||||||
|
then null argumentsKeys && contentType `notElem` [CTTextPlain, CTOctetStream]
|
||||||
|
-- A function has optional and required parameters. Optional parameters have a default value and
|
||||||
|
-- don't require arguments for the function to be executed, required parameters must have an argument present.
|
||||||
|
else case L.partition ppReq params of
|
||||||
|
-- If the function only has required parameters, the arguments keys must match those parameters
|
||||||
|
(reqParams, []) -> argumentsKeys == S.fromList (ppName <$> reqParams)
|
||||||
|
-- If the function only has optional parameters, the arguments keys can match none or any of them(a subset)
|
||||||
|
([], optParams) -> argumentsKeys `S.isSubsetOf` S.fromList (ppName <$> optParams)
|
||||||
|
-- If the function has required and optional parameters, the arguments keys have to match the required parameters
|
||||||
|
-- and can match any or none of the default parameters.
|
||||||
|
(reqParams, optParams) -> argumentsKeys `S.difference` S.fromList (ppName <$> optParams) == S.fromList (ppName <$> reqParams)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ resource.
|
|||||||
module PostgREST.Request.DbRequestBuilder
|
module PostgREST.Request.DbRequestBuilder
|
||||||
( readRequest
|
( readRequest
|
||||||
, mutateRequest
|
, mutateRequest
|
||||||
, returningCols
|
, callRequest
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
@@ -33,6 +33,9 @@ import Data.Tree (Tree (..))
|
|||||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
Schema, TableName)
|
Schema, TableName)
|
||||||
|
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
||||||
|
ProcParam (..),
|
||||||
|
procReturnsScalar)
|
||||||
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
import PostgREST.DbStructure.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
Relationship (..))
|
Relationship (..))
|
||||||
@@ -45,7 +48,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
|
|||||||
restrictRange)
|
restrictRange)
|
||||||
import PostgREST.Request.ApiRequest (Action (..),
|
import PostgREST.Request.ApiRequest (Action (..),
|
||||||
ApiRequest (..),
|
ApiRequest (..),
|
||||||
PayloadJSON (..))
|
Payload (..))
|
||||||
|
|
||||||
import PostgREST.Request.Parsers
|
import PostgREST.Request.Parsers
|
||||||
import PostgREST.Request.Preferences
|
import PostgREST.Request.Preferences
|
||||||
@@ -97,16 +100,16 @@ initReadRequest rootQi =
|
|||||||
rootDepth = 0
|
rootDepth = 0
|
||||||
rootSchema = qiSchema rootQi
|
rootSchema = qiSchema rootQi
|
||||||
rootName = qiName rootQi
|
rootName = qiName rootQi
|
||||||
initial = Node (Select [] rootQi Nothing [] [] [] [] allRange, (rootName, Nothing, Nothing, Nothing, rootDepth)) []
|
initial = Node (Select [] rootQi Nothing [] [] [] [] allRange, (rootName, Nothing, Nothing, Nothing, Nothing, rootDepth)) []
|
||||||
treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest
|
treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest
|
||||||
treeEntry depth (Node fld@((fn, _),_,alias, embedHint) fldForest) (Node (q, i) rForest) =
|
treeEntry depth (Node fld@((fn, _),_,alias, hint, joinType) fldForest) (Node (q, i) rForest) =
|
||||||
let nxtDepth = succ depth in
|
let nxtDepth = succ depth in
|
||||||
case fldForest of
|
case fldForest of
|
||||||
[] -> Node (q {select=fld:select q}, i) rForest
|
[] -> Node (q {select=fld:select q}, i) rForest
|
||||||
_ -> Node (q, i) $
|
_ -> Node (q, i) $
|
||||||
foldr (treeEntry nxtDepth)
|
foldr (treeEntry nxtDepth)
|
||||||
(Node (Select [] (QualifiedIdentifier rootSchema fn) Nothing [] [] [] [] allRange,
|
(Node (Select [] (QualifiedIdentifier rootSchema fn) Nothing [] [] [] [] allRange,
|
||||||
(fn, Nothing, alias, embedHint, nxtDepth)) [])
|
(fn, Nothing, alias, hint, joinType, nxtDepth)) [])
|
||||||
fldForest:rForest
|
fldForest:rForest
|
||||||
|
|
||||||
-- | Enforces the `max-rows` config on the result
|
-- | Enforces the `max-rows` config on the result
|
||||||
@@ -122,16 +125,16 @@ augmentRequestWithJoin schema allRels request =
|
|||||||
>>= addJoinConditions Nothing
|
>>= addJoinConditions Nothing
|
||||||
|
|
||||||
addRels :: Schema -> [Relationship] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest
|
addRels :: Schema -> [Relationship] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||||
addRels schema allRels parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, hint, depth)) forest) =
|
addRels schema allRels parentNode (Node (query@Select{from=tbl}, (nodeName, _, alias, hint, joinType, depth)) forest) =
|
||||||
case parentNode of
|
case parentNode of
|
||||||
Just (Node (Select{from=parentNodeQi}, _) _) ->
|
Just (Node (Select{from=parentNodeQi}, _) _) ->
|
||||||
let newFrom r = if qiName tbl == nodeName then tableQi (relForeignTable r) else tbl
|
let newFrom r = if qiName tbl == nodeName then tableQi (relForeignTable r) else tbl
|
||||||
newReadNode = (\r -> (query{from=newFrom r}, (nodeName, Just r, alias, Nothing, depth))) <$> rel
|
newReadNode = (\r -> (query{from=newFrom r}, (nodeName, Just r, alias, hint, joinType, depth))) <$> rel
|
||||||
rel = findRel schema allRels (qiName parentNodeQi) nodeName hint
|
rel = findRel schema allRels (qiName parentNodeQi) nodeName hint
|
||||||
in
|
in
|
||||||
Node <$> newReadNode <*> (updateForest . hush $ Node <$> newReadNode <*> pure forest)
|
Node <$> newReadNode <*> (updateForest . hush $ Node <$> newReadNode <*> pure forest)
|
||||||
_ ->
|
_ ->
|
||||||
let rn = (query, (nodeName, Nothing, alias, Nothing, depth)) in
|
let rn = (query, (nodeName, Nothing, alias, Nothing, joinType, depth)) in
|
||||||
Node rn <$> updateForest (Just $ Node rn forest)
|
Node rn <$> updateForest (Just $ Node rn forest)
|
||||||
where
|
where
|
||||||
updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest]
|
updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest]
|
||||||
@@ -147,7 +150,7 @@ addRels schema allRels parentNode (Node (query@Select{from=tbl}, (nodeName, _, a
|
|||||||
-- target = table / view / constraint / column-from-origin
|
-- target = table / view / constraint / column-from-origin
|
||||||
-- hint = table / view / constraint / column-from-origin / column-from-target
|
-- hint = table / view / constraint / column-from-origin / column-from-target
|
||||||
-- (hint can take table / view values to aid in finding the junction in an m2m relationship)
|
-- (hint can take table / view values to aid in finding the junction in an m2m relationship)
|
||||||
findRel :: Schema -> [Relationship] -> NodeName -> NodeName -> Maybe EmbedHint -> Either ApiRequestError Relationship
|
findRel :: Schema -> [Relationship] -> NodeName -> NodeName -> Maybe Hint -> Either ApiRequestError Relationship
|
||||||
findRel schema allRels origin target hint =
|
findRel schema allRels origin target hint =
|
||||||
case rel of
|
case rel of
|
||||||
[] -> Left $ NoRelBetween origin target
|
[] -> Left $ NoRelBetween origin target
|
||||||
@@ -207,7 +210,7 @@ findRel schema allRels origin target hint =
|
|||||||
|
|
||||||
-- previousAlias is only used for the case of self joins
|
-- previousAlias is only used for the case of self joins
|
||||||
addJoinConditions :: Maybe Alias -> ReadRequest -> Either ApiRequestError ReadRequest
|
addJoinConditions :: Maybe Alias -> ReadRequest -> Either ApiRequestError ReadRequest
|
||||||
addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, rel, _, _, depth)) forest) =
|
addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_, rel, _, _, _, depth)) forest) =
|
||||||
case rel of
|
case rel of
|
||||||
Just r@Relationship{relCardinality=M2M Junction{junTable}} ->
|
Just r@Relationship{relCardinality=M2M Junction{junTable}} ->
|
||||||
let rq = augmentQuery r in
|
let rq = augmentQuery r in
|
||||||
@@ -304,7 +307,7 @@ addProperty f (targetNodeName:remainingPath, a) (Node rn forest) =
|
|||||||
Nothing -> Node rn forest -- the property is silenty dropped in the Request does not contain the required path
|
Nothing -> Node rn forest -- the property is silenty dropped in the Request does not contain the required path
|
||||||
Just tn -> Node rn (addProperty f (remainingPath, a) tn:delete tn forest)
|
Just tn -> Node rn (addProperty f (remainingPath, a) tn:delete tn forest)
|
||||||
where
|
where
|
||||||
pathNode = find (\(Node (_,(nodeName,_,alias,_,_)) _) -> nodeName == targetNodeName || alias == Just targetNodeName) forest
|
pathNode = find (\(Node (_,(nodeName,_,alias,_,_, _)) _) -> nodeName == targetNodeName || alias == Just targetNodeName) forest
|
||||||
|
|
||||||
mutateRequest :: Schema -> TableName -> ApiRequest -> [FieldName] -> ReadRequest -> Either Error MutateRequest
|
mutateRequest :: Schema -> TableName -> ApiRequest -> [FieldName] -> ReadRequest -> Either Error MutateRequest
|
||||||
mutateRequest schema tName apiRequest pkCols readReq = mapLeft ApiRequestError $
|
mutateRequest schema tName apiRequest pkCols readReq = mapLeft ApiRequestError $
|
||||||
@@ -341,7 +344,25 @@ mutateRequest schema tName apiRequest pkCols readReq = mapLeft ApiRequestError $
|
|||||||
-- update/delete filters can be only on the root table
|
-- update/delete filters can be only on the root table
|
||||||
(mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest)
|
(mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest)
|
||||||
onlyRoot = filter (not . ( "." `isInfixOf` ) . fst)
|
onlyRoot = filter (not . ( "." `isInfixOf` ) . fst)
|
||||||
body = pjRaw <$> iPayload apiRequest
|
body = payRaw <$> iPayload apiRequest -- the body is assumed to be json at this stage(ApiRequest validates)
|
||||||
|
|
||||||
|
callRequest :: ProcDescription -> ApiRequest -> ReadRequest -> CallRequest
|
||||||
|
callRequest proc apiReq readReq = FunctionCall {
|
||||||
|
funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc)
|
||||||
|
, funCParams = callParams
|
||||||
|
, funCArgs = payRaw <$> iPayload apiReq
|
||||||
|
, funCScalar = procReturnsScalar proc
|
||||||
|
, funCMultipleCall = iPreferParameters apiReq == Just MultipleObjects
|
||||||
|
, funCReturning = returningCols readReq []
|
||||||
|
}
|
||||||
|
where
|
||||||
|
paramsAsSingleObject = iPreferParameters apiReq == Just SingleObject
|
||||||
|
callParams = case pdParams proc of
|
||||||
|
[prm] | paramsAsSingleObject -> OnePosParam prm
|
||||||
|
| ppName prm == mempty -> OnePosParam prm
|
||||||
|
| otherwise -> KeyParams $ specifiedParams [prm]
|
||||||
|
prms -> KeyParams $ specifiedParams prms
|
||||||
|
specifiedParams params = filter (\x -> ppName x `S.member` iColumns apiReq) params
|
||||||
|
|
||||||
returningCols :: ReadRequest -> [FieldName] -> [FieldName]
|
returningCols :: ReadRequest -> [FieldName] -> [FieldName]
|
||||||
returningCols rr@(Node _ forest) pkCols
|
returningCols rr@(Node _ forest) pkCols
|
||||||
@@ -358,7 +379,7 @@ returningCols rr@(Node _ forest) pkCols
|
|||||||
-- projects. So this adds the foreign key columns to ensure the embedding
|
-- projects. So this adds the foreign key columns to ensure the embedding
|
||||||
-- succeeds, result would be `RETURNING name, client_id`.
|
-- succeeds, result would be `RETURNING name, client_id`.
|
||||||
fkCols = concat $ mapMaybe (\case
|
fkCols = concat $ mapMaybe (\case
|
||||||
Node (_, (_, Just Relationship{relColumns=cols}, _, _, _)) _ -> Just cols
|
Node (_, (_, Just Relationship{relColumns=cols}, _, _, _, _)) _ -> Just cols
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
) forest
|
) forest
|
||||||
-- However if the "client_id" is present, e.g. mutateRequest to
|
-- However if the "client_id" is present, e.g. mutateRequest to
|
||||||
|
|||||||
@@ -47,8 +47,7 @@ import PostgREST.RangeQuery (NonnegRange)
|
|||||||
|
|
||||||
import PostgREST.Request.Types
|
import PostgREST.Request.Types
|
||||||
|
|
||||||
import Protolude hiding (intercalate, option, replace, toS, try)
|
import Protolude hiding (intercalate, option, replace, try)
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
pRequestSelect :: Text -> Either ApiRequestError [Tree SelectItem]
|
pRequestSelect :: Text -> Either ApiRequestError [Tree SelectItem]
|
||||||
pRequestSelect selStr =
|
pRequestSelect selStr =
|
||||||
@@ -73,7 +72,7 @@ pRequestOrder (k, v) = mapError $ (,) <$> path <*> ord'
|
|||||||
path = fst <$> treePath
|
path = fst <$> treePath
|
||||||
ord' = parse pOrder ("failed to parse order (" ++ toS v ++ ")") $ toS v
|
ord' = parse pOrder ("failed to parse order (" ++ toS v ++ ")") $ toS v
|
||||||
|
|
||||||
pRequestRange :: (ByteString, NonnegRange) -> Either ApiRequestError (EmbedPath, NonnegRange)
|
pRequestRange :: (Text, NonnegRange) -> Either ApiRequestError (EmbedPath, NonnegRange)
|
||||||
pRequestRange (k, v) = mapError $ (,) <$> path <*> pure v
|
pRequestRange (k, v) = mapError $ (,) <$> path <*> pure v
|
||||||
where
|
where
|
||||||
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
|
||||||
@@ -117,7 +116,7 @@ pFieldForest = pFieldTree `sepBy1` lexeme (char ',')
|
|||||||
Node <$> pFieldSelect <*> pure []
|
Node <$> pFieldSelect <*> pure []
|
||||||
|
|
||||||
pStar :: Parser Text
|
pStar :: Parser Text
|
||||||
pStar = toS <$> (string "*" $> ("*"::ByteString))
|
pStar = string "*" $> "*"
|
||||||
|
|
||||||
pFieldName :: Parser Text
|
pFieldName :: Parser Text
|
||||||
pFieldName =
|
pFieldName =
|
||||||
@@ -158,13 +157,23 @@ pRelationSelect :: Parser SelectItem
|
|||||||
pRelationSelect = lexeme $ try ( do
|
pRelationSelect = lexeme $ try ( do
|
||||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||||
fld <- pField
|
fld <- pField
|
||||||
hint <- optionMaybe (
|
prm1 <- optionMaybe pEmbedParam
|
||||||
try ( char '!' *> pFieldName) <|>
|
prm2 <- optionMaybe pEmbedParam
|
||||||
-- deprecated, remove in next major version
|
return (fld, Nothing, alias, embedParamHint prm1 <|> embedParamHint prm2, embedParamJoin prm1 <|> embedParamJoin prm2)
|
||||||
try ( char '.' *> pFieldName)
|
|
||||||
)
|
|
||||||
return (fld, Nothing, alias, hint)
|
|
||||||
)
|
)
|
||||||
|
where
|
||||||
|
pEmbedParam :: Parser EmbedParam
|
||||||
|
pEmbedParam =
|
||||||
|
char '!' *> (
|
||||||
|
try (string "left" $> EPJoinType JTLeft) <|>
|
||||||
|
try (string "inner" $> EPJoinType JTInner) <|>
|
||||||
|
try (EPHint <$> pFieldName))
|
||||||
|
embedParamHint prm = case prm of
|
||||||
|
Just (EPHint hint) -> Just hint
|
||||||
|
_ -> Nothing
|
||||||
|
embedParamJoin prm = case prm of
|
||||||
|
Just (EPJoinType jt) -> Just jt
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
pFieldSelect :: Parser SelectItem
|
pFieldSelect :: Parser SelectItem
|
||||||
pFieldSelect = lexeme $
|
pFieldSelect = lexeme $
|
||||||
@@ -173,11 +182,11 @@ pFieldSelect = lexeme $
|
|||||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||||
fld <- pField
|
fld <- pField
|
||||||
cast' <- optionMaybe (string "::" *> many letter)
|
cast' <- optionMaybe (string "::" *> many letter)
|
||||||
return (fld, toS <$> cast', alias, Nothing)
|
return (fld, toS <$> cast', alias, Nothing, Nothing)
|
||||||
)
|
)
|
||||||
<|> do
|
<|> do
|
||||||
s <- pStar
|
s <- pStar
|
||||||
return ((s, []), Nothing, Nothing, Nothing)
|
return ((s, []), Nothing, Nothing, Nothing, Nothing)
|
||||||
|
|
||||||
pOpExpr :: Parser SingleVal -> Parser OpExpr
|
pOpExpr :: Parser SingleVal -> Parser OpExpr
|
||||||
pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)) <|> OpExpr False <$> pOperation
|
pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)) <|> OpExpr False <$> pOperation
|
||||||
@@ -186,15 +195,22 @@ pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)
|
|||||||
pOperation =
|
pOperation =
|
||||||
Op . toS <$> foldl1 (<|>) (try . ((<* pDelimiter) . string) . toS <$> M.keys ops) <*> pSVal
|
Op . toS <$> foldl1 (<|>) (try . ((<* pDelimiter) . string) . toS <$> M.keys ops) <*> pSVal
|
||||||
<|> In <$> (try (string "in" *> pDelimiter) *> pListVal)
|
<|> In <$> (try (string "in" *> pDelimiter) *> pListVal)
|
||||||
|
<|> Is <$> (try (string "is" *> pDelimiter) *> pTriVal)
|
||||||
<|> pFts
|
<|> pFts
|
||||||
<?> "operator (eq, gt, ...)"
|
<?> "operator (eq, gt, ...)"
|
||||||
|
|
||||||
|
pTriVal = try (string "null" $> TriNull)
|
||||||
|
<|> try (string "unknown" $> TriUnknown)
|
||||||
|
<|> try (string "true" $> TriTrue)
|
||||||
|
<|> try (string "false" $> TriFalse)
|
||||||
|
<?> "null or trilean value (unknown, true, false)"
|
||||||
|
|
||||||
pFts = do
|
pFts = do
|
||||||
op <- foldl1 (<|>) (try . string . toS <$> ftsOps)
|
op <- foldl1 (<|>) (try . string . toS <$> ftsOps)
|
||||||
lang <- optionMaybe $ try (between (char '(') (char ')') (many (letter <|> digit <|> oneOf "_")))
|
lang <- optionMaybe $ try (between (char '(') (char ')') (many (letter <|> digit <|> oneOf "_")))
|
||||||
pDelimiter >> Fts (toS op) (toS <$> lang) <$> pSVal
|
pDelimiter >> Fts (toS op) (toS <$> lang) <$> pSVal
|
||||||
|
|
||||||
ops = M.filterWithKey (const . flip notElem ("in":ftsOps)) operators
|
ops = M.filterWithKey (const . flip notElem ("in":"is":ftsOps)) operators
|
||||||
ftsOps = M.keys ftsOperators
|
ftsOps = M.keys ftsOperators
|
||||||
|
|
||||||
pSingleVal :: Parser SingleVal
|
pSingleVal :: Parser SingleVal
|
||||||
@@ -207,7 +223,9 @@ pListElement :: Parser Text
|
|||||||
pListElement = try (pQuotedValue <* notFollowedBy (noneOf ",)")) <|> (toS <$> many (noneOf ",)"))
|
pListElement = try (pQuotedValue <* notFollowedBy (noneOf ",)")) <|> (toS <$> many (noneOf ",)"))
|
||||||
|
|
||||||
pQuotedValue :: Parser Text
|
pQuotedValue :: Parser Text
|
||||||
pQuotedValue = toS <$> (char '"' *> many (noneOf "\"") <* char '"')
|
pQuotedValue = toS <$> (char '"' *> many pCharsOrSlashed <* char '"')
|
||||||
|
where
|
||||||
|
pCharsOrSlashed = noneOf "\\\"" <|> (char '\\' *> anyChar)
|
||||||
|
|
||||||
pDelimiter :: Parser Char
|
pDelimiter :: Parser Char
|
||||||
pDelimiter = char '.' <?> "delimiter (.)"
|
pDelimiter = char '.' <?> "delimiter (.)"
|
||||||
|
|||||||
@@ -1,54 +1,200 @@
|
|||||||
module PostgREST.Request.Preferences where
|
-- |
|
||||||
|
-- Module: PostgREST.Request.Preferences
|
||||||
|
-- Description: Track client preferences to be employed when processing requests
|
||||||
|
--
|
||||||
|
-- Track client prefences set in HTTP 'Prefer' headers according to RFC7240[1].
|
||||||
|
--
|
||||||
|
-- [1] https://datatracker.ietf.org/doc/html/rfc7240
|
||||||
|
--
|
||||||
|
module PostgREST.Request.Preferences
|
||||||
|
( Preferences(..)
|
||||||
|
, PreferCount(..)
|
||||||
|
, PreferParameters(..)
|
||||||
|
, PreferRepresentation(..)
|
||||||
|
, PreferResolution(..)
|
||||||
|
, PreferTransaction(..)
|
||||||
|
, fromHeaders
|
||||||
|
, ToAppliedHeader(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
import qualified Data.Map as Map
|
||||||
|
import qualified Network.HTTP.Types.Header as HTTP
|
||||||
|
|
||||||
import GHC.Show
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
|
-- $setup
|
||||||
|
-- Setup for doctests
|
||||||
|
-- >>> import Text.Pretty.Simple (pPrint)
|
||||||
|
-- >>> deriving instance Show PreferResolution
|
||||||
|
-- >>> deriving instance Show PreferRepresentation
|
||||||
|
-- >>> deriving instance Show PreferParameters
|
||||||
|
-- >>> deriving instance Show PreferCount
|
||||||
|
-- >>> deriving instance Show PreferTransaction
|
||||||
|
-- >>> deriving instance Show Preferences
|
||||||
|
|
||||||
|
-- | Preferences recognized by the application.
|
||||||
|
data Preferences
|
||||||
|
= Preferences
|
||||||
|
{ preferResolution :: Maybe PreferResolution
|
||||||
|
, preferRepresentation :: Maybe PreferRepresentation
|
||||||
|
, preferParameters :: Maybe PreferParameters
|
||||||
|
, preferCount :: Maybe PreferCount
|
||||||
|
, preferTransaction :: Maybe PreferTransaction
|
||||||
|
}
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Parse HTTP headers based on RFC7240[1] to identify preferences.
|
||||||
|
--
|
||||||
|
-- One header with comma-separated values can be used to set multiple preferences:
|
||||||
|
--
|
||||||
|
-- >>> pPrint $ fromHeaders [("Prefer", "resolution=ignore-duplicates, count=exact")]
|
||||||
|
-- Preferences
|
||||||
|
-- { preferResolution = Just IgnoreDuplicates
|
||||||
|
-- , preferRepresentation = Nothing
|
||||||
|
-- , preferParameters = Nothing
|
||||||
|
-- , preferCount = Just ExactCount
|
||||||
|
-- , preferTransaction = Nothing
|
||||||
|
-- }
|
||||||
|
--
|
||||||
|
-- Multiple headers can also be used:
|
||||||
|
--
|
||||||
|
-- >>> pPrint $ fromHeaders [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact")]
|
||||||
|
-- Preferences
|
||||||
|
-- { preferResolution = Just IgnoreDuplicates
|
||||||
|
-- , preferRepresentation = Nothing
|
||||||
|
-- , preferParameters = Nothing
|
||||||
|
-- , preferCount = Just ExactCount
|
||||||
|
-- , preferTransaction = Nothing
|
||||||
|
-- }
|
||||||
|
--
|
||||||
|
-- If a preference is set more than once, only the first is used:
|
||||||
|
--
|
||||||
|
-- >>> preferTransaction $ fromHeaders [("Prefer", "tx=commit, tx=rollback")]
|
||||||
|
-- Just Commit
|
||||||
|
--
|
||||||
|
-- This is also the case across multiple headers:
|
||||||
|
--
|
||||||
|
-- >>> :{
|
||||||
|
-- preferResolution . fromHeaders $
|
||||||
|
-- [ ("Prefer", "resolution=ignore-duplicates")
|
||||||
|
-- , ("Prefer", "resolution=merge-duplicates")
|
||||||
|
-- ]
|
||||||
|
-- :}
|
||||||
|
-- Just IgnoreDuplicates
|
||||||
|
--
|
||||||
|
-- Preferences not recognized by the application are ignored:
|
||||||
|
--
|
||||||
|
-- >>> preferResolution $ fromHeaders [("Prefer", "resolution=foo")]
|
||||||
|
-- Nothing
|
||||||
|
--
|
||||||
|
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
|
||||||
|
--
|
||||||
|
-- >>> pPrint $ fromHeaders [("prefer", "count=exact, tx=commit ,return=minimal")]
|
||||||
|
-- Preferences
|
||||||
|
-- { preferResolution = Nothing
|
||||||
|
-- , preferRepresentation = Just None
|
||||||
|
-- , preferParameters = Nothing
|
||||||
|
-- , preferCount = Just ExactCount
|
||||||
|
-- , preferTransaction = Just Commit
|
||||||
|
-- }
|
||||||
|
--
|
||||||
|
fromHeaders :: [HTTP.Header] -> Preferences
|
||||||
|
fromHeaders headers =
|
||||||
|
Preferences
|
||||||
|
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
|
||||||
|
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
|
||||||
|
, preferParameters = parsePrefs [SingleObject, MultipleObjects]
|
||||||
|
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
||||||
|
, preferTransaction = parsePrefs [Commit, Rollback]
|
||||||
|
}
|
||||||
|
where
|
||||||
|
prefHeaders = filter ((==) HTTP.hPrefer . fst) headers
|
||||||
|
prefs = fmap BS.strip . concatMap (BS.split ',' . snd) $ prefHeaders
|
||||||
|
|
||||||
|
parsePrefs :: ToHeaderValue a => [a] -> Maybe a
|
||||||
|
parsePrefs vals =
|
||||||
|
head $ mapMaybe (flip Map.lookup $ prefMap vals) prefs
|
||||||
|
|
||||||
|
prefMap :: ToHeaderValue a => [a] -> Map.Map ByteString a
|
||||||
|
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Convert a preference into the value that we look for in the 'Prefer' headers.
|
||||||
|
--
|
||||||
|
-- >>> toHeaderValue MergeDuplicates
|
||||||
|
-- "resolution=merge-duplicates"
|
||||||
|
--
|
||||||
|
class ToHeaderValue a where
|
||||||
|
toHeaderValue :: a -> ByteString
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- Header to indicate that a preference has been applied.
|
||||||
|
--
|
||||||
|
-- >>> toAppliedHeader MergeDuplicates
|
||||||
|
-- ("Preference-Applied","resolution=merge-duplicates")
|
||||||
|
--
|
||||||
|
class ToHeaderValue a => ToAppliedHeader a where
|
||||||
|
toAppliedHeader :: a -> HTTP.Header
|
||||||
|
toAppliedHeader x = (HTTP.hPreferenceApplied, toHeaderValue x)
|
||||||
|
|
||||||
|
-- | How to handle duplicate values.
|
||||||
data PreferResolution
|
data PreferResolution
|
||||||
= MergeDuplicates
|
= MergeDuplicates
|
||||||
| IgnoreDuplicates
|
| IgnoreDuplicates
|
||||||
|
|
||||||
instance Show PreferResolution where
|
instance ToHeaderValue PreferResolution where
|
||||||
show MergeDuplicates = "resolution=merge-duplicates"
|
toHeaderValue MergeDuplicates = "resolution=merge-duplicates"
|
||||||
show IgnoreDuplicates = "resolution=ignore-duplicates"
|
toHeaderValue IgnoreDuplicates = "resolution=ignore-duplicates"
|
||||||
|
|
||||||
-- | How to return the mutated data. From https://tools.ietf.org/html/rfc7240#section-4.2
|
instance ToAppliedHeader PreferResolution
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- How to return the mutated data.
|
||||||
|
--
|
||||||
|
-- From https://tools.ietf.org/html/rfc7240#section-4.2
|
||||||
data PreferRepresentation
|
data PreferRepresentation
|
||||||
= Full -- ^ Return the body plus the Location header(in case of POST).
|
= Full -- ^ Return the body plus the Location header(in case of POST).
|
||||||
| HeadersOnly -- ^ Return the Location header(in case of POST). This needs a SELECT privilege on the pk.
|
| HeadersOnly -- ^ Return the Location header(in case of POST). This needs a SELECT privilege on the pk.
|
||||||
| None -- ^ Return nothing from the mutated data.
|
| None -- ^ Return nothing from the mutated data.
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
instance Show PreferRepresentation where
|
instance ToHeaderValue PreferRepresentation where
|
||||||
show Full = "return=representation"
|
toHeaderValue Full = "return=representation"
|
||||||
show None = "return=minimal"
|
toHeaderValue None = "return=minimal"
|
||||||
show HeadersOnly = "return=headers-only"
|
toHeaderValue HeadersOnly = "return=headers-only"
|
||||||
|
|
||||||
|
-- | How to pass parameters to stored procedures.
|
||||||
data PreferParameters
|
data PreferParameters
|
||||||
= SingleObject -- ^ Pass all parameters as a single json object to a stored procedure
|
= SingleObject -- ^ Pass all parameters as a single json object to a stored procedure.
|
||||||
| MultipleObjects -- ^ Pass an array of json objects as params to a stored procedure
|
| MultipleObjects -- ^ Pass an array of json objects as params to a stored procedure.
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
instance Show PreferParameters where
|
instance ToHeaderValue PreferParameters where
|
||||||
show SingleObject = "params=single-object"
|
toHeaderValue SingleObject = "params=single-object"
|
||||||
show MultipleObjects = "params=multiple-objects"
|
toHeaderValue MultipleObjects = "params=multiple-objects"
|
||||||
|
|
||||||
|
-- | How to determine the count of (expected) results
|
||||||
data PreferCount
|
data PreferCount
|
||||||
= ExactCount -- ^ exact count(slower)
|
= ExactCount -- ^ Exact count (slower).
|
||||||
| PlannedCount -- ^ PostgreSQL query planner rows count guess. Done by using EXPLAIN {query}.
|
| PlannedCount -- ^ PostgreSQL query planner rows count guess. Done by using EXPLAIN {query}.
|
||||||
| EstimatedCount -- ^ use the query planner rows if the count is superior to max-rows, otherwise get the exact count.
|
| EstimatedCount -- ^ Use the query planner rows if the count is superior to max-rows, otherwise get the exact count.
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
instance Show PreferCount where
|
instance ToHeaderValue PreferCount where
|
||||||
show ExactCount = "count=exact"
|
toHeaderValue ExactCount = "count=exact"
|
||||||
show PlannedCount = "count=planned"
|
toHeaderValue PlannedCount = "count=planned"
|
||||||
show EstimatedCount = "count=estimated"
|
toHeaderValue EstimatedCount = "count=estimated"
|
||||||
|
|
||||||
|
-- | Whether to commit or roll back transactions.
|
||||||
data PreferTransaction
|
data PreferTransaction
|
||||||
= Commit -- Commit transaction - the default.
|
= Commit -- ^ Commit transaction - the default.
|
||||||
| Rollback -- Rollback transaction after sending the response - does not persist changes, e.g. for running tests.
|
| Rollback -- ^ Rollback transaction after sending the response - does not persist changes, e.g. for running tests.
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
instance Show PreferTransaction where
|
instance ToHeaderValue PreferTransaction where
|
||||||
show Commit = "tx=commit"
|
toHeaderValue Commit = "tx=commit"
|
||||||
show Rollback = "tx=rollback"
|
toHeaderValue Rollback = "tx=rollback"
|
||||||
|
|
||||||
|
instance ToAppliedHeader PreferTransaction
|
||||||
|
|||||||
@@ -2,11 +2,16 @@
|
|||||||
module PostgREST.Request.Types
|
module PostgREST.Request.Types
|
||||||
( Alias
|
( Alias
|
||||||
, Depth
|
, Depth
|
||||||
, EmbedHint
|
, EmbedParam(..)
|
||||||
, EmbedPath
|
, EmbedPath
|
||||||
, Field
|
, Field
|
||||||
, Filter(..)
|
, Filter(..)
|
||||||
|
, Hint
|
||||||
|
, CallQuery(..)
|
||||||
|
, CallParams(..)
|
||||||
|
, CallRequest
|
||||||
, JoinCondition(..)
|
, JoinCondition(..)
|
||||||
|
, JoinType(..)
|
||||||
, JsonOperand(..)
|
, JsonOperand(..)
|
||||||
, JsonOperation(..)
|
, JsonOperation(..)
|
||||||
, JsonPath
|
, JsonPath
|
||||||
@@ -26,18 +31,18 @@ module PostgREST.Request.Types
|
|||||||
, ReadRequest
|
, ReadRequest
|
||||||
, SelectItem
|
, SelectItem
|
||||||
, SingleVal
|
, SingleVal
|
||||||
|
, TrileanVal(..)
|
||||||
, fstFieldNames
|
, fstFieldNames
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
|
|
||||||
import Data.Tree (Tree (..))
|
import Data.Tree (Tree (..))
|
||||||
|
|
||||||
import qualified GHC.Show (show)
|
|
||||||
|
|
||||||
import PostgREST.DbStructure.Identifiers (FieldName,
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
QualifiedIdentifier)
|
QualifiedIdentifier)
|
||||||
|
import PostgREST.DbStructure.Proc (ProcParam (..))
|
||||||
import PostgREST.DbStructure.Relationship (Relationship)
|
import PostgREST.DbStructure.Relationship (Relationship)
|
||||||
import PostgREST.RangeQuery (NonnegRange)
|
import PostgREST.RangeQuery (NonnegRange)
|
||||||
import PostgREST.Request.Preferences (PreferResolution)
|
import PostgREST.Request.Preferences (PreferResolution)
|
||||||
@@ -47,9 +52,10 @@ import Protolude
|
|||||||
|
|
||||||
type ReadRequest = Tree ReadNode
|
type ReadRequest = Tree ReadNode
|
||||||
type MutateRequest = MutateQuery
|
type MutateRequest = MutateQuery
|
||||||
|
type CallRequest = CallQuery
|
||||||
|
|
||||||
type ReadNode =
|
type ReadNode =
|
||||||
(ReadQuery, (NodeName, Maybe Relationship, Maybe Alias, Maybe EmbedHint, Depth))
|
(ReadQuery, (NodeName, Maybe Relationship, Maybe Alias, Maybe Hint, Maybe JoinType, Depth))
|
||||||
|
|
||||||
type NodeName = Text
|
type NodeName = Text
|
||||||
type Depth = Integer
|
type Depth = Integer
|
||||||
@@ -86,24 +92,16 @@ data OrderDirection
|
|||||||
| OrderDesc
|
| OrderDesc
|
||||||
deriving (Eq)
|
deriving (Eq)
|
||||||
|
|
||||||
instance Show OrderDirection where
|
|
||||||
show OrderAsc = "ASC"
|
|
||||||
show OrderDesc = "DESC"
|
|
||||||
|
|
||||||
data OrderNulls
|
data OrderNulls
|
||||||
= OrderNullsFirst
|
= OrderNullsFirst
|
||||||
| OrderNullsLast
|
| OrderNullsLast
|
||||||
deriving (Eq)
|
deriving (Eq)
|
||||||
|
|
||||||
instance Show OrderNulls where
|
|
||||||
show OrderNullsFirst = "NULLS FIRST"
|
|
||||||
show OrderNullsLast = "NULLS LAST"
|
|
||||||
|
|
||||||
data MutateQuery
|
data MutateQuery
|
||||||
= Insert
|
= Insert
|
||||||
{ in_ :: QualifiedIdentifier
|
{ in_ :: QualifiedIdentifier
|
||||||
, insCols :: S.Set FieldName
|
, insCols :: S.Set FieldName
|
||||||
, insBody :: Maybe BL.ByteString
|
, insBody :: Maybe LBS.ByteString
|
||||||
, onConflict :: Maybe (PreferResolution, [FieldName])
|
, onConflict :: Maybe (PreferResolution, [FieldName])
|
||||||
, where_ :: [LogicTree]
|
, where_ :: [LogicTree]
|
||||||
, returning :: [FieldName]
|
, returning :: [FieldName]
|
||||||
@@ -111,7 +109,7 @@ data MutateQuery
|
|||||||
| Update
|
| Update
|
||||||
{ in_ :: QualifiedIdentifier
|
{ in_ :: QualifiedIdentifier
|
||||||
, updCols :: S.Set FieldName
|
, updCols :: S.Set FieldName
|
||||||
, updBody :: Maybe BL.ByteString
|
, updBody :: Maybe LBS.ByteString
|
||||||
, where_ :: [LogicTree]
|
, where_ :: [LogicTree]
|
||||||
, returning :: [FieldName]
|
, returning :: [FieldName]
|
||||||
}
|
}
|
||||||
@@ -121,17 +119,38 @@ data MutateQuery
|
|||||||
, returning :: [FieldName]
|
, returning :: [FieldName]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data CallQuery = FunctionCall
|
||||||
|
{ funCQi :: QualifiedIdentifier
|
||||||
|
, funCParams :: CallParams
|
||||||
|
, funCArgs :: Maybe LBS.ByteString
|
||||||
|
, funCScalar :: Bool
|
||||||
|
, funCMultipleCall :: Bool
|
||||||
|
, funCReturning :: [FieldName]
|
||||||
|
}
|
||||||
|
|
||||||
|
data CallParams
|
||||||
|
= KeyParams [ProcParam] -- ^ Call with key params: func(a := val1, b:= val2)
|
||||||
|
| OnePosParam ProcParam -- ^ Call with positional params(only one supported): func(val)
|
||||||
|
|
||||||
-- | The select value in `/tbl?select=alias:field::cast`
|
-- | The select value in `/tbl?select=alias:field::cast`
|
||||||
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe EmbedHint)
|
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe Hint, Maybe JoinType)
|
||||||
|
|
||||||
type Field = (FieldName, JsonPath)
|
type Field = (FieldName, JsonPath)
|
||||||
type Cast = Text
|
type Cast = Text
|
||||||
type Alias = Text
|
type Alias = Text
|
||||||
|
type Hint = Text
|
||||||
|
|
||||||
-- | Disambiguates an embedding operation when there's multiple relationships
|
data EmbedParam
|
||||||
-- between two tables. Can be the name of a foreign key constraint, column
|
-- | Disambiguates an embedding operation when there's multiple relationships
|
||||||
-- name or the junction in an m2m relationship.
|
-- between two tables. Can be the name of a foreign key constraint, column
|
||||||
type EmbedHint = Text
|
-- name or the junction in an m2m relationship.
|
||||||
|
= EPHint Hint
|
||||||
|
| EPJoinType JoinType
|
||||||
|
|
||||||
|
data JoinType
|
||||||
|
= JTInner
|
||||||
|
| JTLeft
|
||||||
|
deriving Eq
|
||||||
|
|
||||||
-- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path
|
-- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path
|
||||||
-- ["clients", "projects"]
|
-- ["clients", "projects"]
|
||||||
@@ -158,7 +177,7 @@ data JsonOperand
|
|||||||
-- First level FieldNames(e.g get a,b from /table?select=a,b,other(c,d))
|
-- First level FieldNames(e.g get a,b from /table?select=a,b,other(c,d))
|
||||||
fstFieldNames :: ReadRequest -> [FieldName]
|
fstFieldNames :: ReadRequest -> [FieldName]
|
||||||
fstFieldNames (Node (sel, _) _) =
|
fstFieldNames (Node (sel, _) _) =
|
||||||
fst . (\(f, _, _, _) -> f) <$> select sel
|
fst . (\(f, _, _, _, _) -> f) <$> select sel
|
||||||
|
|
||||||
|
|
||||||
-- | Boolean logic expression tree e.g. "and(name.eq.N,or(id.eq.1,id.eq.2))" is:
|
-- | Boolean logic expression tree e.g. "and(name.eq.N,or(id.eq.1,id.eq.2))" is:
|
||||||
@@ -178,10 +197,6 @@ data LogicOperator
|
|||||||
| Or
|
| Or
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
instance Show LogicOperator where
|
|
||||||
show And = "AND"
|
|
||||||
show Or = "OR"
|
|
||||||
|
|
||||||
data Filter = Filter
|
data Filter = Filter
|
||||||
{ field :: Field
|
{ field :: Field
|
||||||
, opExpr :: OpExpr
|
, opExpr :: OpExpr
|
||||||
@@ -195,6 +210,7 @@ data OpExpr =
|
|||||||
data Operation
|
data Operation
|
||||||
= Op Operator SingleVal
|
= Op Operator SingleVal
|
||||||
| In ListVal
|
| In ListVal
|
||||||
|
| Is TrileanVal
|
||||||
| Fts Operator (Maybe Language) SingleVal
|
| Fts Operator (Maybe Language) SingleVal
|
||||||
deriving (Eq)
|
deriving (Eq)
|
||||||
|
|
||||||
@@ -206,3 +222,11 @@ type SingleVal = Text
|
|||||||
|
|
||||||
-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)
|
-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)
|
||||||
type ListVal = [Text]
|
type ListVal = [Text]
|
||||||
|
|
||||||
|
-- | Three-valued logic values
|
||||||
|
data TrileanVal
|
||||||
|
= TriTrue
|
||||||
|
| TriFalse
|
||||||
|
| TriNull
|
||||||
|
| TriUnknown
|
||||||
|
deriving Eq
|
||||||
|
|||||||
+24
-11
@@ -1,29 +1,42 @@
|
|||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
|
|
||||||
module PostgREST.Version
|
module PostgREST.Version
|
||||||
( docsVersion
|
( docsVersion
|
||||||
, prettyVersion
|
, prettyVersion
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Text as T
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
import Data.Version (versionBranch)
|
import Data.Version (showVersion, versionBranch)
|
||||||
import Development.GitRev (gitHash)
|
import Development.GitRev (gitHash)
|
||||||
import Paths_postgrest (version)
|
import Paths_postgrest (version)
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
-- | User friendly version number
|
-- | User friendly version number such as '1.1.1'.
|
||||||
prettyVersion :: Text
|
-- Pre-release versions are tagged as such, e.g., '1.1.1.1 (pre-release)'.
|
||||||
|
-- If a git hash is available, it's added to the version, e.g., '1.1.1 (abcdef0)'.
|
||||||
|
prettyVersion :: ByteString
|
||||||
prettyVersion =
|
prettyVersion =
|
||||||
T.intercalate "." (map show $ versionBranch version) <> gitRev
|
toUtf8 (showVersion version) <> preRelease <> gitRev
|
||||||
where
|
where
|
||||||
gitRev =
|
gitRev =
|
||||||
if $(gitHash) == "UNKNOWN"
|
if $(gitHash) == ("UNKNOWN" :: Text) then
|
||||||
then mempty
|
mempty
|
||||||
else " (" <> T.take 7 $(gitHash) <> ")"
|
else
|
||||||
|
" (" <> BS.take 7 $(gitHash) <> ")"
|
||||||
|
preRelease = if isPreRelease then " (pre-release)" else mempty
|
||||||
|
|
||||||
-- | Version number used in docs
|
|
||||||
|
-- | Version number used in docs.
|
||||||
|
-- Uses only the two first components of the version. Example: 'v1.1'
|
||||||
docsVersion :: Text
|
docsVersion :: Text
|
||||||
docsVersion = "v" <> T.dropEnd 1 (T.dropWhileEnd (/= '.') prettyVersion)
|
docsVersion =
|
||||||
|
"v" <> (T.intercalate "." . map show . take 2 $ versionBranch version)
|
||||||
|
|
||||||
|
|
||||||
|
-- | Versions with four components (e.g., '1.1.1.1') are treated as pre-releases.
|
||||||
|
isPreRelease :: Bool
|
||||||
|
isPreRelease =
|
||||||
|
length (versionBranch version) == 4
|
||||||
|
|||||||
+24
-21
@@ -9,13 +9,15 @@ module PostgREST.Workers
|
|||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
import qualified Hasql.Connection as C
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Hasql.Notifications as N
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Hasql.Pool as P
|
import qualified Hasql.Notifications as SQL
|
||||||
import qualified Hasql.Transaction.Sessions as HT
|
import qualified Hasql.Pool as SQL
|
||||||
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
|
|
||||||
import Control.Retry (RetryStatus, capDelay, exponentialBackoff,
|
import Control.Retry (RetryStatus, capDelay, exponentialBackoff,
|
||||||
retrying, rsPreviousDelay)
|
retrying, rsPreviousDelay)
|
||||||
|
import Hasql.Connection (acquire)
|
||||||
|
|
||||||
import PostgREST.AppState (AppState)
|
import PostgREST.AppState (AppState)
|
||||||
import PostgREST.Config (AppConfig (..), readAppConfig)
|
import PostgREST.Config (AppConfig (..), readAppConfig)
|
||||||
@@ -27,8 +29,7 @@ import PostgREST.Error (PgError (PgError), checkIsFatal,
|
|||||||
|
|
||||||
import qualified PostgREST.AppState as AppState
|
import qualified PostgREST.AppState as AppState
|
||||||
|
|
||||||
import Protolude hiding (head, toS)
|
import Protolude
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
|
|
||||||
-- | Current database connection status data ConnectionStatus
|
-- | Current database connection status data ConnectionStatus
|
||||||
@@ -108,7 +109,7 @@ connectionWorker appState = do
|
|||||||
connectionStatus :: AppState -> IO ConnectionStatus
|
connectionStatus :: AppState -> IO ConnectionStatus
|
||||||
connectionStatus appState =
|
connectionStatus appState =
|
||||||
retrying retrySettings shouldRetry $
|
retrying retrySettings shouldRetry $
|
||||||
const $ P.release pool >> getConnectionStatus
|
const $ SQL.release pool >> getConnectionStatus
|
||||||
where
|
where
|
||||||
pool = AppState.getPool appState
|
pool = AppState.getPool appState
|
||||||
retrySettings = capDelay delayMicroseconds $ exponentialBackoff backoffMicroseconds
|
retrySettings = capDelay delayMicroseconds $ exponentialBackoff backoffMicroseconds
|
||||||
@@ -117,11 +118,11 @@ connectionStatus appState =
|
|||||||
|
|
||||||
getConnectionStatus :: IO ConnectionStatus
|
getConnectionStatus :: IO ConnectionStatus
|
||||||
getConnectionStatus = do
|
getConnectionStatus = do
|
||||||
pgVersion <- P.use pool queryPgVersion
|
pgVersion <- SQL.use pool queryPgVersion
|
||||||
case pgVersion of
|
case pgVersion of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
let err = PgError False e
|
let err = PgError False e
|
||||||
AppState.logWithZTime appState . toS $ errorPayload err
|
AppState.logWithZTime appState . T.decodeUtf8 . LBS.toStrict $ errorPayload err
|
||||||
case checkIsFatal err of
|
case checkIsFatal err of
|
||||||
Just reason ->
|
Just reason ->
|
||||||
return $ FatalConnectionError reason
|
return $ FatalConnectionError reason
|
||||||
@@ -144,21 +145,23 @@ connectionStatus appState =
|
|||||||
"Attempting to reconnect to the database in "
|
"Attempting to reconnect to the database in "
|
||||||
<> (show delay::Text)
|
<> (show delay::Text)
|
||||||
<> " seconds..."
|
<> " seconds..."
|
||||||
|
when itShould $ AppState.putRetryNextIn appState delay
|
||||||
return itShould
|
return itShould
|
||||||
|
|
||||||
-- | Load the DbStructure by using a connection from the pool.
|
-- | Load the DbStructure by using a connection from the pool.
|
||||||
loadSchemaCache :: AppState -> IO SCacheStatus
|
loadSchemaCache :: AppState -> IO SCacheStatus
|
||||||
loadSchemaCache appState = do
|
loadSchemaCache appState = do
|
||||||
AppConfig{..} <- AppState.getConfig appState
|
AppConfig{..} <- AppState.getConfig appState
|
||||||
|
actualPgVersion <- AppState.getPgVersion appState
|
||||||
result <-
|
result <-
|
||||||
let transaction = if configDbPreparedStatements then HT.transaction else HT.unpreparedTransaction in
|
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
|
||||||
P.use (AppState.getPool appState) . transaction HT.ReadCommitted HT.Read $
|
SQL.use (AppState.getPool appState) . transaction SQL.ReadCommitted SQL.Read $
|
||||||
queryDbStructure (toList configDbSchemas) configDbExtraSearchPath configDbPreparedStatements
|
queryDbStructure (toList configDbSchemas) configDbExtraSearchPath actualPgVersion configDbPreparedStatements
|
||||||
case result of
|
case result of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
let
|
let
|
||||||
err = PgError False e
|
err = PgError False e
|
||||||
putErr = AppState.logWithZTime appState . toS $ errorPayload err
|
putErr = AppState.logWithZTime appState . T.decodeUtf8 . LBS.toStrict $ errorPayload err
|
||||||
case checkIsFatal err of
|
case checkIsFatal err of
|
||||||
Just hint -> do
|
Just hint -> do
|
||||||
AppState.logWithZTime appState "A fatal error ocurred when loading the schema cache"
|
AppState.logWithZTime appState "A fatal error ocurred when loading the schema cache"
|
||||||
@@ -172,8 +175,8 @@ loadSchemaCache appState = do
|
|||||||
|
|
||||||
Right dbStructure -> do
|
Right dbStructure -> do
|
||||||
AppState.putDbStructure appState dbStructure
|
AppState.putDbStructure appState dbStructure
|
||||||
when (isJust configDbRootSpec) $
|
when (isJust configDbRootSpec) .
|
||||||
AppState.putJsonDbS appState $ toS $ JSON.encode dbStructure
|
AppState.putJsonDbS appState . LBS.toStrict $ JSON.encode dbStructure
|
||||||
AppState.logWithZTime appState "Schema cache loaded"
|
AppState.logWithZTime appState "Schema cache loaded"
|
||||||
return SCLoaded
|
return SCLoaded
|
||||||
|
|
||||||
@@ -193,12 +196,12 @@ listener appState = do
|
|||||||
|
|
||||||
-- forkFinally allows to detect if the thread dies
|
-- forkFinally allows to detect if the thread dies
|
||||||
void . flip forkFinally (handleFinally dbChannel) $ do
|
void . flip forkFinally (handleFinally dbChannel) $ do
|
||||||
dbOrError <- C.acquire $ toS configDbUri
|
dbOrError <- acquire $ toUtf8 configDbUri
|
||||||
case dbOrError of
|
case dbOrError of
|
||||||
Right db -> do
|
Right db -> do
|
||||||
AppState.logWithZTime appState $ "Listening for notifications on the " <> dbChannel <> " channel"
|
AppState.logWithZTime appState $ "Listening for notifications on the " <> dbChannel <> " channel"
|
||||||
N.listen db $ N.toPgIdentifier dbChannel
|
SQL.listen db $ SQL.toPgIdentifier dbChannel
|
||||||
N.waitForNotifications handleNotification db
|
SQL.waitForNotifications handleNotification db
|
||||||
_ ->
|
_ ->
|
||||||
die $ "Could not listen for notifications on the " <> dbChannel <> " channel"
|
die $ "Could not listen for notifications on the " <> dbChannel <> " channel"
|
||||||
where
|
where
|
||||||
@@ -233,7 +236,7 @@ reReadConfig startingUp appState = do
|
|||||||
Left e -> do
|
Left e -> do
|
||||||
let
|
let
|
||||||
err = PgError False e
|
err = PgError False e
|
||||||
putErr = AppState.logWithZTime appState . toS $ errorPayload err
|
putErr = AppState.logWithZTime appState . T.decodeUtf8 . LBS.toStrict $ errorPayload err
|
||||||
AppState.logWithZTime appState
|
AppState.logWithZTime appState
|
||||||
"An error ocurred when trying to query database settings for the config parameters"
|
"An error ocurred when trying to query database settings for the config parameters"
|
||||||
case checkIsFatal err of
|
case checkIsFatal err of
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
resolver: lts-18.2 # 2021-07-10, GHC 8.10.4
|
resolver: lts-18.14 # 2021-10-24, GHC 8.10.7
|
||||||
|
|
||||||
nix:
|
nix:
|
||||||
packages:
|
packages:
|
||||||
|
|||||||
+4
-4
@@ -27,7 +27,7 @@ packages:
|
|||||||
hackage: ptr-0.16.8.1@sha256:525219ec5f5da5c699725f7efcef91b00a7d44120fc019878b85c09440bf51d6,2686
|
hackage: ptr-0.16.8.1@sha256:525219ec5f5da5c699725f7efcef91b00a7d44120fc019878b85c09440bf51d6,2686
|
||||||
snapshots:
|
snapshots:
|
||||||
- completed:
|
- completed:
|
||||||
size: 585392
|
size: 586069
|
||||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/2.yaml
|
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/14.yaml
|
||||||
sha256: 7abb45c0cc5eb349448b66d8753655542d45d387ad26970419282eab3d860724
|
sha256: 87842ecbaa8ca9cee59a7e6be52369dbed82ed075cb4e0d152614a627e8fd488
|
||||||
original: lts-18.2
|
original: lts-18.14
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 324 B |
@@ -19,20 +19,18 @@ spec =
|
|||||||
{
|
{
|
||||||
"details": [
|
"details": [
|
||||||
{
|
{
|
||||||
"cardinality": "m2o",
|
"cardinality": "many-to-one",
|
||||||
"relationship": "message_sender_fkey[sender][id]",
|
"relationship": "message_sender_fkey[sender][id]",
|
||||||
"origin": "test.message",
|
"embedding": "message with person"
|
||||||
"target": "test.person"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "m2o",
|
"cardinality": "many-to-one",
|
||||||
"relationship": "message_sender_fkey[sender][id]",
|
"relationship": "message_sender_fkey[sender][id]",
|
||||||
"origin": "test.message",
|
"embedding": "message with person_detail"
|
||||||
"target": "test.person_detail"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hint": "By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)",
|
"hint": "Try changing 'sender' to one of the following: 'person!message_sender_fkey', 'person_detail!message_sender_fkey'. Find the desired relationship in the 'details' key.",
|
||||||
"message": "More than one relationship was found for message and sender"
|
"message": "Could not embed because more than one relationship was found for 'message' and 'sender'"
|
||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
{ matchStatus = 300
|
{ matchStatus = 300
|
||||||
@@ -45,26 +43,23 @@ spec =
|
|||||||
{
|
{
|
||||||
"details": [
|
"details": [
|
||||||
{
|
{
|
||||||
"cardinality": "m2o",
|
"cardinality": "many-to-one",
|
||||||
"relationship": "main_project[main_project_id][big_project_id]",
|
"relationship": "main_project[main_project_id][big_project_id]",
|
||||||
"origin": "test.sites",
|
"embedding": "sites with big_projects"
|
||||||
"target": "test.big_projects"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "m2m",
|
"cardinality": "many-to-many",
|
||||||
"relationship": "test.jobs[jobs_site_id_fkey][jobs_big_project_id_fkey]",
|
"relationship": "test.jobs[jobs_site_id_fkey][jobs_big_project_id_fkey]",
|
||||||
"origin": "test.sites",
|
"embedding": "sites with big_projects"
|
||||||
"target": "test.big_projects"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "m2m",
|
"cardinality": "many-to-many",
|
||||||
"relationship": "test.main_jobs[jobs_site_id_fkey][jobs_big_project_id_fkey]",
|
"relationship": "test.main_jobs[jobs_site_id_fkey][jobs_big_project_id_fkey]",
|
||||||
"origin": "test.sites",
|
"embedding": "sites with big_projects"
|
||||||
"target": "test.big_projects"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hint": "By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)",
|
"hint": "Try changing 'big_projects' to one of the following: 'big_projects!main_project', 'big_projects!jobs', 'big_projects!main_jobs'. Find the desired relationship in the 'details' key.",
|
||||||
"message": "More than one relationship was found for sites and big_projects"
|
"message": "Could not embed because more than one relationship was found for 'sites' and 'big_projects'"
|
||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
{ matchStatus = 300
|
{ matchStatus = 300
|
||||||
@@ -77,20 +72,18 @@ spec =
|
|||||||
{
|
{
|
||||||
"details": [
|
"details": [
|
||||||
{
|
{
|
||||||
"cardinality": "m2o",
|
"cardinality": "many-to-one",
|
||||||
"relationship": "agents_department_id_fkey[department_id][id]",
|
"relationship": "agents_department_id_fkey[department_id][id]",
|
||||||
"origin": "test.agents",
|
"embedding": "agents with departments"
|
||||||
"target": "test.departments"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "o2m",
|
"cardinality": "one-to-many",
|
||||||
"relationship": "departments_head_id_fkey[id][head_id]",
|
"relationship": "departments_head_id_fkey[id][head_id]",
|
||||||
"origin": "test.agents",
|
"embedding": "agents with departments"
|
||||||
"target": "test.departments"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hint": "By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)",
|
"hint": "Try changing 'departments' to one of the following: 'departments!agents_department_id_fkey', 'departments!departments_head_id_fkey'. Find the desired relationship in the 'details' key.",
|
||||||
"message": "More than one relationship was found for agents and departments"
|
"message": "Could not embed because more than one relationship was found for 'agents' and 'departments'"
|
||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
{ matchStatus = 300
|
{ matchStatus = 300
|
||||||
@@ -106,32 +99,28 @@ spec =
|
|||||||
{
|
{
|
||||||
"details": [
|
"details": [
|
||||||
{
|
{
|
||||||
"cardinality": "m2m",
|
"cardinality": "many-to-many",
|
||||||
"relationship": "test.whatev_jobs[whatev_jobs_site_id_1_fkey][whatev_jobs_project_id_1_fkey]",
|
"relationship": "test.whatev_jobs[whatev_jobs_site_id_1_fkey][whatev_jobs_project_id_1_fkey]",
|
||||||
"origin": "test.whatev_sites",
|
"embedding": "whatev_sites with whatev_projects"
|
||||||
"target": "test.whatev_projects"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "m2m",
|
"cardinality": "many-to-many",
|
||||||
"relationship": "test.whatev_jobs[whatev_jobs_site_id_1_fkey][whatev_jobs_project_id_2_fkey]",
|
"relationship": "test.whatev_jobs[whatev_jobs_site_id_1_fkey][whatev_jobs_project_id_2_fkey]",
|
||||||
"origin": "test.whatev_sites",
|
"embedding": "whatev_sites with whatev_projects"
|
||||||
"target": "test.whatev_projects"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "m2m",
|
"cardinality": "many-to-many",
|
||||||
"relationship": "test.whatev_jobs[whatev_jobs_site_id_2_fkey][whatev_jobs_project_id_1_fkey]",
|
"relationship": "test.whatev_jobs[whatev_jobs_site_id_2_fkey][whatev_jobs_project_id_1_fkey]",
|
||||||
"origin": "test.whatev_sites",
|
"embedding": "whatev_sites with whatev_projects"
|
||||||
"target": "test.whatev_projects"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"cardinality": "m2m",
|
"cardinality": "many-to-many",
|
||||||
"relationship": "test.whatev_jobs[whatev_jobs_site_id_2_fkey][whatev_jobs_project_id_2_fkey]",
|
"relationship": "test.whatev_jobs[whatev_jobs_site_id_2_fkey][whatev_jobs_project_id_2_fkey]",
|
||||||
"origin": "test.whatev_sites",
|
"embedding": "whatev_sites with whatev_projects"
|
||||||
"target": "test.whatev_projects"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hint": "By following the 'details' key, disambiguate the request by changing the url to /origin?select=relationship(*) or /origin?select=target!relationship(*)",
|
"hint": "Try changing 'whatev_projects' to one of the following: 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs', 'whatev_projects!whatev_jobs'. Find the desired relationship in the 'details' key.",
|
||||||
"message": "More than one relationship was found for whatev_sites and whatev_projects"
|
"message": "Could not embed because more than one relationship was found for 'whatev_sites' and 'whatev_projects'"
|
||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
{ matchStatus = 300
|
{ matchStatus = 300
|
||||||
@@ -423,16 +412,6 @@ spec =
|
|||||||
"refereeds":[]}]
|
"refereeds":[]}]
|
||||||
}]|] { matchHeaders = [matchContentTypeJson] }
|
}]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
-- TODO Remove in next major version
|
|
||||||
describe "old dot '.' symbol, deprecated" $
|
|
||||||
it "still works" $ do
|
|
||||||
get "/clients?id=eq.1&select=id,projects:projects.client_id(id,tasks(id))" `shouldRespondWith`
|
|
||||||
[json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
get "/tasks?select=id,users:users.users_tasks(id)" `shouldRespondWith`
|
|
||||||
[json|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
context "m2m embed when there's a junction in an internal schema" $ do
|
context "m2m embed when there's a junction in an internal schema" $ do
|
||||||
-- https://github.com/PostgREST/postgrest/issues/1736
|
-- https://github.com/PostgREST/postgrest/issues/1736
|
||||||
it "works with no ambiguity when there's an exposed view of the junction" $ do
|
it "works with no ambiguity when there's an exposed view of the junction" $ do
|
||||||
|
|||||||
@@ -0,0 +1,356 @@
|
|||||||
|
module Feature.EmbedInnerJoinSpec where
|
||||||
|
|
||||||
|
import Network.HTTP.Types
|
||||||
|
import Network.Wai (Application)
|
||||||
|
|
||||||
|
import Test.Hspec
|
||||||
|
import Test.Hspec.Wai
|
||||||
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
|
import Protolude hiding (get)
|
||||||
|
import SpecHelper
|
||||||
|
|
||||||
|
spec :: SpecWith ((), Application)
|
||||||
|
spec =
|
||||||
|
describe "Embedding with an inner join" $ do
|
||||||
|
context "many-to-one relationships" $ do
|
||||||
|
it "ignores null embeddings while the default left join doesn't" $ do
|
||||||
|
get "/projects?select=id,clients!inner(id)" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"clients":{"id":1}}, {"id":2,"clients":{"id":1}},
|
||||||
|
{"id":3,"clients":{"id":2}}, {"id":4,"clients":{"id":2}}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/projects?select=id,clients!left(id)" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"clients":{"id":1}}, {"id":2,"clients":{"id":1}},
|
||||||
|
{"id":3,"clients":{"id":2}}, {"id":4,"clients":{"id":2}},
|
||||||
|
{"id":5,"clients":null}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/projects?select=id,clients!inner(id)" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-3/4" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "filters source tables when the embedded table is filtered" $ do
|
||||||
|
get "/projects?select=id,clients!inner(id)&clients.id=eq.1" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"clients":{"id":1}},
|
||||||
|
{"id":2,"clients":{"id":1}}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/projects?select=id,clients!inner(id)&clients.id=eq.2" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":3,"clients":{"id":2}},
|
||||||
|
{"id":4,"clients":{"id":2}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/projects?select=id,clients!inner(id)&clients.id=eq.0" `shouldRespondWith`
|
||||||
|
[json|[]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/projects?select=id,clients!inner(id)&clients.id=eq.1" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-1/2" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "filters source tables when a two levels below embedded table is filtered" $ do
|
||||||
|
get "/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.1" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"projects":{"id":1,"clients":{"id":1}}},
|
||||||
|
{"id":2,"projects":{"id":1,"clients":{"id":1}}},
|
||||||
|
{"id":3,"projects":{"id":2,"clients":{"id":1}}},
|
||||||
|
{"id":4,"projects":{"id":2,"clients":{"id":1}}}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.2" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":5,"projects":{"id":3,"clients":{"id":2}}},
|
||||||
|
{"id":6,"projects":{"id":3,"clients":{"id":2}}},
|
||||||
|
{"id":7,"projects":{"id":4,"clients":{"id":2}}},
|
||||||
|
{"id":8,"projects":{"id":4,"clients":{"id":2}}}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/tasks?select=id,projects!inner(id,clients!inner(id))&projects.clients.id=eq.1" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-3/4" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "only affects the source table rows if his direct embedding is an inner join" $ do
|
||||||
|
get "/tasks?select=id,projects(id,clients!inner(id))&projects.clients.id=eq.2" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"projects":null},
|
||||||
|
{"id":2,"projects":null},
|
||||||
|
{"id":3,"projects":null},
|
||||||
|
{"id":4,"projects":null},
|
||||||
|
{"id":5,"projects":{"id":3,"clients":{"id":2}}},
|
||||||
|
{"id":6,"projects":{"id":3,"clients":{"id":2}}},
|
||||||
|
{"id":7,"projects":{"id":4,"clients":{"id":2}}},
|
||||||
|
{"id":8,"projects":{"id":4,"clients":{"id":2}}}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/tasks?select=id,projects(id,clients!inner(id))&projects.clients.id=eq.2" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-7/8" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with views" $ do
|
||||||
|
get "/books?select=title,authors!inner(name)&authors.name=eq.George%20Orwell" `shouldRespondWith`
|
||||||
|
[json| [{"title":"1984","authors":{"name":"George Orwell"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/books?select=title,authors!inner(name)&authors.name=eq.George%20Orwell" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-0/1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
context "one-to-many relationships" $ do
|
||||||
|
it "ignores empty array embeddings while the default left join doesn't" $ do
|
||||||
|
get "/entities?select=id,child_entities!inner(id)" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"child_entities":[{"id":1}, {"id":2}, {"id":4}, {"id":5}]},
|
||||||
|
{"id":2,"child_entities":[{"id":3}, {"id":6}]}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/entities?select=id,child_entities!left(id)" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"id":1,"child_entities":[{"id":1}, {"id":2}, {"id":4}, {"id":5}]},
|
||||||
|
{"id":2,"child_entities":[{"id":3}, {"id":6}]},
|
||||||
|
{"id":3,"child_entities":[]},
|
||||||
|
{"id":4,"child_entities":[]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/entities?select=id,child_entities!inner(id)" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-1/2" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "filters source tables when the embedded table is filtered" $ do
|
||||||
|
get "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.1" `shouldRespondWith`
|
||||||
|
[json|[{"id":1,"child_entities":[{"id":1}]}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.3" `shouldRespondWith`
|
||||||
|
[json|[{"id":2,"child_entities":[{"id":3}]}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.0" `shouldRespondWith`
|
||||||
|
[json|[]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/entities?select=id,child_entities!inner(id)&child_entities.id=eq.1" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-0/1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "filters source tables when a two levels below embedded table is filtered" $ do
|
||||||
|
get "/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=in.(1,5)"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"child_entities": [
|
||||||
|
{ "id": 1, "grandchild_entities": [ { "id": 1 } ] },
|
||||||
|
{ "id": 2, "grandchild_entities": [ { "id": 5 } ] }]
|
||||||
|
}
|
||||||
|
]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=eq.2" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"child_entities": [
|
||||||
|
{ "id": 1, "grandchild_entities": [ { "id": 2 } ] } ]
|
||||||
|
}
|
||||||
|
]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/entities?select=id,child_entities!inner(id,grandchild_entities!inner(id))&child_entities.grandchild_entities.id=in.(1,5)" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-0/1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "only affects the source table rows if his direct embedding is an inner join" $ do
|
||||||
|
get "/entities?select=id,child_entities!inner(id,grandchild_entities(id))&child_entities.grandchild_entities.id=eq.2" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"child_entities": [
|
||||||
|
{ "id": 1, "grandchild_entities": [ { "id": 2 } ] },
|
||||||
|
{ "id": 2, "grandchild_entities": [] },
|
||||||
|
{ "id": 4, "grandchild_entities": [] },
|
||||||
|
{ "id": 5, "grandchild_entities": [] } ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"child_entities": [
|
||||||
|
{ "id": 3, "grandchild_entities": [] },
|
||||||
|
{ "id": 6, "grandchild_entities": [] } ]
|
||||||
|
}
|
||||||
|
]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/entities?select=id,child_entities!inner(id,grandchild_entities(id))&child_entities.grandchild_entities.id=eq.2" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-1/2" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with views" $ do
|
||||||
|
get "/authors?select=*,books!inner(*)&books.title=eq.1984" `shouldRespondWith`
|
||||||
|
[json| [{"id":1,"name":"George Orwell","books":[{"id":1,"title":"1984","publication_year":1949,"author_id":1}]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/authors?select=*,books!inner(*)&books.title=eq.1984" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-0/1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
context "many-to-many relationships" $ do
|
||||||
|
it "ignores empty array embeddings while the default left join doesn't" $ do
|
||||||
|
get "/products?select=id,suppliers!inner(id)" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"id":1,"suppliers":[{"id":1}, {"id":2}]},
|
||||||
|
{"id":2,"suppliers":[{"id":1}, {"id":3}]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/products?select=id,suppliers!left(id)" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"id":1,"suppliers":[{"id":1}, {"id":2}]},
|
||||||
|
{"id":2,"suppliers":[{"id":1}, {"id":3}]},
|
||||||
|
{"id":3,"suppliers":[]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/products?select=id,suppliers!inner(id)" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-1/2" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "filters source tables when the embedded table is filtered" $ do
|
||||||
|
get "/products?select=id,suppliers!inner(id)&suppliers.id=eq.2" `shouldRespondWith`
|
||||||
|
[json| [{"id":1,"suppliers":[{"id":2}]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/products?select=id,suppliers!inner(id)&suppliers.id=eq.3" `shouldRespondWith`
|
||||||
|
[json| [{"id":2,"suppliers":[{"id":3}]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/products?select=id,suppliers!inner(id)&suppliers.id=eq.0" `shouldRespondWith`
|
||||||
|
[json| [] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/products?select=id,suppliers!inner(id)&suppliers.id=eq.2" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-0/1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "filters source tables when a two levels below embedded table is filtered" $ do
|
||||||
|
get "/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.3"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"id":1,"suppliers":[{"id":2,"trade_unions":[{"id":3}]}]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.4"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"id":1,"suppliers":[{"id":2,"trade_unions":[{"id":4}]}]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/products?select=id,suppliers!inner(id,trade_unions!inner(id))&suppliers.trade_unions.id=eq.3" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-0/1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "only affects the source table rows if his direct embedding is an inner join" $ do
|
||||||
|
get "/products?select=id,suppliers!inner(id,trade_unions(id))&suppliers.trade_unions.id=eq.3" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"suppliers":[{"id":1,"trade_unions":[]}, {"id":2,"trade_unions":[{"id":3}]}]},
|
||||||
|
{"id":2,"suppliers":[{"id":1,"trade_unions":[]}, {"id":3,"trade_unions":[]}]}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/products?select=id,suppliers!inner(id,trade_unions(id))&suppliers.trade_unions.id=eq.3" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-1/2" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with views" $ do
|
||||||
|
get "/actors?select=*,films!inner(*)&films.title=eq.douze%20commandements" `shouldRespondWith`
|
||||||
|
[json| [{"id":1,"name":"john","films":[{"id":12,"title":"douze commandements"}]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/films?select=*,actors!inner(*)&actors.name=eq.john" `shouldRespondWith`
|
||||||
|
[json| [{"id":12,"title":"douze commandements","actors":[{"id":1,"name":"john"}]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/actors?select=*,films!inner(*)&films.title=eq.douze%20commandements" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-0/1" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with m2o and m2m relationships combined" $ do
|
||||||
|
get "/projects?select=name,clients!inner(name),users!inner(name)" `shouldRespondWith`
|
||||||
|
[json| [
|
||||||
|
{"name":"Windows 7","clients":{"name":"Microsoft"},"users":[{"name":"Angela Martin"}, {"name":"Dwight Schrute"}]},
|
||||||
|
{"name":"Windows 10","clients":{"name":"Microsoft"},"users":[{"name":"Angela Martin"}]},
|
||||||
|
{"name":"IOS","clients":{"name":"Apple"},"users":[{"name":"Michael Scott"}, {"name":"Dwight Schrute"}]},
|
||||||
|
{"name":"OSX","clients":{"name":"Apple"},"users":[{"name":"Michael Scott"}]}]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/projects?select=name,clients!inner(name),users!inner(name)" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-3/4" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with rpc" $ do
|
||||||
|
get "/rpc/getallprojects?select=id,clients!inner(id)&clients.id=eq.1" `shouldRespondWith`
|
||||||
|
[json| [{"id":1,"clients":{"id":1}}, {"id":2,"clients":{"id":1}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/rpc/getallprojects?select=id,clients!inner(id)&clients.id=eq.1" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-1/2" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works when using hints" $ do
|
||||||
|
get "/projects?select=id,clients!client!inner(id)&clients.id=eq.2" `shouldRespondWith`
|
||||||
|
[json| [{"id":3,"clients":{"id":2}}, {"id":4,"clients":{"id":2}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/projects?select=id,client!inner(id)&client.id=eq.2" `shouldRespondWith`
|
||||||
|
[json| [{"id":3,"client":{"id":2}}, {"id":4,"client":{"id":2}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/projects?select=id,clients!client!inner(id)&clients.id=eq.2" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-1/2" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with many one-to-many relationships" $ do
|
||||||
|
-- https://github.com/PostgREST/postgrest/issues/1977
|
||||||
|
get "/client?select=id,name,contact!inner(name),clientinfo!inner(other)" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"name":"Walmart","contact":[{"name":"Wally Walton"}, {"name":"Wilma Wellers"}],"clientinfo":[{"other":"123 Main St"}]},
|
||||||
|
{"id":2,"name":"Target", "contact":[{"name":"Tabby Targo"}],"clientinfo":[{"other":"456 South 3rd St"}]},
|
||||||
|
{"id":3,"name":"Big Lots","contact":[{"name":"Bobby Bots"}, {"name":"Bonnie Bits"}, {"name":"Billy Boats"}],"clientinfo":[{"other":"789 Palm Tree Ln"}]}
|
||||||
|
]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/client?select=id,name,contact!inner(name),clientinfo!inner(other)&contact.name=eq.Wally%20Walton" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":1,"name":"Walmart","contact":[{"name":"Wally Walton"}],"clientinfo":[{"other":"123 Main St"}]}
|
||||||
|
]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/client?select=id,name,contact!inner(name),clientinfo!inner(other)&clientinfo.other=eq.456%20South%203rd%20St" `shouldRespondWith`
|
||||||
|
[json|[
|
||||||
|
{"id":2,"name":"Target","clientinfo":[{"other":"456 South 3rd St"}],"contact":[{"name":"Tabby Targo"}]}
|
||||||
|
]|]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
request methodHead "/client?select=id,name,contact!inner(name),clientinfo!inner(other)" [("Prefer", "count=exact")] mempty
|
||||||
|
`shouldRespondWith` ""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-2/3" ]
|
||||||
|
}
|
||||||
@@ -11,8 +11,8 @@ import Test.Hspec.Wai
|
|||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
import Text.Heredoc
|
import Text.Heredoc
|
||||||
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion112,
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion110,
|
||||||
pgVersion130)
|
pgVersion112, pgVersion130)
|
||||||
|
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
@@ -111,7 +111,7 @@ spec actualPgVersion = do
|
|||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "requesting headers only representation" $
|
context "requesting headers only representation" $ do
|
||||||
it "should not throw and return location header when selecting without PK" $
|
it "should not throw and return location header when selecting without PK" $
|
||||||
request methodPost "/projects?select=name,client_id" [("Prefer", "return=headers-only")]
|
request methodPost "/projects?select=name,client_id" [("Prefer", "return=headers-only")]
|
||||||
[json|{"id":11,"name":"New Project","client_id":2}|] `shouldRespondWith` ""
|
[json|{"id":11,"name":"New Project","client_id":2}|] `shouldRespondWith` ""
|
||||||
@@ -120,6 +120,15 @@ spec actualPgVersion = do
|
|||||||
, "Content-Range" <:> "*/*" ]
|
, "Content-Range" <:> "*/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $
|
||||||
|
it "should not throw and return location header for partitioned tables when selecting without PK" $
|
||||||
|
request methodPost "/car_models" [("Prefer", "return=headers-only")]
|
||||||
|
[json|{"name":"Enzo","year":2021}|] `shouldRespondWith` ""
|
||||||
|
{ matchStatus = 201
|
||||||
|
, matchHeaders = [ "Location" <:> "/car_models?name=eq.Enzo&year=eq.2021"
|
||||||
|
, "Content-Range" <:> "*/*" ]
|
||||||
|
}
|
||||||
|
|
||||||
context "requesting no representation" $
|
context "requesting no representation" $
|
||||||
it "should not throw and return no location header when selecting without PK" $
|
it "should not throw and return no location header when selecting without PK" $
|
||||||
request methodPost "/projects?select=name,client_id" []
|
request methodPost "/projects?select=name,client_id" []
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import Test.Hspec.Wai
|
|||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion112,
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion112,
|
||||||
pgVersion121, pgVersion95)
|
pgVersion121)
|
||||||
|
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
@@ -209,46 +209,45 @@ spec actualPgVersion = describe "json and jsonb operators" $ do
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| [{ "data": { "id":" \"escaped" } }] |]
|
[json| [{ "data": { "id":" \"escaped" } }] |]
|
||||||
|
|
||||||
when (actualPgVersion >= pgVersion95) $
|
context "json array negative index" $ do
|
||||||
context "json array negative index" $ do
|
it "can select with negative indexes" $ do
|
||||||
it "can select with negative indexes" $ do
|
get "/json_arr?select=data->>-1::int&id=in.(1,2)" `shouldRespondWith`
|
||||||
get "/json_arr?select=data->>-1::int&id=in.(1,2)" `shouldRespondWith`
|
[json| [{"data":3}, {"data":6}] |]
|
||||||
[json| [{"data":3}, {"data":6}] |]
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
get "/json_arr?select=data->0->>-2::int&id=in.(3,4)" `shouldRespondWith`
|
||||||
get "/json_arr?select=data->0->>-2::int&id=in.(3,4)" `shouldRespondWith`
|
[json| [{"data":8}, {"data":7}] |]
|
||||||
[json| [{"data":8}, {"data":7}] |]
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
get "/json_arr?select=data->-2->>a&id=in.(5,6)" `shouldRespondWith`
|
||||||
get "/json_arr?select=data->-2->>a&id=in.(5,6)" `shouldRespondWith`
|
[json| [{"a":"A"}, {"a":"[1,2,3]"}] |]
|
||||||
[json| [{"a":"A"}, {"a":"[1,2,3]"}] |]
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "can filter with negative indexes" $ do
|
it "can filter with negative indexes" $ do
|
||||||
get "/json_arr?select=data&data->>-3=eq.1" `shouldRespondWith`
|
get "/json_arr?select=data&data->>-3=eq.1" `shouldRespondWith`
|
||||||
[json| [{"data":[1, 2, 3]}] |]
|
[json| [{"data":[1, 2, 3]}] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
get "/json_arr?select=data&data->-1->>-3=eq.11" `shouldRespondWith`
|
get "/json_arr?select=data&data->-1->>-3=eq.11" `shouldRespondWith`
|
||||||
[json| [{"data":[[9, 8, 7], [11, 12, 13]]}] |]
|
[json| [{"data":[[9, 8, 7], [11, 12, 13]]}] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
get "/json_arr?select=data&data->-1->>b=eq.B" `shouldRespondWith`
|
get "/json_arr?select=data&data->-1->>b=eq.B" `shouldRespondWith`
|
||||||
[json| [{"data":[{"a": "A"}, {"b": "B"}]}] |]
|
[json| [{"data":[{"a": "A"}, {"b": "B"}]}] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
get "/json_arr?select=data&data->-1->b->>-1=eq.5" `shouldRespondWith`
|
get "/json_arr?select=data&data->-1->b->>-1=eq.5" `shouldRespondWith`
|
||||||
[json| [{"data":[{"a": [1,2,3]}, {"b": [4,5]}]}] |]
|
[json| [{"data":[{"a": [1,2,3]}, {"b": [4,5]}]}] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "should fail on badly formed negatives" $ do
|
it "should fail on badly formed negatives" $ do
|
||||||
get "/json_arr?select=data->>-78xy" `shouldRespondWith`
|
get "/json_arr?select=data->>-78xy" `shouldRespondWith`
|
||||||
[json|
|
[json|
|
||||||
{"details": "unexpected 'x' expecting digit, \"->\", \"::\" or end of input",
|
{"details": "unexpected 'x' expecting digit, \"->\", \"::\" or end of input",
|
||||||
"message": "\"failed to parse select parameter (data->>-78xy)\" (line 1, column 11)"} |]
|
"message": "\"failed to parse select parameter (data->>-78xy)\" (line 1, column 11)"} |]
|
||||||
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
get "/json_arr?select=data->>--34" `shouldRespondWith`
|
get "/json_arr?select=data->>--34" `shouldRespondWith`
|
||||||
[json|
|
[json|
|
||||||
{"details": "unexpected \"-\" expecting digit",
|
{"details": "unexpected \"-\" expecting digit",
|
||||||
"message": "\"failed to parse select parameter (data->>--34)\" (line 1, column 9)"} |]
|
"message": "\"failed to parse select parameter (data->>--34)\" (line 1, column 9)"} |]
|
||||||
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
get "/json_arr?select=data->>-xy-4" `shouldRespondWith`
|
get "/json_arr?select=data->>-xy-4" `shouldRespondWith`
|
||||||
[json|
|
[json|
|
||||||
{"details":"unexpected \"x\" expecting digit",
|
{"details":"unexpected \"x\" expecting digit",
|
||||||
"message":"\"failed to parse select parameter (data->>-xy-4)\" (line 1, column 9)"} |]
|
"message":"\"failed to parse select parameter (data->>-xy-4)\" (line 1, column 9)"} |]
|
||||||
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
module Feature.LegacyGucsSpec where
|
||||||
|
|
||||||
|
import Network.Wai (Application)
|
||||||
|
|
||||||
|
import Network.HTTP.Types
|
||||||
|
import Test.Hspec hiding (pendingWith)
|
||||||
|
import Test.Hspec.Wai
|
||||||
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
|
import Protolude hiding (get)
|
||||||
|
import SpecHelper
|
||||||
|
|
||||||
|
spec :: SpecWith ((), Application)
|
||||||
|
spec =
|
||||||
|
describe "remote procedure call with legacy gucs disabled" $ do
|
||||||
|
it "custom header is set" $
|
||||||
|
request methodPost "/rpc/get_guc_value" [("Custom-Header", "test")]
|
||||||
|
[json| { "prefix": "request.headers", "name": "custom-header" } |]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|"test"|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "standard header is set" $
|
||||||
|
request methodPost "/rpc/get_guc_value" [("Origin", "http://example.com")]
|
||||||
|
[json| { "prefix": "request.headers", "name": "origin" } |]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|"http://example.com"|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "current role is available as GUC claim" $
|
||||||
|
request methodPost "/rpc/get_guc_value" []
|
||||||
|
[json| { "prefix": "request.jwt.claims", "name": "role" } |]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|"postgrest_test_anonymous"|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "single cookie ends up as claims" $
|
||||||
|
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue")]
|
||||||
|
[json| {"prefix": "request.cookies", "name":"acookie"} |]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|"cookievalue"|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = []
|
||||||
|
}
|
||||||
|
|
||||||
|
it "multiple cookies ends up as claims" $
|
||||||
|
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")]
|
||||||
|
[json| {"prefix": "request.cookies", "name":"secondcookie"} |]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|"anothervalue"|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = []
|
||||||
|
}
|
||||||
|
|
||||||
|
it "gets the Authorization value" $
|
||||||
|
request methodPost "/rpc/get_guc_value" [authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"]
|
||||||
|
[json| {"prefix": "request.headers", "name":"authorization"} |]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = []
|
||||||
|
}
|
||||||
@@ -15,10 +15,8 @@ import Test.Hspec.Wai.JSON
|
|||||||
import Protolude
|
import Protolude
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion96)
|
spec :: SpecWith ((), Application)
|
||||||
|
spec =
|
||||||
spec :: PgVersion -> SpecWith ((), Application)
|
|
||||||
spec actualPgVersion =
|
|
||||||
describe "multiple schemas in single instance" $ do
|
describe "multiple schemas in single instance" $ do
|
||||||
context "Reading tables on different schemas" $ do
|
context "Reading tables on different schemas" $ do
|
||||||
it "succeeds in reading table from default schema v1 if no schema is selected via header" $
|
it "succeeds in reading table from default schema v1 if no schema is selected via header" $
|
||||||
@@ -191,16 +189,15 @@ spec actualPgVersion =
|
|||||||
[json|[{"id": 1, "name": "child v2-3", "parent_id": 3}]|]
|
[json|[{"id": 1, "name": "child v2-3", "parent_id": 3}]|]
|
||||||
{ matchHeaders = ["Content-Profile" <:> "v2"] }
|
{ matchHeaders = ["Content-Profile" <:> "v2"] }
|
||||||
|
|
||||||
when (actualPgVersion >= pgVersion96) $
|
it "succeeds on PUT on the v2 schema" $
|
||||||
it "succeeds on PUT on the v2 schema" $
|
request methodPut "/children?id=eq.111" [("Content-Profile", "v2"), ("Prefer", "return=representation")]
|
||||||
request methodPut "/children?id=eq.111" [("Content-Profile", "v2"), ("Prefer", "return=representation")]
|
[json| [ { "id": 111, "name": "child v2-111", "parent_id": null } ]|]
|
||||||
[json| [ { "id": 111, "name": "child v2-111", "parent_id": null } ]|]
|
`shouldRespondWith`
|
||||||
`shouldRespondWith`
|
[json|[{ "id": 111, "name": "child v2-111", "parent_id": null }]|]
|
||||||
[json|[{ "id": 111, "name": "child v2-111", "parent_id": null }]|]
|
{
|
||||||
{
|
matchStatus = 200
|
||||||
matchStatus = 200
|
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
|
||||||
, matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]
|
}
|
||||||
}
|
|
||||||
|
|
||||||
context "OpenAPI output" $ do
|
context "OpenAPI output" $ do
|
||||||
it "succeeds in reading table definition from default schema v1 if no schema is selected via header" $ do
|
it "succeeds in reading table definition from default schema v1 if no schema is selected via header" $ do
|
||||||
|
|||||||
@@ -11,12 +11,15 @@ import Network.HTTP.Types
|
|||||||
import Test.Hspec hiding (pendingWith)
|
import Test.Hspec hiding (pendingWith)
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
|
|
||||||
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
||||||
|
pgVersion110)
|
||||||
|
|
||||||
import PostgREST.Version (docsVersion)
|
import PostgREST.Version (docsVersion)
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
spec :: SpecWith ((), Application)
|
spec :: PgVersion -> SpecWith ((), Application)
|
||||||
spec = describe "OpenAPI" $ do
|
spec actualPgVersion = describe "OpenAPI" $ do
|
||||||
it "root path returns a valid openapi spec" $ do
|
it "root path returns a valid openapi spec" $ do
|
||||||
validateOpenApiResponse [("Accept", "application/openapi+json")]
|
validateOpenApiResponse [("Accept", "application/openapi+json")]
|
||||||
request methodHead "/" (acceptHdrs "application/openapi+json") ""
|
request methodHead "/" (acceptHdrs "application/openapi+json") ""
|
||||||
@@ -195,6 +198,32 @@ spec = describe "OpenAPI" $ do
|
|||||||
]
|
]
|
||||||
|]
|
|]
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion100) $ do
|
||||||
|
describe "Partitioned table" $
|
||||||
|
|
||||||
|
it "includes partitioned table properties" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
|
||||||
|
let method s = key "paths" . key "/car_models" . key s
|
||||||
|
getSummary = r ^? method "get" . key "summary"
|
||||||
|
getDescription = r ^? method "get" . key "description"
|
||||||
|
getParameterName = r ^? method "get" . key "parameters" . nth 0 . key "$ref"
|
||||||
|
getParameterYear = r ^? method "get" . key "parameters" . nth 1 . key "$ref"
|
||||||
|
getParameterRef = r ^? method "get" . key "parameters" . nth 2 . key "$ref"
|
||||||
|
|
||||||
|
liftIO $ do
|
||||||
|
|
||||||
|
getSummary `shouldBe` Just "A partitioned table"
|
||||||
|
|
||||||
|
getDescription `shouldBe` Just "A test for partitioned tables"
|
||||||
|
|
||||||
|
getParameterName `shouldBe` Just "#/parameters/rowFilter.car_models.name"
|
||||||
|
|
||||||
|
getParameterYear `shouldBe` Just "#/parameters/rowFilter.car_models.year"
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $
|
||||||
|
getParameterRef `shouldBe` Just "#/parameters/rowFilter.car_models.car_brand_name"
|
||||||
|
|
||||||
describe "Materialized view" $
|
describe "Materialized view" $
|
||||||
|
|
||||||
it "includes materialized view properties" $ do
|
it "includes materialized view properties" $ do
|
||||||
@@ -396,6 +425,62 @@ spec = describe "OpenAPI" $ do
|
|||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
|
describe "Detects default values" $ do
|
||||||
|
|
||||||
|
it "text" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
|
||||||
|
let defaultValue = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "text" . key "default"
|
||||||
|
|
||||||
|
liftIO $
|
||||||
|
|
||||||
|
defaultValue `shouldBe` Just "default"
|
||||||
|
|
||||||
|
it "boolean" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
|
||||||
|
let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "boolean" . key "default"
|
||||||
|
|
||||||
|
liftIO $
|
||||||
|
|
||||||
|
types `shouldBe` Just (Bool False)
|
||||||
|
|
||||||
|
it "integer" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
|
||||||
|
let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "integer" . key "default"
|
||||||
|
|
||||||
|
liftIO $
|
||||||
|
|
||||||
|
types `shouldBe` Just (Number 42)
|
||||||
|
|
||||||
|
it "numeric" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
|
||||||
|
let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "numeric" . key "default"
|
||||||
|
|
||||||
|
liftIO $
|
||||||
|
|
||||||
|
types `shouldBe` Just (Number 42.2)
|
||||||
|
|
||||||
|
it "date" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
|
||||||
|
let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "date" . key "default"
|
||||||
|
|
||||||
|
liftIO $
|
||||||
|
|
||||||
|
types `shouldBe` Just "1900-01-01"
|
||||||
|
|
||||||
|
it "time" $ do
|
||||||
|
r <- simpleBody <$> get "/"
|
||||||
|
|
||||||
|
let types = r ^? key "definitions" . key "openapi_defaults" . key "properties" . key "time" . key "default"
|
||||||
|
|
||||||
|
liftIO $
|
||||||
|
|
||||||
|
types `shouldBe` Just "13:00:00"
|
||||||
|
|
||||||
describe "RPC" $ do
|
describe "RPC" $ do
|
||||||
|
|
||||||
it "includes function summary/description and body schema for arguments" $ do
|
it "includes function summary/description and body schema for arguments" $ do
|
||||||
|
|||||||
@@ -7,11 +7,14 @@ import Network.HTTP.Types
|
|||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
|
|
||||||
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
||||||
|
pgVersion110)
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
spec :: SpecWith ((), Application)
|
spec :: PgVersion -> SpecWith ((), Application)
|
||||||
spec = describe "Allow header" $ do
|
spec actualPgVersion = describe "Allow header" $ do
|
||||||
context "a table" $ do
|
context "a table" $ do
|
||||||
it "includes read/write verbs for writeable table" $ do
|
it "includes read/write verbs for writeable table" $ do
|
||||||
r <- request methodOptions "/items" [] ""
|
r <- request methodOptions "/items" [] ""
|
||||||
@@ -19,6 +22,19 @@ spec = describe "Allow header" $ do
|
|||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy`
|
||||||
matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
|
matchHeader "Allow" "OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion100) $
|
||||||
|
context "a partitioned table" $ do
|
||||||
|
it "includes read/write verbs for writeable partitioned tables" $ do
|
||||||
|
r <- request methodOptions "/car_models" [] ""
|
||||||
|
liftIO $
|
||||||
|
simpleHeaders r `shouldSatisfy`
|
||||||
|
matchHeader "Allow" (
|
||||||
|
if actualPgVersion >= pgVersion110 then
|
||||||
|
"OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE"
|
||||||
|
else
|
||||||
|
"OPTIONS,GET,HEAD,POST,PATCH,DELETE"
|
||||||
|
)
|
||||||
|
|
||||||
context "a view" $ do
|
context "a view" $ do
|
||||||
context "auto updatable" $ do
|
context "auto updatable" $ do
|
||||||
it "includes read/write verbs for auto updatable views with pk" $ do
|
it "includes read/write verbs for auto updatable views with pk" $ do
|
||||||
|
|||||||
+176
-35
@@ -8,8 +8,8 @@ import Test.Hspec hiding (pendingWith)
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion112,
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion110,
|
||||||
pgVersion121, pgVersion96)
|
pgVersion112, pgVersion121)
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
@@ -72,6 +72,23 @@ spec actualPgVersion = do
|
|||||||
|
|
||||||
get "/nullable_integer?a=is.null" `shouldRespondWith` [json|[{"a":null}]|]
|
get "/nullable_integer?a=is.null" `shouldRespondWith` [json|[{"a":null}]|]
|
||||||
|
|
||||||
|
it "matches with trilean values" $ do
|
||||||
|
get "/chores?done=is.true" `shouldRespondWith`
|
||||||
|
[json| [{"id": 1, "name": "take out the garbage", "done": true }] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
get "/chores?done=is.false" `shouldRespondWith`
|
||||||
|
[json| [{"id": 2, "name": "do the laundry", "done": false }] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
get "/chores?done=is.unknown" `shouldRespondWith`
|
||||||
|
[json| [{"id": 3, "name": "wash the dishes", "done": null }] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "fails if 'is' used and there's no null or trilean value" $ do
|
||||||
|
get "/chores?done=is.nil" `shouldRespondWith` 400
|
||||||
|
get "/chores?done=is.ok" `shouldRespondWith` 400
|
||||||
|
|
||||||
it "matches with like" $ do
|
it "matches with like" $ do
|
||||||
get "/simple_pk?k=like.*yx" `shouldRespondWith`
|
get "/simple_pk?k=like.*yx" `shouldRespondWith`
|
||||||
[json|[{"k":"xyyx","extra":"u"}]|]
|
[json|[{"k":"xyyx","extra":"u"}]|]
|
||||||
@@ -182,34 +199,33 @@ spec actualPgVersion = do
|
|||||||
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]
|
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
when (actualPgVersion >= pgVersion96) $
|
context "Use of the phraseto_tsquery function" $ do
|
||||||
context "Use of the phraseto_tsquery function" $ do
|
it "finds matches" $
|
||||||
it "finds matches" $
|
get "/tsearch?text_search_vector=phfts.The%20Fat%20Cats" `shouldRespondWith`
|
||||||
get "/tsearch?text_search_vector=phfts.The%20Fat%20Cats" `shouldRespondWith`
|
[json| [{"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]
|
||||||
[json| [{"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "finds matches with different dictionaries" $
|
it "finds matches with different dictionaries" $
|
||||||
get "/tsearch?text_search_vector=phfts(german).Art%20Spass" `shouldRespondWith`
|
get "/tsearch?text_search_vector=phfts(german).Art%20Spass" `shouldRespondWith`
|
||||||
[json| [{"text_search_vector": "'art':4 'spass':5 'unmog':7" }] |]
|
[json| [{"text_search_vector": "'art':4 'spass':5 'unmog':7" }] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "can be negated with not operator" $
|
it "can be negated with not operator" $
|
||||||
get "/tsearch?text_search_vector=not.phfts(english).The%20Fat%20Cats" `shouldRespondWith`
|
get "/tsearch?text_search_vector=not.phfts(english).The%20Fat%20Cats" `shouldRespondWith`
|
||||||
[json| [
|
[json| [
|
||||||
{"text_search_vector": "'fun':5 'imposs':9 'kind':3"},
|
{"text_search_vector": "'fun':5 'imposs':9 'kind':3"},
|
||||||
{"text_search_vector": "'also':2 'fun':3 'possibl':8"},
|
{"text_search_vector": "'also':2 'fun':3 'possibl':8"},
|
||||||
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},
|
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},
|
||||||
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]
|
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "can be used with or query param" $
|
it "can be used with or query param" $
|
||||||
get "/tsearch?or=(text_search_vector.phfts(german).Art%20Spass, text_search_vector.phfts(french).amusant, text_search_vector.fts(english).impossible)" `shouldRespondWith`
|
get "/tsearch?or=(text_search_vector.phfts(german).Art%20Spass, text_search_vector.phfts(french).amusant, text_search_vector.fts(english).impossible)" `shouldRespondWith`
|
||||||
[json|[
|
[json|[
|
||||||
{"text_search_vector": "'fun':5 'imposs':9 'kind':3" },
|
{"text_search_vector": "'fun':5 'imposs':9 'kind':3" },
|
||||||
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
|
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
|
||||||
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}
|
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}
|
||||||
]|] { matchHeaders = [matchContentTypeJson] }
|
]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "matches with computed column" $
|
it "matches with computed column" $
|
||||||
get "/items?always_true=eq.true&order=id.asc" `shouldRespondWith`
|
get "/items?always_true=eq.true&order=id.asc" `shouldRespondWith`
|
||||||
@@ -395,6 +411,100 @@ spec actualPgVersion = do
|
|||||||
[json|[{"id":1,"computed_overload":true}]|]
|
[json|[{"id":1,"computed_overload":true}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $ do
|
||||||
|
describe "partitioned tables embedding" $ do
|
||||||
|
it "can request a table as parent from a partitioned table" $
|
||||||
|
get "/car_models?name=in.(DeLorean,Murcielago)&select=name,year,car_brands(name)&order=name.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"name":"DeLorean","year":1981,"car_brands":{"name":"DMC"}},
|
||||||
|
{"name":"Murcielago","year":2001,"car_brands":{"name":"Lamborghini"}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can request partitioned tables as children from a table" $
|
||||||
|
get "/car_brands?select=name,car_models(name,year)&order=name.asc&car_models.order=name.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"name":"DMC","car_models":[{"name":"DeLorean","year":1981}]},
|
||||||
|
{"name":"Ferrari","car_models":[{"name":"F310-B","year":1997}]},
|
||||||
|
{"name":"Lamborghini","car_models":[{"name":"Murcielago","year":2001},{"name":"Veneno","year":2013}]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion121) $ do
|
||||||
|
it "can request tables as children from a partitioned table" $
|
||||||
|
get "/car_models?name=in.(DeLorean,F310-B)&select=name,year,car_racers(name)&order=name.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"name":"DeLorean","year":1981,"car_racers":[]},
|
||||||
|
{"name":"F310-B","year":1997,"car_racers":[{"name":"Michael Schumacher"}]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can request a partitioned table as parent from a table" $
|
||||||
|
get "/car_racers?select=name,car_models(name,year)&order=name.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"name":"Alain Prost","car_models":null},
|
||||||
|
{"name":"Michael Schumacher","car_models":{"name":"F310-B","year":1997}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can request partitioned tables as children from a partitioned table" $
|
||||||
|
get "/car_models?name=in.(DeLorean,Murcielago,Veneno)&select=name,year,car_model_sales(date,quantity)&order=name.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"name":"DeLorean","year":1981,"car_model_sales":[{"date":"2021-01-14","quantity":7},{"date":"2021-01-15","quantity":9}]},
|
||||||
|
{"name":"Murcielago","year":2001,"car_model_sales":[{"date":"2021-02-11","quantity":1},{"date":"2021-02-12","quantity":3}]},
|
||||||
|
{"name":"Veneno","year":2013,"car_model_sales":[]}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can request a partitioned table as parent from a partitioned table" $ do
|
||||||
|
get "/car_model_sales?date=in.(2021-01-15,2021-02-11)&select=date,quantity,car_models(name,year)&order=date.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"date":"2021-01-15","quantity":9,"car_models":{"name":"DeLorean","year":1981}},
|
||||||
|
{"date":"2021-02-11","quantity":1,"car_models":{"name":"Murcielago","year":2001}}] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "can request many to many relationships between partitioned tables ignoring the intermediate table partitions" $
|
||||||
|
get "/car_models?select=name,year,car_dealers(name,city)&order=name.asc&limit=4" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"name":"DeLorean","year":1981,"car_dealers":[{"name":"Springfield Cars S.A.","city":"Springfield"}]},
|
||||||
|
{"name":"F310-B","year":1997,"car_dealers":[]},
|
||||||
|
{"name":"Murcielago","year":2001,"car_dealers":[{"name":"The Best Deals S.A.","city":"Franklin"}]},
|
||||||
|
{"name":"Veneno","year":2013,"car_dealers":[]}] |]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "cannot request partitions as children from a partitioned table" $
|
||||||
|
get "/car_models?id=in.(1,2,4)&select=id,name,car_model_sales_202101(id)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
{"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",
|
||||||
|
"message":"Could not find a relationship between car_models and car_model_sales_202101 in the schema cache"} |]
|
||||||
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "cannot request a partitioned table as parent from a partition" $
|
||||||
|
get "/car_model_sales_202101?select=id,name,car_models(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
{"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",
|
||||||
|
"message":"Could not find a relationship between car_model_sales_202101 and car_models in the schema cache"} |]
|
||||||
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "cannot request a partition as parent from a partitioned table" $
|
||||||
|
get "/car_model_sales?id=in.(1,3,4)&select=id,name,car_models_default(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
{"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",
|
||||||
|
"message":"Could not find a relationship between car_model_sales and car_models_default in the schema cache"} |]
|
||||||
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "cannot request partitioned tables as children from a partition" $
|
||||||
|
get "/car_models_default?select=id,name,car_model_sales(id,name)&order=id.asc" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
{"hint":"If a new foreign key between these entities was created in the database, try reloading the schema cache.",
|
||||||
|
"message":"Could not find a relationship between car_models_default and car_model_sales in the schema cache"} |]
|
||||||
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
describe "view embedding" $ do
|
describe "view embedding" $ do
|
||||||
it "can detect fk relations through views to tables in the public schema" $
|
it "can detect fk relations through views to tables in the public schema" $
|
||||||
get "/consumers_view?select=*,orders_view(*)" `shouldRespondWith` 200
|
get "/consumers_view?select=*,orders_view(*)" `shouldRespondWith` 200
|
||||||
@@ -861,25 +971,56 @@ spec actualPgVersion = do
|
|||||||
get "/w_or_wo_comma_names?name=in.(\"Hebdon, John\",\"Williams, Mary\",\"Smith, Joseph\")" `shouldRespondWith`
|
get "/w_or_wo_comma_names?name=in.(\"Hebdon, John\",\"Williams, Mary\",\"Smith, Joseph\")" `shouldRespondWith`
|
||||||
[json| [{"name":"Hebdon, John"},{"name":"Williams, Mary"},{"name":"Smith, Joseph"}] |]
|
[json| [{"name":"Hebdon, John"},{"name":"Williams, Mary"},{"name":"Smith, Joseph"}] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
get "/w_or_wo_comma_names?name=not.in.(\"Hebdon, John\",\"Williams, Mary\",\"Smith, Joseph\")" `shouldRespondWith`
|
get "/w_or_wo_comma_names?name=not.in.(\"Hebdon, John\",\"Williams, Mary\",\"Smith, Joseph\")&limit=3" `shouldRespondWith`
|
||||||
[json| [{"name":"David White"},{"name":"Larry Thompson"},{"name":"Double O Seven(007)"}] |]
|
[json| [ { "name": "David White" }, { "name": "Larry Thompson" }, { "name": "Double O Seven(007)" }] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "succeeds w/ and w/o quoted values" $ do
|
it "succeeds w/ and w/o quoted values" $ do
|
||||||
get "/w_or_wo_comma_names?name=in.(David White,\"Hebdon, John\")" `shouldRespondWith`
|
get "/w_or_wo_comma_names?name=in.(David White,\"Hebdon, John\")" `shouldRespondWith`
|
||||||
[json| [{"name":"Hebdon, John"},{"name":"David White"}] |]
|
[json| [{"name":"Hebdon, John"},{"name":"David White"}] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
get "/w_or_wo_comma_names?name=not.in.(\"Hebdon, John\",Larry Thompson,\"Smith, Joseph\")" `shouldRespondWith`
|
get "/w_or_wo_comma_names?name=not.in.(\"Hebdon, John\",Larry Thompson,\"Smith, Joseph\")&limit=3" `shouldRespondWith`
|
||||||
[json| [{"name":"Williams, Mary"},{"name":"David White"},{"name":"Double O Seven(007)"}] |]
|
[json| [ { "name": "Williams, Mary" }, { "name": "David White" }, { "name": "Double O Seven(007)" }] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
get "/w_or_wo_comma_names?name=in.(\"Double O Seven(007)\")" `shouldRespondWith`
|
get "/w_or_wo_comma_names?name=in.(\"Double O Seven(007)\")" `shouldRespondWith`
|
||||||
[json| [{"name":"Double O Seven(007)"}] |]
|
[json| [{"name":"Double O Seven(007)"}] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "fails on malformed quoted values" $ do
|
context "escaped chars" $ do
|
||||||
get "/w_or_wo_comma_names?name=in.(\"\"Hebdon, John\")" `shouldRespondWith` 400
|
it "accepts escaped double quotes" $
|
||||||
get "/w_or_wo_comma_names?name=in.(\"\"Hebdon, John\"\"Mary)" `shouldRespondWith` 400
|
get "/w_or_wo_comma_names?name=in.(\"Double\\\"Quote\\\"McGraw\\\"\")" `shouldRespondWith`
|
||||||
get "/w_or_wo_comma_names?name=in.(Williams\"Hebdon, John\")" `shouldRespondWith` 400
|
[json| [ { "name": "Double\"Quote\"McGraw\"" } ] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "accepts escaped backslashes" $ do
|
||||||
|
get "/w_or_wo_comma_names?name=in.(\"\\\\\")" `shouldRespondWith`
|
||||||
|
[json| [{ "name": "\\" }] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/w_or_wo_comma_names?name=in.(\"/\\\\Slash/\\\\Beast/\\\\\")" `shouldRespondWith`
|
||||||
|
[json| [ { "name": "/\\Slash/\\Beast/\\" } ] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "passes any escaped char as the same char" $
|
||||||
|
get "/w_or_wo_comma_names?name=in.(\"D\\a\\vid W\\h\\ite\")" `shouldRespondWith`
|
||||||
|
[json| [{ "name": "David White" }] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
describe "IN values without quotes" $ do
|
||||||
|
it "accepts single double quotes as values" $ do
|
||||||
|
get "/w_or_wo_comma_names?name=in.(\")" `shouldRespondWith`
|
||||||
|
[json| [{ "name": "\"" }] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/w_or_wo_comma_names?name=in.(Double\"Quote\"McGraw\")" `shouldRespondWith`
|
||||||
|
[json| [ { "name": "Double\"Quote\"McGraw\"" } ] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "accepts backslashes as values" $ do
|
||||||
|
get "/w_or_wo_comma_names?name=in.(\\)" `shouldRespondWith`
|
||||||
|
[json| [{ "name": "\\" }] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/w_or_wo_comma_names?name=in.(/\\Slash/\\Beast/\\)" `shouldRespondWith`
|
||||||
|
[json| [ { "name": "/\\Slash/\\Beast/\\" } ] |]
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
describe "IN and NOT IN empty set" $ do
|
describe "IN and NOT IN empty set" $ do
|
||||||
context "returns an empty result for IN when no value is present" $ do
|
context "returns an empty result for IN when no value is present" $ do
|
||||||
|
|||||||
+272
-77
@@ -1,11 +1,12 @@
|
|||||||
module Feature.RpcSpec where
|
module Feature.RpcSpec where
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as BL (empty)
|
import qualified Data.ByteString.Lazy as BL (empty, readFile)
|
||||||
|
|
||||||
import Network.Wai (Application)
|
import Network.Wai (Application)
|
||||||
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
||||||
|
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
import Test.Hspec hiding (pendingWith)
|
import Test.Hspec hiding (pendingWith)
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
@@ -14,7 +15,7 @@ import Text.Heredoc
|
|||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
||||||
pgVersion109, pgVersion110,
|
pgVersion109, pgVersion110,
|
||||||
pgVersion112, pgVersion114,
|
pgVersion112, pgVersion114,
|
||||||
pgVersion96)
|
pgVersion140)
|
||||||
|
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
@@ -107,7 +108,7 @@ spec actualPgVersion =
|
|||||||
it "should not ignore unknown args and fail with 404" $
|
it "should not ignore unknown args and fail with 404" $
|
||||||
get "/rpc/add_them?a=1&b=2&smthelse=blabla" `shouldRespondWith`
|
get "/rpc/add_them?a=1&b=2&smthelse=blabla" `shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"hint":"If a new function was created in the database with this name and arguments, try reloading the schema cache.",
|
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
||||||
"message":"Could not find the test.add_them(a, b, smthelse) function in the schema cache" } |]
|
"message":"Could not find the test.add_them(a, b, smthelse) function in the schema cache" } |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
@@ -119,8 +120,8 @@ spec actualPgVersion =
|
|||||||
[json|{}|]
|
[json|{}|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"hint":"If a new function was created in the database with this name and arguments, try reloading the schema cache.",
|
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
||||||
"message":"Could not find the test.sayhello function with a single json or jsonb argument in the schema cache" } |]
|
"message":"Could not find the test.sayhello function with a single json or jsonb parameter in the schema cache" } |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
@@ -128,25 +129,25 @@ spec actualPgVersion =
|
|||||||
it "should fail with 404 for overloaded functions with unknown args" $ do
|
it "should fail with 404 for overloaded functions with unknown args" $ do
|
||||||
get "/rpc/overloaded?wrong_arg=value" `shouldRespondWith`
|
get "/rpc/overloaded?wrong_arg=value" `shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"hint":"If a new function was created in the database with this name and arguments, try reloading the schema cache.",
|
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
||||||
"message":"Could not find the test.overloaded(wrong_arg) function in the schema cache" } |]
|
"message":"Could not find the test.overloaded(wrong_arg) function in the schema cache" } |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
get "/rpc/overloaded?a=1&b=2&wrong_arg=value" `shouldRespondWith`
|
get "/rpc/overloaded?a=1&b=2&wrong_arg=value" `shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"hint":"If a new function was created in the database with this name and arguments, try reloading the schema cache.",
|
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
||||||
"message":"Could not find the test.overloaded(a, b, wrong_arg) function in the schema cache" } |]
|
"message":"Could not find the test.overloaded(a, b, wrong_arg) function in the schema cache" } |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "ambiguous overloaded functions with same arguments but different types" $ do
|
context "ambiguous overloaded functions with same parameters' names but different types" $ do
|
||||||
it "should fail with 300 Multiple Choices without explicit argument type casts" $
|
it "should fail with 300 Multiple Choices without explicit type casts" $
|
||||||
get "/rpc/overloaded_same_args?arg=value" `shouldRespondWith`
|
get "/rpc/overloaded_same_args?arg=value" `shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"hint":"Overloaded functions with the same argument name but different types are not supported",
|
"hint":"Try renaming the parameters or the function itself in the database so function overloading can be resolved",
|
||||||
"message":"Could not choose the best candidate function between: test.overloaded_same_args(arg => integer), test.overloaded_same_args(arg => xml), test.overloaded_same_args(arg => text, num => integer)" } |]
|
"message":"Could not choose the best candidate function between: test.overloaded_same_args(arg => integer), test.overloaded_same_args(arg => xml), test.overloaded_same_args(arg => text, num => integer)"}|]
|
||||||
{ matchStatus = 300
|
{ matchStatus = 300
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
@@ -177,11 +178,11 @@ spec actualPgVersion =
|
|||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "can limit proc results" $ do
|
it "can limit proc results" $ do
|
||||||
post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id?limit=2&offset=1" [json| {} |]
|
post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id&limit=2&offset=1" [json| {} |]
|
||||||
`shouldRespondWith` [json|[{"id":3},{"id":4}]|]
|
`shouldRespondWith` [json|[{"id":3},{"id":4}]|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "1-2/*"] }
|
, matchHeaders = ["Content-Range" <:> "1-2/*"] }
|
||||||
get "/rpc/getallprojects?id=gt.1&id=lt.5&select=id?limit=2&offset=1"
|
get "/rpc/getallprojects?id=gt.1&id=lt.5&select=id&limit=2&offset=1"
|
||||||
`shouldRespondWith` [json|[{"id":3},{"id":4}]|]
|
`shouldRespondWith` [json|[{"id":3},{"id":4}]|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "1-2/*"] }
|
, matchHeaders = ["Content-Range" <:> "1-2/*"] }
|
||||||
@@ -493,6 +494,11 @@ spec actualPgVersion =
|
|||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleStatus p `shouldBe` badRequest400
|
simpleStatus p `shouldBe` badRequest400
|
||||||
isErrorFormat (simpleBody p) `shouldBe` True
|
isErrorFormat (simpleBody p) `shouldBe` True
|
||||||
|
it "treats plpgsql assert as internal server error" $ do
|
||||||
|
p <- post "/rpc/assert" "{}"
|
||||||
|
liftIO $ do
|
||||||
|
simpleStatus p `shouldBe` internalServerError500
|
||||||
|
isErrorFormat (simpleBody p) `shouldBe` True
|
||||||
|
|
||||||
context "unsupported verbs" $ do
|
context "unsupported verbs" $ do
|
||||||
it "DELETE fails" $
|
it "DELETE fails" $
|
||||||
@@ -571,6 +577,17 @@ spec actualPgVersion =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"num":1,"str":"two","b":false}|]
|
[json|{"num":1,"str":"two","b":false}|]
|
||||||
|
|
||||||
|
context "procs with TABLE return" $ do
|
||||||
|
it "returns an object result when there is a single-column TABLE return type" $
|
||||||
|
get "/rpc/single_column_table_return"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"a": "A"}]|]
|
||||||
|
|
||||||
|
it "returns an object result when there is a multi-column TABLE return type" $
|
||||||
|
get "/rpc/multi_column_table_return"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"a": "A", "b": "B"}]|]
|
||||||
|
|
||||||
context "procs with VARIADIC params" $ do
|
context "procs with VARIADIC params" $ do
|
||||||
when (actualPgVersion < pgVersion100) $
|
when (actualPgVersion < pgVersion100) $
|
||||||
it "works with POST (Postgres < 10)" $
|
it "works with POST (Postgres < 10)" $
|
||||||
@@ -726,6 +743,36 @@ spec actualPgVersion =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|"123"|]
|
[json|"123"|]
|
||||||
|
|
||||||
|
-- https://github.com/PostgREST/postgrest/issues/1672
|
||||||
|
context "embedding overloaded functions with the same signature except for the last param with a default value" $ do
|
||||||
|
it "overloaded_default(text default)" $ do
|
||||||
|
request methodPost "/rpc/overloaded_default?select=id,name,users(name)"
|
||||||
|
[("Content-Type", "application/json")]
|
||||||
|
[json|{}|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"id": 2, "name": "Code w7", "users": [{"name": "Angela Martin"}]}] |]
|
||||||
|
|
||||||
|
it "overloaded_default(int)" $
|
||||||
|
request methodPost "/rpc/overloaded_default"
|
||||||
|
[("Content-Type", "application/json")]
|
||||||
|
[json|{"must_param":1}|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{"val":1}|]
|
||||||
|
|
||||||
|
it "overloaded_default(int, text default)" $ do
|
||||||
|
request methodPost "/rpc/overloaded_default?select=id,name,users(name)"
|
||||||
|
[("Content-Type", "application/json")]
|
||||||
|
[json|{"a":4}|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"id": 5, "name": "Design IOS", "users": [{"name": "Michael Scott"}, {"name": "Dwight Schrute"}]}] |]
|
||||||
|
|
||||||
|
it "overloaded_default(int, int)" $
|
||||||
|
request methodPost "/rpc/overloaded_default"
|
||||||
|
[("Content-Type", "application/json")]
|
||||||
|
[json|{"a":2,"must_param":4}|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{"a":2,"val":4}|]
|
||||||
|
|
||||||
context "only for POST rpc" $ do
|
context "only for POST rpc" $ do
|
||||||
it "gives a parse filter error if GET style proc args are specified" $
|
it "gives a parse filter error if GET style proc args are specified" $
|
||||||
post "/rpc/sayhello?name=John" [json|{name: "John"}|] `shouldRespondWith` 400
|
post "/rpc/sayhello?name=John" [json|{name: "John"}|] `shouldRespondWith` 400
|
||||||
@@ -786,7 +833,12 @@ spec actualPgVersion =
|
|||||||
it "custom header is set" $
|
it "custom header is set" $
|
||||||
request methodPost "/rpc/get_guc_value"
|
request methodPost "/rpc/get_guc_value"
|
||||||
[("Custom-Header", "test")]
|
[("Custom-Header", "test")]
|
||||||
[json| { "name": "request.header.custom-header" } |]
|
(
|
||||||
|
if actualPgVersion >= pgVersion140 then
|
||||||
|
[json| { "prefix": "request.headers", "name": "custom-header" } |]
|
||||||
|
else
|
||||||
|
[json| { "name": "request.header.custom-header" } |]
|
||||||
|
)
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|"test"|]
|
[json|"test"|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
@@ -795,7 +847,12 @@ spec actualPgVersion =
|
|||||||
it "standard header is set" $
|
it "standard header is set" $
|
||||||
request methodPost "/rpc/get_guc_value"
|
request methodPost "/rpc/get_guc_value"
|
||||||
[("Origin", "http://example.com")]
|
[("Origin", "http://example.com")]
|
||||||
[json| { "name": "request.header.origin" } |]
|
(
|
||||||
|
if actualPgVersion >= pgVersion140 then
|
||||||
|
[json| { "prefix": "request.headers", "name": "origin" } |]
|
||||||
|
else
|
||||||
|
[json| { "name": "request.header.origin" } |]
|
||||||
|
)
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|"http://example.com"|]
|
[json|"http://example.com"|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
@@ -803,7 +860,12 @@ spec actualPgVersion =
|
|||||||
}
|
}
|
||||||
it "current role is available as GUC claim" $
|
it "current role is available as GUC claim" $
|
||||||
request methodPost "/rpc/get_guc_value" []
|
request methodPost "/rpc/get_guc_value" []
|
||||||
[json| { "name": "request.jwt.claim.role" } |]
|
(
|
||||||
|
if actualPgVersion >= pgVersion140 then
|
||||||
|
[json| { "prefix": "request.jwt.claims", "name": "role" } |]
|
||||||
|
else
|
||||||
|
[json| { "name": "request.jwt.claim.role" } |]
|
||||||
|
)
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|"postgrest_test_anonymous"|]
|
[json|"postgrest_test_anonymous"|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
@@ -811,7 +873,12 @@ spec actualPgVersion =
|
|||||||
}
|
}
|
||||||
it "single cookie ends up as claims" $
|
it "single cookie ends up as claims" $
|
||||||
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue")]
|
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue")]
|
||||||
[json| {"name":"request.cookie.acookie"} |]
|
(
|
||||||
|
if actualPgVersion >= pgVersion140 then
|
||||||
|
[json| {"prefix": "request.cookies", "name":"acookie"} |]
|
||||||
|
else
|
||||||
|
[json| {"name":"request.cookie.acookie"} |]
|
||||||
|
)
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|"cookievalue"|]
|
[json|"cookievalue"|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
@@ -819,7 +886,12 @@ spec actualPgVersion =
|
|||||||
}
|
}
|
||||||
it "multiple cookies ends up as claims" $
|
it "multiple cookies ends up as claims" $
|
||||||
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")]
|
request methodPost "/rpc/get_guc_value" [("Cookie","acookie=cookievalue;secondcookie=anothervalue")]
|
||||||
[json| {"name":"request.cookie.secondcookie"} |]
|
(
|
||||||
|
if actualPgVersion >= pgVersion140 then
|
||||||
|
[json| {"prefix": "request.cookies", "name":"secondcookie"} |]
|
||||||
|
else
|
||||||
|
[json| {"name":"request.cookie.secondcookie"} |]
|
||||||
|
)
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|"anothervalue"|]
|
[json|"anothervalue"|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
@@ -835,7 +907,12 @@ spec actualPgVersion =
|
|||||||
}
|
}
|
||||||
it "gets the Authorization value" $
|
it "gets the Authorization value" $
|
||||||
request methodPost "/rpc/get_guc_value" [authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"]
|
request methodPost "/rpc/get_guc_value" [authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"]
|
||||||
[json| {"name":"request.header.authorization"} |]
|
(
|
||||||
|
if actualPgVersion >= pgVersion140 then
|
||||||
|
[json| {"prefix": "request.headers", "name":"authorization"} |]
|
||||||
|
else
|
||||||
|
[json| {"name":"request.header.authorization"} |]
|
||||||
|
)
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"|]
|
[json|"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"|]
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
@@ -933,72 +1010,70 @@ spec actualPgVersion =
|
|||||||
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
|
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
when (actualPgVersion >= pgVersion96) $
|
it "should work with the phraseto_tsquery function" $
|
||||||
it "should work with the phraseto_tsquery function" $
|
get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith`
|
||||||
get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith`
|
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
|
||||||
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "should work with an argument of custom type in public schema" $
|
it "should work with an argument of custom type in public schema" $
|
||||||
get "/rpc/test_arg?my_arg=something" `shouldRespondWith`
|
get "/rpc/test_arg?my_arg=something" `shouldRespondWith`
|
||||||
[json|"foobar"|]
|
[json|"foobar"|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
when (actualPgVersion >= pgVersion96) $ do
|
context "GUC headers on function calls" $ do
|
||||||
context "GUC headers on function calls" $ do
|
it "succeeds setting the headers" $ do
|
||||||
it "succeeds setting the headers" $ do
|
get "/rpc/get_projects_and_guc_headers?id=eq.2&select=id"
|
||||||
get "/rpc/get_projects_and_guc_headers?id=eq.2&select=id"
|
`shouldRespondWith` [json|[{"id": 2}]|]
|
||||||
`shouldRespondWith` [json|[{"id": 2}]|]
|
{matchHeaders = [
|
||||||
{matchHeaders = [
|
matchContentTypeJson,
|
||||||
matchContentTypeJson,
|
"X-Test" <:> "key1=val1; someValue; key2=val2",
|
||||||
"X-Test" <:> "key1=val1; someValue; key2=val2",
|
"X-Test-2" <:> "key1=val1"]}
|
||||||
"X-Test-2" <:> "key1=val1"]}
|
get "/rpc/get_int_and_guc_headers?num=1"
|
||||||
get "/rpc/get_int_and_guc_headers?num=1"
|
`shouldRespondWith` [json|1|]
|
||||||
`shouldRespondWith` [json|1|]
|
{matchHeaders = [
|
||||||
{matchHeaders = [
|
matchContentTypeJson,
|
||||||
matchContentTypeJson,
|
"X-Test" <:> "key1=val1; someValue; key2=val2",
|
||||||
"X-Test" <:> "key1=val1; someValue; key2=val2",
|
"X-Test-2" <:> "key1=val1"]}
|
||||||
"X-Test-2" <:> "key1=val1"]}
|
post "/rpc/get_int_and_guc_headers" [json|{"num": 1}|]
|
||||||
post "/rpc/get_int_and_guc_headers" [json|{"num": 1}|]
|
`shouldRespondWith` [json|1|]
|
||||||
`shouldRespondWith` [json|1|]
|
{matchHeaders = [
|
||||||
{matchHeaders = [
|
matchContentTypeJson,
|
||||||
matchContentTypeJson,
|
"X-Test" <:> "key1=val1; someValue; key2=val2",
|
||||||
"X-Test" <:> "key1=val1; someValue; key2=val2",
|
"X-Test-2" <:> "key1=val1"]}
|
||||||
"X-Test-2" <:> "key1=val1"]}
|
|
||||||
|
|
||||||
it "fails when setting headers with wrong json structure" $ do
|
it "fails when setting headers with wrong json structure" $ do
|
||||||
get "/rpc/bad_guc_headers_1"
|
get "/rpc/bad_guc_headers_1"
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]
|
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]
|
||||||
{ matchStatus = 500
|
{ matchStatus = 500
|
||||||
, matchHeaders = [ matchContentTypeJson ]
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
}
|
}
|
||||||
get "/rpc/bad_guc_headers_2"
|
get "/rpc/bad_guc_headers_2"
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]
|
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]
|
||||||
{ matchStatus = 500
|
{ matchStatus = 500
|
||||||
, matchHeaders = [ matchContentTypeJson ]
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
}
|
}
|
||||||
get "/rpc/bad_guc_headers_3"
|
get "/rpc/bad_guc_headers_3"
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]
|
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]
|
||||||
{ matchStatus = 500
|
{ matchStatus = 500
|
||||||
, matchHeaders = [ matchContentTypeJson ]
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
}
|
}
|
||||||
post "/rpc/bad_guc_headers_1" [json|{}|]
|
post "/rpc/bad_guc_headers_1" [json|{}|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]
|
[json|{"message":"response.headers guc must be a JSON array composed of objects with a single key and a string value"}|]
|
||||||
{ matchStatus = 500
|
{ matchStatus = 500
|
||||||
, matchHeaders = [ matchContentTypeJson ]
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "can set the same http header twice" $
|
it "can set the same http header twice" $
|
||||||
get "/rpc/set_cookie_twice"
|
get "/rpc/set_cookie_twice"
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
"null"
|
"null"
|
||||||
{ matchHeaders = [ matchContentTypeJson
|
{ matchHeaders = [ matchContentTypeJson
|
||||||
, "Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/"
|
, "Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/"
|
||||||
, "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly" ]}
|
, "Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly" ]}
|
||||||
|
|
||||||
it "can override the Location header on a trigger" $
|
it "can override the Location header on a trigger" $
|
||||||
post "/stuff"
|
post "/stuff"
|
||||||
@@ -1059,3 +1134,123 @@ spec actualPgVersion =
|
|||||||
{ matchStatus = 500
|
{ matchStatus = 500
|
||||||
, matchHeaders = [ matchContentTypeJson ]
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context "single unnamed param" $ do
|
||||||
|
it "can insert json directly" $
|
||||||
|
post "/rpc/unnamed_json_param"
|
||||||
|
[json|{"A": 1, "B": 2, "C": 3}|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{"A": 1, "B": 2, "C": 3}|]
|
||||||
|
|
||||||
|
it "can insert text directly" $
|
||||||
|
request methodPost "/rpc/unnamed_text_param"
|
||||||
|
[("Content-Type", "text/plain"), ("Accept", "text/plain")]
|
||||||
|
[str|unnamed text arg|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[str|unnamed text arg|]
|
||||||
|
|
||||||
|
it "can insert bytea directly" $ do
|
||||||
|
let file = unsafePerformIO $ BL.readFile "test/C.png"
|
||||||
|
r <- request methodPost "/rpc/unnamed_bytea_param"
|
||||||
|
[("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]
|
||||||
|
file
|
||||||
|
liftIO $ do
|
||||||
|
let respBody = simpleBody r
|
||||||
|
respBody `shouldBe` file
|
||||||
|
|
||||||
|
it "will err when no function with single unnamed json parameter exists and application/json is specified" $
|
||||||
|
request methodPost "/rpc/unnamed_int_param" [("Content-Type", "application/json")]
|
||||||
|
[json|{"x": 1, "y": 2}|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
||||||
|
"message": "Could not find the test.unnamed_int_param(x, y) function or the test.unnamed_int_param function with a single unnamed json or jsonb parameter in the schema cache"
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 404
|
||||||
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "will err when no function with single unnamed text parameter exists and text/plain is specified" $
|
||||||
|
request methodPost "/rpc/unnamed_int_param"
|
||||||
|
[("Content-Type", "text/plain")]
|
||||||
|
[str|a simple text|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
||||||
|
"message": "Could not find the test.unnamed_int_param function with a single unnamed text parameter in the schema cache"
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 404
|
||||||
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "will err when no function with single unnamed bytea parameter exists and application/octet-stream is specified" $
|
||||||
|
let file = unsafePerformIO $ BL.readFile "test/C.png" in
|
||||||
|
request methodPost "/rpc/unnamed_int_param"
|
||||||
|
[("Content-Type", "application/octet-stream")]
|
||||||
|
file
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
||||||
|
"message": "Could not find the test.unnamed_int_param function with a single unnamed bytea parameter in the schema cache"
|
||||||
|
}|]
|
||||||
|
{ matchStatus = 404
|
||||||
|
, matchHeaders = [ matchContentTypeJson ]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "should be able to resolve when a single unnamed json parameter exists and other overloaded functions are found" $ do
|
||||||
|
request methodPost "/rpc/overloaded_unnamed_param" [("Content-Type", "application/json")]
|
||||||
|
[json|{}|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| 1 |]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
request methodPost "/rpc/overloaded_unnamed_param" [("Content-Type", "application/json")]
|
||||||
|
[json|{"x": 1, "y": 2}|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| 3 |]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "should be able to fallback to the single unnamed parameter function when other overloaded functions are not found" $ do
|
||||||
|
request methodPost "/rpc/overloaded_unnamed_param"
|
||||||
|
[("Content-Type", "application/json")]
|
||||||
|
[json|{"A": 1, "B": 2, "C": 3}|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{"A": 1, "B": 2, "C": 3}|]
|
||||||
|
request methodPost "/rpc/overloaded_unnamed_param"
|
||||||
|
[("Content-Type", "text/plain"), ("Accept", "text/plain")]
|
||||||
|
[str|unnamed text arg|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[str|unnamed text arg|]
|
||||||
|
let file = unsafePerformIO $ BL.readFile "test/C.png"
|
||||||
|
r <- request methodPost "/rpc/overloaded_unnamed_param"
|
||||||
|
[("Content-Type", "application/octet-stream"), ("Accept", "application/octet-stream")]
|
||||||
|
file
|
||||||
|
liftIO $ do
|
||||||
|
let respBody = simpleBody r
|
||||||
|
respBody `shouldBe` file
|
||||||
|
|
||||||
|
it "should fail to fallback to any single unnamed parameter function when using an unsupported Content-Type header" $ do
|
||||||
|
request methodPost "/rpc/overloaded_unnamed_param"
|
||||||
|
[("Content-Type", "text/csv")]
|
||||||
|
"a,b\n1,2\n4,6\n100,200"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {
|
||||||
|
"hint":"If a new function was created in the database with this name and parameters, try reloading the schema cache.",
|
||||||
|
"message":"Could not find the test.overloaded_unnamed_param(a, b) function in the schema cache"}|]
|
||||||
|
{ matchStatus = 404
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "should fail with multiple choices when two fallback functions with single unnamed json and jsonb parameters exist" $ do
|
||||||
|
request methodPost "/rpc/overloaded_unnamed_json_jsonb_param" [("Content-Type", "application/json")]
|
||||||
|
[json|{"A": 1, "B": 2, "C": 3}|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {
|
||||||
|
"hint":"Try renaming the parameters or the function itself in the database so function overloading can be resolved",
|
||||||
|
"message":"Could not choose the best candidate function between: test.overloaded_unnamed_json_jsonb_param( => json), test.overloaded_unnamed_json_jsonb_param( => jsonb)"}|]
|
||||||
|
{ matchStatus = 300
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import Test.Hspec
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion110)
|
||||||
|
|
||||||
import Protolude hiding (get, put)
|
import Protolude hiding (get, put)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
spec :: SpecWith ((), Application)
|
spec :: PgVersion -> SpecWith ((), Application)
|
||||||
spec =
|
spec actualPgVersion =
|
||||||
describe "UPSERT" $ do
|
describe "UPSERT" $ do
|
||||||
context "with POST" $ do
|
context "with POST" $ do
|
||||||
context "when Prefer: resolution=merge-duplicates is specified" $ do
|
context "when Prefer: resolution=merge-duplicates is specified" $ do
|
||||||
@@ -43,6 +45,20 @@ spec =
|
|||||||
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
|
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $
|
||||||
|
it "INSERTs and UPDATEs rows on composite pk conflict for partitioned tables" $
|
||||||
|
request methodPost "/car_models" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
||||||
|
[json| [
|
||||||
|
{ "name": "Murcielago", "year": 2001, "car_brand_name": null},
|
||||||
|
{ "name": "Roma", "year": 2021, "car_brand_name": "Ferrari" }
|
||||||
|
]|] `shouldRespondWith` [json| [
|
||||||
|
{ "name": "Murcielago", "year": 2001, "car_brand_name": null},
|
||||||
|
{ "name": "Roma", "year": 2021, "car_brand_name": "Ferrari" }
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 201
|
||||||
|
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
it "succeeds when the payload has no elements" $
|
it "succeeds when the payload has no elements" $
|
||||||
request methodPost "/articles" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
request methodPost "/articles" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
||||||
[json|[]|] `shouldRespondWith`
|
[json|[]|] `shouldRespondWith`
|
||||||
@@ -99,6 +115,19 @@ spec =
|
|||||||
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $
|
||||||
|
it "INSERTs and ignores rows on composite pk conflict for partitioned tables" $
|
||||||
|
request methodPost "/car_models" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||||
|
[json| [
|
||||||
|
{ "name": "Murcielago", "year": 2001, "car_brand_name": "Ferrari" },
|
||||||
|
{ "name": "Huracán", "year": 2021, "car_brand_name": "Lamborghini" }
|
||||||
|
]|] `shouldRespondWith` [json| [
|
||||||
|
{ "name": "Huracán", "year": 2021, "car_brand_name": "Lamborghini" }
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 201
|
||||||
|
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
it "INSERTs and ignores rows on single unique key conflict" $
|
it "INSERTs and ignores rows on single unique key conflict" $
|
||||||
request methodPost "/single_unique?on_conflict=unique_key"
|
request methodPost "/single_unique?on_conflict=unique_key"
|
||||||
[("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
[("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||||
@@ -264,6 +293,19 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|]
|
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|]
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $
|
||||||
|
it "succeeds on a partitioned table with composite pk" $ do
|
||||||
|
-- assert that the next request will indeed be an insert
|
||||||
|
get "/car_models?name=eq.Supra&year=eq.2021"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[]|]
|
||||||
|
|
||||||
|
request methodPut "/car_models?name=eq.Supra&year=eq.2021"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
[json| [ { "name": "Supra", "year": 2021 } ]|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| [ { "name": "Supra", "year": 2021, "car_brand_name": null } ]|]
|
||||||
|
|
||||||
it "succeeds if the table has only PK cols and no other cols" $ do
|
it "succeeds if the table has only PK cols and no other cols" $ do
|
||||||
-- assert that the next request will indeed be an insert
|
-- assert that the next request will indeed be an insert
|
||||||
get "/only_pk?id=eq.10"
|
get "/only_pk?id=eq.10"
|
||||||
@@ -315,6 +357,19 @@ spec =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "$60,000.00", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]
|
[json| [ { "first_name": "Frances M.", "last_name": "Roe", "salary": "$60,000.00", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]
|
||||||
|
|
||||||
|
when (actualPgVersion >= pgVersion110) $
|
||||||
|
it "succeeds on a partitioned table with composite pk" $ do
|
||||||
|
-- assert that the next request will indeed be an update
|
||||||
|
get "/car_models?name=eq.DeLorean&year=eq.1981"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": "DMC" } ]|]
|
||||||
|
|
||||||
|
request methodPut "/car_models?name=eq.DeLorean&year=eq.1981"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
[json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": null } ]|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| [ { "name": "DeLorean", "year": 1981, "car_brand_name": null } ]|]
|
||||||
|
|
||||||
it "succeeds if the table has only PK cols and no other cols" $ do
|
it "succeeds if the table has only PK cols and no other cols" $ do
|
||||||
-- assert that the next request will indeed be an update
|
-- assert that the next request will indeed be an update
|
||||||
get "/only_pk?id=eq.1"
|
get "/only_pk?id=eq.1"
|
||||||
|
|||||||
+28
-19
@@ -9,13 +9,12 @@ import Data.List.NonEmpty (toList)
|
|||||||
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
import PostgREST.App (postgrest)
|
import PostgREST.App (postgrest)
|
||||||
import PostgREST.Config (AppConfig (..), LogLevel (..))
|
import PostgREST.Config (AppConfig (..), LogLevel (..))
|
||||||
import PostgREST.Config.Database (queryPgVersion)
|
import PostgREST.Config.Database (queryPgVersion)
|
||||||
import PostgREST.Config.PgVersion (pgVersion96)
|
import PostgREST.DbStructure (queryDbStructure)
|
||||||
import PostgREST.DbStructure (queryDbStructure)
|
import Protolude hiding (toList, toS)
|
||||||
import Protolude hiding (toList, toS)
|
import Protolude.Conv (toS)
|
||||||
import Protolude.Conv (toS)
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
import qualified PostgREST.AppState as AppState
|
import qualified PostgREST.AppState as AppState
|
||||||
@@ -30,11 +29,13 @@ import qualified Feature.CorsSpec
|
|||||||
import qualified Feature.DeleteSpec
|
import qualified Feature.DeleteSpec
|
||||||
import qualified Feature.DisabledOpenApiSpec
|
import qualified Feature.DisabledOpenApiSpec
|
||||||
import qualified Feature.EmbedDisambiguationSpec
|
import qualified Feature.EmbedDisambiguationSpec
|
||||||
|
import qualified Feature.EmbedInnerJoinSpec
|
||||||
import qualified Feature.ExtraSearchPathSpec
|
import qualified Feature.ExtraSearchPathSpec
|
||||||
import qualified Feature.HtmlRawOutputSpec
|
import qualified Feature.HtmlRawOutputSpec
|
||||||
import qualified Feature.IgnorePrivOpenApiSpec
|
import qualified Feature.IgnorePrivOpenApiSpec
|
||||||
import qualified Feature.InsertSpec
|
import qualified Feature.InsertSpec
|
||||||
import qualified Feature.JsonOperatorSpec
|
import qualified Feature.JsonOperatorSpec
|
||||||
|
import qualified Feature.LegacyGucsSpec
|
||||||
import qualified Feature.MultipleSchemaSpec
|
import qualified Feature.MultipleSchemaSpec
|
||||||
import qualified Feature.NoJwtSpec
|
import qualified Feature.NoJwtSpec
|
||||||
import qualified Feature.NonexistentSchemaSpec
|
import qualified Feature.NonexistentSchemaSpec
|
||||||
@@ -67,6 +68,7 @@ main = do
|
|||||||
loadDbStructure pool
|
loadDbStructure pool
|
||||||
(configDbSchemas $ testCfg testDbConn)
|
(configDbSchemas $ testCfg testDbConn)
|
||||||
(configDbExtraSearchPath $ testCfg testDbConn)
|
(configDbExtraSearchPath $ testCfg testDbConn)
|
||||||
|
actualPgVersion
|
||||||
|
|
||||||
let
|
let
|
||||||
-- For tests that run with the same refDbStructure
|
-- For tests that run with the same refDbStructure
|
||||||
@@ -86,7 +88,9 @@ main = do
|
|||||||
loadDbStructure pool
|
loadDbStructure pool
|
||||||
(configDbSchemas config)
|
(configDbSchemas config)
|
||||||
(configDbExtraSearchPath config)
|
(configDbExtraSearchPath config)
|
||||||
|
actualPgVersion
|
||||||
appState <- AppState.initWithPool pool config
|
appState <- AppState.initWithPool pool config
|
||||||
|
AppState.putPgVersion appState actualPgVersion
|
||||||
AppState.putDbStructure appState customDbStructure
|
AppState.putDbStructure appState customDbStructure
|
||||||
when (isJust $ configDbRootSpec config) $
|
when (isJust $ configDbRootSpec config) $
|
||||||
AppState.putJsonDbS appState $ toS $ JSON.encode baseDbStructure
|
AppState.putJsonDbS appState $ toS $ JSON.encode baseDbStructure
|
||||||
@@ -106,6 +110,7 @@ main = do
|
|||||||
responseHeadersApp = app testCfgResponseHeaders
|
responseHeadersApp = app testCfgResponseHeaders
|
||||||
disallowRollbackApp = app testCfgDisallowRollback
|
disallowRollbackApp = app testCfgDisallowRollback
|
||||||
forceRollbackApp = app testCfgForceRollback
|
forceRollbackApp = app testCfgForceRollback
|
||||||
|
testCfgLegacyGucsApp = app testCfgLegacyGucs
|
||||||
|
|
||||||
extraSearchPathApp = appDbs testCfgExtraSearchPath
|
extraSearchPathApp = appDbs testCfgExtraSearchPath
|
||||||
unicodeApp = appDbs testUnicodeCfg
|
unicodeApp = appDbs testUnicodeCfg
|
||||||
@@ -125,16 +130,17 @@ main = do
|
|||||||
, ("Feature.CorsSpec" , Feature.CorsSpec.spec)
|
, ("Feature.CorsSpec" , Feature.CorsSpec.spec)
|
||||||
, ("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
|
, ("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
|
||||||
, ("Feature.EmbedDisambiguationSpec" , Feature.EmbedDisambiguationSpec.spec)
|
, ("Feature.EmbedDisambiguationSpec" , Feature.EmbedDisambiguationSpec.spec)
|
||||||
|
, ("Feature.EmbedInnerJoinSpec" , Feature.EmbedInnerJoinSpec.spec)
|
||||||
, ("Feature.InsertSpec" , Feature.InsertSpec.spec actualPgVersion)
|
, ("Feature.InsertSpec" , Feature.InsertSpec.spec actualPgVersion)
|
||||||
, ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec actualPgVersion)
|
, ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec actualPgVersion)
|
||||||
, ("Feature.OpenApiSpec" , Feature.OpenApiSpec.spec)
|
, ("Feature.OpenApiSpec" , Feature.OpenApiSpec.spec actualPgVersion)
|
||||||
, ("Feature.OptionsSpec" , Feature.OptionsSpec.spec)
|
, ("Feature.OptionsSpec" , Feature.OptionsSpec.spec actualPgVersion)
|
||||||
, ("Feature.QuerySpec" , Feature.QuerySpec.spec actualPgVersion)
|
, ("Feature.QuerySpec" , Feature.QuerySpec.spec actualPgVersion)
|
||||||
, ("Feature.RawOutputTypesSpec" , Feature.RawOutputTypesSpec.spec)
|
, ("Feature.RawOutputTypesSpec" , Feature.RawOutputTypesSpec.spec)
|
||||||
, ("Feature.RpcSpec" , Feature.RpcSpec.spec actualPgVersion)
|
, ("Feature.RpcSpec" , Feature.RpcSpec.spec actualPgVersion)
|
||||||
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
|
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
|
||||||
, ("Feature.UpdateSpec" , Feature.UpdateSpec.spec)
|
, ("Feature.UpdateSpec" , Feature.UpdateSpec.spec)
|
||||||
, ("Feature.UpsertSpec" , Feature.UpsertSpec.spec)
|
, ("Feature.UpsertSpec" , Feature.UpsertSpec.spec actualPgVersion)
|
||||||
]
|
]
|
||||||
|
|
||||||
hspec $ do
|
hspec $ do
|
||||||
@@ -196,16 +202,19 @@ main = do
|
|||||||
parallel $ before extraSearchPathApp $
|
parallel $ before extraSearchPathApp $
|
||||||
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec
|
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec
|
||||||
|
|
||||||
when (actualPgVersion >= pgVersion96) $ do
|
-- this test runs with a root spec function override
|
||||||
-- this test runs with a root spec function override
|
parallel $ before rootSpecApp $
|
||||||
parallel $ before rootSpecApp $
|
describe "Feature.RootSpec" Feature.RootSpec.spec
|
||||||
describe "Feature.RootSpec" Feature.RootSpec.spec
|
parallel $ before responseHeadersApp $
|
||||||
parallel $ before responseHeadersApp $
|
describe "Feature.RpcPreRequestGucsSpec" Feature.RpcPreRequestGucsSpec.spec
|
||||||
describe "Feature.RpcPreRequestGucsSpec" Feature.RpcPreRequestGucsSpec.spec
|
|
||||||
|
|
||||||
-- this test runs with multiple schemas
|
-- this test runs with multiple schemas
|
||||||
parallel $ before multipleSchemaApp $
|
parallel $ before multipleSchemaApp $
|
||||||
describe "Feature.MultipleSchemaSpec" $ Feature.MultipleSchemaSpec.spec actualPgVersion
|
describe "Feature.MultipleSchemaSpec" Feature.MultipleSchemaSpec.spec
|
||||||
|
|
||||||
|
-- this test runs with db-uses-legacy-gucs = false
|
||||||
|
parallel $ before testCfgLegacyGucsApp $
|
||||||
|
describe "Feature.LegacyGucsSpec" Feature.LegacyGucsSpec.spec
|
||||||
|
|
||||||
-- Note: the rollback tests can not run in parallel, because they test persistance and
|
-- Note: the rollback tests can not run in parallel, because they test persistance and
|
||||||
-- this results in race conditions
|
-- this results in race conditions
|
||||||
@@ -223,5 +232,5 @@ main = do
|
|||||||
describe "Feature.RollbackForcedSpec" Feature.RollbackSpec.forced
|
describe "Feature.RollbackForcedSpec" Feature.RollbackSpec.forced
|
||||||
|
|
||||||
where
|
where
|
||||||
loadDbStructure pool schemas extraSearchPath =
|
loadDbStructure pool schemas extraSearchPath actualPgVersion =
|
||||||
either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ queryDbStructure (toList schemas) extraSearchPath True)
|
either (panic.show) id <$> P.use pool (HT.transaction HT.ReadCommitted HT.Read $ queryDbStructure (toList schemas) extraSearchPath actualPgVersion True)
|
||||||
|
|||||||
+14
-11
@@ -15,11 +15,10 @@ import Protolude hiding (get, toS)
|
|||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
import PostgREST.Query.QueryBuilder (requestToCallProcQuery)
|
import PostgREST.Query.QueryBuilder (requestToCallProcQuery)
|
||||||
import PostgREST.Request.ApiRequest (PayloadJSON (..))
|
import PostgREST.Request.Types
|
||||||
|
|
||||||
import PostgREST.DbStructure.Identifiers
|
import PostgREST.DbStructure.Identifiers
|
||||||
import PostgREST.DbStructure.Proc
|
import PostgREST.DbStructure.Proc
|
||||||
import PostgREST.Request.Preferences
|
|
||||||
|
|
||||||
import SpecHelper (getEnvVarWithDefault)
|
import SpecHelper (getEnvVarWithDefault)
|
||||||
|
|
||||||
@@ -34,29 +33,32 @@ main = do
|
|||||||
context "call proc query" $ do
|
context "call proc query" $ do
|
||||||
it "should not exceed cost when calling setof composite proc" $ do
|
it "should not exceed cost when calling setof composite proc" $ do
|
||||||
cost <- exec pool $
|
cost <- exec pool $
|
||||||
requestToCallProcQuery (QualifiedIdentifier "test" "get_projects_below") [PgArg "id" "int" True False]
|
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
|
||||||
(Just $ RawJSON [str| {"id": 3} |]) False Nothing []
|
(KeyParams [ProcParam "id" "int" True False])
|
||||||
|
(Just [str| {"id": 3} |]) False False [])
|
||||||
liftIO $
|
liftIO $
|
||||||
cost `shouldSatisfy` (< Just 40)
|
cost `shouldSatisfy` (< Just 40)
|
||||||
|
|
||||||
it "should not exceed cost when calling setof composite proc with empty params" $ do
|
it "should not exceed cost when calling setof composite proc with empty params" $ do
|
||||||
cost <- exec pool $
|
cost <- exec pool $
|
||||||
requestToCallProcQuery (QualifiedIdentifier "test" "getallprojects") [] Nothing False Nothing []
|
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "getallprojects") (KeyParams []) Nothing False False [])
|
||||||
liftIO $
|
liftIO $
|
||||||
cost `shouldSatisfy` (< Just 30)
|
cost `shouldSatisfy` (< Just 30)
|
||||||
|
|
||||||
it "should not exceed cost when calling scalar proc" $ do
|
it "should not exceed cost when calling scalar proc" $ do
|
||||||
cost <- exec pool $
|
cost <- exec pool $
|
||||||
requestToCallProcQuery (QualifiedIdentifier "test" "add_them") [PgArg "a" "int" True False, PgArg "b" "int" True False]
|
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
|
||||||
(Just $ RawJSON [str| {"a": 3, "b": 4} |]) True Nothing []
|
(KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])
|
||||||
|
(Just [str| {"a": 3, "b": 4} |]) True False [])
|
||||||
liftIO $
|
liftIO $
|
||||||
cost `shouldSatisfy` (< Just 10)
|
cost `shouldSatisfy` (< Just 10)
|
||||||
|
|
||||||
context "params=multiple-objects" $ do
|
context "params=multiple-objects" $ do
|
||||||
it "should not exceed cost when calling setof composite proc" $ do
|
it "should not exceed cost when calling setof composite proc" $ do
|
||||||
cost <- exec pool $
|
cost <- exec pool $
|
||||||
requestToCallProcQuery (QualifiedIdentifier "test" "get_projects_below") [PgArg "id" "int" True False]
|
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
|
||||||
(Just $ RawJSON [str| [{"id": 1}, {"id": 4}] |]) False (Just MultipleObjects) []
|
(KeyParams [ProcParam "id" "int" True False])
|
||||||
|
(Just [str| [{"id": 1}, {"id": 4}] |]) False True [])
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
-- lower bound needed for now to make sure that cost is not Nothing
|
-- lower bound needed for now to make sure that cost is not Nothing
|
||||||
cost `shouldSatisfy` (> Just 2000)
|
cost `shouldSatisfy` (> Just 2000)
|
||||||
@@ -64,8 +66,9 @@ main = do
|
|||||||
|
|
||||||
it "should not exceed cost when calling scalar proc" $ do
|
it "should not exceed cost when calling scalar proc" $ do
|
||||||
cost <- exec pool $
|
cost <- exec pool $
|
||||||
requestToCallProcQuery (QualifiedIdentifier "test" "add_them") [PgArg "a" "int" True False, PgArg "b" "int" True False]
|
requestToCallProcQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
|
||||||
(Just $ RawJSON [str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |]) True Nothing []
|
(KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])
|
||||||
|
(Just [str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |]) True False [])
|
||||||
liftIO $
|
liftIO $
|
||||||
cost `shouldSatisfy` (< Just 10)
|
cost `shouldSatisfy` (< Just 10)
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ _baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
|||||||
, configDbSchemas = fromList ["test"]
|
, configDbSchemas = fromList ["test"]
|
||||||
, configDbConfig = False
|
, configDbConfig = False
|
||||||
, configDbUri = mempty
|
, configDbUri = mempty
|
||||||
|
, configDbUseLegacyGucs = True
|
||||||
, configFilePath = Nothing
|
, configFilePath = Nothing
|
||||||
, configJWKS = parseSecret <$> secret
|
, configJWKS = parseSecret <$> secret
|
||||||
, configJwtAudience = Nothing
|
, configJwtAudience = Nothing
|
||||||
@@ -185,6 +186,9 @@ testCfgResponseHeaders testDbConn = (testCfg testDbConn) { configDbPreRequest =
|
|||||||
testMultipleSchemaCfg :: Text -> AppConfig
|
testMultipleSchemaCfg :: Text -> AppConfig
|
||||||
testMultipleSchemaCfg testDbConn = (testCfg testDbConn) { configDbSchemas = fromList ["v1", "v2"] }
|
testMultipleSchemaCfg testDbConn = (testCfg testDbConn) { configDbSchemas = fromList ["v1", "v2"] }
|
||||||
|
|
||||||
|
testCfgLegacyGucs :: Text -> AppConfig
|
||||||
|
testCfgLegacyGucs testDbConn = (testCfg testDbConn) { configDbUseLegacyGucs = False }
|
||||||
|
|
||||||
resetDb :: Text -> IO ()
|
resetDb :: Text -> IO ()
|
||||||
resetDb dbConn = loadFixture dbConn "data"
|
resetDb dbConn = loadFixture dbConn "data"
|
||||||
|
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
#! /usr/bin/env bash
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
echo "$0 <db-uri> <test-database>"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
echo "Please supply the connection uri for the user with create database privileges"
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$2" ]; then
|
|
||||||
echo "Please supply the test database name"
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ $1 != postgres://* ]]; then
|
|
||||||
echo "Please use a valid connection URI (https://www.postgresql.org/docs/current/static/libpq-connect.html#AEN45347)"
|
|
||||||
usage
|
|
||||||
fi
|
|
||||||
|
|
||||||
BASEPATH=$( cd "$(dirname "$0")" && pwd -P )
|
|
||||||
URI="$1"
|
|
||||||
#Extract host and port--we need this to form the new connection string
|
|
||||||
HOST_PORT=$(echo "$URI" | cut -d'/' -f3 | cut -d'@' -f2 )
|
|
||||||
DB=$2
|
|
||||||
# Specify the username of choice, or let the script create a random unique user by appending the database name
|
|
||||||
TEST_USER_NAME=postgrest_test_authenticator
|
|
||||||
# New password will get assigned only if the user does not already exist
|
|
||||||
# Otherwise make sure to provide the correct password for the existing user
|
|
||||||
TEST_USER_PASS=$(< /dev/urandom env LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)
|
|
||||||
|
|
||||||
if ! PGOPTIONS='-c client_min_messages=WARNING' psql "$URI" -Xq >/dev/null -c 'select rolcreatedb from pg_authid where rolname = current_user;' 2>/dev/null;
|
|
||||||
then
|
|
||||||
echo "ERROR: Please specify the user with 'Create DB' permissions, and ensure that the default database for the username exists."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# plpgsql does not like psql variables, easier to pull this part off with bash variables
|
|
||||||
PGOPTIONS='-c client_min_messages=WARNING' psql "$URI" -Xq >/dev/null <<EOF
|
|
||||||
SELECT pg_terminate_backend(pg_stat_activity.pid)
|
|
||||||
FROM pg_stat_activity
|
|
||||||
WHERE pg_stat_activity.datname = '$DB'
|
|
||||||
AND pid <> pg_backend_pid();
|
|
||||||
|
|
||||||
DROP DATABASE IF EXISTS $DB;
|
|
||||||
DROP ROLE IF EXISTS $TEST_USER_NAME;
|
|
||||||
CREATE USER $TEST_USER_NAME WITH SUPERUSER LOGIN NOINHERIT PASSWORD '$TEST_USER_PASS' CREATEROLE;
|
|
||||||
ALTER ROLE postgrest_test_authenticator SET default_text_search_config TO english;
|
|
||||||
CREATE DATABASE $DB OWNER $TEST_USER_NAME LC_COLLATE 'POSIX' LC_CTYPE 'POSIX' TEMPLATE template0;
|
|
||||||
\\connect $DB
|
|
||||||
ALTER SCHEMA public OWNER TO $TEST_USER_NAME;
|
|
||||||
ALTER DATABASE $DB SET LC_MESSAGES = 'POSIX';
|
|
||||||
ALTER DATABASE $DB SET LC_MONETARY = 'POSIX';
|
|
||||||
ALTER DATABASE $DB SET LC_NUMERIC = 'POSIX';
|
|
||||||
ALTER DATABASE $DB SET LC_TIME = 'POSIX';
|
|
||||||
|
|
||||||
\i $BASEPATH/fixtures/load.sql
|
|
||||||
EOF
|
|
||||||
|
|
||||||
#Remove database path from the connection uri--prevents setting up the new database name with PGDATABASE
|
|
||||||
URI=$(echo "$URI" | cut -d'/' -f1-3)
|
|
||||||
|
|
||||||
# Create a new connection string to use with the test runner
|
|
||||||
export PGRST_DB_URI="postgres://${TEST_USER_NAME}:$TEST_USER_PASS@$HOST_PORT/$DB"
|
|
||||||
export PGRST_DB_ANON_ROLE="postgrest_test_anonymous"
|
|
||||||
export PGRST_DB_SCHEMAS="test"
|
|
||||||
|
|
||||||
shift 2
|
|
||||||
"$@"
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
module Main (main) where
|
||||||
|
|
||||||
|
import Test.DocTest (doctest)
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main =
|
||||||
|
doctest
|
||||||
|
[ "--verbose"
|
||||||
|
, "-XOverloadedStrings"
|
||||||
|
, "-XNoImplicitPrelude"
|
||||||
|
, "-XStandaloneDeriving"
|
||||||
|
, "-isrc"
|
||||||
|
, "src/PostgREST/Request/Preferences.hs"
|
||||||
|
]
|
||||||
Vendored
+67
@@ -345,6 +345,10 @@ INSERT INTO w_or_wo_comma_names VALUES ('Smith, Joseph');
|
|||||||
INSERT INTO w_or_wo_comma_names VALUES ('David White');
|
INSERT INTO w_or_wo_comma_names VALUES ('David White');
|
||||||
INSERT INTO w_or_wo_comma_names VALUES ('Larry Thompson');
|
INSERT INTO w_or_wo_comma_names VALUES ('Larry Thompson');
|
||||||
INSERT INTO w_or_wo_comma_names VALUES ('Double O Seven(007)');
|
INSERT INTO w_or_wo_comma_names VALUES ('Double O Seven(007)');
|
||||||
|
INSERT INTO w_or_wo_comma_names VALUES ('"');
|
||||||
|
INSERT INTO w_or_wo_comma_names VALUES ('Double"Quote"McGraw"');
|
||||||
|
INSERT INTO w_or_wo_comma_names VALUES ('\');
|
||||||
|
INSERT INTO w_or_wo_comma_names VALUES ('/\Slash/\Beast/\');
|
||||||
|
|
||||||
TRUNCATE TABLE items_with_different_col_types CASCADE;
|
TRUNCATE TABLE items_with_different_col_types CASCADE;
|
||||||
INSERT INTO items_with_different_col_types VALUES (1, null, null, null, null, null, null, null);
|
INSERT INTO items_with_different_col_types VALUES (1, null, null, null, null, null, null, null);
|
||||||
@@ -663,3 +667,66 @@ INSERT INTO private.films (id, title) VALUES (12,'douze commandements'), (2001,'
|
|||||||
|
|
||||||
TRUNCATE TABLE private.personnages CASCADE;
|
TRUNCATE TABLE private.personnages CASCADE;
|
||||||
INSERT INTO private.personnages (film_id, role_id, character) VALUES (12,1,'méchant'), (2001,2,'astronaute');
|
INSERT INTO private.personnages (film_id, role_id, character) VALUES (12,1,'méchant'), (2001,2,'astronaute');
|
||||||
|
|
||||||
|
DO $do$BEGIN
|
||||||
|
IF (SELECT current_setting('server_version_num')::INT >= 100000) THEN
|
||||||
|
INSERT INTO test.car_models(name, year) VALUES ('DeLorean',1981);
|
||||||
|
INSERT INTO test.car_models(name, year) VALUES ('F310-B',1997);
|
||||||
|
INSERT INTO test.car_models(name, year) VALUES ('Veneno',2013);
|
||||||
|
INSERT INTO test.car_models(name, year) VALUES ('Murcielago',2001);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF (SELECT current_setting('server_version_num')::INT >= 110000) THEN
|
||||||
|
INSERT INTO test.car_brands(name) VALUES ('DMC');
|
||||||
|
INSERT INTO test.car_brands(name) VALUES ('Ferrari');
|
||||||
|
INSERT INTO test.car_brands(name) VALUES ('Lamborghini');
|
||||||
|
|
||||||
|
UPDATE test.car_models SET car_brand_name = 'DMC' WHERE name = 'DeLorean';
|
||||||
|
UPDATE test.car_models SET car_brand_name = 'Ferrari' WHERE name = 'F310-B';
|
||||||
|
UPDATE test.car_models SET car_brand_name = 'Lamborghini' WHERE name = 'Veneno';
|
||||||
|
UPDATE test.car_models SET car_brand_name = 'Lamborghini' WHERE name = 'Murcielago';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF (SELECT current_setting('server_version_num')::INT >= 120000) THEN
|
||||||
|
INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-01-14',7,'DeLorean',1981);
|
||||||
|
INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-01-15',9,'DeLorean',1981);
|
||||||
|
INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-02-11',1,'Murcielago',2001);
|
||||||
|
INSERT INTO test.car_model_sales(date, quantity, car_model_name, car_model_year) VALUES ('2021-02-12',3,'Murcielago',2001);
|
||||||
|
|
||||||
|
INSERT INTO test.car_racers(name) VALUES ('Alain Prost');
|
||||||
|
INSERT INTO test.car_racers(name, car_model_name, car_model_year) VALUES ('Michael Schumacher', 'F310-B', 1997);
|
||||||
|
|
||||||
|
INSERT INTO test.car_dealers(name,city) VALUES ('Springfield Cars S.A.','Springfield');
|
||||||
|
INSERT INTO test.car_dealers(name,city) VALUES ('The Best Deals S.A.','Franklin');
|
||||||
|
|
||||||
|
INSERT INTO test.car_models_car_dealers(car_model_name, car_model_year, car_dealer_name, car_dealer_city, quantity) VALUES ('DeLorean',1981,'Springfield Cars S.A.','Springfield',15);
|
||||||
|
INSERT INTO test.car_models_car_dealers(car_model_name, car_model_year, car_dealer_name, car_dealer_city, quantity) VALUES ('Murcielago',2001,'The Best Deals S.A.','Franklin',2);
|
||||||
|
END IF;
|
||||||
|
END$do$;
|
||||||
|
|
||||||
|
TRUNCATE TABLE test.products CASCADE;
|
||||||
|
INSERT INTO test.products (id, name) VALUES (1,'product-1'), (2,'product-2'), (3,'product-3');
|
||||||
|
|
||||||
|
TRUNCATE TABLE test.suppliers CASCADE;
|
||||||
|
INSERT INTO test.suppliers (id, name) VALUES (1,'supplier-1'), (2,'supplier-2'), (3, 'supplier-3');
|
||||||
|
|
||||||
|
TRUNCATE TABLE test.products_suppliers CASCADE;
|
||||||
|
INSERT INTO test.products_suppliers (product_id, supplier_id) VALUES (1,1), (1,2), (2,1), (2,3);
|
||||||
|
|
||||||
|
TRUNCATE TABLE test.trade_unions CASCADE;
|
||||||
|
INSERT INTO test.trade_unions (id, name) VALUES (1,'union-1'), (2,'union-2'), (3, 'union-3'), (4, 'union-4');
|
||||||
|
|
||||||
|
TRUNCATE TABLE test.suppliers_trade_unions CASCADE;
|
||||||
|
INSERT INTO test.suppliers_trade_unions (supplier_id, trade_union_id) VALUES (1,1), (1,2), (2,3), (2,4);
|
||||||
|
|
||||||
|
TRUNCATE TABLE test.client CASCADE;
|
||||||
|
INSERT INTO test.client (id,name) values (1,'Walmart'),(2,'Target'),(3,'Big Lots');
|
||||||
|
|
||||||
|
TRUNCATE TABLE test.contact CASCADE;
|
||||||
|
INSERT INTO test.contact (id,name, clientid) values (1,'Wally Walton',1),(2,'Wilma Wellers',1),(3,'Tabby Targo',2),(4,'Bobby Bots',3),(5,'Bonnie Bits',3),(6,'Billy Boats',3) returning *;
|
||||||
|
|
||||||
|
TRUNCATE TABLE test.clientinfo CASCADE;
|
||||||
|
INSERT INTO test.clientinfo (id,clientid, other) values (1,1,'123 Main St'),(2,2,'456 South 3rd St'),(3,3,'789 Palm Tree Ln');
|
||||||
|
|
||||||
|
TRUNCATE TABLE test.chores CASCADE;
|
||||||
|
INSERT INTO test.chores (id, name, done) values (1, 'take out the garbage', true), (2, 'do the laundry', false), (3, 'wash the dishes', null);
|
||||||
|
|||||||
Vendored
+36
@@ -115,6 +115,7 @@ GRANT ALL ON TABLE
|
|||||||
, pgrst_reserved_chars
|
, pgrst_reserved_chars
|
||||||
, authors_w_entities
|
, authors_w_entities
|
||||||
, openapi_types
|
, openapi_types
|
||||||
|
, openapi_defaults
|
||||||
, getallprojects_view
|
, getallprojects_view
|
||||||
, get_projects_above_view
|
, get_projects_above_view
|
||||||
, web_content
|
, web_content
|
||||||
@@ -150,6 +151,15 @@ GRANT ALL ON TABLE
|
|||||||
, schauspieler
|
, schauspieler
|
||||||
, filme
|
, filme
|
||||||
, rollen
|
, rollen
|
||||||
|
, products
|
||||||
|
, suppliers
|
||||||
|
, products_suppliers
|
||||||
|
, trade_unions
|
||||||
|
, suppliers_trade_unions
|
||||||
|
, client
|
||||||
|
, clientinfo
|
||||||
|
, contact
|
||||||
|
, chores
|
||||||
TO postgrest_test_anonymous;
|
TO postgrest_test_anonymous;
|
||||||
|
|
||||||
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
|
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
|
||||||
@@ -179,3 +189,29 @@ REVOKE EXECUTE ON FUNCTION privileged_hello(text) FROM PUBLIC; -- All functions
|
|||||||
GRANT EXECUTE ON FUNCTION privileged_hello(text) TO postgrest_test_author;
|
GRANT EXECUTE ON FUNCTION privileged_hello(text) TO postgrest_test_author;
|
||||||
|
|
||||||
GRANT USAGE ON SCHEMA test TO postgrest_test_default_role;
|
GRANT USAGE ON SCHEMA test TO postgrest_test_default_role;
|
||||||
|
|
||||||
|
|
||||||
|
DO $do$BEGIN
|
||||||
|
IF (SELECT current_setting('server_version_num')::INT >= 100000) THEN
|
||||||
|
GRANT ALL ON TABLE test.car_models TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.car_models_2021 TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.car_models_default TO postgrest_test_anonymous;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF (SELECT current_setting('server_version_num')::INT >= 110000) THEN
|
||||||
|
GRANT ALL ON TABLE test.car_brands TO postgrest_test_anonymous;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF (SELECT current_setting('server_version_num')::INT >= 120000) THEN
|
||||||
|
GRANT ALL ON TABLE test.car_model_sales TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.car_model_sales_202101 TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.car_model_sales_default TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.car_racers TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.car_dealers TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.car_dealers_springfield TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.car_dealers_default TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.car_models_car_dealers TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.car_models_car_dealers_10to20 TO postgrest_test_anonymous;
|
||||||
|
GRANT ALL ON TABLE test.car_models_car_dealers_default TO postgrest_test_anonymous;
|
||||||
|
END IF;
|
||||||
|
END$do$;
|
||||||
|
|||||||
Vendored
+279
-16
@@ -82,7 +82,10 @@ CREATE FUNCTION set_authors_only_owner() RETURNS trigger
|
|||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $$
|
AS $$
|
||||||
begin
|
begin
|
||||||
NEW.owner = current_setting('request.jwt.claim.id');
|
NEW.owner = case when current_setting('server_version_num')::int >= 140000
|
||||||
|
then current_setting('request.jwt.claims')::json->>'id'
|
||||||
|
else current_setting('request.jwt.claim.id')
|
||||||
|
end;
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
end
|
end
|
||||||
$$;
|
$$;
|
||||||
@@ -301,7 +304,10 @@ CREATE OR REPLACE FUNCTION switch_role() RETURNS void
|
|||||||
declare
|
declare
|
||||||
user_id text;
|
user_id text;
|
||||||
Begin
|
Begin
|
||||||
user_id = current_setting('request.jwt.claim.id')::text;
|
user_id = case when current_setting('server_version_num')::int >= 140000
|
||||||
|
then (current_setting('request.jwt.claims')::json->>'id')::text
|
||||||
|
else current_setting('request.jwt.claim.id')::text
|
||||||
|
end;
|
||||||
if user_id = '1'::text then
|
if user_id = '1'::text then
|
||||||
execute 'set local role postgrest_test_author';
|
execute 'set local role postgrest_test_author';
|
||||||
elseif user_id = '2'::text then
|
elseif user_id = '2'::text then
|
||||||
@@ -329,18 +335,42 @@ CREATE FUNCTION reveal_big_jwt() RETURNS TABLE (
|
|||||||
iss text, sub text, exp bigint,
|
iss text, sub text, exp bigint,
|
||||||
nbf bigint, iat bigint, jti text, "http://postgrest.com/foo" boolean
|
nbf bigint, iat bigint, jti text, "http://postgrest.com/foo" boolean
|
||||||
)
|
)
|
||||||
LANGUAGE sql SECURITY DEFINER
|
LANGUAGE plpgsql SECURITY DEFINER
|
||||||
STABLE
|
STABLE
|
||||||
AS $$
|
AS $$
|
||||||
SELECT current_setting('request.jwt.claim.iss') as iss,
|
BEGIN
|
||||||
current_setting('request.jwt.claim.sub') as sub,
|
-- JWT claims are set in JSON format since v14
|
||||||
current_setting('request.jwt.claim.exp')::bigint as exp,
|
IF (current_setting('server_version_num')::INT >= 140000) THEN
|
||||||
current_setting('request.jwt.claim.nbf')::bigint as nbf,
|
RETURN QUERY
|
||||||
current_setting('request.jwt.claim.iat')::bigint as iat,
|
SELECT current_setting('request.jwt.claims')::json->>'iss' as iss,
|
||||||
current_setting('request.jwt.claim.jti') as jti,
|
current_setting('request.jwt.claims')::json->>'sub' as sub,
|
||||||
-- role is not included in the claims list
|
(current_setting('request.jwt.claims')::json->>'exp')::bigint as exp,
|
||||||
current_setting('request.jwt.claim.http://postgrest.com/foo')::boolean
|
(current_setting('request.jwt.claims')::json->>'nbf')::bigint as nbf,
|
||||||
as "http://postgrest.com/foo";
|
(current_setting('request.jwt.claims')::json->>'iat')::bigint as iat,
|
||||||
|
current_setting('request.jwt.claims')::json->>'jti' as jti,
|
||||||
|
(current_setting('request.jwt.claims')::json->>'http://postgrest.com/foo')::boolean
|
||||||
|
as "http://postgrest.com/foo";
|
||||||
|
ELSE
|
||||||
|
RETURN QUERY
|
||||||
|
SELECT current_setting('request.jwt.claim.iss') as iss,
|
||||||
|
current_setting('request.jwt.claim.sub') as sub,
|
||||||
|
current_setting('request.jwt.claim.exp')::bigint as exp,
|
||||||
|
current_setting('request.jwt.claim.nbf')::bigint as nbf,
|
||||||
|
current_setting('request.jwt.claim.iat')::bigint as iat,
|
||||||
|
current_setting('request.jwt.claim.jti') as jti,
|
||||||
|
current_setting('request.jwt.claim.http://postgrest.com/foo')::boolean
|
||||||
|
as "http://postgrest.com/foo";
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE FUNCTION assert() RETURNS void
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
ASSERT false, 'bad thing';
|
||||||
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
|
||||||
@@ -1050,7 +1080,7 @@ create function test.ret_point_overloaded(x int, y int) returns test.point_2d as
|
|||||||
select row(x, y)::test.point_2d;
|
select row(x, y)::test.point_2d;
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
create function test.ret_point_overloaded(json) returns json as $$
|
create function test.ret_point_overloaded(x json) returns json as $$
|
||||||
select $1;
|
select $1;
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
@@ -1093,6 +1123,11 @@ create function test.get_guc_value(name text) returns text as $$
|
|||||||
select nullif(current_setting(name), '')::text;
|
select nullif(current_setting(name), '')::text;
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
|
-- Get the GUC values for Postgres v14.0 and up
|
||||||
|
create function test.get_guc_value(prefix text, name text) returns text as $$
|
||||||
|
select nullif(current_setting(prefix)::json->>name, '')::text;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
create table w_or_wo_comma_names ( name text );
|
create table w_or_wo_comma_names ( name text );
|
||||||
|
|
||||||
create table items_with_different_col_types (
|
create table items_with_different_col_types (
|
||||||
@@ -1203,6 +1238,14 @@ create function test.many_inout_params(INOUT num int, INOUT str text, INOUT b bo
|
|||||||
select num, str, b;
|
select num, str, b;
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
|
create function test.single_column_table_return () returns table (a text) AS $$
|
||||||
|
select 'A'::text;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create function test.multi_column_table_return () returns table (a text, b text) AS $$
|
||||||
|
select 'A'::text, 'B'::text;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
CREATE FUNCTION test.variadic_param(VARIADIC v TEXT[] DEFAULT '{}') RETURNS text[]
|
CREATE FUNCTION test.variadic_param(VARIADIC v TEXT[] DEFAULT '{}') RETURNS text[]
|
||||||
IMMUTABLE
|
IMMUTABLE
|
||||||
LANGUAGE SQL AS $$
|
LANGUAGE SQL AS $$
|
||||||
@@ -1289,6 +1332,22 @@ create or replace function test.overloaded(a text, b text, c text) returns text
|
|||||||
select a || b || c
|
select a || b || c
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded_default(opt_param text default 'Code w7') returns setof test.tasks as $$
|
||||||
|
select * from test.tasks where name like opt_param;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded_default(must_param int) returns jsonb as $$
|
||||||
|
select row_to_json(r)::jsonb from (select must_param as val) as r;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded_default(a int, opt_param text default 'Design IOS') returns setof test.tasks as $$
|
||||||
|
select * from test.tasks where name like opt_param and id > a;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded_default(a int, must_param int) returns jsonb as $$
|
||||||
|
select row_to_json(r)::jsonb from (select a, must_param as val) as r;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
create or replace function test.overloaded_html_form() returns setof int as $$
|
create or replace function test.overloaded_html_form() returns setof int as $$
|
||||||
values (1), (2), (3);
|
values (1), (2), (3);
|
||||||
$$ language sql;
|
$$ language sql;
|
||||||
@@ -1751,6 +1810,15 @@ CREATE TABLE test.openapi_types(
|
|||||||
"a_double_precision" double precision
|
"a_double_precision" double precision
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE test.openapi_defaults(
|
||||||
|
"text" text default 'default',
|
||||||
|
"boolean" boolean default false,
|
||||||
|
"integer" integer default 42,
|
||||||
|
"numeric" numeric default 42.2,
|
||||||
|
"date" date default '1900-01-01'::date,
|
||||||
|
"time" time default '13:00:00'::time without time zone
|
||||||
|
);
|
||||||
|
|
||||||
create function add_them(a integer, b integer)
|
create function add_them(a integer, b integer)
|
||||||
returns integer as $$
|
returns integer as $$
|
||||||
select a + b;
|
select a + b;
|
||||||
@@ -1767,8 +1835,13 @@ openapi json = $$
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
$$;
|
$$;
|
||||||
|
accept text;
|
||||||
begin
|
begin
|
||||||
case current_setting('request.header.accept', true)
|
accept = case when current_setting('server_version_num')::int >= 140000
|
||||||
|
then current_setting('request.headers', true)::json->>'accept'
|
||||||
|
else current_setting('request.header.accept', true)
|
||||||
|
end;
|
||||||
|
case accept
|
||||||
when 'application/openapi+json' then
|
when 'application/openapi+json' then
|
||||||
return openapi;
|
return openapi;
|
||||||
when 'application/json' then
|
when 'application/json' then
|
||||||
@@ -1941,9 +2014,15 @@ add constraint snd_shift foreign key (snd_shift_activity_id, snd_shift
|
|||||||
-- for a pre-request function
|
-- for a pre-request function
|
||||||
create or replace function custom_headers() returns void as $$
|
create or replace function custom_headers() returns void as $$
|
||||||
declare
|
declare
|
||||||
user_agent text := current_setting('request.header.user-agent', true);
|
user_agent text := case when current_setting('server_version_num')::int >= 140000
|
||||||
|
then current_setting('request.headers', true)::json->>'user-agent'
|
||||||
|
else current_setting('request.header.user-agent', true)
|
||||||
|
end;
|
||||||
req_path text := current_setting('request.path', true);
|
req_path text := current_setting('request.path', true);
|
||||||
req_accept text := current_setting('request.header.accept', true);
|
req_accept text := case when current_setting('server_version_num')::int >= 140000
|
||||||
|
then current_setting('request.headers', true)::json->>'accept'
|
||||||
|
else current_setting('request.header.accept', true)
|
||||||
|
end;
|
||||||
req_method text := current_setting('request.method', true);
|
req_method text := current_setting('request.method', true);
|
||||||
begin
|
begin
|
||||||
if user_agent similar to 'MSIE (6.0|7.0)' then
|
if user_agent similar to 'MSIE (6.0|7.0)' then
|
||||||
@@ -2185,3 +2264,187 @@ create table private.rollen (
|
|||||||
foreign key (film_id) references test.filme(id),
|
foreign key (film_id) references test.filme(id),
|
||||||
foreign key (rolle_id) references test.schauspieler(id)
|
foreign key (rolle_id) references test.schauspieler(id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Tables used for testing embedding between partitioned tables
|
||||||
|
|
||||||
|
do $do$begin
|
||||||
|
-- partitioned tables using the PARTITION syntax are supported from pg v10
|
||||||
|
if (select current_setting('server_version_num')::int >= 100000) then
|
||||||
|
create table test.car_models(
|
||||||
|
name varchar(64) not null,
|
||||||
|
year int not null
|
||||||
|
) partition by list (year);
|
||||||
|
|
||||||
|
comment on table test.car_models is
|
||||||
|
$$A partitioned table
|
||||||
|
|
||||||
|
A test for partitioned tables$$;
|
||||||
|
|
||||||
|
create table test.car_models_2021 partition of test.car_models
|
||||||
|
for values in (2021);
|
||||||
|
create table test.car_models_default partition of test.car_models
|
||||||
|
for values in (1981,1997,2001,2013);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- primary keys for partitioned tables are supported from pg v11
|
||||||
|
if (select current_setting('server_version_num')::int >= 110000) then
|
||||||
|
create table test.car_brands (
|
||||||
|
name varchar(64) primary key
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table test.car_models add primary key (name, year);
|
||||||
|
alter table test.car_models add column car_brand_name varchar(64) references test.car_brands(name);
|
||||||
|
end if;
|
||||||
|
|
||||||
|
-- foreign keys referencing partitioned tables are supported from pg v12
|
||||||
|
if (select current_setting('server_version_num')::int >= 120000) then
|
||||||
|
create table test.car_model_sales(
|
||||||
|
date varchar(64) not null,
|
||||||
|
quantity int not null,
|
||||||
|
car_model_name varchar(64),
|
||||||
|
car_model_year int,
|
||||||
|
primary key (date, car_model_name, car_model_year),
|
||||||
|
foreign key (car_model_name, car_model_year) references test.car_models (name, year)
|
||||||
|
) partition by range (date);
|
||||||
|
|
||||||
|
create table test.car_model_sales_202101 partition of test.car_model_sales
|
||||||
|
for values from ('2021-01-01') to ('2021-01-31');
|
||||||
|
|
||||||
|
create table test.car_model_sales_default partition of test.car_model_sales
|
||||||
|
default;
|
||||||
|
|
||||||
|
create table test.car_racers (
|
||||||
|
name varchar(64) not null primary key,
|
||||||
|
car_model_name varchar(64),
|
||||||
|
car_model_year int,
|
||||||
|
foreign key (car_model_name, car_model_year) references test.car_models (name, year)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table test.car_dealers (
|
||||||
|
name varchar(64) not null,
|
||||||
|
city varchar(64) not null,
|
||||||
|
primary key (name, city)
|
||||||
|
) partition by list (city);
|
||||||
|
|
||||||
|
create table test.car_dealers_springfield partition of test.car_dealers
|
||||||
|
for values in ('Springfield');
|
||||||
|
|
||||||
|
create table test.car_dealers_default partition of test.car_dealers
|
||||||
|
default;
|
||||||
|
|
||||||
|
create table test.car_models_car_dealers (
|
||||||
|
car_model_name varchar(64) not null,
|
||||||
|
car_model_year int not null,
|
||||||
|
car_dealer_name varchar(64) not null,
|
||||||
|
car_dealer_city varchar(64) not null,
|
||||||
|
quantity int not null,
|
||||||
|
foreign key (car_model_name, car_model_year) references test.car_models (name, year),
|
||||||
|
foreign key (car_dealer_name, car_dealer_city) references test.car_dealers (name, city)
|
||||||
|
) partition by range (quantity);
|
||||||
|
|
||||||
|
create table test.car_models_car_dealers_10to20 partition of test.car_models_car_dealers
|
||||||
|
for values from (10) to (20);
|
||||||
|
|
||||||
|
create table test.car_models_car_dealers_default partition of test.car_models_car_dealers
|
||||||
|
default;
|
||||||
|
end if;
|
||||||
|
end$do$;
|
||||||
|
|
||||||
|
create or replace function test.unnamed_json_param(json) returns json as $$
|
||||||
|
select $1;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.unnamed_text_param(text) returns text as $$
|
||||||
|
select $1;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.unnamed_bytea_param(bytea) returns bytea as $$
|
||||||
|
select $1::bytea;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.unnamed_int_param(int) returns int as $$
|
||||||
|
select $1;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded_unnamed_param(json) returns json as $$
|
||||||
|
select $1;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded_unnamed_param(bytea) returns bytea as $$
|
||||||
|
select $1;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded_unnamed_param(text) returns text as $$
|
||||||
|
select $1;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded_unnamed_param() returns int as $$
|
||||||
|
select 1;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded_unnamed_param(x int, y int) returns int as $$
|
||||||
|
select x + y;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded_unnamed_json_jsonb_param(json) returns json as $$
|
||||||
|
select $1;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded_unnamed_json_jsonb_param(jsonb) returns jsonb as $$
|
||||||
|
select $1;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.overloaded_unnamed_json_jsonb_param(x int, y int) returns int as $$
|
||||||
|
select x + y;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create table products(
|
||||||
|
id int primary key
|
||||||
|
, name text
|
||||||
|
);
|
||||||
|
|
||||||
|
create table suppliers(
|
||||||
|
id int primary key
|
||||||
|
, name text
|
||||||
|
);
|
||||||
|
|
||||||
|
create table products_suppliers(
|
||||||
|
product_id int references products(id),
|
||||||
|
supplier_id int references suppliers(id),
|
||||||
|
primary key (product_id, supplier_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table trade_unions(
|
||||||
|
id int primary key
|
||||||
|
, name text
|
||||||
|
);
|
||||||
|
|
||||||
|
create table suppliers_trade_unions(
|
||||||
|
supplier_id int references suppliers(id),
|
||||||
|
trade_union_id int references trade_unions(id),
|
||||||
|
primary key (supplier_id, trade_union_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE client (
|
||||||
|
id int primary key
|
||||||
|
, name text
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE contact (
|
||||||
|
id int primary key
|
||||||
|
, name text
|
||||||
|
, clientid int references client(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE clientinfo (
|
||||||
|
id serial primary key
|
||||||
|
, clientid int unique references client(id)
|
||||||
|
, other text
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE chores (
|
||||||
|
id int primary key
|
||||||
|
, name text
|
||||||
|
, done bool
|
||||||
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ db-schemas = "provided_through_alias"
|
|||||||
db-config = "false"
|
db-config = "false"
|
||||||
db-tx-end = "commit"
|
db-tx-end = "commit"
|
||||||
db-uri = "required"
|
db-uri = "required"
|
||||||
|
db-use-legacy-gucs = true
|
||||||
jwt-aud = ""
|
jwt-aud = ""
|
||||||
jwt-role-claim-key = ".\"aliased\""
|
jwt-role-claim-key = ".\"aliased\""
|
||||||
jwt-secret = ""
|
jwt-secret = ""
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ db-schemas = "required"
|
|||||||
db-config = "false"
|
db-config = "false"
|
||||||
db-tx-end = "commit"
|
db-tx-end = "commit"
|
||||||
db-uri = "required"
|
db-uri = "required"
|
||||||
|
db-use-legacy-gucs = true
|
||||||
jwt-aud = ""
|
jwt-aud = ""
|
||||||
jwt-role-claim-key = ".\"role\""
|
jwt-role-claim-key = ".\"role\""
|
||||||
jwt-secret = ""
|
jwt-secret = ""
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ db-schemas = "required"
|
|||||||
db-config = "false"
|
db-config = "false"
|
||||||
db-tx-end = "commit"
|
db-tx-end = "commit"
|
||||||
db-uri = "required"
|
db-uri = "required"
|
||||||
|
db-use-legacy-gucs = true
|
||||||
jwt-aud = ""
|
jwt-aud = ""
|
||||||
jwt-role-claim-key = ".\"role\""
|
jwt-role-claim-key = ".\"role\""
|
||||||
jwt-secret = ""
|
jwt-secret = ""
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ db-schemas = "required"
|
|||||||
db-config = "false"
|
db-config = "false"
|
||||||
db-tx-end = "commit"
|
db-tx-end = "commit"
|
||||||
db-uri = "required"
|
db-uri = "required"
|
||||||
|
db-use-legacy-gucs = true
|
||||||
jwt-aud = ""
|
jwt-aud = ""
|
||||||
jwt-role-claim-key = ".\"role\""
|
jwt-role-claim-key = ".\"role\""
|
||||||
jwt-secret = ""
|
jwt-secret = ""
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ db-schemas = "test,other_tenant1,other_tenant2"
|
|||||||
db-config = "true"
|
db-config = "true"
|
||||||
db-tx-end = "rollback-allow-override"
|
db-tx-end = "rollback-allow-override"
|
||||||
db-uri = "<REPLACED_WITH_DB_URI>"
|
db-uri = "<REPLACED_WITH_DB_URI>"
|
||||||
|
db-use-legacy-gucs = false
|
||||||
jwt-aud = "https://otherexample.org"
|
jwt-aud = "https://otherexample.org"
|
||||||
jwt-role-claim-key = ".\"other\".\"role\""
|
jwt-role-claim-key = ".\"other\".\"role\""
|
||||||
jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE"
|
jwt-secret = "ODERREALLYREALLYREALLYREALLYVERYSAFE"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ db-schemas = "test,tenant1,tenant2"
|
|||||||
db-config = "true"
|
db-config = "true"
|
||||||
db-tx-end = "commit-allow-override"
|
db-tx-end = "commit-allow-override"
|
||||||
db-uri = "<REPLACED_WITH_DB_URI>"
|
db-uri = "<REPLACED_WITH_DB_URI>"
|
||||||
|
db-use-legacy-gucs = false
|
||||||
jwt-aud = "https://example.org"
|
jwt-aud = "https://example.org"
|
||||||
jwt-role-claim-key = ".\"a\".\"role\""
|
jwt-role-claim-key = ".\"a\".\"role\""
|
||||||
jwt-secret = "OVERRIDEREALLYREALLYREALLYREALLYVERYSAFE"
|
jwt-secret = "OVERRIDEREALLYREALLYREALLYREALLYVERYSAFE"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ db-schemas = "multi,tenant,setup"
|
|||||||
db-config = "false"
|
db-config = "false"
|
||||||
db-tx-end = "rollback-allow-override"
|
db-tx-end = "rollback-allow-override"
|
||||||
db-uri = "tmp_db"
|
db-uri = "tmp_db"
|
||||||
|
db-use-legacy-gucs = false
|
||||||
jwt-aud = "https://postgrest.org"
|
jwt-aud = "https://postgrest.org"
|
||||||
jwt-role-claim-key = ".\"user\"[0].\"real-role\""
|
jwt-role-claim-key = ".\"user\"[0].\"real-role\""
|
||||||
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
|
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ db-schemas = "required"
|
|||||||
db-config = "true"
|
db-config = "true"
|
||||||
db-tx-end = "commit"
|
db-tx-end = "commit"
|
||||||
db-uri = "required"
|
db-uri = "required"
|
||||||
|
db-use-legacy-gucs = true
|
||||||
jwt-aud = ""
|
jwt-aud = ""
|
||||||
jwt-role-claim-key = ".\"role\""
|
jwt-role-claim-key = ".\"role\""
|
||||||
jwt-secret = ""
|
jwt-secret = ""
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ PGRST_DB_SCHEMAS: multi, tenant,setup
|
|||||||
PGRST_DB_CONFIG: false
|
PGRST_DB_CONFIG: false
|
||||||
PGRST_DB_TX_END: rollback-allow-override
|
PGRST_DB_TX_END: rollback-allow-override
|
||||||
PGRST_DB_URI: tmp_db
|
PGRST_DB_URI: tmp_db
|
||||||
|
PGRST_DB_EMBED_DEFAULT_JOIN: inner
|
||||||
|
PGRST_DB_USE_LEGACY_GUCS: false
|
||||||
PGRST_JWT_AUD: 'https://postgrest.org'
|
PGRST_JWT_AUD: 'https://postgrest.org'
|
||||||
PGRST_JWT_ROLE_CLAIM_KEY: '.user[0]."real-role"'
|
PGRST_JWT_ROLE_CLAIM_KEY: '.user[0]."real-role"'
|
||||||
PGRST_JWT_SECRET: c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5
|
PGRST_JWT_SECRET: c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ db-schemas = "multi, tenant,setup"
|
|||||||
db-config = "false"
|
db-config = "false"
|
||||||
db-tx-end = "rollback-allow-override"
|
db-tx-end = "rollback-allow-override"
|
||||||
db-uri = "tmp_db"
|
db-uri = "tmp_db"
|
||||||
|
db-use-legacy-gucs = false
|
||||||
jwt-aud = "https://postgrest.org"
|
jwt-aud = "https://postgrest.org"
|
||||||
jwt-role-claim-key = ".user[0].\"real-role\""
|
jwt-role-claim-key = ".user[0].\"real-role\""
|
||||||
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
|
jwt-secret = "c2VjdXJpdHl0aHJvdWdob2JzY3VyaXR5"
|
||||||
|
|||||||
@@ -179,3 +179,8 @@ invalidopenapimodes:
|
|||||||
- 'follow-'
|
- 'follow-'
|
||||||
- 'ignore-'
|
- 'ignore-'
|
||||||
- '.#$$%&$%/'
|
- '.#$$%&$%/'
|
||||||
|
|
||||||
|
invalidjointypes:
|
||||||
|
- 'left!'
|
||||||
|
- 'right'
|
||||||
|
- '.#$$%&$%/'
|
||||||
|
|||||||
@@ -709,6 +709,7 @@ def test_invalid_role_claim_key_notify_reload(defaultenv):
|
|||||||
|
|
||||||
postgrest.session.post("/rpc/reset_invalid_role_claim_key")
|
postgrest.session.post("/rpc/reset_invalid_role_claim_key")
|
||||||
|
|
||||||
|
|
||||||
def test_db_prepared_statements_enable(defaultenv):
|
def test_db_prepared_statements_enable(defaultenv):
|
||||||
"Should use prepared statements when the setting is enabled."
|
"Should use prepared statements when the setting is enabled."
|
||||||
|
|
||||||
@@ -716,6 +717,7 @@ def test_db_prepared_statements_enable(defaultenv):
|
|||||||
response = postgrest.session.post("/rpc/uses_prepared_statements")
|
response = postgrest.session.post("/rpc/uses_prepared_statements")
|
||||||
assert response.text == "true"
|
assert response.text == "true"
|
||||||
|
|
||||||
|
|
||||||
def test_db_prepared_statements_disable(defaultenv):
|
def test_db_prepared_statements_disable(defaultenv):
|
||||||
"Should not use any prepared statements when the setting is disabled."
|
"Should not use any prepared statements when the setting is disabled."
|
||||||
|
|
||||||
|
|||||||
+3
-16
@@ -69,23 +69,10 @@ PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "$PGUSER" --auth=trust \
|
|||||||
|
|
||||||
log "Starting the database cluster..."
|
log "Starting the database cluster..."
|
||||||
# Instead of listening on a local port, we will listen on a unix domain socket.
|
# Instead of listening on a local port, we will listen on a unix domain socket.
|
||||||
pg_ctl -l "$dblog" start -o "-F -c listen_addresses=\"\" -k $PGHOST" \
|
pg_ctl -l "$dblog" -w start -o "-F -c listen_addresses=\"\" -k $PGHOST" \
|
||||||
>> "$setuplog"
|
>> "$setuplog"
|
||||||
|
|
||||||
log "Waiting for the database cluster to be ready..."
|
|
||||||
# Waiting is required for older versions of Postgres (< 10).
|
|
||||||
until pg_isready >> "$setuplog"; do
|
|
||||||
sleep 0.1
|
|
||||||
done
|
|
||||||
|
|
||||||
stopped=0
|
|
||||||
stop() {
|
stop() {
|
||||||
# cleaning up once is enough; otherwise we get some errors
|
|
||||||
if [ $stopped -eq 1 ]; then
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
stopped=1
|
|
||||||
|
|
||||||
log "Stopping the database cluster..."
|
log "Stopping the database cluster..."
|
||||||
pg_ctl stop -m i >> "$setuplog"
|
pg_ctl stop -m i >> "$setuplog"
|
||||||
|
|
||||||
@@ -96,7 +83,7 @@ stop() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
trap stop sigint sigterm exit
|
trap stop EXIT
|
||||||
|
|
||||||
log "Loading fixtures..."
|
log "Loading fixtures..."
|
||||||
psql -v ON_ERROR_STOP=1 -f test/fixtures/load.sql >> "$setuplog"
|
psql -v ON_ERROR_STOP=1 -f test/fixtures/load.sql >> "$setuplog"
|
||||||
@@ -106,4 +93,4 @@ log "Done. Running command..."
|
|||||||
# make sure that the database is shut down and the temporary directory is
|
# make sure that the database is shut down and the temporary directory is
|
||||||
# deleted when the command is done. This is also why we don't `exec` the
|
# deleted when the command is done. This is also why we don't `exec` the
|
||||||
# command - our trap would be lost if we did that.
|
# command - our trap would be lost if we did that.
|
||||||
"$@"
|
("$@")
|
||||||
|
|||||||
Reference in New Issue
Block a user