Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f56bed2a75 | ||
|
|
af8e436732 | ||
|
|
98a29bee04 | ||
|
|
557285b659 | ||
|
|
81501aefa0 | ||
|
|
12c1d4a8e4 | ||
|
|
8aa7368786 | ||
|
|
9d4ff812c9 | ||
|
|
171dd313d9 | ||
|
|
fd24a7374b | ||
|
|
a525790c4c |
@@ -12,8 +12,6 @@ runs:
|
|||||||
using: composite
|
using: composite
|
||||||
steps:
|
steps:
|
||||||
- uses: cachix/install-nix-action@v18
|
- uses: cachix/install-nix-action@v18
|
||||||
with:
|
|
||||||
install_url: https://releases.nixos.org/nix/nix-2.13.3/install
|
|
||||||
- uses: cachix/cachix-action@v12
|
- uses: cachix/cachix-action@v12
|
||||||
with:
|
with:
|
||||||
name: postgrest
|
name: postgrest
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ set -euo pipefail
|
|||||||
# https://docs.github.com/en/rest/reference/checks#list-check-suites-for-a-git-reference
|
# https://docs.github.com/en/rest/reference/checks#list-check-suites-for-a-git-reference
|
||||||
|
|
||||||
cirrus_artifact_name=bin
|
cirrus_artifact_name=bin
|
||||||
gh_auth_header="Authorization: Bearer $GITHUB_TOKEN"
|
|
||||||
gh_accept_header="Accept: application/vnd.github.v3+json"
|
gh_accept_header="Accept: application/vnd.github.v3+json"
|
||||||
|
|
||||||
get_gh_check_runs_url() {
|
get_gh_check_runs_url() {
|
||||||
gh_checks_list_url="https://api.github.com/repos/$GITHUB_REPOSITORY/commits/$GITHUB_COMMIT/check-suites"
|
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 ..."
|
>&2 echo "Getting list of check-suites from $gh_checks_list_url ..."
|
||||||
curl --fail -H "$gh_auth_header" -H "$gh_accept_header" "$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'
|
| jq -r '.check_suites[] | select(.app.slug == "cirrus-ci") | .check_runs_url'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +21,7 @@ wait_for_cirrusci() {
|
|||||||
>&2 echo "Waiting to CirrusCI run to complete (two hours maximum)..."
|
>&2 echo "Waiting to CirrusCI run to complete (two hours maximum)..."
|
||||||
for _ in $(seq 1 120); do
|
for _ in $(seq 1 120); do
|
||||||
echo "Checking for CirrusCI task status at $gh_check_runs_url ..."
|
echo "Checking for CirrusCI task status at $gh_check_runs_url ..."
|
||||||
status=$(curl --fail -H "$gh_auth_header" "$gh_check_runs_url" | jq -r '.check_runs[] | .status')
|
status=$(curl --fail "$gh_check_runs_url" | jq -r '.check_runs[] | .status')
|
||||||
if [ "$status" == "completed" ]; then
|
if [ "$status" == "completed" ]; then
|
||||||
break
|
break
|
||||||
else
|
else
|
||||||
@@ -38,7 +37,7 @@ wait_for_cirrusci() {
|
|||||||
get_cirrus_taskid() {
|
get_cirrus_taskid() {
|
||||||
gh_check_runs_url="$(get_gh_check_runs_url)"
|
gh_check_runs_url="$(get_gh_check_runs_url)"
|
||||||
>&2 echo "Getting the CirrusCI task id from $gh_check_runs_url ..."
|
>&2 echo "Getting the CirrusCI task id from $gh_check_runs_url ..."
|
||||||
curl --fail -H "$gh_auth_header" -H "$gh_accept_header" "$gh_check_runs_url" \
|
curl --fail -H "$gh_accept_header" "$gh_check_runs_url" \
|
||||||
| jq -r '.check_runs[] | .external_id'
|
| jq -r '.check_runs[] | .external_id'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ PGRST_GITHUB_COMMIT="$1"
|
|||||||
SCRIPT_DIR="$2"
|
SCRIPT_DIR="$2"
|
||||||
|
|
||||||
DOCKER_BUILD_DIR="$SCRIPT_DIR/docker-env"
|
DOCKER_BUILD_DIR="$SCRIPT_DIR/docker-env"
|
||||||
CABAL_VERSION="3.6.0.0"
|
|
||||||
GHC_VERSION="9.2.4"
|
|
||||||
|
|
||||||
install_packages() {
|
install_packages() {
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
@@ -28,14 +26,13 @@ install_ghcup() {
|
|||||||
|
|
||||||
install_cabal() {
|
install_cabal() {
|
||||||
ghcup upgrade
|
ghcup upgrade
|
||||||
ghcup install cabal $CABAL_VERSION
|
ghcup install cabal 3.6.0.0
|
||||||
ghcup set cabal $CABAL_VERSION
|
ghcup set cabal 3.6.0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
install_ghc() {
|
install_ghc() {
|
||||||
ghcup upgrade
|
ghcup install ghc 8.10.7
|
||||||
ghcup install ghc $GHC_VERSION
|
ghcup set ghc 8.10.7
|
||||||
ghcup set ghc $GHC_VERSION
|
|
||||||
}
|
}
|
||||||
|
|
||||||
install_packages
|
install_packages
|
||||||
@@ -44,8 +41,8 @@ install_packages
|
|||||||
[ -f ~/.ghcup/env ] && source ~/.ghcup/env
|
[ -f ~/.ghcup/env ] && source ~/.ghcup/env
|
||||||
|
|
||||||
ghcup --version || install_ghcup
|
ghcup --version || install_ghcup
|
||||||
ghcup set cabal $CABAL_VERSION || install_cabal
|
cabal --version || install_cabal
|
||||||
ghcup set ghc $GHC_VERSION || install_ghc
|
ghc --version || install_ghc
|
||||||
|
|
||||||
cd ~/$SCRIPT_DIR
|
cd ~/$SCRIPT_DIR
|
||||||
|
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
name: Cachix
|
|
||||||
|
|
||||||
# This workflow serves to
|
|
||||||
# - keep cachix up to date with the main branch
|
|
||||||
# - incrementally update cachix for large dependency
|
|
||||||
# updates, e.g. after running postgrest-nixpkgs-upgrade,
|
|
||||||
# which can cause the main CI workflow to time out
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- rel-*
|
|
||||||
tags:
|
|
||||||
- v*
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
Seed-Cachix:
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: Linux
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
- os: MacOS
|
|
||||||
runs-on: macos-latest
|
|
||||||
name: Seed ${{ matrix.os }}
|
|
||||||
runs-on: ${{ matrix.runs-on }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Setup Nix Environment
|
|
||||||
uses: ./.github/actions/setup-nix
|
|
||||||
with:
|
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
|
||||||
|
|
||||||
- name: Install cachix tooling
|
|
||||||
run: |
|
|
||||||
nix-env -f default.nix -iA devTools.pushCachix.bin
|
|
||||||
postgrest-push-cachix
|
|
||||||
|
|
||||||
- name: Seed dynamic postgrest build
|
|
||||||
run: |
|
|
||||||
nix-build -A postgrestPackage
|
|
||||||
postgrest-push-cachix
|
|
||||||
|
|
||||||
- name: Seed style tools
|
|
||||||
run: |
|
|
||||||
nix-build -A style
|
|
||||||
postgrest-push-cachix
|
|
||||||
|
|
||||||
- name: Seed test tools
|
|
||||||
run: |
|
|
||||||
nix-build -A tests
|
|
||||||
postgrest-push-cachix
|
|
||||||
|
|
||||||
- name: Seed static toolchain
|
|
||||||
if: matrix.os == 'Linux'
|
|
||||||
run: |
|
|
||||||
nix-build -A packagesStatic.haskellPackages.hello
|
|
||||||
postgrest-push-cachix
|
|
||||||
|
|
||||||
- name: Seed static postgresql build (for libpq)
|
|
||||||
if: matrix.os == 'Linux'
|
|
||||||
run: |
|
|
||||||
nix-build -A packagesStatic.pkgs.postgresql
|
|
||||||
postgrest-push-cachix
|
|
||||||
|
|
||||||
- name: Seed static postgrest build
|
|
||||||
if: matrix.os == 'Linux'
|
|
||||||
run: |
|
|
||||||
nix-build -A postgrestStatic
|
|
||||||
postgrest-push-cachix
|
|
||||||
|
|
||||||
- name: Build and push everything to Cachix
|
|
||||||
run: |
|
|
||||||
nix-build
|
|
||||||
postgrest-push-cachix
|
|
||||||
+33
-57
@@ -46,7 +46,7 @@ jobs:
|
|||||||
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
|
||||||
run: postgrest-coverage
|
run: postgrest-coverage
|
||||||
- name: Upload coverage to codecov
|
- name: Upload coverage to codecov
|
||||||
uses: codecov/codecov-action@v3.1.3
|
uses: codecov/codecov-action@v3.1.1
|
||||||
with:
|
with:
|
||||||
files: ./coverage/codecov.json
|
files: ./coverage/codecov.json
|
||||||
|
|
||||||
@@ -84,7 +84,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Run IO tests
|
- name: Run IO tests
|
||||||
if: always()
|
if: always()
|
||||||
run: postgrest-with-postgresql-${{ matrix.pgVersion }} -f test/io/fixtures.sql postgrest-test-io -vv
|
run: postgrest-with-postgresql-${{ matrix.pgVersion }} -f test/io/fixtures.sql postgrest-test-io
|
||||||
|
|
||||||
|
- name: Run query cost tests
|
||||||
|
if: always()
|
||||||
|
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-querycost
|
||||||
|
|
||||||
|
|
||||||
Test-Memory-Nix:
|
Test-Memory-Nix:
|
||||||
@@ -108,6 +112,7 @@ jobs:
|
|||||||
- name: Setup Nix Environment
|
- name: Setup Nix Environment
|
||||||
uses: ./.github/actions/setup-nix
|
uses: ./.github/actions/setup-nix
|
||||||
with:
|
with:
|
||||||
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
tools: tests
|
tools: tests
|
||||||
|
|
||||||
- name: Build static executable
|
- name: Build static executable
|
||||||
@@ -130,18 +135,12 @@ jobs:
|
|||||||
path: postgrest-docker.tar.gz
|
path: postgrest-docker.tar.gz
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Build and push everything to Cachix (main branch only)
|
||||||
Build-Macos-Nix:
|
if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
name: Build MacOS (Nix)
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Setup Nix Environment
|
|
||||||
uses: ./.github/actions/setup-nix
|
|
||||||
|
|
||||||
- name: Build everything
|
|
||||||
run: |
|
run: |
|
||||||
nix-build
|
nix-build
|
||||||
|
nix-env -f default.nix -iA devTools
|
||||||
|
postgrest-push-cachix
|
||||||
|
|
||||||
|
|
||||||
Build-Stack:
|
Build-Stack:
|
||||||
@@ -203,8 +202,7 @@ jobs:
|
|||||||
- name: Get FreeBSD executable from CirrusCI
|
- name: Get FreeBSD executable from CirrusCI
|
||||||
env:
|
env:
|
||||||
# GITHUB_SHA does weird things for pull request, so we roll our own:
|
# GITHUB_SHA does weird things for pull request, so we roll our own:
|
||||||
GITHUB_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
|
GITHUB_COMMIT: ${{github.event.pull_request.head.sha || github.sha}}
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
run: .github/get_cirrusci_freebsd
|
run: .github/get_cirrusci_freebsd
|
||||||
- name: Save executable as artifact
|
- name: Save executable as artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
@@ -216,15 +214,12 @@ jobs:
|
|||||||
Build-Cabal:
|
Build-Cabal:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
ghc: ['9.2.4']
|
ghc: ['8.10.7', '9.2.4']
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
name: Build Linux (Cabal, GHC ${{ matrix.ghc }})
|
name: Build Linux (Cabal, GHC ${{ matrix.ghc }})
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Workaround runner image issue
|
|
||||||
# https://github.com/actions/runner-images/issues/7061
|
|
||||||
run: sudo chown -R "$USER" /usr/local/.ghcup
|
|
||||||
- name: ghcup
|
- name: ghcup
|
||||||
run: |
|
run: |
|
||||||
ghcup install ghc ${{ matrix.ghc }}
|
ghcup install ghc ${{ matrix.ghc }}
|
||||||
@@ -258,7 +253,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- id: Remote-Dir
|
- id: Remote-Dir
|
||||||
name: Unique directory name for the remote build
|
name: Unique directory name for the remote build
|
||||||
run: echo "remotepath=postgrest-build-$(uuidgen)" >> "$GITHUB_OUTPUT"
|
run: echo "::set-output name=remotepath::postgrest-build-$(uuidgen)"
|
||||||
- name: Copy script files to the remote server
|
- name: Copy script files to the remote server
|
||||||
uses: appleboy/scp-action@master
|
uses: appleboy/scp-action@master
|
||||||
with:
|
with:
|
||||||
@@ -295,7 +290,7 @@ jobs:
|
|||||||
- name: Extract downloaded binaries
|
- name: Extract downloaded binaries
|
||||||
run: tar -xvf result.tar.xz && rm result.tar.xz
|
run: tar -xvf result.tar.xz && rm result.tar.xz
|
||||||
- name: Save aarch64 executable as artifact
|
- name: Save aarch64 executable as artifact
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v2.3.1
|
||||||
with:
|
with:
|
||||||
name: postgrest-ubuntu-aarch64
|
name: postgrest-ubuntu-aarch64
|
||||||
path: result/postgrest
|
path: result/postgrest
|
||||||
@@ -331,14 +326,14 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
echo "Version to be released is $cabal_version"
|
echo "Version to be released is $cabal_version"
|
||||||
echo "version=$cabal_version" >> "$GITHUB_OUTPUT"
|
echo "::set-output name=version::$cabal_version"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$cabal_version" != *.*.*.* ]]; then
|
if [[ "$cabal_version" != *.*.*.* ]]; then
|
||||||
echo "Version is for a full release (version does not have four components)"
|
echo "Version is for a full release (version does not have four components)"
|
||||||
else
|
else
|
||||||
echo "Version is for a pre-release (version has four components, e.g., 1.1.1.1)"
|
echo "Version is for a pre-release (version has four components, e.g., 1.1.1.1)"
|
||||||
echo "isprerelease=1" >> "$GITHUB_OUTPUT"
|
echo "::set-output name=isprerelease::1"
|
||||||
fi
|
fi
|
||||||
- name: Identify changes from CHANGELOG.md
|
- name: Identify changes from CHANGELOG.md
|
||||||
run: |
|
run: |
|
||||||
@@ -429,6 +424,7 @@ jobs:
|
|||||||
name: Release on Docker Hub
|
name: Release on Docker Hub
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs:
|
needs:
|
||||||
|
- Build-Cabal-Arm
|
||||||
- Prepare-Release
|
- Prepare-Release
|
||||||
env:
|
env:
|
||||||
GITHUB_COMMIT: ${{ github.sha }}
|
GITHUB_COMMIT: ${{ github.sha }}
|
||||||
@@ -463,6 +459,18 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "Skipping pushing to 'latest' tag for v$VERSION pre-release..."
|
echo "Skipping pushing to 'latest' tag for v$VERSION pre-release..."
|
||||||
fi
|
fi
|
||||||
|
- name: Publish images for ARM builds on Docker Hub
|
||||||
|
uses: appleboy/ssh-action@master
|
||||||
|
env:
|
||||||
|
REMOTE_DIR: ${{ needs.Build-Cabal-Arm.outputs.remotepath }}
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_ARM_HOST }}
|
||||||
|
username: ubuntu
|
||||||
|
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
|
||||||
|
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
|
||||||
|
script_stop: true
|
||||||
|
envs: GITHUB_COMMIT,DOCKER_REPO,DOCKER_USER,DOCKER_PASS,REMOTE_DIR,VERSION,ISPRERELEASE
|
||||||
|
script: bash ~/$REMOTE_DIR/docker-publish.sh "$GITHUB_COMMIT" "$DOCKER_REPO" "$DOCKER_USER" "$DOCKER_PASS" "$REMOTE_DIR" "$VERSION" "$ISPRERELEASE"
|
||||||
# TODO: Enable dockerhub description update again, once a solution for the permission problem is found:
|
# TODO: Enable dockerhub description update again, once a solution for the permission problem is found:
|
||||||
# https://github.com/docker/hub-feedback/issues/1927
|
# https://github.com/docker/hub-feedback/issues/1927
|
||||||
# - name: Update descriptions on Docker Hub
|
# - name: Update descriptions on Docker Hub
|
||||||
@@ -476,49 +484,17 @@ jobs:
|
|||||||
# echo "Skipping updating description for pre-release..."
|
# echo "Skipping updating description for pre-release..."
|
||||||
# fi
|
# fi
|
||||||
|
|
||||||
Release-Docker-Arm:
|
|
||||||
name: Release Arm Builds on Docker Hub
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs:
|
|
||||||
- Build-Cabal-Arm
|
|
||||||
- Prepare-Release
|
|
||||||
- Release-Docker
|
|
||||||
env:
|
|
||||||
GITHUB_COMMIT: ${{ github.sha }}
|
|
||||||
DOCKER_REPO: postgrest
|
|
||||||
DOCKER_USER: stevechavez
|
|
||||||
DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
|
|
||||||
VERSION: ${{ needs.Prepare-Release.outputs.version }}
|
|
||||||
ISPRERELEASE: ${{ needs.Prepare-Release.outputs.isprerelease }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: Publish images for ARM builds on Docker Hub
|
|
||||||
uses: appleboy/ssh-action@master
|
|
||||||
env:
|
|
||||||
REMOTE_DIR: ${{ needs.Build-Cabal-Arm.outputs.remotepath }}
|
|
||||||
with:
|
|
||||||
host: ${{ secrets.SSH_ARM_HOST }}
|
|
||||||
username: ubuntu
|
|
||||||
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
|
|
||||||
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
|
|
||||||
script_stop: true
|
|
||||||
envs: GITHUB_COMMIT,DOCKER_REPO,DOCKER_USER,DOCKER_PASS,REMOTE_DIR,VERSION,ISPRERELEASE
|
|
||||||
script: bash ~/$REMOTE_DIR/docker-publish.sh "$GITHUB_COMMIT" "$DOCKER_REPO" "$DOCKER_USER" "$DOCKER_PASS" "$REMOTE_DIR" "$VERSION" "$ISPRERELEASE"
|
|
||||||
|
|
||||||
Clean-Arm-Server:
|
Clean-Arm-Server:
|
||||||
name: Remove copied files from server
|
name: Remove copied files from server
|
||||||
needs:
|
needs:
|
||||||
- Build-Cabal-Arm
|
- Build-Cabal-Arm
|
||||||
- Release-Docker-Arm
|
- Release-Docker
|
||||||
if: success() ||
|
if: ${{ always() && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || startsWith(github.ref, 'refs/heads/rel-')) }}
|
||||||
needs.Build-Cabal-Arm.result == 'failure' ||
|
|
||||||
needs.Build-Cabal-Arm.result == 'cancelled' ||
|
|
||||||
(needs.Build-Cabal-Arm.result == 'success' && !startsWith(github.ref, 'refs/tags/v'))
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
REMOTE_DIR: ${{ needs.Build-Cabal-Arm.outputs.remotepath }}
|
REMOTE_DIR: ${{ needs.Build-Cabal-Arm.outputs.remotepath }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v2.4.0
|
||||||
- name: Remove uploaded files from server
|
- name: Remove uploaded files from server
|
||||||
uses: appleboy/ssh-action@master
|
uses: appleboy/ssh-action@master
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ jobs:
|
|||||||
name: loadtest.md
|
name: loadtest.md
|
||||||
path: artifacts
|
path: artifacts
|
||||||
- name: Upload to GitHub Checks
|
- name: Upload to GitHub Checks
|
||||||
uses: LouisBrunner/checks-action@v1.6.0
|
uses: LouisBrunner/checks-action@v1.5.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
sha: ${{ github.event.workflow_run.head_sha }}
|
sha: ${{ github.event.workflow_run.head_sha }}
|
||||||
|
|||||||
@@ -46,14 +46,12 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
|||||||
|
|
||||||
## Lead Backers
|
## Lead Backers
|
||||||
|
|
||||||
- [Roboflow](https://github.com/roboflow)
|
|
||||||
- Evans Fernandes
|
- Evans Fernandes
|
||||||
- [Jan Sommer](https://github.com/nerfpops)
|
- [Jan Sommer](https://github.com/nerfpops)
|
||||||
- [Franz Gusenbauer](https://www.igutech.at/)
|
- [Franz Gusenbauer](https://www.igutech.at/)
|
||||||
|
|
||||||
## Backers
|
## Backers
|
||||||
|
|
||||||
- Zac Miller
|
|
||||||
- Tsingson Qin
|
- Tsingson Qin
|
||||||
- Michel Pelletier
|
- Michel Pelletier
|
||||||
- Jay Hannah
|
- Jay Hannah
|
||||||
|
|||||||
@@ -3,84 +3,6 @@
|
|||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
## Unreleased
|
|
||||||
|
|
||||||
## [11.0.1] - 2023-04-27
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- #2762, Fixes "permission denied for schema" error during schema cache load - @steve-chavez
|
|
||||||
- #2756, Fix bad error message on generated columns when using `Prefer: missing=default` - @steve-chavez
|
|
||||||
- #1139, Allow a 30 second skew for JWT validation - @steve-chavez
|
|
||||||
+ It used to be 1 second, which was too strict
|
|
||||||
|
|
||||||
## [11.0.0] - 2023-04-16
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- #1414, Add related orders - @steve-chavez
|
|
||||||
+ On a many-to-one or one-to-one relationship, you can order a parent by a child column `/projects?select=*,clients(*)&order=clients(name).desc.nullsfirst`
|
|
||||||
- #1233, #1907, #2566, Allow spreading embedded resources - @steve-chavez
|
|
||||||
+ On a many-to-one or one-to-one relationship, you can unnest a json object with `/projects?select=*,...clients(client_name:name)`
|
|
||||||
+ Allows including the join table columns when resource embedding
|
|
||||||
+ Allows disambiguating a recursive m2m embed
|
|
||||||
+ Allows disambiguating an embed that has a many-to-many relationship using two foreign keys on a junction
|
|
||||||
- #2340, Allow embedding without selecting any column - @steve-chavez
|
|
||||||
- #2563, Allow `is.null` or `not.is.null` on an embedded resource - @steve-chavez
|
|
||||||
+ Offers a more flexible replacement for `!inner`, e.g. `/projects?select=*,clients(*)&clients=not.is.null`
|
|
||||||
+ Allows doing an anti join, e.g. `/projects?select=*,clients(*)&clients=is.null`
|
|
||||||
+ Allows using or across related tables conditions
|
|
||||||
- #1100, Customizable OpenAPI title - @AnthonyFisi
|
|
||||||
- #2506, Add `server-trace-header` for tracing HTTP requests. - @steve-chavez
|
|
||||||
+ When the client sends the request header specified in the config it will be included in the response headers.
|
|
||||||
- #2694, Make `db-root-spec` stable. - @steve-chavez
|
|
||||||
+ This can be used to override the OpenAPI spec with a custom database function
|
|
||||||
- #1567, On bulk inserts, missing values can get the column DEFAULT by using the `Prefer: missing=default` header - @steve-chavez
|
|
||||||
- #2501, Allow filtering by`IS DISTINCT FROM` using the `isdistinct` operator, e.g. `/people?alias=isdistinct.foo`
|
|
||||||
- #1569, Allow `any/all` modifiers on the `eq,like,ilike,gt,gte,lt,lte,match,imatch` operators, e.g. `/tbl?id=eq(any).{1,2,3}` - @steve-chavez
|
|
||||||
- This converts the input into an array type
|
|
||||||
- #2561, Configurable role settings - @steve-chavez
|
|
||||||
- Database roles that are members of the connection role get their settings applied, e.g. doing
|
|
||||||
`ALTER ROLE anon SET statement_timeout TO '5s'` will result in that `statement_timeout` getting applied for that role.
|
|
||||||
- Works when switching roles when a JWT is sent
|
|
||||||
- Settings can be reloaded with `NOTIFY pgrst, 'reload config'`.
|
|
||||||
- #2468, Configurable transaction isolation level with `default_transaction_isolation` - @steve-chavez
|
|
||||||
- Can be set per function `create function .. set default_transaction_isolation = 'repeatable read'`
|
|
||||||
- Or per role `alter role .. set default_transaction_isolation = 'serializable'`
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- #2651, Add the missing `get` path item for RPCs to the OpenAPI output - @laurenceisla
|
|
||||||
- #2648, Fix inaccurate error codes with new ones - @laurenceisla
|
|
||||||
+ `PGRST204`: Column is not found
|
|
||||||
+ `PGRST003`: Timed out when acquiring connection to db
|
|
||||||
- #1652, Fix function call with arguments not inlining - @steve-chavez
|
|
||||||
- #2705, Fix bug when using the `Range` header on `PATCH/DELETE` - @laurenceisla
|
|
||||||
+ Fix the`"message": "syntax error at or near \"RETURNING\""` error
|
|
||||||
+ Fix doing a limited update/delete when an `order` query parameter was present
|
|
||||||
- #2742, Fix db settings and pg version queries not getting prepared - @steve-chavez
|
|
||||||
- #2618, Fix `PATCH` requests not recognizing embedded filters and using the top-level resource instead - @steve-chavez
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- #2705, The `Range` header is now only considered on `GET` requests and is ignored for any other method - @laurenceisla
|
|
||||||
+ Other methods should use the `limit/offset` query parameters for sub-ranges
|
|
||||||
+ `PUT` requests no longer return an error when this header is present (using `limit/offset` still triggers the error)
|
|
||||||
- #2733, Remove bulk RPC call with the `Prefer: params=multiple-objects` header. A function with a JSON array or object parameter should be used instead.
|
|
||||||
|
|
||||||
## [10.2.0] - 2023-04-12
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- #2663, Limit maximal postgresql connection lifetime - @robx
|
|
||||||
+ New option `db-pool-max-lifetime` (default 30m)
|
|
||||||
+ `db-pool-acquisition-timeout` is no longer optional and defaults to 10s
|
|
||||||
+ Fixes postgresql resource leak with long-lived connections (#2638)
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- #2667, Fix `db-pool-acquisition-timeout` not logging to stderr when the timeout is reached - @steve-chavez
|
|
||||||
|
|
||||||
## [10.1.2] - 2023-02-01
|
## [10.1.2] - 2023-02-01
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -101,7 +23,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
- #2548, Fix regression when embedding views with partial references to multi column FKs - @wolfgangwalther
|
- #2548, Fix regression when embedding views with partial references to multi column FKs - @wolfgangwalther
|
||||||
- #2558, Fix regression when requesting limit=0 and `db-max-row` is set - @laurenceisla
|
- #2558, Fix regression when requesting limit=0 and `db-max-row` is set - @laurenceisla
|
||||||
- #2542, Return a clear error without hitting the database when trying to update or insert an unknown column with `?columns` - @aljungberg
|
|
||||||
|
|
||||||
## [10.1.0] - 2022-10-28
|
## [10.1.0] - 2022-10-28
|
||||||
|
|
||||||
|
|||||||
@@ -17,4 +17,4 @@ packages: .
|
|||||||
source-repository-package
|
source-repository-package
|
||||||
type: git
|
type: git
|
||||||
location: https://github.com/PostgREST/postgresql-libpq.git
|
location: https://github.com/PostgREST/postgresql-libpq.git
|
||||||
tag: 890a0a16cf57dd401420fdc6c7d576fb696003bc
|
tag: 33ff97db570b5b432255f5f24a68db51453f6eb8
|
||||||
|
|||||||
+11
-18
@@ -41,7 +41,6 @@ let
|
|||||||
allOverlays.postgresql-legacy
|
allOverlays.postgresql-legacy
|
||||||
allOverlays.postgresql-future
|
allOverlays.postgresql-future
|
||||||
(allOverlays.haskell-packages { inherit compiler; })
|
(allOverlays.haskell-packages { inherit compiler; })
|
||||||
allOverlays.slocat
|
|
||||||
];
|
];
|
||||||
|
|
||||||
# Evaluated expression of the Nixpkgs repository.
|
# Evaluated expression of the Nixpkgs repository.
|
||||||
@@ -66,17 +65,11 @@ let
|
|||||||
postgrest =
|
postgrest =
|
||||||
pkgs.haskell.packages."${compiler}".callCabal2nix name src { };
|
pkgs.haskell.packages."${compiler}".callCabal2nix name src { };
|
||||||
|
|
||||||
# Functionality that derives a fully static Haskell package based on
|
# Function that derives a fully static Haskell package based on
|
||||||
# nh2/static-haskell-nix
|
# nh2/static-haskell-nix
|
||||||
staticHaskellPackage =
|
staticHaskellPackage =
|
||||||
import nix/static-haskell-package.nix { inherit nixpkgs system compiler patches allOverlays; };
|
import nix/static-haskell-package.nix { inherit nixpkgs system compiler patches allOverlays; };
|
||||||
|
|
||||||
# Static executable.
|
|
||||||
postgrestStatic =
|
|
||||||
lib.justStaticExecutables (lib.dontCheck (staticHaskellPackage name src).package);
|
|
||||||
|
|
||||||
packagesStatic = (staticHaskellPackage name src).survey;
|
|
||||||
|
|
||||||
# Options passed to cabal in dev tools and tests
|
# Options passed to cabal in dev tools and tests
|
||||||
devCabalOptions =
|
devCabalOptions =
|
||||||
"-f dev --test-show-detail=direct";
|
"-f dev --test-show-detail=direct";
|
||||||
@@ -101,6 +94,10 @@ rec {
|
|||||||
postgrestPackage =
|
postgrestPackage =
|
||||||
lib.dontCheck postgrest;
|
lib.dontCheck postgrest;
|
||||||
|
|
||||||
|
# Static executable.
|
||||||
|
postgrestStatic =
|
||||||
|
lib.justStaticExecutables (lib.dontCheck (staticHaskellPackage name src));
|
||||||
|
|
||||||
# Profiled dynamic executable.
|
# Profiled dynamic executable.
|
||||||
postgrestProfiled =
|
postgrestProfiled =
|
||||||
lib.enableExecutableProfiling (
|
lib.enableExecutableProfiling (
|
||||||
@@ -122,13 +119,14 @@ rec {
|
|||||||
cabalTools =
|
cabalTools =
|
||||||
pkgs.callPackage nix/tools/cabalTools.nix { inherit devCabalOptions postgrest; };
|
pkgs.callPackage nix/tools/cabalTools.nix { inherit devCabalOptions postgrest; };
|
||||||
|
|
||||||
withTools =
|
|
||||||
pkgs.callPackage nix/tools/withTools.nix { inherit cabalTools devCabalOptions postgresqlVersions postgrest; };
|
|
||||||
|
|
||||||
# Development tools.
|
# Development tools.
|
||||||
devTools =
|
devTools =
|
||||||
pkgs.callPackage nix/tools/devTools.nix { inherit tests style devCabalOptions hsie withTools; };
|
pkgs.callPackage nix/tools/devTools.nix { inherit tests style devCabalOptions hsie withTools; };
|
||||||
|
|
||||||
|
# Docker images and loading script.
|
||||||
|
docker =
|
||||||
|
pkgs.callPackage nix/tools/docker { postgrest = postgrestStatic; };
|
||||||
|
|
||||||
# Load testing tools.
|
# Load testing tools.
|
||||||
loadtest =
|
loadtest =
|
||||||
pkgs.callPackage nix/tools/loadtest.nix { inherit withTools; };
|
pkgs.callPackage nix/tools/loadtest.nix { inherit withTools; };
|
||||||
@@ -157,12 +155,7 @@ rec {
|
|||||||
inherit (pkgs.haskell.packages."${compiler}") hpc-codecov;
|
inherit (pkgs.haskell.packages."${compiler}") hpc-codecov;
|
||||||
inherit (pkgs.haskell.packages."${compiler}") weeder;
|
inherit (pkgs.haskell.packages."${compiler}") weeder;
|
||||||
};
|
};
|
||||||
} // pkgs.lib.optionalAttrs pkgs.stdenv.isLinux rec {
|
|
||||||
# Static executable.
|
|
||||||
inherit postgrestStatic;
|
|
||||||
inherit packagesStatic;
|
|
||||||
|
|
||||||
# Docker images and loading script.
|
withTools =
|
||||||
docker =
|
pkgs.callPackage nix/tools/withTools.nix { inherit devCabalOptions postgresqlVersions postgrest; };
|
||||||
pkgs.callPackage nix/tools/docker { postgrest = postgrestStatic; };
|
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-6
@@ -204,16 +204,16 @@ postgrest-loadtest
|
|||||||
# You can loadtest comparing to a different branch
|
# You can loadtest comparing to a different branch
|
||||||
postgrest-loadtest-against master
|
postgrest-loadtest-against master
|
||||||
|
|
||||||
# You can simulate latency client/postgrest and postgrest/database
|
|
||||||
PGRST_DELAY=5ms PGDELAY=5ms postgrest-loadtest
|
|
||||||
|
|
||||||
# You can build postgrest directly with cabal for faster iteration
|
|
||||||
PGRST_BUILD_CABAL=1 postgrest-loadtest
|
|
||||||
|
|
||||||
# Produce a markdown report to be used on CI
|
# Produce a markdown report to be used on CI
|
||||||
postgrest-loadtest-report
|
postgrest-loadtest-report
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Our query cost tests ensure that our generated queries don't surpass a threshold EXPLAIN cost.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
postgrest-test-querycost
|
||||||
|
```
|
||||||
|
|
||||||
doctests for some of our modules are also available:
|
doctests for some of our modules are also available:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Pinned version of Nixpkgs, generated with postgrest-nixpkgs-upgrade.
|
# Pinned version of Nixpkgs, generated with postgrest-nixpkgs-upgrade.
|
||||||
{
|
{
|
||||||
date = "2023-03-25";
|
date = "2022-10-28";
|
||||||
rev = "dbf5322e93bcc6cfc52268367a8ad21c09d76fea";
|
rev = "f44ba1be526c8da9e79a5759feca2365204003f6";
|
||||||
tarballHash = "0lwk4v9dkvd28xpqch0b0jrac4xl9lwm6snrnzx8k5lby72kmkng";
|
tarballHash = "0npbwsdjw88py5w2pjflwh94wgi4jmnmls0k1n7q8m6h94w1y1ps";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,4 @@
|
|||||||
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;
|
postgresql-future = import ./postgresql-future.nix;
|
||||||
slocat = import ./slocat.nix;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,33 +29,33 @@ let
|
|||||||
# To fill in the sha256:
|
# To fill in the sha256:
|
||||||
# update-nix-fetchgit nix/overlays/haskell-packages.nix
|
# update-nix-fetchgit nix/overlays/haskell-packages.nix
|
||||||
|
|
||||||
|
hashtables = lib.dontCheck prev.hashtables_1_3_1;
|
||||||
|
hasql = lib.dontCheck prev.hasql_1_6_1_4;
|
||||||
|
hasql-dynamic-statements = lib.dontCheck prev.hasql-dynamic-statements_0_3_1_2;
|
||||||
|
hasql-pool = lib.dontCheck
|
||||||
|
(prev.callHackageDirect
|
||||||
|
{
|
||||||
|
pkg = "hasql-pool";
|
||||||
|
ver = "0.8.0.6";
|
||||||
|
sha256 = "sha256-2u/cwPk8XfXffaDRzGeyzhL+9k2+2T4b8bGOZwz8AX0=";
|
||||||
|
}
|
||||||
|
{ });
|
||||||
|
hasql-transaction = lib.dontCheck prev.hasql-transaction_1_0_1_2;
|
||||||
|
isomorphism-class = lib.unmarkBroken prev.isomorphism-class;
|
||||||
|
lens = lib.dontCheck prev.lens_5_2;
|
||||||
|
postgresql-binary = lib.dontCheck prev.postgresql-binary_0_13_1;
|
||||||
|
text-builder = lib.dontCheck prev.text-builder_0_6_7;
|
||||||
|
text-builder-dev = lib.dontCheck prev.text-builder-dev_0_3_3;
|
||||||
|
|
||||||
postgresql-libpq = lib.dontCheck
|
postgresql-libpq = lib.dontCheck
|
||||||
(prev.callCabal2nix "postgresql-libpq"
|
(prev.callCabal2nix "postgresql-libpq"
|
||||||
(super.fetchFromGitHub {
|
(super.fetchFromGitHub {
|
||||||
owner = "PostgREST";
|
owner = "PostgREST";
|
||||||
repo = "postgresql-libpq";
|
repo = "postgresql-libpq";
|
||||||
rev = "890a0a16cf57dd401420fdc6c7d576fb696003bc"; # master
|
rev = "cef92cb4c07b56568dffdbf4b719258b82183119"; # master
|
||||||
sha256 = "1wmyhldk0k14y8whp1p4akrkqxf5snh8qsbm7fv5f7kz95nyffd0";
|
sha256 = "0r59klrz47qcnd22s47h612mlz3jbg40wwalfj3f6djwg0cdyr85";
|
||||||
})
|
})
|
||||||
{ });
|
{ });
|
||||||
|
|
||||||
hasql-notifications = lib.dontCheck
|
|
||||||
(prev.callHackageDirect
|
|
||||||
{
|
|
||||||
pkg = "hasql-notifications";
|
|
||||||
ver = "0.2.0.4";
|
|
||||||
sha256 = "sha256-fm1xiDyvDkb5WLOJ73/s8wrWEW23XFS7luAv2brfr8I=";
|
|
||||||
}
|
|
||||||
{ });
|
|
||||||
|
|
||||||
hasql-pool = lib.dontCheck
|
|
||||||
(prev.callHackageDirect
|
|
||||||
{
|
|
||||||
pkg = "hasql-pool";
|
|
||||||
ver = "0.9";
|
|
||||||
sha256 = "sha256-5UshbbaBVY8eJ/9VagNVVxonRwMcd7UmGqDc35pJNFY=";
|
|
||||||
}
|
|
||||||
{ });
|
|
||||||
} // extraOverrides final prev;
|
} // extraOverrides final prev;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
final: prev:
|
|
||||||
{
|
|
||||||
slocat = prev.buildGoModule {
|
|
||||||
name = "slocat";
|
|
||||||
src = prev.fetchFromGitHub {
|
|
||||||
owner = "robx";
|
|
||||||
repo = "slocat";
|
|
||||||
rev = "52e7512c6029fd00483e41ccce260a3b4b9b3b64";
|
|
||||||
sha256 = "sha256-qn6luuh5wqREu3s8RfuMCP5PKdS2WdwPrujRYTpfzQ8=";
|
|
||||||
};
|
|
||||||
vendorSha256 = "sha256-pQpattmS9VmO3ZIQUFn66az8GSmB4IvYhTTCFn6SUmo=";
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -59,7 +59,4 @@ let
|
|||||||
survey =
|
survey =
|
||||||
import "${patched-static-haskell-nix}/survey" { inherit normalPkgs compiler defaultCabalPackageVersionComingWithGhc; };
|
import "${patched-static-haskell-nix}/survey" { inherit normalPkgs compiler defaultCabalPackageVersionComingWithGhc; };
|
||||||
in
|
in
|
||||||
{
|
survey.haskellPackages."${name}"
|
||||||
inherit survey;
|
|
||||||
package = survey.haskellPackages."${name}";
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -37,18 +37,12 @@ let
|
|||||||
checkedShellScript
|
checkedShellScript
|
||||||
{
|
{
|
||||||
name = "postgrest-run";
|
name = "postgrest-run";
|
||||||
docs = "Run PostgREST after building it interactively with cabal-install";
|
docs = "Run PostgREST after buidling it interactively with cabal-install";
|
||||||
args =
|
args = [ "ARG_LEFTOVERS([PostgREST arguments])" ];
|
||||||
[
|
|
||||||
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [PostgREST anonymous role])"
|
|
||||||
"ARG_LEFTOVERS([PostgREST arguments])"
|
|
||||||
];
|
|
||||||
inRootDir = true;
|
inRootDir = true;
|
||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
export PGRST_DB_ANON_ROLE
|
|
||||||
|
|
||||||
exec ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
|
exec ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
|
||||||
postgrest "''${_arg_leftovers[@]}"
|
postgrest "''${_arg_leftovers[@]}"
|
||||||
'';
|
'';
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ let
|
|||||||
}
|
}
|
||||||
''
|
''
|
||||||
${tests}/bin/postgrest-test-spec
|
${tests}/bin/postgrest-test-spec
|
||||||
|
${tests}/bin/postgrest-test-querycost
|
||||||
${tests}/bin/postgrest-test-doctests
|
${tests}/bin/postgrest-test-doctests
|
||||||
${tests}/bin/postgrest-test-io
|
${tests}/bin/postgrest-test-io
|
||||||
${style}/bin/postgrest-lint
|
${style}/bin/postgrest-lint
|
||||||
@@ -164,7 +165,6 @@ let
|
|||||||
# The following unsets all GIT_ variables.
|
# The following unsets all GIT_ variables.
|
||||||
unset "''${!GIT_@}"
|
unset "''${!GIT_@}"
|
||||||
|
|
||||||
# shellcheck disable=SC2317
|
|
||||||
function restore () {
|
function restore () {
|
||||||
ref="$(git stash list --format=format:%gD --grep "$1" -n1)"
|
ref="$(git stash list --format=format:%gD --grep "$1" -n1)"
|
||||||
# this will avoid merge conflicts when applying the stash
|
# this will avoid merge conflicts when applying the stash
|
||||||
@@ -304,5 +304,4 @@ buildToolbox
|
|||||||
hsieGraphModules
|
hsieGraphModules
|
||||||
hsieGraphSymbols
|
hsieGraphSymbols
|
||||||
];
|
];
|
||||||
extra = { inherit pushCachix; };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,14 +56,11 @@ let
|
|||||||
export PGRST_LOG_LEVEL="crit"
|
export PGRST_LOG_LEVEL="crit"
|
||||||
|
|
||||||
mkdir -p "$(dirname "$_arg_output")"
|
mkdir -p "$(dirname "$_arg_output")"
|
||||||
abs_output="$(realpath "$_arg_output")"
|
|
||||||
|
|
||||||
# shellcheck disable=SC2145
|
# shellcheck disable=SC2145
|
||||||
${withTools.withPg} --fixtures "$_arg_testdir"/fixtures.sql \
|
${withTools.withPg} --fixtures "$_arg_testdir"/fixtures.sql \
|
||||||
${withTools.withSlowPg} \
|
|
||||||
${withTools.withPgrst} \
|
${withTools.withPgrst} \
|
||||||
${withTools.withSlowPgrst} \
|
sh -c "cd \"$_arg_testdir\" && ${runner} -targets targets.http -output \"$_arg_output\" \"''${_arg_leftovers[@]}\""
|
||||||
sh -c "cd \"$_arg_testdir\" && ${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
|
||||||
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|||||||
@@ -72,15 +72,14 @@ let
|
|||||||
today_date="$(date '+%Y%m%d')"
|
today_date="$(date '+%Y%m%d')"
|
||||||
today_date_for_changelog="$(date '+%Y-%m-%d')"
|
today_date_for_changelog="$(date '+%Y-%m-%d')"
|
||||||
bump_pre="$major.$minor.$patch.$today_date"
|
bump_pre="$major.$minor.$patch.$today_date"
|
||||||
bump_pre_minor="$major.$((minor+1)).0.$today_date"
|
|
||||||
bump_patch="$major.$minor.$((patch+1))"
|
bump_patch="$major.$minor.$((patch+1))"
|
||||||
bump_minor="$major.$((minor+1)).0"
|
bump_minor="$major.$((minor+1)).0"
|
||||||
bump_major="$((major+1)).0.0"
|
bump_major="$((major+1)).0.0"
|
||||||
|
|
||||||
PS3="Please select the new version: "
|
PS3="Please select the new version: "
|
||||||
select new_version in "$bump_pre" "$bump_pre_minor" "$bump_patch" "$bump_minor" "$bump_major"; do
|
select new_version in "$bump_pre" "$bump_patch" "$bump_minor" "$bump_major"; do
|
||||||
case "$REPLY" in
|
case "$REPLY" in
|
||||||
1|2|3|4|5)
|
1|2|3|4)
|
||||||
echo "Selected $new_version"
|
echo "Selected $new_version"
|
||||||
break
|
break
|
||||||
;;
|
;;
|
||||||
@@ -96,7 +95,7 @@ let
|
|||||||
echo "Committing ..."
|
echo "Committing ..."
|
||||||
git add postgrest.cabal > /dev/null
|
git add postgrest.cabal > /dev/null
|
||||||
|
|
||||||
if [[ "$new_version" != "$bump_pre" && "$new_version" != "$bump_pre_minor" ]]; then
|
if [[ "$new_version" != "$bump_pre" ]]; then
|
||||||
echo "Updating CHANGELOG.md ..."
|
echo "Updating CHANGELOG.md ..."
|
||||||
sed -i -E "s/Unreleased/&\n\n## [$new_version] - $today_date_for_changelog/" CHANGELOG.md > /dev/null
|
sed -i -E "s/Unreleased/&\n\n## [$new_version] - $today_date_for_changelog/" CHANGELOG.md > /dev/null
|
||||||
git add CHANGELOG.md > /dev/null
|
git add CHANGELOG.md > /dev/null
|
||||||
@@ -107,7 +106,7 @@ let
|
|||||||
echo "Tagging ..."
|
echo "Tagging ..."
|
||||||
git tag "v$new_version" > /dev/null
|
git tag "v$new_version" > /dev/null
|
||||||
|
|
||||||
trap "echo Remote not found. Please push manually ..." ERR
|
trap "Couldn't find remote. Please push manually ..." ERR
|
||||||
remote="$(git remote -v | grep PostgREST/postgrest | grep push | cut -f1)"
|
remote="$(git remote -v | grep PostgREST/postgrest | grep push | cut -f1)"
|
||||||
trap "" ERR
|
trap "" ERR
|
||||||
|
|
||||||
|
|||||||
+18
-2
@@ -32,6 +32,18 @@ let
|
|||||||
test:spec -- "''${_arg_leftovers[@]}"
|
test:spec -- "''${_arg_leftovers[@]}"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
testQuerycost =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-test-querycost";
|
||||||
|
docs = "Run the Haskell test suite for query costs";
|
||||||
|
inRootDir = true;
|
||||||
|
withEnv = postgrest.env;
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${withTools.withPg} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:querycost
|
||||||
|
'';
|
||||||
|
|
||||||
testDoctests =
|
testDoctests =
|
||||||
checkedShellScript
|
checkedShellScript
|
||||||
{
|
{
|
||||||
@@ -128,7 +140,7 @@ 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
|
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec test:querycost
|
||||||
|
|
||||||
(
|
(
|
||||||
trap 'echo Found dead code: Check file list above.' ERR ;
|
trap 'echo Found dead code: Check file list above.' ERR ;
|
||||||
@@ -143,11 +155,14 @@ let
|
|||||||
HPCTIXFILE="$tmpdir"/spec.tix \
|
HPCTIXFILE="$tmpdir"/spec.tix \
|
||||||
${withTools.withPg} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec
|
${withTools.withPg} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec
|
||||||
|
|
||||||
|
HPCTIXFILE="$tmpdir"/querycost.tix \
|
||||||
|
${withTools.withPg} ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:querycost
|
||||||
|
|
||||||
# Note: No coverage for doctests, as doctests leverage GHCi and GHCi does not support hpc
|
# 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 \
|
${ghc}/bin/hpc sum --union --exclude=Paths_postgrest --output="$tmpdir"/tests.tix \
|
||||||
"$tmpdir"/io*.tix "$tmpdir"/spec.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
|
||||||
@@ -219,6 +234,7 @@ buildToolbox
|
|||||||
tools =
|
tools =
|
||||||
[
|
[
|
||||||
testSpec
|
testSpec
|
||||||
|
testQuerycost
|
||||||
testDoctests
|
testDoctests
|
||||||
testSpecIdempotence
|
testSpecIdempotence
|
||||||
testIO
|
testIO
|
||||||
|
|||||||
+15
-115
@@ -1,7 +1,6 @@
|
|||||||
{ bash-completion
|
{ bash-completion
|
||||||
, buildToolbox
|
, buildToolbox
|
||||||
, cabal-install
|
, cabal-install
|
||||||
, cabalTools
|
|
||||||
, checkedShellScript
|
, checkedShellScript
|
||||||
, curl
|
, curl
|
||||||
, devCabalOptions
|
, devCabalOptions
|
||||||
@@ -9,20 +8,15 @@
|
|||||||
, lib
|
, lib
|
||||||
, postgresqlVersions
|
, postgresqlVersions
|
||||||
, postgrest
|
, postgrest
|
||||||
, slocat
|
|
||||||
, writeText
|
, writeText
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
withTmpDb =
|
withTmpDb =
|
||||||
{ name, postgresql }:
|
{ name, postgresql }:
|
||||||
let
|
|
||||||
commandName = "postgrest-with-${name}";
|
|
||||||
superuserRole = "postgres";
|
|
||||||
in
|
|
||||||
checkedShellScript
|
checkedShellScript
|
||||||
{
|
{
|
||||||
name = commandName;
|
name = "postgrest-with-${name}";
|
||||||
docs = "Run the given command in a temporary database with ${name}. If you wish to mutate the database, login with the '${superuserRole}' role.";
|
docs = "Run the given command in a temporary database with ${name}";
|
||||||
args =
|
args =
|
||||||
[
|
[
|
||||||
"ARG_OPTIONAL_SINGLE([fixtures], [f], [SQL file to load fixtures from], [test/spec/fixtures/load.sql])"
|
"ARG_OPTIONAL_SINGLE([fixtures], [f], [SQL file to load fixtures from], [test/spec/fixtures/load.sql])"
|
||||||
@@ -31,7 +25,6 @@ let
|
|||||||
"ARG_USE_ENV([PGUSER], [postgrest_test_authenticator], [Authenticator PG role])"
|
"ARG_USE_ENV([PGUSER], [postgrest_test_authenticator], [Authenticator PG role])"
|
||||||
"ARG_USE_ENV([PGDATABASE], [postgres], [PG database name])"
|
"ARG_USE_ENV([PGDATABASE], [postgres], [PG database name])"
|
||||||
"ARG_USE_ENV([PGRST_DB_SCHEMAS], [test], [Schema to expose])"
|
"ARG_USE_ENV([PGRST_DB_SCHEMAS], [test], [Schema to expose])"
|
||||||
"ARG_USE_ENV([PGTZ], [utc], [Timezone to use])"
|
|
||||||
];
|
];
|
||||||
positionalCompletion = "_command";
|
positionalCompletion = "_command";
|
||||||
inRootDir = true;
|
inRootDir = true;
|
||||||
@@ -60,7 +53,6 @@ let
|
|||||||
export PGUSER
|
export PGUSER
|
||||||
export PGDATABASE
|
export PGDATABASE
|
||||||
export PGRST_DB_SCHEMAS
|
export PGRST_DB_SCHEMAS
|
||||||
export PGTZ
|
|
||||||
|
|
||||||
HBA_FILE="$tmpdir/pg_hba.conf"
|
HBA_FILE="$tmpdir/pg_hba.conf"
|
||||||
echo "local $PGDATABASE some_protected_user password" > "$HBA_FILE"
|
echo "local $PGDATABASE some_protected_user password" > "$HBA_FILE"
|
||||||
@@ -69,16 +61,14 @@ let
|
|||||||
log "Initializing database cluster..."
|
log "Initializing database cluster..."
|
||||||
# We try to make the database cluster as independent as possible from the host
|
# We try to make the database cluster as independent as possible from the host
|
||||||
# by specifying the timezone, locale and encoding.
|
# by specifying the timezone, locale and encoding.
|
||||||
# initdb -U creates a superuser(man initdb)
|
PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "$PGUSER" --auth=trust \
|
||||||
PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "${superuserRole}" --auth=trust \
|
|
||||||
>> "$setuplog"
|
>> "$setuplog"
|
||||||
|
|
||||||
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" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\"all\" " \
|
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\"all\"" \
|
||||||
>> "$setuplog"
|
>> "$setuplog"
|
||||||
|
|
||||||
# shellcheck disable=SC2317
|
|
||||||
stop () {
|
stop () {
|
||||||
log "Stopping the database cluster..."
|
log "Stopping the database cluster..."
|
||||||
pg_ctl stop -m i >> "$setuplog"
|
pg_ctl stop -m i >> "$setuplog"
|
||||||
@@ -86,17 +76,10 @@ let
|
|||||||
}
|
}
|
||||||
trap stop EXIT
|
trap stop EXIT
|
||||||
|
|
||||||
log "Creating a minimally privileged $PGUSER connection role..."
|
log "Loading fixtures..."
|
||||||
createuser "$PGUSER" -U "${superuserRole}" --host="$tmpdir/socket" --no-createdb --no-inherit --no-superuser --no-createrole --no-replication --login
|
psql -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog"
|
||||||
|
|
||||||
log "Loading fixtures under the ${superuserRole} role..."
|
|
||||||
psql -U "${superuserRole}" -v PGUSER="$PGUSER" -v ON_ERROR_STOP=1 -f "$_arg_fixtures" >> "$setuplog"
|
|
||||||
|
|
||||||
log "Done. Running command..."
|
log "Done. Running command..."
|
||||||
|
|
||||||
echo "${commandName}: You can connect with: psql 'postgres:///$PGDATABASE?host=$tmpdir/socket' -U $PGUSER"
|
|
||||||
echo "${commandName}: You can tail the logs with: tail -f $tmpdir/db.log"
|
|
||||||
|
|
||||||
("$_arg_command" "''${_arg_leftovers[@]}")
|
("$_arg_command" "''${_arg_leftovers[@]}")
|
||||||
'';
|
'';
|
||||||
|
|
||||||
@@ -146,81 +129,6 @@ let
|
|||||||
|
|
||||||
withPg = builtins.head withPgVersions;
|
withPg = builtins.head withPgVersions;
|
||||||
|
|
||||||
withSlowPg =
|
|
||||||
checkedShellScript
|
|
||||||
{
|
|
||||||
name = "postgrest-with-slow-pg";
|
|
||||||
docs = "Run the given command with simulated high latency postgresql";
|
|
||||||
args =
|
|
||||||
[
|
|
||||||
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
|
||||||
"ARG_LEFTOVERS([command arguments])"
|
|
||||||
"ARG_USE_ENV([PGHOST], [], [PG host (socket name)])"
|
|
||||||
"ARG_USE_ENV([PGDELAY], [0ms], [extra PG latency (duration)])"
|
|
||||||
];
|
|
||||||
positionalCompletion = "_command";
|
|
||||||
inRootDir = true;
|
|
||||||
redirectTixFiles = false;
|
|
||||||
withTmpDir = true;
|
|
||||||
}
|
|
||||||
''
|
|
||||||
delay="''${PGDELAY:-0ms}"
|
|
||||||
echo "delaying data to/from postgres by $delay"
|
|
||||||
|
|
||||||
REALPGHOST="$PGHOST"
|
|
||||||
export PGHOST="$tmpdir/socket"
|
|
||||||
mkdir -p "$PGHOST"
|
|
||||||
|
|
||||||
${slocat}/bin/slocat -delay "$delay" -src "$PGHOST/.s.PGSQL.5432" -dst "$REALPGHOST/.s.PGSQL.5432" &
|
|
||||||
SLOCAT_PID=$!
|
|
||||||
# shellcheck disable=SC2317
|
|
||||||
stop_slocat() {
|
|
||||||
kill "$SLOCAT_PID" || true
|
|
||||||
wait "$SLOCAT_PID" || true
|
|
||||||
}
|
|
||||||
trap stop_slocat EXIT
|
|
||||||
sleep 1 # should wait for socket file to appear instead
|
|
||||||
|
|
||||||
("$_arg_command" "''${_arg_leftovers[@]}")
|
|
||||||
'';
|
|
||||||
|
|
||||||
withSlowPgrst =
|
|
||||||
checkedShellScript
|
|
||||||
{
|
|
||||||
name = "postgrest-with-slow-postgrest";
|
|
||||||
docs = "Run the given command with simulated high latency postgrest";
|
|
||||||
args =
|
|
||||||
[
|
|
||||||
"ARG_POSITIONAL_SINGLE([command], [Command to run])"
|
|
||||||
"ARG_LEFTOVERS([command arguments])"
|
|
||||||
"ARG_USE_ENV([PGRST_SERVER_UNIX_SOCKET], [], [PostgREST host (socket name)])"
|
|
||||||
"ARG_USE_ENV([PGRST_DELAY], [0ms], [extra PostgREST latency (duration)])"
|
|
||||||
];
|
|
||||||
positionalCompletion = "_command";
|
|
||||||
inRootDir = true;
|
|
||||||
redirectTixFiles = false;
|
|
||||||
withTmpDir = true;
|
|
||||||
}
|
|
||||||
''
|
|
||||||
delay="''${PGRST_DELAY:-0ms}"
|
|
||||||
echo "delaying data to/from PostgREST by $delay"
|
|
||||||
|
|
||||||
REAL_PGRST_SERVER_UNIX_SOCKET="$PGRST_SERVER_UNIX_SOCKET"
|
|
||||||
export PGRST_SERVER_UNIX_SOCKET="$tmpdir/postgrest.socket"
|
|
||||||
|
|
||||||
${slocat}/bin/slocat -delay "$delay" -src "$PGRST_SERVER_UNIX_SOCKET" -dst "$REAL_PGRST_SERVER_UNIX_SOCKET" &
|
|
||||||
SLOCAT_PID=$!
|
|
||||||
# shellcheck disable=SC2317
|
|
||||||
stop_slocat() {
|
|
||||||
kill "$SLOCAT_PID" || true
|
|
||||||
wait "$SLOCAT_PID" || true
|
|
||||||
}
|
|
||||||
trap stop_slocat EXIT
|
|
||||||
sleep 1 # should wait for socket file to appear instead
|
|
||||||
|
|
||||||
("$_arg_command" "''${_arg_leftovers[@]}")
|
|
||||||
'';
|
|
||||||
|
|
||||||
withGit =
|
withGit =
|
||||||
let
|
let
|
||||||
name = "postgrest-with-git";
|
name = "postgrest-with-git";
|
||||||
@@ -341,25 +249,17 @@ let
|
|||||||
export PGRST_SERVER_UNIX_SOCKET="$tmpdir"/postgrest.socket
|
export PGRST_SERVER_UNIX_SOCKET="$tmpdir"/postgrest.socket
|
||||||
|
|
||||||
rm -f result
|
rm -f result
|
||||||
if [ -z "''${PGRST_BUILD_CABAL:-}" ]; then
|
echo -n "Building postgrest... "
|
||||||
echo -n "Building postgrest (nix)... "
|
nix-build -A postgrestPackage > "$tmpdir"/build.log 2>&1 || {
|
||||||
nix-build -A postgrestPackage > "$tmpdir"/build.log 2>&1 || {
|
echo "failed, output:"
|
||||||
echo "failed, output:"
|
cat "$tmpdir"/build.log
|
||||||
cat "$tmpdir"/build.log
|
exit 1
|
||||||
exit 1
|
}
|
||||||
}
|
|
||||||
PGRST_CMD=./result/bin/postgrest
|
|
||||||
else
|
|
||||||
echo -n "Building postgrest (cabal)... "
|
|
||||||
postgrest-build
|
|
||||||
PGRST_CMD=postgrest-run
|
|
||||||
fi
|
|
||||||
echo "done."
|
echo "done."
|
||||||
|
|
||||||
echo -n "Starting postgrest... "
|
echo -n "Starting postgrest... "
|
||||||
$PGRST_CMD ${legacyConfig} > "$tmpdir"/run.log 2>&1 &
|
./result/bin/postgrest ${legacyConfig} > "$tmpdir"/run.log 2>&1 &
|
||||||
pid=$!
|
pid=$!
|
||||||
# shellcheck disable=SC2317
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
kill "$pid" || true
|
kill "$pid" || true
|
||||||
}
|
}
|
||||||
@@ -379,7 +279,7 @@ in
|
|||||||
buildToolbox
|
buildToolbox
|
||||||
{
|
{
|
||||||
name = "postgrest-with";
|
name = "postgrest-with";
|
||||||
tools = [ withPgAll withGit withPgrst withSlowPg withSlowPgrst ] ++ withPgVersions;
|
tools = [ withPgAll withGit withPgrst ] ++ withPgVersions;
|
||||||
# make withTools available for other nix files
|
# make withTools available for other nix files
|
||||||
extra = { inherit withGit withPg withPgAll withPgrst withSlowPg withSlowPgrst; };
|
extra = { inherit withGit withPg withPgAll withPgrst; };
|
||||||
}
|
}
|
||||||
|
|||||||
+49
-16
@@ -1,5 +1,5 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 11.0.1
|
version: 10.1.2
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for tables, views, and functions, supporting all HTTP methods that security
|
for tables, views, and functions, supporting all HTTP methods that security
|
||||||
@@ -46,7 +46,7 @@ library
|
|||||||
PostgREST.Cors
|
PostgREST.Cors
|
||||||
PostgREST.SchemaCache
|
PostgREST.SchemaCache
|
||||||
PostgREST.SchemaCache.Identifiers
|
PostgREST.SchemaCache.Identifiers
|
||||||
PostgREST.SchemaCache.Routine
|
PostgREST.SchemaCache.Proc
|
||||||
PostgREST.SchemaCache.Relationship
|
PostgREST.SchemaCache.Relationship
|
||||||
PostgREST.SchemaCache.Table
|
PostgREST.SchemaCache.Table
|
||||||
PostgREST.Error
|
PostgREST.Error
|
||||||
@@ -60,7 +60,6 @@ library
|
|||||||
PostgREST.Plan.CallPlan
|
PostgREST.Plan.CallPlan
|
||||||
PostgREST.Plan.MutatePlan
|
PostgREST.Plan.MutatePlan
|
||||||
PostgREST.Plan.ReadPlan
|
PostgREST.Plan.ReadPlan
|
||||||
PostgREST.Plan.Types
|
|
||||||
PostgREST.RangeQuery
|
PostgREST.RangeQuery
|
||||||
PostgREST.ApiRequest
|
PostgREST.ApiRequest
|
||||||
PostgREST.ApiRequest.Preferences
|
PostgREST.ApiRequest.Preferences
|
||||||
@@ -73,7 +72,7 @@ library
|
|||||||
PostgREST.Workers
|
PostgREST.Workers
|
||||||
other-modules: Paths_postgrest
|
other-modules: Paths_postgrest
|
||||||
build-depends: base >= 4.9 && < 4.17
|
build-depends: base >= 4.9 && < 4.17
|
||||||
, HTTP >= 4000.3.7 && < 4000.5
|
, HTTP >= 4000.3.7 && < 4000.4
|
||||||
, Ranged-sets >= 0.3 && < 0.5
|
, Ranged-sets >= 0.3 && < 0.5
|
||||||
, aeson >= 2.0.3 && < 2.2
|
, aeson >= 2.0.3 && < 2.2
|
||||||
, auto-update >= 0.1.4 && < 0.2
|
, auto-update >= 0.1.4 && < 0.2
|
||||||
@@ -91,7 +90,7 @@ library
|
|||||||
, hasql >= 1.6.1.1 && < 1.7
|
, hasql >= 1.6.1.1 && < 1.7
|
||||||
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||||
, hasql-notifications >= 0.1 && < 0.3
|
, hasql-notifications >= 0.1 && < 0.3
|
||||||
, hasql-pool >= 0.9 && < 0.10
|
, hasql-pool >= 0.8.0.6 && < 0.9
|
||||||
, hasql-transaction >= 1.0.1 && < 1.1
|
, hasql-transaction >= 1.0.1 && < 1.1
|
||||||
, heredoc >= 0.2 && < 0.3
|
, heredoc >= 0.2 && < 0.3
|
||||||
, http-types >= 0.12.2 && < 0.13
|
, http-types >= 0.12.2 && < 0.13
|
||||||
@@ -99,11 +98,11 @@ library
|
|||||||
, interpolatedstring-perl6 >= 1 && < 1.1
|
, interpolatedstring-perl6 >= 1 && < 1.1
|
||||||
, jose >= 0.8.5.1 && < 0.11
|
, jose >= 0.8.5.1 && < 0.11
|
||||||
, lens >= 4.14 && < 5.3
|
, lens >= 4.14 && < 5.3
|
||||||
, lens-aeson >= 1.0.1 && < 1.3
|
, lens-aeson >= 1.0.1 && < 1.2
|
||||||
, mtl >= 2.2.2 && < 2.3
|
, mtl >= 2.2.2 && < 2.3
|
||||||
, network >= 2.6 && < 3.2
|
, network >= 2.6 && < 3.2
|
||||||
, network-uri >= 2.6.1 && < 2.8
|
, network-uri >= 2.6.1 && < 2.8
|
||||||
, optparse-applicative >= 0.13 && < 0.18
|
, optparse-applicative >= 0.13 && < 0.17
|
||||||
, parsec >= 3.1.11 && < 3.2
|
, parsec >= 3.1.11 && < 3.2
|
||||||
, protolude >= 0.3.1 && < 0.4
|
, protolude >= 0.3.1 && < 0.4
|
||||||
, regex-tdfa >= 1.2.2 && < 1.4
|
, regex-tdfa >= 1.2.2 && < 1.4
|
||||||
@@ -187,8 +186,6 @@ test-suite spec
|
|||||||
Feature.CorsSpec
|
Feature.CorsSpec
|
||||||
Feature.ExtraSearchPathSpec
|
Feature.ExtraSearchPathSpec
|
||||||
Feature.LegacyGucsSpec
|
Feature.LegacyGucsSpec
|
||||||
Feature.NoSuperuserSpec
|
|
||||||
Feature.ObservabilitySpec
|
|
||||||
Feature.OpenApi.DisabledOpenApiSpec
|
Feature.OpenApi.DisabledOpenApiSpec
|
||||||
Feature.OpenApi.IgnorePrivOpenApiSpec
|
Feature.OpenApi.IgnorePrivOpenApiSpec
|
||||||
Feature.OpenApi.OpenApiSpec
|
Feature.OpenApi.OpenApiSpec
|
||||||
@@ -213,16 +210,15 @@ test-suite spec
|
|||||||
Feature.Query.QuerySpec
|
Feature.Query.QuerySpec
|
||||||
Feature.Query.RangeSpec
|
Feature.Query.RangeSpec
|
||||||
Feature.Query.RawOutputTypesSpec
|
Feature.Query.RawOutputTypesSpec
|
||||||
Feature.Query.RelatedQueriesSpec
|
|
||||||
Feature.Query.RpcSpec
|
Feature.Query.RpcSpec
|
||||||
Feature.Query.SingularSpec
|
Feature.Query.SingularSpec
|
||||||
Feature.Query.SpreadQueriesSpec
|
|
||||||
Feature.Query.UnicodeSpec
|
Feature.Query.UnicodeSpec
|
||||||
Feature.Query.UpdateSpec
|
Feature.Query.UpdateSpec
|
||||||
Feature.Query.UpsertSpec
|
Feature.Query.UpsertSpec
|
||||||
Feature.RollbackSpec
|
Feature.RollbackSpec
|
||||||
Feature.RpcPreRequestGucsSpec
|
Feature.RpcPreRequestGucsSpec
|
||||||
SpecHelper
|
SpecHelper
|
||||||
|
TestTypes
|
||||||
build-depends: base >= 4.9 && < 4.17
|
build-depends: base >= 4.9 && < 4.17
|
||||||
, aeson >= 2.0.3 && < 2.2
|
, aeson >= 2.0.3 && < 2.2
|
||||||
, aeson-qq >= 0.8.1 && < 0.9
|
, aeson-qq >= 0.8.1 && < 0.9
|
||||||
@@ -232,32 +228,69 @@ test-suite spec
|
|||||||
, bytestring >= 0.10.8 && < 0.12
|
, bytestring >= 0.10.8 && < 0.12
|
||||||
, case-insensitive >= 1.2 && < 1.3
|
, case-insensitive >= 1.2 && < 1.3
|
||||||
, containers >= 0.5.7 && < 0.7
|
, containers >= 0.5.7 && < 0.7
|
||||||
, hasql-pool >= 0.9 && < 0.10
|
, hasql-pool >= 0.8.0.2 && < 0.9
|
||||||
, 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.10
|
, 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
|
||||||
, lens >= 4.14 && < 5.3
|
, lens >= 4.14 && < 5.3
|
||||||
, lens-aeson >= 1.0.1 && < 1.3
|
, lens-aeson >= 1.0.1 && < 1.2
|
||||||
, monad-control >= 1.0.1 && < 1.1
|
, monad-control >= 1.0.1 && < 1.1
|
||||||
, postgrest
|
, postgrest
|
||||||
, process >= 1.4.2 && < 1.7
|
, process >= 1.4.2 && < 1.7
|
||||||
, protolude >= 0.3.1 && < 0.4
|
, protolude >= 0.3.1 && < 0.4
|
||||||
, regex-tdfa >= 1.2.2 && < 1.4
|
, regex-tdfa >= 1.2.2 && < 1.4
|
||||||
, scientific >= 0.3.4 && < 0.4
|
|
||||||
, text >= 1.2.2 && < 1.3
|
, text >= 1.2.2 && < 1.3
|
||||||
, transformers-base >= 0.4.4 && < 0.5
|
, transformers-base >= 0.4.4 && < 0.5
|
||||||
, wai >= 3.2.1 && < 3.3
|
, wai >= 3.2.1 && < 3.3
|
||||||
, wai-extra >= 3.0.19 && < 3.2
|
, wai-extra >= 3.0.19 && < 3.2
|
||||||
ghc-options: -threaded -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
|
||||||
-fno-warn-missing-signatures
|
-fno-warn-missing-signatures
|
||||||
-fwrite-ide-info
|
-fwrite-ide-info
|
||||||
-- https://github.com/PostgREST/postgrest/issues/387
|
-- https://github.com/PostgREST/postgrest/issues/387
|
||||||
-with-rtsopts=-K33K
|
-with-rtsopts=-K33K
|
||||||
|
|
||||||
|
test-suite querycost
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
default-language: Haskell2010
|
||||||
|
default-extensions: OverloadedStrings
|
||||||
|
QuasiQuotes
|
||||||
|
NoImplicitPrelude
|
||||||
|
hs-source-dirs: test/spec
|
||||||
|
main-is: QueryCost.hs
|
||||||
|
other-modules: SpecHelper
|
||||||
|
build-depends: base >= 4.9 && < 4.17
|
||||||
|
, aeson >= 2.0.3 && < 2.2
|
||||||
|
, base64-bytestring >= 1 && < 1.3
|
||||||
|
, bytestring >= 0.10.8 && < 0.12
|
||||||
|
, case-insensitive >= 1.2 && < 1.3
|
||||||
|
, containers >= 0.5.7 && < 0.7
|
||||||
|
, contravariant >= 1.4 && < 1.6
|
||||||
|
, hasql >= 1.6 && < 1.7
|
||||||
|
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||||
|
, hasql-pool >= 0.8.0.2 && < 0.9
|
||||||
|
, hasql-transaction >= 1.0.1 && < 1.1
|
||||||
|
, heredoc >= 0.2 && < 0.3
|
||||||
|
, hspec >= 2.3 && < 2.9
|
||||||
|
, hspec-wai >= 0.10 && < 0.12
|
||||||
|
, hspec-wai-json >= 0.10 && < 0.12
|
||||||
|
, http-types >= 0.12.3 && < 0.13
|
||||||
|
, lens >= 4.14 && < 5.3
|
||||||
|
, lens-aeson >= 1.0.1 && < 1.2
|
||||||
|
, postgrest
|
||||||
|
, process >= 1.4.2 && < 1.7
|
||||||
|
, protolude >= 0.3.1 && < 0.4
|
||||||
|
, regex-tdfa >= 1.2.2 && < 1.4
|
||||||
|
, wai-extra >= 3.0.19 && < 3.2
|
||||||
|
ghc-options: -O0 -Werror -Wall -fwarn-identities
|
||||||
|
-fno-spec-constr -optP-Wno-nonportable-include-path
|
||||||
|
-fwrite-ide-info
|
||||||
|
-- https://github.com/PostgREST/postgrest/issues/387
|
||||||
|
-with-rtsopts=-K1K
|
||||||
|
|
||||||
test-suite doctests
|
test-suite doctests
|
||||||
type: exitcode-stdio-1.0
|
type: exitcode-stdio-1.0
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
|
|||||||
+265
-117
@@ -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 #-}
|
||||||
|
|
||||||
@@ -33,22 +32,27 @@ import qualified Data.Set as S
|
|||||||
import qualified Data.Text.Encoding as T
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
|
|
||||||
import Data.Either.Combinators (mapBoth)
|
|
||||||
|
|
||||||
import Control.Arrow ((***))
|
import Control.Arrow ((***))
|
||||||
import Data.Aeson.Types (emptyArray, emptyObject)
|
import Data.Aeson.Types (emptyArray, emptyObject)
|
||||||
import Data.List (lookup, union)
|
import Data.List (lookup, union)
|
||||||
import Data.Ranged.Ranges (emptyRange, rangeIntersection,
|
import Data.Ranged.Ranges (emptyRange, rangeIntersection,
|
||||||
rangeIsEmpty)
|
rangeIsEmpty)
|
||||||
|
import Data.Tree (Tree (..))
|
||||||
import Network.HTTP.Types.Header (RequestHeaders, hCookie)
|
import Network.HTTP.Types.Header (RequestHeaders, hCookie)
|
||||||
import Network.HTTP.Types.URI (parseSimpleQuery)
|
import Network.HTTP.Types.URI (parseSimpleQuery)
|
||||||
import Network.Wai (Request (..))
|
import Network.Wai (Request (..))
|
||||||
import Network.Wai.Parse (parseHttpAccept)
|
import Network.Wai.Parse (parseHttpAccept)
|
||||||
import Web.Cookie (parseCookies)
|
import Web.Cookie (parseCookies)
|
||||||
|
|
||||||
|
import PostgREST.ApiRequest.Preferences (PreferCount (..),
|
||||||
|
PreferParameters (..),
|
||||||
|
PreferRepresentation (..),
|
||||||
|
PreferResolution (..),
|
||||||
|
PreferTransaction (..))
|
||||||
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
|
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
|
||||||
import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
||||||
RangeError (..))
|
RangeError (..),
|
||||||
|
SelectItem (..))
|
||||||
import PostgREST.Config (AppConfig (..),
|
import PostgREST.Config (AppConfig (..),
|
||||||
OpenAPIMode (..))
|
OpenAPIMode (..))
|
||||||
import PostgREST.MediaType (MTPlanAttrs (..),
|
import PostgREST.MediaType (MTPlanAttrs (..),
|
||||||
@@ -58,9 +62,13 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
|
|||||||
convertToLimitZeroRange,
|
convertToLimitZeroRange,
|
||||||
hasLimitZero,
|
hasLimitZero,
|
||||||
rangeRequested)
|
rangeRequested)
|
||||||
|
import PostgREST.SchemaCache (SchemaCache (..))
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
|
import PostgREST.SchemaCache.Proc (ProcDescription (..),
|
||||||
|
ProcParam (..), ProcsMap,
|
||||||
|
procReturnsScalar)
|
||||||
|
|
||||||
import qualified PostgREST.ApiRequest.Preferences as Preferences
|
import qualified PostgREST.ApiRequest.Preferences as Preferences
|
||||||
import qualified PostgREST.ApiRequest.QueryParams as QueryParams
|
import qualified PostgREST.ApiRequest.QueryParams as QueryParams
|
||||||
@@ -82,7 +90,6 @@ data Payload
|
|||||||
-- ^ 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
|
||||||
}
|
}
|
||||||
| ProcessedUrlEncoded { payArray :: [(Text, Text)], payKeys :: S.Set Text }
|
|
||||||
| RawJSON { payRaw :: LBS.ByteString }
|
| RawJSON { payRaw :: LBS.ByteString }
|
||||||
| RawPay { payRaw :: LBS.ByteString }
|
| RawPay { payRaw :: LBS.ByteString }
|
||||||
|
|
||||||
@@ -107,9 +114,41 @@ data PathInfo
|
|||||||
}
|
}
|
||||||
-- | The target db object of a user action
|
-- | The target db object of a user action
|
||||||
data Target = TargetIdent QualifiedIdentifier
|
data Target = TargetIdent QualifiedIdentifier
|
||||||
| TargetProc{tProc :: QualifiedIdentifier, tpIsRootSpec :: Bool}
|
| TargetProc{tProc :: ProcDescription, tpIsRootSpec :: Bool}
|
||||||
| TargetDefaultSpec{tdsSchema :: Schema} -- The default spec offered at root "/"
|
| TargetDefaultSpec{tdsSchema :: Schema} -- The default spec offered at root "/"
|
||||||
|
|
||||||
|
-- | RPC query param value `/rpc/func?v=<value>`, used for VARIADIC functions on form-urlencoded POST and GETs
|
||||||
|
-- | It can be fixed `?v=1` or repeated `?v=1&v=2&v=3.
|
||||||
|
data RpcParamValue = Fixed Text | Variadic [Text]
|
||||||
|
instance JSON.ToJSON RpcParamValue where
|
||||||
|
toJSON (Fixed v) = JSON.toJSON v
|
||||||
|
toJSON (Variadic v) = JSON.toJSON v
|
||||||
|
|
||||||
|
toRpcParamValue :: ProcDescription -> (Text, Text) -> (Text, RpcParamValue)
|
||||||
|
toRpcParamValue proc (k, v) | prmIsVariadic k = (k, Variadic [v])
|
||||||
|
| otherwise = (k, Fixed v)
|
||||||
|
where
|
||||||
|
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"}
|
||||||
|
jsonRpcParams :: ProcDescription -> [(Text, Text)] -> Payload
|
||||||
|
jsonRpcParams proc prms =
|
||||||
|
if not $ pdHasVariadic proc then -- if proc has no variadic param, save steps and directly convert to json
|
||||||
|
ProcessedJSON (JSON.encode $ HM.fromList $ second JSON.toJSON <$> prms) (S.fromList $ fst <$> prms)
|
||||||
|
else
|
||||||
|
let paramsMap = HM.fromListWith mergeParams $ toRpcParamValue proc <$> prms in
|
||||||
|
ProcessedJSON (JSON.encode paramsMap) (S.fromList $ HM.keys paramsMap)
|
||||||
|
where
|
||||||
|
mergeParams :: RpcParamValue -> RpcParamValue -> RpcParamValue
|
||||||
|
mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a
|
||||||
|
mergeParams v _ = v -- repeated params for non-variadic parameters are not merged
|
||||||
|
|
||||||
|
targetToJsonRpcParams :: Maybe Target -> [(Text, Text)] -> Maybe Payload
|
||||||
|
targetToJsonRpcParams target params =
|
||||||
|
case target of
|
||||||
|
Just TargetProc{tProc} -> Just $ jsonRpcParams tProc params
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Describes what the user wants to do. This data type is a
|
Describes what the user wants to do. This data type is a
|
||||||
translation of the raw elements of an HTTP request into domain
|
translation of the raw elements of an HTTP request into domain
|
||||||
@@ -118,60 +157,37 @@ data Target = TargetIdent QualifiedIdentifier
|
|||||||
if it is an action we are able to perform.
|
if it is an action we are able to perform.
|
||||||
-}
|
-}
|
||||||
data ApiRequest = ApiRequest {
|
data ApiRequest = ApiRequest {
|
||||||
iAction :: Action -- ^ Similar but not identical to HTTP method, e.g. Create/Invoke both POST
|
iAction :: Action -- ^ Similar but not identical to HTTP method, e.g. Create/Invoke both POST
|
||||||
, iRange :: HM.HashMap Text NonnegRange -- ^ Requested range of rows within response
|
, iRange :: HM.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 Payload -- ^ Data sent by client and used for mutation actions
|
, iPayload :: Maybe Payload -- ^ Data sent by client and used for mutation actions
|
||||||
, iPreferences :: Preferences.Preferences -- ^ Prefer header values
|
, iPreferRepresentation :: PreferRepresentation -- ^ If client wants created items echoed back
|
||||||
, iQueryParams :: QueryParams.QueryParams
|
, iPreferParameters :: Maybe PreferParameters -- ^ How to pass parameters to a stored procedure
|
||||||
, iColumns :: S.Set FieldName -- ^ parsed colums from &columns parameter and payload
|
, iPreferCount :: Maybe PreferCount -- ^ Whether the client wants a result count
|
||||||
, iHeaders :: [(ByteString, ByteString)] -- ^ HTTP request headers
|
, iPreferResolution :: Maybe PreferResolution -- ^ Whether the client wants to UPSERT or ignore records on PK conflict
|
||||||
, iCookies :: [(ByteString, ByteString)] -- ^ Request Cookies
|
, iPreferTransaction :: Maybe PreferTransaction -- ^ Whether the clients wants to commit or rollback the transaction
|
||||||
, iPath :: ByteString -- ^ Raw request path
|
, iQueryParams :: QueryParams.QueryParams
|
||||||
, iMethod :: ByteString -- ^ Raw request method
|
, iColumns :: S.Set FieldName -- ^ parsed colums from &columns parameter and payload
|
||||||
, iSchema :: Schema -- ^ The request schema. Can vary depending on profile headers.
|
, iHeaders :: [(ByteString, ByteString)] -- ^ HTTP request headers
|
||||||
, iNegotiatedByProfile :: Bool -- ^ If schema was was chosen according to the profile spec https://www.w3.org/TR/dx-prof-conneg/
|
, iCookies :: [(ByteString, ByteString)] -- ^ Request Cookies
|
||||||
, iAcceptMediaType :: MediaType -- ^ The media type in the Accept header
|
, iPath :: ByteString -- ^ Raw request path
|
||||||
, iContentMediaType :: MediaType -- ^ The media type in the Content-Type header
|
, iMethod :: ByteString -- ^ Raw request method
|
||||||
|
, iSchema :: Schema -- ^ The request schema. Can vary depending on profile headers.
|
||||||
|
, iNegotiatedByProfile :: Bool -- ^ If schema was was chosen according to the profile spec https://www.w3.org/TR/dx-prof-conneg/
|
||||||
|
, iAcceptMediaType :: MediaType -- ^ The media type in the Accept header
|
||||||
|
, iBinaryField :: Maybe FieldName -- ^ field used for raw output
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Examines HTTP request and translates it into user intent.
|
-- | Examines HTTP request and translates it into user intent.
|
||||||
userApiRequest :: AppConfig -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
userApiRequest :: AppConfig -> SchemaCache -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
||||||
userApiRequest conf req reqBody = do
|
userApiRequest conf sCache req reqBody = do
|
||||||
pInfo@PathInfo{..} <- getPathInfo conf $ pathInfo req
|
qPrms <- first QueryParamError $ QueryParams.parse $ rawQueryString req
|
||||||
act <- getAction pInfo method
|
pInfo <- getPathInfo conf $ pathInfo req
|
||||||
qPrms <- first QueryParamError $ QueryParams.parse (pathIsProc && act `elem` [ActionInvoke InvGet, ActionInvoke InvHead]) $ rawQueryString req
|
act <- getAction pInfo $ requestMethod req
|
||||||
(acceptMediaType, contentMediaType) <- getMediaTypes conf hdrs act pInfo
|
mediaTypes <- getMediaTypes conf (requestHeaders req) act pInfo
|
||||||
(schema, negotiatedByProfile) <- getSchema conf hdrs method
|
negotiatedSchema <- getSchema conf (requestHeaders req) (requestMethod req)
|
||||||
(topLevelRange, ranges) <- getRanges method qPrms hdrs
|
apiRequest conf sCache req reqBody qPrms pInfo act mediaTypes negotiatedSchema
|
||||||
(payload, columns) <- getPayload reqBody contentMediaType qPrms act pInfo
|
|
||||||
return $ ApiRequest {
|
|
||||||
iAction = act
|
|
||||||
, iTarget = if | pathIsProc -> TargetProc (QualifiedIdentifier schema pathName) pathIsRootSpec
|
|
||||||
| pathIsDefSpec -> TargetDefaultSpec schema
|
|
||||||
| otherwise -> TargetIdent $ QualifiedIdentifier schema pathName
|
|
||||||
, iRange = ranges
|
|
||||||
, iTopLevelRange = topLevelRange
|
|
||||||
, iPayload = payload
|
|
||||||
, iPreferences = Preferences.fromHeaders hdrs
|
|
||||||
, iQueryParams = qPrms
|
|
||||||
, iColumns = columns
|
|
||||||
, iHeaders = iHdrs
|
|
||||||
, iCookies = iCkies
|
|
||||||
, iPath = rawPathInfo req
|
|
||||||
, iMethod = method
|
|
||||||
, iSchema = schema
|
|
||||||
, iNegotiatedByProfile = negotiatedByProfile
|
|
||||||
, iAcceptMediaType = acceptMediaType
|
|
||||||
, iContentMediaType = contentMediaType
|
|
||||||
}
|
|
||||||
where
|
|
||||||
method = requestMethod req
|
|
||||||
hdrs = requestHeaders req
|
|
||||||
lookupHeader = flip lookup hdrs
|
|
||||||
iHdrs = [ (CI.foldedCase k, v) | (k,v) <- hdrs, k /= hCookie]
|
|
||||||
iCkies = maybe [] parseCookies $ lookupHeader "Cookie"
|
|
||||||
|
|
||||||
getPathInfo :: AppConfig -> [Text] -> Either ApiRequestError PathInfo
|
getPathInfo :: AppConfig -> [Text] -> Either ApiRequestError PathInfo
|
||||||
getPathInfo AppConfig{configOpenApiMode, configDbRootSpec} path =
|
getPathInfo AppConfig{configOpenApiMode, configDbRootSpec} path =
|
||||||
@@ -233,71 +249,114 @@ getSchema AppConfig{configDbSchemas} hdrs method = do
|
|||||||
acceptProfile = T.decodeUtf8 <$> lookupHeader "Accept-Profile"
|
acceptProfile = T.decodeUtf8 <$> lookupHeader "Accept-Profile"
|
||||||
lookupHeader = flip lookup hdrs
|
lookupHeader = flip lookup hdrs
|
||||||
|
|
||||||
getRanges :: ByteString -> QueryParams -> RequestHeaders -> Either ApiRequestError (NonnegRange, HM.HashMap Text NonnegRange)
|
apiRequest :: AppConfig -> SchemaCache -> Request -> RequestBody -> QueryParams.QueryParams -> PathInfo -> Action -> (MediaType, MediaType) -> (Schema, Bool) -> Either ApiRequestError ApiRequest
|
||||||
getRanges method QueryParams{qsOrder,qsRanges} hdrs
|
apiRequest conf sCache req reqBody queryparams@QueryParams{..} PathInfo{pathName, pathIsProc, pathIsRootSpec, pathIsDefSpec} action (acceptMediaType, contentMediaType) (schema, negotiatedByProfile)
|
||||||
| isInvalidRange = Left $ InvalidRange (if rangeIsEmpty headerRange then LowerGTUpper else NegativeLimit)
|
| isInvalidRange = Left $ InvalidRange (if rangeIsEmpty headerRange then LowerGTUpper else NegativeLimit)
|
||||||
|
| shouldParsePayload && isLeft payload = either (Left . InvalidBody) witness payload
|
||||||
|
| not expectParams && not (L.null qsParams) = Left $ ParseRequestError "Unexpected param or filter missing operator" ("Failed to parse " <> show qsParams)
|
||||||
| method `elem` ["PATCH", "DELETE"] && not (null qsRanges) && null qsOrder = Left LimitNoOrderError
|
| method `elem` ["PATCH", "DELETE"] && not (null qsRanges) && null qsOrder = Left LimitNoOrderError
|
||||||
| method == "PUT" && topLevelRange /= allRange = Left PutLimitNotAllowedError
|
| method == "PUT" && topLevelRange /= allRange = Left PutRangeNotAllowedError
|
||||||
| otherwise = Right (topLevelRange, ranges)
|
| otherwise = do
|
||||||
where
|
checkedTarget <- target
|
||||||
-- According to the RFC (https://www.rfc-editor.org/rfc/rfc9110.html#name-range),
|
bField <- binaryField conf acceptMediaType checkedTarget queryparams
|
||||||
-- the Range header must be ignored for all methods other than GET
|
return ApiRequest {
|
||||||
headerRange = if method == "GET" then rangeRequested hdrs else allRange
|
iAction = action
|
||||||
limitRange = fromMaybe allRange (HM.lookup "limit" qsRanges)
|
, iTarget = checkedTarget
|
||||||
headerAndLimitRange = rangeIntersection headerRange limitRange
|
, iRange = ranges
|
||||||
-- Bypass all the ranges and send only the limit zero range (0 <= x <= -1) if
|
, iTopLevelRange = topLevelRange
|
||||||
-- limit=0 is present in the query params (not allowed for the Range header)
|
, iPayload = relevantPayload
|
||||||
ranges = HM.insert "limit" (convertToLimitZeroRange limitRange headerAndLimitRange) qsRanges
|
, iPreferRepresentation = fromMaybe None preferRepresentation
|
||||||
-- The only emptyRange allowed is the limit zero range
|
, iPreferParameters = preferParameters
|
||||||
isInvalidRange = topLevelRange == emptyRange && not (hasLimitZero limitRange)
|
, iPreferCount = preferCount
|
||||||
topLevelRange = fromMaybe allRange $ HM.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
, iPreferResolution = preferResolution
|
||||||
|
, iPreferTransaction = preferTransaction
|
||||||
|
, iQueryParams = queryparams
|
||||||
|
, iColumns = payloadColumns
|
||||||
|
, iHeaders = [ (CI.foldedCase k, v) | (k,v) <- hdrs, k /= hCookie]
|
||||||
|
, iCookies = maybe [] parseCookies $ lookupHeader "Cookie"
|
||||||
|
, iPath = rawPathInfo req
|
||||||
|
, iMethod = method
|
||||||
|
, iSchema = schema
|
||||||
|
, iNegotiatedByProfile = negotiatedByProfile
|
||||||
|
, iAcceptMediaType = acceptMediaType
|
||||||
|
, iBinaryField = bField
|
||||||
|
}
|
||||||
|
where
|
||||||
|
expectParams = pathIsProc && method /= "POST"
|
||||||
|
|
||||||
getPayload :: RequestBody -> MediaType -> QueryParams.QueryParams -> Action -> PathInfo -> Either ApiRequestError (Maybe Payload, S.Set FieldName)
|
columns = case action of
|
||||||
getPayload reqBody contentMediaType QueryParams{qsColumns} action PathInfo{pathIsProc}= do
|
ActionMutate MutationCreate -> qsColumns
|
||||||
checkedPayload <- if shouldParsePayload then payload else Right Nothing
|
ActionMutate MutationUpdate -> qsColumns
|
||||||
let cols = case (checkedPayload, columns) of
|
ActionInvoke InvPost -> qsColumns
|
||||||
(Just ProcessedJSON{payKeys}, _) -> payKeys
|
_ -> Nothing
|
||||||
(Just ProcessedUrlEncoded{payKeys}, _) -> payKeys
|
|
||||||
(Just RawJSON{}, Just cls) -> cls
|
|
||||||
_ -> S.empty
|
|
||||||
return (checkedPayload, cols)
|
|
||||||
where
|
|
||||||
payload :: Either ApiRequestError (Maybe Payload)
|
|
||||||
payload = mapBoth InvalidBody Just $ case (contentMediaType, pathIsProc) of
|
|
||||||
(MTApplicationJSON, _) ->
|
|
||||||
if isJust columns
|
|
||||||
then Right $ RawJSON reqBody
|
|
||||||
else note "All object keys must match" . payloadAttributes reqBody
|
|
||||||
=<< if LBS.null reqBody && pathIsProc
|
|
||||||
then Right emptyObject
|
|
||||||
else first BS.pack $ JSON.eitherDecode reqBody
|
|
||||||
(MTTextCSV, _) -> do
|
|
||||||
json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)
|
|
||||||
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
|
|
||||||
(MTUrlEncoded, isProc) -> do
|
|
||||||
let params = (T.decodeUtf8 *** T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody)
|
|
||||||
if isProc
|
|
||||||
then Right $ ProcessedUrlEncoded params (S.fromList $ fst <$> params)
|
|
||||||
else
|
|
||||||
let paramsMap = HM.fromList $ (identity *** JSON.String) <$> params in
|
|
||||||
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (HM.keys paramsMap)
|
|
||||||
(MTTextPlain, True) -> Right $ RawPay reqBody
|
|
||||||
(MTTextXML, True) -> Right $ RawPay reqBody
|
|
||||||
(MTOctetStream, True) -> Right $ RawPay reqBody
|
|
||||||
(ct, _) -> Left $ "Content-Type not acceptable: " <> MediaType.toMime ct
|
|
||||||
|
|
||||||
shouldParsePayload = case (action, contentMediaType) of
|
payloadColumns =
|
||||||
(ActionMutate MutationCreate, _) -> True
|
case (contentMediaType, action) of
|
||||||
(ActionInvoke InvPost, _) -> True
|
(_, ActionInvoke InvGet) -> S.fromList $ fst <$> qsParams
|
||||||
(ActionMutate MutationSingleUpsert, _) -> True
|
(_, ActionInvoke InvHead) -> S.fromList $ fst <$> qsParams
|
||||||
(ActionMutate MutationUpdate, _) -> True
|
(MTUrlEncoded, _) -> S.fromList $ map (T.decodeUtf8 . fst) $ parseSimpleQuery $ LBS.toStrict reqBody
|
||||||
_ -> False
|
_ -> case (relevantPayload, columns) of
|
||||||
|
(Just ProcessedJSON{payKeys}, _) -> payKeys
|
||||||
|
(Just RawJSON{}, Just cls) -> cls
|
||||||
|
_ -> S.empty
|
||||||
|
payload :: Either ByteString Payload
|
||||||
|
payload = case (contentMediaType, pathIsProc) of
|
||||||
|
(MTApplicationJSON, _) ->
|
||||||
|
if isJust columns
|
||||||
|
then Right $ RawJSON reqBody
|
||||||
|
else note "All object keys must match" . payloadAttributes reqBody
|
||||||
|
=<< if LBS.null reqBody && pathIsProc
|
||||||
|
then Right emptyObject
|
||||||
|
else first BS.pack $ JSON.eitherDecode reqBody
|
||||||
|
(MTTextCSV, _) -> do
|
||||||
|
json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)
|
||||||
|
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
|
||||||
|
(MTUrlEncoded, _) ->
|
||||||
|
let paramsMap = HM.fromList $ (T.decodeUtf8 *** JSON.String . T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody) in
|
||||||
|
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (HM.keys paramsMap)
|
||||||
|
(MTTextPlain, True) -> Right $ RawPay reqBody
|
||||||
|
(MTTextXML, True) -> Right $ RawPay reqBody
|
||||||
|
(MTOctetStream, True) -> Right $ RawPay reqBody
|
||||||
|
(ct, _) -> Left $ "Content-Type not acceptable: " <> MediaType.toMime ct
|
||||||
|
topLevelRange = fromMaybe allRange $ HM.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
||||||
|
|
||||||
columns = case action of
|
target
|
||||||
ActionMutate MutationCreate -> qsColumns
|
| pathIsProc = (`TargetProc` pathIsRootSpec) <$> callFindProc schema pathName
|
||||||
ActionMutate MutationUpdate -> qsColumns
|
| pathIsDefSpec = Right $ TargetDefaultSpec schema
|
||||||
ActionInvoke InvPost -> qsColumns
|
| otherwise = Right $ TargetIdent $ QualifiedIdentifier schema pathName
|
||||||
_ -> Nothing
|
where
|
||||||
|
callFindProc procSch procNam = findProc
|
||||||
|
(QualifiedIdentifier procSch procNam) payloadColumns (preferParameters == Just SingleObject) (dbProcs sCache)
|
||||||
|
contentMediaType (action == ActionInvoke InvPost)
|
||||||
|
|
||||||
|
shouldParsePayload = case (action, contentMediaType) of
|
||||||
|
(ActionMutate MutationCreate, _) -> True
|
||||||
|
(ActionInvoke InvPost, MTUrlEncoded) -> False
|
||||||
|
(ActionInvoke InvPost, _) -> True
|
||||||
|
(ActionMutate MutationSingleUpsert, _) -> True
|
||||||
|
(ActionMutate MutationUpdate, _) -> True
|
||||||
|
_ -> False
|
||||||
|
relevantPayload = case (contentMediaType, action) of
|
||||||
|
-- Though ActionInvoke GET/HEAD doesn't really have a payload, we use the payload variable as a way
|
||||||
|
-- to store the query string arguments to the function.
|
||||||
|
(_, ActionInvoke InvGet) -> targetToJsonRpcParams (rightToMaybe target) qsParams
|
||||||
|
(_, ActionInvoke InvHead) -> targetToJsonRpcParams (rightToMaybe target) qsParams
|
||||||
|
(MTUrlEncoded, ActionInvoke InvPost) -> targetToJsonRpcParams (rightToMaybe target) $ (T.decodeUtf8 *** T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody)
|
||||||
|
_ | shouldParsePayload -> rightToMaybe payload
|
||||||
|
| otherwise -> Nothing
|
||||||
|
method = requestMethod req
|
||||||
|
hdrs = requestHeaders req
|
||||||
|
lookupHeader = flip lookup hdrs
|
||||||
|
Preferences.Preferences{..} = Preferences.fromHeaders hdrs
|
||||||
|
headerRange = rangeRequested hdrs
|
||||||
|
limitRange = fromMaybe allRange (HM.lookup "limit" qsRanges)
|
||||||
|
headerAndLimitRange = rangeIntersection headerRange limitRange
|
||||||
|
|
||||||
|
-- Bypass all the ranges and send only the limit zero range (0 <= x <= -1) if
|
||||||
|
-- limit=0 is present in the query params (not allowed for the Range header)
|
||||||
|
ranges = HM.insert "limit" (convertToLimitZeroRange limitRange headerAndLimitRange) qsRanges
|
||||||
|
-- The only emptyRange allowed is the limit zero range
|
||||||
|
isInvalidRange = topLevelRange == emptyRange && not (hasLimitZero limitRange)
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Find the best match from a list of media types accepted by the
|
Find the best match from a list of media types accepted by the
|
||||||
@@ -386,3 +445,92 @@ requestMediaTypes conf action path =
|
|||||||
[MTApplicationJSON, MTSingularJSON, MTGeoJSON, MTTextCSV] ++
|
[MTApplicationJSON, MTSingularJSON, MTGeoJSON, MTTextCSV] ++
|
||||||
[MTPlan $ MTPlanAttrs Nothing PlanJSON mempty | configDbPlanEnabled conf]
|
[MTPlan $ MTPlanAttrs Nothing PlanJSON mempty | configDbPlanEnabled conf]
|
||||||
rawMediaTypes = configRawMediaTypes conf `union` [MTOctetStream, MTTextPlain, MTTextXML]
|
rawMediaTypes = configRawMediaTypes conf `union` [MTOctetStream, MTTextPlain, MTTextXML]
|
||||||
|
|
||||||
|
{-|
|
||||||
|
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
|
||||||
|
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 -> MediaType -> Bool -> Either ApiRequestError ProcDescription
|
||||||
|
findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPost =
|
||||||
|
case matchProc of
|
||||||
|
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentMediaType isInvPost (HM.keys allProcs) lookupProcName
|
||||||
|
-- If there are no functions with named arguments, fallback to the single unnamed argument function
|
||||||
|
([], [proc]) -> Right proc
|
||||||
|
([], procs) -> Left $ AmbiguousRpc (toList procs)
|
||||||
|
-- Matches the functions with named arguments
|
||||||
|
([proc], _) -> Right proc
|
||||||
|
(procs, _) -> Left $ AmbiguousRpc (toList procs)
|
||||||
|
where
|
||||||
|
matchProc = overloadedProcPartition lookupProcName
|
||||||
|
-- First find the proc by name
|
||||||
|
lookupProcName = HM.lookupDefault mempty qi allProcs
|
||||||
|
-- The partition obtained has the form (overloadedProcs,fallbackProcs)
|
||||||
|
-- where fallbackProcs are functions with a single unnamed parameter
|
||||||
|
overloadedProcPartition = foldr select ([],[])
|
||||||
|
select proc ~(ts,fs)
|
||||||
|
| matchesParams proc = (proc:ts,fs)
|
||||||
|
| hasSingleUnnamedParam proc = (ts,proc:fs)
|
||||||
|
| otherwise = (ts,fs)
|
||||||
|
-- If the function is called with post and has a single unnamed parameter
|
||||||
|
-- it can be called depending on content type and the parameter type
|
||||||
|
hasSingleUnnamedParam ProcDescription{pdParams=[ProcParam{ppType}]} = isInvPost && case (contentMediaType, ppType) of
|
||||||
|
(MTApplicationJSON, "json") -> True
|
||||||
|
(MTApplicationJSON, "jsonb") -> True
|
||||||
|
(MTTextPlain, "text") -> True
|
||||||
|
(MTTextXML, "xml") -> True
|
||||||
|
(MTOctetStream, "bytea") -> True
|
||||||
|
_ -> False
|
||||||
|
hasSingleUnnamedParam _ = False
|
||||||
|
matchesParams proc =
|
||||||
|
let
|
||||||
|
params = pdParams proc
|
||||||
|
firstType = (ppType <$> headMay params)
|
||||||
|
in
|
||||||
|
-- exceptional case for Prefer: params=single-object
|
||||||
|
if paramsAsSingleObject
|
||||||
|
then length params == 1 && (firstType == Just "json" || firstType == Just "jsonb")
|
||||||
|
-- If the function has no parameters, the arguments keys must be empty as well
|
||||||
|
else if null params
|
||||||
|
then null argumentsKeys && not (isInvPost && contentMediaType `elem` [MTOctetStream, MTTextPlain, MTTextXML])
|
||||||
|
-- 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)
|
||||||
|
|
||||||
|
-- | If raw(binary) output is requested, check that MediaType is one of the
|
||||||
|
-- admitted rawMediaTypes and that`?select=...` contains only one field other
|
||||||
|
-- than `*`
|
||||||
|
binaryField :: AppConfig -> MediaType -> Target -> QueryParams -> Either ApiRequestError (Maybe FieldName)
|
||||||
|
binaryField AppConfig{configRawMediaTypes} acceptMediaType target QueryParams{qsSelect}
|
||||||
|
| returnsScalar target && isRawMediaType =
|
||||||
|
Right $ Just "pgrst_scalar"
|
||||||
|
| isRawMediaType =
|
||||||
|
let
|
||||||
|
fieldName = fstFieldName qsSelect
|
||||||
|
in
|
||||||
|
case fieldName of
|
||||||
|
Just fld -> Right $ Just fld
|
||||||
|
Nothing -> Left $ BinaryFieldError acceptMediaType
|
||||||
|
| otherwise =
|
||||||
|
Right Nothing
|
||||||
|
where
|
||||||
|
isRawMediaType = acceptMediaType `elem` configRawMediaTypes `union` [MTOctetStream, MTTextPlain, MTTextXML] || isRawPlan acceptMediaType
|
||||||
|
isRawPlan mt = case mt of
|
||||||
|
MTPlan (MTPlanAttrs (Just MTOctetStream) _ _) -> True
|
||||||
|
MTPlan (MTPlanAttrs (Just MTTextPlain) _ _) -> True
|
||||||
|
MTPlan (MTPlanAttrs (Just MTTextXML) _ _) -> True
|
||||||
|
_ -> False
|
||||||
|
returnsScalar :: Target -> Bool
|
||||||
|
returnsScalar (TargetProc proc _) = procReturnsScalar proc
|
||||||
|
returnsScalar _ = False
|
||||||
|
|
||||||
|
fstFieldName :: [Tree SelectItem] -> Maybe FieldName
|
||||||
|
fstFieldName [Node SelectField{selField=("*", _)} []] = Nothing
|
||||||
|
fstFieldName [Node SelectField{selField=(fld, _)} []] = Just fld
|
||||||
|
fstFieldName _ = Nothing
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
module PostgREST.ApiRequest.Preferences
|
module PostgREST.ApiRequest.Preferences
|
||||||
( Preferences(..)
|
( Preferences(..)
|
||||||
, PreferCount(..)
|
, PreferCount(..)
|
||||||
, PreferMissing(..)
|
|
||||||
, PreferParameters(..)
|
, PreferParameters(..)
|
||||||
, PreferRepresentation(..)
|
, PreferRepresentation(..)
|
||||||
, PreferResolution(..)
|
, PreferResolution(..)
|
||||||
@@ -34,18 +33,16 @@ import Protolude
|
|||||||
-- >>> deriving instance Show PreferParameters
|
-- >>> deriving instance Show PreferParameters
|
||||||
-- >>> deriving instance Show PreferCount
|
-- >>> deriving instance Show PreferCount
|
||||||
-- >>> deriving instance Show PreferTransaction
|
-- >>> deriving instance Show PreferTransaction
|
||||||
-- >>> deriving instance Show PreferMissing
|
|
||||||
-- >>> deriving instance Show Preferences
|
-- >>> deriving instance Show Preferences
|
||||||
|
|
||||||
-- | Preferences recognized by the application.
|
-- | Preferences recognized by the application.
|
||||||
data Preferences
|
data Preferences
|
||||||
= Preferences
|
= Preferences
|
||||||
{ preferResolution :: Maybe PreferResolution
|
{ preferResolution :: Maybe PreferResolution
|
||||||
, preferRepresentation :: PreferRepresentation
|
, preferRepresentation :: Maybe PreferRepresentation
|
||||||
, preferParameters :: Maybe PreferParameters
|
, preferParameters :: Maybe PreferParameters
|
||||||
, preferCount :: Maybe PreferCount
|
, preferCount :: Maybe PreferCount
|
||||||
, preferTransaction :: Maybe PreferTransaction
|
, preferTransaction :: Maybe PreferTransaction
|
||||||
, preferMissing :: Maybe PreferMissing
|
|
||||||
}
|
}
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
@@ -56,23 +53,21 @@ data Preferences
|
|||||||
-- >>> pPrint $ fromHeaders [("Prefer", "resolution=ignore-duplicates, count=exact")]
|
-- >>> pPrint $ fromHeaders [("Prefer", "resolution=ignore-duplicates, count=exact")]
|
||||||
-- Preferences
|
-- Preferences
|
||||||
-- { preferResolution = Just IgnoreDuplicates
|
-- { preferResolution = Just IgnoreDuplicates
|
||||||
-- , preferRepresentation = None
|
-- , preferRepresentation = Nothing
|
||||||
-- , preferParameters = Nothing
|
-- , preferParameters = Nothing
|
||||||
-- , preferCount = Just ExactCount
|
-- , preferCount = Just ExactCount
|
||||||
-- , preferTransaction = Nothing
|
-- , preferTransaction = Nothing
|
||||||
-- , preferMissing = Nothing
|
|
||||||
-- }
|
-- }
|
||||||
--
|
--
|
||||||
-- Multiple headers can also be used:
|
-- Multiple headers can also be used:
|
||||||
--
|
--
|
||||||
-- >>> pPrint $ fromHeaders [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null")]
|
-- >>> pPrint $ fromHeaders [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact")]
|
||||||
-- Preferences
|
-- Preferences
|
||||||
-- { preferResolution = Just IgnoreDuplicates
|
-- { preferResolution = Just IgnoreDuplicates
|
||||||
-- , preferRepresentation = None
|
-- , preferRepresentation = Nothing
|
||||||
-- , preferParameters = Nothing
|
-- , preferParameters = Nothing
|
||||||
-- , preferCount = Just ExactCount
|
-- , preferCount = Just ExactCount
|
||||||
-- , preferTransaction = Nothing
|
-- , preferTransaction = Nothing
|
||||||
-- , preferMissing = Just ApplyNulls
|
|
||||||
-- }
|
-- }
|
||||||
--
|
--
|
||||||
-- If a preference is set more than once, only the first is used:
|
-- If a preference is set more than once, only the first is used:
|
||||||
@@ -97,25 +92,23 @@ data Preferences
|
|||||||
--
|
--
|
||||||
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
|
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
|
||||||
--
|
--
|
||||||
-- >>> pPrint $ fromHeaders [("prefer", "count=exact, tx=commit ,return=representation , missing=default")]
|
-- >>> pPrint $ fromHeaders [("prefer", "count=exact, tx=commit ,return=minimal")]
|
||||||
-- Preferences
|
-- Preferences
|
||||||
-- { preferResolution = Nothing
|
-- { preferResolution = Nothing
|
||||||
-- , preferRepresentation = Full
|
-- , preferRepresentation = Just None
|
||||||
-- , preferParameters = Nothing
|
-- , preferParameters = Nothing
|
||||||
-- , preferCount = Just ExactCount
|
-- , preferCount = Just ExactCount
|
||||||
-- , preferTransaction = Just Commit
|
-- , preferTransaction = Just Commit
|
||||||
-- , preferMissing = Just ApplyDefaults
|
|
||||||
-- }
|
-- }
|
||||||
--
|
--
|
||||||
fromHeaders :: [HTTP.Header] -> Preferences
|
fromHeaders :: [HTTP.Header] -> Preferences
|
||||||
fromHeaders headers =
|
fromHeaders headers =
|
||||||
Preferences
|
Preferences
|
||||||
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
|
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
|
||||||
, preferRepresentation = fromMaybe None $ parsePrefs [Full, None, HeadersOnly]
|
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
|
||||||
, preferParameters = parsePrefs [SingleObject]
|
, preferParameters = parsePrefs [SingleObject, MultipleObjects]
|
||||||
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
||||||
, preferTransaction = parsePrefs [Commit, Rollback]
|
, preferTransaction = parsePrefs [Commit, Rollback]
|
||||||
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
|
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
prefHeaders = filter ((==) HTTP.hPrefer . fst) headers
|
prefHeaders = filter ((==) HTTP.hPrefer . fst) headers
|
||||||
@@ -176,10 +169,13 @@ instance ToHeaderValue PreferRepresentation where
|
|||||||
-- | How to pass parameters to stored procedures.
|
-- | 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.
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
|
-- TODO: Deprecate params=multiple-objects in next major version
|
||||||
instance ToHeaderValue PreferParameters where
|
instance ToHeaderValue PreferParameters where
|
||||||
toHeaderValue SingleObject = "params=single-object"
|
toHeaderValue SingleObject = "params=single-object"
|
||||||
|
toHeaderValue MultipleObjects = "params=multiple-objects"
|
||||||
|
|
||||||
-- | How to determine the count of (expected) results
|
-- | How to determine the count of (expected) results
|
||||||
data PreferCount
|
data PreferCount
|
||||||
@@ -208,17 +204,3 @@ instance ToHeaderValue PreferTransaction where
|
|||||||
toHeaderValue Rollback = "tx=rollback"
|
toHeaderValue Rollback = "tx=rollback"
|
||||||
|
|
||||||
instance ToAppliedHeader PreferTransaction
|
instance ToAppliedHeader PreferTransaction
|
||||||
|
|
||||||
-- |
|
|
||||||
-- How to handle the insertion/update when the keys specified in ?columns are not present
|
|
||||||
-- in the json body.
|
|
||||||
data PreferMissing
|
|
||||||
= ApplyDefaults -- ^ Use the default column value for missing values.
|
|
||||||
| ApplyNulls -- ^ Use the null value for missing values.
|
|
||||||
deriving Eq
|
|
||||||
|
|
||||||
instance ToHeaderValue PreferMissing where
|
|
||||||
toHeaderValue ApplyDefaults = "missing=default"
|
|
||||||
toHeaderValue ApplyNulls = "missing=null"
|
|
||||||
|
|
||||||
instance ToAppliedHeader PreferMissing
|
|
||||||
|
|||||||
@@ -30,13 +30,14 @@ import Data.Ranged.Ranges (Range (..))
|
|||||||
import Data.Tree (Tree (..))
|
import Data.Tree (Tree (..))
|
||||||
import Text.Parsec.Error (errorMessages,
|
import Text.Parsec.Error (errorMessages,
|
||||||
showErrorMessages)
|
showErrorMessages)
|
||||||
|
import Text.Parsec.Prim (parserFail)
|
||||||
import Text.ParserCombinators.Parsec (GenParser, ParseError, Parser,
|
import Text.ParserCombinators.Parsec (GenParser, ParseError, Parser,
|
||||||
anyChar, between, char, digit,
|
anyChar, between, char, digit,
|
||||||
eof, errorPos, letter,
|
eof, errorPos, letter,
|
||||||
lookAhead, many1, noneOf,
|
lookAhead, many1, noneOf,
|
||||||
notFollowedBy, oneOf,
|
notFollowedBy, oneOf,
|
||||||
optionMaybe, sepBy, sepBy1,
|
optionMaybe, sepBy1, string,
|
||||||
string, try, (<?>))
|
try, (<?>))
|
||||||
|
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
rangeGeq, rangeLimit,
|
rangeGeq, rangeLimit,
|
||||||
@@ -45,16 +46,14 @@ import PostgREST.SchemaCache.Identifiers (FieldName)
|
|||||||
|
|
||||||
import PostgREST.ApiRequest.Types (EmbedParam (..), EmbedPath, Field,
|
import PostgREST.ApiRequest.Types (EmbedParam (..), EmbedPath, Field,
|
||||||
Filter (..), FtsOperator (..),
|
Filter (..), FtsOperator (..),
|
||||||
Hint, JoinType (..),
|
JoinType (..), JsonOperand (..),
|
||||||
JsonOperand (..),
|
|
||||||
JsonOperation (..), JsonPath,
|
JsonOperation (..), JsonPath,
|
||||||
ListVal, LogicOperator (..),
|
ListVal, LogicOperator (..),
|
||||||
LogicTree (..), OpExpr (..),
|
LogicTree (..), OpExpr (..),
|
||||||
OpQuantifier (..), Operation (..),
|
Operation (..),
|
||||||
OrderDirection (..),
|
OrderDirection (..),
|
||||||
OrderNulls (..), OrderTerm (..),
|
OrderNulls (..), OrderTerm (..),
|
||||||
QPError (..), QuantOperator (..),
|
QPError (..), SelectItem (..),
|
||||||
SelectItem (..),
|
|
||||||
SimpleOperator (..), SingleVal,
|
SimpleOperator (..), SingleVal,
|
||||||
TrileanVal (..))
|
TrileanVal (..))
|
||||||
|
|
||||||
@@ -67,9 +66,7 @@ import Protolude hiding (try)
|
|||||||
-- >>> deriving instance Show QPError
|
-- >>> deriving instance Show QPError
|
||||||
-- >>> deriving instance Show TrileanVal
|
-- >>> deriving instance Show TrileanVal
|
||||||
-- >>> deriving instance Show FtsOperator
|
-- >>> deriving instance Show FtsOperator
|
||||||
-- >>> deriving instance Show QuantOperator
|
|
||||||
-- >>> deriving instance Show SimpleOperator
|
-- >>> deriving instance Show SimpleOperator
|
||||||
-- >>> deriving instance Show OpQuantifier
|
|
||||||
-- >>> deriving instance Show Operation
|
-- >>> deriving instance Show Operation
|
||||||
-- >>> deriving instance Show OpExpr
|
-- >>> deriving instance Show OpExpr
|
||||||
-- >>> deriving instance Show JsonOperand
|
-- >>> deriving instance Show JsonOperand
|
||||||
@@ -77,11 +74,6 @@ import Protolude hiding (try)
|
|||||||
-- >>> deriving instance Show Filter
|
-- >>> deriving instance Show Filter
|
||||||
-- >>> deriving instance Show JoinType
|
-- >>> deriving instance Show JoinType
|
||||||
-- >>> deriving instance Show SelectItem
|
-- >>> deriving instance Show SelectItem
|
||||||
-- >>> deriving instance Show OrderDirection
|
|
||||||
-- >>> deriving instance Show OrderNulls
|
|
||||||
-- >>> deriving instance Show OrderTerm
|
|
||||||
-- >>> deriving instance Show LogicOperator
|
|
||||||
-- >>> deriving instance Show LogicTree
|
|
||||||
|
|
||||||
data QueryParams =
|
data QueryParams =
|
||||||
QueryParams
|
QueryParams
|
||||||
@@ -116,45 +108,39 @@ data QueryParams =
|
|||||||
--
|
--
|
||||||
-- The canonical representation of the query string has parameters sorted alphabetically:
|
-- The canonical representation of the query string has parameters sorted alphabetically:
|
||||||
--
|
--
|
||||||
-- >>> qsCanonical <$> parse True "a=1&c=3&b=2&d"
|
-- >>> qsCanonical <$> parse "a=1&c=3&b=2&d"
|
||||||
-- Right "a=1&b=2&c=3&d="
|
-- Right "a=1&b=2&c=3&d="
|
||||||
--
|
--
|
||||||
-- 'select' is a reserved parameter that selects the fields to be returned:
|
-- 'select' is a reserved parameter that selects the fields to be returned:
|
||||||
--
|
--
|
||||||
-- >>> qsSelect <$> parse False "select=name,location"
|
-- >>> qsSelect <$> parse "select=name,location"
|
||||||
-- Right [Node {rootLabel = SelectField {selField = ("name",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectField {selField = ("location",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]
|
-- Right [Node {rootLabel = SelectField {selField = ("name",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectField {selField = ("location",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]
|
||||||
--
|
--
|
||||||
-- Filters are parameters whose value contains an operator, separated by a '.' from its value:
|
-- Filters are parameters whose value contains an operator, separated by a '.' from its value:
|
||||||
--
|
--
|
||||||
-- >>> qsFilters <$> parse False "a.b=eq.0"
|
-- >>> qsFilters <$> parse "a.b=eq.0"
|
||||||
-- Right [(["a"],Filter {field = ("b",[]), opExpr = OpExpr False (OpQuant OpEqual Nothing "0")})]
|
-- Right [(["a"],Filter {field = ("b",[]), opExpr = OpExpr False (Op OpEqual "0")})]
|
||||||
--
|
--
|
||||||
-- If the operator specified in a filter does not exist, parsing the query string fails:
|
-- If the operator specified in a filter does not exist, parsing the query string fails:
|
||||||
--
|
--
|
||||||
-- >>> qsFilters <$> parse False "a.b=noop.0"
|
-- >>> qsFilters <$> parse "a.b=noop.0"
|
||||||
-- Left (QPError "\"failed to parse filter (noop.0)\" (line 1, column 1)" "unexpected \"o\" expecting \"not\" or operator (eq, gt, ...)")
|
-- Left (QPError "\"failed to parse filter (noop.0)\" (line 1, column 6)" "unknown single value operator noop")
|
||||||
parse :: Bool -> ByteString -> Either QPError QueryParams
|
parse :: ByteString -> Either QPError QueryParams
|
||||||
parse isRpcGet qs = do
|
parse qs =
|
||||||
rOrd <- pRequestOrder `traverse` order
|
QueryParams
|
||||||
rLogic <- pRequestLogicTree `traverse` logic
|
canonical
|
||||||
rCols <- pRequestColumns columns
|
params
|
||||||
rSel <- pRequestSelect select
|
ranges
|
||||||
(rFlts, params) <- L.partition hasOp <$> pRequestFilter isRpcGet `traverse` filters
|
<$> pRequestOrder `traverse` order
|
||||||
(rFltsRoot, rFltsNotRoot) <- pure $ L.partition hasRootFilter rFlts
|
<*> pRequestLogicTree `traverse` logic
|
||||||
rOnConflict <- pRequestOnConflict `traverse` onConflict
|
<*> pRequestColumns columns
|
||||||
|
<*> pRequestSelect select
|
||||||
let rFltsFields = S.fromList (fst <$> filters)
|
<*> pRequestFilter `traverse` filters
|
||||||
params' = mapMaybe (\case {(_, Filter (fld, _) (NoOpExpr v)) -> Just (fld,v); _ -> Nothing}) params
|
<*> (fmap snd <$> (pRequestFilter `traverse` filtersRoot))
|
||||||
rFltsRoot' = snd <$> rFltsRoot
|
<*> pRequestFilter `traverse` filtersNotRoot
|
||||||
|
<*> pure (S.fromList (fst <$> filters))
|
||||||
return $ QueryParams canonical params' ranges rOrd rLogic rCols rSel rFlts rFltsRoot' rFltsNotRoot rFltsFields rOnConflict
|
<*> sequenceA (pRequestOnConflict <$> onConflict)
|
||||||
where
|
where
|
||||||
hasRootFilter, hasOp :: (EmbedPath, Filter) -> Bool
|
|
||||||
hasRootFilter ([], _) = True
|
|
||||||
hasRootFilter _ = False
|
|
||||||
hasOp (_, Filter (_, _) (NoOpExpr _)) = False
|
|
||||||
hasOp _ = True
|
|
||||||
|
|
||||||
logic = filter (endingIn ["and", "or"] . fst) nonemptyParams
|
logic = filter (endingIn ["and", "or"] . fst) nonemptyParams
|
||||||
select = fromMaybe "*" $ lookupParam "select"
|
select = fromMaybe "*" $ lookupParam "select"
|
||||||
onConflict = lookupParam "on_conflict"
|
onConflict = lookupParam "on_conflict"
|
||||||
@@ -181,11 +167,32 @@ parse isRpcGet qs = do
|
|||||||
endingIn xx key = lastWord `elem` xx
|
endingIn xx key = lastWord `elem` xx
|
||||||
where lastWord = L.last $ T.split (== '.') key
|
where lastWord = L.last $ T.split (== '.') key
|
||||||
|
|
||||||
filters = filter (isFilter . fst) nonemptyParams
|
(filters, params) = L.partition isParam filtersAndParams
|
||||||
isFilter k = not (endingIn reservedEmbeddable k) && notElem k reserved
|
isParam (k, v) = isEmbedPath k || hasOperator v || hasFtsOperator v
|
||||||
|
|
||||||
|
filtersAndParams = filter (isFilterOrParam . fst) nonemptyParams
|
||||||
|
isFilterOrParam k = not (endingIn reservedEmbeddable k) && notElem k reserved
|
||||||
reserved = ["select", "columns", "on_conflict"]
|
reserved = ["select", "columns", "on_conflict"]
|
||||||
reservedEmbeddable = ["order", "limit", "offset", "and", "or"]
|
reservedEmbeddable = ["order", "limit", "offset", "and", "or"]
|
||||||
|
|
||||||
|
(filtersNotRoot, filtersRoot) = L.partition isNotRoot filters
|
||||||
|
isNotRoot = flip T.isInfixOf "." . fst
|
||||||
|
|
||||||
|
-- TODO: These checks are redundant to the parsers, should use parsers to differentiate params
|
||||||
|
hasOperator val =
|
||||||
|
case T.splitOn "." val of
|
||||||
|
"not" : _ : _ -> True
|
||||||
|
"is" : _ -> True
|
||||||
|
"in" : _ -> True
|
||||||
|
x : _ -> isJust (operator x) || isJust (ftsOperator x)
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
hasFtsOperator val =
|
||||||
|
case T.splitOn "(" val of
|
||||||
|
x : _ : _ -> isJust $ ftsOperator x
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
isEmbedPath = T.isInfixOf "."
|
||||||
replaceLast x s = T.intercalate "." $ L.init (T.split (=='.') s) <> [x]
|
replaceLast x s = T.intercalate "." $ L.init (T.split (=='.') s) <> [x]
|
||||||
|
|
||||||
ranges :: HM.HashMap Text (Range Integer)
|
ranges :: HM.HashMap Text (Range Integer)
|
||||||
@@ -202,31 +209,39 @@ parse isRpcGet qs = do
|
|||||||
offsetParams =
|
offsetParams =
|
||||||
HM.fromList [(k, maybe allRange rangeGeq (readMaybe v)) | (k,v) <- offsets]
|
HM.fromList [(k, maybe allRange rangeGeq (readMaybe v)) | (k,v) <- offsets]
|
||||||
|
|
||||||
simpleOperator :: Parser SimpleOperator
|
operator :: Text -> Maybe SimpleOperator
|
||||||
simpleOperator =
|
operator = \case
|
||||||
try (string "neq" $> OpNotEqual) <|>
|
"eq" -> Just OpEqual
|
||||||
try (string "cs" $> OpContains) <|>
|
"gte" -> Just OpGreaterThanEqual
|
||||||
try (string "cd" $> OpContained) <|>
|
"gt" -> Just OpGreaterThan
|
||||||
try (string "ov" $> OpOverlap) <|>
|
"lte" -> Just OpLessThanEqual
|
||||||
try (string "sl" $> OpStrictlyLeft) <|>
|
"lt" -> Just OpLessThan
|
||||||
try (string "sr" $> OpStrictlyRight) <|>
|
"neq" -> Just OpNotEqual
|
||||||
try (string "nxr" $> OpNotExtendsRight) <|>
|
"like" -> Just OpLike
|
||||||
try (string "nxl" $> OpNotExtendsLeft) <|>
|
"ilike" -> Just OpILike
|
||||||
try (string "adj" $> OpAdjacent) <?>
|
"cs" -> Just OpContains
|
||||||
"unknown single value operator"
|
"cd" -> Just OpContained
|
||||||
|
"ov" -> Just OpOverlap
|
||||||
|
"sl" -> Just OpStrictlyLeft
|
||||||
|
"sr" -> Just OpStrictlyRight
|
||||||
|
"nxr" -> Just OpNotExtendsRight
|
||||||
|
"nxl" -> Just OpNotExtendsLeft
|
||||||
|
"adj" -> Just OpAdjacent
|
||||||
|
"match" -> Just OpMatch
|
||||||
|
"imatch" -> Just OpIMatch
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
ftsOperator :: Text -> Maybe FtsOperator
|
||||||
|
ftsOperator = \case
|
||||||
|
"fts" -> Just FilterFts
|
||||||
|
"plfts" -> Just FilterFtsPlain
|
||||||
|
"phfts" -> Just FilterFtsPhrase
|
||||||
|
"wfts" -> Just FilterFtsWebsearch
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
|
||||||
|
-- PARSERS
|
||||||
|
|
||||||
quantOperator :: Parser QuantOperator
|
|
||||||
quantOperator =
|
|
||||||
try (string "eq" $> OpEqual) <|>
|
|
||||||
try (string "gte" $> OpGreaterThanEqual) <|>
|
|
||||||
try (string "gt" $> OpGreaterThan) <|>
|
|
||||||
try (string "lte" $> OpLessThanEqual) <|>
|
|
||||||
try (string "lt" $> OpLessThan) <|>
|
|
||||||
try (string "like" $> OpLike) <|>
|
|
||||||
try (string "ilike" $> OpILike) <|>
|
|
||||||
try (string "match" $> OpMatch) <|>
|
|
||||||
try (string "imatch" $> OpIMatch) <?>
|
|
||||||
"unknown single value operator"
|
|
||||||
|
|
||||||
pRequestSelect :: Text -> Either QPError [Tree SelectItem]
|
pRequestSelect :: Text -> Either QPError [Tree SelectItem]
|
||||||
pRequestSelect selStr =
|
pRequestSelect selStr =
|
||||||
@@ -236,25 +251,11 @@ pRequestOnConflict :: Text -> Either QPError [FieldName]
|
|||||||
pRequestOnConflict oncStr =
|
pRequestOnConflict oncStr =
|
||||||
mapError $ P.parse pColumns ("failed to parse on_conflict parameter (" <> toS oncStr <> ")") (toS oncStr)
|
mapError $ P.parse pColumns ("failed to parse on_conflict parameter (" <> toS oncStr <> ")") (toS oncStr)
|
||||||
|
|
||||||
-- |
|
pRequestFilter :: (Text, Text) -> Either QPError (EmbedPath, Filter)
|
||||||
-- Parse `id=eq.1`(id, eq.1) into (EmbedPath, Filter)
|
pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
|
||||||
--
|
|
||||||
-- >>> pRequestFilter False ("id", "eq.1")
|
|
||||||
-- Right ([],Filter {field = ("id",[]), opExpr = OpExpr False (OpQuant OpEqual Nothing "1")})
|
|
||||||
--
|
|
||||||
-- >>> pRequestFilter False ("id", "val")
|
|
||||||
-- Left (QPError "\"failed to parse filter (val)\" (line 1, column 1)" "unexpected \"v\" expecting \"not\" or operator (eq, gt, ...)")
|
|
||||||
--
|
|
||||||
-- >>> pRequestFilter True ("id", "val")
|
|
||||||
-- Right ([],Filter {field = ("id",[]), opExpr = NoOpExpr "val"})
|
|
||||||
pRequestFilter :: Bool -> (Text, Text) -> Either QPError (EmbedPath, Filter)
|
|
||||||
pRequestFilter isRpcGet (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
|
|
||||||
where
|
where
|
||||||
treePath = P.parse pTreePath ("failed to parse tree path (" ++ toS k ++ ")") $ toS k
|
treePath = P.parse pTreePath ("failed to parse tree path (" ++ toS k ++ ")") $ toS k
|
||||||
oper = P.parse parseFlt ("failed to parse filter (" ++ toS v ++ ")") $ toS v
|
oper = P.parse (pOpExpr pSingleVal) ("failed to parse filter (" ++ toS v ++ ")") $ toS v
|
||||||
parseFlt = if isRpcGet
|
|
||||||
then pOpExpr pSingleVal <|> pure (NoOpExpr v)
|
|
||||||
else pOpExpr pSingleVal
|
|
||||||
path = fst <$> treePath
|
path = fst <$> treePath
|
||||||
fld = snd <$> treePath
|
fld = snd <$> treePath
|
||||||
|
|
||||||
@@ -313,28 +314,20 @@ pTreePath = do
|
|||||||
-- >>> P.parse pFieldForest "" "*,client(*,nested(*))"
|
-- >>> P.parse pFieldForest "" "*,client(*,nested(*))"
|
||||||
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "client", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "nested", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]}]
|
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "client", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "nested", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]}]
|
||||||
--
|
--
|
||||||
-- >>> P.parse pFieldForest "" "*,...client(*),other(*)"
|
|
||||||
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SpreadRelation {selRelation = "client", selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]},Node {rootLabel = SelectRelation {selRelation = "other", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
|
|
||||||
--
|
|
||||||
-- >>> P.parse pFieldForest "" ""
|
|
||||||
-- Right []
|
|
||||||
--
|
|
||||||
-- >>> P.parse pFieldForest "" "id,clients(name[])"
|
-- >>> P.parse pFieldForest "" "id,clients(name[])"
|
||||||
-- Left (line 1, column 16):
|
-- Left (line 1, column 16):
|
||||||
-- unexpected '['
|
-- unexpected '['
|
||||||
-- expecting letter, digit, "-", "->>", "->", "::", ")", "," or end of input
|
-- expecting letter, digit, "-", "!", "(", "->>", "->", "::", ")", "," or end of input
|
||||||
--
|
|
||||||
-- >>> P.parse pFieldForest "" "data->>-78xy"
|
|
||||||
-- Left (line 1, column 11):
|
|
||||||
-- unexpected 'x'
|
|
||||||
-- expecting digit, "->", "::", ".", "," or end of input
|
|
||||||
pFieldForest :: Parser [Tree SelectItem]
|
pFieldForest :: Parser [Tree SelectItem]
|
||||||
pFieldForest = pFieldTree `sepBy` lexeme (char ',')
|
pFieldForest = pFieldTree `sepBy1` lexeme (char ',')
|
||||||
where
|
where
|
||||||
pFieldTree = Node <$> try pSpreadRelationSelect <*> between (char '(') (char ')') pFieldForest <|>
|
pFieldTree :: Parser (Tree SelectItem)
|
||||||
Node <$> try pRelationSelect <*> between (char '(') (char ')') pFieldForest <|>
|
pFieldTree = try (Node <$> pRelationSelect <*> between (char '(') (char ')') pFieldForest) <|>
|
||||||
Node <$> pFieldSelect <*> pure []
|
Node <$> pFieldSelect <*> pure []
|
||||||
|
|
||||||
|
pStar :: Parser Text
|
||||||
|
pStar = string "*" $> "*"
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- Parse field names
|
-- Parse field names
|
||||||
--
|
--
|
||||||
@@ -400,23 +393,6 @@ pFieldName =
|
|||||||
--
|
--
|
||||||
-- >>> P.parse pJsonPath "" "->0.desc"
|
-- >>> P.parse pJsonPath "" "->0.desc"
|
||||||
-- Right [JArrow {jOp = JIdx {jVal = "+0"}}]
|
-- Right [JArrow {jOp = JIdx {jVal = "+0"}}]
|
||||||
--
|
|
||||||
-- Fails on badly formed negatives
|
|
||||||
--
|
|
||||||
-- >>> P.parse pJsonPath "" "->>-78xy"
|
|
||||||
-- Left (line 1, column 7):
|
|
||||||
-- unexpected 'x'
|
|
||||||
-- expecting digit, "->", "::", ".", "," or end of input
|
|
||||||
--
|
|
||||||
-- >>> P.parse pJsonPath "" "->>--34"
|
|
||||||
-- Left (line 1, column 5):
|
|
||||||
-- unexpected "-"
|
|
||||||
-- expecting digit
|
|
||||||
--
|
|
||||||
-- >>> P.parse pJsonPath "" "->>-xy-4"
|
|
||||||
-- Left (line 1, column 5):
|
|
||||||
-- unexpected "x"
|
|
||||||
-- expecting digit
|
|
||||||
pJsonPath :: Parser JsonPath
|
pJsonPath :: Parser JsonPath
|
||||||
pJsonPath = many pJsonOperation
|
pJsonPath = many pJsonOperation
|
||||||
where
|
where
|
||||||
@@ -472,12 +448,27 @@ aliasSeparator = char ':' >> notFollowedBy (char ':')
|
|||||||
-- Left (line 1, column 6):
|
-- Left (line 1, column 6):
|
||||||
-- unexpected '>'
|
-- unexpected '>'
|
||||||
pRelationSelect :: Parser SelectItem
|
pRelationSelect :: Parser SelectItem
|
||||||
pRelationSelect = lexeme $ do
|
pRelationSelect = lexeme $ try ( do
|
||||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||||
name <- pFieldName
|
name <- pFieldName
|
||||||
(hint, jType) <- pEmbedParams
|
prm1 <- optionMaybe pEmbedParam
|
||||||
|
prm2 <- optionMaybe pEmbedParam
|
||||||
try (void $ lookAhead (string "("))
|
try (void $ lookAhead (string "("))
|
||||||
return $ SelectRelation name alias hint jType
|
return $ SelectRelation name alias (embedParamHint prm1 <|> embedParamHint prm2) (embedParamJoin prm1 <|> embedParamJoin prm2)
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- Parse regular fields in select
|
-- Parse regular fields in select
|
||||||
@@ -515,123 +506,43 @@ pRelationSelect = lexeme $ do
|
|||||||
-- unexpected end of input
|
-- unexpected end of input
|
||||||
-- expecting letter or digit
|
-- expecting letter or digit
|
||||||
pFieldSelect :: Parser SelectItem
|
pFieldSelect :: Parser SelectItem
|
||||||
pFieldSelect = lexeme $ try (do
|
pFieldSelect = lexeme $
|
||||||
|
try (
|
||||||
|
do
|
||||||
|
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
||||||
|
fld <- pField
|
||||||
|
cast' <- optionMaybe (string "::" *> pIdentifier)
|
||||||
|
pEnd
|
||||||
|
return $ SelectField fld (toS <$> cast') alias
|
||||||
|
)
|
||||||
|
<|> do
|
||||||
s <- pStar
|
s <- pStar
|
||||||
pEnd
|
pEnd
|
||||||
return $ SelectField (s, []) Nothing Nothing)
|
return $ SelectField (s, []) Nothing Nothing
|
||||||
<|> do
|
|
||||||
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
|
|
||||||
fld <- pField
|
|
||||||
cast' <- optionMaybe (string "::" *> pIdentifier)
|
|
||||||
pEnd
|
|
||||||
return $ SelectField fld (toS <$> cast') alias
|
|
||||||
where
|
where
|
||||||
pEnd = try (void $ lookAhead (string ")")) <|>
|
pEnd = try (void $ lookAhead (string ")")) <|>
|
||||||
try (void $ lookAhead (string ",")) <|>
|
try (void $ lookAhead (string ",")) <|>
|
||||||
try eof
|
try eof
|
||||||
pStar = string "*" $> "*"
|
|
||||||
|
|
||||||
|
|
||||||
-- |
|
|
||||||
-- Parse spread relations in select
|
|
||||||
--
|
|
||||||
-- >>> P.parse pSpreadRelationSelect "" "...rel(*)"
|
|
||||||
-- Right (SpreadRelation {selRelation = "rel", selHint = Nothing, selJoinType = Nothing})
|
|
||||||
--
|
|
||||||
-- >>> P.parse pSpreadRelationSelect "" "...rel!hint!inner(*)"
|
|
||||||
-- Right (SpreadRelation {selRelation = "rel", selHint = Just "hint", selJoinType = Just JTInner})
|
|
||||||
--
|
|
||||||
-- >>> P.parse pSpreadRelationSelect "" "rel(*)"
|
|
||||||
-- Left (line 1, column 1):
|
|
||||||
-- unexpected "r"
|
|
||||||
-- expecting "..."
|
|
||||||
--
|
|
||||||
-- >>> P.parse pSpreadRelationSelect "" "alias:...rel(*)"
|
|
||||||
-- Left (line 1, column 1):
|
|
||||||
-- unexpected "a"
|
|
||||||
-- expecting "..."
|
|
||||||
--
|
|
||||||
-- >>> P.parse pSpreadRelationSelect "" "...rel->jsonpath(*)"
|
|
||||||
-- Left (line 1, column 9):
|
|
||||||
-- unexpected '>'
|
|
||||||
pSpreadRelationSelect :: Parser SelectItem
|
|
||||||
pSpreadRelationSelect = lexeme $ do
|
|
||||||
name <- string "..." >> pFieldName
|
|
||||||
(hint, jType) <- pEmbedParams
|
|
||||||
try (void $ lookAhead (string "("))
|
|
||||||
return $ SpreadRelation name hint jType
|
|
||||||
|
|
||||||
pEmbedParams :: Parser (Maybe Hint, Maybe JoinType)
|
|
||||||
pEmbedParams = do
|
|
||||||
prm1 <- optionMaybe pEmbedParam
|
|
||||||
prm2 <- optionMaybe pEmbedParam
|
|
||||||
return (embedParamHint prm1 <|> embedParamHint prm2, embedParamJoin prm1 <|> embedParamJoin prm2)
|
|
||||||
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
|
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- Parse operator expression used in horizontal filtering
|
-- Parse operator expression used in horizontal filtering
|
||||||
--
|
--
|
||||||
-- >>> P.parse (pOpExpr pSingleVal) "" "fts().value"
|
-- >>> P.parse (pOpExpr pSingleVal) "" "fts().value"
|
||||||
-- Left (line 1, column 5):
|
-- Left (line 1, column 7):
|
||||||
-- unexpected ")"
|
-- unknown single value operator fts()
|
||||||
-- expecting operator (eq, gt, ...)
|
|
||||||
--
|
|
||||||
-- >>> P.parse (pOpExpr pSingleVal) "" "eq(any).value"
|
|
||||||
-- Right (OpExpr False (OpQuant OpEqual (Just QuantAny) "value"))
|
|
||||||
--
|
|
||||||
-- >>> P.parse (pOpExpr pSingleVal) "" "eq(all).value"
|
|
||||||
-- Right (OpExpr False (OpQuant OpEqual (Just QuantAll) "value"))
|
|
||||||
--
|
|
||||||
-- >>> P.parse (pOpExpr pSingleVal) "" "not.eq(all).value"
|
|
||||||
-- Right (OpExpr True (OpQuant OpEqual (Just QuantAll) "value"))
|
|
||||||
--
|
|
||||||
-- >>> P.parse (pOpExpr pSingleVal) "" "eq().value"
|
|
||||||
-- Left (line 1, column 4):
|
|
||||||
-- unexpected ")"
|
|
||||||
-- expecting operator (eq, gt, ...)
|
|
||||||
--
|
|
||||||
-- >>> P.parse (pOpExpr pSingleVal) "" "is().value"
|
|
||||||
-- Left (line 1, column 3):
|
|
||||||
-- unexpected "("
|
|
||||||
-- expecting operator (eq, gt, ...)
|
|
||||||
--
|
|
||||||
-- >>> P.parse (pOpExpr pSingleVal) "" "in().value"
|
|
||||||
-- Left (line 1, column 3):
|
|
||||||
-- unexpected "("
|
|
||||||
-- expecting operator (eq, gt, ...)
|
|
||||||
pOpExpr :: Parser SingleVal -> Parser OpExpr
|
pOpExpr :: Parser SingleVal -> Parser OpExpr
|
||||||
pOpExpr pSVal = do
|
pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)) <|> OpExpr False <$> pOperation
|
||||||
boolExpr <- try (string "not" *> pDelimiter $> True) <|> pure False
|
|
||||||
OpExpr boolExpr <$> pOperation
|
|
||||||
where
|
where
|
||||||
pOperation :: Parser Operation
|
pOperation :: Parser Operation
|
||||||
pOperation = pIn <|> pIs <|> pIsDist <|> try pFts <|> try pSimpleOp <|> try pQuantOp <?> "operator (eq, gt, ...)"
|
pOperation = pIn <|> pIs <|> try pFts <|> pOp <?> "operator (eq, gt, ...)"
|
||||||
|
|
||||||
pIn = In <$> (try (string "in" *> pDelimiter) *> pListVal)
|
pIn = In <$> (try (string "in" *> pDelimiter) *> pListVal)
|
||||||
pIs = Is <$> (try (string "is" *> pDelimiter) *> pTriVal)
|
pIs = Is <$> (try (string "is" *> pDelimiter) *> pTriVal)
|
||||||
|
|
||||||
pIsDist = IsDistinctFrom <$> (try (string "isdistinct" *> pDelimiter) *> pSVal)
|
pOp = do
|
||||||
|
opStr <- try (P.manyTill anyChar (try pDelimiter))
|
||||||
pSimpleOp = do
|
op <- parseMaybe ("unknown single value operator " <> opStr) . operator $ toS opStr
|
||||||
op <- simpleOperator
|
Op op <$> pSVal
|
||||||
pDelimiter *> (Op op <$> pSVal)
|
|
||||||
|
|
||||||
pQuantOp = do
|
|
||||||
op <- quantOperator
|
|
||||||
quant <- optionMaybe $ try (between (char '(') (char ')') (try (string "any" $> QuantAny) <|> string "all" $> QuantAll))
|
|
||||||
pDelimiter *> (OpQuant op quant <$> pSVal)
|
|
||||||
|
|
||||||
pTriVal = try (ciString "null" $> TriNull)
|
pTriVal = try (ciString "null" $> TriNull)
|
||||||
<|> try (ciString "unknown" $> TriUnknown)
|
<|> try (ciString "unknown" $> TriUnknown)
|
||||||
@@ -640,14 +551,15 @@ pOpExpr pSVal = do
|
|||||||
<?> "null or trilean value (unknown, true, false)"
|
<?> "null or trilean value (unknown, true, false)"
|
||||||
|
|
||||||
pFts = do
|
pFts = do
|
||||||
op <- try (string "fts" $> FilterFts)
|
opStr <- try (P.many (noneOf ".("))
|
||||||
<|> try (string "plfts" $> FilterFtsPlain)
|
op <- parseMaybe ("unknown fts operator " <> opStr) . ftsOperator $ toS opStr
|
||||||
<|> try (string "phfts" $> FilterFtsPhrase)
|
|
||||||
<|> try (string "wfts" $> FilterFtsWebsearch)
|
|
||||||
|
|
||||||
lang <- optionMaybe $ try (between (char '(') (char ')') pIdentifier)
|
lang <- optionMaybe $ try (between (char '(') (char ')') pIdentifier)
|
||||||
pDelimiter >> Fts op (toS <$> lang) <$> pSVal
|
pDelimiter >> Fts op (toS <$> lang) <$> pSVal
|
||||||
|
|
||||||
|
parseMaybe :: [Char] -> Maybe a -> Parser a
|
||||||
|
parseMaybe err Nothing = parserFail err
|
||||||
|
parseMaybe _ (Just x) = pure x
|
||||||
|
|
||||||
-- case insensitive char and string
|
-- case insensitive char and string
|
||||||
ciChar :: Char -> GenParser Char state Char
|
ciChar :: Char -> GenParser Char state Char
|
||||||
ciChar c = char c <|> char (toUpper c)
|
ciChar c = char c <|> char (toUpper c)
|
||||||
@@ -671,119 +583,24 @@ pQuotedValue = toS <$> (char '"' *> many pCharsOrSlashed <* char '"')
|
|||||||
pDelimiter :: Parser Char
|
pDelimiter :: Parser Char
|
||||||
pDelimiter = char '.' <?> "delimiter (.)"
|
pDelimiter = char '.' <?> "delimiter (.)"
|
||||||
|
|
||||||
-- |
|
|
||||||
-- Parses the elements in the order query parameter
|
|
||||||
--
|
|
||||||
-- >>> P.parse pOrder "" "name.desc.nullsfirst"
|
|
||||||
-- Right [OrderTerm {otTerm = ("name",[]), otDirection = Just OrderDesc, otNullOrder = Just OrderNullsFirst}]
|
|
||||||
--
|
|
||||||
-- >>> P.parse pOrder "" "json_col->key.asc.nullslast"
|
|
||||||
-- Right [OrderTerm {otTerm = ("json_col",[JArrow {jOp = JKey {jVal = "key"}}]), otDirection = Just OrderAsc, otNullOrder = Just OrderNullsLast}]
|
|
||||||
--
|
|
||||||
-- >>> P.parse pOrder "" "clients(json_col->key).desc.nullsfirst"
|
|
||||||
-- Right [OrderRelationTerm {otRelation = "clients", otRelTerm = ("json_col",[JArrow {jOp = JKey {jVal = "key"}}]), otDirection = Just OrderDesc, otNullOrder = Just OrderNullsFirst}]
|
|
||||||
--
|
|
||||||
-- >>> P.parse pOrder "" "clients(name,id)"
|
|
||||||
-- Left (line 1, column 8):
|
|
||||||
-- unexpected '('
|
|
||||||
-- expecting letter, digit, "-", "->>", "->", delimiter (.), "," or end of input
|
|
||||||
--
|
|
||||||
-- >>> P.parse pOrder "" "name,clients(name),id"
|
|
||||||
-- Right [OrderTerm {otTerm = ("name",[]), otDirection = Nothing, otNullOrder = Nothing},OrderRelationTerm {otRelation = "clients", otRelTerm = ("name",[]), otDirection = Nothing, otNullOrder = Nothing},OrderTerm {otTerm = ("id",[]), otDirection = Nothing, otNullOrder = Nothing}]
|
|
||||||
--
|
|
||||||
-- >>> P.parse pOrder "" "id.ac"
|
|
||||||
-- Left (line 1, column 4):
|
|
||||||
-- unexpected "c"
|
|
||||||
-- expecting "asc", "desc", "nullsfirst" or "nullslast"
|
|
||||||
--
|
|
||||||
-- >>> P.parse pOrder "" "id.descc"
|
|
||||||
-- Left (line 1, column 8):
|
|
||||||
-- unexpected 'c'
|
|
||||||
-- expecting delimiter (.), "," or end of input
|
|
||||||
--
|
|
||||||
-- >>> P.parse pOrder "" "id.nulsfist"
|
|
||||||
-- Left (line 1, column 4):
|
|
||||||
-- unexpected "n"
|
|
||||||
-- expecting "asc", "desc", "nullsfirst" or "nullslast"
|
|
||||||
--
|
|
||||||
-- >>> P.parse pOrder "" "id.nullslasttt"
|
|
||||||
-- Left (line 1, column 13):
|
|
||||||
-- unexpected 't'
|
|
||||||
-- expecting "," or end of input
|
|
||||||
--
|
|
||||||
-- >>> P.parse pOrder "" "id.smth34"
|
|
||||||
-- Left (line 1, column 4):
|
|
||||||
-- unexpected "s"
|
|
||||||
-- expecting "asc", "desc", "nullsfirst" or "nullslast"
|
|
||||||
--
|
|
||||||
-- >>> P.parse pOrder "" "id.asc.nlsfst"
|
|
||||||
-- Left (line 1, column 8):
|
|
||||||
-- unexpected "l"
|
|
||||||
-- expecting "nullsfirst" or "nullslast"
|
|
||||||
--
|
|
||||||
-- >>> P.parse pOrder "" "id.asc.nullslasttt"
|
|
||||||
-- Left (line 1, column 17):
|
|
||||||
-- unexpected 't'
|
|
||||||
-- expecting "," or end of input
|
|
||||||
--
|
|
||||||
-- >>> P.parse pOrder "" "id.asc.smth34"
|
|
||||||
-- Left (line 1, column 8):
|
|
||||||
-- unexpected "s"
|
|
||||||
-- expecting "nullsfirst" or "nullslast"
|
|
||||||
pOrder :: Parser [OrderTerm]
|
pOrder :: Parser [OrderTerm]
|
||||||
pOrder = lexeme (try pOrderRelationTerm <|> pOrderTerm) `sepBy1` char ','
|
pOrder = lexeme pOrderTerm `sepBy1` char ','
|
||||||
|
|
||||||
|
pOrderTerm :: Parser OrderTerm
|
||||||
|
pOrderTerm = do
|
||||||
|
fld <- pField
|
||||||
|
dir <- optionMaybe $
|
||||||
|
try (pDelimiter *> string "asc" $> OrderAsc) <|>
|
||||||
|
try (pDelimiter *> string "desc" $> OrderDesc)
|
||||||
|
nls <- optionMaybe pNulls <* pEnd <|>
|
||||||
|
pEnd $> Nothing
|
||||||
|
return $ OrderTerm fld dir nls
|
||||||
where
|
where
|
||||||
pOrderTerm = do
|
|
||||||
fld <- pField
|
|
||||||
dir <- optionMaybe pOrdDir
|
|
||||||
nls <- optionMaybe pNulls <* pEnd <|>
|
|
||||||
pEnd $> Nothing
|
|
||||||
return $ OrderTerm fld dir nls
|
|
||||||
|
|
||||||
pOrderRelationTerm = do
|
|
||||||
nam <- pFieldName
|
|
||||||
fld <- between (char '(') (char ')') pField
|
|
||||||
dir <- optionMaybe pOrdDir
|
|
||||||
nls <- optionMaybe pNulls <* pEnd <|> pEnd $> Nothing
|
|
||||||
return $ OrderRelationTerm nam fld dir nls
|
|
||||||
|
|
||||||
pNulls :: Parser OrderNulls
|
|
||||||
pNulls = try (pDelimiter *> string "nullsfirst" $> OrderNullsFirst) <|>
|
pNulls = try (pDelimiter *> string "nullsfirst" $> OrderNullsFirst) <|>
|
||||||
try (pDelimiter *> string "nullslast" $> OrderNullsLast)
|
try (pDelimiter *> string "nullslast" $> OrderNullsLast)
|
||||||
|
pEnd = try (void $ lookAhead (char ',')) <|>
|
||||||
|
try eof
|
||||||
|
|
||||||
pOrdDir :: Parser OrderDirection
|
|
||||||
pOrdDir = try (pDelimiter *> string "asc" $> OrderAsc) <|>
|
|
||||||
try (pDelimiter *> string "desc" $> OrderDesc)
|
|
||||||
|
|
||||||
pEnd = try (void $ lookAhead (char ',')) <|> try eof
|
|
||||||
|
|
||||||
-- |
|
|
||||||
-- Parses the elements inside or/and
|
|
||||||
--
|
|
||||||
-- >>> P.parse pLogicTree "" "or()"
|
|
||||||
-- Left (line 1, column 4):
|
|
||||||
-- unexpected ")"
|
|
||||||
-- expecting field name (* or [a..z0..9_$]), negation operator (not) or logic operator (and, or)
|
|
||||||
--
|
|
||||||
-- >>> P.parse pLogicTree "" "or(id.in.1,2,id.eq.3)"
|
|
||||||
-- Left (line 1, column 10):
|
|
||||||
-- unexpected "1"
|
|
||||||
-- expecting "("
|
|
||||||
--
|
|
||||||
-- >>> P.parse pLogicTree "" "or)("
|
|
||||||
-- Left (line 1, column 3):
|
|
||||||
-- unexpected ")"
|
|
||||||
-- expecting "("
|
|
||||||
--
|
|
||||||
-- >>> P.parse pLogicTree "" "and(ord(id.eq.1,id.eq.1),id.eq.2)"
|
|
||||||
-- Left (line 1, column 7):
|
|
||||||
-- unexpected "d"
|
|
||||||
-- expecting "("
|
|
||||||
--
|
|
||||||
-- >>> P.parse pLogicTree "" "or(id.eq.1,not.xor(id.eq.2,id.eq.3))"
|
|
||||||
-- Left (line 1, column 16):
|
|
||||||
-- unexpected "x"
|
|
||||||
-- expecting logic operator (and, or)
|
|
||||||
pLogicTree :: Parser LogicTree
|
pLogicTree :: Parser LogicTree
|
||||||
pLogicTree = Stmnt <$> try pLogicFilter
|
pLogicTree = Stmnt <$> try pLogicFilter
|
||||||
<|> Expr <$> pNot <*> pLogicOp <*> (lexeme (char '(') *> pLogicTree `sepBy1` lexeme (char ',') <* lexeme (char ')'))
|
<|> Expr <$> pNot <*> pLogicOp <*> (lexeme (char '(') *> pLogicTree `sepBy1` lexeme (char ',') <* lexeme (char ')'))
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ module PostgREST.ApiRequest.Types
|
|||||||
, NodeName
|
, NodeName
|
||||||
, OpExpr(..)
|
, OpExpr(..)
|
||||||
, Operation (..)
|
, Operation (..)
|
||||||
, OpQuantifier(..)
|
|
||||||
, OrderDirection(..)
|
, OrderDirection(..)
|
||||||
, OrderNulls(..)
|
, OrderNulls(..)
|
||||||
, OrderTerm(..)
|
, OrderTerm(..)
|
||||||
@@ -28,7 +27,6 @@ module PostgREST.ApiRequest.Types
|
|||||||
, SingleVal
|
, SingleVal
|
||||||
, TrileanVal(..)
|
, TrileanVal(..)
|
||||||
, SimpleOperator(..)
|
, SimpleOperator(..)
|
||||||
, QuantOperator(..)
|
|
||||||
, FtsOperator(..)
|
, FtsOperator(..)
|
||||||
, SelectItem(..)
|
, SelectItem(..)
|
||||||
) where
|
) where
|
||||||
@@ -36,37 +34,30 @@ module PostgREST.ApiRequest.Types
|
|||||||
import PostgREST.MediaType (MediaType (..))
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier)
|
QualifiedIdentifier)
|
||||||
|
import PostgREST.SchemaCache.Proc (ProcDescription (..))
|
||||||
import PostgREST.SchemaCache.Relationship (Relationship,
|
import PostgREST.SchemaCache.Relationship (Relationship,
|
||||||
RelationshipsMap)
|
RelationshipsMap)
|
||||||
import PostgREST.SchemaCache.Routine (Routine (..))
|
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
-- | The value in `/tbl?select=alias:field::cast`
|
-- | The select value in `/tbl?select=alias:field::cast`
|
||||||
data SelectItem
|
data SelectItem
|
||||||
= SelectField
|
= SelectField
|
||||||
{ selField :: Field
|
{ selField :: Field
|
||||||
, selCast :: Maybe Cast
|
, selCast :: Maybe Cast
|
||||||
, selAlias :: Maybe Alias
|
, selAlias :: Maybe Alias
|
||||||
}
|
}
|
||||||
-- | The value in `/tbl?select=alias:another_tbl(*)`
|
|
||||||
| SelectRelation
|
| SelectRelation
|
||||||
{ selRelation :: FieldName
|
{ selRelation :: FieldName
|
||||||
, selAlias :: Maybe Alias
|
, selAlias :: Maybe Alias
|
||||||
, selHint :: Maybe Hint
|
, selHint :: Maybe Hint
|
||||||
, selJoinType :: Maybe JoinType
|
, selJoinType :: Maybe JoinType
|
||||||
}
|
}
|
||||||
-- | The value in `/tbl?select=...another_tbl(*)`
|
|
||||||
| SpreadRelation
|
|
||||||
{ selRelation :: FieldName
|
|
||||||
, selHint :: Maybe Hint
|
|
||||||
, selJoinType :: Maybe JoinType
|
|
||||||
}
|
|
||||||
deriving (Eq)
|
deriving (Eq)
|
||||||
|
|
||||||
data ApiRequestError
|
data ApiRequestError
|
||||||
= AmbiguousRelBetween Text Text [Relationship]
|
= AmbiguousRelBetween Text Text [Relationship]
|
||||||
| AmbiguousRpc [Routine]
|
| AmbiguousRpc [ProcDescription]
|
||||||
| BinaryFieldError MediaType
|
| BinaryFieldError MediaType
|
||||||
| MediaTypeError [ByteString]
|
| MediaTypeError [ByteString]
|
||||||
| InvalidBody ByteString
|
| InvalidBody ByteString
|
||||||
@@ -76,16 +67,13 @@ data ApiRequestError
|
|||||||
| LimitNoOrderError
|
| LimitNoOrderError
|
||||||
| NotFound
|
| NotFound
|
||||||
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||||
| NoRpc Text Text [Text] Bool MediaType Bool [QualifiedIdentifier] [Routine]
|
| NoRpc Text Text [Text] Bool MediaType Bool [QualifiedIdentifier] [ProcDescription]
|
||||||
| NotEmbedded Text
|
| NotEmbedded Text
|
||||||
| PutLimitNotAllowedError
|
| ParseRequestError Text Text
|
||||||
|
| PutRangeNotAllowedError
|
||||||
| QueryParamError QPError
|
| QueryParamError QPError
|
||||||
| RelatedOrderNotToOne Text Text
|
|
||||||
| SpreadNotToOne Text Text
|
|
||||||
| UnacceptableFilter Text
|
|
||||||
| UnacceptableSchema [Text]
|
| UnacceptableSchema [Text]
|
||||||
| UnsupportedMethod ByteString
|
| UnsupportedMethod ByteString
|
||||||
| ColumnNotFound Text Text
|
|
||||||
|
|
||||||
data QPError = QPError Text Text
|
data QPError = QPError Text Text
|
||||||
data RangeError
|
data RangeError
|
||||||
@@ -96,19 +84,12 @@ data RangeError
|
|||||||
type NodeName = Text
|
type NodeName = Text
|
||||||
type Depth = Integer
|
type Depth = Integer
|
||||||
|
|
||||||
data OrderTerm
|
data OrderTerm = OrderTerm
|
||||||
= OrderTerm
|
{ otTerm :: Field
|
||||||
{ otTerm :: Field
|
, otDirection :: Maybe OrderDirection
|
||||||
, otDirection :: Maybe OrderDirection
|
, otNullOrder :: Maybe OrderNulls
|
||||||
, otNullOrder :: Maybe OrderNulls
|
}
|
||||||
}
|
deriving (Eq)
|
||||||
| OrderRelationTerm
|
|
||||||
{ otRelation :: FieldName
|
|
||||||
, otRelTerm :: Field
|
|
||||||
, otDirection :: Maybe OrderDirection
|
|
||||||
, otNullOrder :: Maybe OrderNulls
|
|
||||||
}
|
|
||||||
deriving Eq
|
|
||||||
|
|
||||||
data OrderDirection
|
data OrderDirection
|
||||||
= OrderAsc
|
= OrderAsc
|
||||||
@@ -149,7 +130,7 @@ type JsonPath = [JsonOperation]
|
|||||||
data JsonOperation
|
data JsonOperation
|
||||||
= JArrow { jOp :: JsonOperand }
|
= JArrow { jOp :: JsonOperand }
|
||||||
| J2Arrow { jOp :: JsonOperand }
|
| J2Arrow { jOp :: JsonOperand }
|
||||||
deriving (Eq, Ord)
|
deriving (Eq)
|
||||||
|
|
||||||
-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text
|
-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text
|
||||||
-- because we reuse our escaping functons and let pg do the casting with
|
-- because we reuse our escaping functons and let pg do the casting with
|
||||||
@@ -157,7 +138,7 @@ data JsonOperation
|
|||||||
data JsonOperand
|
data JsonOperand
|
||||||
= JKey { jVal :: Text }
|
= JKey { jVal :: Text }
|
||||||
| JIdx { jVal :: Text }
|
| JIdx { jVal :: Text }
|
||||||
deriving (Eq, Ord)
|
deriving (Eq)
|
||||||
|
|
||||||
-- | 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:
|
||||||
--
|
--
|
||||||
@@ -176,28 +157,20 @@ data LogicOperator
|
|||||||
| Or
|
| Or
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
data Filter
|
data Filter = Filter
|
||||||
= Filter
|
|
||||||
{ field :: Field
|
{ field :: Field
|
||||||
, opExpr :: OpExpr
|
, opExpr :: OpExpr
|
||||||
}
|
}
|
||||||
| FilterNullEmbed Bool FieldName
|
|
||||||
deriving (Eq)
|
deriving (Eq)
|
||||||
|
|
||||||
data OpExpr
|
data OpExpr =
|
||||||
= OpExpr Bool Operation
|
OpExpr Bool Operation
|
||||||
| NoOpExpr Text
|
|
||||||
deriving (Eq)
|
deriving (Eq)
|
||||||
|
|
||||||
data OpQuantifier = QuantAny | QuantAll
|
|
||||||
deriving Eq
|
|
||||||
|
|
||||||
data Operation
|
data Operation
|
||||||
= Op SimpleOperator SingleVal
|
= Op SimpleOperator SingleVal
|
||||||
| OpQuant QuantOperator (Maybe OpQuantifier) SingleVal
|
|
||||||
| In ListVal
|
| In ListVal
|
||||||
| Is TrileanVal
|
| Is TrileanVal
|
||||||
| IsDistinctFrom SingleVal
|
|
||||||
| Fts FtsOperator (Maybe Language) SingleVal
|
| Fts FtsOperator (Maybe Language) SingleVal
|
||||||
deriving (Eq)
|
deriving (Eq)
|
||||||
|
|
||||||
@@ -217,21 +190,15 @@ data TrileanVal
|
|||||||
| TriUnknown
|
| TriUnknown
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
-- Operators that are quantifiable, i.e. they can be used with the any/all modifiers
|
data SimpleOperator
|
||||||
data QuantOperator
|
|
||||||
= OpEqual
|
= OpEqual
|
||||||
| OpGreaterThanEqual
|
| OpGreaterThanEqual
|
||||||
| OpGreaterThan
|
| OpGreaterThan
|
||||||
| OpLessThanEqual
|
| OpLessThanEqual
|
||||||
| OpLessThan
|
| OpLessThan
|
||||||
|
| OpNotEqual
|
||||||
| OpLike
|
| OpLike
|
||||||
| OpILike
|
| OpILike
|
||||||
| OpMatch
|
|
||||||
| OpIMatch
|
|
||||||
deriving Eq
|
|
||||||
|
|
||||||
data SimpleOperator
|
|
||||||
= OpNotEqual
|
|
||||||
| OpContains
|
| OpContains
|
||||||
| OpContained
|
| OpContained
|
||||||
| OpOverlap
|
| OpOverlap
|
||||||
@@ -240,9 +207,10 @@ data SimpleOperator
|
|||||||
| OpNotExtendsRight
|
| OpNotExtendsRight
|
||||||
| OpNotExtendsLeft
|
| OpNotExtendsLeft
|
||||||
| OpAdjacent
|
| OpAdjacent
|
||||||
|
| OpMatch
|
||||||
|
| OpIMatch
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
--
|
|
||||||
-- | Operators for full text search operators
|
-- | Operators for full text search operators
|
||||||
data FtsOperator
|
data FtsOperator
|
||||||
= FilterFts
|
= FilterFts
|
||||||
|
|||||||
+45
-68
@@ -9,7 +9,6 @@ Some of its functionality includes:
|
|||||||
- Producing HTTP Headers according to RFCs.
|
- Producing HTTP Headers according to RFCs.
|
||||||
- Content Negotiation
|
- Content Negotiation
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
module PostgREST.App
|
module PostgREST.App
|
||||||
( SignalHandlerInstaller
|
( SignalHandlerInstaller
|
||||||
@@ -20,15 +19,13 @@ module PostgREST.App
|
|||||||
|
|
||||||
|
|
||||||
import Control.Monad.Except (liftEither)
|
import Control.Monad.Except (liftEither)
|
||||||
import Data.Either.Combinators (mapLeft, whenLeft)
|
import Data.Either.Combinators (mapLeft)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.String (IsString (..))
|
import Data.String (IsString (..))
|
||||||
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
||||||
setServerName)
|
setServerName)
|
||||||
import System.Posix.Types (FileMode)
|
import System.Posix.Types (FileMode)
|
||||||
|
|
||||||
import qualified Data.HashMap.Strict as HM
|
|
||||||
import qualified Hasql.Pool as SQL
|
|
||||||
import qualified Hasql.Transaction.Sessions as SQL
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
import qualified Network.Wai as Wai
|
import qualified Network.Wai as Wai
|
||||||
import qualified Network.Wai.Handler.Warp as Warp
|
import qualified Network.Wai.Handler.Warp as Warp
|
||||||
@@ -45,17 +42,16 @@ import qualified PostgREST.Query as Query
|
|||||||
import qualified PostgREST.Response as Response
|
import qualified PostgREST.Response as Response
|
||||||
import qualified PostgREST.Workers as Workers
|
import qualified PostgREST.Workers as Workers
|
||||||
|
|
||||||
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
|
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
|
||||||
Mutation (..), Target (..))
|
Mutation (..), Target (..))
|
||||||
import PostgREST.AppState (AppState)
|
import PostgREST.AppState (AppState)
|
||||||
import PostgREST.Auth (AuthResult (..))
|
import PostgREST.Auth (AuthResult (..))
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..), LogLevel (..))
|
||||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||||
import PostgREST.Error (Error)
|
import PostgREST.Error (Error)
|
||||||
import PostgREST.Query (DbHandler)
|
import PostgREST.Query (DbHandler)
|
||||||
import PostgREST.SchemaCache (SchemaCache (..))
|
import PostgREST.SchemaCache (SchemaCache (..))
|
||||||
import PostgREST.SchemaCache.Routine (Routine (..))
|
import PostgREST.Version (prettyVersion)
|
||||||
import PostgREST.Version (prettyVersion)
|
|
||||||
|
|
||||||
import Protolude hiding (Handler)
|
import Protolude hiding (Handler)
|
||||||
|
|
||||||
@@ -75,7 +71,7 @@ run installHandlers maybeRunWithSocket appState = do
|
|||||||
|
|
||||||
Workers.runAdmin conf appState $ serverSettings conf
|
Workers.runAdmin conf appState $ serverSettings conf
|
||||||
|
|
||||||
let app = postgrest conf appState (Workers.connectionWorker appState)
|
let app = postgrest configLogLevel appState (Workers.connectionWorker appState)
|
||||||
|
|
||||||
case configServerUnixSocket of
|
case configServerUnixSocket of
|
||||||
Just socket ->
|
Just socket ->
|
||||||
@@ -99,25 +95,25 @@ serverSettings AppConfig{..} =
|
|||||||
& setServerName ("postgrest/" <> prettyVersion)
|
& setServerName ("postgrest/" <> prettyVersion)
|
||||||
|
|
||||||
-- | PostgREST application
|
-- | PostgREST application
|
||||||
postgrest :: AppConfig -> AppState.AppState -> IO () -> Wai.Application
|
postgrest :: LogLevel -> AppState.AppState -> IO () -> Wai.Application
|
||||||
postgrest conf appState connWorker =
|
postgrest logLevel appState connWorker =
|
||||||
Response.traceHeaderMiddleware conf .
|
|
||||||
Cors.middleware .
|
Cors.middleware .
|
||||||
Auth.middleware appState .
|
Auth.middleware appState .
|
||||||
Logger.middleware (configLogLevel conf) $
|
Logger.middleware logLevel $
|
||||||
-- fromJust can be used, because the auth middleware will **always** add
|
-- fromJust can be used, because the auth middleware will **always** add
|
||||||
-- some AuthResult to the vault.
|
-- some AuthResult to the vault.
|
||||||
\req respond -> case fromJust $ Auth.getResult req of
|
\req respond -> case fromJust $ Auth.getResult req of
|
||||||
Left err -> respond $ Error.errorResponseFor err
|
Left err -> respond $ Error.errorResponseFor err
|
||||||
Right authResult -> do
|
Right authResult -> do
|
||||||
appConf <- AppState.getConfig appState -- the config must be read again because it can reload
|
conf <- AppState.getConfig appState
|
||||||
maybeSchemaCache <- AppState.getSchemaCache appState
|
maybeSchemaCache <- AppState.getSchemaCache appState
|
||||||
pgVer <- AppState.getPgVersion appState
|
pgVer <- AppState.getPgVersion appState
|
||||||
|
jsonDbS <- AppState.getJsonDbS appState
|
||||||
|
|
||||||
let
|
let
|
||||||
eitherResponse :: IO (Either Error Wai.Response)
|
eitherResponse :: IO (Either Error Wai.Response)
|
||||||
eitherResponse =
|
eitherResponse =
|
||||||
runExceptT $ postgrestResponse appState appConf maybeSchemaCache pgVer authResult req
|
runExceptT $ postgrestResponse appState conf maybeSchemaCache jsonDbS pgVer authResult 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
|
||||||
@@ -133,11 +129,12 @@ postgrestResponse
|
|||||||
:: AppState.AppState
|
:: AppState.AppState
|
||||||
-> AppConfig
|
-> AppConfig
|
||||||
-> Maybe SchemaCache
|
-> Maybe SchemaCache
|
||||||
|
-> ByteString
|
||||||
-> PgVersion
|
-> PgVersion
|
||||||
-> AuthResult
|
-> AuthResult
|
||||||
-> Wai.Request
|
-> Wai.Request
|
||||||
-> Handler IO Wai.Response
|
-> Handler IO Wai.Response
|
||||||
postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@AuthResult{..} req = do
|
postgrestResponse appState conf@AppConfig{..} maybeSchemaCache jsonDbS pgVer authResult@AuthResult{..} req = do
|
||||||
sCache <-
|
sCache <-
|
||||||
case maybeSchemaCache of
|
case maybeSchemaCache of
|
||||||
Just sCache ->
|
Just sCache ->
|
||||||
@@ -149,89 +146,69 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@
|
|||||||
|
|
||||||
apiRequest <-
|
apiRequest <-
|
||||||
liftEither . mapLeft Error.ApiRequestError $
|
liftEither . mapLeft Error.ApiRequestError $
|
||||||
ApiRequest.userApiRequest conf req body
|
ApiRequest.userApiRequest conf sCache req body
|
||||||
|
|
||||||
Response.optionalRollback conf apiRequest $
|
Response.optionalRollback conf apiRequest $
|
||||||
handleRequest authResult conf appState (Just authRole /= configDbAnonRole) configDbPreparedStatements pgVer apiRequest sCache
|
handleRequest authResult conf appState (Query.txMode apiRequest) (Just authRole /= configDbAnonRole) configDbPreparedStatements jsonDbS pgVer apiRequest sCache
|
||||||
|
|
||||||
runDbHandler :: AppState.AppState -> Maybe Text -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
|
runDbHandler :: AppState.AppState -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
|
||||||
runDbHandler appState isoLvl mode authenticated prepared handler = do
|
runDbHandler appState mode authenticated prepared handler = do
|
||||||
dbResp <- lift $ do
|
dbResp <-
|
||||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction
|
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
||||||
res <- AppState.usePool appState . transaction (toIsolationLevel isoLvl) mode $ runExceptT handler
|
lift . AppState.usePool appState . transaction SQL.ReadCommitted mode $ runExceptT handler
|
||||||
whenLeft res (\case
|
|
||||||
SQL.AcquisitionTimeoutUsageError -> AppState.debounceLogAcquisitionTimeout appState -- this can happen rapidly for many requests, so we debounce
|
|
||||||
_ -> pure ())
|
|
||||||
return res
|
|
||||||
|
|
||||||
resp <-
|
resp <-
|
||||||
liftEither . mapLeft Error.PgErr $
|
liftEither . mapLeft Error.PgErr $
|
||||||
mapLeft (Error.PgError authenticated) dbResp
|
mapLeft (Error.PgError authenticated) dbResp
|
||||||
|
|
||||||
liftEither resp
|
liftEither resp
|
||||||
where
|
|
||||||
toIsolationLevel = \case
|
|
||||||
Nothing -> SQL.ReadCommitted
|
|
||||||
Just "repeatable read" -> SQL.RepeatableRead
|
|
||||||
Just "serializable" -> SQL.Serializable
|
|
||||||
_ -> SQL.ReadCommitted
|
|
||||||
|
|
||||||
handleRequest :: AuthResult -> AppConfig -> AppState.AppState -> Bool -> Bool -> PgVersion -> ApiRequest -> SchemaCache -> Handler IO Wai.Response
|
handleRequest :: AuthResult -> AppConfig -> AppState.AppState -> SQL.Mode -> Bool -> Bool -> ByteString -> PgVersion -> ApiRequest -> SchemaCache -> Handler IO Wai.Response
|
||||||
handleRequest AuthResult{..} conf appState authenticated prepared pgVer apiReq@ApiRequest{..} sCache =
|
handleRequest AuthResult{..} conf appState mode authenticated prepared jsonDbS pgVer apiReq@ApiRequest{..} sCache =
|
||||||
case (iAction, iTarget) of
|
case (iAction, iTarget) of
|
||||||
(ActionRead headersOnly, TargetIdent identifier) -> do
|
(ActionRead headersOnly, TargetIdent identifier) -> do
|
||||||
wrPlan <- liftEither $ Plan.wrappedReadPlan identifier conf sCache apiReq
|
rPlan <- liftEither $ Plan.readPlan identifier conf sCache apiReq
|
||||||
resultSet <- runQuery roleIsoLvl (Plan.wrTxMode wrPlan) $ Query.readQuery wrPlan conf apiReq
|
resultSet <- runQuery $ Query.readQuery rPlan conf apiReq
|
||||||
return $ Response.readResponse headersOnly identifier apiReq resultSet
|
return $ Response.readResponse headersOnly identifier apiReq resultSet
|
||||||
|
|
||||||
(ActionMutate MutationCreate, TargetIdent identifier) -> do
|
(ActionMutate MutationCreate, TargetIdent identifier) -> do
|
||||||
mrPlan <- liftEither $ Plan.mutateReadPlan MutationCreate apiReq identifier conf sCache
|
mrPlan <- liftEither $ Plan.mutateReadPlan MutationCreate apiReq identifier conf sCache
|
||||||
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.createQuery mrPlan apiReq conf
|
resultSet <- runQuery $ Query.createQuery mrPlan apiReq conf
|
||||||
return $ Response.createResponse identifier mrPlan apiReq resultSet
|
return $ Response.createResponse identifier mrPlan apiReq resultSet
|
||||||
|
|
||||||
(ActionMutate MutationUpdate, TargetIdent identifier) -> do
|
(ActionMutate MutationUpdate, TargetIdent identifier) -> do
|
||||||
mrPlan <- liftEither $ Plan.mutateReadPlan MutationUpdate apiReq identifier conf sCache
|
mrPlan <- liftEither $ Plan.mutateReadPlan MutationUpdate apiReq identifier conf sCache
|
||||||
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.updateQuery mrPlan apiReq conf
|
resultSet <- runQuery $ Query.updateQuery mrPlan apiReq conf
|
||||||
return $ Response.updateResponse apiReq resultSet
|
return $ Response.updateResponse apiReq resultSet
|
||||||
|
|
||||||
(ActionMutate MutationSingleUpsert, TargetIdent identifier) -> do
|
(ActionMutate MutationSingleUpsert, TargetIdent identifier) -> do
|
||||||
mrPlan <- liftEither $ Plan.mutateReadPlan MutationSingleUpsert apiReq identifier conf sCache
|
mrPlan <- liftEither $ Plan.mutateReadPlan MutationSingleUpsert apiReq identifier conf sCache
|
||||||
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.singleUpsertQuery mrPlan apiReq conf
|
resultSet <- runQuery $ Query.singleUpsertQuery mrPlan apiReq conf
|
||||||
return $ Response.singleUpsertResponse apiReq resultSet
|
return $ Response.singleUpsertResponse apiReq resultSet
|
||||||
|
|
||||||
(ActionMutate MutationDelete, TargetIdent identifier) -> do
|
(ActionMutate MutationDelete, TargetIdent identifier) -> do
|
||||||
mrPlan <- liftEither $ Plan.mutateReadPlan MutationDelete apiReq identifier conf sCache
|
mrPlan <- liftEither $ Plan.mutateReadPlan MutationDelete apiReq identifier conf sCache
|
||||||
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.deleteQuery mrPlan apiReq conf
|
resultSet <- runQuery $ Query.deleteQuery mrPlan apiReq conf
|
||||||
return $ Response.deleteResponse apiReq resultSet
|
return $ Response.deleteResponse apiReq resultSet
|
||||||
|
|
||||||
(ActionInvoke invMethod, TargetProc identifier _) -> do
|
(ActionInvoke invMethod, TargetProc proc _) -> do
|
||||||
cPlan <- liftEither $ Plan.callReadPlan identifier conf sCache apiReq invMethod
|
cPlan <- liftEither $ Plan.callReadPlan proc conf sCache apiReq
|
||||||
resultSet <- runQuery (roleIsoLvl <|> pdIsoLvl (Plan.crProc cPlan))(Plan.crTxMode cPlan) $ Query.invokeQuery (Plan.crProc cPlan) cPlan apiReq conf pgVer
|
resultSet <- runQuery $ Query.invokeQuery proc cPlan apiReq conf
|
||||||
return $ Response.invokeResponse invMethod (Plan.crProc cPlan) apiReq resultSet
|
return $ Response.invokeResponse invMethod proc apiReq resultSet
|
||||||
|
|
||||||
(ActionInspect headersOnly, TargetDefaultSpec tSchema) -> do
|
(ActionInspect headersOnly, TargetDefaultSpec tSchema) -> do
|
||||||
oaiResult <- runQuery roleIsoLvl Plan.inspectPlanTxMode $ Query.openApiQuery sCache pgVer conf tSchema
|
oaiResult <- runQuery $ Query.openApiQuery sCache pgVer conf tSchema
|
||||||
return $ Response.openApiResponse headersOnly oaiResult conf sCache iSchema iNegotiatedByProfile
|
return $ Response.openApiResponse headersOnly oaiResult conf sCache iSchema iNegotiatedByProfile
|
||||||
|
|
||||||
(ActionInfo, TargetIdent identifier) ->
|
(ActionInfo, _) ->
|
||||||
return $ Response.infoIdentResponse identifier sCache
|
return $ Response.infoResponse iTarget sCache
|
||||||
|
|
||||||
(ActionInfo, TargetProc identifier _) -> do
|
|
||||||
cPlan <- liftEither $ Plan.callReadPlan identifier conf sCache apiReq ApiRequest.InvHead
|
|
||||||
return $ Response.infoProcResponse (Plan.crProc cPlan)
|
|
||||||
|
|
||||||
(ActionInfo, TargetDefaultSpec _) ->
|
|
||||||
return Response.infoRootResponse
|
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
-- This is unreachable as the ApiRequest.hs rejects it before
|
-- This is unreachable as the ApiRequest.hs rejects it before
|
||||||
-- TODO Refactor the Action/Target types to remove this line
|
-- TODO Refactor the Action/Target types to remove this line
|
||||||
throwError $ Error.ApiRequestError ApiRequestTypes.NotFound
|
throwError $ Error.ApiRequestError ApiRequestTypes.NotFound
|
||||||
where
|
where
|
||||||
roleSettings = fromMaybe mempty (HM.lookup authRole $ configRoleSettings conf)
|
runQuery query =
|
||||||
roleIsoLvl = decodeUtf8 <$> HM.lookup "default_transaction_isolation" roleSettings
|
runDbHandler appState mode authenticated prepared $ do
|
||||||
runQuery isoLvl mode query =
|
Query.setPgLocals conf authClaims authRole apiReq jsonDbS pgVer
|
||||||
runDbHandler appState isoLvl mode authenticated prepared $ do
|
|
||||||
Query.setPgLocals conf authClaims authRole (HM.toList roleSettings) apiReq pgVer
|
|
||||||
Query.runPreReq conf
|
|
||||||
query
|
query
|
||||||
|
|||||||
+30
-42
@@ -7,6 +7,7 @@ module PostgREST.AppState
|
|||||||
, getConfig
|
, getConfig
|
||||||
, getSchemaCache
|
, getSchemaCache
|
||||||
, getIsListenerOn
|
, getIsListenerOn
|
||||||
|
, getJsonDbS
|
||||||
, getMainThreadId
|
, getMainThreadId
|
||||||
, getPgVersion
|
, getPgVersion
|
||||||
, getRetryNextIn
|
, getRetryNextIn
|
||||||
@@ -15,27 +16,22 @@ module PostgREST.AppState
|
|||||||
, init
|
, init
|
||||||
, initWithPool
|
, initWithPool
|
||||||
, logWithZTime
|
, logWithZTime
|
||||||
, logPgrstError
|
|
||||||
, putConfig
|
, putConfig
|
||||||
, putSchemaCache
|
, putSchemaCache
|
||||||
, putIsListenerOn
|
, putIsListenerOn
|
||||||
|
, putJsonDbS
|
||||||
, putPgVersion
|
, putPgVersion
|
||||||
, putRetryNextIn
|
, putRetryNextIn
|
||||||
, signalListener
|
, signalListener
|
||||||
, usePool
|
, usePool
|
||||||
, waitListener
|
, waitListener
|
||||||
, debounceLogAcquisitionTimeout
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Hasql.Pool as SQL
|
||||||
import qualified Data.Text.Encoding as T
|
import qualified Hasql.Session as SQL
|
||||||
import qualified Hasql.Pool as SQL
|
|
||||||
import qualified Hasql.Session as SQL
|
|
||||||
import qualified PostgREST.Error as Error
|
|
||||||
|
|
||||||
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
|
||||||
updateAction)
|
updateAction)
|
||||||
import Control.Debounce
|
|
||||||
import Data.IORef (IORef, atomicWriteIORef, newIORef,
|
import Data.IORef (IORef, atomicWriteIORef, newIORef,
|
||||||
readIORef)
|
readIORef)
|
||||||
import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
|
import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
|
||||||
@@ -51,29 +47,29 @@ import Protolude
|
|||||||
|
|
||||||
data AppState = AppState
|
data AppState = AppState
|
||||||
-- | Database connection pool
|
-- | Database connection pool
|
||||||
{ statePool :: SQL.Pool
|
{ statePool :: SQL.Pool
|
||||||
-- | Database server version, will be updated by the connectionWorker
|
-- | Database server version, will be updated by the connectionWorker
|
||||||
, 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
|
||||||
, stateSchemaCache :: IORef (Maybe SchemaCache)
|
, stateSchemaCache :: IORef (Maybe SchemaCache)
|
||||||
|
-- | Cached SchemaCache in json
|
||||||
|
, stateJsonDbS :: IORef ByteString
|
||||||
-- | Binary semaphore to make sure just one connectionWorker can run at a time
|
-- | Binary semaphore to make sure just one connectionWorker can run at a time
|
||||||
, stateWorkerSem :: MVar ()
|
, stateWorkerSem :: MVar ()
|
||||||
-- | Binary semaphore used to sync the listener(NOTIFY reload) with the connectionWorker.
|
-- | Binary semaphore used to sync the listener(NOTIFY reload) with the connectionWorker.
|
||||||
, stateListener :: MVar ()
|
, stateListener :: MVar ()
|
||||||
-- | State of the LISTEN channel, used for the admin server checks
|
-- | State of the LISTEN channel, used for the admin server checks
|
||||||
, stateIsListenerOn :: IORef Bool
|
, stateIsListenerOn :: IORef Bool
|
||||||
-- | Config that can change at runtime
|
-- | Config that can change at runtime
|
||||||
, stateConf :: IORef AppConfig
|
, stateConf :: IORef AppConfig
|
||||||
-- | Time used for verifying JWT expiration
|
-- | Time used for verifying JWT expiration
|
||||||
, stateGetTime :: IO UTCTime
|
, stateGetTime :: IO UTCTime
|
||||||
-- | Time with time zone used for worker logs
|
-- | Time with time zone used for worker logs
|
||||||
, 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
|
-- | Keeps track of when the next retry for connecting to database is scheduled
|
||||||
, stateRetryNextIn :: IORef Int
|
, stateRetryNextIn :: IORef Int
|
||||||
-- | Logs a pool error with a debounce
|
|
||||||
, debounceLogAcquisitionTimeout :: IO ()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init :: AppConfig -> IO AppState
|
init :: AppConfig -> IO AppState
|
||||||
@@ -82,10 +78,11 @@ init conf = do
|
|||||||
initWithPool pool conf
|
initWithPool pool conf
|
||||||
|
|
||||||
initWithPool :: SQL.Pool -> AppConfig -> IO AppState
|
initWithPool :: SQL.Pool -> AppConfig -> IO AppState
|
||||||
initWithPool pool conf = do
|
initWithPool pool conf =
|
||||||
appState <- AppState pool
|
AppState pool
|
||||||
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
||||||
<*> newIORef Nothing
|
<*> newIORef Nothing
|
||||||
|
<*> newIORef mempty
|
||||||
<*> newEmptyMVar
|
<*> newEmptyMVar
|
||||||
<*> newEmptyMVar
|
<*> newEmptyMVar
|
||||||
<*> newIORef False
|
<*> newIORef False
|
||||||
@@ -94,28 +91,16 @@ initWithPool pool conf = do
|
|||||||
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
<*> mkAutoUpdate defaultUpdateSettings { updateAction = getZonedTime }
|
||||||
<*> myThreadId
|
<*> myThreadId
|
||||||
<*> newIORef 0
|
<*> newIORef 0
|
||||||
<*> pure (pure ())
|
|
||||||
|
|
||||||
deb <-
|
|
||||||
let oneSecond = 1000000 in
|
|
||||||
mkDebounce defaultDebounceSettings
|
|
||||||
{ debounceAction = logPgrstError appState SQL.AcquisitionTimeoutUsageError
|
|
||||||
, debounceFreq = 5*oneSecond
|
|
||||||
, debounceEdge = leadingEdge -- logs at the start and the end
|
|
||||||
}
|
|
||||||
|
|
||||||
return appState { debounceLogAcquisitionTimeout = deb }
|
|
||||||
|
|
||||||
destroy :: AppState -> IO ()
|
destroy :: AppState -> IO ()
|
||||||
destroy = destroyPool
|
destroy = destroyPool
|
||||||
|
|
||||||
initPool :: AppConfig -> IO SQL.Pool
|
initPool :: AppConfig -> IO SQL.Pool
|
||||||
initPool AppConfig{..} =
|
initPool AppConfig{..} =
|
||||||
SQL.acquire
|
SQL.acquire configDbPoolSize timeoutMilliseconds $ toUtf8 configDbUri
|
||||||
configDbPoolSize
|
where
|
||||||
(fromIntegral configDbPoolAcquisitionTimeout)
|
timeoutMilliseconds = (* oneSecond) <$> configDbPoolAcquisitionTimeout
|
||||||
(fromIntegral configDbPoolMaxLifetime)
|
oneSecond = 1000000
|
||||||
(toUtf8 configDbUri)
|
|
||||||
|
|
||||||
-- | Run an action with a database connection.
|
-- | Run an action with a database connection.
|
||||||
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
|
||||||
@@ -142,6 +127,12 @@ getSchemaCache = readIORef . stateSchemaCache
|
|||||||
putSchemaCache :: AppState -> Maybe SchemaCache -> IO ()
|
putSchemaCache :: AppState -> Maybe SchemaCache -> IO ()
|
||||||
putSchemaCache appState = atomicWriteIORef (stateSchemaCache appState)
|
putSchemaCache appState = atomicWriteIORef (stateSchemaCache appState)
|
||||||
|
|
||||||
|
getJsonDbS :: AppState -> IO ByteString
|
||||||
|
getJsonDbS = readIORef . stateJsonDbS
|
||||||
|
|
||||||
|
putJsonDbS :: AppState -> ByteString -> IO ()
|
||||||
|
putJsonDbS appState = atomicWriteIORef (stateJsonDbS appState)
|
||||||
|
|
||||||
getWorkerSem :: AppState -> MVar ()
|
getWorkerSem :: AppState -> MVar ()
|
||||||
getWorkerSem = stateWorkerSem
|
getWorkerSem = stateWorkerSem
|
||||||
|
|
||||||
@@ -166,9 +157,6 @@ logWithZTime appState txt = do
|
|||||||
zTime <- stateGetZTime appState
|
zTime <- stateGetZTime appState
|
||||||
hPutStrLn stderr $ toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <> txt
|
hPutStrLn stderr $ toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <> txt
|
||||||
|
|
||||||
logPgrstError :: AppState -> SQL.UsageError -> IO ()
|
|
||||||
logPgrstError appState e = logWithZTime appState . T.decodeUtf8 . LBS.toStrict $ Error.errorPayload $ Error.PgError False e
|
|
||||||
|
|
||||||
getMainThreadId :: AppState -> ThreadId
|
getMainThreadId :: AppState -> ThreadId
|
||||||
getMainThreadId = stateMainThreadId
|
getMainThreadId = stateMainThreadId
|
||||||
|
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ import qualified Data.Aeson as JSON
|
|||||||
import qualified Data.Aeson.Key as K
|
import qualified Data.Aeson.Key as K
|
||||||
import qualified Data.Aeson.KeyMap as KM
|
import qualified Data.Aeson.KeyMap as KM
|
||||||
import qualified Data.Aeson.Types as JSON
|
import qualified Data.Aeson.Types as JSON
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||||
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Data.Vault.Lazy as Vault
|
import qualified Data.Vault.Lazy as Vault
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
import qualified Network.HTTP.Types.Header as HTTP
|
import qualified Network.HTTP.Types.Header as HTTP
|
||||||
@@ -47,7 +47,7 @@ import Protolude
|
|||||||
|
|
||||||
data AuthResult = AuthResult
|
data AuthResult = AuthResult
|
||||||
{ authClaims :: KM.KeyMap JSON.Value
|
{ authClaims :: KM.KeyMap JSON.Value
|
||||||
, authRole :: BS.ByteString
|
, authRole :: Text
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | Receives the JWT secret and audience (from config) and a JWT and returns a
|
-- | Receives the JWT secret and audience (from config) and a JWT and returns a
|
||||||
@@ -63,7 +63,7 @@ parseToken AppConfig{..} token time = do
|
|||||||
liftEither . mapLeft jwtClaimsError $ JSON.toJSON <$> eitherClaims
|
liftEither . mapLeft jwtClaimsError $ JSON.toJSON <$> eitherClaims
|
||||||
where
|
where
|
||||||
validation =
|
validation =
|
||||||
JWT.defaultJWTValidationSettings audienceCheck & set JWT.allowedSkew 30
|
JWT.defaultJWTValidationSettings audienceCheck & set JWT.allowedSkew 1
|
||||||
|
|
||||||
audienceCheck :: JWT.StringOrURI -> Bool
|
audienceCheck :: JWT.StringOrURI -> Bool
|
||||||
audienceCheck = maybe (const True) (==) configJwtAudience
|
audienceCheck = maybe (const True) (==) configJwtAudience
|
||||||
@@ -79,7 +79,7 @@ parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
|
|||||||
role <- liftEither . maybeToRight JwtTokenRequired $
|
role <- liftEither . maybeToRight JwtTokenRequired $
|
||||||
unquoted <$> walkJSPath (Just jclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
unquoted <$> walkJSPath (Just jclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
||||||
return AuthResult
|
return AuthResult
|
||||||
{ authClaims = mclaims & KM.insert "role" (JSON.toJSON $ decodeUtf8 role)
|
{ authClaims = mclaims & KM.insert "role" (JSON.toJSON role)
|
||||||
, authRole = role
|
, authRole = role
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
@@ -89,9 +89,9 @@ parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
|
|||||||
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
|
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
|
||||||
walkJSPath _ _ = Nothing
|
walkJSPath _ _ = Nothing
|
||||||
|
|
||||||
unquoted :: JSON.Value -> BS.ByteString
|
unquoted :: JSON.Value -> Text
|
||||||
unquoted (JSON.String t) = encodeUtf8 t
|
unquoted (JSON.String t) = t
|
||||||
unquoted v = LBS.toStrict $ JSON.encode v
|
unquoted v = T.decodeUtf8 . LBS.toStrict $ JSON.encode v
|
||||||
-- impossible case - just added to please -Wincomplete-patterns
|
-- impossible case - just added to please -Wincomplete-patterns
|
||||||
parseClaims _ _ = return AuthResult { authClaims = KM.empty, authRole = mempty }
|
parseClaims _ _ = return AuthResult { authClaims = KM.empty, authRole = mempty }
|
||||||
|
|
||||||
@@ -117,5 +117,5 @@ authResultKey = unsafePerformIO Vault.newKey
|
|||||||
getResult :: Wai.Request -> Maybe (Either Error AuthResult)
|
getResult :: Wai.Request -> Maybe (Either Error AuthResult)
|
||||||
getResult = Vault.lookup authResultKey . Wai.vault
|
getResult = Vault.lookup authResultKey . Wai.vault
|
||||||
|
|
||||||
getRole :: Wai.Request -> Maybe BS.ByteString
|
getRole :: Wai.Request -> Maybe Text
|
||||||
getRole req = authRole <$> (rightToMaybe =<< getResult req)
|
getRole req = authRole <$> (rightToMaybe =<< getResult req)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import Protolude hiding (hPutStrLn)
|
|||||||
main :: App.SignalHandlerInstaller -> Maybe App.SocketRunner -> CLI -> IO ()
|
main :: App.SignalHandlerInstaller -> Maybe App.SocketRunner -> CLI -> IO ()
|
||||||
main installSignalHandlers runAppWithSocket CLI{cliCommand, cliPath} = do
|
main installSignalHandlers runAppWithSocket CLI{cliCommand, cliPath} = do
|
||||||
conf@AppConfig{..} <-
|
conf@AppConfig{..} <-
|
||||||
either panic identity <$> Config.readAppConfig mempty cliPath Nothing mempty
|
either panic identity <$> Config.readAppConfig mempty cliPath Nothing
|
||||||
|
|
||||||
-- Per https://github.com/PostgREST/postgrest/issues/268, we want to
|
-- Per https://github.com/PostgREST/postgrest/issues/268, we want to
|
||||||
-- explicitly close the connections to PostgreSQL on shutdown.
|
-- explicitly close the connections to PostgreSQL on shutdown.
|
||||||
@@ -151,9 +151,6 @@ exampleConfigFile =
|
|||||||
|## Time in seconds to wait to acquire a slot from the connection pool
|
|## Time in seconds to wait to acquire a slot from the connection pool
|
||||||
|# db-pool-acquisition-timeout = 10
|
|# db-pool-acquisition-timeout = 10
|
||||||
|
|
|
|
||||||
|## Time in seconds after which to recycle pool connections
|
|
||||||
|# db-pool-max-lifetime = 1800
|
|
||||||
|
|
|
||||||
|## Stored proc to exec immediately after auth
|
|## Stored proc to exec immediately after auth
|
||||||
|# db-pre-request = "stored_proc_name"
|
|# db-pre-request = "stored_proc_name"
|
||||||
|
|
|
|
||||||
|
|||||||
+12
-23
@@ -32,7 +32,6 @@ import qualified Data.Aeson as JSON
|
|||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.ByteString.Base64 as B64
|
import qualified Data.ByteString.Base64 as B64
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.CaseInsensitive as CI
|
|
||||||
import qualified Data.Configurator as C
|
import qualified Data.Configurator as C
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
@@ -51,7 +50,6 @@ import Numeric (readOct, showOct)
|
|||||||
import System.Environment (getEnvironment)
|
import System.Environment (getEnvironment)
|
||||||
import System.Posix.Types (FileMode)
|
import System.Posix.Types (FileMode)
|
||||||
|
|
||||||
import PostgREST.Config.Database (RoleSettings)
|
|
||||||
import PostgREST.Config.JSPath (JSPath, JSPathExp (..),
|
import PostgREST.Config.JSPath (JSPath, JSPathExp (..),
|
||||||
dumpJSPath, pRoleClaimKey)
|
dumpJSPath, pRoleClaimKey)
|
||||||
import PostgREST.Config.Proxy (Proxy (..),
|
import PostgREST.Config.Proxy (Proxy (..),
|
||||||
@@ -65,15 +63,14 @@ import Protolude hiding (Proxy, toList)
|
|||||||
|
|
||||||
data AppConfig = AppConfig
|
data AppConfig = AppConfig
|
||||||
{ configAppSettings :: [(Text, Text)]
|
{ configAppSettings :: [(Text, Text)]
|
||||||
, configDbAnonRole :: Maybe BS.ByteString
|
, configDbAnonRole :: Maybe Text
|
||||||
, configDbChannel :: Text
|
, configDbChannel :: Text
|
||||||
, configDbChannelEnabled :: Bool
|
, configDbChannelEnabled :: Bool
|
||||||
, configDbExtraSearchPath :: [Text]
|
, configDbExtraSearchPath :: [Text]
|
||||||
, configDbMaxRows :: Maybe Integer
|
, configDbMaxRows :: Maybe Integer
|
||||||
, configDbPlanEnabled :: Bool
|
, configDbPlanEnabled :: Bool
|
||||||
, configDbPoolSize :: Int
|
, configDbPoolSize :: Int
|
||||||
, configDbPoolAcquisitionTimeout :: Int
|
, configDbPoolAcquisitionTimeout :: Maybe Int
|
||||||
, configDbPoolMaxLifetime :: Int
|
|
||||||
, configDbPreRequest :: Maybe QualifiedIdentifier
|
, configDbPreRequest :: Maybe QualifiedIdentifier
|
||||||
, configDbPreparedStatements :: Bool
|
, configDbPreparedStatements :: Bool
|
||||||
, configDbRootSpec :: Maybe QualifiedIdentifier
|
, configDbRootSpec :: Maybe QualifiedIdentifier
|
||||||
@@ -96,11 +93,9 @@ data AppConfig = AppConfig
|
|||||||
, configRawMediaTypes :: [MediaType]
|
, configRawMediaTypes :: [MediaType]
|
||||||
, configServerHost :: Text
|
, configServerHost :: Text
|
||||||
, configServerPort :: Int
|
, configServerPort :: Int
|
||||||
, configServerTraceHeader :: Maybe (CI.CI BS.ByteString)
|
|
||||||
, configServerUnixSocket :: Maybe FilePath
|
, configServerUnixSocket :: Maybe FilePath
|
||||||
, configServerUnixSocketMode :: FileMode
|
, configServerUnixSocketMode :: FileMode
|
||||||
, configAdminServerPort :: Maybe Int
|
, configAdminServerPort :: Maybe Int
|
||||||
, configRoleSettings :: RoleSettings
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data LogLevel = LogCrit | LogError | LogWarn | LogInfo
|
data LogLevel = LogCrit | LogError | LogWarn | LogInfo
|
||||||
@@ -128,15 +123,14 @@ toText conf =
|
|||||||
where
|
where
|
||||||
-- apply conf to all pgrst settings
|
-- apply conf to all pgrst settings
|
||||||
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
||||||
[("db-anon-role", q . T.decodeUtf8 . fromMaybe "" . configDbAnonRole)
|
[("db-anon-role", q . fromMaybe "" . configDbAnonRole)
|
||||||
,("db-channel", q . configDbChannel)
|
,("db-channel", q . configDbChannel)
|
||||||
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
|
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
|
||||||
,("db-extra-search-path", q . T.intercalate "," . configDbExtraSearchPath)
|
,("db-extra-search-path", q . T.intercalate "," . configDbExtraSearchPath)
|
||||||
,("db-max-rows", maybe "\"\"" show . configDbMaxRows)
|
,("db-max-rows", maybe "\"\"" show . configDbMaxRows)
|
||||||
,("db-plan-enabled", T.toLower . show . configDbPlanEnabled)
|
,("db-plan-enabled", T.toLower . show . configDbPlanEnabled)
|
||||||
,("db-pool", show . configDbPoolSize)
|
,("db-pool", show . configDbPoolSize)
|
||||||
,("db-pool-acquisition-timeout", show . configDbPoolAcquisitionTimeout)
|
,("db-pool-acquisition-timeout", maybe "\"\"" show . configDbPoolAcquisitionTimeout)
|
||||||
,("db-pool-max-lifetime", show . configDbPoolMaxLifetime)
|
|
||||||
,("db-pre-request", q . maybe mempty dumpQi . 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 dumpQi . configDbRootSpec)
|
,("db-root-spec", q . maybe mempty dumpQi . configDbRootSpec)
|
||||||
@@ -156,7 +150,6 @@ toText conf =
|
|||||||
,("raw-media-types", q . T.decodeUtf8 . BS.intercalate "," . fmap toMime . configRawMediaTypes)
|
,("raw-media-types", q . T.decodeUtf8 . BS.intercalate "," . fmap toMime . configRawMediaTypes)
|
||||||
,("server-host", q . configServerHost)
|
,("server-host", q . configServerHost)
|
||||||
,("server-port", show . configServerPort)
|
,("server-port", show . configServerPort)
|
||||||
,("server-trace-header", q . T.decodeUtf8 . maybe mempty CI.original . configServerTraceHeader)
|
|
||||||
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
|
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
|
||||||
,("server-unix-socket-mode", q . T.pack . showSocketMode)
|
,("server-unix-socket-mode", q . T.pack . showSocketMode)
|
||||||
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
|
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
|
||||||
@@ -193,13 +186,13 @@ instance JustIfMaybe a (Maybe a) where
|
|||||||
|
|
||||||
-- | Reads and parses the config and overrides its parameters from env vars,
|
-- | Reads and parses the config and overrides its parameters from env vars,
|
||||||
-- files or db settings.
|
-- files or db settings.
|
||||||
readAppConfig :: [(Text, Text)] -> Maybe FilePath -> Maybe Text -> RoleSettings -> IO (Either Text AppConfig)
|
readAppConfig :: [(Text, Text)] -> Maybe FilePath -> Maybe Text -> IO (Either Text AppConfig)
|
||||||
readAppConfig dbSettings optPath prevDbUri roleSettings = do
|
readAppConfig dbSettings optPath prevDbUri = do
|
||||||
env <- readPGRSTEnvironment
|
env <- readPGRSTEnvironment
|
||||||
-- if no filename provided, start with an empty map to read config from environment
|
-- if no filename provided, start with an empty map to read config from environment
|
||||||
conf <- maybe (return $ Right M.empty) loadConfig optPath
|
conf <- maybe (return $ Right M.empty) loadConfig optPath
|
||||||
|
|
||||||
case C.runParser (parser optPath env dbSettings roleSettings) =<< mapLeft show conf of
|
case C.runParser (parser optPath env dbSettings) =<< mapLeft show conf of
|
||||||
Left err ->
|
Left err ->
|
||||||
return . Left $ "Error in config " <> err
|
return . Left $ "Error in config " <> err
|
||||||
Right parsedConfig ->
|
Right parsedConfig ->
|
||||||
@@ -214,11 +207,11 @@ readAppConfig dbSettings optPath prevDbUri roleSettings = do
|
|||||||
decodeJWKS <$>
|
decodeJWKS <$>
|
||||||
(decodeSecret =<< readSecretFile =<< readDbUriFile prevDbUri parsedConfig)
|
(decodeSecret =<< readSecretFile =<< readDbUriFile prevDbUri parsedConfig)
|
||||||
|
|
||||||
parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> C.Parser C.Config AppConfig
|
parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> C.Parser C.Config AppConfig
|
||||||
parser optPath env dbSettings roleSettings =
|
parser optPath env dbSettings =
|
||||||
AppConfig
|
AppConfig
|
||||||
<$> parseAppSettings "app.settings"
|
<$> parseAppSettings "app.settings"
|
||||||
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
|
<*> optString "db-anon-role"
|
||||||
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
||||||
<*> (fromMaybe True <$> optBool "db-channel-enabled")
|
<*> (fromMaybe True <$> optBool "db-channel-enabled")
|
||||||
<*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
|
<*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
|
||||||
@@ -226,8 +219,7 @@ parser optPath env dbSettings roleSettings =
|
|||||||
(optInt "max-rows")
|
(optInt "max-rows")
|
||||||
<*> (fromMaybe False <$> optBool "db-plan-enabled")
|
<*> (fromMaybe False <$> optBool "db-plan-enabled")
|
||||||
<*> (fromMaybe 10 <$> optInt "db-pool")
|
<*> (fromMaybe 10 <$> optInt "db-pool")
|
||||||
<*> (fromMaybe 10 <$> optInt "db-pool-acquisition-timeout")
|
<*> optInt "db-pool-acquisition-timeout"
|
||||||
<*> (fromMaybe 1800 <$> optInt "db-pool-max-lifetime")
|
|
||||||
<*> (fmap toQi <$> optWithAlias (optString "db-pre-request")
|
<*> (fmap toQi <$> optWithAlias (optString "db-pre-request")
|
||||||
(optString "pre-request"))
|
(optString "pre-request"))
|
||||||
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
||||||
@@ -255,11 +247,9 @@ parser optPath env dbSettings roleSettings =
|
|||||||
<*> (maybe [] (fmap (MTOther . encodeUtf8) . splitOnCommas) <$> optValue "raw-media-types")
|
<*> (maybe [] (fmap (MTOther . encodeUtf8) . splitOnCommas) <$> optValue "raw-media-types")
|
||||||
<*> (fromMaybe "!4" <$> optString "server-host")
|
<*> (fromMaybe "!4" <$> optString "server-host")
|
||||||
<*> (fromMaybe 3000 <$> optInt "server-port")
|
<*> (fromMaybe 3000 <$> optInt "server-port")
|
||||||
<*> (fmap (CI.mk . encodeUtf8) <$> optString "server-trace-header")
|
|
||||||
<*> (fmap T.unpack <$> optString "server-unix-socket")
|
<*> (fmap T.unpack <$> optString "server-unix-socket")
|
||||||
<*> parseSocketFileMode "server-unix-socket-mode"
|
<*> parseSocketFileMode "server-unix-socket-mode"
|
||||||
<*> optInt "admin-server-port"
|
<*> optInt "admin-server-port"
|
||||||
<*> pure roleSettings
|
|
||||||
where
|
where
|
||||||
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
|
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
|
||||||
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
|
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
|
||||||
@@ -365,8 +355,7 @@ parser optPath env dbSettings roleSettings =
|
|||||||
let dbSettingName = T.pack $ dashToUnderscore <$> toS key in
|
let dbSettingName = T.pack $ dashToUnderscore <$> toS key in
|
||||||
if dbSettingName `notElem` [
|
if dbSettingName `notElem` [
|
||||||
"server_host", "server_port", "server_unix_socket", "server_unix_socket_mode", "admin_server_port", "log_level",
|
"server_host", "server_port", "server_unix_socket", "server_unix_socket_mode", "admin_server_port", "log_level",
|
||||||
"db_uri", "db_channel_enabled", "db_channel", "db_pool", "db_pool_acquisition_timeout",
|
"db_uri", "db_channel_enabled", "db_channel", "db_pool", "db_pool_acquisition_timeout", "db_config"]
|
||||||
"db_pool_max_lifetime", "db_config"]
|
|
||||||
then lookup dbSettingName dbSettings
|
then lookup dbSettingName dbSettings
|
||||||
else Nothing
|
else Nothing
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,11 @@
|
|||||||
module PostgREST.Config.Database
|
module PostgREST.Config.Database
|
||||||
( pgVersionStatement
|
( pgVersionStatement
|
||||||
, queryDbSettings
|
, queryDbSettings
|
||||||
, queryRoleSettings
|
|
||||||
, queryPgVersion
|
, queryPgVersion
|
||||||
, RoleSettings
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Arrow ((***))
|
|
||||||
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||||
|
|
||||||
import qualified Data.HashMap.Strict as HM
|
|
||||||
|
|
||||||
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 Hasql.Session (Session, statement)
|
import Hasql.Session (Session, statement)
|
||||||
@@ -25,13 +19,11 @@ import Text.InterpolatedString.Perl6 (q)
|
|||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
type RoleSettings = (HM.HashMap ByteString (HM.HashMap ByteString ByteString))
|
queryPgVersion :: Session PgVersion
|
||||||
|
queryPgVersion = statement mempty pgVersionStatement
|
||||||
|
|
||||||
queryPgVersion :: Bool -> Session PgVersion
|
pgVersionStatement :: SQL.Statement () PgVersion
|
||||||
queryPgVersion prepared = statement mempty $ pgVersionStatement prepared
|
pgVersionStatement = SQL.Statement sql HE.noParams versionRow False
|
||||||
|
|
||||||
pgVersionStatement :: Bool -> SQL.Statement () PgVersion
|
|
||||||
pgVersionStatement = SQL.Statement sql HE.noParams versionRow
|
|
||||||
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
|
||||||
@@ -39,11 +31,11 @@ pgVersionStatement = SQL.Statement sql HE.noParams versionRow
|
|||||||
queryDbSettings :: Bool -> Session [(Text, Text)]
|
queryDbSettings :: Bool -> Session [(Text, Text)]
|
||||||
queryDbSettings prepared =
|
queryDbSettings prepared =
|
||||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
||||||
transaction SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ dbSettingsStatement prepared
|
transaction SQL.ReadCommitted SQL.Read $ 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 :: Bool -> SQL.Statement () [(Text, Text)]
|
dbSettingsStatement :: SQL.Statement () [(Text, Text)]
|
||||||
dbSettingsStatement = SQL.Statement sql HE.noParams decodeSettings
|
dbSettingsStatement = SQL.Statement sql HE.noParams decodeSettings False
|
||||||
where
|
where
|
||||||
sql = [q|
|
sql = [q|
|
||||||
WITH
|
WITH
|
||||||
@@ -69,45 +61,5 @@ dbSettingsStatement = SQL.Statement sql HE.noParams decodeSettings
|
|||||||
|]
|
|]
|
||||||
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
||||||
|
|
||||||
queryRoleSettings :: Bool -> Session RoleSettings
|
|
||||||
queryRoleSettings prepared =
|
|
||||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
|
||||||
transaction SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ roleSettingsStatement prepared
|
|
||||||
|
|
||||||
roleSettingsStatement :: Bool -> SQL.Statement () RoleSettings
|
|
||||||
roleSettingsStatement = SQL.Statement sql HE.noParams decodeRoleSettings
|
|
||||||
where
|
|
||||||
sql = [q|
|
|
||||||
with
|
|
||||||
role_setting as (
|
|
||||||
select r.rolname, unnest(r.rolconfig) as setting
|
|
||||||
from pg_auth_members m
|
|
||||||
join pg_roles r on r.oid = m.roleid
|
|
||||||
where member = current_user::regrole::oid
|
|
||||||
),
|
|
||||||
kv_settings AS (
|
|
||||||
SELECT
|
|
||||||
rolname,
|
|
||||||
substr(setting, 1, strpos(setting, '=') - 1) as key,
|
|
||||||
lower(substr(setting, strpos(setting, '=') + 1)) as value
|
|
||||||
FROM role_setting
|
|
||||||
)
|
|
||||||
select rolname, array_agg(row(key, value))
|
|
||||||
from kv_settings
|
|
||||||
group by rolname;
|
|
||||||
|]
|
|
||||||
decodeRoleSettings = HM.fromList . map (bimap encodeUtf8 (HM.fromList . ((encodeUtf8 *** encodeUtf8) <$>))) <$> HD.rowList aRow
|
|
||||||
aRow :: HD.Row (Text, [(Text, Text)])
|
|
||||||
aRow = (,) <$> column HD.text <*> compositeArrayColumn ((,) <$> compositeField HD.text <*> compositeField HD.text)
|
|
||||||
|
|
||||||
column :: HD.Value a -> HD.Row a
|
column :: HD.Value a -> HD.Row a
|
||||||
column = HD.column . HD.nonNullable
|
column = HD.column . HD.nonNullable
|
||||||
|
|
||||||
compositeField :: HD.Value a -> HD.Composite a
|
|
||||||
compositeField = HD.field . HD.nonNullable
|
|
||||||
|
|
||||||
compositeArrayColumn :: HD.Composite a -> HD.Row [a]
|
|
||||||
compositeArrayColumn = arrayColumn . HD.composite
|
|
||||||
|
|
||||||
arrayColumn :: HD.Value a -> HD.Row [a]
|
|
||||||
arrayColumn = column . HD.listArray . HD.nonNullable
|
|
||||||
|
|||||||
+19
-51
@@ -39,12 +39,12 @@ import qualified PostgREST.MediaType as MediaType
|
|||||||
|
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
|
import PostgREST.SchemaCache.Proc (ProcDescription (..),
|
||||||
|
ProcParam (..))
|
||||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
Relationship (..),
|
Relationship (..),
|
||||||
RelationshipsMap)
|
RelationshipsMap)
|
||||||
import PostgREST.SchemaCache.Routine (Routine (..),
|
|
||||||
RoutineParam (..))
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
@@ -68,19 +68,15 @@ instance PgrstError ApiRequestError where
|
|||||||
status InvalidRpcMethod{} = HTTP.status405
|
status InvalidRpcMethod{} = HTTP.status405
|
||||||
status InvalidRange{} = HTTP.status416
|
status InvalidRange{} = HTTP.status416
|
||||||
status NotFound = HTTP.status404
|
status NotFound = HTTP.status404
|
||||||
|
|
||||||
status NoRelBetween{} = HTTP.status400
|
status NoRelBetween{} = HTTP.status400
|
||||||
status NoRpc{} = HTTP.status404
|
status NoRpc{} = HTTP.status404
|
||||||
status NotEmbedded{} = HTTP.status400
|
status NotEmbedded{} = HTTP.status400
|
||||||
status PutLimitNotAllowedError = HTTP.status400
|
status ParseRequestError{} = HTTP.status400
|
||||||
|
status PutRangeNotAllowedError = HTTP.status400
|
||||||
status QueryParamError{} = HTTP.status400
|
status QueryParamError{} = HTTP.status400
|
||||||
status RelatedOrderNotToOne{} = HTTP.status400
|
|
||||||
status SpreadNotToOne{} = HTTP.status400
|
|
||||||
status UnacceptableFilter{} = HTTP.status400
|
|
||||||
status UnacceptableSchema{} = HTTP.status406
|
status UnacceptableSchema{} = HTTP.status406
|
||||||
status UnsupportedMethod{} = HTTP.status405
|
status UnsupportedMethod{} = HTTP.status405
|
||||||
status LimitNoOrderError = HTTP.status400
|
status LimitNoOrderError = HTTP.status400
|
||||||
status ColumnNotFound{} = HTTP.status400
|
|
||||||
|
|
||||||
headers _ = [MediaType.toContentType MTApplicationJSON]
|
headers _ = [MediaType.toContentType MTApplicationJSON]
|
||||||
|
|
||||||
@@ -108,6 +104,11 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
LowerGTUpper -> "The lower boundary must be lower than or equal to the upper boundary in the Range header."
|
LowerGTUpper -> "The lower boundary must be lower than or equal to the upper boundary in the Range header."
|
||||||
OutOfBounds lower total -> "An offset of " <> lower <> " was requested, but there are only " <> total <> " rows."),
|
OutOfBounds lower total -> "An offset of " <> lower <> " was requested, but there are only " <> total <> " rows."),
|
||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
|
toJSON (ParseRequestError message details) = JSON.object [
|
||||||
|
"code" .= ApiRequestErrorCode04,
|
||||||
|
"message" .= message,
|
||||||
|
"details" .= details,
|
||||||
|
"hint" .= JSON.Null]
|
||||||
toJSON InvalidFilters = JSON.object [
|
toJSON InvalidFilters = JSON.object [
|
||||||
"code" .= ApiRequestErrorCode05,
|
"code" .= ApiRequestErrorCode05,
|
||||||
"message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text),
|
"message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text),
|
||||||
@@ -126,7 +127,7 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
toJSON NotFound = JSON.object []
|
toJSON NotFound = JSON.object []
|
||||||
toJSON (NotEmbedded resource) = JSON.object [
|
toJSON (NotEmbedded resource) = JSON.object [
|
||||||
"code" .= ApiRequestErrorCode08,
|
"code" .= ApiRequestErrorCode08,
|
||||||
"message" .= ("'" <> resource <> "' is not an embedded resource in this request" :: Text),
|
"message" .= ("Cannot apply filter because '" <> resource <> "' is not an embedded resource in this request" :: Text),
|
||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= ("Verify that '" <> resource <> "' is included in the 'select' query parameter." :: Text)]
|
"hint" .= ("Verify that '" <> resource <> "' is included in the 'select' query parameter." :: Text)]
|
||||||
|
|
||||||
@@ -142,9 +143,9 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
|
|
||||||
toJSON PutLimitNotAllowedError = JSON.object [
|
toJSON PutRangeNotAllowedError = JSON.object [
|
||||||
"code" .= ApiRequestErrorCode14,
|
"code" .= ApiRequestErrorCode14,
|
||||||
"message" .= ("limit/offset querystring parameters are not allowed for PUT" :: Text),
|
"message" .= ("Range header and limit/offset querystring parameters are not allowed for PUT" :: Text),
|
||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
|
|
||||||
@@ -154,24 +155,6 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
|
|
||||||
toJSON (RelatedOrderNotToOne origin target) = JSON.object [
|
|
||||||
"code" .= ApiRequestErrorCode18,
|
|
||||||
"message" .= ("A related order on '" <> target <> "' is not possible" :: Text),
|
|
||||||
"details" .= ("'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship" :: Text),
|
|
||||||
"hint" .= JSON.Null]
|
|
||||||
|
|
||||||
toJSON (SpreadNotToOne origin target) = JSON.object [
|
|
||||||
"code" .= ApiRequestErrorCode19,
|
|
||||||
"message" .= ("A spread operation on '" <> target <> "' is not possible" :: Text),
|
|
||||||
"details" .= ("'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship" :: Text),
|
|
||||||
"hint" .= JSON.Null]
|
|
||||||
|
|
||||||
toJSON (UnacceptableFilter target) = JSON.object [
|
|
||||||
"code" .= ApiRequestErrorCode20,
|
|
||||||
"message" .= ("Bad operator on the '" <> target <> "' embedded resource":: Text),
|
|
||||||
"details" .= ("Only is null or not is null filters are allowed on embedded resources":: Text),
|
|
||||||
"hint" .= JSON.Null]
|
|
||||||
|
|
||||||
toJSON (NoRelBetween parent child embedHint schema allRels) = JSON.object [
|
toJSON (NoRelBetween parent child embedHint schema allRels) = JSON.object [
|
||||||
"code" .= SchemaCacheErrorCode00,
|
"code" .= SchemaCacheErrorCode00,
|
||||||
"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),
|
||||||
@@ -211,11 +194,6 @@ instance JSON.ToJSON ApiRequestError where
|
|||||||
"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]),
|
"message" .= ("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs]),
|
||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text)]
|
"hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text)]
|
||||||
toJSON (ColumnNotFound relName colName) = JSON.object [
|
|
||||||
"code" .= SchemaCacheErrorCode04,
|
|
||||||
"message" .= ("Column '" <> colName <> "' of relation '" <> relName <> "' does not exist" :: Text),
|
|
||||||
"details" .= JSON.Null,
|
|
||||||
"hint" .= JSON.Null]
|
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- If no relationship is found then:
|
-- If no relationship is found then:
|
||||||
@@ -283,7 +261,7 @@ noRelBetweenHint parent child schema allRels = ("Perhaps you meant '" <>) <$>
|
|||||||
-- to all the overloaded functions' params using the form "param1, param2, param3, ..."
|
-- to all the overloaded functions' params using the form "param1, param2, param3, ..."
|
||||||
-- and shows the best match as hint.
|
-- and shows the best match as hint.
|
||||||
--
|
--
|
||||||
-- >>> let procsDesc = [Function {pdParams = [RoutineParam {ppName="val"}, RoutineParam {ppName="param"}, RoutineParam {ppName="name"}]}, Function {pdParams = [RoutineParam {ppName="id"}, RoutineParam {ppName="attr"}]}]
|
-- >>> let procsDesc = [ProcDescription {pdParams = [ProcParam {ppName="val"}, ProcParam {ppName="param"}, ProcParam {ppName="name"}]}, ProcDescription {pdParams = [ProcParam {ppName="id"}, ProcParam {ppName="attr"}]}]
|
||||||
--
|
--
|
||||||
-- >>> noRpcHint "api" "test" ["vall", "pqaram", "nam"] procs procsDesc
|
-- >>> noRpcHint "api" "test" ["vall", "pqaram", "nam"] procs procsDesc
|
||||||
-- Just "Perhaps you meant to call the function api.test(name, param, val)"
|
-- Just "Perhaps you meant to call the function api.test(name, param, val)"
|
||||||
@@ -300,7 +278,7 @@ noRelBetweenHint parent child schema allRels = ("Perhaps you meant '" <>) <$>
|
|||||||
-- >>> noRpcHint "api" "test" ["noclosealternative"] procs procsDesc
|
-- >>> noRpcHint "api" "test" ["noclosealternative"] procs procsDesc
|
||||||
-- Nothing
|
-- Nothing
|
||||||
--
|
--
|
||||||
noRpcHint :: Text -> Text -> [Text] -> [QualifiedIdentifier] -> [Routine] -> Maybe Text
|
noRpcHint :: Text -> Text -> [Text] -> [QualifiedIdentifier] -> [ProcDescription] -> Maybe Text
|
||||||
noRpcHint schema procName params allProcs overloadedProcs =
|
noRpcHint schema procName params allProcs overloadedProcs =
|
||||||
fmap (("Perhaps you meant to call the function " <> schema <> ".") <>) possibleProcs
|
fmap (("Perhaps you meant to call the function " <> schema <> ".") <>) possibleProcs
|
||||||
where
|
where
|
||||||
@@ -376,7 +354,7 @@ instance JSON.ToJSON SQL.UsageError where
|
|||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
toJSON (SQL.SessionUsageError e) = JSON.toJSON e -- SQL.Error
|
toJSON (SQL.SessionUsageError e) = JSON.toJSON e -- SQL.Error
|
||||||
toJSON SQL.AcquisitionTimeoutUsageError = JSON.object [
|
toJSON SQL.AcquisitionTimeoutUsageError = JSON.object [
|
||||||
"code" .= ConnectionErrorCode03,
|
"code" .= ConnectionErrorCode00,
|
||||||
"message" .= ("Timed out acquiring connection from connection pool." :: Text),
|
"message" .= ("Timed out acquiring connection from connection pool." :: Text),
|
||||||
"details" .= JSON.Null,
|
"details" .= JSON.Null,
|
||||||
"hint" .= JSON.Null]
|
"hint" .= JSON.Null]
|
||||||
@@ -446,13 +424,13 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
|
|||||||
|
|
||||||
_ -> HTTP.status500
|
_ -> HTTP.status500
|
||||||
|
|
||||||
checkIsFatal :: SQL.UsageError -> Maybe Text
|
checkIsFatal :: PgError -> Maybe Text
|
||||||
checkIsFatal (SQL.ConnectionUsageError e)
|
checkIsFatal (PgError _ (SQL.ConnectionUsageError e))
|
||||||
| isAuthFailureMessage = Just $ toS failureMessage
|
| isAuthFailureMessage = Just $ toS failureMessage
|
||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
where isAuthFailureMessage = "FATAL: password authentication failed" `isInfixOf` failureMessage
|
where isAuthFailureMessage = "FATAL: password authentication failed" `isInfixOf` failureMessage
|
||||||
failureMessage = BS.unpack $ fromMaybe mempty e
|
failureMessage = BS.unpack $ fromMaybe mempty e
|
||||||
checkIsFatal(SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError serverError)))
|
checkIsFatal (PgError _ (SQL.SessionUsageError (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.
|
||||||
SQL.ServerError "42601" _ _ _ _
|
SQL.ServerError "42601" _ _ _ _
|
||||||
@@ -575,13 +553,12 @@ data ErrorCode
|
|||||||
= ConnectionErrorCode00
|
= ConnectionErrorCode00
|
||||||
| ConnectionErrorCode01
|
| ConnectionErrorCode01
|
||||||
| ConnectionErrorCode02
|
| ConnectionErrorCode02
|
||||||
| ConnectionErrorCode03
|
|
||||||
-- API Request errors
|
-- API Request errors
|
||||||
| ApiRequestErrorCode00
|
| ApiRequestErrorCode00
|
||||||
| ApiRequestErrorCode01
|
| ApiRequestErrorCode01
|
||||||
| ApiRequestErrorCode02
|
| ApiRequestErrorCode02
|
||||||
| ApiRequestErrorCode03
|
| ApiRequestErrorCode03
|
||||||
| ApiRequestErrorCode04 -- no longer used (used to be mapped to ParseRequestError)
|
| ApiRequestErrorCode04
|
||||||
| ApiRequestErrorCode05
|
| ApiRequestErrorCode05
|
||||||
| ApiRequestErrorCode06
|
| ApiRequestErrorCode06
|
||||||
| ApiRequestErrorCode07
|
| ApiRequestErrorCode07
|
||||||
@@ -595,15 +572,11 @@ data ErrorCode
|
|||||||
| ApiRequestErrorCode15
|
| ApiRequestErrorCode15
|
||||||
| ApiRequestErrorCode16
|
| ApiRequestErrorCode16
|
||||||
| ApiRequestErrorCode17
|
| ApiRequestErrorCode17
|
||||||
| ApiRequestErrorCode18
|
|
||||||
| ApiRequestErrorCode19
|
|
||||||
| ApiRequestErrorCode20
|
|
||||||
-- Schema Cache errors
|
-- Schema Cache errors
|
||||||
| SchemaCacheErrorCode00
|
| SchemaCacheErrorCode00
|
||||||
| SchemaCacheErrorCode01
|
| SchemaCacheErrorCode01
|
||||||
| SchemaCacheErrorCode02
|
| SchemaCacheErrorCode02
|
||||||
| SchemaCacheErrorCode03
|
| SchemaCacheErrorCode03
|
||||||
| SchemaCacheErrorCode04
|
|
||||||
-- JWT authentication errors
|
-- JWT authentication errors
|
||||||
| JWTErrorCode00
|
| JWTErrorCode00
|
||||||
| JWTErrorCode01
|
| JWTErrorCode01
|
||||||
@@ -621,7 +594,6 @@ buildErrorCode code = "PGRST" <> case code of
|
|||||||
ConnectionErrorCode00 -> "000"
|
ConnectionErrorCode00 -> "000"
|
||||||
ConnectionErrorCode01 -> "001"
|
ConnectionErrorCode01 -> "001"
|
||||||
ConnectionErrorCode02 -> "002"
|
ConnectionErrorCode02 -> "002"
|
||||||
ConnectionErrorCode03 -> "003"
|
|
||||||
|
|
||||||
ApiRequestErrorCode00 -> "100"
|
ApiRequestErrorCode00 -> "100"
|
||||||
ApiRequestErrorCode01 -> "101"
|
ApiRequestErrorCode01 -> "101"
|
||||||
@@ -641,15 +613,11 @@ buildErrorCode code = "PGRST" <> case code of
|
|||||||
ApiRequestErrorCode15 -> "115"
|
ApiRequestErrorCode15 -> "115"
|
||||||
ApiRequestErrorCode16 -> "116"
|
ApiRequestErrorCode16 -> "116"
|
||||||
ApiRequestErrorCode17 -> "117"
|
ApiRequestErrorCode17 -> "117"
|
||||||
ApiRequestErrorCode18 -> "118"
|
|
||||||
ApiRequestErrorCode19 -> "119"
|
|
||||||
ApiRequestErrorCode20 -> "120"
|
|
||||||
|
|
||||||
SchemaCacheErrorCode00 -> "200"
|
SchemaCacheErrorCode00 -> "200"
|
||||||
SchemaCacheErrorCode01 -> "201"
|
SchemaCacheErrorCode01 -> "201"
|
||||||
SchemaCacheErrorCode02 -> "202"
|
SchemaCacheErrorCode02 -> "202"
|
||||||
SchemaCacheErrorCode03 -> "203"
|
SchemaCacheErrorCode03 -> "203"
|
||||||
SchemaCacheErrorCode04 -> "204"
|
|
||||||
|
|
||||||
JWTErrorCode00 -> "300"
|
JWTErrorCode00 -> "300"
|
||||||
JWTErrorCode01 -> "301"
|
JWTErrorCode01 -> "301"
|
||||||
|
|||||||
@@ -26,5 +26,5 @@ middleware logLevel = case logLevel of
|
|||||||
{ Wai.outputFormat = Wai.ApacheWithSettings $
|
{ Wai.outputFormat = Wai.ApacheWithSettings $
|
||||||
Wai.defaultApacheSettings
|
Wai.defaultApacheSettings
|
||||||
& Wai.setApacheRequestFilter (\_ res -> filterStatus $ Wai.responseStatus res)
|
& Wai.setApacheRequestFilter (\_ res -> filterStatus $ Wai.responseStatus res)
|
||||||
& Wai.setApacheUserGetter Auth.getRole
|
& Wai.setApacheUserGetter (fmap encodeUtf8 . Auth.getRole)
|
||||||
}
|
}
|
||||||
|
|||||||
+47
-259
@@ -13,27 +13,21 @@ resource.
|
|||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE OverloadedRecordDot #-}
|
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
module PostgREST.Plan
|
module PostgREST.Plan
|
||||||
( wrappedReadPlan
|
( readPlan
|
||||||
, mutateReadPlan
|
, mutateReadPlan
|
||||||
, callReadPlan
|
, callReadPlan
|
||||||
, WrappedReadPlan(..)
|
|
||||||
, MutateReadPlan(..)
|
, MutateReadPlan(..)
|
||||||
, CallReadPlan(..)
|
, CallReadPlan(..)
|
||||||
, inspectPlanTxMode
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import qualified Data.HashMap.Strict as HM
|
||||||
|
import qualified Data.Set as S
|
||||||
|
import qualified PostgREST.SchemaCache.Proc as Proc
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import Data.Either.Combinators (mapLeft)
|
||||||
import qualified Data.HashMap.Strict as HM
|
|
||||||
import qualified Data.List as L
|
|
||||||
import qualified Data.Set as S
|
|
||||||
import qualified PostgREST.SchemaCache.Routine as Routine
|
|
||||||
|
|
||||||
import Data.Either.Combinators (mapLeft, mapRight)
|
|
||||||
import Data.List (delete)
|
import Data.List (delete)
|
||||||
import Data.Tree (Tree (..))
|
import Data.Tree (Tree (..))
|
||||||
|
|
||||||
@@ -44,8 +38,6 @@ import PostgREST.ApiRequest (Action (..),
|
|||||||
Payload (..))
|
Payload (..))
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..))
|
||||||
import PostgREST.Error (Error (..))
|
import PostgREST.Error (Error (..))
|
||||||
import PostgREST.MediaType (MTPlanAttrs (..),
|
|
||||||
MediaType (..))
|
|
||||||
import PostgREST.Query.SqlFragment (sourceCTEName)
|
import PostgREST.Query.SqlFragment (sourceCTEName)
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
convertToLimitZeroRange,
|
convertToLimitZeroRange,
|
||||||
@@ -54,150 +46,48 @@ import PostgREST.SchemaCache (SchemaCache (..))
|
|||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
|
import PostgREST.SchemaCache.Proc (ProcDescription (..),
|
||||||
|
ProcParam (..),
|
||||||
|
procReturnsScalar)
|
||||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
Relationship (..),
|
Relationship (..),
|
||||||
RelationshipsMap,
|
RelationshipsMap)
|
||||||
relIsToOne)
|
import PostgREST.SchemaCache.Table (tablePKCols)
|
||||||
import PostgREST.SchemaCache.Routine (Routine (..), RoutineMap,
|
|
||||||
RoutineParam (..),
|
import PostgREST.Plan.CallPlan
|
||||||
funcReturnsCompositeAlias,
|
import PostgREST.Plan.MutatePlan
|
||||||
funcReturnsScalar,
|
import PostgREST.Plan.ReadPlan as ReadPlan
|
||||||
funcReturnsSetOfScalar)
|
|
||||||
import PostgREST.SchemaCache.Table (Table (tableName),
|
|
||||||
tablePKCols)
|
|
||||||
|
|
||||||
import PostgREST.ApiRequest.Preferences
|
import PostgREST.ApiRequest.Preferences
|
||||||
import PostgREST.ApiRequest.Types
|
import PostgREST.ApiRequest.Types
|
||||||
import PostgREST.Plan.CallPlan
|
|
||||||
import PostgREST.Plan.MutatePlan
|
|
||||||
import PostgREST.Plan.ReadPlan as ReadPlan
|
|
||||||
import PostgREST.Plan.Types
|
|
||||||
|
|
||||||
import qualified Hasql.Transaction.Sessions as SQL
|
|
||||||
import qualified PostgREST.ApiRequest.QueryParams as QueryParams
|
import qualified PostgREST.ApiRequest.QueryParams as QueryParams
|
||||||
|
|
||||||
import Protolude hiding (from)
|
import Protolude hiding (from)
|
||||||
|
|
||||||
data WrappedReadPlan = WrappedReadPlan {
|
|
||||||
wrReadPlan :: ReadPlanTree
|
|
||||||
, wrTxMode :: SQL.Mode
|
|
||||||
, wrBinField :: Maybe FieldName
|
|
||||||
}
|
|
||||||
|
|
||||||
data MutateReadPlan = MutateReadPlan {
|
data MutateReadPlan = MutateReadPlan {
|
||||||
mrReadPlan :: ReadPlanTree
|
mrReadPlan :: ReadPlanTree
|
||||||
, mrMutatePlan :: MutatePlan
|
, mrMutatePlan :: MutatePlan
|
||||||
, mrTxMode :: SQL.Mode
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data CallReadPlan = CallReadPlan {
|
data CallReadPlan = CallReadPlan {
|
||||||
crReadPlan :: ReadPlanTree
|
crReadPlan :: ReadPlanTree
|
||||||
, crCallPlan :: CallPlan
|
, crCallPlan :: CallPlan
|
||||||
, crTxMode :: SQL.Mode
|
|
||||||
, crProc :: Routine
|
|
||||||
, crBinField :: Maybe FieldName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error WrappedReadPlan
|
|
||||||
wrappedReadPlan identifier conf sCache apiRequest = do
|
|
||||||
rPlan <- readPlan identifier conf sCache apiRequest
|
|
||||||
binField <- mapLeft ApiRequestError $ binaryField conf (iAcceptMediaType apiRequest) Nothing rPlan
|
|
||||||
return $ WrappedReadPlan rPlan SQL.Read binField
|
|
||||||
|
|
||||||
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error MutateReadPlan
|
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error MutateReadPlan
|
||||||
mutateReadPlan mutation apiRequest identifier conf sCache = do
|
mutateReadPlan mutation apiRequest identifier conf sCache = do
|
||||||
rPlan <- readPlan identifier conf sCache apiRequest
|
rPlan <- readPlan identifier conf sCache apiRequest
|
||||||
mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan
|
mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan
|
||||||
return $ MutateReadPlan rPlan mPlan SQL.Write
|
return $ MutateReadPlan rPlan mPlan
|
||||||
|
|
||||||
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
|
callReadPlan :: ProcDescription -> AppConfig -> SchemaCache -> ApiRequest -> Either Error CallReadPlan
|
||||||
callReadPlan identifier conf sCache apiRequest invMethod = do
|
callReadPlan proc conf sCache apiRequest = do
|
||||||
let paramKeys = case invMethod of
|
let identifier = QualifiedIdentifier (pdSchema proc) (fromMaybe (pdName proc) $ Proc.procTableName proc)
|
||||||
InvGet -> S.fromList $ fst <$> qsParams'
|
rPlan <- readPlan identifier conf sCache apiRequest
|
||||||
InvHead -> S.fromList $ fst <$> qsParams'
|
let cPlan = callPlan proc apiRequest rPlan
|
||||||
InvPost -> iColumns apiRequest
|
return $ CallReadPlan rPlan cPlan
|
||||||
proc@Function{..} <- mapLeft ApiRequestError $
|
|
||||||
findProc identifier paramKeys (preferParameters == Just SingleObject) (dbRoutines sCache) (iContentMediaType apiRequest) (invMethod == InvPost)
|
|
||||||
let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations
|
|
||||||
rPlan <- readPlan relIdentifier conf sCache apiRequest
|
|
||||||
let args = case (invMethod, iContentMediaType apiRequest) of
|
|
||||||
(InvGet, _) -> jsonRpcParams proc qsParams'
|
|
||||||
(InvHead, _) -> jsonRpcParams proc qsParams'
|
|
||||||
(InvPost, MTUrlEncoded) -> maybe mempty (jsonRpcParams proc . payArray) $ iPayload apiRequest
|
|
||||||
(InvPost, _) -> maybe mempty payRaw $ iPayload apiRequest
|
|
||||||
txMode = case (invMethod, pdVolatility) of
|
|
||||||
(InvGet, _) -> SQL.Read
|
|
||||||
(InvHead, _) -> SQL.Read
|
|
||||||
(InvPost, Routine.Stable) -> SQL.Read
|
|
||||||
(InvPost, Routine.Immutable) -> SQL.Read
|
|
||||||
(InvPost, Routine.Volatile) -> SQL.Write
|
|
||||||
cPlan = callPlan proc apiRequest paramKeys args rPlan
|
|
||||||
binField <- mapLeft ApiRequestError $ binaryField conf (iAcceptMediaType apiRequest) (Just proc) rPlan
|
|
||||||
return $ CallReadPlan rPlan cPlan txMode proc binField
|
|
||||||
where
|
|
||||||
Preferences{..} = iPreferences apiRequest
|
|
||||||
qsParams' = QueryParams.qsParams (iQueryParams apiRequest)
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
|
|
||||||
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 -> RoutineMap -> MediaType -> Bool -> Either ApiRequestError Routine
|
|
||||||
findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPost =
|
|
||||||
case matchProc of
|
|
||||||
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentMediaType isInvPost (HM.keys allProcs) lookupProcName
|
|
||||||
-- If there are no functions with named arguments, fallback to the single unnamed argument function
|
|
||||||
([], [proc]) -> Right proc
|
|
||||||
([], procs) -> Left $ AmbiguousRpc (toList procs)
|
|
||||||
-- Matches the functions with named arguments
|
|
||||||
([proc], _) -> Right proc
|
|
||||||
(procs, _) -> Left $ AmbiguousRpc (toList procs)
|
|
||||||
where
|
|
||||||
matchProc = overloadedProcPartition lookupProcName
|
|
||||||
-- First find the proc by name
|
|
||||||
lookupProcName = HM.lookupDefault mempty qi allProcs
|
|
||||||
-- The partition obtained has the form (overloadedProcs,fallbackProcs)
|
|
||||||
-- where fallbackProcs are functions with a single unnamed parameter
|
|
||||||
overloadedProcPartition = foldr select ([],[])
|
|
||||||
select proc ~(ts,fs)
|
|
||||||
| matchesParams proc = (proc:ts,fs)
|
|
||||||
| hasSingleUnnamedParam proc = (ts,proc:fs)
|
|
||||||
| otherwise = (ts,fs)
|
|
||||||
-- If the function is called with post and has a single unnamed parameter
|
|
||||||
-- it can be called depending on content type and the parameter type
|
|
||||||
hasSingleUnnamedParam Function{pdParams=[RoutineParam{ppType}]} = isInvPost && case (contentMediaType, ppType) of
|
|
||||||
(MTApplicationJSON, "json") -> True
|
|
||||||
(MTApplicationJSON, "jsonb") -> True
|
|
||||||
(MTTextPlain, "text") -> True
|
|
||||||
(MTTextXML, "xml") -> True
|
|
||||||
(MTOctetStream, "bytea") -> True
|
|
||||||
_ -> False
|
|
||||||
hasSingleUnnamedParam _ = False
|
|
||||||
matchesParams proc =
|
|
||||||
let
|
|
||||||
params = pdParams proc
|
|
||||||
firstType = (ppType <$> headMay params)
|
|
||||||
in
|
|
||||||
-- exceptional case for Prefer: params=single-object
|
|
||||||
if paramsAsSingleObject
|
|
||||||
then length params == 1 && (firstType == Just "json" || firstType == Just "jsonb")
|
|
||||||
-- If the function has no parameters, the arguments keys must be empty as well
|
|
||||||
else if null params
|
|
||||||
then null argumentsKeys && not (isInvPost && contentMediaType `elem` [MTOctetStream, MTTextPlain, MTTextXML])
|
|
||||||
-- 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)
|
|
||||||
|
|
||||||
inspectPlanTxMode :: SQL.Mode
|
|
||||||
inspectPlanTxMode = SQL.Read
|
|
||||||
|
|
||||||
-- | Builds the ReadPlan tree on a number of stages.
|
-- | Builds the ReadPlan tree on a number of stages.
|
||||||
-- | Adds filters, order, limits on its respective nodes.
|
-- | Adds filters, order, limits on its respective nodes.
|
||||||
@@ -206,9 +96,6 @@ readPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Eit
|
|||||||
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows} SchemaCache{dbRelationships} apiRequest =
|
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows} SchemaCache{dbRelationships} apiRequest =
|
||||||
mapLeft ApiRequestError $
|
mapLeft ApiRequestError $
|
||||||
treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
|
treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
|
||||||
addNullEmbedFilters =<<
|
|
||||||
validateSpreadEmbeds =<<
|
|
||||||
addRelatedOrders =<<
|
|
||||||
addRels qiSchema (iAction apiRequest) dbRelationships Nothing =<<
|
addRels qiSchema (iAction apiRequest) dbRelationships Nothing =<<
|
||||||
addLogicTrees apiRequest =<<
|
addLogicTrees apiRequest =<<
|
||||||
addRanges apiRequest =<<
|
addRanges apiRequest =<<
|
||||||
@@ -221,23 +108,15 @@ initReadRequest qi@QualifiedIdentifier{..} =
|
|||||||
foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi, relName=qiName, depth=rootDepth} []
|
foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi, relName=qiName, depth=rootDepth} []
|
||||||
where
|
where
|
||||||
rootDepth = 0
|
rootDepth = 0
|
||||||
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing False rootDepth
|
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing rootDepth
|
||||||
treeEntry :: Depth -> Tree SelectItem -> ReadPlanTree -> ReadPlanTree
|
treeEntry :: Depth -> Tree SelectItem -> ReadPlanTree -> ReadPlanTree
|
||||||
treeEntry depth (Node si fldForest) (Node q rForest) =
|
treeEntry depth (Node SelectRelation{..} fldForest) (Node q rForest) =
|
||||||
let nxtDepth = succ depth in
|
let nxtDepth = succ depth in
|
||||||
case si of
|
Node q $
|
||||||
SelectRelation{..} ->
|
foldr (treeEntry nxtDepth)
|
||||||
Node q $
|
(Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relAlias=selAlias, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth} [])
|
||||||
foldr (treeEntry nxtDepth)
|
fldForest:rForest
|
||||||
(Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relAlias=selAlias, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth} [])
|
treeEntry _ (Node SelectField{..} _) (Node q rForest) = Node q{select=(selField, selCast, selAlias):select q} rForest
|
||||||
fldForest:rForest
|
|
||||||
SpreadRelation{..} ->
|
|
||||||
Node q $
|
|
||||||
foldr (treeEntry nxtDepth)
|
|
||||||
(Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth, relIsSpread=True} [])
|
|
||||||
fldForest:rForest
|
|
||||||
SelectField{..} ->
|
|
||||||
Node q{select=(selField, selCast, selAlias):select q} rForest
|
|
||||||
|
|
||||||
-- | Enforces the `max-rows` config on the result
|
-- | Enforces the `max-rows` config on the result
|
||||||
treeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
treeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
@@ -399,9 +278,11 @@ addFilters ApiRequest{..} rReq =
|
|||||||
QueryParams.QueryParams{..} = iQueryParams
|
QueryParams.QueryParams{..} = iQueryParams
|
||||||
flts =
|
flts =
|
||||||
case iAction of
|
case iAction of
|
||||||
ActionInvoke _ -> qsFilters
|
ActionInvoke InvGet -> qsFilters
|
||||||
ActionRead _ -> qsFilters
|
ActionInvoke InvHead -> qsFilters
|
||||||
_ -> qsFiltersNotRoot
|
ActionInvoke _ -> qsFilters
|
||||||
|
ActionRead _ -> qsFilters
|
||||||
|
_ -> qsFiltersNotRoot
|
||||||
|
|
||||||
addFilterToNode :: (EmbedPath, Filter) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
addFilterToNode :: (EmbedPath, Filter) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addFilterToNode =
|
addFilterToNode =
|
||||||
@@ -418,47 +299,6 @@ addOrders ApiRequest{..} rReq =
|
|||||||
addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addOrderToNode = updateNode (\o (Node q f) -> Node q{order=o} f)
|
addOrderToNode = updateNode (\o (Node q f) -> Node q{order=o} f)
|
||||||
|
|
||||||
-- Validates that the related resource on the order is an embedded resource,
|
|
||||||
-- e.g. if `clients` is inside the `select` in /projects?order=clients(id)&select=*,clients(*),
|
|
||||||
-- and if it's a to-one relationship, it adds the right alias to the OrderRelationTerm so the generated query can succeed.
|
|
||||||
-- TODO might be clearer if there's an additional intermediate type
|
|
||||||
addRelatedOrders :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
|
||||||
addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|
||||||
newOrder <- getRelOrder `traverse` order
|
|
||||||
Node rp{order=newOrder} <$> addRelatedOrders `traverse` forest
|
|
||||||
where
|
|
||||||
getRelOrder ot@OrderTerm{} = Right ot
|
|
||||||
getRelOrder ot@OrderRelationTerm{otRelation} =
|
|
||||||
let foundRP = rootLabel <$> find (\(Node ReadPlan{relName, relAlias} _) -> otRelation == fromMaybe relName relAlias) forest in
|
|
||||||
case foundRP of
|
|
||||||
Just ReadPlan{relName,relAlias,relAggAlias,relToParent} ->
|
|
||||||
let isToOne = relIsToOne <$> relToParent
|
|
||||||
name = fromMaybe relName relAlias in
|
|
||||||
if isToOne == Just True
|
|
||||||
then Right $ ot{otRelation=relAggAlias}
|
|
||||||
else Left $ RelatedOrderNotToOne (qiName from) name
|
|
||||||
Nothing ->
|
|
||||||
Left $ NotEmbedded otRelation
|
|
||||||
|
|
||||||
-- Searches for null filters on embeds, e.g. `clients` on /projects?select=*,clients()&clients=not.is.null.
|
|
||||||
-- If these are found, it changes the filter to use the internal aggregate name(`projects_clients_1`) so the filter can succeed.
|
|
||||||
-- It fails if operators other than is.null or not.is.null are used.
|
|
||||||
addNullEmbedFilters :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
|
||||||
addNullEmbedFilters (Node rp@ReadPlan{where_=oldLogic} forest) = do
|
|
||||||
let readPlans = rootLabel <$> forest
|
|
||||||
newLogic <- getFilters readPlans `traverse` oldLogic
|
|
||||||
Node rp{ReadPlan.where_= newLogic} <$> (addNullEmbedFilters `traverse` forest)
|
|
||||||
where
|
|
||||||
getFilters :: [ReadPlan] -> LogicTree -> Either ApiRequestError LogicTree
|
|
||||||
getFilters rPlans (Expr b lOp trees) = Expr b lOp <$> (getFilters rPlans `traverse` trees)
|
|
||||||
getFilters rPlans flt@(Stmnt (Filter (fld, []) opExpr)) =
|
|
||||||
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
|
|
||||||
case (foundRP, opExpr) of
|
|
||||||
(Just ReadPlan{relAggAlias}, OpExpr b (Is TriNull)) -> Right $ Stmnt $ FilterNullEmbed b relAggAlias
|
|
||||||
(Just ReadPlan{relName}, _) -> Left $ UnacceptableFilter relName
|
|
||||||
_ -> Right flt
|
|
||||||
getFilters _ flt@(Stmnt _) = Right flt
|
|
||||||
|
|
||||||
addRanges :: ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
addRanges :: ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addRanges ApiRequest{..} rReq =
|
addRanges ApiRequest{..} rReq =
|
||||||
case iAction of
|
case iAction of
|
||||||
@@ -480,15 +320,6 @@ addLogicTrees ApiRequest{..} rReq =
|
|||||||
addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addLogicTreeToNode = updateNode (\t (Node q@ReadPlan{where_=lf} f) -> Node q{ReadPlan.where_=t:lf} f)
|
addLogicTreeToNode = updateNode (\t (Node q@ReadPlan{where_=lf} f) -> Node q{ReadPlan.where_=t:lf} f)
|
||||||
|
|
||||||
-- Validates that spread embeds are only done on to-one relationships
|
|
||||||
validateSpreadEmbeds :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
|
||||||
validateSpreadEmbeds (Node rp@ReadPlan{relToParent=Nothing} forest) = Node rp <$> validateSpreadEmbeds `traverse` forest
|
|
||||||
validateSpreadEmbeds (Node rp@ReadPlan{relIsSpread,relToParent=Just rel,relName} forest) = do
|
|
||||||
validRP <- if relIsSpread && not (relIsToOne rel)
|
|
||||||
then Left $ SpreadNotToOne (qiName $ relTable rel) relName -- TODO using relTable is not entirely right because ReadPlan might have an alias, need to store the parent alias on ReadPlan
|
|
||||||
else Right rp
|
|
||||||
Node validRP <$> validateSpreadEmbeds `traverse` forest
|
|
||||||
|
|
||||||
-- Find a Node of the Tree and apply a function to it
|
-- Find a Node of the Tree and apply a function to it
|
||||||
updateNode :: (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
updateNode :: (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
updateNode f ([], a) rr = f a <$> rr
|
updateNode f ([], a) rr = f a <$> rr
|
||||||
@@ -504,20 +335,19 @@ updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
|
|||||||
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> relName == targetNodeName || relAlias == Just targetNodeName) forest
|
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> relName == targetNodeName || relAlias == Just targetNodeName) forest
|
||||||
|
|
||||||
mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan
|
mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan
|
||||||
mutatePlan mutation qi ApiRequest{iPreferences=preferences, ..} sCache readReq = mapLeft ApiRequestError $
|
mutatePlan mutation qi ApiRequest{..} sCache readReq = mapLeft ApiRequestError $
|
||||||
case mutation of
|
case mutation of
|
||||||
MutationCreate ->
|
MutationCreate ->
|
||||||
mapRight (\typedColumns -> Insert qi typedColumns body ((,) <$> preferences.preferResolution <*> Just confCols) [] returnings pkCols applyDefaults) typedColumnsOrError
|
Right $ Insert qi iColumns body ((,) <$> iPreferResolution <*> Just confCols) [] returnings pkCols
|
||||||
MutationUpdate ->
|
MutationUpdate -> Right $ Update qi iColumns body combinedLogic iTopLevelRange rootOrder returnings
|
||||||
mapRight (\typedColumns -> Update qi typedColumns body combinedLogic iTopLevelRange rootOrder returnings applyDefaults) typedColumnsOrError
|
|
||||||
MutationSingleUpsert ->
|
MutationSingleUpsert ->
|
||||||
if null qsLogic &&
|
if null qsLogic &&
|
||||||
qsFilterFields == S.fromList pkCols &&
|
qsFilterFields == S.fromList pkCols &&
|
||||||
not (null (S.fromList pkCols)) &&
|
not (null (S.fromList pkCols)) &&
|
||||||
all (\case
|
all (\case
|
||||||
Filter _ (OpExpr False (OpQuant OpEqual Nothing _)) -> True
|
Filter _ (OpExpr False (Op OpEqual _)) -> True
|
||||||
_ -> False) qsFiltersRoot
|
_ -> False) qsFiltersRoot
|
||||||
then mapRight (\typedColumns -> Insert qi typedColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty False) typedColumnsOrError
|
then Right $ Insert qi iColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty
|
||||||
else
|
else
|
||||||
Left InvalidFilters
|
Left InvalidFilters
|
||||||
MutationDelete -> Right $ Delete qi combinedLogic iTopLevelRange rootOrder returnings
|
MutationDelete -> Right $ Delete qi combinedLogic iTopLevelRange rootOrder returnings
|
||||||
@@ -525,7 +355,7 @@ mutatePlan mutation qi ApiRequest{iPreferences=preferences, ..} sCache readReq =
|
|||||||
confCols = fromMaybe pkCols qsOnConflict
|
confCols = fromMaybe pkCols qsOnConflict
|
||||||
QueryParams.QueryParams{..} = iQueryParams
|
QueryParams.QueryParams{..} = iQueryParams
|
||||||
returnings =
|
returnings =
|
||||||
if preferences.preferRepresentation == None
|
if iPreferRepresentation == None
|
||||||
then []
|
then []
|
||||||
else inferColsEmbedNeeds readReq pkCols
|
else inferColsEmbedNeeds readReq pkCols
|
||||||
pkCols = maybe mempty tablePKCols $ HM.lookup qi $ dbTables sCache
|
pkCols = maybe mempty tablePKCols $ HM.lookup qi $ dbTables sCache
|
||||||
@@ -533,35 +363,24 @@ mutatePlan mutation qi ApiRequest{iPreferences=preferences, ..} sCache readReq =
|
|||||||
rootOrder = maybe [] snd $ find (\(x, _) -> null x) qsOrder
|
rootOrder = maybe [] snd $ find (\(x, _) -> null x) qsOrder
|
||||||
combinedLogic = foldr addFilterToLogicForest logic qsFiltersRoot
|
combinedLogic = foldr addFilterToLogicForest logic qsFiltersRoot
|
||||||
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
|
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
|
||||||
tbl = HM.lookup qi $ dbTables sCache
|
|
||||||
typedColumnsOrError = resolveOrError tbl `traverse` S.toList iColumns
|
|
||||||
applyDefaults = preferences.preferMissing == Just ApplyDefaults
|
|
||||||
|
|
||||||
resolveOrError :: Maybe Table -> FieldName -> Either ApiRequestError TypedField
|
callPlan :: ProcDescription -> ApiRequest -> ReadPlanTree -> CallPlan
|
||||||
resolveOrError Nothing _ = Left NotFound
|
callPlan proc apiReq readReq = FunctionCall {
|
||||||
resolveOrError (Just table) field =
|
|
||||||
case resolveTableField table field of
|
|
||||||
Nothing -> Left $ ColumnNotFound (tableName table) field
|
|
||||||
Just typedField -> Right typedField
|
|
||||||
|
|
||||||
callPlan :: Routine -> ApiRequest -> S.Set FieldName -> LBS.ByteString -> ReadPlanTree -> CallPlan
|
|
||||||
callPlan proc ApiRequest{iPreferences=Preferences{..}} paramKeys args readReq = FunctionCall {
|
|
||||||
funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc)
|
funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc)
|
||||||
, funCParams = callParams
|
, funCParams = callParams
|
||||||
, funCArgs = Just args
|
, funCArgs = payRaw <$> iPayload apiReq
|
||||||
, funCScalar = funcReturnsScalar proc
|
, funCScalar = procReturnsScalar proc
|
||||||
, funCSetOfScalar = funcReturnsSetOfScalar proc
|
, funCMultipleCall = iPreferParameters apiReq == Just MultipleObjects
|
||||||
, funCRetCompositeAlias = funcReturnsCompositeAlias proc
|
|
||||||
, funCReturning = inferColsEmbedNeeds readReq []
|
, funCReturning = inferColsEmbedNeeds readReq []
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
paramsAsSingleObject = preferParameters == Just SingleObject
|
paramsAsSingleObject = iPreferParameters apiReq == Just SingleObject
|
||||||
specifiedParams = filter (\x -> ppName x `S.member` paramKeys)
|
|
||||||
callParams = case pdParams proc of
|
callParams = case pdParams proc of
|
||||||
[prm] | paramsAsSingleObject -> OnePosParam prm
|
[prm] | paramsAsSingleObject -> OnePosParam prm
|
||||||
| ppName prm == mempty -> OnePosParam prm
|
| ppName prm == mempty -> OnePosParam prm
|
||||||
| otherwise -> KeyParams $ specifiedParams [prm]
|
| otherwise -> KeyParams $ specifiedParams [prm]
|
||||||
prms -> KeyParams $ specifiedParams prms
|
prms -> KeyParams $ specifiedParams prms
|
||||||
|
specifiedParams = filter (\x -> ppName x `S.member` iColumns apiReq)
|
||||||
|
|
||||||
-- | Infers the columns needed for an embed to be successful after a mutation or a function call.
|
-- | Infers the columns needed for an embed to be successful after a mutation or a function call.
|
||||||
inferColsEmbedNeeds :: ReadPlanTree -> [FieldName] -> [FieldName]
|
inferColsEmbedNeeds :: ReadPlanTree -> [FieldName] -> [FieldName]
|
||||||
@@ -612,34 +431,3 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
|||||||
-- they are later concatenated with AND in the QueryBuilder
|
-- they are later concatenated with AND in the QueryBuilder
|
||||||
addFilterToLogicForest :: Filter -> [LogicTree] -> [LogicTree]
|
addFilterToLogicForest :: Filter -> [LogicTree] -> [LogicTree]
|
||||||
addFilterToLogicForest flt lf = Stmnt flt : lf
|
addFilterToLogicForest flt lf = Stmnt flt : lf
|
||||||
|
|
||||||
-- | If raw(binary) output is requested, check that MediaType is one of the
|
|
||||||
-- admitted rawMediaTypes and that`?select=...` contains only one field other
|
|
||||||
-- than `*`
|
|
||||||
binaryField :: AppConfig -> MediaType -> Maybe Routine -> ReadPlanTree -> Either ApiRequestError (Maybe FieldName)
|
|
||||||
binaryField AppConfig{configRawMediaTypes} acceptMediaType proc rpTree
|
|
||||||
| isRawMediaType =
|
|
||||||
if (funcReturnsScalar <$> proc) == Just True ||
|
|
||||||
(funcReturnsSetOfScalar <$> proc) == Just True
|
|
||||||
then Right $ Just "pgrst_scalar"
|
|
||||||
else
|
|
||||||
let
|
|
||||||
fieldName = fstFieldName rpTree
|
|
||||||
in
|
|
||||||
case fieldName of
|
|
||||||
Just fld -> Right $ Just fld
|
|
||||||
Nothing -> Left $ BinaryFieldError acceptMediaType
|
|
||||||
| otherwise =
|
|
||||||
Right Nothing
|
|
||||||
where
|
|
||||||
isRawMediaType = acceptMediaType `elem` configRawMediaTypes `L.union` [MTOctetStream, MTTextPlain, MTTextXML] || isRawPlan acceptMediaType
|
|
||||||
isRawPlan mt = case mt of
|
|
||||||
MTPlan (MTPlanAttrs (Just MTOctetStream) _ _) -> True
|
|
||||||
MTPlan (MTPlanAttrs (Just MTTextPlain) _ _) -> True
|
|
||||||
MTPlan (MTPlanAttrs (Just MTTextXML) _ _) -> True
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
fstFieldName :: ReadPlanTree -> Maybe FieldName
|
|
||||||
fstFieldName (Node ReadPlan{select=(("*", []), _, _):_} []) = Nothing
|
|
||||||
fstFieldName (Node ReadPlan{select=[((fld, []), _, _)]} []) = Just fld
|
|
||||||
fstFieldName _ = Nothing
|
|
||||||
|
|||||||
@@ -1,57 +1,25 @@
|
|||||||
{-# LANGUAGE NamedFieldPuns #-}
|
|
||||||
module PostgREST.Plan.CallPlan
|
module PostgREST.Plan.CallPlan
|
||||||
( CallPlan(..)
|
( CallPlan(..)
|
||||||
, CallParams(..)
|
, CallParams(..)
|
||||||
, jsonRpcParams
|
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.HashMap.Strict as HM
|
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier)
|
QualifiedIdentifier)
|
||||||
import PostgREST.SchemaCache.Routine (Routine (..),
|
import PostgREST.SchemaCache.Proc (ProcParam (..))
|
||||||
RoutineParam (..))
|
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
data CallPlan = FunctionCall
|
data CallPlan = FunctionCall
|
||||||
{ funCQi :: QualifiedIdentifier
|
{ funCQi :: QualifiedIdentifier
|
||||||
, funCParams :: CallParams
|
, funCParams :: CallParams
|
||||||
, funCArgs :: Maybe LBS.ByteString
|
, funCArgs :: Maybe LBS.ByteString
|
||||||
, funCScalar :: Bool
|
, funCScalar :: Bool
|
||||||
, funCSetOfScalar :: Bool
|
, funCMultipleCall :: Bool
|
||||||
, funCRetCompositeAlias :: Bool
|
, funCReturning :: [FieldName]
|
||||||
, funCReturning :: [FieldName]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data CallParams
|
data CallParams
|
||||||
= KeyParams [RoutineParam] -- ^ Call with key params: func(a := val1, b:= val2)
|
= KeyParams [ProcParam] -- ^ Call with key params: func(a := val1, b:= val2)
|
||||||
| OnePosParam RoutineParam -- ^ Call with positional params(only one supported): func(val)
|
| OnePosParam ProcParam -- ^ Call with positional params(only one supported): func(val)
|
||||||
|
|
||||||
-- | Convert rpc params `/rpc/func?a=val1&b=val2` to json `{"a": "val1", "b": "val2"}
|
|
||||||
jsonRpcParams :: Routine -> [(Text, Text)] -> LBS.ByteString
|
|
||||||
jsonRpcParams proc prms =
|
|
||||||
if not $ pdHasVariadic proc then -- if proc has no variadic param, save steps and directly convert to json
|
|
||||||
JSON.encode $ HM.fromList $ second JSON.toJSON <$> prms
|
|
||||||
else
|
|
||||||
let paramsMap = HM.fromListWith mergeParams $ toRpcParamValue proc <$> prms in
|
|
||||||
JSON.encode paramsMap
|
|
||||||
where
|
|
||||||
mergeParams :: RpcParamValue -> RpcParamValue -> RpcParamValue
|
|
||||||
mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a
|
|
||||||
mergeParams v _ = v -- repeated params for non-variadic parameters are not merged
|
|
||||||
|
|
||||||
toRpcParamValue :: Routine -> (Text, Text) -> (Text, RpcParamValue)
|
|
||||||
toRpcParamValue proc (k, v) | prmIsVariadic k = (k, Variadic [v])
|
|
||||||
| otherwise = (k, Fixed v)
|
|
||||||
where
|
|
||||||
prmIsVariadic prm = isJust $ find (\RoutineParam{ppName, ppVar} -> ppName == prm && ppVar) $ pdParams proc
|
|
||||||
|
|
||||||
-- | RPC query param value `/rpc/func?v=<value>`, used for VARIADIC functions on form-urlencoded POST and GETs
|
|
||||||
-- | It can be fixed `?v=1` or repeated `?v=1&v=2&v=3.
|
|
||||||
data RpcParamValue = Fixed Text | Variadic [Text]
|
|
||||||
instance JSON.ToJSON RpcParamValue where
|
|
||||||
toJSON (Fixed v) = JSON.toJSON v
|
|
||||||
toJSON (Variadic v) = JSON.toJSON v
|
|
||||||
|
|||||||
@@ -4,37 +4,34 @@ module PostgREST.Plan.MutatePlan
|
|||||||
where
|
where
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
import qualified Data.Set as S
|
||||||
|
|
||||||
import PostgREST.ApiRequest.Preferences (PreferResolution)
|
import PostgREST.ApiRequest.Preferences (PreferResolution)
|
||||||
import PostgREST.ApiRequest.Types (LogicTree, OrderTerm)
|
import PostgREST.ApiRequest.Types (LogicTree, OrderTerm)
|
||||||
import PostgREST.Plan.Types (TypedField)
|
|
||||||
import PostgREST.RangeQuery (NonnegRange)
|
import PostgREST.RangeQuery (NonnegRange)
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier)
|
QualifiedIdentifier)
|
||||||
|
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
data MutatePlan
|
data MutatePlan
|
||||||
= Insert
|
= Insert
|
||||||
{ in_ :: QualifiedIdentifier
|
{ in_ :: QualifiedIdentifier
|
||||||
, insCols :: [TypedField]
|
, insCols :: S.Set FieldName
|
||||||
, insBody :: Maybe LBS.ByteString
|
, insBody :: Maybe LBS.ByteString
|
||||||
, onConflict :: Maybe (PreferResolution, [FieldName])
|
, onConflict :: Maybe (PreferResolution, [FieldName])
|
||||||
, where_ :: [LogicTree]
|
, where_ :: [LogicTree]
|
||||||
, returning :: [FieldName]
|
, returning :: [FieldName]
|
||||||
, insPkCols :: [FieldName]
|
, insPkCols :: [FieldName]
|
||||||
, applyDefs :: Bool
|
|
||||||
}
|
}
|
||||||
| Update
|
| Update
|
||||||
{ in_ :: QualifiedIdentifier
|
{ in_ :: QualifiedIdentifier
|
||||||
, updCols :: [TypedField]
|
, updCols :: S.Set FieldName
|
||||||
, updBody :: Maybe LBS.ByteString
|
, updBody :: Maybe LBS.ByteString
|
||||||
, where_ :: [LogicTree]
|
, where_ :: [LogicTree]
|
||||||
, mutRange :: NonnegRange
|
, mutRange :: NonnegRange
|
||||||
, mutOrder :: [OrderTerm]
|
, mutOrder :: [OrderTerm]
|
||||||
, returning :: [FieldName]
|
, returning :: [FieldName]
|
||||||
, applyDefs :: Bool
|
|
||||||
}
|
}
|
||||||
| Delete
|
| Delete
|
||||||
{ in_ :: QualifiedIdentifier
|
{ in_ :: QualifiedIdentifier
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ data ReadPlan = ReadPlan
|
|||||||
, relAggAlias :: Alias
|
, relAggAlias :: Alias
|
||||||
, relHint :: Maybe Hint
|
, relHint :: Maybe Hint
|
||||||
, relJoinType :: Maybe JoinType
|
, relJoinType :: Maybe JoinType
|
||||||
, relIsSpread :: Bool
|
|
||||||
, depth :: Depth
|
, depth :: Depth
|
||||||
-- ^ used for aliasing
|
-- ^ used for aliasing
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
module PostgREST.Plan.Types
|
|
||||||
( TypedField(..)
|
|
||||||
, resolveTableField
|
|
||||||
) where
|
|
||||||
|
|
||||||
import qualified Data.HashMap.Strict.InsOrd as HMI
|
|
||||||
|
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName)
|
|
||||||
import PostgREST.SchemaCache.Table (Column (..), Table (..))
|
|
||||||
|
|
||||||
import Protolude
|
|
||||||
|
|
||||||
-- | A TypedField is a field with sufficient information to be read from JSON with `json_to_recordset`.
|
|
||||||
data TypedField = TypedField
|
|
||||||
{ tfName :: FieldName
|
|
||||||
, tfIRType :: Text -- ^ The initial type of the field, before any casting.
|
|
||||||
, tfDefault :: Maybe Text
|
|
||||||
} deriving (Eq)
|
|
||||||
|
|
||||||
resolveTableField :: Table -> FieldName -> Maybe TypedField
|
|
||||||
resolveTableField table fieldName =
|
|
||||||
case HMI.lookup fieldName (tableColumns table) of
|
|
||||||
Just column -> Just $ TypedField (colName column) (colNominalType column) (colDefault column)
|
|
||||||
Nothing -> Nothing
|
|
||||||
+77
-62
@@ -7,16 +7,15 @@ module PostgREST.Query
|
|||||||
, openApiQuery
|
, openApiQuery
|
||||||
, readQuery
|
, readQuery
|
||||||
, singleUpsertQuery
|
, singleUpsertQuery
|
||||||
|
, txMode
|
||||||
, updateQuery
|
, updateQuery
|
||||||
, setPgLocals
|
, setPgLocals
|
||||||
, runPreReq
|
|
||||||
, DbHandler
|
, DbHandler
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.Aeson.Key as K
|
import qualified Data.Aeson.Key as K
|
||||||
import qualified Data.Aeson.KeyMap as KM
|
import qualified Data.Aeson.KeyMap as KM
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
@@ -24,23 +23,25 @@ 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 SQL (Snippet)
|
import qualified Hasql.DynamicStatements.Snippet as SQL (Snippet)
|
||||||
import qualified Hasql.DynamicStatements.Statement as SQL
|
import qualified Hasql.DynamicStatements.Statement as SQL
|
||||||
import qualified Hasql.Encoders as HE
|
|
||||||
import qualified Hasql.Statement as SQL
|
|
||||||
import qualified Hasql.Transaction as SQL
|
import qualified Hasql.Transaction as SQL
|
||||||
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
|
|
||||||
import qualified PostgREST.Error as Error
|
import qualified PostgREST.Error as Error
|
||||||
import qualified PostgREST.Query.QueryBuilder as QueryBuilder
|
import qualified PostgREST.Query.QueryBuilder as QueryBuilder
|
||||||
import qualified PostgREST.Query.Statements as Statements
|
import qualified PostgREST.Query.Statements as Statements
|
||||||
import qualified PostgREST.RangeQuery as RangeQuery
|
import qualified PostgREST.RangeQuery as RangeQuery
|
||||||
import qualified PostgREST.SchemaCache as SchemaCache
|
import qualified PostgREST.SchemaCache as SchemaCache
|
||||||
import qualified PostgREST.SchemaCache.Routine as Routine
|
import qualified PostgREST.SchemaCache.Proc as Proc
|
||||||
|
|
||||||
import Data.Scientific (FPFormat (..), formatScientific, isInteger)
|
import Data.Scientific (FPFormat (..), formatScientific, isInteger)
|
||||||
|
|
||||||
import PostgREST.ApiRequest (ApiRequest (..))
|
import PostgREST.ApiRequest (Action (..),
|
||||||
|
ApiRequest (..),
|
||||||
|
InvokeMethod (..),
|
||||||
|
Target (..))
|
||||||
import PostgREST.ApiRequest.Preferences (PreferCount (..),
|
import PostgREST.ApiRequest.Preferences (PreferCount (..),
|
||||||
|
PreferParameters (..),
|
||||||
PreferTransaction (..),
|
PreferTransaction (..),
|
||||||
Preferences (..),
|
|
||||||
shouldCount)
|
shouldCount)
|
||||||
import PostgREST.Config (AppConfig (..),
|
import PostgREST.Config (AppConfig (..),
|
||||||
OpenAPIMode (..))
|
OpenAPIMode (..))
|
||||||
@@ -49,9 +50,9 @@ import PostgREST.Config.PgVersion (PgVersion (..),
|
|||||||
import PostgREST.Error (Error)
|
import PostgREST.Error (Error)
|
||||||
import PostgREST.MediaType (MediaType (..))
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import PostgREST.Plan (CallReadPlan (..),
|
import PostgREST.Plan (CallReadPlan (..),
|
||||||
MutateReadPlan (..),
|
MutateReadPlan (..))
|
||||||
WrappedReadPlan (..))
|
|
||||||
import PostgREST.Plan.MutatePlan (MutatePlan (..))
|
import PostgREST.Plan.MutatePlan (MutatePlan (..))
|
||||||
|
import PostgREST.Plan.ReadPlan (ReadPlanTree)
|
||||||
import PostgREST.Query.SqlFragment (fromQi, intercalateSnippet,
|
import PostgREST.Query.SqlFragment (fromQi, intercalateSnippet,
|
||||||
pgFmtIdentList,
|
pgFmtIdentList,
|
||||||
setConfigLocal,
|
setConfigLocal,
|
||||||
@@ -60,29 +61,31 @@ import PostgREST.Query.Statements (ResultSet (..))
|
|||||||
import PostgREST.SchemaCache (SchemaCache (..))
|
import PostgREST.SchemaCache (SchemaCache (..))
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
import PostgREST.SchemaCache.Routine (Routine (..), RoutineMap)
|
import PostgREST.SchemaCache.Proc (ProcDescription (..),
|
||||||
|
ProcVolatility (..),
|
||||||
|
ProcsMap)
|
||||||
import PostgREST.SchemaCache.Table (TablesMap)
|
import PostgREST.SchemaCache.Table (TablesMap)
|
||||||
|
|
||||||
import Protolude hiding (Handler)
|
import Protolude hiding (Handler)
|
||||||
|
|
||||||
type DbHandler = ExceptT Error SQL.Transaction
|
type DbHandler = ExceptT Error SQL.Transaction
|
||||||
|
|
||||||
readQuery :: WrappedReadPlan -> AppConfig -> ApiRequest -> DbHandler ResultSet
|
readQuery :: ReadPlanTree -> AppConfig -> ApiRequest -> DbHandler ResultSet
|
||||||
readQuery WrappedReadPlan{wrReadPlan, wrBinField} conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}, ..} = do
|
readQuery req conf@AppConfig{..} apiReq@ApiRequest{..} = do
|
||||||
let countQuery = QueryBuilder.readPlanToCountQuery wrReadPlan
|
let countQuery = QueryBuilder.readPlanToCountQuery req
|
||||||
resultSet <-
|
resultSet <-
|
||||||
lift . SQL.statement mempty $
|
lift . SQL.statement mempty $
|
||||||
Statements.prepareRead
|
Statements.prepareRead
|
||||||
(QueryBuilder.readPlanToQuery wrReadPlan)
|
(QueryBuilder.readPlanToQuery req)
|
||||||
(if preferCount == Just EstimatedCount then
|
(if iPreferCount == Just EstimatedCount then
|
||||||
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
||||||
QueryBuilder.limitedQuery countQuery ((+ 1) <$> configDbMaxRows)
|
QueryBuilder.limitedQuery countQuery ((+ 1) <$> configDbMaxRows)
|
||||||
else
|
else
|
||||||
countQuery
|
countQuery
|
||||||
)
|
)
|
||||||
(shouldCount preferCount)
|
(shouldCount iPreferCount)
|
||||||
iAcceptMediaType
|
iAcceptMediaType
|
||||||
wrBinField
|
iBinaryField
|
||||||
configDbPreparedStatements
|
configDbPreparedStatements
|
||||||
failNotSingular iAcceptMediaType resultSet
|
failNotSingular iAcceptMediaType resultSet
|
||||||
optionalRollback conf apiReq
|
optionalRollback conf apiReq
|
||||||
@@ -90,8 +93,8 @@ readQuery WrappedReadPlan{wrReadPlan, wrBinField} conf@AppConfig{..} apiReq@ApiR
|
|||||||
|
|
||||||
resultSetWTotal :: AppConfig -> ApiRequest -> ResultSet -> SQL.Snippet -> DbHandler ResultSet
|
resultSetWTotal :: AppConfig -> ApiRequest -> ResultSet -> SQL.Snippet -> DbHandler ResultSet
|
||||||
resultSetWTotal _ _ rs@RSPlan{} _ = return rs
|
resultSetWTotal _ _ rs@RSPlan{} _ = return rs
|
||||||
resultSetWTotal AppConfig{..} ApiRequest{iPreferences=Preferences{..}} rs@RSStandard{rsTableTotal=tableTotal} countQuery =
|
resultSetWTotal AppConfig{..} ApiRequest{..} rs@RSStandard{rsTableTotal=tableTotal} countQuery =
|
||||||
case preferCount of
|
case iPreferCount of
|
||||||
Just PlannedCount -> do
|
Just PlannedCount -> do
|
||||||
total <- explain
|
total <- explain
|
||||||
return rs{rsTableTotal=total}
|
return rs{rsTableTotal=total}
|
||||||
@@ -152,45 +155,63 @@ deleteQuery mrPlan apiReq@ApiRequest{..} conf = do
|
|||||||
optionalRollback conf apiReq
|
optionalRollback conf apiReq
|
||||||
pure resultSet
|
pure resultSet
|
||||||
|
|
||||||
invokeQuery :: Routine -> CallReadPlan -> ApiRequest -> AppConfig -> PgVersion -> DbHandler ResultSet
|
invokeQuery :: ProcDescription -> CallReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
|
||||||
invokeQuery proc CallReadPlan{crReadPlan, crCallPlan, crBinField} apiReq@ApiRequest{iPreferences=Preferences{..}, ..} conf@AppConfig{..} pgVer = do
|
invokeQuery proc CallReadPlan{crReadPlan, crCallPlan} apiReq@ApiRequest{..} conf@AppConfig{..} = do
|
||||||
resultSet <-
|
resultSet <-
|
||||||
lift . SQL.statement mempty $
|
lift . SQL.statement mempty $
|
||||||
Statements.prepareCall
|
Statements.prepareCall
|
||||||
(Routine.funcReturnsScalar proc)
|
(Proc.procReturnsScalar proc)
|
||||||
(Routine.funcReturnsSingleComposite proc)
|
(Proc.procReturnsSingle proc)
|
||||||
(Routine.funcReturnsSetOfScalar proc)
|
(QueryBuilder.callPlanToQuery crCallPlan)
|
||||||
(QueryBuilder.callPlanToQuery crCallPlan pgVer)
|
|
||||||
(QueryBuilder.readPlanToQuery crReadPlan)
|
(QueryBuilder.readPlanToQuery crReadPlan)
|
||||||
(QueryBuilder.readPlanToCountQuery crReadPlan)
|
(QueryBuilder.readPlanToCountQuery crReadPlan)
|
||||||
(shouldCount preferCount)
|
(shouldCount iPreferCount)
|
||||||
iAcceptMediaType
|
iAcceptMediaType
|
||||||
crBinField
|
(iPreferParameters == Just MultipleObjects)
|
||||||
|
iBinaryField
|
||||||
configDbPreparedStatements
|
configDbPreparedStatements
|
||||||
|
|
||||||
optionalRollback conf apiReq
|
optionalRollback conf apiReq
|
||||||
failNotSingular iAcceptMediaType resultSet
|
failNotSingular iAcceptMediaType resultSet
|
||||||
pure resultSet
|
pure resultSet
|
||||||
|
|
||||||
openApiQuery :: SchemaCache -> PgVersion -> AppConfig -> Schema -> DbHandler (Maybe (TablesMap, RoutineMap, Maybe Text))
|
openApiQuery :: SchemaCache -> PgVersion -> AppConfig -> Schema -> DbHandler (Maybe (TablesMap, ProcsMap, Maybe Text))
|
||||||
openApiQuery sCache pgVer AppConfig{..} tSchema =
|
openApiQuery sCache pgVer AppConfig{..} tSchema =
|
||||||
lift $ case configOpenApiMode of
|
lift $ case configOpenApiMode of
|
||||||
OAFollowPriv -> do
|
OAFollowPriv -> do
|
||||||
tableAccess <- SQL.statement [tSchema] (SchemaCache.accessibleTables pgVer configDbPreparedStatements)
|
tableAccess <- SQL.statement [tSchema] (SchemaCache.accessibleTables pgVer configDbPreparedStatements)
|
||||||
Just <$> ((,,)
|
Just <$> ((,,)
|
||||||
(HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache)
|
(HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache)
|
||||||
<$> SQL.statement tSchema (SchemaCache.accessibleFuncs pgVer configDbPreparedStatements)
|
<$> SQL.statement tSchema (SchemaCache.accessibleProcs pgVer configDbPreparedStatements)
|
||||||
<*> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
|
<*> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
|
||||||
OAIgnorePriv ->
|
OAIgnorePriv ->
|
||||||
Just <$> ((,,)
|
Just <$> ((,,)
|
||||||
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbTables sCache)
|
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbTables sCache)
|
||||||
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbRoutines sCache)
|
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbProcs sCache)
|
||||||
<$> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
|
<$> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
|
||||||
OADisabled ->
|
OADisabled ->
|
||||||
pure Nothing
|
pure Nothing
|
||||||
|
|
||||||
|
txMode :: ApiRequest -> SQL.Mode
|
||||||
|
txMode ApiRequest{..} =
|
||||||
|
case (iAction, iTarget) of
|
||||||
|
(ActionRead _, _) ->
|
||||||
|
SQL.Read
|
||||||
|
(ActionInspect _, _) ->
|
||||||
|
SQL.Read
|
||||||
|
(ActionInvoke InvGet, _) ->
|
||||||
|
SQL.Read
|
||||||
|
(ActionInvoke InvHead, _) ->
|
||||||
|
SQL.Read
|
||||||
|
(ActionInvoke InvPost, TargetProc ProcDescription{pdVolatility=Stable} _) ->
|
||||||
|
SQL.Read
|
||||||
|
(ActionInvoke InvPost, TargetProc ProcDescription{pdVolatility=Immutable} _) ->
|
||||||
|
SQL.Read
|
||||||
|
_ ->
|
||||||
|
SQL.Write
|
||||||
|
|
||||||
writeQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
|
writeQuery :: MutateReadPlan -> ApiRequest -> AppConfig -> DbHandler ResultSet
|
||||||
writeQuery MutateReadPlan{mrReadPlan, mrMutatePlan} apiReq@ApiRequest{iPreferences=Preferences{..}} conf =
|
writeQuery MutateReadPlan{mrReadPlan, mrMutatePlan} apiReq conf =
|
||||||
let
|
let
|
||||||
(isInsert, pkCols) = case mrMutatePlan of {Insert{insPkCols} -> (True, insPkCols); _ -> (False, mempty);}
|
(isInsert, pkCols) = case mrMutatePlan of {Insert{insPkCols} -> (True, insPkCols); _ -> (False, mempty);}
|
||||||
in
|
in
|
||||||
@@ -200,7 +221,7 @@ writeQuery MutateReadPlan{mrReadPlan, mrMutatePlan} apiReq@ApiRequest{iPreferenc
|
|||||||
(QueryBuilder.mutatePlanToQuery mrMutatePlan)
|
(QueryBuilder.mutatePlanToQuery mrMutatePlan)
|
||||||
isInsert
|
isInsert
|
||||||
(iAcceptMediaType apiReq)
|
(iAcceptMediaType apiReq)
|
||||||
preferRepresentation
|
(iPreferRepresentation apiReq)
|
||||||
pkCols
|
pkCols
|
||||||
(configDbPreparedStatements conf)
|
(configDbPreparedStatements conf)
|
||||||
|
|
||||||
@@ -224,23 +245,24 @@ failsChangesOffLimits (Just maxChanges) RSStandard{rsQueryTotal=queryTotal} =
|
|||||||
|
|
||||||
-- | Set a transaction to roll back if requested
|
-- | Set a transaction to roll back if requested
|
||||||
optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
|
optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
|
||||||
optionalRollback AppConfig{..} ApiRequest{iPreferences=Preferences{..}} = do
|
optionalRollback AppConfig{..} ApiRequest{..} = do
|
||||||
lift $ when (shouldRollback || (configDbTxRollbackAll && not shouldCommit)) $ do
|
lift $ when (shouldRollback || (configDbTxRollbackAll && not shouldCommit)) $ do
|
||||||
SQL.sql "SET CONSTRAINTS ALL IMMEDIATE"
|
SQL.sql "SET CONSTRAINTS ALL IMMEDIATE"
|
||||||
SQL.condemn
|
SQL.condemn
|
||||||
where
|
where
|
||||||
shouldCommit =
|
shouldCommit =
|
||||||
configDbTxAllowOverride && preferTransaction == Just Commit
|
configDbTxAllowOverride && iPreferTransaction == Just Commit
|
||||||
shouldRollback =
|
shouldRollback =
|
||||||
configDbTxAllowOverride && preferTransaction == Just Rollback
|
configDbTxAllowOverride && iPreferTransaction == Just Rollback
|
||||||
|
|
||||||
-- | Runs local (transaction scoped) GUCs for every request.
|
-- | Runs local(transaction scoped) GUCs for every request, plus the pre-request function
|
||||||
setPgLocals :: AppConfig -> KM.KeyMap JSON.Value -> BS.ByteString -> [(ByteString, ByteString)] ->
|
setPgLocals :: AppConfig -> KM.KeyMap JSON.Value -> Text ->
|
||||||
ApiRequest -> PgVersion -> DbHandler ()
|
ApiRequest -> ByteString -> PgVersion -> DbHandler ()
|
||||||
setPgLocals AppConfig{..} claims role roleSettings req actualPgVersion = lift $
|
setPgLocals conf claims role req jsonDbS actualPgVersion = do
|
||||||
SQL.statement mempty $ SQL.dynamicallyParameterized
|
lift $ SQL.statement mempty $ SQL.dynamicallyParameterized
|
||||||
("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ roleSettingsSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql))
|
("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql ++ specSql))
|
||||||
HD.noResult configDbPreparedStatements
|
HD.noResult (configDbPreparedStatements conf)
|
||||||
|
lift $ traverse_ SQL.sql preReqSql
|
||||||
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)
|
||||||
@@ -253,13 +275,16 @@ setPgLocals AppConfig{..} claims role roleSettings req actualPgVersion = lift $
|
|||||||
claimsSql = if usesLegacyGucs
|
claimsSql = if usesLegacyGucs
|
||||||
then setConfigLocal "request.jwt.claim." <$> [(toUtf8 $ K.toText c, toUtf8 $ unquoted v) | (c,v) <- KM.toList claims]
|
then setConfigLocal "request.jwt.claim." <$> [(toUtf8 $ K.toText c, toUtf8 $ unquoted v) | (c,v) <- KM.toList claims]
|
||||||
else [setConfigLocal mempty ("request.jwt.claims", LBS.toStrict $ JSON.encode claims)]
|
else [setConfigLocal mempty ("request.jwt.claims", LBS.toStrict $ JSON.encode claims)]
|
||||||
roleSql = [setConfigLocal mempty ("role", role)]
|
roleSql = [setConfigLocal mempty ("role", toUtf8 role)]
|
||||||
roleSettingsSql = setConfigLocal mempty <$> roleSettings
|
appSettingsSql = setConfigLocal mempty <$> (join bimap toUtf8 <$> configAppSettings conf)
|
||||||
appSettingsSql = setConfigLocal mempty <$> (join bimap toUtf8 <$> configAppSettings)
|
|
||||||
searchPathSql =
|
searchPathSql =
|
||||||
let schemas = pgFmtIdentList (iSchema req : configDbExtraSearchPath) in
|
let schemas = pgFmtIdentList (iSchema req : configDbExtraSearchPath conf) in
|
||||||
setConfigLocal mempty ("search_path", schemas)
|
setConfigLocal mempty ("search_path", schemas)
|
||||||
usesLegacyGucs = configDbUseLegacyGucs && actualPgVersion < pgVersion140
|
preReqSql = (\f -> "select " <> fromQi f <> "();") <$> configDbPreRequest conf
|
||||||
|
specSql = case iTarget req of
|
||||||
|
TargetProc{tpIsRootSpec=True} -> [setConfigLocal mempty ("request.spec", jsonDbS)]
|
||||||
|
_ -> mempty
|
||||||
|
usesLegacyGucs = configDbUseLegacyGucs conf && actualPgVersion < pgVersion140
|
||||||
|
|
||||||
unquoted :: JSON.Value -> Text
|
unquoted :: JSON.Value -> Text
|
||||||
unquoted (JSON.String t) = t
|
unquoted (JSON.String t) = t
|
||||||
@@ -267,13 +292,3 @@ setPgLocals AppConfig{..} claims role roleSettings req actualPgVersion = lift $
|
|||||||
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 = T.decodeUtf8 . LBS.toStrict $ JSON.encode v
|
unquoted v = T.decodeUtf8 . LBS.toStrict $ JSON.encode v
|
||||||
|
|
||||||
-- | Runs the pre-request function.
|
|
||||||
runPreReq :: AppConfig -> DbHandler ()
|
|
||||||
runPreReq conf = lift $ traverse_ (SQL.statement mempty . stmt) (configDbPreRequest conf)
|
|
||||||
where
|
|
||||||
stmt req = SQL.Statement
|
|
||||||
("select " <> fromQi req <> "()")
|
|
||||||
HE.noParams
|
|
||||||
HD.noResult
|
|
||||||
(configDbPreparedStatements conf)
|
|
||||||
|
|||||||
@@ -17,25 +17,22 @@ module PostgREST.Query.QueryBuilder
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
import qualified Data.Set as S
|
||||||
import qualified Hasql.DynamicStatements.Snippet as SQL
|
import qualified Hasql.DynamicStatements.Snippet as SQL
|
||||||
|
|
||||||
import Data.Tree (Tree (..))
|
import Data.Tree (Tree (..))
|
||||||
|
|
||||||
import PostgREST.ApiRequest.Preferences (PreferResolution (..))
|
import PostgREST.ApiRequest.Preferences (PreferResolution (..))
|
||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion110,
|
|
||||||
pgVersion130)
|
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
||||||
|
import PostgREST.SchemaCache.Proc (ProcParam (..))
|
||||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
Relationship (..),
|
Relationship (..))
|
||||||
relIsToOne)
|
|
||||||
import PostgREST.SchemaCache.Routine (RoutineParam (..))
|
|
||||||
|
|
||||||
import PostgREST.ApiRequest.Types
|
import PostgREST.ApiRequest.Types
|
||||||
import PostgREST.Plan.CallPlan
|
import PostgREST.Plan.CallPlan
|
||||||
import PostgREST.Plan.MutatePlan
|
import PostgREST.Plan.MutatePlan
|
||||||
import PostgREST.Plan.ReadPlan
|
import PostgREST.Plan.ReadPlan
|
||||||
import PostgREST.Plan.Types
|
|
||||||
import PostgREST.Query.SqlFragment
|
import PostgREST.Query.SqlFragment
|
||||||
import PostgREST.RangeQuery (allRange)
|
import PostgREST.RangeQuery (allRange)
|
||||||
|
|
||||||
@@ -44,7 +41,7 @@ import Protolude
|
|||||||
readPlanToQuery :: ReadPlanTree -> SQL.Snippet
|
readPlanToQuery :: ReadPlanTree -> SQL.Snippet
|
||||||
readPlanToQuery (Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds} forest) =
|
readPlanToQuery (Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds} forest) =
|
||||||
"SELECT " <>
|
"SELECT " <>
|
||||||
intercalateSnippet ", " ((pgFmtSelectItem qi <$> (if null select && null forest then defSelect else select)) ++ selects) <> " " <>
|
intercalateSnippet ", " ((pgFmtSelectItem qi <$> select) ++ selects) <> " " <>
|
||||||
fromFrag <> " " <>
|
fromFrag <> " " <>
|
||||||
intercalateSnippet " " joins <> " " <>
|
intercalateSnippet " " joins <> " " <>
|
||||||
(if null logicForest && null relJoinConds
|
(if null logicForest && null relJoinConds
|
||||||
@@ -55,41 +52,45 @@ readPlanToQuery (Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,o
|
|||||||
where
|
where
|
||||||
fromFrag = fromF relToParent mainQi fromAlias
|
fromFrag = fromF relToParent mainQi fromAlias
|
||||||
qi = getQualifiedIdentifier relToParent mainQi fromAlias
|
qi = getQualifiedIdentifier relToParent mainQi fromAlias
|
||||||
defSelect = [(("*", []), Nothing, Nothing)] -- gets all the columns in case of an empty select, ignoring/obtaining these columns is done at the aggregation stage
|
|
||||||
(selects, joins) = foldr getSelectsJoins ([],[]) forest
|
(selects, joins) = foldr getSelectsJoins ([],[]) forest
|
||||||
|
|
||||||
getSelectsJoins :: ReadPlanTree -> ([SQL.Snippet], [SQL.Snippet]) -> ([SQL.Snippet], [SQL.Snippet])
|
getSelectsJoins :: ReadPlanTree -> ([SQL.Snippet], [SQL.Snippet]) -> ([SQL.Snippet], [SQL.Snippet])
|
||||||
getSelectsJoins (Node ReadPlan{relToParent=Nothing} _) _ = ([], [])
|
getSelectsJoins (Node ReadPlan{relToParent=Nothing} _) _ = ([], [])
|
||||||
getSelectsJoins rr@(Node ReadPlan{select, relName, relToParent=Just rel, relAggAlias, relAlias, relJoinType, relIsSpread} forest) (selects,joins) =
|
getSelectsJoins rr@(Node ReadPlan{relName, relToParent=Just rel, relAggAlias, relAlias, relJoinType=joinType} _) (selects,joins) =
|
||||||
let
|
let
|
||||||
subquery = readPlanToQuery rr
|
subquery = readPlanToQuery rr
|
||||||
aliasOrName = pgFmtIdent $ fromMaybe relName relAlias
|
aliasOrName = pgFmtIdent $ fromMaybe relName relAlias
|
||||||
aggAlias = pgFmtIdent relAggAlias
|
aggAlias = pgFmtIdent relAggAlias
|
||||||
correlatedSubquery sub al cond =
|
correlatedSubquery sub al cond =
|
||||||
(if relJoinType == Just JTInner then "INNER" else "LEFT") <> " JOIN LATERAL ( " <> sub <> " ) AS " <> SQL.sql al <> " ON " <> cond
|
(if joinType == Just JTInner then "INNER" else "LEFT") <> " JOIN LATERAL ( " <> sub <> " ) AS " <> SQL.sql al <> " ON " <> cond
|
||||||
(sel, joi) = if relIsToOne rel
|
isToOne = case rel of
|
||||||
|
Relationship{relCardinality=M2O _ _} -> True
|
||||||
|
Relationship{relCardinality=O2O _ _} -> True
|
||||||
|
ComputedRelationship{relToOne=True} -> True
|
||||||
|
_ -> False
|
||||||
|
(sel, joi) = if isToOne
|
||||||
then
|
then
|
||||||
( if relIsSpread
|
( SQL.sql ("row_to_json(" <> aggAlias <> ".*) AS " <> aliasOrName)
|
||||||
then SQL.sql aggAlias <> ".*"
|
|
||||||
else SQL.sql ("row_to_json(" <> aggAlias <> ".*) AS " <> aliasOrName)
|
|
||||||
, correlatedSubquery subquery aggAlias "TRUE")
|
, correlatedSubquery subquery aggAlias "TRUE")
|
||||||
else
|
else
|
||||||
( SQL.sql $ "COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> aliasOrName
|
( SQL.sql $ "COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> aliasOrName
|
||||||
, correlatedSubquery (
|
, correlatedSubquery (
|
||||||
"SELECT json_agg(" <> SQL.sql aggAlias <> ") AS " <> SQL.sql aggAlias <>
|
"SELECT json_agg(" <> SQL.sql aggAlias <> ") AS " <> SQL.sql aggAlias <>
|
||||||
"FROM (" <> subquery <> " ) AS " <> SQL.sql aggAlias
|
"FROM (" <> subquery <> " ) AS " <> SQL.sql aggAlias
|
||||||
) aggAlias $ if relJoinType == Just JTInner then SQL.sql aggAlias <> " IS NOT NULL" else "TRUE")
|
) aggAlias $ if joinType == Just JTInner then SQL.sql aggAlias <> " IS NOT NULL" else "TRUE")
|
||||||
in
|
in
|
||||||
(if null select && null forest then selects else sel:selects, joi:joins)
|
(sel:selects, joi:joins)
|
||||||
|
|
||||||
mutatePlanToQuery :: MutatePlan -> SQL.Snippet
|
mutatePlanToQuery :: MutatePlan -> SQL.Snippet
|
||||||
mutatePlanToQuery (Insert mainQi iCols body onConflct putConditions returnings _ applyDefaults) =
|
mutatePlanToQuery (Insert mainQi iCols body onConflct putConditions returnings _) =
|
||||||
"INSERT INTO " <> SQL.sql (fromQi mainQi) <> SQL.sql (if null iCols then " " else "(" <> cols <> ") ") <>
|
"WITH " <> normalizedBody body <> " " <>
|
||||||
fromJsonBodyF body iCols True False applyDefaults <>
|
"INSERT INTO " <> SQL.sql (fromQi mainQi) <> SQL.sql (if S.null iCols then " " else "(" <> cols <> ") ") <>
|
||||||
|
"SELECT " <> SQL.sql cols <> " " <>
|
||||||
|
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 "pgrst_body") <$> putConditions)) <>
|
(if null putConditions then mempty else "WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree (QualifiedIdentifier mempty "_") <$> putConditions)) <>
|
||||||
SQL.sql (BS.unwords [
|
SQL.sql (BS.unwords [
|
||||||
maybe mempty (\(oncDo, oncCols) ->
|
maybe "" (\(oncDo, oncCols) ->
|
||||||
if null oncCols then
|
if null oncCols then
|
||||||
mempty
|
mempty
|
||||||
else
|
else
|
||||||
@@ -97,32 +98,33 @@ mutatePlanToQuery (Insert mainQi iCols body onConflct putConditions returnings _
|
|||||||
IgnoreDuplicates ->
|
IgnoreDuplicates ->
|
||||||
"DO NOTHING"
|
"DO NOTHING"
|
||||||
MergeDuplicates ->
|
MergeDuplicates ->
|
||||||
if null iCols
|
if S.null iCols
|
||||||
then "DO NOTHING"
|
then "DO NOTHING"
|
||||||
else "DO UPDATE SET " <> BS.intercalate ", " ((pgFmtIdent . tfName) <> const " = EXCLUDED." <> (pgFmtIdent . tfName) <$> iCols)
|
else "DO UPDATE SET " <> BS.intercalate ", " (pgFmtIdent <> const " = EXCLUDED." <> pgFmtIdent <$> S.toList iCols)
|
||||||
) onConflct,
|
) onConflct,
|
||||||
returningF mainQi returnings
|
returningF mainQi returnings
|
||||||
])
|
])
|
||||||
where
|
where
|
||||||
cols = BS.intercalate ", " $ pgFmtIdent . tfName <$> iCols
|
cols = BS.intercalate ", " $ pgFmtIdent <$> S.toList iCols
|
||||||
|
|
||||||
-- An update without a limit is always filtered with a WHERE
|
-- An update without a limit is always filtered with a WHERE
|
||||||
mutatePlanToQuery (Update mainQi uCols body logicForest range ordts returnings applyDefaults)
|
mutatePlanToQuery (Update mainQi uCols body logicForest range ordts returnings)
|
||||||
| null uCols =
|
| S.null uCols =
|
||||||
-- 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
|
||||||
SQL.sql $ "SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false"
|
SQL.sql $ "SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false"
|
||||||
|
|
||||||
| range == allRange =
|
| range == allRange =
|
||||||
|
"WITH " <> normalizedBody body <> " " <>
|
||||||
"UPDATE " <> mainTbl <> " SET " <> SQL.sql nonRangeCols <> " " <>
|
"UPDATE " <> mainTbl <> " SET " <> SQL.sql nonRangeCols <> " " <>
|
||||||
fromJsonBodyF body uCols False False applyDefaults <>
|
"FROM (SELECT * FROM json_populate_recordset (null::" <> mainTbl <> " , " <> SQL.sql selectBody <> " )) _ " <>
|
||||||
whereLogic <> " " <>
|
whereLogic <> " " <>
|
||||||
SQL.sql (returningF mainQi returnings)
|
SQL.sql (returningF mainQi returnings)
|
||||||
|
|
||||||
| otherwise =
|
| otherwise =
|
||||||
"WITH " <>
|
"WITH " <> normalizedBody body <> ", " <>
|
||||||
"pgrst_update_body AS (" <> fromJsonBodyF body uCols True True applyDefaults <> "), " <>
|
"pgrst_update_body AS (SELECT * FROM json_populate_recordset (null::" <> mainTbl <> " , " <> SQL.sql selectBody <> " ) LIMIT 1), " <>
|
||||||
"pgrst_affected_rows AS (" <>
|
"pgrst_affected_rows AS (" <>
|
||||||
"SELECT " <> SQL.sql rangeIdF <> " FROM " <> mainTbl <>
|
"SELECT " <> SQL.sql rangeIdF <> " FROM " <> mainTbl <>
|
||||||
whereLogic <> " " <>
|
whereLogic <> " " <>
|
||||||
@@ -138,8 +140,8 @@ mutatePlanToQuery (Update mainQi uCols body logicForest range ordts returnings a
|
|||||||
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
|
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
|
||||||
mainTbl = SQL.sql (fromQi mainQi)
|
mainTbl = SQL.sql (fromQi mainQi)
|
||||||
emptyBodyReturnedColumns = if null returnings then "NULL" else BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)
|
emptyBodyReturnedColumns = if null returnings then "NULL" else BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)
|
||||||
nonRangeCols = BS.intercalate ", " (pgFmtIdent . tfName <> const " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_body") . tfName <$> uCols)
|
nonRangeCols = BS.intercalate ", " (pgFmtIdent <> const " = _." <> pgFmtIdent <$> S.toList uCols)
|
||||||
rangeCols = BS.intercalate ", " ((\col -> pgFmtIdent (tfName col) <> " = (SELECT " <> pgFmtIdent (tfName col) <> " FROM pgrst_update_body) ") <$> uCols)
|
rangeCols = BS.intercalate ", " ((\col -> pgFmtIdent col <> " = (SELECT " <> pgFmtIdent col <> " FROM pgrst_update_body) ") <$> S.toList uCols)
|
||||||
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (fst . otTerm <$> ordts)
|
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (fst . otTerm <$> ordts)
|
||||||
|
|
||||||
mutatePlanToQuery (Delete mainQi logicForest range ordts returnings)
|
mutatePlanToQuery (Delete mainQi logicForest range ordts returnings)
|
||||||
@@ -165,29 +167,52 @@ mutatePlanToQuery (Delete mainQi logicForest range ordts returnings)
|
|||||||
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
|
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
|
||||||
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (fst . otTerm <$> ordts)
|
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (fst . otTerm <$> ordts)
|
||||||
|
|
||||||
callPlanToQuery :: CallPlan -> PgVersion -> SQL.Snippet
|
callPlanToQuery :: CallPlan -> SQL.Snippet
|
||||||
callPlanToQuery (FunctionCall qi params args returnsScalar returnsSetOfScalar returnsCompositeAlias returnings) pgVer =
|
callPlanToQuery (FunctionCall qi params args returnsScalar multipleCall returnings) =
|
||||||
"SELECT " <> (if returnsScalar || returnsSetOfScalar then "pgrst_call AS pgrst_scalar " else returnedColumns) <> " " <>
|
prmsCTE <> argsBody
|
||||||
fromCall
|
|
||||||
where
|
where
|
||||||
fromCall = case params of
|
(prmsCTE, argFrag) = case params of
|
||||||
OnePosParam prm -> "FROM " <> callIt (singleParameter args $ encodeUtf8 $ ppType prm)
|
OnePosParam prm -> ("WITH pgrst_args AS (SELECT NULL)", singleParameter args (encodeUtf8 $ ppType prm))
|
||||||
KeyParams [] -> "FROM " <> callIt mempty
|
KeyParams [] -> (mempty, mempty)
|
||||||
KeyParams prms -> fromJsonBodyF args ((\p -> TypedField (ppName p) (ppType p) Nothing) <$> prms) False True False <> ", " <>
|
KeyParams prms -> (
|
||||||
"LATERAL " <> callIt (fmtParams prms)
|
"WITH " <> normalizedBody args <> ", " <>
|
||||||
|
SQL.sql (
|
||||||
|
BS.unwords [
|
||||||
|
"pgrst_args AS (",
|
||||||
|
"SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <> fmtParams prms (const mempty) (\a -> " " <> encodeUtf8 (ppType a)) <> ")",
|
||||||
|
")"])
|
||||||
|
, SQL.sql $ if multipleCall
|
||||||
|
then fmtParams prms varadicPrefix (\a -> " := pgrst_args." <> pgFmtIdent (ppName a))
|
||||||
|
else fmtParams prms varadicPrefix (\a -> " := (SELECT " <> pgFmtIdent (ppName a) <> " FROM pgrst_args LIMIT 1)")
|
||||||
|
)
|
||||||
|
|
||||||
callIt :: SQL.Snippet -> SQL.Snippet
|
fmtParams :: [ProcParam] -> (ProcParam -> SqlFragment) -> (ProcParam -> SqlFragment) -> SqlFragment
|
||||||
callIt argument | pgVer < pgVersion130 && pgVer >= pgVersion110 && returnsCompositeAlias = "(SELECT (" <> SQL.sql (fromQi qi) <> "(" <> argument <> ")).*) pgrst_call"
|
fmtParams prms prmFragPre prmFragSuf = BS.intercalate ", "
|
||||||
| otherwise = SQL.sql (fromQi qi) <> "(" <> argument <> ") pgrst_call"
|
((\a -> prmFragPre a <> pgFmtIdent (ppName a) <> prmFragSuf a) <$> prms)
|
||||||
|
|
||||||
fmtParams :: [RoutineParam] -> SQL.Snippet
|
varadicPrefix :: ProcParam -> SqlFragment
|
||||||
fmtParams prms = SQL.sql $ BS.intercalate ", "
|
varadicPrefix a = if ppVar a then "VARIADIC " else mempty
|
||||||
((\a -> (if ppVar a then "VARIADIC " else mempty) <> pgFmtIdent (ppName a) <> " := pgrst_body." <> pgFmtIdent (ppName a)) <$> prms)
|
|
||||||
|
argsBody :: SQL.Snippet
|
||||||
|
argsBody
|
||||||
|
| multipleCall =
|
||||||
|
if returnsScalar
|
||||||
|
then "SELECT " <> callIt <> " AS pgrst_scalar FROM pgrst_args"
|
||||||
|
else "SELECT pgrst_lat_args.* FROM pgrst_args, " <>
|
||||||
|
"LATERAL ( SELECT " <> returnedColumns <> " FROM " <> callIt <> " ) pgrst_lat_args"
|
||||||
|
| otherwise =
|
||||||
|
if returnsScalar
|
||||||
|
then "SELECT " <> callIt <> " AS pgrst_scalar"
|
||||||
|
else "SELECT " <> returnedColumns <> " FROM " <> callIt
|
||||||
|
|
||||||
|
callIt :: SQL.Snippet
|
||||||
|
callIt = SQL.sql (fromQi qi) <> "(" <> argFrag <> ")"
|
||||||
|
|
||||||
returnedColumns :: SQL.Snippet
|
returnedColumns :: SQL.Snippet
|
||||||
returnedColumns
|
returnedColumns
|
||||||
| null returnings = "*"
|
| null returnings = "*"
|
||||||
| otherwise = SQL.sql $ BS.intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty "pgrst_call") <$> 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.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ module PostgREST.Query.SqlFragment
|
|||||||
, limitOffsetF
|
, limitOffsetF
|
||||||
, locationF
|
, locationF
|
||||||
, mutRangeF
|
, mutRangeF
|
||||||
|
, normalizedBody
|
||||||
, orderF
|
, orderF
|
||||||
, pgFmtColumn
|
, pgFmtColumn
|
||||||
, pgFmtIdent
|
, pgFmtIdent
|
||||||
@@ -29,10 +30,10 @@ module PostgREST.Query.SqlFragment
|
|||||||
, pgFmtLogicTree
|
, pgFmtLogicTree
|
||||||
, pgFmtOrderTerm
|
, pgFmtOrderTerm
|
||||||
, pgFmtSelectItem
|
, pgFmtSelectItem
|
||||||
, fromJsonBodyF
|
|
||||||
, responseHeadersF
|
, responseHeadersF
|
||||||
, responseStatusF
|
, responseStatusF
|
||||||
, returningF
|
, returningF
|
||||||
|
, selectBody
|
||||||
, singleParameter
|
, singleParameter
|
||||||
, sourceCTEName
|
, sourceCTEName
|
||||||
, unknownEncoder
|
, unknownEncoder
|
||||||
@@ -64,18 +65,15 @@ import PostgREST.ApiRequest.Types (Alias, Cast, Field,
|
|||||||
JsonPath,
|
JsonPath,
|
||||||
LogicOperator (..),
|
LogicOperator (..),
|
||||||
LogicTree (..), OpExpr (..),
|
LogicTree (..), OpExpr (..),
|
||||||
OpQuantifier (..),
|
|
||||||
Operation (..),
|
Operation (..),
|
||||||
OrderDirection (..),
|
OrderDirection (..),
|
||||||
OrderNulls (..),
|
OrderNulls (..),
|
||||||
OrderTerm (..),
|
OrderTerm (..),
|
||||||
QuantOperator (..),
|
|
||||||
SimpleOperator (..),
|
SimpleOperator (..),
|
||||||
TrileanVal (..))
|
TrileanVal (..))
|
||||||
import PostgREST.MediaType (MTPlanFormat (..),
|
import PostgREST.MediaType (MTPlanFormat (..),
|
||||||
MTPlanOption (..))
|
MTPlanOption (..))
|
||||||
import PostgREST.Plan.ReadPlan (JoinCondition (..))
|
import PostgREST.Plan.ReadPlan (JoinCondition (..))
|
||||||
import PostgREST.Plan.Types (TypedField (..))
|
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
rangeLimit, rangeOffset)
|
rangeLimit, rangeOffset)
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
@@ -93,27 +91,24 @@ noLocationF = "array[]::text[]"
|
|||||||
sourceCTEName :: SqlFragment
|
sourceCTEName :: SqlFragment
|
||||||
sourceCTEName = "pgrst_source"
|
sourceCTEName = "pgrst_source"
|
||||||
|
|
||||||
simpleOperator :: SimpleOperator -> SqlFragment
|
singleValOperator :: SimpleOperator -> SqlFragment
|
||||||
simpleOperator = \case
|
singleValOperator = \case
|
||||||
OpNotEqual -> "<>"
|
|
||||||
OpContains -> "@>"
|
|
||||||
OpContained -> "<@"
|
|
||||||
OpOverlap -> "&&"
|
|
||||||
OpStrictlyLeft -> "<<"
|
|
||||||
OpStrictlyRight -> ">>"
|
|
||||||
OpNotExtendsRight -> "&<"
|
|
||||||
OpNotExtendsLeft -> "&>"
|
|
||||||
OpAdjacent -> "-|-"
|
|
||||||
|
|
||||||
quantOperator :: QuantOperator -> SqlFragment
|
|
||||||
quantOperator = \case
|
|
||||||
OpEqual -> "="
|
OpEqual -> "="
|
||||||
OpGreaterThanEqual -> ">="
|
OpGreaterThanEqual -> ">="
|
||||||
OpGreaterThan -> ">"
|
OpGreaterThan -> ">"
|
||||||
OpLessThanEqual -> "<="
|
OpLessThanEqual -> "<="
|
||||||
OpLessThan -> "<"
|
OpLessThan -> "<"
|
||||||
|
OpNotEqual -> "<>"
|
||||||
OpLike -> "like"
|
OpLike -> "like"
|
||||||
OpILike -> "ilike"
|
OpILike -> "ilike"
|
||||||
|
OpContains -> "@>"
|
||||||
|
OpContained -> "<@"
|
||||||
|
OpOverlap -> "&&"
|
||||||
|
OpStrictlyLeft -> "<<"
|
||||||
|
OpStrictlyRight -> ">>"
|
||||||
|
OpNotExtendsRight -> "&<"
|
||||||
|
OpNotExtendsLeft -> "&>"
|
||||||
|
OpAdjacent -> "-|-"
|
||||||
OpMatch -> "~"
|
OpMatch -> "~"
|
||||||
OpIMatch -> "~*"
|
OpIMatch -> "~*"
|
||||||
|
|
||||||
@@ -124,6 +119,25 @@ ftsOperator = \case
|
|||||||
FilterFtsPhrase -> "@@ phraseto_tsquery"
|
FilterFtsPhrase -> "@@ phraseto_tsquery"
|
||||||
FilterFtsWebsearch -> "@@ websearch_to_tsquery"
|
FilterFtsWebsearch -> "@@ websearch_to_tsquery"
|
||||||
|
|
||||||
|
-- |
|
||||||
|
-- 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
|
||||||
|
-- We do this in SQL to avoid processing the JSON in application code
|
||||||
|
-- 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 =
|
||||||
|
"pgrst_payload AS (SELECT " <> jsonPlaceHolder <> " AS json_data), " <>
|
||||||
|
SQL.sql (BS.unwords [
|
||||||
|
"pgrst_body AS (",
|
||||||
|
"SELECT",
|
||||||
|
"CASE WHEN json_typeof(json_data) = 'array'",
|
||||||
|
"THEN json_data",
|
||||||
|
"ELSE json_build_array(json_data)",
|
||||||
|
"END AS val",
|
||||||
|
"FROM pgrst_payload)"])
|
||||||
|
where
|
||||||
|
jsonPlaceHolder = SQL.encoderAndParam (HE.nullable HE.jsonLazyBytes) body
|
||||||
|
|
||||||
singleParameter :: Maybe LBS.ByteString -> ByteString -> SQL.Snippet
|
singleParameter :: Maybe LBS.ByteString -> ByteString -> SQL.Snippet
|
||||||
singleParameter body typ =
|
singleParameter body typ =
|
||||||
if typ == "bytea"
|
if typ == "bytea"
|
||||||
@@ -131,6 +145,9 @@ singleParameter body typ =
|
|||||||
then SQL.encoderAndParam (HE.nullable HE.bytea) (LBS.toStrict <$> body)
|
then SQL.encoderAndParam (HE.nullable HE.bytea) (LBS.toStrict <$> body)
|
||||||
else SQL.encoderAndParam (HE.nullable HE.unknown) (LBS.toStrict <$> body) <> "::" <> SQL.sql typ
|
else SQL.encoderAndParam (HE.nullable HE.unknown) (LBS.toStrict <$> body) <> "::" <> SQL.sql typ
|
||||||
|
|
||||||
|
selectBody :: SqlFragment
|
||||||
|
selectBody = "(SELECT val FROM pgrst_body)"
|
||||||
|
|
||||||
-- Here we build the pg array literal, e.g '{"Hebdon, John","Other","Another"}', manually.
|
-- Here we build the pg array literal, e.g '{"Hebdon, John","Other","Another"}', manually.
|
||||||
-- This is necessary to pass an "unknown" array and let pg infer the type.
|
-- This is necessary to pass an "unknown" array and let pg infer the type.
|
||||||
-- There are backslashes here, but since this value is parametrized and is not a string constant
|
-- There are backslashes here, but since this value is parametrized and is not a string constant
|
||||||
@@ -148,16 +165,6 @@ pgBuildArrayLiteral vals =
|
|||||||
pgFmtIdent :: Text -> SqlFragment
|
pgFmtIdent :: Text -> SqlFragment
|
||||||
pgFmtIdent x = encodeUtf8 $ "\"" <> T.replace "\"" "\"\"" (trimNullChars x) <> "\""
|
pgFmtIdent x = encodeUtf8 $ "\"" <> T.replace "\"" "\"\"" (trimNullChars x) <> "\""
|
||||||
|
|
||||||
-- Only use it if the input comes from the database itself, like on `jsonb_build_object('column_from_a_table', val)..`
|
|
||||||
pgFmtLit :: Text -> Text
|
|
||||||
pgFmtLit x =
|
|
||||||
let trimmed = trimNullChars x
|
|
||||||
escaped = "'" <> T.replace "'" "''" trimmed <> "'"
|
|
||||||
slashed = T.replace "\\" "\\\\" escaped in
|
|
||||||
if "\\" `T.isInfixOf` escaped
|
|
||||||
then "E" <> slashed
|
|
||||||
else slashed
|
|
||||||
|
|
||||||
trimNullChars :: Text -> Text
|
trimNullChars :: Text -> Text
|
||||||
trimNullChars = T.takeWhile (/= '\x0')
|
trimNullChars = T.takeWhile (/= '\x0')
|
||||||
|
|
||||||
@@ -183,17 +190,15 @@ asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
|
|||||||
")"
|
")"
|
||||||
asCsvBodyF = "coalesce(string_agg(substring(_postgrest_t::text, 2, length(_postgrest_t::text) - 2), '\n'), '')"
|
asCsvBodyF = "coalesce(string_agg(substring(_postgrest_t::text, 2, length(_postgrest_t::text) - 2), '\n'), '')"
|
||||||
|
|
||||||
|
asJsonF :: Bool -> SqlFragment
|
||||||
|
asJsonF returnsScalar
|
||||||
|
| returnsScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar), '[]')::character varying"
|
||||||
|
| otherwise = "coalesce(json_agg(_postgrest_t), '[]')::character varying"
|
||||||
|
|
||||||
asJsonSingleF :: Bool -> SqlFragment
|
asJsonSingleF :: Bool -> SqlFragment
|
||||||
asJsonSingleF returnsScalar
|
asJsonSingleF returnsScalar
|
||||||
| returnsScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar)->0, 'null')"
|
| returnsScalar = "coalesce((json_agg(_postgrest_t.pgrst_scalar)->0)::text, 'null')"
|
||||||
| otherwise = "coalesce(json_agg(_postgrest_t)->0, 'null')"
|
| otherwise = "coalesce((json_agg(_postgrest_t)->0)::text, 'null')"
|
||||||
|
|
||||||
asJsonF :: Bool -> Bool -> Bool -> SqlFragment
|
|
||||||
asJsonF returnsScalar returnsSetOfScalar returnsSingleComposite
|
|
||||||
| returnsSingleComposite = "coalesce(json_agg(_postgrest_t)->0, 'null')"
|
|
||||||
| returnsScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar)->0, 'null')"
|
|
||||||
| returnsSetOfScalar = "coalesce(json_agg(_postgrest_t.pgrst_scalar), '[]')"
|
|
||||||
| otherwise = "coalesce(json_agg(_postgrest_t), '[]')"
|
|
||||||
|
|
||||||
asXmlF :: FieldName -> SqlFragment
|
asXmlF :: FieldName -> SqlFragment
|
||||||
asXmlF fieldName = "coalesce(xmlagg(_postgrest_t." <> pgFmtIdent fieldName <> "), '')"
|
asXmlF fieldName = "coalesce(xmlagg(_postgrest_t." <> pgFmtIdent fieldName <> "), '')"
|
||||||
@@ -237,53 +242,13 @@ pgFmtSelectItem table (f@(fName, jp), Nothing, alias) = pgFmtField table f <> SQ
|
|||||||
-- 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 " <> SQL.sql (encodeUtf8 cast) <> " )" <> SQL.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)
|
||||||
|
|
||||||
-- TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body
|
|
||||||
fromJsonBodyF :: Maybe LBS.ByteString -> [TypedField] -> Bool -> Bool -> Bool -> SQL.Snippet
|
|
||||||
fromJsonBodyF body fields includeSelect includeLimitOne includeDefaults =
|
|
||||||
SQL.sql
|
|
||||||
(if includeSelect then "SELECT " <> parsedCols <> " " else mempty) <>
|
|
||||||
"FROM (SELECT " <> jsonPlaceHolder <> " AS json_data) pgrst_payload, " <>
|
|
||||||
-- convert a json object into a json array, this way we can use json_to_recordset for all json payloads
|
|
||||||
-- Otherwise we'd have to use json_to_record for json objects and json_to_recordset for json arrays
|
|
||||||
-- We do this in SQL to avoid processing the JSON in application code
|
|
||||||
"LATERAL (SELECT CASE WHEN " <> jsonTypeofF <> "(pgrst_payload.json_data) = 'array' THEN pgrst_payload.json_data ELSE " <> jsonBuildArrayF <> "(pgrst_payload.json_data) END AS val) pgrst_uniform_json, " <>
|
|
||||||
(if includeDefaults
|
|
||||||
then "LATERAL (SELECT jsonb_agg(jsonb_build_object(" <> defsJsonb <> ") || elem) AS val from jsonb_array_elements(pgrst_uniform_json.val) elem) pgrst_json_defs, "
|
|
||||||
else mempty) <>
|
|
||||||
"LATERAL (SELECT * FROM " <>
|
|
||||||
(if null fields
|
|
||||||
-- When we are inserting no columns (e.g. using default values), we can't use our ordinary `json_to_recordset`
|
|
||||||
-- because it can't extract records with no columns (there's no valid syntax for the `AS (colName colType,...)`
|
|
||||||
-- part). But we still need to ensure as many rows are created as there are array elements.
|
|
||||||
then SQL.sql $ jsonArrayElementsF <> "(" <> finalBodyF <> ") _ "
|
|
||||||
else SQL.sql $ jsonToRecordsetF <> "(" <> finalBodyF <> ") AS _(" <> typedCols <> ") " <> if includeLimitOne then "LIMIT 1" else mempty
|
|
||||||
) <>
|
|
||||||
") pgrst_body "
|
|
||||||
where
|
|
||||||
parsedCols = BS.intercalate ", " $ fromQi . QualifiedIdentifier "pgrst_body" . tfName <$> fields
|
|
||||||
typedCols = BS.intercalate ", " $ pgFmtIdent . tfName <> const " " <> encodeUtf8 . tfIRType <$> fields
|
|
||||||
defsJsonb = SQL.sql $ BS.intercalate "," fieldsWDefaults
|
|
||||||
fieldsWDefaults = mapMaybe (\case
|
|
||||||
TypedField{tfName=nam, tfDefault=Just def} -> Just $ encodeUtf8 (pgFmtLit nam <> ", " <> def)
|
|
||||||
TypedField{tfDefault=Nothing} -> Nothing
|
|
||||||
) fields
|
|
||||||
(finalBodyF, jsonTypeofF, jsonBuildArrayF, jsonArrayElementsF, jsonToRecordsetF) =
|
|
||||||
if includeDefaults
|
|
||||||
then ("pgrst_json_defs.val", "jsonb_typeof", "jsonb_build_array", "jsonb_array_elements", "jsonb_to_recordset")
|
|
||||||
else ("pgrst_uniform_json.val", "json_typeof", "json_build_array", "json_array_elements", "json_to_recordset")
|
|
||||||
jsonPlaceHolder = SQL.encoderAndParam (HE.nullable $ if includeDefaults then HE.jsonbLazyBytes else HE.jsonLazyBytes) body
|
|
||||||
|
|
||||||
pgFmtOrderTerm :: QualifiedIdentifier -> OrderTerm -> SQL.Snippet
|
pgFmtOrderTerm :: QualifiedIdentifier -> OrderTerm -> SQL.Snippet
|
||||||
pgFmtOrderTerm qi ot =
|
pgFmtOrderTerm qi ot =
|
||||||
fmtOTerm ot <> " " <>
|
pgFmtField qi (otTerm ot) <> " " <>
|
||||||
SQL.sql (BS.unwords [
|
SQL.sql (BS.unwords [
|
||||||
maybe mempty direction $ otDirection ot,
|
maybe mempty direction $ otDirection ot,
|
||||||
maybe mempty nullOrder $ otNullOrder ot])
|
maybe mempty nullOrder $ otNullOrder ot])
|
||||||
where
|
where
|
||||||
fmtOTerm = \case
|
|
||||||
OrderTerm{otTerm} -> pgFmtField qi otTerm
|
|
||||||
OrderRelationTerm{otRelation, otRelTerm} -> pgFmtField (QualifiedIdentifier mempty otRelation) otRelTerm
|
|
||||||
|
|
||||||
direction OrderAsc = "ASC"
|
direction OrderAsc = "ASC"
|
||||||
direction OrderDesc = "DESC"
|
direction OrderDesc = "DESC"
|
||||||
|
|
||||||
@@ -292,44 +257,37 @@ pgFmtOrderTerm qi ot =
|
|||||||
|
|
||||||
|
|
||||||
pgFmtFilter :: QualifiedIdentifier -> Filter -> SQL.Snippet
|
pgFmtFilter :: QualifiedIdentifier -> Filter -> SQL.Snippet
|
||||||
pgFmtFilter _ (FilterNullEmbed hasNot fld) = SQL.sql (pgFmtIdent fld) <> " IS " <> (if hasNot then "NOT" else mempty) <> " NULL"
|
pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper of
|
||||||
pgFmtFilter _ (Filter _ (NoOpExpr _)) = mempty -- TODO unreachable because NoOpExpr is filtered on QueryParams
|
Op op val -> pgFmtFieldOp op <> " " <> case op of
|
||||||
pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> pgFmtField table fld <> case oper of
|
OpLike -> unknownLiteral (T.map star val)
|
||||||
Op op val -> " " <> SQL.sql (simpleOperator op) <> " " <> unknownLiteral val
|
OpILike -> unknownLiteral (T.map star val)
|
||||||
|
_ -> unknownLiteral val
|
||||||
OpQuant op quant val -> " " <> SQL.sql (quantOperator op) <> " " <> case op of
|
|
||||||
OpLike -> fmtQuant quant $ unknownLiteral (T.map star val)
|
|
||||||
OpILike -> fmtQuant quant $ unknownLiteral (T.map star val)
|
|
||||||
_ -> fmtQuant quant $ unknownLiteral val
|
|
||||||
|
|
||||||
-- IS cannot be prepared. `PREPARE boolplan AS SELECT * FROM projects where id IS $1` will give a syntax error.
|
-- 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;`
|
-- 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.
|
-- 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
|
-- This is why `IS` operands are whitelisted at the Parsers.hs level
|
||||||
Is triVal -> " IS " <> case triVal of
|
Is triVal -> pgFmtField table fld <> " IS " <> case triVal of
|
||||||
TriTrue -> "TRUE"
|
TriTrue -> "TRUE"
|
||||||
TriFalse -> "FALSE"
|
TriFalse -> "FALSE"
|
||||||
TriNull -> "NULL"
|
TriNull -> "NULL"
|
||||||
TriUnknown -> "UNKNOWN"
|
TriUnknown -> "UNKNOWN"
|
||||||
|
|
||||||
IsDistinctFrom val -> " IS DISTINCT FROM " <> unknownLiteral val
|
|
||||||
|
|
||||||
-- 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 -> " " <> case vals of
|
In vals -> pgFmtField table fld <> " " <> case vals of
|
||||||
[""] -> "= ANY('{}') "
|
[""] -> "= ANY('{}') "
|
||||||
_ -> "= ANY (" <> unknownLiteral (pgBuildArrayLiteral vals) <> ") "
|
_ -> "= ANY (" <> unknownLiteral (pgBuildArrayLiteral vals) <> ") "
|
||||||
|
|
||||||
Fts op lang val -> " " <> SQL.sql (ftsOperator op) <> "(" <> ftsLang lang <> unknownLiteral val <> ") "
|
Fts op lang val ->
|
||||||
|
pgFmtFieldFts op <> "(" <> ftsLang lang <> unknownLiteral val <> ") "
|
||||||
where
|
where
|
||||||
ftsLang = maybe mempty (\l -> unknownLiteral l <> ", ")
|
ftsLang = maybe mempty (\l -> unknownLiteral l <> ", ")
|
||||||
|
pgFmtFieldOp op = pgFmtField table fld <> " " <> SQL.sql (singleValOperator op)
|
||||||
|
pgFmtFieldFts op = pgFmtField table fld <> " " <> SQL.sql (ftsOperator op)
|
||||||
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
|
||||||
fmtQuant q val = case q of
|
|
||||||
Just QuantAny -> "ANY(" <> val <> ")"
|
|
||||||
Just QuantAll -> "ALL(" <> val <> ")"
|
|
||||||
Nothing -> val
|
|
||||||
|
|
||||||
pgFmtJoinCondition :: JoinCondition -> SQL.Snippet
|
pgFmtJoinCondition :: JoinCondition -> SQL.Snippet
|
||||||
pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
|
pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ prepareWrite selectQuery mutateQuery isInsert mt rep pKeys =
|
|||||||
| getMediaType mt == MTTextCSV = asCsvF
|
| getMediaType mt == MTTextCSV = asCsvF
|
||||||
| getMediaType mt == MTGeoJSON = asGeoJsonF
|
| getMediaType mt == MTGeoJSON = asGeoJsonF
|
||||||
| getMediaType mt == MTSingularJSON = asJsonSingleF False
|
| getMediaType mt == MTSingularJSON = asJsonSingleF False
|
||||||
| otherwise = asJsonF False False False
|
| otherwise = asJsonF False
|
||||||
|
|
||||||
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
|
||||||
@@ -123,17 +123,17 @@ prepareRead selectQuery countQuery countTotal mt binaryField =
|
|||||||
| getMediaType mt == MTGeoJSON = asGeoJsonF
|
| getMediaType mt == MTGeoJSON = asGeoJsonF
|
||||||
| isJust binaryField && getMediaType mt == MTTextXML = asXmlF $ fromJust binaryField
|
| isJust binaryField && getMediaType mt == MTTextXML = asXmlF $ fromJust binaryField
|
||||||
| isJust binaryField = asBinaryF $ fromJust binaryField
|
| isJust binaryField = asBinaryF $ fromJust binaryField
|
||||||
| otherwise = asJsonF False False False
|
| otherwise = asJsonF False
|
||||||
|
|
||||||
decodeIt :: HD.Result ResultSet
|
decodeIt :: HD.Result ResultSet
|
||||||
decodeIt = case mt of
|
decodeIt = case mt of
|
||||||
MTPlan{} -> planRow
|
MTPlan{} -> planRow
|
||||||
_ -> HD.singleRow $ standardRow True
|
_ -> HD.singleRow $ standardRow True
|
||||||
|
|
||||||
prepareCall :: Bool -> Bool -> Bool -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
|
prepareCall :: Bool -> Bool -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
|
||||||
MediaType -> Maybe FieldName -> Bool ->
|
MediaType -> Bool -> Maybe FieldName -> Bool ->
|
||||||
SQL.Statement () ResultSet
|
SQL.Statement () ResultSet
|
||||||
prepareCall returnsScalar returnsSingleComposite returnsSetOfScalar callProcQuery selectQuery countQuery countTotal mt binaryField =
|
prepareCall returnsScalar returnsSingle callProcQuery selectQuery countQuery countTotal mt multObjects binaryField =
|
||||||
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
|
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
|
||||||
where
|
where
|
||||||
snippet =
|
snippet =
|
||||||
@@ -156,7 +156,8 @@ prepareCall returnsScalar returnsSingleComposite returnsSetOfScalar callProcQuer
|
|||||||
| getMediaType mt == MTGeoJSON = asGeoJsonF
|
| getMediaType mt == MTGeoJSON = asGeoJsonF
|
||||||
| isJust binaryField && getMediaType mt == MTTextXML = asXmlF $ fromJust binaryField
|
| isJust binaryField && getMediaType mt == MTTextXML = asXmlF $ fromJust binaryField
|
||||||
| isJust binaryField = asBinaryF $ fromJust binaryField
|
| isJust binaryField = asBinaryF $ fromJust binaryField
|
||||||
| otherwise = asJsonF returnsScalar returnsSetOfScalar returnsSingleComposite
|
| returnsSingle && not multObjects = asJsonSingleF returnsScalar
|
||||||
|
| otherwise = asJsonF returnsScalar
|
||||||
|
|
||||||
decodeIt :: HD.Result ResultSet
|
decodeIt :: HD.Result ResultSet
|
||||||
decodeIt = case mt of
|
decodeIt = case mt of
|
||||||
|
|||||||
+41
-57
@@ -3,9 +3,7 @@
|
|||||||
module PostgREST.Response
|
module PostgREST.Response
|
||||||
( createResponse
|
( createResponse
|
||||||
, deleteResponse
|
, deleteResponse
|
||||||
, infoIdentResponse
|
, infoResponse
|
||||||
, infoProcResponse
|
|
||||||
, infoRootResponse
|
|
||||||
, invokeResponse
|
, invokeResponse
|
||||||
, openApiResponse
|
, openApiResponse
|
||||||
, readResponse
|
, readResponse
|
||||||
@@ -14,14 +12,12 @@ module PostgREST.Response
|
|||||||
, addRetryHint
|
, addRetryHint
|
||||||
, isServiceUnavailable
|
, isServiceUnavailable
|
||||||
, optionalRollback
|
, optionalRollback
|
||||||
, traceHeaderMiddleware
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
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 as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Data.List as L
|
|
||||||
import Data.Text.Read (decimal)
|
import Data.Text.Read (decimal)
|
||||||
import qualified Network.HTTP.Types.Header as HTTP
|
import qualified Network.HTTP.Types.Header as HTTP
|
||||||
import qualified Network.HTTP.Types.Status as HTTP
|
import qualified Network.HTTP.Types.Status as HTTP
|
||||||
@@ -34,10 +30,10 @@ import qualified PostgREST.RangeQuery as RangeQuery
|
|||||||
import qualified PostgREST.Response.OpenAPI as OpenAPI
|
import qualified PostgREST.Response.OpenAPI as OpenAPI
|
||||||
|
|
||||||
import PostgREST.ApiRequest (ApiRequest (..),
|
import PostgREST.ApiRequest (ApiRequest (..),
|
||||||
InvokeMethod (..))
|
InvokeMethod (..),
|
||||||
|
Target (..))
|
||||||
import PostgREST.ApiRequest.Preferences (PreferRepresentation (..),
|
import PostgREST.ApiRequest.Preferences (PreferRepresentation (..),
|
||||||
PreferTransaction (..),
|
PreferTransaction (..),
|
||||||
Preferences (..),
|
|
||||||
shouldCount,
|
shouldCount,
|
||||||
toAppliedHeader)
|
toAppliedHeader)
|
||||||
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
|
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
|
||||||
@@ -50,12 +46,13 @@ import PostgREST.Response.GucHeader (GucHeader, unwrapGucHeader)
|
|||||||
import PostgREST.SchemaCache (SchemaCache (..))
|
import PostgREST.SchemaCache (SchemaCache (..))
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
import PostgREST.SchemaCache.Routine (FuncVolatility (..),
|
import PostgREST.SchemaCache.Proc (ProcDescription (..),
|
||||||
Routine (..), RoutineMap)
|
ProcVolatility (..),
|
||||||
|
ProcsMap)
|
||||||
import PostgREST.SchemaCache.Table (Table (..), TablesMap)
|
import PostgREST.SchemaCache.Table (Table (..), TablesMap)
|
||||||
|
|
||||||
import qualified PostgREST.ApiRequest.Types as ApiRequestTypes
|
import qualified PostgREST.ApiRequest.Types as ApiRequestTypes
|
||||||
import qualified PostgREST.SchemaCache.Routine as Routine
|
import qualified PostgREST.SchemaCache.Proc as Proc
|
||||||
|
|
||||||
import Protolude hiding (Handler, toS)
|
import Protolude hiding (Handler, toS)
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
@@ -86,7 +83,7 @@ readResponse headersOnly identifier ctxApiRequest@ApiRequest{..} resultSet = cas
|
|||||||
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
createResponse :: QualifiedIdentifier -> MutateReadPlan -> ApiRequest -> ResultSet -> Wai.Response
|
createResponse :: QualifiedIdentifier -> MutateReadPlan -> ApiRequest -> ResultSet -> Wai.Response
|
||||||
createResponse QualifiedIdentifier{..} MutateReadPlan{mrMutatePlan} ctxApiRequest@ApiRequest{iPreferences=Preferences{..}, ..} resultSet = case resultSet of
|
createResponse QualifiedIdentifier{..} MutateReadPlan{mrMutatePlan} ctxApiRequest@ApiRequest{..} resultSet = case resultSet of
|
||||||
RSStandard{..} -> do
|
RSStandard{..} -> do
|
||||||
let
|
let
|
||||||
pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;}
|
pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;}
|
||||||
@@ -103,15 +100,14 @@ createResponse QualifiedIdentifier{..} MutateReadPlan{mrMutatePlan} ctxApiReques
|
|||||||
<> HTTP.renderSimpleQuery True rsLocation
|
<> HTTP.renderSimpleQuery True rsLocation
|
||||||
)
|
)
|
||||||
, Just . RangeQuery.contentRangeH 1 0 $
|
, Just . RangeQuery.contentRangeH 1 0 $
|
||||||
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
if shouldCount iPreferCount then Just rsQueryTotal else Nothing
|
||||||
, if null pkCols && isNothing (qsOnConflict iQueryParams) then
|
, if null pkCols && isNothing (qsOnConflict iQueryParams) then
|
||||||
Nothing
|
Nothing
|
||||||
else
|
else
|
||||||
toAppliedHeader <$> preferResolution
|
toAppliedHeader <$> iPreferResolution
|
||||||
, toAppliedHeader <$> preferMissing
|
|
||||||
]
|
]
|
||||||
|
|
||||||
if preferRepresentation == Full then
|
if iPreferRepresentation == Full then
|
||||||
response HTTP.status201 (headers ++ contentTypeHeaders ctxApiRequest) (LBS.fromStrict rsBody)
|
response HTTP.status201 (headers ++ contentTypeHeaders ctxApiRequest) (LBS.fromStrict rsBody)
|
||||||
else
|
else
|
||||||
response HTTP.status201 headers mempty
|
response HTTP.status201 headers mempty
|
||||||
@@ -120,16 +116,16 @@ createResponse QualifiedIdentifier{..} MutateReadPlan{mrMutatePlan} ctxApiReques
|
|||||||
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
updateResponse :: ApiRequest -> ResultSet -> Wai.Response
|
updateResponse :: ApiRequest -> ResultSet -> Wai.Response
|
||||||
updateResponse ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resultSet = case resultSet of
|
updateResponse ctxApiRequest@ApiRequest{..} resultSet = case resultSet of
|
||||||
RSStandard{..} -> do
|
RSStandard{..} -> do
|
||||||
let
|
let
|
||||||
response = gucResponse rsGucStatus rsGucHeaders
|
response = gucResponse rsGucStatus rsGucHeaders
|
||||||
contentRangeHeader =
|
contentRangeHeader =
|
||||||
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
|
RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
|
||||||
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
if shouldCount iPreferCount then Just rsQueryTotal else Nothing
|
||||||
headers = catMaybes [contentRangeHeader, toAppliedHeader <$> preferMissing]
|
headers = [contentRangeHeader]
|
||||||
|
|
||||||
if preferRepresentation == Full then
|
if iPreferRepresentation == Full then
|
||||||
response HTTP.status200
|
response HTTP.status200
|
||||||
(headers ++ contentTypeHeaders ctxApiRequest)
|
(headers ++ contentTypeHeaders ctxApiRequest)
|
||||||
(LBS.fromStrict rsBody)
|
(LBS.fromStrict rsBody)
|
||||||
@@ -140,12 +136,12 @@ updateResponse ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resultSet
|
|||||||
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
singleUpsertResponse :: ApiRequest -> ResultSet -> Wai.Response
|
singleUpsertResponse :: ApiRequest -> ResultSet -> Wai.Response
|
||||||
singleUpsertResponse ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resultSet = case resultSet of
|
singleUpsertResponse ctxApiRequest@ApiRequest{..} resultSet = case resultSet of
|
||||||
RSStandard {..} -> do
|
RSStandard {..} -> do
|
||||||
let
|
let
|
||||||
response = gucResponse rsGucStatus rsGucHeaders
|
response = gucResponse rsGucStatus rsGucHeaders
|
||||||
|
|
||||||
if preferRepresentation == Full then
|
if iPreferRepresentation == Full then
|
||||||
response HTTP.status200 (contentTypeHeaders ctxApiRequest) (LBS.fromStrict rsBody)
|
response HTTP.status200 (contentTypeHeaders ctxApiRequest) (LBS.fromStrict rsBody)
|
||||||
else
|
else
|
||||||
response HTTP.status204 [] mempty
|
response HTTP.status204 [] mempty
|
||||||
@@ -154,16 +150,16 @@ singleUpsertResponse ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resu
|
|||||||
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
deleteResponse :: ApiRequest -> ResultSet -> Wai.Response
|
deleteResponse :: ApiRequest -> ResultSet -> Wai.Response
|
||||||
deleteResponse ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resultSet = case resultSet of
|
deleteResponse ctxApiRequest@ApiRequest{..} resultSet = case resultSet of
|
||||||
RSStandard {..} -> do
|
RSStandard {..} -> do
|
||||||
let
|
let
|
||||||
response = gucResponse rsGucStatus rsGucHeaders
|
response = gucResponse rsGucStatus rsGucHeaders
|
||||||
contentRangeHeader =
|
contentRangeHeader =
|
||||||
RangeQuery.contentRangeH 1 0 $
|
RangeQuery.contentRangeH 1 0 $
|
||||||
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
if shouldCount iPreferCount then Just rsQueryTotal else Nothing
|
||||||
headers = [contentRangeHeader]
|
headers = [contentRangeHeader]
|
||||||
|
|
||||||
if preferRepresentation == Full then
|
if iPreferRepresentation == Full then
|
||||||
response HTTP.status200
|
response HTTP.status200
|
||||||
(headers ++ contentTypeHeaders ctxApiRequest)
|
(headers ++ contentTypeHeaders ctxApiRequest)
|
||||||
(LBS.fromStrict rsBody)
|
(LBS.fromStrict rsBody)
|
||||||
@@ -173,12 +169,20 @@ deleteResponse ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} resultSet
|
|||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
infoIdentResponse :: QualifiedIdentifier -> SchemaCache -> Wai.Response
|
infoResponse :: Target -> SchemaCache -> Wai.Response
|
||||||
infoIdentResponse identifier sCache =
|
infoResponse target sCache =
|
||||||
case HM.lookup identifier (dbTables sCache) of
|
case target of
|
||||||
Just tbl -> respondInfo $ allowH tbl
|
TargetIdent identifier ->
|
||||||
Nothing -> Error.errorResponseFor $ Error.ApiRequestError ApiRequestTypes.NotFound
|
case HM.lookup identifier (dbTables sCache) of
|
||||||
|
Just tbl -> respondInfo $ allowH tbl
|
||||||
|
Nothing -> Error.errorResponseFor $ Error.ApiRequestError ApiRequestTypes.NotFound
|
||||||
|
TargetProc pd _
|
||||||
|
| pdVolatility pd == Volatile -> respondInfo "OPTIONS,POST"
|
||||||
|
| otherwise -> respondInfo "OPTIONS,GET,HEAD,POST"
|
||||||
|
TargetDefaultSpec _ -> respondInfo "OPTIONS,GET,HEAD"
|
||||||
where
|
where
|
||||||
|
respondInfo allowHeader = Wai.responseLBS HTTP.status200 [allOrigins, (HTTP.hAllow, allowHeader)] mempty
|
||||||
|
allOrigins = ("Access-Control-Allow-Origin", "*")
|
||||||
allowH table =
|
allowH table =
|
||||||
let hasPK = not . null $ tablePKCols table in
|
let hasPK = not . null $ tablePKCols table in
|
||||||
BS.intercalate "," $
|
BS.intercalate "," $
|
||||||
@@ -188,19 +192,7 @@ infoIdentResponse identifier sCache =
|
|||||||
["PATCH" | tableUpdatable table] ++
|
["PATCH" | tableUpdatable table] ++
|
||||||
["DELETE" | tableDeletable table]
|
["DELETE" | tableDeletable table]
|
||||||
|
|
||||||
infoProcResponse :: Routine -> Wai.Response
|
invokeResponse :: InvokeMethod -> ProcDescription -> ApiRequest -> ResultSet -> Wai.Response
|
||||||
infoProcResponse proc | pdVolatility proc == Volatile = respondInfo "OPTIONS,POST"
|
|
||||||
| otherwise = respondInfo "OPTIONS,GET,HEAD,POST"
|
|
||||||
|
|
||||||
infoRootResponse :: Wai.Response
|
|
||||||
infoRootResponse = respondInfo "OPTIONS,GET,HEAD"
|
|
||||||
|
|
||||||
respondInfo :: ByteString -> Wai.Response
|
|
||||||
respondInfo allowHeader =
|
|
||||||
let allOrigins = ("Access-Control-Allow-Origin", "*") in
|
|
||||||
Wai.responseLBS HTTP.status200 [allOrigins, (HTTP.hAllow, allowHeader)] mempty
|
|
||||||
|
|
||||||
invokeResponse :: InvokeMethod -> Routine -> ApiRequest -> ResultSet -> Wai.Response
|
|
||||||
invokeResponse invMethod proc ctxApiRequest@ApiRequest{..} resultSet = case resultSet of
|
invokeResponse invMethod proc ctxApiRequest@ApiRequest{..} resultSet = case resultSet of
|
||||||
RSStandard {..} -> do
|
RSStandard {..} -> do
|
||||||
let
|
let
|
||||||
@@ -213,7 +205,7 @@ invokeResponse invMethod proc ctxApiRequest@ApiRequest{..} resultSet = case resu
|
|||||||
else LBS.fromStrict rsBody
|
else LBS.fromStrict rsBody
|
||||||
headers = [contentRange]
|
headers = [contentRange]
|
||||||
|
|
||||||
if Routine.funcReturnsVoid proc then
|
if Proc.procReturnsVoid proc then
|
||||||
response HTTP.status204 headers mempty
|
response HTTP.status204 headers mempty
|
||||||
else
|
else
|
||||||
response status
|
response status
|
||||||
@@ -223,7 +215,7 @@ invokeResponse invMethod proc ctxApiRequest@ApiRequest{..} resultSet = case resu
|
|||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
Wai.responseLBS HTTP.status200 (contentTypeHeaders ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
openApiResponse :: Bool -> Maybe (TablesMap, RoutineMap, Maybe Text) -> AppConfig -> SchemaCache -> Schema -> Bool -> Wai.Response
|
openApiResponse :: Bool -> Maybe (TablesMap, ProcsMap, Maybe Text) -> AppConfig -> SchemaCache -> Schema -> Bool -> Wai.Response
|
||||||
openApiResponse headersOnly body conf sCache schema negotiatedByProfile =
|
openApiResponse headersOnly body conf sCache schema negotiatedByProfile =
|
||||||
Wai.responseLBS HTTP.status200
|
Wai.responseLBS HTTP.status200
|
||||||
(MediaType.toContentType MTOpenAPI : maybeToList (profileHeader schema negotiatedByProfile))
|
(MediaType.toContentType MTOpenAPI : maybeToList (profileHeader schema negotiatedByProfile))
|
||||||
@@ -271,14 +263,14 @@ isServiceUnavailable :: Wai.Response -> Bool
|
|||||||
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
|
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
|
||||||
|
|
||||||
optionalRollback :: AppConfig -> ApiRequest -> ExceptT Error.Error IO Wai.Response -> ExceptT Error.Error IO Wai.Response
|
optionalRollback :: AppConfig -> ApiRequest -> ExceptT Error.Error IO Wai.Response -> ExceptT Error.Error IO Wai.Response
|
||||||
optionalRollback AppConfig{..} ApiRequest{iPreferences=Preferences{..}} resp = do
|
optionalRollback AppConfig{..} ApiRequest{..} resp = do
|
||||||
newRes <- catchError resp $ return . Error.errorResponseFor
|
newRes <- catchError resp $ return . Error.errorResponseFor
|
||||||
return $ Wai.mapResponseHeaders preferenceApplied newRes
|
return $ Wai.mapResponseHeaders preferenceApplied newRes
|
||||||
where
|
where
|
||||||
shouldCommit =
|
shouldCommit =
|
||||||
configDbTxAllowOverride && preferTransaction == Just Commit
|
configDbTxAllowOverride && iPreferTransaction == Just Commit
|
||||||
shouldRollback =
|
shouldRollback =
|
||||||
configDbTxAllowOverride && preferTransaction == Just Rollback
|
configDbTxAllowOverride && iPreferTransaction == Just Rollback
|
||||||
preferenceApplied
|
preferenceApplied
|
||||||
| shouldCommit =
|
| shouldCommit =
|
||||||
addHeadersIfNotIncluded
|
addHeadersIfNotIncluded
|
||||||
@@ -294,11 +286,3 @@ addHeadersIfNotIncluded :: [HTTP.Header] -> [HTTP.Header] -> [HTTP.Header]
|
|||||||
addHeadersIfNotIncluded newHeaders initialHeaders =
|
addHeadersIfNotIncluded newHeaders initialHeaders =
|
||||||
filter (\(nk, _) -> isNothing $ find (\(ik, _) -> ik == nk) initialHeaders) newHeaders ++
|
filter (\(nk, _) -> isNothing $ find (\(ik, _) -> ik == nk) initialHeaders) newHeaders ++
|
||||||
initialHeaders
|
initialHeaders
|
||||||
|
|
||||||
traceHeaderMiddleware :: AppConfig -> Wai.Middleware
|
|
||||||
traceHeaderMiddleware AppConfig{configServerTraceHeader} app req respond =
|
|
||||||
case configServerTraceHeader of
|
|
||||||
Nothing -> app req respond
|
|
||||||
Just hdr ->
|
|
||||||
let hdrVal = L.lookup hdr $ Wai.requestHeaders req in
|
|
||||||
app req (respond . Wai.mapResponseHeaders ([(hdr, fromMaybe mempty hdrVal)] ++))
|
|
||||||
|
|||||||
@@ -28,21 +28,20 @@ import PostgREST.Config (AppConfig (..), Proxy (..),
|
|||||||
isMalformedProxyUri, toURI)
|
isMalformedProxyUri, toURI)
|
||||||
import PostgREST.SchemaCache (SchemaCache (..))
|
import PostgREST.SchemaCache (SchemaCache (..))
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
||||||
|
import PostgREST.SchemaCache.Proc (ProcDescription (..),
|
||||||
|
ProcParam (..))
|
||||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||||
Relationship (..),
|
Relationship (..),
|
||||||
RelationshipsMap)
|
RelationshipsMap)
|
||||||
import PostgREST.SchemaCache.Routine (Routine (..),
|
|
||||||
RoutineParam (..))
|
|
||||||
import PostgREST.SchemaCache.Table (Column (..), Table (..),
|
import PostgREST.SchemaCache.Table (Column (..), Table (..),
|
||||||
TablesMap,
|
TablesMap)
|
||||||
tableColumnsList)
|
|
||||||
import PostgREST.Version (docsVersion, prettyVersion)
|
import PostgREST.Version (docsVersion, prettyVersion)
|
||||||
|
|
||||||
import PostgREST.MediaType
|
import PostgREST.MediaType
|
||||||
|
|
||||||
import Protolude hiding (Proxy, get)
|
import Protolude hiding (Proxy, get)
|
||||||
|
|
||||||
encode :: AppConfig -> SchemaCache -> TablesMap -> HM.HashMap k [Routine] -> Maybe Text -> LBS.ByteString
|
encode :: AppConfig -> SchemaCache -> TablesMap -> HM.HashMap k [ProcDescription] -> Maybe Text -> LBS.ByteString
|
||||||
encode conf sCache tables procs schemaDescription =
|
encode conf sCache tables procs schemaDescription =
|
||||||
JSON.encode $
|
JSON.encode $
|
||||||
postgrestSpec
|
postgrestSpec
|
||||||
@@ -73,15 +72,9 @@ toSwaggerType colType = case T.takeEnd 2 colType of
|
|||||||
"[]" -> Just SwaggerArray
|
"[]" -> Just SwaggerArray
|
||||||
_ -> Just SwaggerString
|
_ -> Just SwaggerString
|
||||||
|
|
||||||
typeFromArray :: Text -> Text
|
makeSwaggerItemType :: Maybe (SwaggerType t) -> Text -> Maybe (Referenced Schema)
|
||||||
typeFromArray = T.dropEnd 2
|
makeSwaggerItemType itemType colType = case itemType of
|
||||||
|
Just SwaggerArray -> Just $ Inline (mempty & type_ .~ toSwaggerType (T.dropEnd 2 colType))
|
||||||
toSwaggerTypeFromArray :: Text -> Maybe (SwaggerType t)
|
|
||||||
toSwaggerTypeFromArray arrType = toSwaggerType $ typeFromArray arrType
|
|
||||||
|
|
||||||
makePropertyItems :: Text -> Maybe (Referenced Schema)
|
|
||||||
makePropertyItems arrType = case toSwaggerType arrType of
|
|
||||||
Just SwaggerArray -> Just $ Inline (mempty & type_ .~ toSwaggerTypeFromArray arrType)
|
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
parseDefault :: Text -> Text -> Text
|
parseDefault :: Text -> Text -> Text
|
||||||
@@ -100,8 +93,8 @@ makeTableDef rels t =
|
|||||||
(tn, (mempty :: Schema)
|
(tn, (mempty :: Schema)
|
||||||
& description .~ tableDescription t
|
& description .~ tableDescription t
|
||||||
& type_ ?~ SwaggerObject
|
& type_ ?~ SwaggerObject
|
||||||
& properties .~ fromList (makeProperty t rels <$> tableColumnsList t)
|
& properties .~ fromList (makeProperty t rels <$> tableColumns t)
|
||||||
& required .~ fmap colName (filter (not . colNullable) $ tableColumnsList t))
|
& required .~ fmap colName (filter (not . colNullable) $ tableColumns t))
|
||||||
|
|
||||||
makeProperty :: Table -> RelationshipsMap -> Column -> (Text, Referenced Schema)
|
makeProperty :: Table -> RelationshipsMap -> Column -> (Text, Referenced Schema)
|
||||||
makeProperty tbl rels col = (colName col, Inline s)
|
makeProperty tbl rels col = (colName col, Inline s)
|
||||||
@@ -135,6 +128,7 @@ makeProperty tbl rels col = (colName col, Inline s)
|
|||||||
Just $ T.append (maybe "" (`T.append` "\n\n") $ colDescription col) (T.intercalate "\n" n)
|
Just $ T.append (maybe "" (`T.append` "\n\n") $ colDescription col) (T.intercalate "\n" n)
|
||||||
else
|
else
|
||||||
colDescription col
|
colDescription col
|
||||||
|
pType = toSwaggerType (colType col)
|
||||||
s =
|
s =
|
||||||
(mempty :: Schema)
|
(mempty :: Schema)
|
||||||
& default_ .~ (JSON.decode . toUtf8Lazy . parseDefault (colType col) =<< colDefault col)
|
& default_ .~ (JSON.decode . toUtf8Lazy . parseDefault (colType col) =<< colDefault col)
|
||||||
@@ -142,10 +136,10 @@ makeProperty tbl rels col = (colName col, Inline s)
|
|||||||
& enum_ .~ e
|
& enum_ .~ e
|
||||||
& format ?~ colType col
|
& format ?~ colType col
|
||||||
& maxLength .~ (fromIntegral <$> colMaxLen col)
|
& maxLength .~ (fromIntegral <$> colMaxLen col)
|
||||||
& type_ .~ toSwaggerType (colType col)
|
& type_ .~ pType
|
||||||
& items .~ (SwaggerItemsObject <$> makePropertyItems (colType col))
|
& items .~ (SwaggerItemsObject <$> makeSwaggerItemType pType (colType col))
|
||||||
|
|
||||||
makeProcSchema :: Routine -> Schema
|
makeProcSchema :: ProcDescription -> Schema
|
||||||
makeProcSchema pd =
|
makeProcSchema pd =
|
||||||
(mempty :: Schema)
|
(mempty :: Schema)
|
||||||
& description .~ pdDescription pd
|
& description .~ pdDescription pd
|
||||||
@@ -153,12 +147,12 @@ makeProcSchema pd =
|
|||||||
& properties .~ fromList (fmap makeProcProperty (pdParams pd))
|
& properties .~ fromList (fmap makeProcProperty (pdParams pd))
|
||||||
& required .~ fmap ppName (filter ppReq (pdParams pd))
|
& required .~ fmap ppName (filter ppReq (pdParams pd))
|
||||||
|
|
||||||
makeProcProperty :: RoutineParam -> (Text, Referenced Schema)
|
makeProcProperty :: ProcParam -> (Text, Referenced Schema)
|
||||||
makeProcProperty (RoutineParam 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
|
||||||
& items .~ (SwaggerItemsObject <$> makePropertyItems t)
|
& items .~ (SwaggerItemsObject <$> makeSwaggerItemType (toSwaggerType t) t)
|
||||||
& format ?~ t
|
& format ?~ t
|
||||||
|
|
||||||
makePreferParam :: [Text] -> Param
|
makePreferParam :: [Text] -> Param
|
||||||
@@ -180,37 +174,8 @@ makePreferParam ts =
|
|||||||
"resolution" -> ["resolution=ignore-duplicates", "resolution=merge-duplicates"]
|
"resolution" -> ["resolution=ignore-duplicates", "resolution=merge-duplicates"]
|
||||||
_ -> []
|
_ -> []
|
||||||
|
|
||||||
makeProcGetParam :: RoutineParam -> Referenced Param
|
makeProcParam :: ProcDescription -> [Referenced Param]
|
||||||
makeProcGetParam (RoutineParam n t r v) =
|
makeProcParam pd =
|
||||||
Inline $ (mempty :: Param)
|
|
||||||
& name .~ n
|
|
||||||
& required ?~ r
|
|
||||||
& schema .~ ParamOther fullSchema
|
|
||||||
where
|
|
||||||
fullSchema = if v then schemaMulti else schemaNotMulti
|
|
||||||
baseSchema = (mempty :: ParamOtherSchema)
|
|
||||||
& in_ .~ ParamQuery
|
|
||||||
schemaNotMulti = baseSchema
|
|
||||||
& format ?~ t
|
|
||||||
& type_ ?~ toParamType (toSwaggerType t)
|
|
||||||
schemaMulti = baseSchema
|
|
||||||
& type_ ?~ fromMaybe SwaggerString (toSwaggerType t)
|
|
||||||
& items ?~ SwaggerItemsPrimitive (Just CollectionMulti)
|
|
||||||
((mempty :: ParamSchema x)
|
|
||||||
& type_ .~ toSwaggerTypeFromArray t
|
|
||||||
& format ?~ typeFromArray t)
|
|
||||||
toParamType paramType = case paramType of
|
|
||||||
-- Array uses {} in query params
|
|
||||||
Just SwaggerArray -> SwaggerString
|
|
||||||
-- Type must be specified in query params
|
|
||||||
Nothing -> SwaggerString
|
|
||||||
_ -> fromJust paramType
|
|
||||||
|
|
||||||
makeProcGetParams :: [RoutineParam] -> [Referenced Param]
|
|
||||||
makeProcGetParams = fmap makeProcGetParam
|
|
||||||
|
|
||||||
makeProcPostParams :: Routine -> [Referenced Param]
|
|
||||||
makeProcPostParams pd =
|
|
||||||
[ Inline $ (mempty :: Param)
|
[ Inline $ (mempty :: Param)
|
||||||
& name .~ "args"
|
& name .~ "args"
|
||||||
& required ?~ True
|
& required ?~ True
|
||||||
@@ -276,7 +241,7 @@ makeParamDefs ti =
|
|||||||
& in_ .~ ParamQuery
|
& in_ .~ ParamQuery
|
||||||
& type_ ?~ SwaggerString))
|
& type_ ?~ SwaggerString))
|
||||||
]
|
]
|
||||||
<> concat [ makeObjectBody (tableName t) : makeRowFilters (tableName t) (tableColumnsList t)
|
<> concat [ makeObjectBody (tableName t) : makeRowFilters (tableName t) (tableColumns t)
|
||||||
| t <- ti
|
| t <- ti
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -337,29 +302,24 @@ makePathItem t = ("/" ++ T.unpack tn, p $ tableInsertable t || tableUpdatable t
|
|||||||
p False = pr
|
p False = pr
|
||||||
p True = pw
|
p True = pw
|
||||||
tn = tableName t
|
tn = tableName t
|
||||||
rs = [ T.intercalate "." ["rowFilter", tn, colName c ] | c <- tableColumnsList t ]
|
rs = [ T.intercalate "." ["rowFilter", tn, colName c ] | c <- tableColumns t ]
|
||||||
ref = Ref . Reference
|
ref = Ref . Reference
|
||||||
|
|
||||||
makeProcPathItem :: Routine -> (FilePath, PathItem)
|
makeProcPathItem :: ProcDescription -> (FilePath, PathItem)
|
||||||
makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
|
makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
|
||||||
where
|
where
|
||||||
-- Use first line of proc description as summary; rest as description (if present)
|
-- Use first line of proc description as summary; rest as description (if present)
|
||||||
-- We strip leading newlines from description so that users can include a blank line between summary and description
|
-- We strip leading newlines from description so that users can include a blank line between summary and description
|
||||||
(pSum, pDesc) = fmap fst &&& fmap (T.dropWhile (=='\n') . snd) $
|
(pSum, pDesc) = fmap fst &&& fmap (T.dropWhile (=='\n') . snd) $
|
||||||
T.breakOn "\n" <$> pdDescription pd
|
T.breakOn "\n" <$> pdDescription pd
|
||||||
procOp = (mempty :: Operation)
|
postOp = (mempty :: Operation)
|
||||||
& summary .~ pSum
|
& summary .~ pSum
|
||||||
& description .~ mfilter (/="") pDesc
|
& description .~ mfilter (/="") pDesc
|
||||||
|
& parameters .~ makeProcParam pd
|
||||||
& tags .~ Set.fromList ["(rpc) " <> pdName pd]
|
& tags .~ Set.fromList ["(rpc) " <> pdName pd]
|
||||||
& produces ?~ makeMimeList [MTApplicationJSON, MTSingularJSON]
|
& produces ?~ makeMimeList [MTApplicationJSON, MTSingularJSON]
|
||||||
& at 200 ?~ "OK"
|
& at 200 ?~ "OK"
|
||||||
getOp = procOp
|
pe = (mempty :: PathItem) & post ?~ postOp
|
||||||
& parameters .~ makeProcGetParams (pdParams pd)
|
|
||||||
postOp = procOp
|
|
||||||
& parameters .~ makeProcPostParams pd
|
|
||||||
pe = (mempty :: PathItem)
|
|
||||||
& get ?~ getOp
|
|
||||||
& post ?~ postOp
|
|
||||||
|
|
||||||
makeRootPathItem :: (FilePath, PathItem)
|
makeRootPathItem :: (FilePath, PathItem)
|
||||||
makeRootPathItem = ("/", p)
|
makeRootPathItem = ("/", p)
|
||||||
@@ -372,7 +332,7 @@ makeRootPathItem = ("/", p)
|
|||||||
pr = (mempty :: PathItem) & get ?~ getOp
|
pr = (mempty :: PathItem) & get ?~ getOp
|
||||||
p = pr
|
p = pr
|
||||||
|
|
||||||
makePathItems :: [Routine] -> [Table] -> InsOrdHashMap FilePath PathItem
|
makePathItems :: [ProcDescription] -> [Table] -> InsOrdHashMap FilePath PathItem
|
||||||
makePathItems pds ti = fromList $ makeRootPathItem :
|
makePathItems pds ti = fromList $ makeRootPathItem :
|
||||||
fmap makePathItem ti ++ fmap makeProcPathItem pds
|
fmap makePathItem ti ++ fmap makeProcPathItem pds
|
||||||
|
|
||||||
@@ -392,14 +352,14 @@ escapeHostName "*6" = "0.0.0.0"
|
|||||||
escapeHostName "!6" = "0.0.0.0"
|
escapeHostName "!6" = "0.0.0.0"
|
||||||
escapeHostName h = h
|
escapeHostName h = h
|
||||||
|
|
||||||
postgrestSpec :: RelationshipsMap -> [Routine] -> [Table] -> (Text, Text, Integer, Text) -> Maybe Text -> Bool -> Swagger
|
postgrestSpec :: RelationshipsMap -> [ProcDescription] -> [Table] -> (Text, Text, Integer, Text) -> Maybe Text -> Bool -> Swagger
|
||||||
postgrestSpec rels pds ti (s, h, p, b) sd allowSecurityDef = (mempty :: Swagger)
|
postgrestSpec rels pds ti (s, h, p, b) sd allowSecurityDef = (mempty :: Swagger)
|
||||||
& basePath ?~ T.unpack b
|
& basePath ?~ T.unpack b
|
||||||
& schemes ?~ [s']
|
& schemes ?~ [s']
|
||||||
& info .~ ((mempty :: Info)
|
& info .~ ((mempty :: Info)
|
||||||
& version .~ T.decodeUtf8 prettyVersion
|
& version .~ T.decodeUtf8 prettyVersion
|
||||||
& title .~ fromMaybe "PostgREST API" dTitle
|
& title .~ "PostgREST API"
|
||||||
& description ?~ fromMaybe "This is a dynamic API generated by PostgREST" dDesc)
|
& description ?~ d)
|
||||||
& externalDocs ?~ ((mempty :: ExternalDocs)
|
& externalDocs ?~ ((mempty :: ExternalDocs)
|
||||||
& description ?~ "PostgREST Documentation"
|
& description ?~ "PostgREST Documentation"
|
||||||
& url .~ URL ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
|
& url .~ URL ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
|
||||||
@@ -414,9 +374,8 @@ postgrestSpec rels pds ti (s, h, p, b) sd allowSecurityDef = (mempty :: Swagger)
|
|||||||
where
|
where
|
||||||
s' = if s == "http" then Http else Https
|
s' = if s == "http" then Http else Https
|
||||||
h' = Just $ Host (T.unpack $ escapeHostName h) (Just (fromInteger p))
|
h' = Just $ Host (T.unpack $ escapeHostName h) (Just (fromInteger p))
|
||||||
|
d = fromMaybe "This is a dynamic API generated by PostgREST" sd
|
||||||
securityDefName = "JWT"
|
securityDefName = "JWT"
|
||||||
(dTitle, dDesc) = fmap fst &&& fmap (T.dropWhile (=='\n') . snd) $
|
|
||||||
T.breakOn "\n" <$> sd
|
|
||||||
|
|
||||||
pickProxy :: Maybe Text -> Maybe Proxy
|
pickProxy :: Maybe Text -> Maybe Proxy
|
||||||
pickProxy proxy
|
pickProxy proxy
|
||||||
|
|||||||
@@ -22,38 +22,38 @@ module PostgREST.SchemaCache
|
|||||||
( SchemaCache(..)
|
( SchemaCache(..)
|
||||||
, querySchemaCache
|
, querySchemaCache
|
||||||
, accessibleTables
|
, accessibleTables
|
||||||
, accessibleFuncs
|
, accessibleProcs
|
||||||
, schemaDescription
|
, schemaDescription
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Data.HashMap.Strict.InsOrd as HMI
|
import qualified Data.Set as S
|
||||||
import qualified Data.Set as S
|
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 SQL
|
||||||
import qualified Hasql.Statement as SQL
|
import qualified Hasql.Transaction as SQL
|
||||||
import qualified Hasql.Transaction as SQL
|
|
||||||
|
|
||||||
import Contravariant.Extras (contrazip2)
|
import Contravariant.Extras (contrazip2)
|
||||||
import Text.InterpolatedString.Perl6 (q)
|
import Text.InterpolatedString.Perl6 (q)
|
||||||
|
|
||||||
import PostgREST.Config.Database (pgVersionStatement)
|
import PostgREST.Config.Database (pgVersionStatement)
|
||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
||||||
pgVersion110, pgVersion120)
|
pgVersion110)
|
||||||
import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName,
|
import PostgREST.SchemaCache.Identifiers (AccessSet, FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
|
import PostgREST.SchemaCache.Proc (PgType (..),
|
||||||
|
ProcDescription (..),
|
||||||
|
ProcParam (..),
|
||||||
|
ProcVolatility (..),
|
||||||
|
ProcsMap, RetType (..))
|
||||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
Relationship (..),
|
Relationship (..),
|
||||||
RelationshipsMap)
|
RelationshipsMap)
|
||||||
import PostgREST.SchemaCache.Routine (FuncVolatility (..),
|
import PostgREST.SchemaCache.Table (Column (..), Table (..),
|
||||||
PgType (..), RetType (..),
|
TablesMap)
|
||||||
Routine (..), RoutineMap,
|
|
||||||
RoutineParam (..))
|
|
||||||
import PostgREST.SchemaCache.Table (Column (..), ColumnMap,
|
|
||||||
Table (..), TablesMap)
|
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ import Protolude
|
|||||||
data SchemaCache = SchemaCache
|
data SchemaCache = SchemaCache
|
||||||
{ dbTables :: TablesMap
|
{ dbTables :: TablesMap
|
||||||
, dbRelationships :: RelationshipsMap
|
, dbRelationships :: RelationshipsMap
|
||||||
, dbRoutines :: RoutineMap
|
, dbProcs :: ProcsMap
|
||||||
}
|
}
|
||||||
deriving (Generic, JSON.ToJSON)
|
deriving (Generic, JSON.ToJSON)
|
||||||
|
|
||||||
@@ -106,11 +106,11 @@ type SqlQuery = ByteString
|
|||||||
querySchemaCache :: [Schema] -> [Schema] -> Bool -> SQL.Transaction SchemaCache
|
querySchemaCache :: [Schema] -> [Schema] -> Bool -> SQL.Transaction SchemaCache
|
||||||
querySchemaCache schemas extraSearchPath prepared = do
|
querySchemaCache schemas extraSearchPath prepared = do
|
||||||
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
|
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
|
||||||
pgVer <- SQL.statement mempty $ pgVersionStatement prepared
|
pgVer <- SQL.statement mempty pgVersionStatement
|
||||||
tabs <- SQL.statement schemas $ allTables pgVer prepared
|
tabs <- SQL.statement schemas $ allTables pgVer prepared
|
||||||
keyDeps <- SQL.statement (schemas, extraSearchPath) $ allViewsKeyDependencies prepared
|
keyDeps <- SQL.statement (schemas, extraSearchPath) $ allViewsKeyDependencies prepared
|
||||||
m2oRels <- SQL.statement mempty $ allM2OandO2ORels pgVer prepared
|
m2oRels <- SQL.statement mempty $ allM2OandO2ORels pgVer prepared
|
||||||
funcs <- SQL.statement schemas $ allFunctions pgVer prepared
|
procs <- SQL.statement schemas $ allProcs pgVer prepared
|
||||||
cRels <- SQL.statement mempty $ allComputedRels prepared
|
cRels <- SQL.statement mempty $ allComputedRels prepared
|
||||||
|
|
||||||
let tabsWViewsPks = addViewPrimaryKeys tabs keyDeps
|
let tabsWViewsPks = addViewPrimaryKeys tabs keyDeps
|
||||||
@@ -119,7 +119,7 @@ querySchemaCache schemas extraSearchPath prepared = do
|
|||||||
return $ removeInternal schemas $ SchemaCache {
|
return $ removeInternal schemas $ SchemaCache {
|
||||||
dbTables = tabsWViewsPks
|
dbTables = tabsWViewsPks
|
||||||
, dbRelationships = getOverrideRelationshipsMap rels cRels
|
, dbRelationships = getOverrideRelationshipsMap rels cRels
|
||||||
, dbRoutines = funcs
|
, dbProcs = procs
|
||||||
}
|
}
|
||||||
|
|
||||||
-- | overrides detected relationships with the computed relationships and gets the RelationshipsMap
|
-- | overrides detected relationships with the computed relationships and gets the RelationshipsMap
|
||||||
@@ -149,7 +149,7 @@ removeInternal schemas dbStruct =
|
|||||||
dbTables = HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch `elem` schemas) $ dbTables dbStruct
|
dbTables = HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch `elem` schemas) $ dbTables dbStruct
|
||||||
, dbRelationships = filter (\r -> qiSchema (relForeignTable r) `elem` schemas && not (hasInternalJunction r)) <$>
|
, dbRelationships = filter (\r -> qiSchema (relForeignTable r) `elem` schemas && not (hasInternalJunction r)) <$>
|
||||||
HM.filterWithKey (\(QualifiedIdentifier sch _, _) _ -> sch `elem` schemas ) (dbRelationships dbStruct)
|
HM.filterWithKey (\(QualifiedIdentifier sch _, _) _ -> sch `elem` schemas ) (dbRelationships dbStruct)
|
||||||
, dbRoutines = dbRoutines dbStruct -- procs are only obtained from the exposed schemas, no need to filter them.
|
, dbProcs = dbProcs dbStruct -- procs are only obtained from the exposed schemas, no need to filter them.
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
hasInternalJunction ComputedRelationship{} = False
|
hasInternalJunction ComputedRelationship{} = False
|
||||||
@@ -178,20 +178,15 @@ decodeTables =
|
|||||||
<*> column HD.bool
|
<*> column HD.bool
|
||||||
<*> column HD.bool
|
<*> column HD.bool
|
||||||
<*> arrayColumn HD.text
|
<*> arrayColumn HD.text
|
||||||
<*> parseCols (compositeArrayColumn
|
<*> compositeArrayColumn
|
||||||
(Column
|
(Column
|
||||||
<$> compositeField HD.text
|
<$> compositeField HD.text
|
||||||
<*> nullableCompositeField HD.text
|
<*> nullableCompositeField HD.text
|
||||||
<*> compositeField HD.bool
|
<*> compositeField HD.bool
|
||||||
<*> compositeField HD.text
|
<*> compositeField HD.text
|
||||||
<*> compositeField HD.text
|
|
||||||
<*> nullableCompositeField HD.int4
|
<*> nullableCompositeField HD.int4
|
||||||
<*> nullableCompositeField HD.text
|
<*> nullableCompositeField HD.text
|
||||||
<*> compositeFieldArray HD.text))
|
<*> compositeFieldArray HD.text)
|
||||||
|
|
||||||
|
|
||||||
parseCols :: HD.Row [Column] -> HD.Row ColumnMap
|
|
||||||
parseCols = fmap (HMI.fromList . map (\col@Column{colName} -> (colName, col)))
|
|
||||||
|
|
||||||
decodeRels :: HD.Result [Relationship]
|
decodeRels :: HD.Result [Relationship]
|
||||||
decodeRels =
|
decodeRels =
|
||||||
@@ -227,17 +222,17 @@ viewKeyDepFromRow (s1,t1,s2,v2,cons,consType,sCols) = ViewKeyDependency (Qualifi
|
|||||||
| consType == "f" = FKDep
|
| consType == "f" = FKDep
|
||||||
| otherwise = FKDepRef -- f_ref, we build this type in the query
|
| otherwise = FKDepRef -- f_ref, we build this type in the query
|
||||||
|
|
||||||
decodeFuncs :: HD.Result RoutineMap
|
decodeProcs :: HD.Result ProcsMap
|
||||||
decodeFuncs =
|
decodeProcs =
|
||||||
-- Duplicate rows for a function means they're overloaded, order these by least args according to Routine Ord instance
|
-- Duplicate rows for a function means they're overloaded, order these by least args according to ProcDescription Ord instance
|
||||||
map sort . HM.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addKey) <$> HD.rowList funcRow
|
map sort . HM.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addKey) <$> HD.rowList procRow
|
||||||
where
|
where
|
||||||
funcRow = Function
|
procRow = ProcDescription
|
||||||
<$> column HD.text
|
<$> column HD.text
|
||||||
<*> column HD.text
|
<*> column HD.text
|
||||||
<*> nullableColumn HD.text
|
<*> nullableColumn HD.text
|
||||||
<*> compositeArrayColumn
|
<*> compositeArrayColumn
|
||||||
(RoutineParam
|
(ProcParam
|
||||||
<$> compositeField HD.text
|
<$> compositeField HD.text
|
||||||
<*> compositeField HD.text
|
<*> compositeField HD.text
|
||||||
<*> compositeField HD.bool
|
<*> compositeField HD.bool
|
||||||
@@ -250,38 +245,38 @@ decodeFuncs =
|
|||||||
<*> column HD.bool)
|
<*> column HD.bool)
|
||||||
<*> (parseVolatility <$> column HD.char)
|
<*> (parseVolatility <$> column HD.char)
|
||||||
<*> column HD.bool
|
<*> column HD.bool
|
||||||
<*> nullableColumn HD.text
|
|
||||||
|
|
||||||
addKey :: Routine -> (QualifiedIdentifier, Routine)
|
addKey :: ProcDescription -> (QualifiedIdentifier, ProcDescription)
|
||||||
addKey pd = (QualifiedIdentifier (pdSchema pd) (pdName pd), pd)
|
addKey pd = (QualifiedIdentifier (pdSchema pd) (pdName pd), pd)
|
||||||
|
|
||||||
parseRetType :: Text -> Text -> Bool -> Bool -> Bool -> RetType
|
parseRetType :: Text -> Text -> Bool -> Bool -> Bool -> Maybe RetType
|
||||||
parseRetType schema name isSetOf isComposite isCompositeAlias
|
parseRetType schema name isSetOf isComposite isVoid
|
||||||
| isSetOf = SetOf pgType
|
| isVoid = Nothing
|
||||||
| otherwise = Single pgType
|
| isSetOf = Just (SetOf pgType)
|
||||||
|
| otherwise = Just (Single pgType)
|
||||||
where
|
where
|
||||||
qi = QualifiedIdentifier schema name
|
qi = QualifiedIdentifier schema name
|
||||||
pgType
|
pgType
|
||||||
| isComposite = Composite qi isCompositeAlias
|
| isComposite = Composite qi
|
||||||
| otherwise = Scalar qi
|
| otherwise = Scalar
|
||||||
|
|
||||||
parseVolatility :: Char -> FuncVolatility
|
parseVolatility :: Char -> ProcVolatility
|
||||||
parseVolatility v | v == 'i' = Immutable
|
parseVolatility v | v == 'i' = Immutable
|
||||||
| v == 's' = Stable
|
| v == 's' = Stable
|
||||||
| otherwise = Volatile -- only 'v' can happen here
|
| otherwise = Volatile -- only 'v' can happen here
|
||||||
|
|
||||||
allFunctions :: PgVersion -> Bool -> SQL.Statement [Schema] RoutineMap
|
allProcs :: PgVersion -> Bool -> SQL.Statement [Schema] ProcsMap
|
||||||
allFunctions pgVer = SQL.Statement sql (arrayParam HE.text) decodeFuncs
|
allProcs pgVer = SQL.Statement sql (arrayParam HE.text) decodeProcs
|
||||||
where
|
where
|
||||||
sql = funcsSqlQuery pgVer <> " AND pn.nspname = ANY($1)"
|
sql = procsSqlQuery pgVer <> " AND pn.nspname = ANY($1)"
|
||||||
|
|
||||||
accessibleFuncs :: PgVersion -> Bool -> SQL.Statement Schema RoutineMap
|
accessibleProcs :: PgVersion -> Bool -> SQL.Statement Schema ProcsMap
|
||||||
accessibleFuncs pgVer = SQL.Statement sql (param HE.text) decodeFuncs
|
accessibleProcs pgVer = SQL.Statement sql (param HE.text) decodeProcs
|
||||||
where
|
where
|
||||||
sql = funcsSqlQuery pgVer <> " AND pn.nspname = $1 AND has_function_privilege(p.oid, 'execute')"
|
sql = procsSqlQuery pgVer <> " AND pn.nspname = $1 AND has_function_privilege(p.oid, 'execute')"
|
||||||
|
|
||||||
funcsSqlQuery :: PgVersion -> SqlQuery
|
procsSqlQuery :: PgVersion -> SqlQuery
|
||||||
funcsSqlQuery pgVer = [q|
|
procsSqlQuery pgVer = [q|
|
||||||
-- Recursively get the base types of domains
|
-- Recursively get the base types of domains
|
||||||
WITH
|
WITH
|
||||||
base_types AS (
|
base_types AS (
|
||||||
@@ -338,10 +333,9 @@ funcsSqlQuery pgVer = [q|
|
|||||||
-- if any TABLE, INOUT or OUT arguments present, treat as composite
|
-- if any TABLE, INOUT or OUT arguments present, treat as composite
|
||||||
or COALESCE(proargmodes::text[] && '{t,b,o}', false)
|
or COALESCE(proargmodes::text[] && '{t,b,o}', false)
|
||||||
) AS rettype_is_composite,
|
) AS rettype_is_composite,
|
||||||
bt.oid <> bt.base as rettype_is_composite_alias,
|
('void'::regtype = t.oid) AS rettype_is_void,
|
||||||
p.provolatile,
|
p.provolatile,
|
||||||
p.provariadic > 0 as hasvariadic,
|
p.provariadic > 0 as hasvariadic
|
||||||
lower((regexp_split_to_array((regexp_split_to_array(config, '='))[2], ','))[1]) AS transaction_isolation_level
|
|
||||||
FROM pg_proc p
|
FROM pg_proc p
|
||||||
LEFT JOIN arguments a ON a.oid = p.oid
|
LEFT JOIN arguments a ON a.oid = p.oid
|
||||||
JOIN pg_namespace pn ON pn.oid = p.pronamespace
|
JOIN pg_namespace pn ON pn.oid = p.pronamespace
|
||||||
@@ -350,7 +344,6 @@ funcsSqlQuery pgVer = [q|
|
|||||||
JOIN pg_namespace tn ON tn.oid = t.typnamespace
|
JOIN pg_namespace tn ON tn.oid = t.typnamespace
|
||||||
LEFT JOIN pg_class comp ON comp.oid = t.typrelid
|
LEFT JOIN pg_class comp ON comp.oid = t.typrelid
|
||||||
LEFT JOIN pg_description as d ON d.objoid = p.oid
|
LEFT JOIN pg_description as d ON d.objoid = p.oid
|
||||||
LEFT JOIN LATERAL unnest(proconfig) config ON config like 'default_transaction_isolation%'
|
|
||||||
WHERE t.oid <> 'trigger'::regtype AND COALESCE(a.callable, true)
|
WHERE t.oid <> 'trigger'::regtype AND COALESCE(a.callable, true)
|
||||||
|] <> (if pgVer >= pgVersion110 then "AND prokind = 'f'" else "AND NOT (proisagg OR proiswindow)")
|
|] <> (if pgVer >= pgVersion110 then "AND prokind = 'f'" else "AND NOT (proisagg OR proiswindow)")
|
||||||
|
|
||||||
@@ -406,7 +399,7 @@ test | personnages_view | test | actors_view | personnage
|
|||||||
-}
|
-}
|
||||||
addViewM2OAndO2ORels :: [ViewKeyDependency] -> [Relationship] -> [Relationship]
|
addViewM2OAndO2ORels :: [ViewKeyDependency] -> [Relationship] -> [Relationship]
|
||||||
addViewM2OAndO2ORels keyDeps rels =
|
addViewM2OAndO2ORels keyDeps rels =
|
||||||
rels ++ concatMap viewRels rels
|
rels ++ concat (viewRels <$> rels)
|
||||||
where
|
where
|
||||||
isM2O card = case card of {M2O _ _ -> True; _ -> False;}
|
isM2O card = case card of {M2O _ _ -> True; _ -> False;}
|
||||||
isO2O card = case card of {O2O _ _ -> True; _ -> False;}
|
isO2O card = case card of {O2O _ _ -> True; _ -> False;}
|
||||||
@@ -456,7 +449,7 @@ addViewM2OAndO2ORels keyDeps rels =
|
|||||||
, keyDepColsTblVw <- expandKeyDepCols $ keyDepCols tblVw ]
|
, keyDepColsTblVw <- expandKeyDepCols $ keyDepCols tblVw ]
|
||||||
else []
|
else []
|
||||||
viewRels _ = []
|
viewRels _ = []
|
||||||
expandKeyDepCols kdc = zip (fst <$> kdc) <$> traverse snd kdc
|
expandKeyDepCols kdc = zip (fst <$> kdc) <$> sequenceA (snd <$> kdc)
|
||||||
|
|
||||||
addInverseRels :: [Relationship] -> [Relationship]
|
addInverseRels :: [Relationship] -> [Relationship]
|
||||||
addInverseRels rels =
|
addInverseRels rels =
|
||||||
@@ -492,7 +485,7 @@ addViewPrimaryKeys tabs keyDeps =
|
|||||||
-- * We don't have any logic that requires the client to name a PK column (compared to the column hints in embedding for FKs),
|
-- * We don't have any logic that requires the client to name a PK column (compared to the column hints in embedding for FKs),
|
||||||
-- so we don't need to know about the other references.
|
-- so we don't need to know about the other references.
|
||||||
-- * We need to choose a single reference for each column, otherwise we'd output too many columns in location headers etc.
|
-- * We need to choose a single reference for each column, otherwise we'd output too many columns in location headers etc.
|
||||||
takeFirstPK = mapMaybe (head . snd)
|
takeFirstPK pkCols = catMaybes $ head . snd <$> pkCols
|
||||||
|
|
||||||
allTables :: PgVersion -> Bool -> SQL.Statement [Schema] TablesMap
|
allTables :: PgVersion -> Bool -> SQL.Statement [Schema] TablesMap
|
||||||
allTables pgVer =
|
allTables pgVer =
|
||||||
@@ -506,8 +499,6 @@ tablesSqlQuery pgVer =
|
|||||||
-- the tbl_constraints/key_col_usage CTEs are based on the standard "information_schema.table_constraints"/"information_schema.key_column_usage" views,
|
-- the tbl_constraints/key_col_usage CTEs are based on the standard "information_schema.table_constraints"/"information_schema.key_column_usage" views,
|
||||||
-- we cannot use those directly as they include the following privilege filter:
|
-- we cannot use those directly as they include the following privilege filter:
|
||||||
-- (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text));
|
-- (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text));
|
||||||
-- on the "columns" CTE, left joining on pg_depend and pg_class is used to obtain the sequence name as a column default in case there are GENERATED .. AS IDENTITY,
|
|
||||||
-- generated columns are only available from pg >= 10 but the query is agnostic to versions. dep.deptype = 'i' is done because there are other 'a' dependencies on PKs
|
|
||||||
[q|
|
[q|
|
||||||
WITH
|
WITH
|
||||||
columns AS (
|
columns AS (
|
||||||
@@ -516,8 +507,7 @@ tablesSqlQuery pgVer =
|
|||||||
c.relname::name AS table_name,
|
c.relname::name AS table_name,
|
||||||
a.attname::name AS column_name,
|
a.attname::name AS column_name,
|
||||||
d.description AS description,
|
d.description AS description,
|
||||||
|] <> columnDefault <>
|
pg_get_expr(ad.adbin, ad.adrelid)::text AS column_default,
|
||||||
[q|
|
|
||||||
not (a.attnotnull OR t.typtype = 'd' AND t.typnotnull) AS is_nullable,
|
not (a.attnotnull OR t.typtype = 'd' AND t.typnotnull) AS is_nullable,
|
||||||
CASE
|
CASE
|
||||||
WHEN t.typtype = 'd' THEN
|
WHEN t.typtype = 'd' THEN
|
||||||
@@ -531,7 +521,6 @@ tablesSqlQuery pgVer =
|
|||||||
ELSE format_type(a.atttypid, a.atttypmod)
|
ELSE format_type(a.atttypid, a.atttypmod)
|
||||||
END
|
END
|
||||||
END::text AS data_type,
|
END::text AS data_type,
|
||||||
t.oid AS data_type_id,
|
|
||||||
information_schema._pg_char_max_length(
|
information_schema._pg_char_max_length(
|
||||||
information_schema._pg_truetypid(a.*, t.*),
|
information_schema._pg_truetypid(a.*, t.*),
|
||||||
information_schema._pg_truetypmod(a.*, t.*)
|
information_schema._pg_truetypmod(a.*, t.*)
|
||||||
@@ -551,12 +540,6 @@ tablesSqlQuery pgVer =
|
|||||||
ON t.typtype = 'd' AND t.typbasetype = bt.oid
|
ON t.typtype = 'd' AND t.typbasetype = bt.oid
|
||||||
LEFT JOIN (pg_collation co JOIN pg_namespace nco ON co.collnamespace = nco.oid)
|
LEFT JOIN (pg_collation co JOIN pg_namespace nco ON co.collnamespace = nco.oid)
|
||||||
ON a.attcollation = co.oid AND (nco.nspname <> 'pg_catalog'::name OR co.collname <> 'default'::name)
|
ON a.attcollation = co.oid AND (nco.nspname <> 'pg_catalog'::name OR co.collname <> 'default'::name)
|
||||||
LEFT JOIN pg_depend dep
|
|
||||||
ON dep.refobjid = a.attrelid and dep.refobjsubid = a.attnum and dep.deptype = 'i'
|
|
||||||
LEFT JOIN pg_class seqclass
|
|
||||||
ON seqclass.oid = dep.objid
|
|
||||||
LEFT JOIN pg_namespace seqsch
|
|
||||||
ON seqsch.oid = seqclass.relnamespace
|
|
||||||
WHERE
|
WHERE
|
||||||
NOT pg_is_other_temp_schema(nc.oid)
|
NOT pg_is_other_temp_schema(nc.oid)
|
||||||
AND a.attnum > 0
|
AND a.attnum > 0
|
||||||
@@ -573,7 +556,6 @@ tablesSqlQuery pgVer =
|
|||||||
info.description,
|
info.description,
|
||||||
info.is_nullable::boolean,
|
info.is_nullable::boolean,
|
||||||
info.data_type,
|
info.data_type,
|
||||||
info.data_type_id::regtype::text,
|
|
||||||
info.character_maximum_length,
|
info.character_maximum_length,
|
||||||
info.column_default,
|
info.column_default,
|
||||||
coalesce(enum_info.vals, '{}')) order by info.position) as columns
|
coalesce(enum_info.vals, '{}')) order by info.position) as columns
|
||||||
@@ -705,19 +687,7 @@ tablesSqlQuery pgVer =
|
|||||||
"ORDER BY table_schema, table_name"
|
"ORDER BY table_schema, table_name"
|
||||||
where
|
where
|
||||||
relIsPartition = if pgVer >= pgVersion100 then " AND not c.relispartition " else mempty
|
relIsPartition = if pgVer >= pgVersion100 then " AND not c.relispartition " else mempty
|
||||||
columnDefault
|
|
||||||
| pgVer >= pgVersion120 = [q|
|
|
||||||
CASE
|
|
||||||
WHEN a.attidentity = 'd' THEN format('nextval(%s)', quote_literal(seqsch.nspname || '.' || seqclass.relname))
|
|
||||||
WHEN a.attgenerated = 's' THEN null
|
|
||||||
ELSE pg_get_expr(ad.adbin, ad.adrelid)::text
|
|
||||||
END AS column_default,|]
|
|
||||||
| pgVer >= pgVersion100 = [q|
|
|
||||||
CASE
|
|
||||||
WHEN a.attidentity = 'd' THEN format('nextval(%s)', quote_literal(seqsch.nspname || '.' || seqclass.relname))
|
|
||||||
ELSE pg_get_expr(ad.adbin, ad.adrelid)::text
|
|
||||||
END AS column_default,|]
|
|
||||||
| otherwise = "pg_get_expr(ad.adbin, ad.adrelid)::text as column_default,"
|
|
||||||
|
|
||||||
-- | Gets many-to-one relationships and one-to-one(O2O) relationships, which are a refinement of the many-to-one's
|
-- | Gets many-to-one relationships and one-to-one(O2O) relationships, which are a refinement of the many-to-one's
|
||||||
allM2OandO2ORels :: PgVersion -> Bool -> SQL.Statement () [Relationship]
|
allM2OandO2ORels :: PgVersion -> Bool -> SQL.Statement () [Relationship]
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
|
||||||
|
module PostgREST.SchemaCache.Proc
|
||||||
|
( PgType(..)
|
||||||
|
, ProcDescription(..)
|
||||||
|
, ProcParam(..)
|
||||||
|
, ProcVolatility(..)
|
||||||
|
, ProcsMap
|
||||||
|
, RetType(..)
|
||||||
|
, procReturnsScalar
|
||||||
|
, procReturnsSingle
|
||||||
|
, procReturnsVoid
|
||||||
|
, procTableName
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
import qualified Data.HashMap.Strict as HM
|
||||||
|
|
||||||
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||||
|
Schema, TableName)
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
data PgType
|
||||||
|
= Scalar
|
||||||
|
| Composite QualifiedIdentifier
|
||||||
|
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
data RetType
|
||||||
|
= Single PgType
|
||||||
|
| SetOf PgType
|
||||||
|
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
data ProcVolatility
|
||||||
|
= Volatile
|
||||||
|
| Stable
|
||||||
|
| Immutable
|
||||||
|
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
data ProcDescription = ProcDescription
|
||||||
|
{ pdSchema :: Schema
|
||||||
|
, pdName :: Text
|
||||||
|
, pdDescription :: Maybe Text
|
||||||
|
, pdParams :: [ProcParam]
|
||||||
|
, pdReturnType :: Maybe RetType
|
||||||
|
, pdVolatility :: ProcVolatility
|
||||||
|
, pdHasVariadic :: Bool
|
||||||
|
}
|
||||||
|
deriving (Eq, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
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
|
||||||
|
ProcDescription schema1 name1 des1 prms1 rt1 vol1 hasVar1 `compare` ProcDescription schema2 name2 des2 prms2 rt2 vol2 hasVar2
|
||||||
|
| schema1 == schema2 && name1 == name2 && length prms1 < length prms2 = LT
|
||||||
|
| schema2 == schema2 && name1 == name2 && length prms1 > length prms2 = GT
|
||||||
|
| 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).
|
||||||
|
-- | It uses a HashMap for a faster lookup.
|
||||||
|
type ProcsMap = HM.HashMap QualifiedIdentifier [ProcDescription]
|
||||||
|
|
||||||
|
procReturnsScalar :: ProcDescription -> Bool
|
||||||
|
procReturnsScalar proc = case proc of
|
||||||
|
ProcDescription{pdReturnType = Just (Single Scalar)} -> True
|
||||||
|
ProcDescription{pdReturnType = Just (SetOf Scalar)} -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
procReturnsSingle :: ProcDescription -> Bool
|
||||||
|
procReturnsSingle proc = case proc of
|
||||||
|
ProcDescription{pdReturnType = Just (Single _)} -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
procReturnsVoid :: ProcDescription -> Bool
|
||||||
|
procReturnsVoid proc = case proc of
|
||||||
|
ProcDescription{pdReturnType = Nothing} -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
procTableName :: ProcDescription -> Maybe TableName
|
||||||
|
procTableName proc = case pdReturnType proc of
|
||||||
|
Just (SetOf (Composite qi)) -> Just $ qiName qi
|
||||||
|
Just (Single (Composite qi)) -> Just $ qiName qi
|
||||||
|
_ -> Nothing
|
||||||
@@ -6,7 +6,6 @@ module PostgREST.SchemaCache.Relationship
|
|||||||
, Relationship(..)
|
, Relationship(..)
|
||||||
, Junction(..)
|
, Junction(..)
|
||||||
, RelationshipsMap
|
, RelationshipsMap
|
||||||
, relIsToOne
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
@@ -63,10 +62,3 @@ data Junction = Junction
|
|||||||
|
|
||||||
-- | Key based on the source table and the foreign table schema
|
-- | Key based on the source table and the foreign table schema
|
||||||
type RelationshipsMap = HM.HashMap (QualifiedIdentifier, Schema) [Relationship]
|
type RelationshipsMap = HM.HashMap (QualifiedIdentifier, Schema) [Relationship]
|
||||||
|
|
||||||
relIsToOne :: Relationship -> Bool
|
|
||||||
relIsToOne rel = case rel of
|
|
||||||
Relationship{relCardinality=M2O _ _} -> True
|
|
||||||
Relationship{relCardinality=O2O _ _} -> True
|
|
||||||
ComputedRelationship{relToOne=True} -> True
|
|
||||||
_ -> False
|
|
||||||
|
|||||||
@@ -1,104 +0,0 @@
|
|||||||
{-# LANGUAGE DeriveAnyClass #-}
|
|
||||||
{-# LANGUAGE DeriveGeneric #-}
|
|
||||||
|
|
||||||
module PostgREST.SchemaCache.Routine
|
|
||||||
( PgType(..)
|
|
||||||
, Routine(..)
|
|
||||||
, RoutineParam(..)
|
|
||||||
, FuncVolatility(..)
|
|
||||||
, RoutineMap
|
|
||||||
, RetType(..)
|
|
||||||
, funcReturnsScalar
|
|
||||||
, funcReturnsSetOfScalar
|
|
||||||
, funcReturnsSingleComposite
|
|
||||||
, funcReturnsVoid
|
|
||||||
, funcTableName
|
|
||||||
, funcReturnsCompositeAlias
|
|
||||||
) where
|
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
|
||||||
import qualified Data.HashMap.Strict as HM
|
|
||||||
|
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
|
||||||
Schema, TableName)
|
|
||||||
|
|
||||||
import Protolude
|
|
||||||
|
|
||||||
data PgType
|
|
||||||
= Scalar QualifiedIdentifier
|
|
||||||
| Composite QualifiedIdentifier Bool -- True if the composite is a domain alias(used to work around a bug in pg 11 and 12, see QueryBuilder.hs)
|
|
||||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
data RetType
|
|
||||||
= Single PgType
|
|
||||||
| SetOf PgType
|
|
||||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
data FuncVolatility
|
|
||||||
= Volatile
|
|
||||||
| Stable
|
|
||||||
| Immutable
|
|
||||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
data Routine = Function
|
|
||||||
{ pdSchema :: Schema
|
|
||||||
, pdName :: Text
|
|
||||||
, pdDescription :: Maybe Text
|
|
||||||
, pdParams :: [RoutineParam]
|
|
||||||
, pdReturnType :: RetType
|
|
||||||
, pdVolatility :: FuncVolatility
|
|
||||||
, pdHasVariadic :: Bool
|
|
||||||
, pdIsoLvl :: Maybe Text
|
|
||||||
}
|
|
||||||
deriving (Eq, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
data RoutineParam = RoutineParam
|
|
||||||
{ 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 Routine where
|
|
||||||
Function schema1 name1 des1 prms1 rt1 vol1 hasVar1 iso1 `compare` Function schema2 name2 des2 prms2 rt2 vol2 hasVar2 iso2
|
|
||||||
| schema1 == schema2 && name1 == name2 && length prms1 < length prms2 = LT
|
|
||||||
| schema2 == schema2 && name1 == name2 && length prms1 > length prms2 = GT
|
|
||||||
| otherwise = (schema1, name1, des1, prms1, rt1, vol1, hasVar1, iso1) `compare` (schema2, name2, des2, prms2, rt2, vol2, hasVar2, iso2)
|
|
||||||
|
|
||||||
-- | A map of all procs, all of which can be overloaded(one entry will have more than one Routine).
|
|
||||||
-- | It uses a HashMap for a faster lookup.
|
|
||||||
type RoutineMap = HM.HashMap QualifiedIdentifier [Routine]
|
|
||||||
|
|
||||||
funcReturnsScalar :: Routine -> Bool
|
|
||||||
funcReturnsScalar proc = case proc of
|
|
||||||
Function{pdReturnType = Single (Scalar{})} -> True
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
funcReturnsSetOfScalar :: Routine -> Bool
|
|
||||||
funcReturnsSetOfScalar proc = case proc of
|
|
||||||
Function{pdReturnType = SetOf (Scalar{})} -> True
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
funcReturnsCompositeAlias :: Routine -> Bool
|
|
||||||
funcReturnsCompositeAlias proc = case proc of
|
|
||||||
Function{pdReturnType = Single (Composite _ True)} -> True
|
|
||||||
Function{pdReturnType = SetOf (Composite _ True)} -> True
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
funcReturnsSingleComposite :: Routine -> Bool
|
|
||||||
funcReturnsSingleComposite proc = case proc of
|
|
||||||
Function{pdReturnType = Single (Composite _ _)} -> True
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
funcReturnsVoid :: Routine -> Bool
|
|
||||||
funcReturnsVoid proc = case proc of
|
|
||||||
Function{pdReturnType = Single (Scalar (QualifiedIdentifier "pg_catalog" "void"))} -> True
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
funcTableName :: Routine -> Maybe TableName
|
|
||||||
funcTableName proc = case pdReturnType proc of
|
|
||||||
SetOf (Composite qi _) -> Just $ qiName qi
|
|
||||||
Single (Composite qi _) -> Just $ qiName qi
|
|
||||||
_ -> Nothing
|
|
||||||
@@ -1,18 +1,14 @@
|
|||||||
{-# LANGUAGE DeriveAnyClass #-}
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
{-# LANGUAGE DeriveGeneric #-}
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
|
||||||
|
|
||||||
module PostgREST.SchemaCache.Table
|
module PostgREST.SchemaCache.Table
|
||||||
( Column(..)
|
( Column(..)
|
||||||
, Table(..)
|
, Table(..)
|
||||||
, tableColumnsList
|
|
||||||
, TablesMap
|
, TablesMap
|
||||||
, ColumnMap
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Data.HashMap.Strict.InsOrd as HMI
|
|
||||||
|
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
@@ -32,12 +28,9 @@ data Table = Table
|
|||||||
, tableUpdatable :: Bool
|
, tableUpdatable :: Bool
|
||||||
, tableDeletable :: Bool
|
, tableDeletable :: Bool
|
||||||
, tablePKCols :: [FieldName]
|
, tablePKCols :: [FieldName]
|
||||||
, tableColumns :: ColumnMap
|
, tableColumns :: [Column]
|
||||||
}
|
}
|
||||||
deriving (Show, Generic, JSON.ToJSON)
|
deriving (Show, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
tableColumnsList :: Table -> [Column]
|
|
||||||
tableColumnsList = HMI.elems . tableColumns
|
|
||||||
|
|
||||||
instance Eq Table where
|
instance Eq Table where
|
||||||
Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2
|
Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2
|
||||||
@@ -47,7 +40,6 @@ data Column = Column
|
|||||||
, colDescription :: Maybe Text
|
, colDescription :: Maybe Text
|
||||||
, colNullable :: Bool
|
, colNullable :: Bool
|
||||||
, colType :: Text
|
, colType :: Text
|
||||||
, colNominalType :: Text
|
|
||||||
, colMaxLen :: Maybe Int32
|
, colMaxLen :: Maybe Int32
|
||||||
, colDefault :: Maybe Text
|
, colDefault :: Maybe Text
|
||||||
, colEnum :: [Text]
|
, colEnum :: [Text]
|
||||||
@@ -55,4 +47,3 @@ data Column = Column
|
|||||||
deriving (Eq, Show, Ord, Generic, JSON.ToJSON)
|
deriving (Eq, Show, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
type TablesMap = HM.HashMap QualifiedIdentifier Table
|
type TablesMap = HM.HashMap QualifiedIdentifier Table
|
||||||
type ColumnMap = HMI.InsOrdHashMap FieldName Column
|
|
||||||
|
|||||||
+26
-25
@@ -9,8 +9,11 @@ module PostgREST.Workers
|
|||||||
, runAdmin
|
, runAdmin
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Hasql.Notifications as SQL
|
import qualified Hasql.Notifications as SQL
|
||||||
import qualified Hasql.Session as SQL
|
import qualified Hasql.Session as SQL
|
||||||
import qualified Hasql.Transaction.Sessions as SQL
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
@@ -27,10 +30,10 @@ import Network.Socket.ByteString
|
|||||||
|
|
||||||
import PostgREST.AppState (AppState)
|
import PostgREST.AppState (AppState)
|
||||||
import PostgREST.Config (AppConfig (..), readAppConfig)
|
import PostgREST.Config (AppConfig (..), readAppConfig)
|
||||||
import PostgREST.Config.Database (queryDbSettings, queryPgVersion,
|
import PostgREST.Config.Database (queryDbSettings, queryPgVersion)
|
||||||
queryRoleSettings)
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion (..), minimumPgVersion)
|
import PostgREST.Config.PgVersion (PgVersion (..), minimumPgVersion)
|
||||||
import PostgREST.Error (checkIsFatal)
|
import PostgREST.Error (PgError (PgError), checkIsFatal,
|
||||||
|
errorPayload)
|
||||||
import PostgREST.SchemaCache (querySchemaCache)
|
import PostgREST.SchemaCache (querySchemaCache)
|
||||||
|
|
||||||
import qualified PostgREST.AppState as AppState
|
import qualified PostgREST.AppState as AppState
|
||||||
@@ -125,11 +128,12 @@ establishConnection appState =
|
|||||||
|
|
||||||
getConnectionStatus :: IO ConnectionStatus
|
getConnectionStatus :: IO ConnectionStatus
|
||||||
getConnectionStatus = do
|
getConnectionStatus = do
|
||||||
pgVersion <- AppState.usePool appState $ queryPgVersion False -- No need to prepare the query here, as the connection might not be established
|
pgVersion <- AppState.usePool appState queryPgVersion
|
||||||
case pgVersion of
|
case pgVersion of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
AppState.logPgrstError appState e
|
let err = PgError False e
|
||||||
case checkIsFatal e of
|
AppState.logWithZTime appState . T.decodeUtf8 . LBS.toStrict $ errorPayload err
|
||||||
|
case checkIsFatal err of
|
||||||
Just reason ->
|
Just reason ->
|
||||||
return $ FatalConnectionError reason
|
return $ FatalConnectionError reason
|
||||||
Nothing ->
|
Nothing ->
|
||||||
@@ -164,20 +168,25 @@ loadSchemaCache appState = do
|
|||||||
querySchemaCache (toList configDbSchemas) configDbExtraSearchPath configDbPreparedStatements
|
querySchemaCache (toList configDbSchemas) configDbExtraSearchPath configDbPreparedStatements
|
||||||
case result of
|
case result of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
case checkIsFatal e of
|
let
|
||||||
|
err = PgError False e
|
||||||
|
putErr = AppState.logWithZTime appState . T.decodeUtf8 . LBS.toStrict $ errorPayload err
|
||||||
|
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"
|
||||||
AppState.logPgrstError appState e
|
putErr
|
||||||
AppState.logWithZTime appState hint
|
AppState.logWithZTime appState hint
|
||||||
return SCFatalFail
|
return SCFatalFail
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
AppState.putSchemaCache appState Nothing
|
AppState.putSchemaCache appState Nothing
|
||||||
AppState.logWithZTime appState "An error ocurred when loading the schema cache"
|
AppState.logWithZTime appState "An error ocurred when loading the schema cache"
|
||||||
AppState.logPgrstError appState e
|
putErr
|
||||||
return SCOnRetry
|
return SCOnRetry
|
||||||
|
|
||||||
Right sCache -> do
|
Right sCache -> do
|
||||||
AppState.putSchemaCache appState (Just sCache)
|
AppState.putSchemaCache appState (Just sCache)
|
||||||
|
when (isJust configDbRootSpec) .
|
||||||
|
AppState.putJsonDbS appState . LBS.toStrict $ JSON.encode sCache
|
||||||
AppState.logWithZTime appState "Schema cache loaded"
|
AppState.logWithZTime appState "Schema cache loaded"
|
||||||
return SCLoaded
|
return SCLoaded
|
||||||
|
|
||||||
@@ -240,31 +249,23 @@ reReadConfig startingUp appState = do
|
|||||||
qDbSettings <- AppState.usePool appState $ queryDbSettings configDbPreparedStatements
|
qDbSettings <- AppState.usePool appState $ queryDbSettings configDbPreparedStatements
|
||||||
case qDbSettings of
|
case qDbSettings of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
|
let
|
||||||
|
err = PgError False e
|
||||||
|
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 e of
|
case checkIsFatal err of
|
||||||
Just hint -> do
|
Just hint -> do
|
||||||
AppState.logPgrstError appState e
|
putErr
|
||||||
AppState.logWithZTime appState hint
|
AppState.logWithZTime appState hint
|
||||||
killThread (AppState.getMainThreadId appState)
|
killThread (AppState.getMainThreadId appState)
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
AppState.logPgrstError appState e
|
putErr
|
||||||
pure mempty
|
pure []
|
||||||
Right x -> pure x
|
Right x -> pure x
|
||||||
else
|
else
|
||||||
pure mempty
|
pure mempty
|
||||||
roleSettings <-
|
readAppConfig dbSettings configFilePath (Just configDbUri) >>= \case
|
||||||
if configDbConfig then do
|
|
||||||
rSettings <- AppState.usePool appState $ queryRoleSettings configDbPreparedStatements
|
|
||||||
case rSettings of
|
|
||||||
Left e -> do
|
|
||||||
AppState.logWithZTime appState "An error ocurred when trying to query the role settings"
|
|
||||||
AppState.logPgrstError appState e
|
|
||||||
pure mempty
|
|
||||||
Right x -> pure x
|
|
||||||
else
|
|
||||||
pure mempty
|
|
||||||
readAppConfig dbSettings configFilePath (Just configDbUri) roleSettings >>= \case
|
|
||||||
Left err ->
|
Left err ->
|
||||||
if startingUp then
|
if startingUp then
|
||||||
panic err -- die on invalid config if the program is starting up
|
panic err -- die on invalid config if the program is starting up
|
||||||
|
|||||||
+20
-4
@@ -1,4 +1,4 @@
|
|||||||
resolver: lts-20.6 # 2023-01-09, GHC 9.2.5
|
resolver: lts-19.14 # 2022-07-01, GHC 9.0.2
|
||||||
|
|
||||||
nix:
|
nix:
|
||||||
packages:
|
packages:
|
||||||
@@ -10,7 +10,23 @@ nix:
|
|||||||
pure: false
|
pure: false
|
||||||
|
|
||||||
extra-deps:
|
extra-deps:
|
||||||
|
- HTTP-4000.3.16
|
||||||
|
- configurator-pg-0.2.6
|
||||||
|
- hashable-1.4.1.0
|
||||||
|
- hashtables-1.3
|
||||||
|
- hasql-1.6.1.1
|
||||||
|
- hasql-dynamic-statements-0.3.1.2
|
||||||
|
- hasql-implicits-0.1.0.5
|
||||||
|
- hasql-notifications-0.2.0.3
|
||||||
|
- hasql-pool-0.8.0.6
|
||||||
|
- hasql-transaction-1.0.1.2
|
||||||
|
- isomorphism-class-0.1.0.6
|
||||||
|
- lens-aeson-1.1.3
|
||||||
|
- optparse-applicative-0.16.1.0
|
||||||
|
- postgresql-binary-0.12.5
|
||||||
|
- protolude-0.3.2
|
||||||
|
- ptr-0.16.8.2
|
||||||
|
- text-builder-0.6.7
|
||||||
|
- text-builder-dev-0.3.3
|
||||||
- git: https://github.com/PostgREST/postgresql-libpq.git
|
- git: https://github.com/PostgREST/postgresql-libpq.git
|
||||||
commit: 890a0a16cf57dd401420fdc6c7d576fb696003bc
|
commit: 33ff97db570b5b432255f5f24a68db51453f6eb8
|
||||||
- hasql-notifications-0.2.0.4
|
|
||||||
- hasql-pool-0.9
|
|
||||||
|
|||||||
+130
-18
@@ -5,33 +5,145 @@
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
- completed:
|
- completed:
|
||||||
commit: 890a0a16cf57dd401420fdc6c7d576fb696003bc
|
hackage: HTTP-4000.3.16@sha256:6042643c15a0b43e522a6693f1e322f05000d519543a84149cb80aeffee34f71,5947
|
||||||
git: https://github.com/PostgREST/postgresql-libpq.git
|
|
||||||
name: postgresql-libpq
|
|
||||||
pantry-tree:
|
pantry-tree:
|
||||||
sha256: 074668b9669b9c49f3c522c8af5c608799a1965e203c463b188b2632995beac2
|
size: 1428
|
||||||
size: 1414
|
sha256: b73a7f6d21cf20bbf819e19039409c9010efb5000d2b72cdd8fd67a9027c14e8
|
||||||
version: 0.9.4.3
|
|
||||||
original:
|
original:
|
||||||
commit: 890a0a16cf57dd401420fdc6c7d576fb696003bc
|
hackage: HTTP-4000.3.16
|
||||||
git: https://github.com/PostgREST/postgresql-libpq.git
|
|
||||||
- completed:
|
- completed:
|
||||||
hackage: hasql-notifications-0.2.0.4@sha256:9a09fa9b97feadd9492c8bd8bc6b9cffe0513510102f08374b0c45ecd479ed67,2028
|
hackage: configurator-pg-0.2.6@sha256:cd9b06a458428e493a4d6def725af7ab1ab0fef678fbd871f9586fc7f9aa70be,2849
|
||||||
|
pantry-tree:
|
||||||
|
size: 2463
|
||||||
|
sha256: 97efe7a22afc93033bda5adcffdabc0f1c30dc32b2c3ba02114ce7cd74c942fd
|
||||||
|
original:
|
||||||
|
hackage: configurator-pg-0.2.6
|
||||||
|
- completed:
|
||||||
|
hackage: hashable-1.4.1.0@sha256:50b2f002c68fe67730ee7a3cd8607486197dd99b084255005ad51ecd6970a41b,5019
|
||||||
|
pantry-tree:
|
||||||
|
size: 1248
|
||||||
|
sha256: 9af2f7a42674f7effcabbebc043f97057240783f1709338a77f58216f4a5f18c
|
||||||
|
original:
|
||||||
|
hackage: hashable-1.4.1.0
|
||||||
|
- completed:
|
||||||
|
hackage: hashtables-1.3@sha256:ab21804fdafbbd8ad918b2911dabb729ae0ea891780fe66bf7804cbcd07edadf,10379
|
||||||
|
pantry-tree:
|
||||||
|
size: 2895
|
||||||
|
sha256: e71f113ad989dbc994e0fb52bcc219d62930de9afa8b3441bf7909e864481b33
|
||||||
|
original:
|
||||||
|
hackage: hashtables-1.3
|
||||||
|
- completed:
|
||||||
|
hackage: hasql-1.6.1.1@sha256:948a2137308cc5354e4997bc3666753867124cd25db792424cb9614b1c1b44cf,6626
|
||||||
|
pantry-tree:
|
||||||
|
size: 2622
|
||||||
|
sha256: 28d21bf061522fc513f040e9c383b90532222b7258216cc094e07736add8be10
|
||||||
|
original:
|
||||||
|
hackage: hasql-1.6.1.1
|
||||||
|
- completed:
|
||||||
|
hackage: hasql-dynamic-statements-0.3.1.2@sha256:417aa533c84f074e2fa16bb2c4d4231326aa512097dd1025d915388e56acd1eb,2675
|
||||||
|
pantry-tree:
|
||||||
|
size: 595
|
||||||
|
sha256: 91696d3f3e0ef3254772ae5a8e4e89be68285febb49b302ed83d85ac4037a417
|
||||||
|
original:
|
||||||
|
hackage: hasql-dynamic-statements-0.3.1.2
|
||||||
|
- completed:
|
||||||
|
hackage: hasql-implicits-0.1.0.5@sha256:d16aacad6dc21428d72447d3ae8bcc03839a2f0aa1ec29c797ed9aca4609f9af,1361
|
||||||
|
pantry-tree:
|
||||||
|
size: 264
|
||||||
|
sha256: 0451b99a0a1d02db673d0c40acdf60d4e769e15852eed9e8dc05bffaf43efb70
|
||||||
|
original:
|
||||||
|
hackage: hasql-implicits-0.1.0.5
|
||||||
|
- completed:
|
||||||
|
hackage: hasql-notifications-0.2.0.3@sha256:aca3f7ee847a8f0b7ef6f989dc48f4a094a06c1a34e92aa3c8bb230085966ea6,2027
|
||||||
pantry-tree:
|
pantry-tree:
|
||||||
sha256: 56f9e240728e7a65711dde45fa2e2075b914e32cd370424aaa4572392378a60e
|
|
||||||
size: 452
|
size: 452
|
||||||
|
sha256: 999f0f2856a00d21f4498a8a58452bbefc4ea972fe2984fd234a68a5fe61d98b
|
||||||
original:
|
original:
|
||||||
hackage: hasql-notifications-0.2.0.4
|
hackage: hasql-notifications-0.2.0.3
|
||||||
- completed:
|
- completed:
|
||||||
hackage: hasql-pool-0.9@sha256:db7a37f6b3a922c37adc3c7ced47a7c10786d1f171e47a735a6e812a587ba44c,2111
|
hackage: hasql-pool-0.8.0.6@sha256:b63bb83409bab5bc20ff24f5d62205e9b117701a0fc24531ddeac20ab8c2a42c,1818
|
||||||
pantry-tree:
|
pantry-tree:
|
||||||
sha256: 49b1181d28c6f5317e794671c2dae155754b834bdcfa30f7e5dbad28e4cf0249
|
|
||||||
size: 346
|
size: 346
|
||||||
|
sha256: c4100946b7eae44375511e35a393abe2e1db0e5637c68cea8f53176b796bfd5b
|
||||||
original:
|
original:
|
||||||
hackage: hasql-pool-0.9
|
hackage: hasql-pool-0.8.0.6
|
||||||
|
- completed:
|
||||||
|
hackage: hasql-transaction-1.0.1.2@sha256:297b158cd1f0727f9b0e175bd7d3741c1bcb725a8094956d0ee79b41aafdb30a,2890
|
||||||
|
pantry-tree:
|
||||||
|
size: 983
|
||||||
|
sha256: 3679e6d5c835cc17a8fa0c252b8221e282880044b7219aa1de2531bbd5c40691
|
||||||
|
original:
|
||||||
|
hackage: hasql-transaction-1.0.1.2
|
||||||
|
- completed:
|
||||||
|
hackage: isomorphism-class-0.1.0.6@sha256:d93da31287359c761953b876354de28381f409c5c50e3241c572a443e50c553d,1703
|
||||||
|
pantry-tree:
|
||||||
|
size: 465
|
||||||
|
sha256: c97f922d1ae8f1a0db4c28fac9383d2716934879e95ff0b2b88ebb861d6fba14
|
||||||
|
original:
|
||||||
|
hackage: isomorphism-class-0.1.0.6
|
||||||
|
- completed:
|
||||||
|
hackage: lens-aeson-1.1.3@sha256:52c8eaecd2d1c2a969c0762277c4a8ee72c339a686727d5785932e72ef9c3050,1764
|
||||||
|
pantry-tree:
|
||||||
|
size: 541
|
||||||
|
sha256: b31392b78f2a03111c805f4400007778eb93b49f998ab41dfbebaaf9b5526bad
|
||||||
|
original:
|
||||||
|
hackage: lens-aeson-1.1.3
|
||||||
|
- completed:
|
||||||
|
hackage: optparse-applicative-0.16.1.0@sha256:418c22ed6a19124d457d96bc66bd22c93ac22fad0c7100fe4972bbb4ac989731,4982
|
||||||
|
pantry-tree:
|
||||||
|
size: 2979
|
||||||
|
sha256: dd092d843091c08691485d68a1908517079b1bc6f3d73928f37635a19dc27fc1
|
||||||
|
original:
|
||||||
|
hackage: optparse-applicative-0.16.1.0
|
||||||
|
- completed:
|
||||||
|
hackage: postgresql-binary-0.12.5@sha256:de9da3cba9be541d6c75ae8da2858c33d83dc1b2e0c639b0b9781816b78a91f4,5594
|
||||||
|
pantry-tree:
|
||||||
|
size: 1619
|
||||||
|
sha256: b392337f91031a5b3407393e2f04dfe4e7a28019e88eae6a9370538b90e28c51
|
||||||
|
original:
|
||||||
|
hackage: postgresql-binary-0.12.5
|
||||||
|
- completed:
|
||||||
|
hackage: protolude-0.3.2@sha256:2a38b3dad40d238ab644e234b692c8911423f9d3ed0e36b62287c4a698d92cd1,2240
|
||||||
|
pantry-tree:
|
||||||
|
size: 1594
|
||||||
|
sha256: a36d2912ac552d950ba4476de7d950b56b82dd28e48b9f4d0efee938f10bc525
|
||||||
|
original:
|
||||||
|
hackage: protolude-0.3.2
|
||||||
|
- completed:
|
||||||
|
hackage: ptr-0.16.8.2@sha256:708ebb95117f2872d2c5a554eb6804cf1126e86abe793b2673f913f14e5eb1ac,3959
|
||||||
|
pantry-tree:
|
||||||
|
size: 1303
|
||||||
|
sha256: 557c438345de19f82bf01d676100da2a191ef06f624e7a4b90b09ac17cbb52a5
|
||||||
|
original:
|
||||||
|
hackage: ptr-0.16.8.2
|
||||||
|
- completed:
|
||||||
|
hackage: text-builder-0.6.7@sha256:efbb3e06107e9c8d1cfe85c963938ca9f375a74379af03da3173be4ef5c37bcf,2364
|
||||||
|
pantry-tree:
|
||||||
|
size: 425
|
||||||
|
sha256: cd0ae197e6f9f3860a8ab71f5b87c4a8452ed1fce2fdfd35e36d68ded6e6648e
|
||||||
|
original:
|
||||||
|
hackage: text-builder-0.6.7
|
||||||
|
- completed:
|
||||||
|
hackage: text-builder-dev-0.3.3@sha256:79ec422defcc2e5b34f94129c72b98d34b2efc1ed8bbd945ccb8f4f535a892c3,2784
|
||||||
|
pantry-tree:
|
||||||
|
size: 724
|
||||||
|
sha256: 8883631a132438e7892fcb13e89d6bbcdc0ac76c56fbea8df8d7aa482ce81f73
|
||||||
|
original:
|
||||||
|
hackage: text-builder-dev-0.3.3
|
||||||
|
- completed:
|
||||||
|
name: postgresql-libpq
|
||||||
|
version: 0.9.4.3
|
||||||
|
git: https://github.com/PostgREST/postgresql-libpq.git
|
||||||
|
pantry-tree:
|
||||||
|
size: 1081
|
||||||
|
sha256: 0df271e48af32eb8292a45301af45e114110d54099ee73dbc609d39770e8175e
|
||||||
|
commit: 33ff97db570b5b432255f5f24a68db51453f6eb8
|
||||||
|
original:
|
||||||
|
git: https://github.com/PostgREST/postgresql-libpq.git
|
||||||
|
commit: 33ff97db570b5b432255f5f24a68db51453f6eb8
|
||||||
snapshots:
|
snapshots:
|
||||||
- completed:
|
- completed:
|
||||||
sha256: 4905c93319aa94aa53da8f41d614d7bacdbfe6c63a8c6132d32e6e62f24a9af4
|
size: 618951
|
||||||
size: 649315
|
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/19/14.yaml
|
||||||
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/20/6.yaml
|
sha256: 4c31d4ef975b0211078862566aedf3b82b6cea569fc2cde4c72a51e5a8d236ce
|
||||||
original: lts-20.6
|
original: lts-19.14
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ db-extra-search-path = "public"
|
|||||||
db-max-rows = 1000
|
db-max-rows = 1000
|
||||||
db-plan-enabled = false
|
db-plan-enabled = false
|
||||||
db-pool = 10
|
db-pool = 10
|
||||||
db-pool-acquisition-timeout = 10
|
db-pool-acquisition-timeout = ""
|
||||||
db-pool-max-lifetime = 1800
|
|
||||||
db-pre-request = "check_alias"
|
db-pre-request = "check_alias"
|
||||||
db-prepared-statements = true
|
db-prepared-statements = true
|
||||||
db-root-spec = "open_alias"
|
db-root-spec = "open_alias"
|
||||||
@@ -26,7 +25,6 @@ openapi-server-proxy-uri = ""
|
|||||||
raw-media-types = ""
|
raw-media-types = ""
|
||||||
server-host = "!4"
|
server-host = "!4"
|
||||||
server-port = 3000
|
server-port = 3000
|
||||||
server-trace-header = ""
|
|
||||||
server-unix-socket = ""
|
server-unix-socket = ""
|
||||||
server-unix-socket-mode = "660"
|
server-unix-socket-mode = "660"
|
||||||
admin-server-port = ""
|
admin-server-port = ""
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ db-extra-search-path = "public"
|
|||||||
db-max-rows = ""
|
db-max-rows = ""
|
||||||
db-plan-enabled = false
|
db-plan-enabled = false
|
||||||
db-pool = 10
|
db-pool = 10
|
||||||
db-pool-acquisition-timeout = 10
|
db-pool-acquisition-timeout = ""
|
||||||
db-pool-max-lifetime = 1800
|
|
||||||
db-pre-request = ""
|
db-pre-request = ""
|
||||||
db-prepared-statements = false
|
db-prepared-statements = false
|
||||||
db-root-spec = ""
|
db-root-spec = ""
|
||||||
@@ -26,7 +25,6 @@ openapi-server-proxy-uri = ""
|
|||||||
raw-media-types = ""
|
raw-media-types = ""
|
||||||
server-host = "!4"
|
server-host = "!4"
|
||||||
server-port = 3000
|
server-port = 3000
|
||||||
server-trace-header = ""
|
|
||||||
server-unix-socket = ""
|
server-unix-socket = ""
|
||||||
server-unix-socket-mode = "660"
|
server-unix-socket-mode = "660"
|
||||||
admin-server-port = ""
|
admin-server-port = ""
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ db-extra-search-path = "public"
|
|||||||
db-max-rows = ""
|
db-max-rows = ""
|
||||||
db-plan-enabled = false
|
db-plan-enabled = false
|
||||||
db-pool = 10
|
db-pool = 10
|
||||||
db-pool-acquisition-timeout = 10
|
db-pool-acquisition-timeout = ""
|
||||||
db-pool-max-lifetime = 1800
|
|
||||||
db-pre-request = ""
|
db-pre-request = ""
|
||||||
db-prepared-statements = false
|
db-prepared-statements = false
|
||||||
db-root-spec = ""
|
db-root-spec = ""
|
||||||
@@ -26,7 +25,6 @@ openapi-server-proxy-uri = ""
|
|||||||
raw-media-types = ""
|
raw-media-types = ""
|
||||||
server-host = "!4"
|
server-host = "!4"
|
||||||
server-port = 3000
|
server-port = 3000
|
||||||
server-trace-header = ""
|
|
||||||
server-unix-socket = ""
|
server-unix-socket = ""
|
||||||
server-unix-socket-mode = "660"
|
server-unix-socket-mode = "660"
|
||||||
admin-server-port = ""
|
admin-server-port = ""
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ db-extra-search-path = "public"
|
|||||||
db-max-rows = ""
|
db-max-rows = ""
|
||||||
db-plan-enabled = false
|
db-plan-enabled = false
|
||||||
db-pool = 10
|
db-pool = 10
|
||||||
db-pool-acquisition-timeout = 10
|
db-pool-acquisition-timeout = ""
|
||||||
db-pool-max-lifetime = 1800
|
|
||||||
db-pre-request = ""
|
db-pre-request = ""
|
||||||
db-prepared-statements = true
|
db-prepared-statements = true
|
||||||
db-root-spec = ""
|
db-root-spec = ""
|
||||||
@@ -26,7 +25,6 @@ openapi-server-proxy-uri = ""
|
|||||||
raw-media-types = ""
|
raw-media-types = ""
|
||||||
server-host = "!4"
|
server-host = "!4"
|
||||||
server-port = 3000
|
server-port = 3000
|
||||||
server-trace-header = ""
|
|
||||||
server-unix-socket = ""
|
server-unix-socket = ""
|
||||||
server-unix-socket-mode = "660"
|
server-unix-socket-mode = "660"
|
||||||
admin-server-port = ""
|
admin-server-port = ""
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ db-extra-search-path = "public,extensions,other"
|
|||||||
db-max-rows = 100
|
db-max-rows = 100
|
||||||
db-plan-enabled = true
|
db-plan-enabled = true
|
||||||
db-pool = 1
|
db-pool = 1
|
||||||
db-pool-acquisition-timeout = 30
|
db-pool-acquisition-timeout = 10
|
||||||
db-pool-max-lifetime = 3600
|
|
||||||
db-pre-request = "test.other_custom_headers"
|
db-pre-request = "test.other_custom_headers"
|
||||||
db-prepared-statements = false
|
db-prepared-statements = false
|
||||||
db-root-spec = "other_root"
|
db-root-spec = "other_root"
|
||||||
@@ -26,7 +25,6 @@ openapi-server-proxy-uri = "https://otherexample.org/api"
|
|||||||
raw-media-types = "application/vnd.pgrst.other-db-config"
|
raw-media-types = "application/vnd.pgrst.other-db-config"
|
||||||
server-host = "0.0.0.0"
|
server-host = "0.0.0.0"
|
||||||
server-port = 80
|
server-port = 80
|
||||||
server-trace-header = "traceparent"
|
|
||||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||||
server-unix-socket-mode = "777"
|
server-unix-socket-mode = "777"
|
||||||
admin-server-port = 3001
|
admin-server-port = 3001
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ db-extra-search-path = "public,extensions,private"
|
|||||||
db-max-rows = 1000
|
db-max-rows = 1000
|
||||||
db-plan-enabled = true
|
db-plan-enabled = true
|
||||||
db-pool = 1
|
db-pool = 1
|
||||||
db-pool-acquisition-timeout = 30
|
db-pool-acquisition-timeout = 10
|
||||||
db-pool-max-lifetime = 3600
|
|
||||||
db-pre-request = "test.custom_headers"
|
db-pre-request = "test.custom_headers"
|
||||||
db-prepared-statements = false
|
db-prepared-statements = false
|
||||||
db-root-spec = "root"
|
db-root-spec = "root"
|
||||||
@@ -26,7 +25,6 @@ openapi-server-proxy-uri = "https://example.org/api"
|
|||||||
raw-media-types = "application/vnd.pgrst.db-config"
|
raw-media-types = "application/vnd.pgrst.db-config"
|
||||||
server-host = "0.0.0.0"
|
server-host = "0.0.0.0"
|
||||||
server-port = 80
|
server-port = 80
|
||||||
server-trace-header = "CF-Ray"
|
|
||||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||||
server-unix-socket-mode = "777"
|
server-unix-socket-mode = "777"
|
||||||
admin-server-port = 3001
|
admin-server-port = 3001
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ db-extra-search-path = "public,test"
|
|||||||
db-max-rows = 1000
|
db-max-rows = 1000
|
||||||
db-plan-enabled = true
|
db-plan-enabled = true
|
||||||
db-pool = 1
|
db-pool = 1
|
||||||
db-pool-acquisition-timeout = 30
|
db-pool-acquisition-timeout = 10
|
||||||
db-pool-max-lifetime = 3600
|
|
||||||
db-pre-request = "please_run_fast"
|
db-pre-request = "please_run_fast"
|
||||||
db-prepared-statements = false
|
db-prepared-statements = false
|
||||||
db-root-spec = "openapi_v3"
|
db-root-spec = "openapi_v3"
|
||||||
@@ -26,7 +25,6 @@ openapi-server-proxy-uri = "https://postgrest.org"
|
|||||||
raw-media-types = "application/vnd.pgrst.config"
|
raw-media-types = "application/vnd.pgrst.config"
|
||||||
server-host = "0.0.0.0"
|
server-host = "0.0.0.0"
|
||||||
server-port = 80
|
server-port = 80
|
||||||
server-trace-header = "X-Request-Id"
|
|
||||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||||
server-unix-socket-mode = "777"
|
server-unix-socket-mode = "777"
|
||||||
admin-server-port = 3001
|
admin-server-port = 3001
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ db-extra-search-path = "public"
|
|||||||
db-max-rows = ""
|
db-max-rows = ""
|
||||||
db-plan-enabled = false
|
db-plan-enabled = false
|
||||||
db-pool = 10
|
db-pool = 10
|
||||||
db-pool-acquisition-timeout = 10
|
db-pool-acquisition-timeout = ""
|
||||||
db-pool-max-lifetime = 1800
|
|
||||||
db-pre-request = ""
|
db-pre-request = ""
|
||||||
db-prepared-statements = true
|
db-prepared-statements = true
|
||||||
db-root-spec = ""
|
db-root-spec = ""
|
||||||
@@ -26,7 +25,6 @@ openapi-server-proxy-uri = ""
|
|||||||
raw-media-types = ""
|
raw-media-types = ""
|
||||||
server-host = "!4"
|
server-host = "!4"
|
||||||
server-port = 3000
|
server-port = 3000
|
||||||
server-trace-header = ""
|
|
||||||
server-unix-socket = ""
|
server-unix-socket = ""
|
||||||
server-unix-socket-mode = "660"
|
server-unix-socket-mode = "660"
|
||||||
admin-server-port = ""
|
admin-server-port = ""
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ PGRST_DB_EXTRA_SEARCH_PATH: public, test
|
|||||||
PGRST_DB_MAX_ROWS: 1000
|
PGRST_DB_MAX_ROWS: 1000
|
||||||
PGRST_DB_PLAN_ENABLED: true
|
PGRST_DB_PLAN_ENABLED: true
|
||||||
PGRST_DB_POOL: 1
|
PGRST_DB_POOL: 1
|
||||||
PGRST_DB_POOL_ACQUISITION_TIMEOUT: 30
|
PGRST_DB_POOL_ACQUISITION_TIMEOUT: 10
|
||||||
PGRST_DB_POOL_MAX_LIFETIME: 3600
|
|
||||||
PGRST_DB_PREPARED_STATEMENTS: false
|
PGRST_DB_PREPARED_STATEMENTS: false
|
||||||
PGRST_DB_PRE_REQUEST: please_run_fast
|
PGRST_DB_PRE_REQUEST: please_run_fast
|
||||||
PGRST_DB_ROOT_SPEC: openapi_v3
|
PGRST_DB_ROOT_SPEC: openapi_v3
|
||||||
@@ -29,7 +28,6 @@ PGRST_OPENAPI_SERVER_PROXY_URI: 'https://postgrest.org'
|
|||||||
PGRST_RAW_MEDIA_TYPES: application/vnd.pgrst.config
|
PGRST_RAW_MEDIA_TYPES: application/vnd.pgrst.config
|
||||||
PGRST_SERVER_HOST: 0.0.0.0
|
PGRST_SERVER_HOST: 0.0.0.0
|
||||||
PGRST_SERVER_PORT: 80
|
PGRST_SERVER_PORT: 80
|
||||||
PGRST_SERVER_TRACE_HEADER: X-Request-Id
|
|
||||||
PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock
|
PGRST_SERVER_UNIX_SOCKET: /tmp/pgrst_io_test.sock
|
||||||
PGRST_SERVER_UNIX_SOCKET_MODE: 777
|
PGRST_SERVER_UNIX_SOCKET_MODE: 777
|
||||||
PGRST_ADMIN_SERVER_PORT: 3001
|
PGRST_ADMIN_SERVER_PORT: 3001
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ db-extra-search-path = "public, test"
|
|||||||
db-max-rows = 1000
|
db-max-rows = 1000
|
||||||
db-plan-enabled = true
|
db-plan-enabled = true
|
||||||
db-pool = 1
|
db-pool = 1
|
||||||
db-pool-acquisition-timeout = 30
|
db-pool-acquisition-timeout = 10
|
||||||
db-pool-max-lifetime = 3600
|
|
||||||
db-pre-request = "please_run_fast"
|
db-pre-request = "please_run_fast"
|
||||||
db-prepared-statements = false
|
db-prepared-statements = false
|
||||||
db-root-spec = "openapi_v3"
|
db-root-spec = "openapi_v3"
|
||||||
@@ -26,7 +25,6 @@ openapi-server-proxy-uri = "https://postgrest.org"
|
|||||||
raw-media-types = "application/vnd.pgrst.config"
|
raw-media-types = "application/vnd.pgrst.config"
|
||||||
server-host = "0.0.0.0"
|
server-host = "0.0.0.0"
|
||||||
server-port = 80
|
server-port = 80
|
||||||
server-trace-header = "X-Request-Id"
|
|
||||||
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
server-unix-socket = "/tmp/pgrst_io_test.sock"
|
||||||
server-unix-socket-mode = "777"
|
server-unix-socket-mode = "777"
|
||||||
admin-server-port = 3001
|
admin-server-port = 3001
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ ALTER ROLE db_config_authenticator SET pgrst.db_pre_request = 'test.custom_heade
|
|||||||
ALTER ROLE db_config_authenticator SET pgrst.db_max_rows = '1000';
|
ALTER ROLE db_config_authenticator SET pgrst.db_max_rows = '1000';
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.db_extra_search_path = 'public, extensions';
|
ALTER ROLE db_config_authenticator SET pgrst.db_extra_search_path = 'public, extensions';
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.not_existing = 'should be ignored';
|
ALTER ROLE db_config_authenticator SET pgrst.not_existing = 'should be ignored';
|
||||||
ALTER ROLE db_config_authenticator SET pgrst.server_trace_header = 'CF-Ray';
|
|
||||||
|
|
||||||
-- override with database specific setting
|
-- override with database specific setting
|
||||||
ALTER ROLE db_config_authenticator IN DATABASE :DBNAME SET pgrst.jwt_secret = 'OVERRIDE=REALLY=REALLY=REALLY=REALLY=VERY=SAFE';
|
ALTER ROLE db_config_authenticator IN DATABASE :DBNAME SET pgrst.jwt_secret = 'OVERRIDE=REALLY=REALLY=REALLY=REALLY=VERY=SAFE';
|
||||||
@@ -61,7 +60,6 @@ ALTER ROLE other_authenticator SET pgrst.db_max_rows = '100';
|
|||||||
ALTER ROLE other_authenticator SET pgrst.db_extra_search_path = 'public, extensions, other';
|
ALTER ROLE other_authenticator SET pgrst.db_extra_search_path = 'public, extensions, other';
|
||||||
ALTER ROLE other_authenticator SET pgrst.openapi_mode = 'disabled';
|
ALTER ROLE other_authenticator SET pgrst.openapi_mode = 'disabled';
|
||||||
ALTER ROLE other_authenticator SET pgrst.openapi_security_active = 'false';
|
ALTER ROLE other_authenticator SET pgrst.openapi_security_active = 'false';
|
||||||
ALTER ROLE other_authenticator SET pgrst.server_trace_header = 'traceparent';
|
|
||||||
|
|
||||||
-- authenticator used for tests that manipulate statement timeout
|
-- authenticator used for tests that manipulate statement timeout
|
||||||
CREATE ROLE timeout_authenticator LOGIN NOINHERIT;
|
CREATE ROLE timeout_authenticator LOGIN NOINHERIT;
|
||||||
|
|||||||
+2
-46
@@ -4,17 +4,11 @@
|
|||||||
set search_path to public;
|
set search_path to public;
|
||||||
|
|
||||||
CREATE ROLE postgrest_test_anonymous;
|
CREATE ROLE postgrest_test_anonymous;
|
||||||
ALTER ROLE :PGUSER SET pgrst.db_anon_role = 'postgrest_test_anonymous';
|
ALTER ROLE :USER SET pgrst.db_anon_role = 'postgrest_test_anonymous';
|
||||||
|
|
||||||
CREATE ROLE postgrest_test_author;
|
CREATE ROLE postgrest_test_author;
|
||||||
|
|
||||||
CREATE ROLE postgrest_test_serializable;
|
GRANT postgrest_test_anonymous, postgrest_test_author TO :USER;
|
||||||
alter role postgrest_test_serializable set default_transaction_isolation = 'serializable';
|
|
||||||
|
|
||||||
CREATE ROLE postgrest_test_repeatable_read;
|
|
||||||
alter role postgrest_test_repeatable_read set default_transaction_isolation = 'REPEATABLE READ';
|
|
||||||
|
|
||||||
GRANT postgrest_test_anonymous, postgrest_test_author, postgrest_test_serializable, postgrest_test_repeatable_read TO :PGUSER;
|
|
||||||
|
|
||||||
CREATE SCHEMA v1;
|
CREATE SCHEMA v1;
|
||||||
GRANT USAGE ON SCHEMA v1 TO postgrest_test_anonymous;
|
GRANT USAGE ON SCHEMA v1 TO postgrest_test_anonymous;
|
||||||
@@ -104,41 +98,3 @@ as $$
|
|||||||
grant all on table cats to postgrest_test_anonymous;
|
grant all on table cats to postgrest_test_anonymous;
|
||||||
notify pgrst, 'reload schema';
|
notify pgrst, 'reload schema';
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
alter role postgrest_test_anonymous set statement_timeout to '2s';
|
|
||||||
alter role postgrest_test_author set statement_timeout to '10s';
|
|
||||||
|
|
||||||
create function change_role_statement_timeout(timeout text) returns void as $_$
|
|
||||||
begin
|
|
||||||
execute format($$
|
|
||||||
alter role current_user set statement_timeout = %L;
|
|
||||||
$$, timeout);
|
|
||||||
end $_$ volatile language plpgsql ;
|
|
||||||
|
|
||||||
create table items as select x as id from generate_series(1,5) x;
|
|
||||||
|
|
||||||
create view items_w_isolation_level as
|
|
||||||
select
|
|
||||||
id,
|
|
||||||
current_setting('transaction_isolation', true) as isolation_level
|
|
||||||
from items;
|
|
||||||
|
|
||||||
grant all on items_w_isolation_level to postgrest_test_anonymous, postgrest_test_repeatable_read, postgrest_test_serializable;
|
|
||||||
|
|
||||||
create function default_isolation_level()
|
|
||||||
returns text as $$
|
|
||||||
select current_setting('transaction_isolation', true);
|
|
||||||
$$
|
|
||||||
language sql;
|
|
||||||
|
|
||||||
create function serializable_isolation_level()
|
|
||||||
returns text as $$
|
|
||||||
select current_setting('transaction_isolation', true);
|
|
||||||
$$
|
|
||||||
language sql set default_transaction_isolation = 'serializable';
|
|
||||||
|
|
||||||
create function repeatable_read_isolation_level()
|
|
||||||
returns text as $$
|
|
||||||
select current_setting('transaction_isolation', true);
|
|
||||||
$$
|
|
||||||
language sql set default_transaction_isolation = 'REPEATABLE READ';
|
|
||||||
|
|||||||
+1
-13
@@ -42,18 +42,6 @@ class PostgrestProcess:
|
|||||||
process: object
|
process: object
|
||||||
session: object
|
session: object
|
||||||
|
|
||||||
def read_stdout(self, nlines=1):
|
|
||||||
"Wait for line(s) on standard output."
|
|
||||||
output = []
|
|
||||||
for _ in range(10):
|
|
||||||
l = self.process.stdout.readline()
|
|
||||||
if l:
|
|
||||||
output.append(l.decode())
|
|
||||||
if len(output) >= nlines:
|
|
||||||
break
|
|
||||||
time.sleep(0.1)
|
|
||||||
return output
|
|
||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def run(
|
def run(
|
||||||
@@ -155,7 +143,7 @@ def wait_until_exit(postgrest):
|
|||||||
"Wait for PostgREST to exit, or times out"
|
"Wait for PostgREST to exit, or times out"
|
||||||
try:
|
try:
|
||||||
return postgrest.process.wait(timeout=1)
|
return postgrest.process.wait(timeout=1)
|
||||||
except subprocess.TimeoutExpired:
|
except (subprocess.TimeoutExpired):
|
||||||
raise PostgrestTimedOut()
|
raise PostgrestTimedOut()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+16
-114
@@ -416,8 +416,14 @@ def test_invalid_role_claim_key_notify_reload(defaultenv):
|
|||||||
with run(env=env) as postgrest:
|
with run(env=env) as postgrest:
|
||||||
postgrest.session.post("/rpc/invalid_role_claim_key_reload")
|
postgrest.session.post("/rpc/invalid_role_claim_key_reload")
|
||||||
|
|
||||||
output = postgrest.read_stdout()
|
output = None
|
||||||
assert "failed to parse role-claim-key value" in output[0]
|
for _ in range(10):
|
||||||
|
output = postgrest.process.stdout.readline()
|
||||||
|
if output:
|
||||||
|
break
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
assert "failed to parse role-claim-key value" in output.decode()
|
||||||
|
|
||||||
response = postgrest.session.post("/rpc/reset_invalid_role_claim_key")
|
response = postgrest.session.post("/rpc/reset_invalid_role_claim_key")
|
||||||
assert response.status_code == 204
|
assert response.status_code == 204
|
||||||
@@ -529,6 +535,7 @@ def test_pool_size(defaultenv, metapostgrest):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with run(env=env) as postgrest:
|
with run(env=env) as postgrest:
|
||||||
|
|
||||||
start = time.time()
|
start = time.time()
|
||||||
threads = []
|
threads = []
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
@@ -565,11 +572,6 @@ def test_pool_acquisition_timeout(defaultenv, metapostgrest):
|
|||||||
data = response.json()
|
data = response.json()
|
||||||
assert data["message"] == "Timed out acquiring connection from connection pool."
|
assert data["message"] == "Timed out acquiring connection from connection pool."
|
||||||
|
|
||||||
# ensure the message appears on the logs as well
|
|
||||||
output = sorted(postgrest.read_stdout(nlines=2))
|
|
||||||
assert " 504 " in output[0]
|
|
||||||
assert "Timed out acquiring connection from connection pool." in output[1]
|
|
||||||
|
|
||||||
|
|
||||||
def test_change_statement_timeout_held_connection(defaultenv, metapostgrest):
|
def test_change_statement_timeout_held_connection(defaultenv, metapostgrest):
|
||||||
"Statement timeout changes take effect immediately, even with a request outliving the reconfiguration"
|
"Statement timeout changes take effect immediately, even with a request outliving the reconfiguration"
|
||||||
@@ -660,6 +662,7 @@ def test_admin_ready_includes_schema_cache_state(defaultenv, metapostgrest):
|
|||||||
}
|
}
|
||||||
|
|
||||||
with run(env=env) as postgrest:
|
with run(env=env) as postgrest:
|
||||||
|
|
||||||
# make it impossible to load the schema cache, by setting statement timeout to 1ms
|
# make it impossible to load the schema cache, by setting statement timeout to 1ms
|
||||||
set_statement_timeout(metapostgrest, role, 1)
|
set_statement_timeout(metapostgrest, role, 1)
|
||||||
|
|
||||||
@@ -715,6 +718,7 @@ def test_admin_works_with_host_special_values(specialhostvalue, defaultenv):
|
|||||||
"Should get a success from the admin live and ready endpoints when using special host values for the main app"
|
"Should get a success from the admin live and ready endpoints when using special host values for the main app"
|
||||||
|
|
||||||
with run(env=defaultenv, port=freeport(), host=specialhostvalue) as postgrest:
|
with run(env=defaultenv, port=freeport(), host=specialhostvalue) as postgrest:
|
||||||
|
|
||||||
response = postgrest.admin.get("/live")
|
response = postgrest.admin.get("/live")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
@@ -770,6 +774,7 @@ def test_no_pool_connection_required_on_bad_http_logic(defaultenv):
|
|||||||
"no pool connection should be consumed for failing on invalid http logic"
|
"no pool connection should be consumed for failing on invalid http logic"
|
||||||
|
|
||||||
with run(env=defaultenv, no_pool_connection_available=True) as postgrest:
|
with run(env=defaultenv, no_pool_connection_available=True) as postgrest:
|
||||||
|
|
||||||
# not found nested route shouldn't require opening a connection
|
# not found nested route shouldn't require opening a connection
|
||||||
response = postgrest.session.head("/path/notfound")
|
response = postgrest.session.head("/path/notfound")
|
||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
@@ -785,6 +790,7 @@ def test_no_pool_connection_required_on_options(defaultenv):
|
|||||||
"no pool connection should be consumed for OPTIONS requests"
|
"no pool connection should be consumed for OPTIONS requests"
|
||||||
|
|
||||||
with run(env=defaultenv, no_pool_connection_available=True) as postgrest:
|
with run(env=defaultenv, no_pool_connection_available=True) as postgrest:
|
||||||
|
|
||||||
# OPTIONS on a table shouldn't require opening a connection
|
# OPTIONS on a table shouldn't require opening a connection
|
||||||
response = postgrest.session.options("/projects")
|
response = postgrest.session.options("/projects")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
@@ -804,6 +810,7 @@ def test_no_pool_connection_required_on_bad_jwt_claim(defaultenv):
|
|||||||
env = {**defaultenv, "PGRST_JWT_SECRET": SECRET}
|
env = {**defaultenv, "PGRST_JWT_SECRET": SECRET}
|
||||||
|
|
||||||
with run(env=env, no_pool_connection_available=True) as postgrest:
|
with run(env=env, no_pool_connection_available=True) as postgrest:
|
||||||
|
|
||||||
# A JWT with an invalid signature shouldn't open a connection
|
# A JWT with an invalid signature shouldn't open a connection
|
||||||
headers = jwtauthheader({"role": "postgrest_test_author"}, "Wrong Secret")
|
headers = jwtauthheader({"role": "postgrest_test_author"}, "Wrong Secret")
|
||||||
response = postgrest.session.get("/projects", headers=headers)
|
response = postgrest.session.get("/projects", headers=headers)
|
||||||
@@ -814,6 +821,7 @@ def test_no_pool_connection_required_on_bad_embedding(defaultenv):
|
|||||||
"no pool connection should be consumed for failing to embed"
|
"no pool connection should be consumed for failing to embed"
|
||||||
|
|
||||||
with run(env=defaultenv, no_pool_connection_available=True) as postgrest:
|
with run(env=defaultenv, no_pool_connection_available=True) as postgrest:
|
||||||
|
|
||||||
# OPTIONS on a table shouldn't require opening a connection
|
# OPTIONS on a table shouldn't require opening a connection
|
||||||
response = postgrest.session.get("/projects?select=*,unexistent(*)")
|
response = postgrest.session.get("/projects?select=*,unexistent(*)")
|
||||||
assert response.status_code == 400
|
assert response.status_code == 400
|
||||||
@@ -823,6 +831,7 @@ def test_notify_reloading_catalog_cache(defaultenv):
|
|||||||
"notify should reload the connection catalog cache"
|
"notify should reload the connection catalog cache"
|
||||||
|
|
||||||
with run(env=defaultenv) as postgrest:
|
with run(env=defaultenv) as postgrest:
|
||||||
|
|
||||||
# first the id col is an uuid
|
# first the id col is an uuid
|
||||||
response = postgrest.session.get(
|
response = postgrest.session.get(
|
||||||
"/cats?id=eq.dea27321-f988-4a57-93e4-8eeb38f3cf1e"
|
"/cats?id=eq.dea27321-f988-4a57-93e4-8eeb38f3cf1e"
|
||||||
@@ -839,113 +848,6 @@ def test_notify_reloading_catalog_cache(defaultenv):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
def test_role_settings(defaultenv):
|
|
||||||
"statement_timeout should be set per role"
|
|
||||||
|
|
||||||
env = {
|
|
||||||
**defaultenv,
|
|
||||||
"PGRST_JWT_SECRET": SECRET,
|
|
||||||
}
|
|
||||||
|
|
||||||
with run(env=env) as postgrest:
|
|
||||||
# statement_timeout for postgrest_test_anonymous
|
|
||||||
response = postgrest.session.get("/rpc/get_guc_value?name=statement_timeout")
|
|
||||||
assert response.text == '"2s"'
|
|
||||||
|
|
||||||
# reload statement_timeout with NOTIFY
|
|
||||||
response = postgrest.session.post(
|
|
||||||
"/rpc/change_role_statement_timeout", data={"timeout": "5s"}
|
|
||||||
)
|
|
||||||
assert response.status_code == 204
|
|
||||||
|
|
||||||
response = postgrest.session.get("/rpc/reload_pgrst_config")
|
|
||||||
assert response.status_code == 204
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
response = postgrest.session.get("/rpc/get_guc_value?name=statement_timeout")
|
|
||||||
assert response.text == '"5s"'
|
|
||||||
|
|
||||||
# statement_timeout for postgrest_test_author
|
|
||||||
headers = jwtauthheader({"role": "postgrest_test_author"}, SECRET)
|
|
||||||
response = postgrest.session.get(
|
|
||||||
"/rpc/get_guc_value?name=statement_timeout", headers=headers
|
|
||||||
)
|
|
||||||
assert response.text == '"10s"'
|
|
||||||
|
|
||||||
|
|
||||||
def test_isolation_level(defaultenv):
|
|
||||||
"isolation_level should be set per role and per function"
|
|
||||||
|
|
||||||
env = {
|
|
||||||
**defaultenv,
|
|
||||||
"PGRST_JWT_SECRET": SECRET,
|
|
||||||
}
|
|
||||||
|
|
||||||
with run(env=env) as postgrest:
|
|
||||||
# default isolation level for postgrest_test_anonymous
|
|
||||||
response = postgrest.session.get(
|
|
||||||
"/items_w_isolation_level?select=isolation_level&limit=1"
|
|
||||||
)
|
|
||||||
assert response.text == '[{"isolation_level":"read committed"}]'
|
|
||||||
|
|
||||||
# isolation level for postgrest_test_repeatable_read on GET
|
|
||||||
headers = jwtauthheader({"role": "postgrest_test_repeatable_read"}, SECRET)
|
|
||||||
response = postgrest.session.get(
|
|
||||||
"/items_w_isolation_level?select=isolation_level&limit=1", headers=headers
|
|
||||||
)
|
|
||||||
assert response.text == '[{"isolation_level":"repeatable read"}]'
|
|
||||||
|
|
||||||
# isolation level for postgrest_test_serializable on POST
|
|
||||||
headers = jwtauthheader({"role": "postgrest_test_serializable"}, SECRET)
|
|
||||||
headers["Prefer"] = "return=representation"
|
|
||||||
response = postgrest.session.post(
|
|
||||||
"/items_w_isolation_level?select=isolation_level",
|
|
||||||
json={"id": "666"},
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
assert response.text == '[{"isolation_level":"serializable"}]'
|
|
||||||
|
|
||||||
# isolation level for postgrest_test_serializable on PATCH
|
|
||||||
headers = jwtauthheader({"role": "postgrest_test_serializable"}, SECRET)
|
|
||||||
headers["Prefer"] = "return=representation"
|
|
||||||
response = postgrest.session.patch(
|
|
||||||
"/items_w_isolation_level?select=isolation_level&id=eq.666",
|
|
||||||
json={"id": "666"},
|
|
||||||
headers=headers,
|
|
||||||
)
|
|
||||||
assert response.text == '[{"isolation_level":"serializable"}]'
|
|
||||||
|
|
||||||
# isolation level for postgrest_test_serializable on DELETE
|
|
||||||
headers = jwtauthheader({"role": "postgrest_test_serializable"}, SECRET)
|
|
||||||
headers["Prefer"] = "return=representation"
|
|
||||||
response = postgrest.session.delete(
|
|
||||||
"/items_w_isolation_level?select=isolation_level&id=eq.666", headers=headers
|
|
||||||
)
|
|
||||||
assert response.text == '[{"isolation_level":"serializable"}]'
|
|
||||||
|
|
||||||
# default isolation level for function
|
|
||||||
response = postgrest.session.get("/rpc/default_isolation_level")
|
|
||||||
assert response.text == '"read committed"'
|
|
||||||
|
|
||||||
# changes with role isolation level
|
|
||||||
headers = jwtauthheader({"role": "postgrest_test_repeatable_read"}, SECRET)
|
|
||||||
response = postgrest.session.get(
|
|
||||||
"/rpc/default_isolation_level", headers=headers
|
|
||||||
)
|
|
||||||
assert response.text == '"repeatable read"'
|
|
||||||
|
|
||||||
# isolation level can be set per function
|
|
||||||
response = postgrest.session.get("/rpc/serializable_isolation_level")
|
|
||||||
assert response.text == '"serializable"'
|
|
||||||
response = postgrest.session.get("/rpc/repeatable_read_isolation_level")
|
|
||||||
assert response.text == '"repeatable read"'
|
|
||||||
|
|
||||||
# isolation level for a function overrides the role isolation level
|
|
||||||
headers = jwtauthheader({"role": "postgrest_test_repeatable_read"}, SECRET)
|
|
||||||
response = postgrest.session.get("/rpc/serializable_isolation_level")
|
|
||||||
assert response.text == '"serializable"'
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: This test fails now because of https://github.com/PostgREST/postgrest/pull/2122
|
# TODO: This test fails now because of https://github.com/PostgREST/postgrest/pull/2122
|
||||||
# The stack size of 1K(-with-rtsopts=-K1K) is not enough and this fails with "stack overflow"
|
# The stack size of 1K(-with-rtsopts=-K1K) is not enough and this fails with "stack overflow"
|
||||||
# A stack size of 200K seems to be enough for succeess
|
# A stack size of 200K seems to be enough for succeess
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
CREATE ROLE postgrest_test_anonymous;
|
CREATE ROLE postgrest_test_anonymous;
|
||||||
GRANT postgrest_test_anonymous TO :PGUSER;
|
GRANT postgrest_test_anonymous TO :USER;
|
||||||
CREATE SCHEMA test;
|
CREATE SCHEMA test;
|
||||||
|
|
||||||
-- PUT+PATCH target needs one record and column to modify
|
-- PUT+PATCH target needs one record and column to modify
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
INSERT INTO "test"."complex_items"("arr_data", "field-with_sep", "id", "name")
|
|
||||||
SELECT pgrst_body."arr_data", pgrst_body."field-with_sep", pgrst_body."id", pgrst_body."name"
|
|
||||||
FROM (
|
|
||||||
SELECT '[{"id": 4, "name": "Vier"}, {"id": 5, "name": "Funf", "arr_data": null}, {"id": 6, "name": "Sechs", "arr_data": [1, 2, 3], "field-with_sep": 6}]'::jsonb as json_data
|
|
||||||
) pgrst_payload,
|
|
||||||
LATERAL (
|
|
||||||
SELECT CASE WHEN jsonb_typeof(pgrst_payload.json_data) = 'array' THEN pgrst_payload.json_data ELSE jsonb_build_array(pgrst_payload.json_data) END AS val
|
|
||||||
) pgrst_uniform_json,
|
|
||||||
LATERAL (
|
|
||||||
SELECT jsonb_agg(jsonb_build_object('field-with_sep', 1) || elem) AS vals from jsonb_array_elements(pgrst_uniform_json.val) elem
|
|
||||||
) pgrst_json_defs,
|
|
||||||
LATERAL (
|
|
||||||
SELECT * FROM jsonb_to_recordset (pgrst_json_defs.vals) AS _ ("arr_data" integer[], "field-with_sep" integer, "id" bigint, "name" text)
|
|
||||||
) pgrst_body
|
|
||||||
RETURNING "test"."complex_items".*;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
INSERT INTO "test"."complex_items"("arr_data", "field-with_sep", "id", "name")
|
|
||||||
SELECT pgrst_body."arr_data", pgrst_body."field-with_sep", pgrst_body."id", pgrst_body."name"
|
|
||||||
FROM (
|
|
||||||
SELECT '[{"id": 4, "name": "Vier"}, {"id": 5, "name": "Funf", "arr_data": null}, {"id": 6, "name": "Sechs", "arr_data": [1, 2, 3], "field-with_sep": 6}]'::jsonb as json_data
|
|
||||||
) pgrst_payload,
|
|
||||||
LATERAL (
|
|
||||||
SELECT CASE WHEN jsonb_typeof(pgrst_payload.json_data) = 'array' THEN pgrst_payload.json_data ELSE jsonb_build_array(pgrst_payload.json_data) END AS val
|
|
||||||
) pgrst_uniform_json,
|
|
||||||
LATERAL (
|
|
||||||
SELECT * FROM jsonb_to_recordset (pgrst_uniform_json.val) AS _ ("arr_data" integer[], "field-with_sep" integer, "id" bigint, "name" text)
|
|
||||||
) pgrst_body
|
|
||||||
RETURNING "test"."complex_items".*
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
WITH pgrst_source AS (
|
|
||||||
SELECT pgrst_call.*
|
|
||||||
FROM (
|
|
||||||
SELECT '{"id": 4}'::json as json_data
|
|
||||||
) pgrst_payload,
|
|
||||||
LATERAL (
|
|
||||||
SELECT CASE WHEN json_typeof(pgrst_payload.json_data) = 'array' THEN pgrst_payload.json_data ELSE json_build_array(pgrst_payload.json_data) END AS val
|
|
||||||
) pgrst_uniform_json,
|
|
||||||
LATERAL (
|
|
||||||
SELECT * FROM json_to_recordset(pgrst_uniform_json.val) AS _("id" integer) LIMIT 1
|
|
||||||
) pgrst_body,
|
|
||||||
LATERAL "test"."get_projects_below"("id" := pgrst_body.id) pgrst_call
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
null::bigint AS total_result_set,
|
|
||||||
pg_catalog.count(_postgrest_t) AS page_total,
|
|
||||||
coalesce(json_agg(_postgrest_t), '[]')::character varying AS body,
|
|
||||||
nullif(current_setting('response.headers', true), '') AS response_headers,
|
|
||||||
nullif(current_setting('response.status', true), '') AS response_status
|
|
||||||
FROM (SELECT "projects".* FROM "pgrst_source" AS "projects") _postgrest_t;
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
WITH pgrst_source AS (
|
|
||||||
WITH
|
|
||||||
pgrst_payload AS (SELECT '{"id": 4}'::json AS json_data),
|
|
||||||
pgrst_body AS ( SELECT CASE WHEN json_typeof(json_data) = 'array' THEN json_data ELSE json_build_array(json_data) END AS val FROM pgrst_payload),
|
|
||||||
pgrst_args AS ( SELECT * FROM json_to_recordset((SELECT val FROM pgrst_body)) AS _("id" integer) )
|
|
||||||
SELECT "get_projects_below".*
|
|
||||||
FROM "test"."get_projects_below"("id" := (SELECT "id" FROM pgrst_args LIMIT 1))
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
null::bigint AS total_result_set,
|
|
||||||
pg_catalog.count(_postgrest_t) AS page_total,
|
|
||||||
coalesce(json_agg(_postgrest_t), '[]')::character varying AS body,
|
|
||||||
nullif(current_setting('response.headers', true), '') AS response_headers,
|
|
||||||
nullif(current_setting('response.status', true), '') AS response_status
|
|
||||||
FROM (SELECT "projects".* FROM "pgrst_source" AS "projects") _postgrest_t;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
INSERT INTO "test"."complex_items"("arr_data", "field-with_sep", "id", "name")
|
|
||||||
SELECT pgrst_body."arr_data", pgrst_body."field-with_sep", pgrst_body."id", pgrst_body."name"
|
|
||||||
FROM (
|
|
||||||
SELECT '[{"id": 4, "name": "Vier"}, {"id": 5, "name": "Funf", "arr_data": null}, {"id": 6, "name": "Sechs", "arr_data": [1, 2, 3], "field-with_sep": 6}]'::json as json_data
|
|
||||||
) pgrst_payload,
|
|
||||||
LATERAL (
|
|
||||||
SELECT CASE WHEN json_typeof(pgrst_payload.json_data) = 'array' THEN pgrst_payload.json_data ELSE json_build_array(pgrst_payload.json_data) END AS val
|
|
||||||
) pgrst_uniform_json,
|
|
||||||
LATERAL (
|
|
||||||
SELECT * FROM json_to_recordset (pgrst_uniform_json.val) AS _ ("arr_data" integer[], "field-with_sep" integer, "id" bigint, "name" text)
|
|
||||||
) pgrst_body
|
|
||||||
RETURNING "test"."complex_items".*
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
WITH
|
|
||||||
pgrst_payload AS (SELECT '[{"id": 4, "name": "Vier"}, {"id": 5, "name": "Funf", "arr_data": null}, {"id": 6, "name": "Sechs", "arr_data": [1, 2, 3], "field-with_sep": 6}]'::json AS json_data),
|
|
||||||
pgrst_body AS ( SELECT CASE WHEN json_typeof(json_data) = 'array' THEN json_data ELSE json_build_array(json_data) END AS val FROM pgrst_payload)
|
|
||||||
INSERT INTO "test"."complex_items"("arr_data", "field-with_sep", "id", "name")
|
|
||||||
SELECT "arr_data", "field-with_sep", "id", "name"
|
|
||||||
FROM json_to_recordset ((SELECT val FROM pgrst_body)) AS _ ("arr_data" integer[], "field-with_sep" integer, "id" bigint, "name" text)
|
|
||||||
RETURNING "test"."complex_items".*
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
## pgbench tests
|
|
||||||
|
|
||||||
Can be used as:
|
|
||||||
|
|
||||||
```
|
|
||||||
postgrest-with-postgresql-15 -f test/pgbench/fixtures.sql pgbench -n -T 10 -f test/pgbench/1567/old.sql
|
|
||||||
|
|
||||||
postgrest-with-postgresql-15 -f test/pgbench/fixtures.sql pgbench -n -T 10 -f test/pgbench/1567/new.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
## Directory structure
|
|
||||||
|
|
||||||
The directory name is the issue number on github.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
\ir ../spec/fixtures/load.sql
|
|
||||||
|
|
||||||
ALTER TABLE test.complex_items
|
|
||||||
DROP CONSTRAINT complex_items_pkey;
|
|
||||||
|
|
||||||
ALTER TABLE test.complex_items
|
|
||||||
ALTER COLUMN "field-with_sep" DROP NOT NULL;
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
module Feature.NoSuperuserSpec where
|
|
||||||
|
|
||||||
import Network.Wai (Application)
|
|
||||||
|
|
||||||
import Network.HTTP.Types
|
|
||||||
import Test.Hspec
|
|
||||||
import Test.Hspec.Wai
|
|
||||||
|
|
||||||
import Protolude
|
|
||||||
|
|
||||||
spec :: SpecWith ((), Application)
|
|
||||||
spec =
|
|
||||||
describe "No Superuser" $ do
|
|
||||||
it "proves that the authenticator role is not a superuser" $ do
|
|
||||||
request methodGet "/rpc/is_superuser"
|
|
||||||
mempty
|
|
||||||
""
|
|
||||||
`shouldRespondWith`
|
|
||||||
"false"
|
|
||||||
{ matchStatus = 200 }
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
module Feature.ObservabilitySpec where
|
|
||||||
|
|
||||||
import Network.Wai (Application)
|
|
||||||
|
|
||||||
import Network.HTTP.Types
|
|
||||||
import Test.Hspec
|
|
||||||
import Test.Hspec.Wai
|
|
||||||
|
|
||||||
import Protolude
|
|
||||||
|
|
||||||
spec :: SpecWith ((), Application)
|
|
||||||
spec =
|
|
||||||
describe "Observability" $ do
|
|
||||||
it "includes the server trace header on the response" $ do
|
|
||||||
request methodHead "/"
|
|
||||||
[ ("X-Request-Id", "1") ]
|
|
||||||
""
|
|
||||||
`shouldRespondWith`
|
|
||||||
""
|
|
||||||
{ matchHeaders = [ "X-Request-Id" <:> "1"] }
|
|
||||||
|
|
||||||
request methodHead "/projects"
|
|
||||||
[ ("X-Request-Id", "2") ]
|
|
||||||
""
|
|
||||||
`shouldRespondWith`
|
|
||||||
""
|
|
||||||
{ matchHeaders = [ "X-Request-Id" <:> "2"] }
|
|
||||||
|
|
||||||
request methodHead "/rpc/add_them?a=2&b=4"
|
|
||||||
[ ("X-Request-Id", "3") ]
|
|
||||||
""
|
|
||||||
`shouldRespondWith`
|
|
||||||
""
|
|
||||||
{ matchHeaders = [ "X-Request-Id" <:> "3"] }
|
|
||||||
@@ -42,20 +42,6 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
|||||||
|
|
||||||
liftIO $ docsUrl `shouldBe` Just (String ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
|
liftIO $ docsUrl `shouldBe` Just (String ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
|
||||||
|
|
||||||
describe "schema" $ do
|
|
||||||
|
|
||||||
it "includes title and comments to schema" $ do
|
|
||||||
r <- simpleBody <$> get "/"
|
|
||||||
|
|
||||||
let childGetTitle = r ^? key "info" . key "title"
|
|
||||||
let childGetDescription = r ^? key "info" . key "description"
|
|
||||||
|
|
||||||
liftIO $ do
|
|
||||||
|
|
||||||
childGetTitle `shouldBe` Just "My API title"
|
|
||||||
|
|
||||||
childGetDescription `shouldBe` Just "My API description\nthat spans\nmultiple lines"
|
|
||||||
|
|
||||||
describe "table" $ do
|
describe "table" $ do
|
||||||
|
|
||||||
it "includes paths to tables" $ do
|
it "includes paths to tables" $ do
|
||||||
@@ -708,153 +694,7 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
|||||||
|
|
||||||
describe "RPC" $ do
|
describe "RPC" $ do
|
||||||
|
|
||||||
it "includes function summary/description and query parameters for arguments in the get path item" $ do
|
it "includes function summary/description and body schema for arguments" $ do
|
||||||
r <- simpleBody <$> get "/"
|
|
||||||
|
|
||||||
let method s = key "paths" . key "/rpc/varied_arguments_openapi" . key s
|
|
||||||
args = r ^? method "get" . key "parameters"
|
|
||||||
summary = r ^? method "get" . key "summary"
|
|
||||||
description = r ^? method "get" . key "description"
|
|
||||||
|
|
||||||
liftIO $ do
|
|
||||||
|
|
||||||
summary `shouldBe` Just "An RPC function"
|
|
||||||
|
|
||||||
description `shouldBe` Just "Just a test for RPC function arguments"
|
|
||||||
|
|
||||||
args `shouldBe` Just
|
|
||||||
[aesonQQ|
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"format": "double precision",
|
|
||||||
"in": "query",
|
|
||||||
"name": "double",
|
|
||||||
"required": true,
|
|
||||||
"type": "number"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "character varying",
|
|
||||||
"in": "query",
|
|
||||||
"name": "varchar",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "boolean",
|
|
||||||
"in": "query",
|
|
||||||
"name": "boolean",
|
|
||||||
"required": true,
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "date",
|
|
||||||
"in": "query",
|
|
||||||
"name": "date",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "money",
|
|
||||||
"in": "query",
|
|
||||||
"name": "money",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "enum_menagerie_type",
|
|
||||||
"in": "query",
|
|
||||||
"name": "enum",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "text[]",
|
|
||||||
"in": "query",
|
|
||||||
"name": "text_arr",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "integer[]",
|
|
||||||
"in": "query",
|
|
||||||
"name": "int_arr",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "boolean[]",
|
|
||||||
"in": "query",
|
|
||||||
"name": "bool_arr",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "character[]",
|
|
||||||
"in": "query",
|
|
||||||
"name": "char_arr",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "character varying[]",
|
|
||||||
"in": "query",
|
|
||||||
"name": "varchar_arr",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "bigint[]",
|
|
||||||
"in": "query",
|
|
||||||
"name": "bigint_arr",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "numeric[]",
|
|
||||||
"in": "query",
|
|
||||||
"name": "numeric_arr",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "json[]",
|
|
||||||
"in": "query",
|
|
||||||
"name": "json_arr",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "jsonb[]",
|
|
||||||
"in": "query",
|
|
||||||
"name": "jsonb_arr",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "integer",
|
|
||||||
"in": "query",
|
|
||||||
"name": "integer",
|
|
||||||
"required": false,
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "json",
|
|
||||||
"in": "query",
|
|
||||||
"name": "json",
|
|
||||||
"required": false,
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"format": "jsonb",
|
|
||||||
"in": "query",
|
|
||||||
"name": "jsonb",
|
|
||||||
"required": false,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|]
|
|
||||||
|
|
||||||
it "includes function summary/description and body schema for arguments in the post path item" $ do
|
|
||||||
r <- simpleBody <$> get "/"
|
r <- simpleBody <$> get "/"
|
||||||
|
|
||||||
let method s = key "paths" . key "/rpc/varied_arguments_openapi" . key s
|
let method s = key "paths" . key "/rpc/varied_arguments_openapi" . key s
|
||||||
@@ -1019,26 +859,6 @@ spec actualPgVersion = describe "OpenAPI" $ do
|
|||||||
|
|
||||||
liftIO $ params `shouldBe` Just [aesonQQ|["num", "str"]|]
|
liftIO $ params `shouldBe` Just [aesonQQ|["num", "str"]|]
|
||||||
|
|
||||||
it "uses a multi collection format when the function has a VARIADIC parameter" $ do
|
|
||||||
r <- simpleBody <$> get "/"
|
|
||||||
let param = r ^? key "paths" . key "/rpc/variadic_param"
|
|
||||||
. key "get" . key "parameters" . nth 0
|
|
||||||
|
|
||||||
liftIO $ param `shouldBe` Just
|
|
||||||
[aesonQQ|
|
|
||||||
{
|
|
||||||
"collectionFormat": "multi",
|
|
||||||
"in": "query",
|
|
||||||
"items": {
|
|
||||||
"format": "text",
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"name": "v",
|
|
||||||
"required": false,
|
|
||||||
"type": "array"
|
|
||||||
}
|
|
||||||
|]
|
|
||||||
|
|
||||||
describe "Security" $
|
describe "Security" $
|
||||||
it "does not include security or security definitions by default" $ do
|
it "does not include security or security definitions by default" $ do
|
||||||
r <- simpleBody <$> get "/"
|
r <- simpleBody <$> get "/"
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import Test.Hspec.Wai.JSON
|
|||||||
|
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
|
|
||||||
|
import SpecHelper
|
||||||
|
|
||||||
spec :: SpecWith ((), Application)
|
spec :: SpecWith ((), Application)
|
||||||
spec =
|
spec =
|
||||||
describe "root spec function" $ do
|
describe "root spec function" $ do
|
||||||
@@ -20,3 +22,9 @@ spec =
|
|||||||
"info": {"title": "PostgREST API", "description": "This is a dynamic API generated by PostgREST"}
|
"info": {"title": "PostgREST API", "description": "This is a dynamic API generated by PostgREST"}
|
||||||
}|]
|
}|]
|
||||||
{ matchHeaders = ["Content-Type" <:> "application/openapi+json; charset=utf-8"] }
|
{ matchHeaders = ["Content-Type" <:> "application/openapi+json; charset=utf-8"] }
|
||||||
|
|
||||||
|
it "accepts application/json" $
|
||||||
|
request methodGet "/"
|
||||||
|
[("Accept", "application/json")] "" `shouldRespondWith`
|
||||||
|
200
|
||||||
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|||||||
@@ -92,9 +92,6 @@ spec actualPgVersion =
|
|||||||
{"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 handle isdistinct" $
|
|
||||||
get "/entities?and=(id.gte.2,arr.isdistinct.{1,2})&select=id" `shouldRespondWith`
|
|
||||||
[json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
when (actualPgVersion >= pgVersion112) $
|
when (actualPgVersion >= pgVersion112) $
|
||||||
it "can handle wfts (websearch_to_tsquery)" $
|
it "can handle wfts (websearch_to_tsquery)" $
|
||||||
@@ -141,8 +138,6 @@ spec actualPgVersion =
|
|||||||
[json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
[json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
get "/ranges?range=adj.(3,10]&select=id" `shouldRespondWith`
|
get "/ranges?range=adj.(3,10]&select=id" `shouldRespondWith`
|
||||||
[json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
|
[json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
get "/ranges?range=isdistinct.[1,3]&select=id" `shouldRespondWith`
|
|
||||||
[json|[{ "id": 2 }, { "id": 3 }, { "id": 4 }, {"id": 5}]|] { matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "can handle array operators" $ do
|
it "can handle array operators" $ do
|
||||||
get "/entities?arr=eq.{1,2,3}&select=id" `shouldRespondWith`
|
get "/entities?arr=eq.{1,2,3}&select=id" `shouldRespondWith`
|
||||||
@@ -171,8 +166,6 @@ spec actualPgVersion =
|
|||||||
[json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
|
[json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
get "/entities?arr=ov.{2,3}&select=id" `shouldRespondWith`
|
get "/entities?arr=ov.{2,3}&select=id" `shouldRespondWith`
|
||||||
[json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
|
[json|[{ "id": 2 }, { "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
get "/entities?arr=isdistinct.{1,2}&select=id" `shouldRespondWith`
|
|
||||||
[json|[{ "id": 1 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
context "operators with not" $ do
|
context "operators with not" $ do
|
||||||
it "eq, cs, like can be negated" $
|
it "eq, cs, like can be negated" $
|
||||||
@@ -187,9 +180,6 @@ spec actualPgVersion =
|
|||||||
it "gt, lte, ilike can be negated" $
|
it "gt, lte, ilike can be negated" $
|
||||||
get "/entities?and=(name.not.ilike.*ITY2,or(id.not.gt.4,id.not.lte.1))&select=id" `shouldRespondWith`
|
get "/entities?and=(name.not.ilike.*ITY2,or(id.not.gt.4,id.not.lte.1))&select=id" `shouldRespondWith`
|
||||||
[json|[{"id": 1}, {"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }
|
[json|[{"id": 1}, {"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }
|
||||||
it "isdistinct can be negated" $
|
|
||||||
get "/entities?and=(id.not.eq.2,arr.not.isdistinct.{1,2,3})&select=id" `shouldRespondWith`
|
|
||||||
[json|[{"id": 3}]|] { matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
context "and/or params with quotes" $ do
|
context "and/or params with quotes" $ do
|
||||||
it "eq can have quotes" $
|
it "eq can have quotes" $
|
||||||
@@ -262,3 +252,35 @@ spec actualPgVersion =
|
|||||||
|
|
||||||
it "can query columns that begin with and/or reserved words" $
|
it "can query columns that begin with and/or reserved words" $
|
||||||
get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200
|
get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200
|
||||||
|
|
||||||
|
it "fails when using IN without () and provides meaningful error message" $
|
||||||
|
get "/entities?or=(id.in.1,2,id.eq.3)" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"details": "unexpected \"1\" expecting \"(\"",
|
||||||
|
"message": "\"failed to parse logic tree ((id.in.1,2,id.eq.3))\" (line 1, column 10)",
|
||||||
|
"code": "PGRST100",
|
||||||
|
"hint": null
|
||||||
|
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "fails on malformed query params and provides meaningful error message" $ do
|
||||||
|
get "/entities?or=)(" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"details": "unexpected \")\" expecting \"(\"",
|
||||||
|
"message": "\"failed to parse logic tree ()()\" (line 1, column 3)",
|
||||||
|
"code": "PGRST100",
|
||||||
|
"hint": null
|
||||||
|
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/entities?and=(ord(id.eq.1,id.eq.1),id.eq.2)" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"details": "unexpected \"d\" expecting \"(\"",
|
||||||
|
"message": "\"failed to parse logic tree ((ord(id.eq.1,id.eq.1),id.eq.2))\" (line 1, column 7)",
|
||||||
|
"code": "PGRST100",
|
||||||
|
"hint": null
|
||||||
|
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/entities?or=(id.eq.1,not.xor(id.eq.2,id.eq.3))" `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
|
"details": "unexpected \"x\" expecting logic operator (and, or)",
|
||||||
|
"message": "\"failed to parse logic tree ((id.eq.1,not.xor(id.eq.2,id.eq.3)))\" (line 1, column 16)",
|
||||||
|
"code": "PGRST100",
|
||||||
|
"hint": null
|
||||||
|
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ spec =
|
|||||||
it "works with the limit and offset query params" $
|
it "works with the limit and offset query params" $
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
baseTable "limited_delete_items" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodDelete "/limited_delete_items?order=id&limit=1&offset=1" mempty mempty
|
requestMutation methodDelete "/limited_delete_items?order=id&limit=1&offset=1" mempty
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[
|
[json|[
|
||||||
{ "id": 1, "name": "item-1" }
|
{ "id": 1, "name": "item-1" }
|
||||||
@@ -164,7 +164,7 @@ spec =
|
|||||||
it "works with the limit query param plus a filter" $
|
it "works with the limit query param plus a filter" $
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
baseTable "limited_delete_items" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodDelete "/limited_delete_items?order=id&limit=1&id=gt.1" mempty mempty
|
requestMutation methodDelete "/limited_delete_items?order=id&limit=1&id=gt.1" mempty
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[
|
[json|[
|
||||||
{ "id": 1, "name": "item-1" }
|
{ "id": 1, "name": "item-1" }
|
||||||
@@ -200,7 +200,7 @@ spec =
|
|||||||
it "works with views with an explicit order by unique col" $
|
it "works with views with an explicit order by unique col" $
|
||||||
baseTable "limited_delete_items_view" "id" tblDataBefore
|
baseTable "limited_delete_items_view" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodDelete "/limited_delete_items_view?order=id&limit=1&offset=1" mempty mempty
|
requestMutation methodDelete "/limited_delete_items_view?order=id&limit=1&offset=1" mempty
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[
|
[json|[
|
||||||
{ "id": 1, "name": "item-1" }
|
{ "id": 1, "name": "item-1" }
|
||||||
@@ -210,7 +210,7 @@ spec =
|
|||||||
it "works with views with an explicit order by composite pk" $
|
it "works with views with an explicit order by composite pk" $
|
||||||
baseTable "limited_delete_items_cpk_view" "id" tblDataBefore
|
baseTable "limited_delete_items_cpk_view" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodDelete "/limited_delete_items_cpk_view?order=id,name&limit=1&offset=1" mempty mempty
|
requestMutation methodDelete "/limited_delete_items_cpk_view?order=id,name&limit=1&offset=1" mempty
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[
|
[json|[
|
||||||
{ "id": 1, "name": "item-1" }
|
{ "id": 1, "name": "item-1" }
|
||||||
@@ -220,53 +220,9 @@ spec =
|
|||||||
it "works on a table without a pk by ordering by 'ctid'" $
|
it "works on a table without a pk by ordering by 'ctid'" $
|
||||||
baseTable "limited_delete_items_no_pk" "id" tblDataBefore
|
baseTable "limited_delete_items_no_pk" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodDelete "/limited_delete_items_no_pk?order=ctid&limit=1&offset=1" mempty mempty
|
requestMutation methodDelete "/limited_delete_items_no_pk?order=ctid&limit=1&offset=1" mempty
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[
|
[json|[
|
||||||
{ "id": 1, "name": "item-1" }
|
{ "id": 1, "name": "item-1" }
|
||||||
, { "id": 3, "name": "item-3" }
|
, { "id": 3, "name": "item-3" }
|
||||||
]|]
|
]|]
|
||||||
|
|
||||||
it "ignores the Range header" $ do
|
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0)) mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[]|]
|
|
||||||
|
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items?id=gte.2"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0)) mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[ { "id": 1, "name": "item-1" } ]|]
|
|
||||||
|
|
||||||
it "ignores the Range header and does not do a limited delete" $
|
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items?order=id"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0)) mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[]|]
|
|
||||||
|
|
||||||
it "ignores the Range header and does not throw an invalid range error" $
|
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items?order=id&limit=1&offset=1"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0)) mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "ignores the Range header but not the limit and offset params" $
|
|
||||||
baseTable "limited_delete_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodDelete "/limited_delete_items?order=id&limit=2&offset=1"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 1 1)) mempty
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
]|]
|
|
||||||
|
|||||||
@@ -68,8 +68,10 @@ spec =
|
|||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "errs when there are more than two fks on a junction table but it can be disambiguated with spread embeds" $ do
|
it "errs when there are more than two fks on a junction table(currently impossible to disambiguate, only choice is to split the table)" $
|
||||||
-- We have 4 possibilities for doing the junction JOIN here.
|
-- We have 4 possibilities for doing the junction JOIN here.
|
||||||
|
-- This could be solved by specifying two additional fks, like whatev_projects!fk1!fk2(*)
|
||||||
|
-- If the need arises this capability can be added later without causing a breaking change
|
||||||
get "/whatev_sites?select=*,whatev_projects(*)" `shouldRespondWith`
|
get "/whatev_sites?select=*,whatev_projects(*)" `shouldRespondWith`
|
||||||
[json|
|
[json|
|
||||||
{
|
{
|
||||||
@@ -103,23 +105,6 @@ spec =
|
|||||||
{ matchStatus = 300
|
{ matchStatus = 300
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
-- Each of those 4 possibilities can be done with spread embeds, by following the details in the error above
|
|
||||||
get "/whatev_sites?select=*,whatev_jobs!site_id_1(...whatev_projects!project_id_1(*))" `shouldRespondWith` [json|[]|]
|
|
||||||
get "/whatev_sites?select=*,whatev_jobs!site_id_1(...whatev_projects!project_id_2(*))" `shouldRespondWith` [json|[]|]
|
|
||||||
get "/whatev_sites?select=*,whatev_jobs!site_id_2(...whatev_projects!project_id_1(*))" `shouldRespondWith` [json|[]|]
|
|
||||||
get "/whatev_sites?select=*,whatev_jobs!site_id_2(...whatev_projects!project_id_2(*))" `shouldRespondWith` [json|[]|]
|
|
||||||
|
|
||||||
it "can disambiguate a recursive m2m with spread embeds" $ do
|
|
||||||
get "/posters?select=*,subscribers:subscriptions!subscribed(...posters!subscriber(*))&limit=1" `shouldRespondWith`
|
|
||||||
[json| [ {"id":1,"name":"Mark","subscribers":[{"id":3,"name":"Bill"}, {"id":4,"name":"Jeff"}]}]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/posters?select=*,subscriptions!subscriber(...posters!subscribed(*))&limit=1" `shouldRespondWith`
|
|
||||||
[json| [{"id":1,"name":"Mark","subscriptions":[{"id":2,"name":"Elon"}]}]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "errs on an ambiguous embed that has two one-to-one relationships" $
|
it "errs on an ambiguous embed that has two one-to-one relationships" $
|
||||||
get "/first?select=second(*)" `shouldRespondWith`
|
get "/first?select=second(*)" `shouldRespondWith`
|
||||||
|
|||||||
@@ -11,10 +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, pgVersion100,
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion110,
|
||||||
pgVersion110, pgVersion112,
|
pgVersion112, pgVersion130)
|
||||||
pgVersion120, pgVersion130,
|
|
||||||
pgVersion140)
|
|
||||||
|
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
@@ -422,125 +420,20 @@ spec actualPgVersion = do
|
|||||||
, matchHeaders = []
|
, matchHeaders = []
|
||||||
}
|
}
|
||||||
|
|
||||||
it "disallows ?columns which don't exist" $
|
|
||||||
post "/articles?columns=helicopter"
|
|
||||||
[json|[
|
|
||||||
{"id": 204, "body": "yyy"},
|
|
||||||
{"id": 205, "body": "zzz"}]|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|{"code":"PGRST204","details":null,"hint":null,"message":"Column 'helicopter' of relation 'articles' does not exist"} |]
|
|
||||||
{ matchStatus = 400
|
|
||||||
, matchHeaders = []
|
|
||||||
}
|
|
||||||
|
|
||||||
it "returns missing table error even if also has invalid ?columns" $
|
|
||||||
post "/garlic?columns=helicopter"
|
|
||||||
[json|[
|
|
||||||
{"id": 204, "body": "yyy"},
|
|
||||||
{"id": 205, "body": "zzz"}]|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|{} |]
|
|
||||||
{ matchStatus = 404
|
|
||||||
, matchHeaders = []
|
|
||||||
}
|
|
||||||
|
|
||||||
it "disallows array elements that are not json objects" $
|
it "disallows array elements that are not json objects" $
|
||||||
post "/articles?columns=id,body"
|
post "/articles?columns=id,body"
|
||||||
[json|[
|
[json|[
|
||||||
{"id": 204, "body": "yyy"},
|
{"id": 204, "body": "yyy"},
|
||||||
333,
|
333,
|
||||||
"asdf",
|
"asdf",
|
||||||
{"id": 205, "body": "zzz"}]|] `shouldRespondWith` 400
|
{"id": 205, "body": "zzz"}]|] `shouldRespondWith`
|
||||||
|
[json|{
|
||||||
context "apply defaults on missing values" $ do
|
"code": "22023",
|
||||||
-- inserting the array fails on pg 9.6, but the feature should work normally
|
"details": null,
|
||||||
when (actualPgVersion >= pgVersion100) $
|
"hint": null,
|
||||||
it "inserts table default values(field-with_sep) when json keys are undefined" $
|
"message": "argument of json_populate_recordset must be an array of objects"}|]
|
||||||
request methodPost "/complex_items?columns=id,name,field-with_sep,arr_data" [("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
{ matchStatus = 400
|
||||||
[json|[
|
, matchHeaders = []
|
||||||
{"id": 4, "name": "Vier"},
|
|
||||||
{"id": 5, "name": "Funf", "arr_data": null},
|
|
||||||
{"id": 6, "name": "Sechs", "field-with_sep": 6, "arr_data": "{1,2,3}"}
|
|
||||||
]|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id": 4, "name": "Vier", "field-with_sep": 1, "settings":null,"arr_data":null},
|
|
||||||
{"id": 5, "name": "Funf", "field-with_sep": 1, "settings":null,"arr_data":null},
|
|
||||||
{"id": 6, "name": "Sechs", "field-with_sep": 6, "settings":null,"arr_data":[1,2,3]}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 201
|
|
||||||
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "inserts view default values(field-with_sep) when json keys are undefined" $
|
|
||||||
request methodPost "/complex_items_view?columns=id,name" [("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
|
||||||
[json|[
|
|
||||||
{"id": 7, "name": "Sieben"},
|
|
||||||
{"id": 8}
|
|
||||||
]|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id": 7, "name": "Sieben", "field-with_sep": 1, "settings":null,"arr_data":null},
|
|
||||||
{"id": 8, "name": "Default", "field-with_sep": 1, "settings":null,"arr_data":null}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 201
|
|
||||||
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "doesn't insert json duplicate keys(since it uses jsonb)" $
|
|
||||||
request methodPost "/tbl_w_json?columns=id,data" [("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
|
||||||
[json| { "data": { "a": 1, "a": 2 }, "id": 3 } |]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json| [ { "data": { "a": 2 }, "id": 3 } ] |]
|
|
||||||
{ matchStatus = 201
|
|
||||||
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
|
||||||
}
|
|
||||||
|
|
||||||
when (actualPgVersion >= pgVersion100) $
|
|
||||||
it "inserts a default on a generated by default as identity column" $
|
|
||||||
request methodPost "/channels?columns=id,data,slug&select=data,slug" [("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
|
||||||
[json| { "slug": "foo" } |]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json| [{"data":{"foo": "bar"},"slug":"foo"}] |] -- id 1 was inserted here, we don't get it for idempotence in the tests
|
|
||||||
{ matchStatus = 201
|
|
||||||
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
|
||||||
}
|
|
||||||
|
|
||||||
when (actualPgVersion >= pgVersion120) $
|
|
||||||
it "fails with a good error message on generated always columns" $
|
|
||||||
request methodPost "/foo?columns=a,b" [("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
|
||||||
[json| [
|
|
||||||
{"a": "val"},
|
|
||||||
{"a": "val", "b": "val"}
|
|
||||||
]|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
(if actualPgVersion < pgVersion140
|
|
||||||
then [json| {
|
|
||||||
"code": "42601",
|
|
||||||
"details": "Column \"b\" is a generated column.",
|
|
||||||
"hint": null,
|
|
||||||
"message": "cannot insert into column \"b\""
|
|
||||||
}|]
|
|
||||||
else [json| {
|
|
||||||
"code": "428C9",
|
|
||||||
"details": "Column \"b\" is a generated column.",
|
|
||||||
"hint": null,
|
|
||||||
"message": "cannot insert a non-DEFAULT value into column \"b\""
|
|
||||||
}|])
|
|
||||||
{ matchStatus = 400 }
|
|
||||||
|
|
||||||
it "inserts json that has duplicate keys" $ do
|
|
||||||
request methodPost "/tbl_w_json" [("Prefer", "return=representation")]
|
|
||||||
[json| { "data": { "a": 1, "a": 2 }, "id": 3 } |]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json| [ { "data": { "a": 1, "a": 2 }, "id": 3 } ] |]
|
|
||||||
{ matchStatus = 201
|
|
||||||
}
|
|
||||||
request methodPost "/tbl_w_json?columns=id,data" [("Prefer", "return=representation")]
|
|
||||||
[json| { "data": { "a": 1, "a": 2 }, "id": 3 } |]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json| [ { "data": { "a": 1, "a": 2 }, "id": 3 } ] |]
|
|
||||||
{ matchStatus = 201
|
|
||||||
}
|
}
|
||||||
|
|
||||||
context "with unicode values" $ do
|
context "with unicode values" $ do
|
||||||
|
|||||||
@@ -292,11 +292,25 @@ spec actualPgVersion = describe "json and jsonb operators" $ do
|
|||||||
[json| [{"data":[{"a": [1,2,3]}, {"b": [4,5]}]}] |]
|
[json| [{"data":[{"a": [1,2,3]}, {"b": [4,5]}]}] |]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "gives a meaningful error on bad syntax" $
|
it "should fail on badly formed negatives" $ do
|
||||||
get "/json_arr?select=data->>--34" `shouldRespondWith`
|
get "/json_arr?select=data->>-78xy" `shouldRespondWith`
|
||||||
[json|
|
[json|
|
||||||
{"details": "unexpected \"-\" expecting digit",
|
{"details": "unexpected 'x' expecting digit, \"->\", \"::\", \".\", \",\" or end of input",
|
||||||
"message": "\"failed to parse select parameter (data->>--34)\" (line 1, column 9)",
|
"message": "\"failed to parse select parameter (data->>-78xy)\" (line 1, column 11)",
|
||||||
"code": "PGRST100",
|
"code": "PGRST100",
|
||||||
"hint": null} |]
|
"hint": null} |]
|
||||||
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/json_arr?select=data->>--34" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
{"details": "unexpected \"-\" expecting digit",
|
||||||
|
"message": "\"failed to parse select parameter (data->>--34)\" (line 1, column 9)",
|
||||||
|
"code": "PGRST100",
|
||||||
|
"hint": null} |]
|
||||||
|
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
get "/json_arr?select=data->>-xy-4" `shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
{"details":"unexpected \"x\" expecting digit",
|
||||||
|
"message":"\"failed to parse select parameter (data->>-xy-4)\" (line 1, column 9)",
|
||||||
|
"code": "PGRST100",
|
||||||
|
"hint": null} |]
|
||||||
|
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ spec =
|
|||||||
it "allows full table update if a filter is present" $
|
it "allows full table update if a filter is present" $
|
||||||
baseTable "safe_update_items" "id" tblDataBefore
|
baseTable "safe_update_items" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodPatch "/safe_update_items?id=gt.0" mempty [json| {"name": "updated-item"} |]
|
requestMutation methodPatch "/safe_update_items?id=gt.0" [json| {"name": "updated-item"} |]
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[
|
[json|[
|
||||||
{ "id": 1, "name": "updated-item", "observation": null }
|
{ "id": 1, "name": "updated-item", "observation": null }
|
||||||
@@ -61,7 +61,7 @@ spec =
|
|||||||
it "allows full table delete if a filter is present" $
|
it "allows full table delete if a filter is present" $
|
||||||
baseTable "safe_delete_items" "id" tblDataBefore
|
baseTable "safe_delete_items" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodDelete "/safe_delete_items?id=gt.0" mempty mempty
|
requestMutation methodDelete "/safe_delete_items?id=gt.0" mempty
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[]|]
|
[json|[]|]
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ disabledSpec =
|
|||||||
it "works if no condition is present" $
|
it "works if no condition is present" $
|
||||||
baseTable "unsafe_update_items" "id" tblDataBefore
|
baseTable "unsafe_update_items" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodPatch "/unsafe_update_items" mempty [json| {"name": "updated-item"} |]
|
requestMutation methodPatch "/unsafe_update_items" [json| {"name": "updated-item"} |]
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[
|
[json|[
|
||||||
{ "id": 1, "name": "updated-item", "observation": null }
|
{ "id": 1, "name": "updated-item", "observation": null }
|
||||||
@@ -84,6 +84,6 @@ disabledSpec =
|
|||||||
it "works if no condition is present" $
|
it "works if no condition is present" $
|
||||||
baseTable "unsafe_delete_items" "id" tblDataBefore
|
baseTable "unsafe_delete_items" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodDelete "/unsafe_delete_items" mempty mempty
|
requestMutation methodDelete "/unsafe_delete_items" mempty
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[]|]
|
[json|[]|]
|
||||||
|
|||||||
@@ -8,16 +8,14 @@ import Network.Wai.Test (SResponse (..))
|
|||||||
|
|
||||||
import Data.Aeson.Lens
|
import Data.Aeson.Lens
|
||||||
import Data.Aeson.QQ
|
import Data.Aeson.QQ
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.Text as T
|
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
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
|
||||||
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion120,
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion100,
|
||||||
pgVersion130)
|
pgVersion120, pgVersion130)
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
@@ -28,7 +26,7 @@ spec actualPgVersion = do
|
|||||||
r <- request methodGet "/projects?id=in.(1,2,3)"
|
r <- request methodGet "/projects?id=in.(1,2,3)"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
||||||
|
|
||||||
let totalCost = planCost r
|
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
@@ -37,14 +35,14 @@ spec actualPgVersion = do
|
|||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe`
|
totalCost `shouldBe`
|
||||||
if actualPgVersion > pgVersion120
|
if actualPgVersion > pgVersion120
|
||||||
then 15.63
|
then Just [aesonQQ|15.63|]
|
||||||
else 15.69
|
else Just [aesonQQ|15.69|]
|
||||||
|
|
||||||
it "outputs the total cost for a single filter on a view" $ do
|
it "outputs the total cost for a single filter on a view" $ do
|
||||||
r <- request methodGet "/projects_view?id=gt.2"
|
r <- request methodGet "/projects_view?id=gt.2"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
||||||
|
|
||||||
let totalCost = planCost r
|
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
@@ -53,20 +51,34 @@ spec actualPgVersion = do
|
|||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe`
|
totalCost `shouldBe`
|
||||||
if actualPgVersion > pgVersion120
|
if actualPgVersion > pgVersion120
|
||||||
then 24.28
|
then Just [aesonQQ|24.28|]
|
||||||
else 32.27
|
else Just [aesonQQ|32.28|]
|
||||||
|
|
||||||
it "outputs blocks info when using the buffers option" $
|
it "outputs blocks info when using the buffers option" $
|
||||||
if actualPgVersion >= pgVersion130
|
if actualPgVersion >= pgVersion130
|
||||||
then do
|
then do
|
||||||
r <- request methodGet "/projects" (acceptHdrs "application/vnd.pgrst.plan+json; options=buffers") ""
|
r <- request methodGet "/projects" (acceptHdrs "application/vnd.pgrst.plan+json; options=buffers") ""
|
||||||
|
|
||||||
let resBody = simpleBody r
|
let blocks = simpleBody r ^? nth 0 . key "Planning"
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; options=buffers; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; options=buffers; charset=utf-8")
|
||||||
resBody `shouldSatisfy` (\t -> T.isInfixOf "Shared Hit Blocks" (decodeUtf8 $ BS.toStrict t))
|
blocks `shouldBe`
|
||||||
|
Just [aesonQQ|
|
||||||
|
{
|
||||||
|
"Shared Hit Blocks": 0,
|
||||||
|
"Shared Read Blocks": 0,
|
||||||
|
"Shared Dirtied Blocks": 0,
|
||||||
|
"Shared Written Blocks": 0,
|
||||||
|
"Local Hit Blocks": 0,
|
||||||
|
"Local Read Blocks": 0,
|
||||||
|
"Local Dirtied Blocks": 0,
|
||||||
|
"Local Written Blocks": 0,
|
||||||
|
"Temp Read Blocks": 0,
|
||||||
|
"Temp Written Blocks": 0
|
||||||
|
}
|
||||||
|
|]
|
||||||
else do
|
else do
|
||||||
-- analyze is required for buffers on pg < 13
|
-- analyze is required for buffers on pg < 13
|
||||||
r <- request methodGet "/projects" (acceptHdrs "application/vnd.pgrst.plan+json; options=analyze|buffers") ""
|
r <- request methodGet "/projects" (acceptHdrs "application/vnd.pgrst.plan+json; options=analyze|buffers") ""
|
||||||
@@ -125,8 +137,8 @@ spec actualPgVersion = do
|
|||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; options=verbose; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/json\"; options=verbose; charset=utf-8")
|
||||||
aggCol `shouldBe`
|
aggCol `shouldBe`
|
||||||
if actualPgVersion >= pgVersion120
|
if actualPgVersion >= pgVersion120
|
||||||
then Just [aesonQQ| "COALESCE(json_agg(ROW(projects.id, projects.name, projects.client_id)), '[]'::json)" |]
|
then Just [aesonQQ| "(COALESCE(json_agg(ROW(projects.id, projects.name, projects.client_id)), '[]'::json))::character varying" |]
|
||||||
else Just [aesonQQ| "COALESCE(json_agg(ROW(pgrst_source.id, pgrst_source.name, pgrst_source.client_id)), '[]'::json)" |]
|
else Just [aesonQQ| "(COALESCE(json_agg(ROW(pgrst_source.id, pgrst_source.name, pgrst_source.client_id)), '[]'::json))::character varying" |]
|
||||||
|
|
||||||
it "outputs the plan for application/vnd.pgrst.object " $ do
|
it "outputs the plan for application/vnd.pgrst.object " $ do
|
||||||
r <- request methodGet "/projects_view" (acceptHdrs "application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object\"; options=verbose") ""
|
r <- request methodGet "/projects_view" (acceptHdrs "application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object\"; options=verbose") ""
|
||||||
@@ -138,62 +150,71 @@ spec actualPgVersion = do
|
|||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object+json\"; options=verbose; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object+json\"; options=verbose; charset=utf-8")
|
||||||
aggCol `shouldBe`
|
aggCol `shouldBe`
|
||||||
if actualPgVersion >= pgVersion120
|
if actualPgVersion >= pgVersion120
|
||||||
then Just [aesonQQ| "COALESCE((json_agg(ROW(projects.id, projects.name, projects.client_id)) -> 0), 'null'::json)" |]
|
then Just [aesonQQ| "COALESCE(((json_agg(ROW(projects.id, projects.name, projects.client_id)) -> 0))::text, 'null'::text)" |]
|
||||||
else Just [aesonQQ| "COALESCE((json_agg(ROW(pgrst_source.id, pgrst_source.name, pgrst_source.client_id)) -> 0), 'null'::json)" |]
|
else Just [aesonQQ| "COALESCE(((json_agg(ROW(pgrst_source.id, pgrst_source.name, pgrst_source.client_id)) -> 0))::text, 'null'::text)" |]
|
||||||
|
|
||||||
describe "writes plans" $ do
|
describe "writes plans" $ do
|
||||||
it "outputs the total cost for an insert" $ do
|
it "outputs the total cost for an insert" $ do
|
||||||
r <- request methodPost "/projects"
|
r <- request methodPost "/projects"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") [json|{"id":100, "name": "Project 100"}|]
|
(acceptHdrs "application/vnd.pgrst.plan+json") [json|{"id":100, "name": "Project 100"}|]
|
||||||
|
|
||||||
let totalCost = planCost r
|
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` 3.27
|
totalCost `shouldBe`
|
||||||
|
if actualPgVersion > pgVersion120
|
||||||
|
then Just [aesonQQ|3.28|]
|
||||||
|
else Just [aesonQQ|3.33|]
|
||||||
|
|
||||||
it "outputs the total cost for an update" $ do
|
it "outputs the total cost for an update" $ do
|
||||||
r <- request methodPatch "/projects?id=eq.3"
|
r <- request methodPatch "/projects?id=eq.3"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") [json|{"name": "Patched Project"}|]
|
(acceptHdrs "application/vnd.pgrst.plan+json") [json|{"name": "Patched Project"}|]
|
||||||
|
|
||||||
let totalCost = planCost r
|
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` 12.45
|
totalCost `shouldBe`
|
||||||
|
if actualPgVersion > pgVersion120
|
||||||
|
then Just [aesonQQ|12.45|]
|
||||||
|
else Just [aesonQQ|12.5|]
|
||||||
|
|
||||||
it "outputs the total cost for a delete" $ do
|
it "outputs the total cost for a delete" $ do
|
||||||
r <- request methodDelete "/projects?id=in.(1,2,3)"
|
r <- request methodDelete "/projects?id=in.(1,2,3)"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
||||||
|
|
||||||
let totalCost = planCost r
|
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` 15.68
|
totalCost `shouldBe` Just [aesonQQ|15.68|]
|
||||||
|
|
||||||
it "outputs the total cost for a single upsert" $ do
|
it "outputs the total cost for a single upsert" $ do
|
||||||
r <- request methodPut "/tiobe_pls?name=eq.Go"
|
r <- request methodPut "/tiobe_pls?name=eq.Go"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json")
|
(acceptHdrs "application/vnd.pgrst.plan+json")
|
||||||
[json| [ { "name": "Go", "rank": 19 } ]|]
|
[json| [ { "name": "Go", "rank": 19 } ]|]
|
||||||
|
|
||||||
let totalCost = planCost r
|
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` 1.29
|
totalCost `shouldBe`
|
||||||
|
if actualPgVersion >= pgVersion120
|
||||||
|
then Just [aesonQQ|1.3|]
|
||||||
|
else Just [aesonQQ|1.35|]
|
||||||
|
|
||||||
it "outputs the plan for application/vnd.pgrst.object" $ do
|
it "outputs the plan for application/vnd.pgrst.object" $ do
|
||||||
r <- request methodDelete "/projects?id=eq.6"
|
r <- request methodDelete "/projects?id=eq.6"
|
||||||
@@ -204,21 +225,21 @@ spec actualPgVersion = do
|
|||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object+json\"; options=verbose; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; for=\"application/vnd.pgrst.object+json\"; options=verbose; charset=utf-8")
|
||||||
aggCol `shouldBe` Just [aesonQQ| "COALESCE((json_agg(ROW(projects.id, projects.name, projects.client_id)) -> 0), 'null'::json)" |]
|
aggCol `shouldBe` Just [aesonQQ| "COALESCE(((json_agg(ROW(projects.id, projects.name, projects.client_id)) -> 0))::text, 'null'::text)" |]
|
||||||
|
|
||||||
describe "function plan" $ do
|
describe "function plan" $ do
|
||||||
it "outputs the total cost for a function call" $ do
|
it "outputs the total cost for a function call" $ do
|
||||||
r <- request methodGet "/rpc/getallprojects?id=in.(1,2,3)"
|
r <- request methodGet "/rpc/getallprojects?id=in.(1,2,3)"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
||||||
|
|
||||||
let totalCost = planCost r
|
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
||||||
resHeaders = simpleHeaders r
|
resHeaders = simpleHeaders r
|
||||||
resStatus = simpleStatus r
|
resStatus = simpleStatus r
|
||||||
|
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
resHeaders `shouldSatisfy` elem ("Content-Type", "application/vnd.pgrst.plan+json; charset=utf-8")
|
||||||
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
resStatus `shouldBe` Status { statusCode = 200, statusMessage="OK" }
|
||||||
totalCost `shouldBe` 68.56
|
totalCost `shouldBe` Just [aesonQQ|68.57|]
|
||||||
|
|
||||||
it "outputs the plan for text/xml" $ do
|
it "outputs the plan for text/xml" $ do
|
||||||
r <- request methodGet "/rpc/return_scalar_xml"
|
r <- request methodGet "/rpc/return_scalar_xml"
|
||||||
@@ -261,93 +282,33 @@ spec actualPgVersion = do
|
|||||||
describe "resource embedding costs" $ do
|
describe "resource embedding costs" $ do
|
||||||
it "a one to many doesn't surpass a threshold" $ do
|
it "a one to many doesn't surpass a threshold" $ do
|
||||||
r <- request methodGet "/clients?select=*,projects(*)&id=eq.1"
|
r <- request methodGet "/clients?select=*,projects(*)&id=eq.1"
|
||||||
[planHdr] ""
|
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
||||||
|
|
||||||
liftIO $ planCost r `shouldSatisfy` (< 33.3)
|
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
||||||
|
liftIO $ totalCost `shouldBe`
|
||||||
|
if actualPgVersion > pgVersion120
|
||||||
|
then Just [aesonQQ|33.25|]
|
||||||
|
else Just [aesonQQ|33.27|]
|
||||||
|
|
||||||
it "a many to one doesn't surpass a threshold" $ do
|
it "a many to one doesn't surpass a threshold" $ do
|
||||||
r <- request methodGet "/projects?select=*,clients(*)&id=eq.1"
|
r <- request methodGet "/projects?select=*,clients(*)&id=eq.1"
|
||||||
[planHdr] ""
|
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
||||||
|
|
||||||
liftIO $ planCost r `shouldSatisfy` (< 16.5)
|
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
||||||
|
liftIO $ totalCost `shouldBe`
|
||||||
|
if actualPgVersion > pgVersion120
|
||||||
|
then Just [aesonQQ|16.39|]
|
||||||
|
else Just [aesonQQ|16.41|]
|
||||||
|
|
||||||
it "a many to many doesn't surpass a threshold" $ do
|
it "a many to many doesn't surpass a threshold" $ do
|
||||||
r <- request methodGet "/users?select=*,tasks(*)&id=eq.1"
|
r <- request methodGet "/users?select=*,tasks(*)&id=eq.1"
|
||||||
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
(acceptHdrs "application/vnd.pgrst.plan+json") ""
|
||||||
|
|
||||||
liftIO $ planCost r `shouldSatisfy` (< 70.9)
|
let totalCost = simpleBody r ^? nth 0 . key "Plan" . key "Total Cost"
|
||||||
|
liftIO $ totalCost `shouldBe`
|
||||||
context "!inner vs embed not null" $ do
|
if | actualPgVersion > pgVersion120 -> Just [aesonQQ|69.34|]
|
||||||
it "on an o2m, an !inner has a similar cost to not.null" $ do
|
| actualPgVersion > pgVersion100 -> Just [aesonQQ|69.36|]
|
||||||
r1 <- request methodGet "/clients?select=*,projects!inner(*)&id=eq.1"
|
| otherwise -> Just [aesonQQ|70.81|]
|
||||||
[planHdr] ""
|
|
||||||
|
|
||||||
liftIO $ planCost r1 `shouldSatisfy` (< 33.3)
|
|
||||||
|
|
||||||
r2 <- request methodGet "/clients?select=*,projects(*)&projects=not.is.null&id=eq.1"
|
|
||||||
[planHdr] ""
|
|
||||||
|
|
||||||
liftIO $ planCost r2 `shouldSatisfy` (< 33.3)
|
|
||||||
|
|
||||||
it "on an m2o, an !inner has a similar cost to not.null" $ do
|
|
||||||
r1 <- request methodGet "/projects?select=*,clients!inner(*)&id=eq.1"
|
|
||||||
[planHdr] ""
|
|
||||||
|
|
||||||
liftIO $ planCost r1 `shouldSatisfy` (< 16.42)
|
|
||||||
|
|
||||||
r2 <- request methodGet "/projects?select=*,clients(*)&clients=not.is.null&id=eq.1"
|
|
||||||
[planHdr] ""
|
|
||||||
|
|
||||||
liftIO $ planCost r2 `shouldSatisfy` (< 16.42)
|
|
||||||
|
|
||||||
it "on an m2m, an !inner has a similar cost to not.null" $ do
|
|
||||||
r1 <- request methodGet "/users?select=*,tasks!inner(*)&tasks.id=eq.1"
|
|
||||||
[planHdr] ""
|
|
||||||
|
|
||||||
liftIO $ planCost r1 `shouldSatisfy` (< 20876.14)
|
|
||||||
|
|
||||||
r2 <- request methodGet "/users?select=*,tasks(*)&tasks.id=eq.1&tasks=not.is.null"
|
|
||||||
[planHdr] ""
|
|
||||||
|
|
||||||
liftIO $ planCost r2 `shouldSatisfy` (< 20876.14)
|
|
||||||
|
|
||||||
describe "function call costs" $ do
|
|
||||||
it "should not exceed cost when calling setof composite proc" $ do
|
|
||||||
r <- request methodGet "/rpc/get_projects_below?id=3"
|
|
||||||
[planHdr] ""
|
|
||||||
|
|
||||||
liftIO $ planCost r `shouldSatisfy` (< 45.4)
|
|
||||||
|
|
||||||
it "should not exceed cost when calling setof composite proc with empty params" $ do
|
|
||||||
r <- request methodGet "/rpc/getallprojects"
|
|
||||||
[planHdr] ""
|
|
||||||
|
|
||||||
liftIO $ planCost r `shouldSatisfy` (< 71.0)
|
|
||||||
|
|
||||||
it "should not exceed cost when calling scalar proc" $ do
|
|
||||||
r <- request methodGet "/rpc/add_them?a=3&b=4"
|
|
||||||
[planHdr] ""
|
|
||||||
|
|
||||||
liftIO $ planCost r `shouldSatisfy` (< 1.18)
|
|
||||||
|
|
||||||
context "function inlining" $ do
|
|
||||||
it "should inline a zero argument function(the function won't appear in the plan tree)" $ do
|
|
||||||
r <- request methodGet "/rpc/getallusers?id=eq.1"
|
|
||||||
[(hAccept, "application/vnd.pgrst.plan")] ""
|
|
||||||
|
|
||||||
let resBody = simpleBody r
|
|
||||||
|
|
||||||
liftIO $ do
|
|
||||||
resBody `shouldSatisfy` (\t -> not $ T.isInfixOf "getallusers" (decodeUtf8 $ BS.toStrict t))
|
|
||||||
|
|
||||||
it "should inline a function with arguments(the function won't appear in the plan tree)" $ do
|
|
||||||
r <- request methodGet "/rpc/getitemrange?min=10&max=15"
|
|
||||||
[(hAccept, "application/vnd.pgrst.plan")] ""
|
|
||||||
|
|
||||||
let resBody = simpleBody r
|
|
||||||
|
|
||||||
liftIO $ do
|
|
||||||
resBody `shouldSatisfy` (\t -> not $ T.isInfixOf "getitemrange" (decodeUtf8 $ BS.toStrict t))
|
|
||||||
|
|
||||||
disabledSpec :: SpecWith ((), Application)
|
disabledSpec :: SpecWith ((), Application)
|
||||||
disabledSpec =
|
disabledSpec =
|
||||||
|
|||||||
@@ -291,7 +291,7 @@ spec actualPgVersion = do
|
|||||||
{"hint":"Verify that 'non_existent_projects' is included in the 'select' query parameter.",
|
{"hint":"Verify that 'non_existent_projects' is included in the 'select' query parameter.",
|
||||||
"details":null,
|
"details":null,
|
||||||
"code":"PGRST108",
|
"code":"PGRST108",
|
||||||
"message":"'non_existent_projects' is not an embedded resource in this request"}|]
|
"message":"Cannot apply filter because 'non_existent_projects' is not an embedded resource in this request"}|]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
@@ -300,7 +300,7 @@ spec actualPgVersion = do
|
|||||||
{"hint":"Verify that 'amiga_projectsss' is included in the 'select' query parameter.",
|
{"hint":"Verify that 'amiga_projectsss' is included in the 'select' query parameter.",
|
||||||
"details":null,
|
"details":null,
|
||||||
"code":"PGRST108",
|
"code":"PGRST108",
|
||||||
"message":"'amiga_projectsss' is not an embedded resource in this request"}|]
|
"message":"Cannot apply filter because 'amiga_projectsss' is not an embedded resource in this request"}|]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
@@ -309,7 +309,7 @@ spec actualPgVersion = do
|
|||||||
{"hint":"Verify that 'tasks2' is included in the 'select' query parameter.",
|
{"hint":"Verify that 'tasks2' is included in the 'select' query parameter.",
|
||||||
"details":null,
|
"details":null,
|
||||||
"code":"PGRST108",
|
"code":"PGRST108",
|
||||||
"message":"'tasks2' is not an embedded resource in this request"}|]
|
"message":"Cannot apply filter because 'tasks2' is not an embedded resource in this request"}|]
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
@@ -324,24 +324,14 @@ spec actualPgVersion = do
|
|||||||
[json|[{"id":1},{"id":2}]|]
|
[json|[{"id":1},{"id":2}]|]
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "matches with IS DISTINCT FROM" $
|
|
||||||
get "/no_pk?select=a&a=isdistinct.2" `shouldRespondWith`
|
|
||||||
[json|[{"a":null},{"a":"1"}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "matches with IS DISTINCT FROM using not operator" $
|
|
||||||
get "/no_pk?select=a&a=not.isdistinct.2" `shouldRespondWith`
|
|
||||||
[json|[{"a":"2"}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
describe "Shaping response with select parameter" $ do
|
describe "Shaping response with select parameter" $ do
|
||||||
it "selectStar works in absense of parameter" $
|
it "selectStar works in absense of parameter" $
|
||||||
get "/complex_items?id=eq.3" `shouldRespondWith`
|
get "/complex_items?id=eq.3" `shouldRespondWith`
|
||||||
[json|[{"id":3,"name":"Three","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3],"field-with_sep":3}]|]
|
[json|[{"id":3,"name":"Three","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3],"field-with_sep":1}]|]
|
||||||
|
|
||||||
it "dash `-` in column names is accepted" $
|
it "dash `-` in column names is accepted" $
|
||||||
get "/complex_items?id=eq.3&select=id,field-with_sep" `shouldRespondWith`
|
get "/complex_items?id=eq.3&select=id,field-with_sep" `shouldRespondWith`
|
||||||
[json|[{"id":3,"field-with_sep":3}]|]
|
[json|[{"id":3,"field-with_sep":1}]|]
|
||||||
|
|
||||||
it "one simple column" $
|
it "one simple column" $
|
||||||
get "/complex_items?select=id" `shouldRespondWith`
|
get "/complex_items?select=id" `shouldRespondWith`
|
||||||
@@ -931,12 +921,50 @@ spec actualPgVersion = do
|
|||||||
get "/projects?id=eq.1&select=id, name, clients(id, name)&clients.order=name.asc" `shouldRespondWith`
|
get "/projects?id=eq.1&select=id, name, clients(id, name)&clients.order=name.asc" `shouldRespondWith`
|
||||||
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"}}]|]
|
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"}}]|]
|
||||||
|
|
||||||
it "gives meaningful error message on bad syntax" $ do
|
context "order syntax errors" $ do
|
||||||
get "/items?order=id.asc.nullslasttt" `shouldRespondWith`
|
it "gives meaningful error messages when asc/desc/nulls{first,last} are misspelled" $ do
|
||||||
[json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.asc.nullslasttt)\" (line 1, column 17)","code":"PGRST100","hint":null}|]
|
get "/items?order=id.ac" `shouldRespondWith`
|
||||||
{ matchStatus = 400
|
[json|{"details":"unexpected \"c\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.ac)\" (line 1, column 4)","code":"PGRST100","hint":null}|]
|
||||||
, matchHeaders = [matchContentTypeJson]
|
{ matchStatus = 400
|
||||||
}
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
get "/items?order=id.descc" `shouldRespondWith`
|
||||||
|
[json|{"details":"unexpected 'c' expecting delimiter (.), \",\" or end of input","message":"\"failed to parse order (id.descc)\" (line 1, column 8)","code":"PGRST100","hint":null}|]
|
||||||
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
get "/items?order=id.nulsfist" `shouldRespondWith`
|
||||||
|
[json|{"details":"unexpected \"n\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.nulsfist)\" (line 1, column 4)","code":"PGRST100","hint":null}|]
|
||||||
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
get "/items?order=id.nullslasttt" `shouldRespondWith`
|
||||||
|
[json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.nullslasttt)\" (line 1, column 13)","code":"PGRST100","hint":null}|]
|
||||||
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
get "/items?order=id.smth34" `shouldRespondWith`
|
||||||
|
[json|{"details":"unexpected \"s\" expecting \"asc\", \"desc\", \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.smth34)\" (line 1, column 4)","code":"PGRST100","hint":null}|]
|
||||||
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "gives meaningful error messages when nulls{first,last} are misspelled after asc/desc" $ do
|
||||||
|
get "/items?order=id.asc.nlsfst" `shouldRespondWith`
|
||||||
|
[json|{"details":"unexpected \"l\" expecting \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.asc.nlsfst)\" (line 1, column 8)","code":"PGRST100","hint":null}|]
|
||||||
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
get "/items?order=id.asc.nullslasttt" `shouldRespondWith`
|
||||||
|
[json|{"details":"unexpected 't' expecting \",\" or end of input","message":"\"failed to parse order (id.asc.nullslasttt)\" (line 1, column 17)","code":"PGRST100","hint":null}|]
|
||||||
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
get "/items?order=id.asc.smth34" `shouldRespondWith`
|
||||||
|
[json|{"details":"unexpected \"s\" expecting \"nullsfirst\" or \"nullslast\"","message":"\"failed to parse order (id.asc.smth34)\" (line 1, column 8)","code":"PGRST100","hint":null}|]
|
||||||
|
{ matchStatus = 400
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
describe "Accept headers" $ do
|
describe "Accept headers" $ do
|
||||||
it "should respond an unknown accept type with 415" $
|
it "should respond an unknown accept type with 415" $
|
||||||
@@ -1012,8 +1040,7 @@ spec actualPgVersion = do
|
|||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "fails if an operator is not given" $
|
it "fails if an operator is not given" $
|
||||||
get "/ghostBusters?id=0" `shouldRespondWith`
|
get "/ghostBusters?id=0" `shouldRespondWith` [json| {"details":"Failed to parse [(\"id\",\"0\")]","message":"Unexpected param or filter missing operator","code":"PGRST104","hint":null} |]
|
||||||
[json| {"code":"PGRST100","details":"unexpected \"0\" expecting \"not\" or operator (eq, gt, ...)","hint":null,"message":"\"failed to parse filter (0)\" (line 1, column 1)"} |]
|
|
||||||
{ matchStatus = 400
|
{ matchStatus = 400
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
@@ -1235,96 +1262,3 @@ spec actualPgVersion = do
|
|||||||
liftIO $ do
|
liftIO $ do
|
||||||
let respHeaders = simpleHeaders r
|
let respHeaders = simpleHeaders r
|
||||||
respHeaders `shouldSatisfy` noProfileHeader
|
respHeaders `shouldSatisfy` noProfileHeader
|
||||||
|
|
||||||
context "empty embed" $ do
|
|
||||||
it "works on a many-to-one relationship" $ do
|
|
||||||
get "/projects?select=id,name,clients()" `shouldRespondWith`
|
|
||||||
[json| [
|
|
||||||
{"id":1,"name":"Windows 7"},
|
|
||||||
{"id":2,"name":"Windows 10"},
|
|
||||||
{"id":3,"name":"IOS"},
|
|
||||||
{"id":4,"name":"OSX"},
|
|
||||||
{"id":5,"name":"Orphan"}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
get "/projects?select=id,name,clients!inner()&clients.id=eq.2" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":3,"name":"IOS"},
|
|
||||||
{"id":4,"name":"OSX"}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "works on a one-to-many relationship" $ do
|
|
||||||
get "/clients?select=id,name,projects()" `shouldRespondWith`
|
|
||||||
[json| [{"id":1,"name":"Microsoft"}, {"id":2,"name":"Apple"}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
get "/clients?select=id,name,projects!inner()&projects.name=eq.IOS" `shouldRespondWith`
|
|
||||||
[json|[{"id":2,"name":"Apple"}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "works on a many-to-many relationship" $ do
|
|
||||||
get "/users?select=*,tasks!inner()" `shouldRespondWith`
|
|
||||||
[json| [{"id":1,"name":"Angela Martin"}, {"id":2,"name":"Michael Scott"}, {"id":3,"name":"Dwight Schrute"}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
get "/users?select=*,tasks!inner()&tasks.id=eq.3" `shouldRespondWith`
|
|
||||||
[json|[{"id":1,"name":"Angela Martin"}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
context "empty root select" $
|
|
||||||
it "gives all columns" $ do
|
|
||||||
get "/projects?select=" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":1,"name":"Windows 7","client_id":1},
|
|
||||||
{"id":2,"name":"Windows 10","client_id":1},
|
|
||||||
{"id":3,"name":"IOS","client_id":2},
|
|
||||||
{"id":4,"name":"OSX","client_id":2},
|
|
||||||
{"id":5,"name":"Orphan","client_id":null}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
get "/rpc/getallprojects?select=" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":1,"name":"Windows 7","client_id":1},
|
|
||||||
{"id":2,"name":"Windows 10","client_id":1},
|
|
||||||
{"id":3,"name":"IOS","client_id":2},
|
|
||||||
{"id":4,"name":"OSX","client_id":2},
|
|
||||||
{"id":5,"name":"Orphan","client_id":null}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
context "any/all quantifiers" $ do
|
|
||||||
it "works with the eq operator" $
|
|
||||||
get "/projects?id=eq(any).{3,4,5}" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":3,"name":"IOS","client_id":2},
|
|
||||||
{"id":4,"name":"OSX","client_id":2},
|
|
||||||
{"id":5,"name":"Orphan","client_id":null}
|
|
||||||
]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "works with the gt/gte operator" $ do
|
|
||||||
get "/projects?id=gt(all).{4,3}" `shouldRespondWith`
|
|
||||||
[json|[{"id":5,"name":"Orphan","client_id":null}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
get "/projects?id=gte(all).{4,3}" `shouldRespondWith`
|
|
||||||
[json|[{"id":4,"name":"OSX","client_id":2}, {"id":5,"name":"Orphan","client_id":null}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "works with the lt/lte operator" $ do
|
|
||||||
get "/projects?id=lt(all).{4,3}" `shouldRespondWith`
|
|
||||||
[json|[{"id":1,"name":"Windows 7","client_id":1}, {"id":2,"name":"Windows 10","client_id":1}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
get "/projects?id=lte(all).{4,3}" `shouldRespondWith`
|
|
||||||
[json|[{"id":1,"name":"Windows 7","client_id":1}, {"id":2,"name":"Windows 10","client_id":1}, {"id":3,"name":"IOS","client_id":2}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "works with the like/ilike operator" $ do
|
|
||||||
get "/articles?body=like(any).{%plan%,%brain%}&select=id" `shouldRespondWith`
|
|
||||||
[json|[ {"id":1}, {"id":2} ]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
get "/articles?body=ilike(all).{%plan%,%greatness%}&select=id" `shouldRespondWith`
|
|
||||||
[json|[ {"id":1} ]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|
||||||
it "works with the match/imatch operator" $ do
|
|
||||||
get "/articles?body=match(any).{stop,thing}&select=id" `shouldRespondWith`
|
|
||||||
[json|[{"id":1}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
get "/articles?body=imatch(any).{stop,thing}&select=id" `shouldRespondWith`
|
|
||||||
[json|[{"id":1}, {"id":2}]|]
|
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
module Feature.Query.RangeSpec where
|
module Feature.Query.RangeSpec where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
|
||||||
import Network.Wai (Application)
|
import Network.Wai (Application)
|
||||||
import Network.Wai.Test (SResponse (simpleHeaders, simpleStatus))
|
import Network.Wai.Test (SResponse (simpleHeaders, simpleStatus))
|
||||||
|
|
||||||
@@ -11,29 +13,36 @@ import Test.Hspec.Wai.JSON
|
|||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
|
defaultRange :: BL.ByteString
|
||||||
|
defaultRange = [json| { "min": 0, "max": 15 } |]
|
||||||
|
|
||||||
|
emptyRange :: BL.ByteString
|
||||||
|
emptyRange = [json| { "min": 2, "max": 2 } |]
|
||||||
|
|
||||||
spec :: SpecWith ((), Application)
|
spec :: SpecWith ((), Application)
|
||||||
spec = do
|
spec = do
|
||||||
describe "GET /rpc/getitemrange" $ do
|
describe "POST /rpc/getitemrange" $ do
|
||||||
context "without range headers" $ do
|
context "without range headers" $ do
|
||||||
context "with response under server size limit" $
|
context "with response under server size limit" $
|
||||||
it "returns whole range with status 200" $
|
it "returns whole range with status 200" $
|
||||||
get "/rpc/getitemrange?min=0&max=15" `shouldRespondWith` 200
|
post "/rpc/getitemrange" defaultRange `shouldRespondWith` 200
|
||||||
|
|
||||||
context "when I don't want the count" $ do
|
context "when I don't want the count" $ do
|
||||||
it "returns range Content-Range with */* for empty range" $
|
it "returns range Content-Range with */* for empty range" $
|
||||||
get "/rpc/getitemrange?min=2&max=2"
|
request methodPost "/rpc/getitemrange" [] emptyRange
|
||||||
`shouldRespondWith` [json| [] |] {matchHeaders = ["Content-Range" <:> "*/*"]}
|
`shouldRespondWith` [json| [] |] {matchHeaders = ["Content-Range" <:> "*/*"]}
|
||||||
|
|
||||||
it "returns range Content-Range with range/*" $
|
it "returns range Content-Range with range/*" $
|
||||||
get "/rpc/getitemrange?order=id&min=0&max=15"
|
post "/rpc/getitemrange?order=id"
|
||||||
|
defaultRange
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
|
[json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
|
||||||
{ matchHeaders = ["Content-Range" <:> "0-14/*"] }
|
{ matchHeaders = ["Content-Range" <:> "0-14/*"] }
|
||||||
|
|
||||||
context "of invalid range" $ do
|
context "of invalid range" $ do
|
||||||
it "refuses a range with nonzero start when there are no items" $
|
it "refuses a range with nonzero start when there are no items" $
|
||||||
request methodGet "/rpc/getitemrange?offset=1&min=2&max=2"
|
request methodPost "/rpc/getitemrange?offset=1"
|
||||||
[("Prefer", "count=exact")] mempty
|
[("Prefer", "count=exact")] emptyRange
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"message":"Requested range not satisfiable",
|
"message":"Requested range not satisfiable",
|
||||||
@@ -46,8 +55,8 @@ spec = do
|
|||||||
}
|
}
|
||||||
|
|
||||||
it "refuses a range requesting start past last item" $
|
it "refuses a range requesting start past last item" $
|
||||||
request methodGet "/rpc/getitemrange?offset=100&min=0&max=15"
|
request methodPost "/rpc/getitemrange?offset=100"
|
||||||
[("Prefer", "count=exact")] mempty
|
[("Prefer", "count=exact")] defaultRange
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"message":"Requested range not satisfiable",
|
"message":"Requested range not satisfiable",
|
||||||
@@ -62,37 +71,37 @@ spec = do
|
|||||||
context "with range headers" $ do
|
context "with range headers" $ do
|
||||||
context "of acceptable range" $ do
|
context "of acceptable range" $ do
|
||||||
it "succeeds with partial content" $ do
|
it "succeeds with partial content" $ do
|
||||||
r <- request methodGet "/rpc/getitemrange?min=0&max=15"
|
r <- request methodPost "/rpc/getitemrange"
|
||||||
(rangeHdrs $ ByteRangeFromTo 0 1) mempty
|
(rangeHdrs $ ByteRangeFromTo 0 1) defaultRange
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy`
|
||||||
matchHeader "Content-Range" "0-1/*"
|
matchHeader "Content-Range" "0-1/*"
|
||||||
simpleStatus r `shouldBe` ok200
|
simpleStatus r `shouldBe` ok200
|
||||||
|
|
||||||
it "understands open-ended ranges" $
|
it "understands open-ended ranges" $
|
||||||
request methodGet "/rpc/getitemrange?min=0&max=15"
|
request methodPost "/rpc/getitemrange"
|
||||||
(rangeHdrs $ ByteRangeFrom 0) mempty
|
(rangeHdrs $ ByteRangeFrom 0) defaultRange
|
||||||
`shouldRespondWith` 200
|
`shouldRespondWith` 200
|
||||||
|
|
||||||
it "returns an empty body when there are no results" $
|
it "returns an empty body when there are no results" $
|
||||||
request methodGet "/rpc/getitemrange?min=2&max=2"
|
request methodPost "/rpc/getitemrange"
|
||||||
(rangeHdrs $ ByteRangeFromTo 0 1) mempty
|
(rangeHdrs $ ByteRangeFromTo 0 1) emptyRange
|
||||||
`shouldRespondWith` "[]"
|
`shouldRespondWith` "[]"
|
||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "*/*"]
|
, matchHeaders = ["Content-Range" <:> "*/*"]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "allows one-item requests" $ do
|
it "allows one-item requests" $ do
|
||||||
r <- request methodGet "/rpc/getitemrange?min=0&max=15"
|
r <- request methodPost "/rpc/getitemrange"
|
||||||
(rangeHdrs $ ByteRangeFromTo 0 0) mempty
|
(rangeHdrs $ ByteRangeFromTo 0 0) defaultRange
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy`
|
||||||
matchHeader "Content-Range" "0-0/*"
|
matchHeader "Content-Range" "0-0/*"
|
||||||
simpleStatus r `shouldBe` ok200
|
simpleStatus r `shouldBe` ok200
|
||||||
|
|
||||||
it "handles ranges beyond collection length via truncation" $ do
|
it "handles ranges beyond collection length via truncation" $ do
|
||||||
r <- request methodGet "/rpc/getitemrange?min=0&max=15"
|
r <- request methodPost "/rpc/getitemrange"
|
||||||
(rangeHdrs $ ByteRangeFromTo 10 100) mempty
|
(rangeHdrs $ ByteRangeFromTo 10 100) defaultRange
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleHeaders r `shouldSatisfy`
|
simpleHeaders r `shouldSatisfy`
|
||||||
matchHeader "Content-Range" "10-14/*"
|
matchHeader "Content-Range" "10-14/*"
|
||||||
@@ -100,8 +109,8 @@ spec = do
|
|||||||
|
|
||||||
context "of invalid range" $ do
|
context "of invalid range" $ do
|
||||||
it "fails with 416 for offside range" $
|
it "fails with 416 for offside range" $
|
||||||
request methodGet "/rpc/getitemrange?min=2&max=2"
|
request methodPost "/rpc/getitemrange"
|
||||||
(rangeHdrs $ ByteRangeFromTo 1 0) mempty
|
(rangeHdrs $ ByteRangeFromTo 1 0) emptyRange
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"message":"Requested range not satisfiable",
|
"message":"Requested range not satisfiable",
|
||||||
@@ -112,8 +121,8 @@ spec = do
|
|||||||
{ matchStatus = 416 }
|
{ matchStatus = 416 }
|
||||||
|
|
||||||
it "refuses a range with nonzero start when there are no items" $
|
it "refuses a range with nonzero start when there are no items" $
|
||||||
request methodGet "/rpc/getitemrange?min=2&max=2"
|
request methodPost "/rpc/getitemrange"
|
||||||
(rangeHdrsWithCount $ ByteRangeFromTo 1 2) mempty
|
(rangeHdrsWithCount $ ByteRangeFromTo 1 2) emptyRange
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"message":"Requested range not satisfiable",
|
"message":"Requested range not satisfiable",
|
||||||
@@ -126,8 +135,8 @@ spec = do
|
|||||||
}
|
}
|
||||||
|
|
||||||
it "refuses a range requesting start past last item" $
|
it "refuses a range requesting start past last item" $
|
||||||
request methodGet "/rpc/getitemrange?min=0&max=15"
|
request methodPost "/rpc/getitemrange"
|
||||||
(rangeHdrsWithCount $ ByteRangeFromTo 100 199) mempty
|
(rangeHdrsWithCount $ ByteRangeFromTo 100 199) defaultRange
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {
|
[json| {
|
||||||
"message":"Requested range not satisfiable",
|
"message":"Requested range not satisfiable",
|
||||||
|
|||||||
@@ -1,256 +0,0 @@
|
|||||||
module Feature.Query.RelatedQueriesSpec where
|
|
||||||
|
|
||||||
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 "related queries" $ do
|
|
||||||
context "related orders" $ do
|
|
||||||
it "works on a many-to-one relationship" $ do
|
|
||||||
get "/projects?select=id,clients(name)&order=clients(name).nullsfirst" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":5,"clients":null},
|
|
||||||
{"id":3,"clients":{"name":"Apple"}},
|
|
||||||
{"id":4,"clients":{"name":"Apple"}},
|
|
||||||
{"id":1,"clients":{"name":"Microsoft"}},
|
|
||||||
{"id":2,"clients":{"name":"Microsoft"}} ]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/projects?select=id,client:clients(name)&order=client(name).asc" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":3,"client":{"name":"Apple"}},
|
|
||||||
{"id":4,"client":{"name":"Apple"}},
|
|
||||||
{"id":1,"client":{"name":"Microsoft"}},
|
|
||||||
{"id":2,"client":{"name":"Microsoft"}},
|
|
||||||
{"id":5,"client":null} ]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/videogames?select=id,computed_designers(id)&order=computed_designers(id).desc" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":3,"computed_designers":{"id":2}},
|
|
||||||
{"id":4,"computed_designers":{"id":2}},
|
|
||||||
{"id":1,"computed_designers":{"id":1}},
|
|
||||||
{"id":2,"computed_designers":{"id":1}}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "works on a one-to-one relationship and jsonb column" $ do
|
|
||||||
get "/trash?select=id,trash_details(id,jsonb_col)&order=trash_details(jsonb_col->key).asc" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":2,"trash_details":{"id":2,"jsonb_col":{"key": 6}}},
|
|
||||||
{"id":3,"trash_details":{"id":3,"jsonb_col":{"key": 8}}},
|
|
||||||
{"id":1,"trash_details":{"id":1,"jsonb_col":{"key": 10}}}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/trash?select=id,trash_details(id,jsonb_col)&order=trash_details(jsonb_col->key).desc" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":1,"trash_details":{"id":1,"jsonb_col":{"key": 10}}},
|
|
||||||
{"id":3,"trash_details":{"id":3,"jsonb_col":{"key": 8}}},
|
|
||||||
{"id":2,"trash_details":{"id":2,"jsonb_col":{"key": 6}}}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "works on an embedded resource" $ do
|
|
||||||
get "/users?select=name,tasks(id,name,projects(id,name))&tasks.order=projects(id).desc&limit=1" `shouldRespondWith`
|
|
||||||
[json| [{
|
|
||||||
"name":"Angela Martin",
|
|
||||||
"tasks":[
|
|
||||||
{"id": 3, "name":"Design w10","projects":{"id":2,"name":"Windows 10"}},
|
|
||||||
{"id": 4, "name":"Code w10","projects":{"id":2,"name":"Windows 10"}},
|
|
||||||
{"id": 1, "name":"Design w7","projects":{"id":1,"name":"Windows 7"}},
|
|
||||||
{"id": 2, "name":"Code w7","projects":{"id":1,"name":"Windows 7"}}
|
|
||||||
]
|
|
||||||
}]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/users?select=name,tasks(id,name,projects(id,name))&tasks.order=projects(id).desc,name&limit=1" `shouldRespondWith`
|
|
||||||
[json| [{
|
|
||||||
"name":"Angela Martin",
|
|
||||||
"tasks":[
|
|
||||||
{"id": 4, "name":"Code w10","projects":{"id":2,"name":"Windows 10"}},
|
|
||||||
{"id": 3, "name":"Design w10","projects":{"id":2,"name":"Windows 10"}},
|
|
||||||
{"id": 2, "name":"Code w7","projects":{"id":1,"name":"Windows 7"}},
|
|
||||||
{"id": 1, "name":"Design w7","projects":{"id":1,"name":"Windows 7"}}
|
|
||||||
]
|
|
||||||
}]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/users?select=name,tasks(id,name,projects(id,name))&tasks.order=projects(id).asc&limit=1" `shouldRespondWith`
|
|
||||||
[json|[{
|
|
||||||
"name":"Angela Martin",
|
|
||||||
"tasks":[
|
|
||||||
{"id":1,"name":"Design w7","projects":{"id":1,"name":"Windows 7"}},
|
|
||||||
{"id":2,"name":"Code w7","projects":{"id":1,"name":"Windows 7"}},
|
|
||||||
{"id":3,"name":"Design w10","projects":{"id":2,"name":"Windows 10"}},
|
|
||||||
{"id":4,"name":"Code w10","projects":{"id":2,"name":"Windows 10"}}
|
|
||||||
]
|
|
||||||
}]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "fails when is not a to-one relationship" $ do
|
|
||||||
get "/clients?select=*,projects(*)&order=projects(id)" `shouldRespondWith`
|
|
||||||
[json|{
|
|
||||||
"code":"PGRST118",
|
|
||||||
"details":"'clients' and 'projects' do not form a many-to-one or one-to-one relationship",
|
|
||||||
"hint":null,
|
|
||||||
"message":"A related order on 'projects' is not possible"
|
|
||||||
}|]
|
|
||||||
{ matchStatus = 400
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/clients?select=*,pros:projects(*)&order=pros(id)" `shouldRespondWith`
|
|
||||||
[json|{
|
|
||||||
"code":"PGRST118",
|
|
||||||
"details":"'clients' and 'pros' do not form a many-to-one or one-to-one relationship",
|
|
||||||
"hint":null,
|
|
||||||
"message":"A related order on 'pros' is not possible"
|
|
||||||
}|]
|
|
||||||
{ matchStatus = 400
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/designers?select=id,computed_videogames(id)&order=computed_videogames(id).desc" `shouldRespondWith`
|
|
||||||
[json|{
|
|
||||||
"code":"PGRST118",
|
|
||||||
"details":"'designers' and 'computed_videogames' do not form a many-to-one or one-to-one relationship",
|
|
||||||
"hint":null,
|
|
||||||
"message":"A related order on 'computed_videogames' is not possible"
|
|
||||||
}|]
|
|
||||||
{ matchStatus = 400
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "fails when the resource is not embedded" $
|
|
||||||
get "/projects?select=id,clients(name)&order=clientsx(name).nullsfirst" `shouldRespondWith`
|
|
||||||
[json|{
|
|
||||||
"code":"PGRST108",
|
|
||||||
"details":null,
|
|
||||||
"hint":"Verify that 'clientsx' is included in the 'select' query parameter.",
|
|
||||||
"message":"'clientsx' is not an embedded resource in this request"
|
|
||||||
}|]
|
|
||||||
{ matchStatus = 400
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
context "related conditions through null operator on embed" $ do
|
|
||||||
it "works on a many-to-one relationship" $ do
|
|
||||||
get "/projects?select=name,clients()&clients=not.is.null" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"name":"Windows 7"},
|
|
||||||
{"name":"Windows 10"},
|
|
||||||
{"name":"IOS"},
|
|
||||||
{"name":"OSX"}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/projects?select=name,clients()&clients=is.null" `shouldRespondWith`
|
|
||||||
[json|[{"name":"Orphan"}]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/projects?select=name,computed_clients()&computed_clients=is.null" `shouldRespondWith`
|
|
||||||
[json|[{"name":"Orphan"}]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "works on a one-to-many relationship" $ do
|
|
||||||
get "/entities?select=name,child_entities()&child_entities=not.is.null" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"name":"entity 1"},
|
|
||||||
{"name":"entity 2"}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/entities?select=name,child_entities()&child_entities=is.null" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"name":"entity 3"},
|
|
||||||
{"name":null}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/entities?select=name,childs:child_entities()&childs=is.null" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"name":"entity 3"},
|
|
||||||
{"name":null}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "works on a many-to-many relationship" $ do
|
|
||||||
get "/users?select=name,tasks()&tasks.id=eq.1&tasks=not.is.null" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"name":"Angela Martin"},
|
|
||||||
{"name":"Dwight Schrute"}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/users?select=name,tasks()&tasks.id=eq.1&tasks=is.null" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"name":"Michael Scott"}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "works on nested embeds" $ do
|
|
||||||
get "/entities?select=name,child_entities(name,grandchild_entities())&child_entities.grandchild_entities=not.is.null&child_entities=not.is.null" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"name":"entity 1","child_entities":[{"name":"child entity 1"}, {"name":"child entity 2"}]}]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "can do an or across embeds" $
|
|
||||||
get "/client?select=*,clientinfo(),contact()&clientinfo.other=ilike.*main*&contact.name=ilike.*tabby*&or=(clientinfo.not.is.null,contact.not.is.null)" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":1,"name":"Walmart"},
|
|
||||||
{"id":2,"name":"Target"}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "only works with is null or is not null operators" $
|
|
||||||
get "/projects?select=name,clients(*)&clients=eq.3" `shouldRespondWith`
|
|
||||||
[json|{
|
|
||||||
"code":"PGRST120",
|
|
||||||
"details":"Only is null or not is null filters are allowed on embedded resources",
|
|
||||||
"hint":null,
|
|
||||||
"message":"Bad operator on the 'clients' embedded resource"
|
|
||||||
}|]
|
|
||||||
{ matchStatus = 400
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "doesn't interfere filtering when embedding using the column name" $
|
|
||||||
get "/projects?select=name,client_id,client:client_id(name)&client_id=eq.2" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"name":"IOS","client_id":2,"client":{"name":"Apple"}},
|
|
||||||
{"name":"OSX","client_id":2,"client":{"name":"Apple"}}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
@@ -24,65 +24,50 @@ spec :: PgVersion -> SpecWith ((), Application)
|
|||||||
spec actualPgVersion =
|
spec actualPgVersion =
|
||||||
describe "remote procedure call" $ do
|
describe "remote procedure call" $ do
|
||||||
context "a proc that returns a set" $ do
|
context "a proc that returns a set" $ do
|
||||||
context "returns paginated results" $ do
|
it "returns paginated results" $ do
|
||||||
it "using the Range header" $
|
request methodPost "/rpc/getitemrange"
|
||||||
request methodGet "/rpc/getitemrange?min=2&max=4"
|
(rangeHdrs (ByteRangeFromTo 0 0)) [json| { "min": 2, "max": 4 } |]
|
||||||
(rangeHdrs (ByteRangeFromTo 1 1)) mempty
|
`shouldRespondWith` [json| [{"id":3}] |]
|
||||||
`shouldRespondWith` [json| [{"id":4}] |]
|
{ matchStatus = 200
|
||||||
{ matchStatus = 200
|
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||||
, matchHeaders = ["Content-Range" <:> "1-1/*"]
|
}
|
||||||
}
|
request methodGet "/rpc/getitemrange?min=2&max=4"
|
||||||
|
(rangeHdrs (ByteRangeFromTo 0 0)) ""
|
||||||
|
`shouldRespondWith` [json| [{"id":3}] |]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||||
|
}
|
||||||
|
request methodHead "/rpc/getitemrange?min=2&max=4"
|
||||||
|
(rangeHdrs (ByteRangeFromTo 0 0)) ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
""
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [ matchContentTypeJson
|
||||||
|
, "Content-Range" <:> "0-0/*" ]
|
||||||
|
}
|
||||||
|
|
||||||
it "using limit and offset" $ do
|
it "includes total count if requested" $ do
|
||||||
post "/rpc/getitemrange?limit=1&offset=1" [json| { "min": 2, "max": 4 } |]
|
request methodPost "/rpc/getitemrange"
|
||||||
`shouldRespondWith` [json| [{"id":4}] |]
|
(rangeHdrsWithCount (ByteRangeFromTo 0 0))
|
||||||
{ matchStatus = 200
|
[json| { "min": 2, "max": 4 } |]
|
||||||
, matchHeaders = ["Content-Range" <:> "1-1/*"]
|
`shouldRespondWith` [json| [{"id":3}] |]
|
||||||
}
|
{ matchStatus = 206 -- it now knows the response is partial
|
||||||
get "/rpc/getitemrange?min=2&max=4&limit=1&offset=1"
|
, matchHeaders = ["Content-Range" <:> "0-0/2"]
|
||||||
`shouldRespondWith` [json| [{"id":4}] |]
|
}
|
||||||
{ matchStatus = 200
|
request methodGet "/rpc/getitemrange?min=2&max=4"
|
||||||
, matchHeaders = ["Content-Range" <:> "1-1/*"]
|
(rangeHdrsWithCount (ByteRangeFromTo 0 0)) ""
|
||||||
}
|
`shouldRespondWith` [json| [{"id":3}] |]
|
||||||
request methodHead "/rpc/getitemrange?min=2&max=4&limit=1&offset=1" mempty mempty
|
{ matchStatus = 206
|
||||||
`shouldRespondWith`
|
, matchHeaders = ["Content-Range" <:> "0-0/2"]
|
||||||
""
|
}
|
||||||
{ matchStatus = 200
|
request methodHead "/rpc/getitemrange?min=2&max=4"
|
||||||
, matchHeaders = [ matchContentTypeJson
|
(rangeHdrsWithCount (ByteRangeFromTo 0 0)) ""
|
||||||
, "Content-Range" <:> "1-1/*" ]
|
`shouldRespondWith`
|
||||||
}
|
""
|
||||||
|
{ matchStatus = 206
|
||||||
context "includes total count if requested" $ do
|
, matchHeaders = [ matchContentTypeJson
|
||||||
it "using the Range header" $
|
, "Content-Range" <:> "0-0/2" ]
|
||||||
request methodGet "/rpc/getitemrange?min=2&max=4"
|
}
|
||||||
(rangeHdrsWithCount (ByteRangeFromTo 1 1)) ""
|
|
||||||
`shouldRespondWith` [json| [{"id":4}] |]
|
|
||||||
{ matchStatus = 206 -- it now knows the response is partial
|
|
||||||
, matchHeaders = ["Content-Range" <:> "1-1/2"]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "using limit and offset" $ do
|
|
||||||
request methodPost "/rpc/getitemrange?limit=1&offset=1"
|
|
||||||
[("Prefer", "count=exact")]
|
|
||||||
[json| { "min": 2, "max": 4 } |]
|
|
||||||
`shouldRespondWith` [json| [{"id":4}] |]
|
|
||||||
{ matchStatus = 206 -- it now knows the response is partial
|
|
||||||
, matchHeaders = ["Content-Range" <:> "1-1/2"]
|
|
||||||
}
|
|
||||||
request methodGet "/rpc/getitemrange?min=2&max=4&limit=1&offset=1"
|
|
||||||
[("Prefer", "count=exact")] mempty
|
|
||||||
`shouldRespondWith` [json| [{"id":4}] |]
|
|
||||||
{ matchStatus = 206
|
|
||||||
, matchHeaders = ["Content-Range" <:> "1-1/2"]
|
|
||||||
}
|
|
||||||
request methodHead "/rpc/getitemrange?min=2&max=4&limit=1&offset=1"
|
|
||||||
[("Prefer", "count=exact")] mempty
|
|
||||||
`shouldRespondWith`
|
|
||||||
""
|
|
||||||
{ matchStatus = 206
|
|
||||||
, matchHeaders = [ matchContentTypeJson
|
|
||||||
, "Content-Range" <:> "1-1/2" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "includes exact count if requested" $ do
|
it "includes exact count if requested" $ do
|
||||||
request methodHead "/rpc/getallprojects"
|
request methodHead "/rpc/getallprojects"
|
||||||
@@ -128,58 +113,6 @@ spec actualPgVersion =
|
|||||||
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
|
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
|
||||||
}
|
}
|
||||||
|
|
||||||
context "ignores Range header when method is different than GET" $ do
|
|
||||||
it "without limit and offset" $ do
|
|
||||||
request methodPost "/rpc/getitemrange"
|
|
||||||
(rangeHdrsWithCount (ByteRangeFromTo 1 1))
|
|
||||||
[json| { "min": 2, "max": 4 } |]
|
|
||||||
`shouldRespondWith` [json| [{"id": 3}, {"id": 4}] |]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
|
||||||
}
|
|
||||||
request methodHead "/rpc/getitemrange?min=2&max=4"
|
|
||||||
(rangeHdrsWithCount (ByteRangeFromTo 1 1)) ""
|
|
||||||
`shouldRespondWith`
|
|
||||||
""
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [ matchContentTypeJson
|
|
||||||
, "Content-Range" <:> "0-1/2" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "with limit and offset" $ do
|
|
||||||
request methodPost "/rpc/getitemrange?limit=2&offset=1"
|
|
||||||
(rangeHdrsWithCount (ByteRangeFromTo 1 1))
|
|
||||||
[json| { "min": 2, "max": 5 } |]
|
|
||||||
`shouldRespondWith` [json| [{"id": 4}, {"id": 5}] |]
|
|
||||||
{ matchStatus = 206
|
|
||||||
, matchHeaders = ["Content-Range" <:> "1-2/3"]
|
|
||||||
}
|
|
||||||
request methodHead "/rpc/getitemrange?min=2&max=5&limit=2&offset=1"
|
|
||||||
(rangeHdrsWithCount (ByteRangeFromTo 1 1)) ""
|
|
||||||
`shouldRespondWith`
|
|
||||||
""
|
|
||||||
{ matchStatus = 206
|
|
||||||
, matchHeaders = [ matchContentTypeJson
|
|
||||||
, "Content-Range" <:> "1-2/3" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "does not throw an invalid range error" $ do
|
|
||||||
request methodPost "/rpc/getitemrange?limit=2&offset=1"
|
|
||||||
(rangeHdrsWithCount (ByteRangeFromTo 0 0))
|
|
||||||
[json| { "min": 2, "max": 5 } |]
|
|
||||||
`shouldRespondWith` [json| [{"id": 4}, {"id": 5}] |]
|
|
||||||
{ matchStatus = 206
|
|
||||||
, matchHeaders = ["Content-Range" <:> "1-2/3"]
|
|
||||||
}
|
|
||||||
request methodHead "/rpc/getitemrange?min=2&max=5&limit=2&offset=1"
|
|
||||||
(rangeHdrsWithCount (ByteRangeFromTo 0 0)) ""
|
|
||||||
`shouldRespondWith`
|
|
||||||
""
|
|
||||||
{ matchStatus = 206
|
|
||||||
, matchHeaders = [ matchContentTypeJson
|
|
||||||
, "Content-Range" <:> "1-2/3" ]
|
|
||||||
}
|
|
||||||
|
|
||||||
context "unknown function" $ do
|
context "unknown function" $ do
|
||||||
it "returns 404" $
|
it "returns 404" $
|
||||||
post "/rpc/fakefunc" [json| {} |] `shouldRespondWith` 404
|
post "/rpc/fakefunc" [json| {} |] `shouldRespondWith` 404
|
||||||
@@ -929,6 +862,42 @@ spec actualPgVersion =
|
|||||||
`shouldRespondWith` "3"
|
`shouldRespondWith` "3"
|
||||||
{ matchHeaders = [matchContentTypeJson] }
|
{ matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
context "bulk RPC with params=multiple-objects" $ do
|
||||||
|
it "works with a scalar function an returns a json array" $
|
||||||
|
request methodPost "/rpc/add_them" [("Prefer", "params=multiple-objects")]
|
||||||
|
[json|[
|
||||||
|
{"a": 1, "b": 2},
|
||||||
|
{"a": 4, "b": 6},
|
||||||
|
{"a": 100, "b": 200} ]|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[3, 10, 300]
|
||||||
|
|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
|
it "works with a scalar function an returns a json array when posting CSV" $
|
||||||
|
request methodPost "/rpc/add_them" [("Content-Type", "text/csv"), ("Prefer", "params=multiple-objects")]
|
||||||
|
"a,b\n1,2\n4,6\n100,200"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[3, 10, 300]
|
||||||
|
|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
|
it "works with a non-scalar result" $
|
||||||
|
request methodPost "/rpc/get_projects_below?select=id,name" [("Prefer", "params=multiple-objects")]
|
||||||
|
[json|[
|
||||||
|
{"id": 1},
|
||||||
|
{"id": 5} ]|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|
|
||||||
|
[{"id":1,"name":"Windows 7"},
|
||||||
|
{"id":2,"name":"Windows 10"},
|
||||||
|
{"id":3,"name":"IOS"},
|
||||||
|
{"id":4,"name":"OSX"}]
|
||||||
|
|] { matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
context "HTTP request env vars" $ do
|
context "HTTP request env vars" $ do
|
||||||
it "custom header is set" $
|
it "custom header is set" $
|
||||||
request methodPost "/rpc/get_guc_value"
|
request methodPost "/rpc/get_guc_value"
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
module Feature.Query.SpreadQueriesSpec where
|
|
||||||
|
|
||||||
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 "spread embeds" $ do
|
|
||||||
it "works on a many-to-one relationship" $ do
|
|
||||||
get "/projects?select=id,...clients(client_name:name)" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":1,"client_name":"Microsoft"},
|
|
||||||
{"id":2,"client_name":"Microsoft"},
|
|
||||||
{"id":3,"client_name":"Apple"},
|
|
||||||
{"id":4,"client_name":"Apple"},
|
|
||||||
{"id":5,"client_name":null}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/grandchild_entities?select=name,...child_entities(parent_name:name,...entities(grandparent_name:name))&limit=3" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"name":"grandchild entity 1","parent_name":"child entity 1","grandparent_name":"entity 1"},
|
|
||||||
{"name":"grandchild entity 2","parent_name":"child entity 1","grandparent_name":"entity 1"},
|
|
||||||
{"name":"grandchild entity 3","parent_name":"child entity 2","grandparent_name":"entity 1"}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/videogames?select=name,...computed_designers(designer_name:name)" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"name":"Civilization I","designer_name":"Sid Meier"},
|
|
||||||
{"name":"Civilization II","designer_name":"Sid Meier"},
|
|
||||||
{"name":"Final Fantasy I","designer_name":"Hironobu Sakaguchi"},
|
|
||||||
{"name":"Final Fantasy II","designer_name":"Hironobu Sakaguchi"}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "works inside a normal embed" $
|
|
||||||
get "/grandchild_entities?select=name,child_entity:child_entities(name,...entities(parent_name:name))&limit=1" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"name":"grandchild entity 1","child_entity":{"name":"child entity 1","parent_name":"entity 1"}}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "works on a one-to-one relationship" $
|
|
||||||
get "/country?select=name,...capital(capital:name)" `shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"name":"Afghanistan","capital":"Kabul"},
|
|
||||||
{"name":"Algeria","capital":"Algiers"}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "fails when is not a to-one relationship" $ do
|
|
||||||
get "/clients?select=*,...projects(*)" `shouldRespondWith`
|
|
||||||
[json|{
|
|
||||||
"code":"PGRST119",
|
|
||||||
"details":"'clients' and 'projects' do not form a many-to-one or one-to-one relationship",
|
|
||||||
"hint":null,
|
|
||||||
"message":"A spread operation on 'projects' is not possible"
|
|
||||||
}|]
|
|
||||||
{ matchStatus = 400
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/designers?select=*,...computed_videogames(*)" `shouldRespondWith`
|
|
||||||
[json|{
|
|
||||||
"code":"PGRST119",
|
|
||||||
"details":"'designers' and 'computed_videogames' do not form a many-to-one or one-to-one relationship",
|
|
||||||
"hint":null,
|
|
||||||
"message":"A spread operation on 'computed_videogames' is not possible"
|
|
||||||
}|]
|
|
||||||
{ matchStatus = 400
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "can include or exclude attributes of the junction on a m2m" $ do
|
|
||||||
get "/users?select=*,tasks:users_tasks(*,...tasks(*))&limit=1" `shouldRespondWith`
|
|
||||||
[json|[{
|
|
||||||
"id":1,"name":"Angela Martin",
|
|
||||||
"tasks": [
|
|
||||||
{"user_id":1,"task_id":1,"id":1,"name":"Design w7","project_id":1},
|
|
||||||
{"user_id":1,"task_id":2,"id":2,"name":"Code w7","project_id":1},
|
|
||||||
{"user_id":1,"task_id":3,"id":3,"name":"Design w10","project_id":2},
|
|
||||||
{"user_id":1,"task_id":4,"id":4,"name":"Code w10","project_id":2}
|
|
||||||
]
|
|
||||||
}]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
get "/users?select=*,tasks:users_tasks(...tasks(*))&limit=1" `shouldRespondWith`
|
|
||||||
[json|[{
|
|
||||||
"id":1,"name":"Angela Martin",
|
|
||||||
"tasks":[
|
|
||||||
{"id":1,"name":"Design w7","project_id":1},
|
|
||||||
{"id":2,"name":"Code w7","project_id":1},
|
|
||||||
{"id":3,"name":"Design w10","project_id":2},
|
|
||||||
{"id":4,"name":"Code w10","project_id":2}
|
|
||||||
]
|
|
||||||
}]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
@@ -308,256 +308,99 @@ spec = do
|
|||||||
request methodPatch "/articles?id=eq.2001&columns=body" [("Prefer", "return=representation")]
|
request methodPatch "/articles?id=eq.2001&columns=body" [("Prefer", "return=representation")]
|
||||||
[json| {"body": "Some real content", "smth": "here", "other": "stuff", "fake_id": 13} |] `shouldRespondWith` 200
|
[json| {"body": "Some real content", "smth": "here", "other": "stuff", "fake_id": 13} |] `shouldRespondWith` 200
|
||||||
|
|
||||||
it "disallows ?columns which don't exist" $ do
|
|
||||||
request methodPatch "/articles?id=eq.1&columns=helicopter"
|
|
||||||
[("Prefer", "return=representation")]
|
|
||||||
[json|{"body": "yyy"}|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|{"code":"PGRST204","details":null,"hint":null,"message":"Column 'helicopter' of relation 'articles' does not exist"} |]
|
|
||||||
{ matchStatus = 400
|
|
||||||
, matchHeaders = []
|
|
||||||
}
|
|
||||||
|
|
||||||
it "returns missing table error even if also has invalid ?columns" $ do
|
|
||||||
request methodPatch "/garlic?columns=helicopter"
|
|
||||||
[("Prefer", "return=representation")]
|
|
||||||
[json|[
|
|
||||||
{"id": 204, "body": "yyy"},
|
|
||||||
{"id": 205, "body": "zzz"}]|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|{} |]
|
|
||||||
{ matchStatus = 404
|
|
||||||
, matchHeaders = []
|
|
||||||
}
|
|
||||||
|
|
||||||
context "apply defaults on missing values" $ do
|
|
||||||
it "updates table using default values(field-with_sep) when json keys are undefined" $ do
|
|
||||||
request methodPatch "/complex_items?id=eq.3&columns=name,field-with_sep"
|
|
||||||
[("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
|
||||||
[json|{"name": "Tres"}|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":3,"name":"Tres","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3],"field-with_sep":1}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "updates with limit/offset using table default values(field-with_sep) when json keys are undefined" $ do
|
|
||||||
request methodPatch "/complex_items?select=id,name&columns=name,field-with_sep&limit=1&offset=2&order=id"
|
|
||||||
[("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
|
||||||
[json|{"name": "Tres"}|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":3,"name":"Tres"}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "updates table default values(field-with_sep) when json keys are undefined" $ do
|
|
||||||
request methodPatch "/complex_items?id=eq.3&columns=name,field-with_sep"
|
|
||||||
[("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
|
||||||
[json|{"name": "Tres"}|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":3,"name":"Tres","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3],"field-with_sep":1}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "updates view default values(field-with_sep) when json keys are undefined" $
|
|
||||||
request methodPatch "/complex_items_view?id=eq.3&columns=arr_data,name"
|
|
||||||
[("Prefer", "return=representation"), ("Prefer", "missing=default")]
|
|
||||||
[json|
|
|
||||||
{"arr_data":null}
|
|
||||||
|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{"id":3,"name":"Default","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":null,"field-with_sep":3}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200
|
|
||||||
, matchHeaders = ["Preference-Applied" <:> "missing=default"]
|
|
||||||
}
|
|
||||||
|
|
||||||
context "tables with self reference foreign keys" $ do
|
context "tables with self reference foreign keys" $ do
|
||||||
context "embeds children after update" $ do
|
it "embeds children after update" $
|
||||||
it "without filters" $
|
request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name)"
|
||||||
request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name)"
|
[("Prefer", "return=representation")]
|
||||||
[("Prefer", "return=representation")]
|
[json|{"name": "tardis-patched"}|]
|
||||||
[json|{"name": "tardis-patched"}|]
|
`shouldRespondWith`
|
||||||
`shouldRespondWith`
|
[json|
|
||||||
[json|
|
[ { "id": 0, "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ]
|
||||||
[ { "id": 0, "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ]
|
|]
|
||||||
|]
|
{ matchStatus = 200,
|
||||||
{ matchStatus = 200,
|
matchHeaders = [matchContentTypeJson]
|
||||||
matchHeaders = [matchContentTypeJson]
|
}
|
||||||
}
|
|
||||||
|
|
||||||
it "with filters" $
|
it "embeds parent, children and grandchildren after update" $
|
||||||
request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name)&web_content.name=like.f*"
|
request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name,web_content(name)),parent_content:p_web_id(name)"
|
||||||
[("Prefer", "return=representation")]
|
[("Prefer", "return=representation")]
|
||||||
[json|{"name": "tardis-patched"}|]
|
[json|{"name": "tardis-patched-2"}|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|
|
[json| [
|
||||||
[ { "id": 0, "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" } ]} ]
|
{
|
||||||
|]
|
"id": 0,
|
||||||
{ matchStatus = 200,
|
"name": "tardis-patched-2",
|
||||||
matchHeaders = [matchContentTypeJson]
|
"parent_content": { "name": "wat" },
|
||||||
|
"web_content": [
|
||||||
|
{ "name": "fezz", "web_content": [ { "name": "wut" } ] },
|
||||||
|
{ "name": "foo", "web_content": [] },
|
||||||
|
{ "name": "bar", "web_content": [] }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
] |]
|
||||||
|
{ matchStatus = 200,
|
||||||
|
matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
context "embeds parent, children and grandchildren after update" $ do
|
it "embeds children after update without explicitly including the id in the ?select" $
|
||||||
it "without filters" $
|
request methodPatch "/web_content?id=eq.0&select=name,web_content(name)"
|
||||||
request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name,web_content(name)),parent_content:p_web_id(name)"
|
[("Prefer", "return=representation")]
|
||||||
[("Prefer", "return=representation")]
|
[json|{"name": "tardis-patched"}|]
|
||||||
[json|{"name": "tardis-patched-2"}|]
|
`shouldRespondWith`
|
||||||
`shouldRespondWith`
|
[json|
|
||||||
[json| [
|
[ { "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ]
|
||||||
{
|
|]
|
||||||
"id": 0,
|
{ matchStatus = 200,
|
||||||
"name": "tardis-patched-2",
|
matchHeaders = [matchContentTypeJson]
|
||||||
"parent_content": { "name": "wat" },
|
}
|
||||||
"web_content": [
|
|
||||||
{ "name": "fezz", "web_content": [ { "name": "wut" } ] },
|
|
||||||
{ "name": "foo", "web_content": [] },
|
|
||||||
{ "name": "bar", "web_content": [] }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
] |]
|
|
||||||
{ matchStatus = 200,
|
|
||||||
matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "with filters" $
|
it "embeds an M2M relationship plus parent after update" $
|
||||||
request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name,web_content(name)),parent_content:p_web_id(name)&web_content.name=like.f*&web_content.web_content.id=eq.4&parent_content.name=neq.wat"
|
request methodPatch "/users?id=eq.1&select=name,tasks(name,project:projects(name))"
|
||||||
[("Prefer", "return=representation")]
|
[("Prefer", "return=representation")]
|
||||||
[json|{"name": "tardis-patched-2"}|]
|
[json|{"name": "Kevin Malone"}|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| [
|
[json|[
|
||||||
{
|
{
|
||||||
"id": 0,
|
"name": "Kevin Malone",
|
||||||
"name": "tardis-patched-2",
|
"tasks": [
|
||||||
"parent_content": null,
|
{ "name": "Design w7", "project": { "name": "Windows 7" } },
|
||||||
"web_content": [
|
{ "name": "Code w7", "project": { "name": "Windows 7" } },
|
||||||
{ "name": "fezz", "web_content": [ { "name": "wut" } ] },
|
{ "name": "Design w10", "project": { "name": "Windows 10" } },
|
||||||
{ "name": "foo", "web_content": [] }
|
{ "name": "Code w10", "project": { "name": "Windows 10" } }
|
||||||
]
|
]
|
||||||
}
|
|
||||||
] |]
|
|
||||||
{ matchStatus = 200,
|
|
||||||
matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
}
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 200,
|
||||||
|
matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
context "embeds children after update without explicitly including the id in the ?select" $ do
|
it "embeds an O2O relationship after update" $ do
|
||||||
it "without filters" $
|
request methodPatch "/students?id=eq.1&select=name,students_info(address)"
|
||||||
request methodPatch "/web_content?id=eq.0&select=name,web_content(name)"
|
[("Prefer", "return=representation")]
|
||||||
[("Prefer", "return=representation")]
|
[json|{"name": "Johnny Doe"}|]
|
||||||
[json|{"name": "tardis-patched"}|]
|
`shouldRespondWith`
|
||||||
`shouldRespondWith`
|
[json|[
|
||||||
[json|
|
{
|
||||||
[ { "name": "tardis-patched", "web_content": [ { "name": "fezz" }, { "name": "foo" }, { "name": "bar" } ]} ]
|
"name": "Johnny Doe",
|
||||||
|]
|
"students_info":{"address":"Street 1"}
|
||||||
{ matchStatus = 200,
|
|
||||||
matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
}
|
||||||
|
]|]
|
||||||
it "with filters" $
|
{ matchStatus = 200,
|
||||||
request methodPatch "/web_content?id=eq.0&select=name,web_content(name)&web_content.name=like.b*"
|
matchHeaders = [matchContentTypeJson]
|
||||||
[("Prefer", "return=representation")]
|
}
|
||||||
[json|{"name": "tardis-patched"}|]
|
request methodPatch "/students_info?id=eq.1&select=address,students(name)"
|
||||||
`shouldRespondWith`
|
[("Prefer", "return=representation")]
|
||||||
[json|
|
[json|{"address": "New Street 1"}|]
|
||||||
[ { "name": "tardis-patched", "web_content": [ { "name": "bar" } ]} ]
|
`shouldRespondWith`
|
||||||
|]
|
[json|[
|
||||||
{ matchStatus = 200,
|
{
|
||||||
matchHeaders = [matchContentTypeJson]
|
"address": "New Street 1",
|
||||||
}
|
"students":{"name": "John Doe"}
|
||||||
|
|
||||||
context "tables with foreign keys referencing other tables" $ do
|
|
||||||
context "embeds an M2M relationship plus parent after update" $ do
|
|
||||||
it "without filters" $
|
|
||||||
request methodPatch "/users?id=eq.1&select=name,tasks(name,project:projects(name))"
|
|
||||||
[("Prefer", "return=representation")]
|
|
||||||
[json|{"name": "Kevin Malone"}|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{
|
|
||||||
"name": "Kevin Malone",
|
|
||||||
"tasks": [
|
|
||||||
{ "name": "Design w7", "project": { "name": "Windows 7" } },
|
|
||||||
{ "name": "Code w7", "project": { "name": "Windows 7" } },
|
|
||||||
{ "name": "Design w10", "project": { "name": "Windows 10" } },
|
|
||||||
{ "name": "Code w10", "project": { "name": "Windows 10" } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200,
|
|
||||||
matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "with filters" $
|
|
||||||
request methodPatch "/users?id=eq.1&select=name,tasks(name,project:projects(name))&tasks.name=ilike.code*&tasks.project.name=like.*10"
|
|
||||||
[("Prefer", "return=representation")]
|
|
||||||
[json|{"name": "Kevin Malone"}|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{
|
|
||||||
"name": "Kevin Malone",
|
|
||||||
"tasks": [
|
|
||||||
{ "name": "Code w7", "project": null },
|
|
||||||
{ "name": "Code w10", "project": { "name": "Windows 10" } }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200,
|
|
||||||
matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
context "embeds an O2O relationship after update" $ do
|
|
||||||
it "without filters" $ do
|
|
||||||
request methodPatch "/students?id=eq.1&select=name,students_info(address)"
|
|
||||||
[("Prefer", "return=representation")]
|
|
||||||
[json|{"name": "Johnny Doe"}|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{
|
|
||||||
"name": "Johnny Doe",
|
|
||||||
"students_info":{"address":"Street 1"}
|
|
||||||
}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200,
|
|
||||||
matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
request methodPatch "/students_info?id=eq.1&select=address,students(name)"
|
|
||||||
[("Prefer", "return=representation")]
|
|
||||||
[json|{"address": "New Street 1"}|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{
|
|
||||||
"address": "New Street 1",
|
|
||||||
"students":{"name": "John Doe"}
|
|
||||||
}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200,
|
|
||||||
matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
|
||||||
|
|
||||||
it "with filters" $ do
|
|
||||||
request methodPatch "/students?id=eq.1&select=name,students_info(address)&students_info.code=like.0002"
|
|
||||||
[("Prefer", "return=representation")]
|
|
||||||
[json|{"name": "Johnny Doe"}|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|[
|
|
||||||
{
|
|
||||||
"name": "Johnny Doe",
|
|
||||||
"students_info": null
|
|
||||||
}
|
|
||||||
]|]
|
|
||||||
{ matchStatus = 200,
|
|
||||||
matchHeaders = [matchContentTypeJson]
|
|
||||||
}
|
}
|
||||||
|
]|]
|
||||||
|
{ matchStatus = 200,
|
||||||
|
matchHeaders = [matchContentTypeJson]
|
||||||
|
}
|
||||||
|
|
||||||
context "table with limited privileges" $ do
|
context "table with limited privileges" $ do
|
||||||
it "succeeds updating row and gives a 204 when using return=minimal" $
|
it "succeeds updating row and gives a 204 when using return=minimal" $
|
||||||
@@ -584,7 +427,7 @@ spec = do
|
|||||||
it "works with the limit query param" $
|
it "works with the limit query param" $
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
baseTable "limited_update_items" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodPatch "/limited_update_items?order=id&limit=2" mempty
|
requestMutation methodPatch "/limited_update_items?order=id&limit=2"
|
||||||
[json| {"name": "updated-item"} |]
|
[json| {"name": "updated-item"} |]
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[
|
[json|[
|
||||||
@@ -596,7 +439,7 @@ spec = do
|
|||||||
it "works with the limit query param plus a filter" $
|
it "works with the limit query param plus a filter" $
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
baseTable "limited_update_items" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodPatch "/limited_update_items?order=id&limit=1&id=gt.2" mempty
|
requestMutation methodPatch "/limited_update_items?order=id&limit=1&id=gt.2"
|
||||||
[json| {"name": "updated-item"} |]
|
[json| {"name": "updated-item"} |]
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[
|
[json|[
|
||||||
@@ -608,7 +451,7 @@ spec = do
|
|||||||
it "works with the limit and offset query params" $
|
it "works with the limit and offset query params" $
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
baseTable "limited_update_items" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodPatch "/limited_update_items?order=id&limit=1&offset=1" mempty
|
requestMutation methodPatch "/limited_update_items?order=id&limit=1&offset=1"
|
||||||
[json| {"name": "updated-item"} |]
|
[json| {"name": "updated-item"} |]
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[
|
[json|[
|
||||||
@@ -646,7 +489,7 @@ spec = do
|
|||||||
it "works with views with an explicit order by unique col" $
|
it "works with views with an explicit order by unique col" $
|
||||||
baseTable "limited_update_items_view" "id" tblDataBefore
|
baseTable "limited_update_items_view" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodPatch "/limited_update_items_view?order=id&limit=1&offset=1" mempty
|
requestMutation methodPatch "/limited_update_items_view?order=id&limit=1&offset=1"
|
||||||
[json| {"name": "updated-item"} |]
|
[json| {"name": "updated-item"} |]
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[
|
[json|[
|
||||||
@@ -658,7 +501,7 @@ spec = do
|
|||||||
it "works with views with an explicit order by composite pk" $
|
it "works with views with an explicit order by composite pk" $
|
||||||
baseTable "limited_update_items_cpk_view" "id" tblDataBefore
|
baseTable "limited_update_items_cpk_view" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodPatch "/limited_update_items_cpk_view?order=id,name&limit=1&offset=1" mempty
|
requestMutation methodPatch "/limited_update_items_cpk_view?order=id,name&limit=1&offset=1"
|
||||||
[json| {"name": "updated-item"} |]
|
[json| {"name": "updated-item"} |]
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[
|
[json|[
|
||||||
@@ -670,7 +513,7 @@ spec = do
|
|||||||
it "works on a table without a pk by ordering by 'ctid'" $
|
it "works on a table without a pk by ordering by 'ctid'" $
|
||||||
baseTable "limited_update_items_no_pk" "id" tblDataBefore
|
baseTable "limited_update_items_no_pk" "id" tblDataBefore
|
||||||
`mutatesWith`
|
`mutatesWith`
|
||||||
requestMutation methodPatch "/limited_update_items_no_pk?order=ctid&limit=1" mempty
|
requestMutation methodPatch "/limited_update_items_no_pk?order=ctid&limit=1"
|
||||||
[json| {"name": "updated-item"} |]
|
[json| {"name": "updated-item"} |]
|
||||||
`shouldMutateInto`
|
`shouldMutateInto`
|
||||||
[json|[
|
[json|[
|
||||||
@@ -678,67 +521,3 @@ spec = do
|
|||||||
, { "id": 2, "name": "item-2" }
|
, { "id": 2, "name": "item-2" }
|
||||||
, { "id": 3, "name": "item-3" }
|
, { "id": 3, "name": "item-3" }
|
||||||
]|]
|
]|]
|
||||||
|
|
||||||
it "ignores the Range header" $ do
|
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0))
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "updated-item" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "updated-item" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items?id=gte.2"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0))
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "updated-item" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "ignores the Range header and does not do a limited update" $
|
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items?order=id"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0))
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "updated-item" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "updated-item" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "ignores the Range header and does not throw an invalid range error" $
|
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items?order=id&limit=1&offset=1"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 0 0))
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "item-3" }
|
|
||||||
]|]
|
|
||||||
|
|
||||||
it "ignores the Range header but not the limit and offset params" $
|
|
||||||
baseTable "limited_update_items" "id" tblDataBefore
|
|
||||||
`mutatesWith`
|
|
||||||
requestMutation methodPatch "/limited_update_items?order=id&limit=2&offset=1"
|
|
||||||
(rangeHdrs (ByteRangeFromTo 1 1))
|
|
||||||
[json| {"name": "updated-item"} |]
|
|
||||||
`shouldMutateInto`
|
|
||||||
[json|[
|
|
||||||
{ "id": 1, "name": "item-1" }
|
|
||||||
, { "id": 2, "name": "updated-item" }
|
|
||||||
, { "id": 3, "name": "updated-item" }
|
|
||||||
]|]
|
|
||||||
|
|||||||
@@ -195,18 +195,25 @@ spec actualPgVersion =
|
|||||||
|
|
||||||
context "with PUT" $ do
|
context "with PUT" $ do
|
||||||
context "Restrictions" $ do
|
context "Restrictions" $ do
|
||||||
|
it "fails if Range is specified" $
|
||||||
|
request methodPut "/tiobe_pls?name=eq.Javascript" [("Range", "0-5")]
|
||||||
|
[json| [ { "name": "Javascript", "rank": 1 } ]|]
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT","code":"PGRST114","details":null,"hint":null}|]
|
||||||
|
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "fails if limit is specified" $
|
it "fails if limit is specified" $
|
||||||
put "/tiobe_pls?name=eq.Javascript&limit=1"
|
put "/tiobe_pls?name=eq.Javascript&limit=1"
|
||||||
[json| [ { "name": "Javascript", "rank": 1 } ]|]
|
[json| [ { "name": "Javascript", "rank": 1 } ]|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"limit/offset querystring parameters are not allowed for PUT","code":"PGRST114","details":null,"hint":null}|]
|
[json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT","code":"PGRST114","details":null,"hint":null}|]
|
||||||
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
|
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "fails if offset is specified" $
|
it "fails if offset is specified" $
|
||||||
put "/tiobe_pls?name=eq.Javascript&offset=1"
|
put "/tiobe_pls?name=eq.Javascript&offset=1"
|
||||||
[json| [ { "name": "Javascript", "rank": 1 } ]|]
|
[json| [ { "name": "Javascript", "rank": 1 } ]|]
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|{"message":"limit/offset querystring parameters are not allowed for PUT","code":"PGRST114","details":null,"hint":null}|]
|
[json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT","code":"PGRST114","details":null,"hint":null}|]
|
||||||
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
|
{ matchStatus = 400 , matchHeaders = [matchContentTypeJson] }
|
||||||
|
|
||||||
it "rejects every other filter than pk cols eq's" $ do
|
it "rejects every other filter than pk cols eq's" $ do
|
||||||
@@ -375,18 +382,6 @@ spec actualPgVersion =
|
|||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json|[ { "id": 1 } ]|]
|
[json|[ { "id": 1 } ]|]
|
||||||
|
|
||||||
it "ignores the Range header" $ do
|
|
||||||
-- assert that the next request will indeed be an update
|
|
||||||
get "/tiobe_pls?name=eq.Java"
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json|[ { "name": "Java", "rank": 1 } ]|]
|
|
||||||
|
|
||||||
request methodPut "/tiobe_pls?name=eq.Java"
|
|
||||||
[("Prefer", "return=representation"), ("Range", "1-1")]
|
|
||||||
[json| [ { "name": "Java", "rank": 5 } ]|]
|
|
||||||
`shouldRespondWith`
|
|
||||||
[json| [ { "name": "Java", "rank": 5 } ]|]
|
|
||||||
|
|
||||||
-- TODO: move this to SingularSpec?
|
-- TODO: move this to SingularSpec?
|
||||||
it "works with return=representation and vnd.pgrst.object+json" $
|
it "works with return=representation and vnd.pgrst.object+json" $
|
||||||
request methodPut "/tiobe_pls?name=eq.Ruby"
|
request methodPut "/tiobe_pls?name=eq.Ruby"
|
||||||
|
|||||||
+11
-17
@@ -1,5 +1,6 @@
|
|||||||
module Main where
|
module Main where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Hasql.Pool as P
|
import qualified Hasql.Pool as P
|
||||||
import qualified Hasql.Transaction.Sessions as HT
|
import qualified Hasql.Transaction.Sessions as HT
|
||||||
|
|
||||||
@@ -9,10 +10,11 @@ import Data.List.NonEmpty (toList)
|
|||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
import PostgREST.App (postgrest)
|
import PostgREST.App (postgrest)
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..), LogLevel (..))
|
||||||
import PostgREST.Config.Database (queryPgVersion)
|
import PostgREST.Config.Database (queryPgVersion)
|
||||||
import PostgREST.SchemaCache (querySchemaCache)
|
import PostgREST.SchemaCache (querySchemaCache)
|
||||||
import Protolude hiding (toList, toS)
|
import Protolude hiding (toList, toS)
|
||||||
|
import Protolude.Conv (toS)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
import qualified PostgREST.AppState as AppState
|
import qualified PostgREST.AppState as AppState
|
||||||
@@ -27,8 +29,6 @@ import qualified Feature.ConcurrentSpec
|
|||||||
import qualified Feature.CorsSpec
|
import qualified Feature.CorsSpec
|
||||||
import qualified Feature.ExtraSearchPathSpec
|
import qualified Feature.ExtraSearchPathSpec
|
||||||
import qualified Feature.LegacyGucsSpec
|
import qualified Feature.LegacyGucsSpec
|
||||||
import qualified Feature.NoSuperuserSpec
|
|
||||||
import qualified Feature.ObservabilitySpec
|
|
||||||
import qualified Feature.OpenApi.DisabledOpenApiSpec
|
import qualified Feature.OpenApi.DisabledOpenApiSpec
|
||||||
import qualified Feature.OpenApi.IgnorePrivOpenApiSpec
|
import qualified Feature.OpenApi.IgnorePrivOpenApiSpec
|
||||||
import qualified Feature.OpenApi.OpenApiSpec
|
import qualified Feature.OpenApi.OpenApiSpec
|
||||||
@@ -53,10 +53,8 @@ import qualified Feature.Query.QueryLimitedSpec
|
|||||||
import qualified Feature.Query.QuerySpec
|
import qualified Feature.Query.QuerySpec
|
||||||
import qualified Feature.Query.RangeSpec
|
import qualified Feature.Query.RangeSpec
|
||||||
import qualified Feature.Query.RawOutputTypesSpec
|
import qualified Feature.Query.RawOutputTypesSpec
|
||||||
import qualified Feature.Query.RelatedQueriesSpec
|
|
||||||
import qualified Feature.Query.RpcSpec
|
import qualified Feature.Query.RpcSpec
|
||||||
import qualified Feature.Query.SingularSpec
|
import qualified Feature.Query.SingularSpec
|
||||||
import qualified Feature.Query.SpreadQueriesSpec
|
|
||||||
import qualified Feature.Query.UnicodeSpec
|
import qualified Feature.Query.UnicodeSpec
|
||||||
import qualified Feature.Query.UpdateSpec
|
import qualified Feature.Query.UpdateSpec
|
||||||
import qualified Feature.Query.UpsertSpec
|
import qualified Feature.Query.UpsertSpec
|
||||||
@@ -66,9 +64,9 @@ import qualified Feature.RpcPreRequestGucsSpec
|
|||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
pool <- P.acquire 3 10 60 $ toUtf8 $ configDbUri testCfg
|
pool <- P.acquire 3 Nothing $ toUtf8 $ configDbUri testCfg
|
||||||
|
|
||||||
actualPgVersion <- either (panic . show) id <$> P.use pool (queryPgVersion False)
|
actualPgVersion <- either (panic . show) id <$> P.use pool queryPgVersion
|
||||||
|
|
||||||
baseSchemaCache <-
|
baseSchemaCache <-
|
||||||
loadSchemaCache pool
|
loadSchemaCache pool
|
||||||
@@ -81,7 +79,9 @@ main = do
|
|||||||
appState <- AppState.initWithPool pool config
|
appState <- AppState.initWithPool pool config
|
||||||
AppState.putPgVersion appState actualPgVersion
|
AppState.putPgVersion appState actualPgVersion
|
||||||
AppState.putSchemaCache appState (Just baseSchemaCache)
|
AppState.putSchemaCache appState (Just baseSchemaCache)
|
||||||
return ((), postgrest config appState $ pure ())
|
when (isJust $ configDbRootSpec config) $
|
||||||
|
AppState.putJsonDbS appState $ toS $ JSON.encode baseSchemaCache
|
||||||
|
return ((), postgrest LogCrit appState $ pure ())
|
||||||
|
|
||||||
-- For tests that run with a different SchemaCache(depends on configSchemas)
|
-- For tests that run with a different SchemaCache(depends on configSchemas)
|
||||||
appDbs config = do
|
appDbs config = do
|
||||||
@@ -92,7 +92,9 @@ main = do
|
|||||||
appState <- AppState.initWithPool pool config
|
appState <- AppState.initWithPool pool config
|
||||||
AppState.putPgVersion appState actualPgVersion
|
AppState.putPgVersion appState actualPgVersion
|
||||||
AppState.putSchemaCache appState (Just customSchemaCache)
|
AppState.putSchemaCache appState (Just customSchemaCache)
|
||||||
return ((), postgrest config appState $ pure ())
|
when (isJust $ configDbRootSpec config) $
|
||||||
|
AppState.putJsonDbS appState $ toS $ JSON.encode baseSchemaCache
|
||||||
|
return ((), postgrest LogCrit appState $ pure ())
|
||||||
|
|
||||||
let withApp = app testCfg
|
let withApp = app testCfg
|
||||||
maxRowsApp = app testMaxRowsCfg
|
maxRowsApp = app testMaxRowsCfg
|
||||||
@@ -113,7 +115,6 @@ main = do
|
|||||||
testCfgLegacyGucsApp = app testCfgLegacyGucs
|
testCfgLegacyGucsApp = app testCfgLegacyGucs
|
||||||
planEnabledApp = app testPlanEnabledCfg
|
planEnabledApp = app testPlanEnabledCfg
|
||||||
pgSafeUpdateApp = app testPgSafeUpdateEnabledCfg
|
pgSafeUpdateApp = app testPgSafeUpdateEnabledCfg
|
||||||
obsApp = app testObservabilityCfg
|
|
||||||
|
|
||||||
extraSearchPathApp = appDbs testCfgExtraSearchPath
|
extraSearchPathApp = appDbs testCfgExtraSearchPath
|
||||||
unicodeApp = appDbs testUnicodeCfg
|
unicodeApp = appDbs testUnicodeCfg
|
||||||
@@ -148,9 +149,6 @@ main = do
|
|||||||
, ("Feature.Query.UpdateSpec" , Feature.Query.UpdateSpec.spec)
|
, ("Feature.Query.UpdateSpec" , Feature.Query.UpdateSpec.spec)
|
||||||
, ("Feature.Query.UpsertSpec" , Feature.Query.UpsertSpec.spec actualPgVersion)
|
, ("Feature.Query.UpsertSpec" , Feature.Query.UpsertSpec.spec actualPgVersion)
|
||||||
, ("Feature.Query.ComputedRelsSpec" , Feature.Query.ComputedRelsSpec.spec)
|
, ("Feature.Query.ComputedRelsSpec" , Feature.Query.ComputedRelsSpec.spec)
|
||||||
, ("Feature.Query.RelatedQueriesSpec" , Feature.Query.RelatedQueriesSpec.spec)
|
|
||||||
, ("Feature.Query.SpreadQueriesSpec" , Feature.Query.SpreadQueriesSpec.spec)
|
|
||||||
, ("Feature.NoSuperuserSpec" , Feature.NoSuperuserSpec.spec)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
hspec $ do
|
hspec $ do
|
||||||
@@ -245,10 +243,6 @@ main = do
|
|||||||
parallel $ before pgSafeUpdateApp $
|
parallel $ before pgSafeUpdateApp $
|
||||||
describe "Feature.Query.PgSafeUpdateSpec.spec" Feature.Query.PgSafeUpdateSpec.spec
|
describe "Feature.Query.PgSafeUpdateSpec.spec" Feature.Query.PgSafeUpdateSpec.spec
|
||||||
|
|
||||||
-- this test runs with server-trace-header set
|
|
||||||
parallel $ before obsApp $
|
|
||||||
describe "Feature.ObservabilitySpec.spec" Feature.ObservabilitySpec.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
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
-- TODO Can be replaced now by obtaining the EXPLAIN plan and adding the cost tests on PlanSpec.hs
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Control.Lens ((^?))
|
||||||
|
import qualified Data.Aeson.Lens as L
|
||||||
|
import qualified Hasql.Decoders as HD
|
||||||
|
import qualified Hasql.DynamicStatements.Snippet as H
|
||||||
|
import qualified Hasql.DynamicStatements.Statement as H
|
||||||
|
import qualified Hasql.Pool as P
|
||||||
|
import qualified Hasql.Statement as H
|
||||||
|
import qualified Hasql.Transaction as HT
|
||||||
|
import qualified Hasql.Transaction.Sessions as HT
|
||||||
|
import Text.Heredoc
|
||||||
|
|
||||||
|
import Protolude hiding (get, toS)
|
||||||
|
|
||||||
|
import PostgREST.Plan.CallPlan
|
||||||
|
import PostgREST.Query.QueryBuilder (callPlanToQuery)
|
||||||
|
|
||||||
|
import PostgREST.SchemaCache.Identifiers
|
||||||
|
import PostgREST.SchemaCache.Proc
|
||||||
|
|
||||||
|
import Test.Hspec
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
pool <- P.acquire 3 Nothing "postgresql://"
|
||||||
|
|
||||||
|
hspec $ describe "QueryCost" $
|
||||||
|
context "call proc query" $ do
|
||||||
|
it "should not exceed cost when calling setof composite proc" $ do
|
||||||
|
cost <- exec pool $
|
||||||
|
callPlanToQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
|
||||||
|
(KeyParams [ProcParam "id" "int" True False])
|
||||||
|
(Just [str| {"id": 3} |]) False False [])
|
||||||
|
liftIO $
|
||||||
|
cost `shouldSatisfy` (< Just 40)
|
||||||
|
|
||||||
|
it "should not exceed cost when calling setof composite proc with empty params" $ do
|
||||||
|
cost <- exec pool $
|
||||||
|
callPlanToQuery (FunctionCall (QualifiedIdentifier "test" "getallprojects") (KeyParams []) Nothing False False [])
|
||||||
|
liftIO $
|
||||||
|
cost `shouldSatisfy` (< Just 30)
|
||||||
|
|
||||||
|
it "should not exceed cost when calling scalar proc" $ do
|
||||||
|
cost <- exec pool $
|
||||||
|
callPlanToQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
|
||||||
|
(KeyParams [ProcParam "a" "int" True False, ProcParam "b" "int" True False])
|
||||||
|
(Just [str| {"a": 3, "b": 4} |]) True False [])
|
||||||
|
liftIO $
|
||||||
|
cost `shouldSatisfy` (< Just 10)
|
||||||
|
|
||||||
|
context "params=multiple-objects" $ do
|
||||||
|
it "should not exceed cost when calling setof composite proc" $ do
|
||||||
|
cost <- exec pool $
|
||||||
|
callPlanToQuery (FunctionCall (QualifiedIdentifier "test" "get_projects_below")
|
||||||
|
(KeyParams [ProcParam "id" "int" True False])
|
||||||
|
(Just [str| [{"id": 1}, {"id": 4}] |]) False True [])
|
||||||
|
liftIO $ do
|
||||||
|
-- lower bound needed for now to make sure that cost is not Nothing
|
||||||
|
cost `shouldSatisfy` (> Just 2000)
|
||||||
|
cost `shouldSatisfy` (< Just 2100)
|
||||||
|
|
||||||
|
it "should not exceed cost when calling scalar proc" $ do
|
||||||
|
cost <- exec pool $
|
||||||
|
callPlanToQuery (FunctionCall (QualifiedIdentifier "test" "add_them")
|
||||||
|
(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 $
|
||||||
|
cost `shouldSatisfy` (< Just 10)
|
||||||
|
|
||||||
|
|
||||||
|
exec :: P.Pool -> H.Snippet -> IO (Maybe Int64)
|
||||||
|
exec pool query =
|
||||||
|
join . rightToMaybe <$>
|
||||||
|
P.use pool (HT.transaction HT.ReadCommitted HT.Read $ HT.statement mempty $ explainCost query)
|
||||||
|
|
||||||
|
explainCost :: H.Snippet -> H.Statement () (Maybe Int64)
|
||||||
|
explainCost query =
|
||||||
|
H.dynamicallyParameterized snippet decodeExplain False
|
||||||
|
where
|
||||||
|
snippet = "EXPLAIN (FORMAT JSON) " <> query
|
||||||
|
decodeExplain :: HD.Result (Maybe Int64)
|
||||||
|
decodeExplain =
|
||||||
|
let row = HD.singleRow $ HD.column $ HD.nonNullable HD.bytea in
|
||||||
|
(^? L.nth 0 . L.key "Plan" . L.key "Total Cost" . L._Integral) <$> row
|
||||||
+6
-28
@@ -1,16 +1,13 @@
|
|||||||
module SpecHelper where
|
module SpecHelper where
|
||||||
|
|
||||||
import Control.Lens ((^?))
|
|
||||||
import Data.Aeson.Lens
|
|
||||||
import qualified Data.ByteString.Base64 as B64 (decodeLenient)
|
import qualified Data.ByteString.Base64 as B64 (decodeLenient)
|
||||||
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 BL
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
import Data.Scientific (toRealFloat)
|
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
|
|
||||||
import Data.Aeson (Value (..), decode, encode)
|
import Data.Aeson (Value (..), decode, encode)
|
||||||
import Data.CaseInsensitive (CI (..), mk, original)
|
import Data.CaseInsensitive (CI (..), original)
|
||||||
import Data.List (lookup)
|
import Data.List (lookup)
|
||||||
import Data.List.NonEmpty (fromList)
|
import Data.List.NonEmpty (fromList)
|
||||||
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
import Network.Wai.Test (SResponse (simpleBody, simpleHeaders, simpleStatus))
|
||||||
@@ -82,8 +79,7 @@ baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
|||||||
, configDbMaxRows = Nothing
|
, configDbMaxRows = Nothing
|
||||||
, configDbPlanEnabled = False
|
, configDbPlanEnabled = False
|
||||||
, configDbPoolSize = 10
|
, configDbPoolSize = 10
|
||||||
, configDbPoolAcquisitionTimeout = 10
|
, configDbPoolAcquisitionTimeout = Nothing
|
||||||
, configDbPoolMaxLifetime = 1800
|
|
||||||
, configDbPreRequest = Just $ QualifiedIdentifier "test" "switch_role"
|
, configDbPreRequest = Just $ QualifiedIdentifier "test" "switch_role"
|
||||||
, configDbPreparedStatements = True
|
, configDbPreparedStatements = True
|
||||||
, configDbRootSpec = Nothing
|
, configDbRootSpec = Nothing
|
||||||
@@ -104,13 +100,11 @@ baseCfg = let secret = Just $ encodeUtf8 "reallyreallyreallyreallyverysafe" in
|
|||||||
, configRawMediaTypes = []
|
, configRawMediaTypes = []
|
||||||
, configServerHost = "localhost"
|
, configServerHost = "localhost"
|
||||||
, configServerPort = 3000
|
, configServerPort = 3000
|
||||||
, configServerTraceHeader = Nothing
|
|
||||||
, configServerUnixSocket = Nothing
|
, configServerUnixSocket = Nothing
|
||||||
, configServerUnixSocketMode = 432
|
, configServerUnixSocketMode = 432
|
||||||
, configDbTxAllowOverride = True
|
, configDbTxAllowOverride = True
|
||||||
, configDbTxRollbackAll = True
|
, configDbTxRollbackAll = True
|
||||||
, configAdminServerPort = Nothing
|
, configAdminServerPort = Nothing
|
||||||
, configRoleSettings = mempty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testCfg :: AppConfig
|
testCfg :: AppConfig
|
||||||
@@ -206,12 +200,9 @@ testCfgLegacyGucs = baseCfg { configDbUseLegacyGucs = False }
|
|||||||
testPgSafeUpdateEnabledCfg :: AppConfig
|
testPgSafeUpdateEnabledCfg :: AppConfig
|
||||||
testPgSafeUpdateEnabledCfg = baseCfg { configDbPreRequest = Just $ QualifiedIdentifier "test" "load_safeupdate" }
|
testPgSafeUpdateEnabledCfg = baseCfg { configDbPreRequest = Just $ QualifiedIdentifier "test" "load_safeupdate" }
|
||||||
|
|
||||||
testObservabilityCfg :: AppConfig
|
|
||||||
testObservabilityCfg = baseCfg { configServerTraceHeader = Just $ mk "X-Request-Id" }
|
|
||||||
|
|
||||||
analyzeTable :: Text -> IO ()
|
analyzeTable :: Text -> IO ()
|
||||||
analyzeTable tableName =
|
analyzeTable tableName =
|
||||||
void $ readProcess "psql" ["-U", "postgres", "--set", "ON_ERROR_STOP=1", "-a", "-c", toS $ "ANALYZE test.\"" <> tableName <> "\""] []
|
void $ readProcess "psql" ["--set", "ON_ERROR_STOP=1", "-a", "-c", toS $ "ANALYZE test.\"" <> tableName <> "\""] []
|
||||||
|
|
||||||
rangeHdrs :: ByteRange -> [Header]
|
rangeHdrs :: ByteRange -> [Header]
|
||||||
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
|
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
|
||||||
@@ -222,9 +213,6 @@ rangeHdrsWithCount r = ("Prefer", "count=exact") : rangeHdrs r
|
|||||||
acceptHdrs :: BS.ByteString -> [Header]
|
acceptHdrs :: BS.ByteString -> [Header]
|
||||||
acceptHdrs mime = [(hAccept, mime)]
|
acceptHdrs mime = [(hAccept, mime)]
|
||||||
|
|
||||||
planHdr :: Header
|
|
||||||
planHdr = (hAccept, "application/vnd.pgrst.plan+json")
|
|
||||||
|
|
||||||
rangeUnit :: Header
|
rangeUnit :: Header
|
||||||
rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")
|
rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")
|
||||||
|
|
||||||
@@ -282,19 +270,9 @@ baseTable :: ByteString -> ByteString -> Value -> BaseTable
|
|||||||
baseTable = BaseTable
|
baseTable = BaseTable
|
||||||
|
|
||||||
-- | The mutation (update/delete) that will be applied to the base table
|
-- | The mutation (update/delete) that will be applied to the base table
|
||||||
requestMutation :: Method -> ByteString -> [Header] -> BL.ByteString -> WaiExpectation ()
|
requestMutation :: Method -> ByteString -> BL.ByteString -> WaiExpectation ()
|
||||||
requestMutation method path headers body =
|
requestMutation method path body =
|
||||||
request method path (("Prefer", "tx=commit") : headers) body `shouldRespondWith` 204
|
request method path [("Prefer", "tx=commit")] body `shouldRespondWith` 204
|
||||||
|
|
||||||
data BaseTable = BaseTable ByteString ByteString Value
|
data BaseTable = BaseTable ByteString ByteString Value
|
||||||
data MutationCheck = MutationCheck BaseTable (WaiExpectation ())
|
data MutationCheck = MutationCheck BaseTable (WaiExpectation ())
|
||||||
|
|
||||||
planCost :: SResponse -> Float
|
|
||||||
planCost resp =
|
|
||||||
let res = simpleBody resp ^? nth 0 . key "Plan" . key "Total Cost" in
|
|
||||||
-- big value in case parsing fails
|
|
||||||
fromMaybe 1000000000.0 $ unbox =<< res
|
|
||||||
where
|
|
||||||
unbox :: Value -> Maybe Float
|
|
||||||
unbox (Number n) = Just $ toRealFloat n
|
|
||||||
unbox _ = Nothing
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
module TestTypes (
|
||||||
|
IncPK(..)
|
||||||
|
, CompoundPK(..)
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Data.Aeson ((.:))
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
data IncPK = IncPK {
|
||||||
|
incId :: Int
|
||||||
|
, incNullableStr :: Maybe Text
|
||||||
|
, incStr :: Text
|
||||||
|
, incInsert :: Text
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
instance JSON.FromJSON IncPK where
|
||||||
|
parseJSON (JSON.Object r) = IncPK <$>
|
||||||
|
r .: "id" <*>
|
||||||
|
r .: "nullable_string" <*>
|
||||||
|
r .: "non_nullable_string" <*>
|
||||||
|
r .: "inserted_at"
|
||||||
|
parseJSON _ = mzero
|
||||||
|
|
||||||
|
data CompoundPK = CompoundPK {
|
||||||
|
compoundK1 :: Int
|
||||||
|
, compoundK2 :: Text
|
||||||
|
, compoundExtra :: Maybe Int
|
||||||
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
|
instance JSON.FromJSON CompoundPK where
|
||||||
|
parseJSON (JSON.Object r) = CompoundPK <$>
|
||||||
|
r .: "k1" <*>
|
||||||
|
r .: "k2" <*>
|
||||||
|
r .: "extra"
|
||||||
|
parseJSON _ = mzero
|
||||||
Vendored
+1
-14
@@ -168,7 +168,7 @@ INSERT INTO touched_files VALUES
|
|||||||
TRUNCATE TABLE complex_items CASCADE;
|
TRUNCATE TABLE complex_items CASCADE;
|
||||||
INSERT INTO complex_items VALUES (1, 'One', '{"foo":{"int":1,"bar":"baz"}}', '{1}');
|
INSERT INTO complex_items VALUES (1, 'One', '{"foo":{"int":1,"bar":"baz"}}', '{1}');
|
||||||
INSERT INTO complex_items VALUES (2, 'Two', '{"foo":{"int":1,"bar":"baz"}}', '{1,2}');
|
INSERT INTO complex_items VALUES (2, 'Two', '{"foo":{"int":1,"bar":"baz"}}', '{1,2}');
|
||||||
INSERT INTO complex_items VALUES (3, 'Three', '{"foo":{"int":1,"bar":"baz"}}', '{1,2,3}', 3);
|
INSERT INTO complex_items VALUES (3, 'Three', '{"foo":{"int":1,"bar":"baz"}}', '{1,2,3}');
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
@@ -379,7 +379,6 @@ INSERT INTO ranges VALUES (1, '[1,3]');
|
|||||||
INSERT INTO ranges VALUES (2, '[3,6]');
|
INSERT INTO ranges VALUES (2, '[3,6]');
|
||||||
INSERT INTO ranges VALUES (3, '[6,9]');
|
INSERT INTO ranges VALUES (3, '[6,9]');
|
||||||
INSERT INTO ranges VALUES (4, '[9,12]');
|
INSERT INTO ranges VALUES (4, '[9,12]');
|
||||||
INSERT INTO ranges VALUES (5, null);
|
|
||||||
|
|
||||||
TRUNCATE TABLE being CASCADE;
|
TRUNCATE TABLE being CASCADE;
|
||||||
INSERT INTO being VALUES (1), (2), (3), (4);
|
INSERT INTO being VALUES (1), (2), (3), (4);
|
||||||
@@ -827,15 +826,3 @@ INSERT INTO country(id, name) VALUES (1, 'Afghanistan'), (2, 'Algeria');
|
|||||||
|
|
||||||
TRUNCATE TABLE capital CASCADE;
|
TRUNCATE TABLE capital CASCADE;
|
||||||
INSERT INTO capital(id, name, country_id) VALUES (1, 'Kabul', 1), (2, 'Algiers', 2);
|
INSERT INTO capital(id, name, country_id) VALUES (1, 'Kabul', 1), (2, 'Algiers', 2);
|
||||||
|
|
||||||
TRUNCATE TABLE trash CASCADE;
|
|
||||||
INSERT INTO trash(id) VALUES (1), (2), (3);
|
|
||||||
|
|
||||||
TRUNCATE TABLE trash_details CASCADE;
|
|
||||||
INSERT INTO trash_details(id,jsonb_col) VALUES (1,'{"key": 10}'), (2,'{"key": 6}'), (3,'{"key": 8}');
|
|
||||||
|
|
||||||
TRUNCATE TABLE posters CASCADE;
|
|
||||||
INSERT INTO posters(id,name) VALUES (1,'Mark'), (2,'Elon'), (3,'Bill'), (4,'Jeff');
|
|
||||||
|
|
||||||
TRUNCATE TABLE subscriptions CASCADE;
|
|
||||||
INSERT INTO subscriptions(subscriber,subscribed) VALUES (3,1), (4,1), (1,2);
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user