Merge branch 'docs/main' into main
@@ -2,7 +2,7 @@
|
||||
When submitting a new feature or fix:
|
||||
|
||||
- Add a new entry to the CHANGELOG - https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md#unreleased
|
||||
- If relevant, update the docs - https://github.com/PostgREST/postgrest-docs
|
||||
- If relevant, update the docs
|
||||
- Use a prefix for the PR title or commits, e.g. "fix: description of the fix".
|
||||
+ `fix`, bug fixes
|
||||
+ `feat`, new features added
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Docs
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- rel-*
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- rel-*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v25
|
||||
- run: nix-env -f docs/default.nix -iA build
|
||||
- run: postgrest-docs-build
|
||||
- run: git diff --exit-code HEAD locales || echo "Please commit changes to the locales/ folder after running postgrest-docs-build."
|
||||
|
||||
spellcheck:
|
||||
name: Run spellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v25
|
||||
- run: nix-env -f docs/default.nix -iA spellcheck
|
||||
- run: postgrest-docs-spellcheck
|
||||
|
||||
dictcheck:
|
||||
name: Run dictcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v25
|
||||
- run: nix-env -f docs/default.nix -iA dictcheck
|
||||
- run: postgrest-docs-dictcheck
|
||||
|
||||
linkcheck:
|
||||
name: Run linkcheck
|
||||
if: github.base_ref == 'main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: cachix/install-nix-action@v25
|
||||
- run: nix-env -f docs/default.nix -iA linkcheck
|
||||
- run: postgrest-docs-linkcheck
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
version: 2
|
||||
sphinx:
|
||||
configuration: docs/conf.py
|
||||
python:
|
||||
install:
|
||||
- requirements: docs/requirements.txt
|
||||
build:
|
||||
os: ubuntu-22.04
|
||||
tools:
|
||||
python: "3.11"
|
||||
@@ -0,0 +1,3 @@
|
||||
This repository follows the same contribution guidelines as the main PostgREST repository contribution guidelines:
|
||||
|
||||
https://github.com/PostgREST/postgrest/blob/main/.github/CONTRIBUTING.md
|
||||
@@ -0,0 +1,9 @@
|
||||
_build
|
||||
Pipfile.lock
|
||||
*.aux
|
||||
*.log
|
||||
_diagrams/db.pdf
|
||||
misspellings
|
||||
unuseddict
|
||||
.history
|
||||
*.mo
|
||||
@@ -0,0 +1,38 @@
|
||||
# PostgREST documentation https://postgrest.org/
|
||||
|
||||
PostgREST docs use the reStructuredText format, check this [cheatsheet](https://github.com/ralsina/rst-cheatsheet/blob/master/rst-cheatsheet.rst) to get acquainted with it.
|
||||
|
||||
To build the docs locally, use [nix](https://nixos.org/nix/):
|
||||
|
||||
```bash
|
||||
nix-shell
|
||||
```
|
||||
|
||||
Once in the nix-shell you have the following commands available:
|
||||
|
||||
- `postgrest-docs-build`: Build the docs.
|
||||
- `postgrest-docs-serve`: Build the docs and start a livereload server on `http://localhost:5500`.
|
||||
- `postgrest-docs-spellcheck`: Run aspell.
|
||||
|
||||
## Documentation structure
|
||||
|
||||
This documentation is structured according to tutorials-howtos-topics-references. For more details on the rationale of this structure,
|
||||
see https://www.divio.com/blog/documentation.
|
||||
|
||||
## Translating
|
||||
|
||||
To create `.po` files for translation into a new language pass the language code as the first argument to `postgrest-docs-build`.
|
||||
|
||||
Example to add German/de:
|
||||
|
||||
```
|
||||
postgrest-docs-build de
|
||||
```
|
||||
|
||||
The livereload server also supports a language/locale argument to show the translated docs during translation:
|
||||
|
||||
```
|
||||
postgrest-docs-serve de
|
||||
```
|
||||
|
||||
Spellcheck is currently only available for the default language.
|
||||
@@ -0,0 +1,42 @@
|
||||
## ERD
|
||||
|
||||
The ER diagrams were created with https://github.com/BurntSushi/erd/.
|
||||
|
||||
You can go download erd from https://github.com/BurntSushi/erd/releases and then do:
|
||||
|
||||
```bash
|
||||
./erd_static-x86-64 -i film.er -o ../_static/film.png
|
||||
```
|
||||
|
||||
The fonts used belong to the GNU FreeFont family. You can download them here: http://ftp.gnu.org/gnu/freefont/
|
||||
|
||||
## LaTeX
|
||||
|
||||
The schema structure diagram is done with LaTeX. You can use a GUI like https://www.mathcha.io/editor to create the .tex file.
|
||||
|
||||
Then use this command to generate the png file.
|
||||
|
||||
```bash
|
||||
pdflatex --shell-escape -halt-on-error db.tex
|
||||
|
||||
## and move it to the static folder(it's not easy to do it in one go with the pdflatex)
|
||||
mv db.png ../_static/
|
||||
```
|
||||
|
||||
LaTeX is used because it's a tweakable plain text format.
|
||||
|
||||
You can install the full latex suite with `nix`:
|
||||
|
||||
```
|
||||
nix-env -iA texlive.combined.scheme-full
|
||||
```
|
||||
|
||||
To tweak the file with a live reload environment use:
|
||||
|
||||
```bash
|
||||
# open the pdf(zathura used as an example)
|
||||
zathura db.pdf &
|
||||
|
||||
# live reload with entr
|
||||
echo db.tex | entr pdflatex --shell-escape -halt-on-error db.tex
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
entity {font: "FreeSans"}
|
||||
relationship {font: "FreeMono"}
|
||||
|
||||
[Box_Office]
|
||||
*bo_date
|
||||
*+film_id
|
||||
gross_revenue
|
||||
|
||||
[Films]
|
||||
*id
|
||||
+director_id
|
||||
title
|
||||
`...`
|
||||
|
||||
Box_Office +--1 Films
|
||||
@@ -0,0 +1,71 @@
|
||||
\documentclass[convert]{standalone}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{tikz}
|
||||
\usepackage{mathdots}
|
||||
\usepackage{yhmath}
|
||||
\usepackage{cancel}
|
||||
\usepackage{color}
|
||||
\usepackage{siunitx}
|
||||
\usepackage{array}
|
||||
\usepackage{multirow}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{gensymb}
|
||||
\usepackage{tabularx}
|
||||
\usepackage{booktabs}
|
||||
\usetikzlibrary{fadings}
|
||||
\usetikzlibrary{patterns}
|
||||
\usetikzlibrary{shadows.blur}
|
||||
\usetikzlibrary{shapes}
|
||||
|
||||
\begin{document}
|
||||
|
||||
\newcommand\customScale{0.35}
|
||||
|
||||
\begin{tikzpicture}[x=0.75pt,y=0.75pt,yscale=-1,xscale=1, scale=\customScale, every node/.style={scale=\customScale}]
|
||||
|
||||
%Shape: Can [id:dp7234864758664346]
|
||||
\draw [fill={rgb, 255:red, 47; green, 97; blue, 144 } ,fill opacity=1 ] (497.5,51.5) -- (497.5,255.5) .. controls (497.5,275.66) and (423.18,292) .. (331.5,292) .. controls (239.82,292) and (165.5,275.66) .. (165.5,255.5) -- (165.5,51.5) .. controls (165.5,31.34) and (239.82,15) .. (331.5,15) .. controls (423.18,15) and (497.5,31.34) .. (497.5,51.5) .. controls (497.5,71.66) and (423.18,88) .. (331.5,88) .. controls (239.82,88) and (165.5,71.66) .. (165.5,51.5) ;
|
||||
%Shape: Rectangle [id:dp7384065579958246]
|
||||
\draw [fill={rgb, 255:red, 236; green, 227; blue, 227 } ,fill opacity=1 ] (189,115) -- (252.5,115) -- (252.5,155) -- (189,155) -- cycle ;
|
||||
%Shape: Rectangle [id:dp24763906430298177]
|
||||
\draw [fill={rgb, 255:red, 236; green, 227; blue, 227 } ,fill opacity=1 ] (292,118) -- (362,118) -- (362,158) -- (292,158) -- cycle ;
|
||||
%Shape: Rectangle [id:dp3775601612537265]
|
||||
\draw [fill={rgb, 255:red, 236; green, 227; blue, 227 } ,fill opacity=1 ] (397,114) -- (467,114) -- (467,154) -- (397,154) -- cycle ;
|
||||
%Shape: Rectangle [id:dp7071457022893852]
|
||||
\draw [fill={rgb, 255:red, 248; green, 231; blue, 28 } ,fill opacity=1 ] (269,199) -- (397.5,199) -- (397.5,273) -- (269,273) -- cycle ;
|
||||
%Straight Lines [id:da8846759047437789]
|
||||
\draw (268,234) -- (226.44,155.77) ;
|
||||
\draw [shift={(225.5,154)}, rotate = 422.02] [color={rgb, 255:red, 0; green, 0; blue, 0 } ][line width=0.75] (10.93,-3.29) .. controls (6.95,-1.4) and (3.31,-0.3) .. (0,0) .. controls (3.31,0.3) and (6.95,1.4) .. (10.93,3.29) ;
|
||||
%Straight Lines [id:da6908444738113828]
|
||||
\draw (309.5,198) -- (307.6,161) ;
|
||||
\draw [shift={(307.5,159)}, rotate = 447.06] [color={rgb, 255:red, 0; green, 0; blue, 0 } ][line width=0.75] (10.93,-3.29) .. controls (6.95,-1.4) and (3.31,-0.3) .. (0,0) .. controls (3.31,0.3) and (6.95,1.4) .. (10.93,3.29) ;
|
||||
%Straight Lines [id:da7168757864413169]
|
||||
\draw (398.5,233) -- (431.72,154.84) ;
|
||||
\draw [shift={(432.5,153)}, rotate = 473.03] [color={rgb, 255:red, 0; green, 0; blue, 0 } ][line width=0.75] (10.93,-3.29) .. controls (6.95,-1.4) and (3.31,-0.3) .. (0,0) .. controls (3.31,0.3) and (6.95,1.4) .. (10.93,3.29) ;
|
||||
%Up Down Arrow [id:dp14059754167108496]
|
||||
\draw [fill={rgb, 255:red, 126; green, 211; blue, 33 } ,fill opacity=1 ] (312.5,288.5) -- (330,273) -- (347.5,288.5) -- (338.75,288.5) -- (338.75,319.5) -- (347.5,319.5) -- (330,335) -- (312.5,319.5) -- (321.25,319.5) -- (321.25,288.5) -- cycle ;
|
||||
|
||||
% Text Node
|
||||
\draw (201,129) node [anchor=north west][inner sep=0.75pt] [align=left] {tables};
|
||||
% Text Node
|
||||
\draw (307,130) node [anchor=north west][inner sep=0.75pt] [align=left ] {tables};
|
||||
% Text Node
|
||||
\draw (414,127) node [anchor=north west][inner sep=0.75pt] [align=left] {tables};
|
||||
% Text Node
|
||||
\draw (272,203) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 0; green, 0; blue, 0 } ,opacity=1 ] [align=center] { \\ views \\ + \\ \ \ stored procedures};
|
||||
|
||||
% Text Node
|
||||
\draw (322,178) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{api}};
|
||||
% Text Node
|
||||
\draw (190,97) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{internal}};
|
||||
% Text Node
|
||||
\draw (300,99) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{private}};
|
||||
% Text Node
|
||||
\draw (417,101) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{core}};
|
||||
% Text Node
|
||||
\draw (358,306) node [anchor=north west][inner sep=0.75pt] [align=left] {REST};
|
||||
|
||||
\end{tikzpicture}
|
||||
|
||||
|
||||
\end{document}
|
||||
@@ -0,0 +1,12 @@
|
||||
# Build using: -e ortho
|
||||
|
||||
entity {font: "FreeSans"}
|
||||
relationship {font: "FreeMono"}
|
||||
|
||||
[Employees]
|
||||
*id
|
||||
first_name
|
||||
last_name
|
||||
+supervisor_id
|
||||
|
||||
Employees 1--* Employees
|
||||
@@ -0,0 +1,51 @@
|
||||
entity {font: "FreeSans"}
|
||||
relationship {font: "FreeSerif"}
|
||||
|
||||
[Films]
|
||||
*id
|
||||
+director_id
|
||||
title
|
||||
year
|
||||
rating
|
||||
language
|
||||
|
||||
[Directors]
|
||||
*id
|
||||
first_name
|
||||
last_name
|
||||
|
||||
[Actors]
|
||||
*id
|
||||
first_name
|
||||
last_name
|
||||
|
||||
[Roles]
|
||||
*+film_id
|
||||
*+actor_id
|
||||
character
|
||||
|
||||
[Competitions]
|
||||
*id
|
||||
name
|
||||
year
|
||||
|
||||
[Nominations]
|
||||
*+competition_id
|
||||
*+film_id
|
||||
rank
|
||||
|
||||
[Technical_Specs]
|
||||
*+film_id
|
||||
runtime
|
||||
camera
|
||||
sound
|
||||
|
||||
Roles *--1 Actors
|
||||
Roles *--1 Films
|
||||
|
||||
Nominations *--1 Competitions
|
||||
Nominations *--1 Films
|
||||
|
||||
Films *--1 Directors
|
||||
|
||||
Films 1--1 Technical_Specs
|
||||
@@ -0,0 +1,20 @@
|
||||
# Build using: -e ortho
|
||||
|
||||
entity {font: "FreeSans"}
|
||||
relationship {font: "FreeMono"}
|
||||
|
||||
[Addresses]
|
||||
*id
|
||||
name
|
||||
city
|
||||
state
|
||||
postal_code
|
||||
|
||||
[Orders]
|
||||
*id
|
||||
name
|
||||
+billing_address_id
|
||||
+shipping_address_id
|
||||
|
||||
Orders *--1 Addresses
|
||||
Orders *--1 Addresses
|
||||
@@ -0,0 +1,16 @@
|
||||
entity {font: "FreeSans"}
|
||||
relationship {font: "FreeMono"}
|
||||
|
||||
[Premieres]
|
||||
*id
|
||||
location
|
||||
date
|
||||
+film_id
|
||||
|
||||
[Films]
|
||||
*id
|
||||
+director_id
|
||||
title
|
||||
`...`
|
||||
|
||||
Premieres *--1 Films
|
||||
@@ -0,0 +1,12 @@
|
||||
# Build using: -e ortho
|
||||
|
||||
entity {font: "FreeSans"}
|
||||
relationship {font: "FreeMono"}
|
||||
|
||||
[Presidents]
|
||||
*id
|
||||
first_name
|
||||
last_name
|
||||
+predecessor_id
|
||||
|
||||
Presidents 1--? Presidents
|
||||
@@ -0,0 +1,18 @@
|
||||
# Build using: -e ortho
|
||||
|
||||
entity {font: "FreeSans"}
|
||||
relationship {font: "FreeMono"}
|
||||
|
||||
[Users]
|
||||
*id
|
||||
first_name
|
||||
last_name
|
||||
username
|
||||
|
||||
[Subscriptions]
|
||||
*+subscriber_id
|
||||
*+subscribed_id
|
||||
type
|
||||
|
||||
Users 1--* Subscriptions
|
||||
Subscriptions *--1 Users
|
||||
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,95 @@
|
||||
.wy-nav-content {
|
||||
max-width: initial;
|
||||
}
|
||||
|
||||
#postgrest-documentation > h1 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div.wy-menu.rst-pro {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
div.highlight {
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
div.line-block {
|
||||
margin-bottom: 0px !important;
|
||||
}
|
||||
|
||||
#sponsors {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#sponsors h2 {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#sponsors img{
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#thanks{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#thanks img{
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
#thanks h2{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#thanks p{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#thanks ul{
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
max-width: 800px;
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.wy-table-responsive table td {
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.wy-table-responsive {
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
#tutorials span.caption-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#references span.caption-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#explanations span.caption-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#how-tos span.caption-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#ecosystem span.caption-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#integrations span.caption-text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#api span.caption-text {
|
||||
display: none;
|
||||
}
|
||||
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 468 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 187 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 31 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# sphinx-intl fails if LC_ALL is not set
|
||||
export LC_ALL=${LC_ALL:-C}
|
||||
|
||||
function build() {
|
||||
sphinx-build --color -W -a -n . -b "$@"
|
||||
}
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
# clean previous build, otherwise some errors might be supressed
|
||||
rm -rf "_build/html/default"
|
||||
|
||||
if [ -d languages ]; then
|
||||
# default to updating all existing locales
|
||||
build gettext _build/gettext
|
||||
sphinx-intl update -p _build/gettext
|
||||
fi
|
||||
|
||||
build html "_build/html/default"
|
||||
else
|
||||
# clean previous build, otherwise some errors might be supressed
|
||||
rm -rf "_build/html/$1"
|
||||
|
||||
# update and build specific locale, can be used to create new locale
|
||||
build gettext _build/gettext
|
||||
sphinx-intl update -p _build/gettext -l "$1"
|
||||
|
||||
build html "_build/html/$1" -D "language=$1"
|
||||
fi
|
||||
@@ -0,0 +1,318 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# PostgREST documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Oct 9 16:53:00 2016.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
"sphinx_tabs.tabs",
|
||||
"sphinx_copybutton",
|
||||
"sphinxext.opengraph",
|
||||
]
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ["_templates"]
|
||||
|
||||
# The suffix(es) of source filenames.
|
||||
# You can specify multiple suffix as a list of string:
|
||||
# source_suffix = ['.rst', '.md']
|
||||
source_suffix = ".rst"
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = "index"
|
||||
|
||||
# General information about the project.
|
||||
project = "PostgREST"
|
||||
author = "Joe Nelson, Steve Chavez"
|
||||
copyright = "2017, " + author
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = "11.2"
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = "11.2.0"
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#
|
||||
# This is also used if you do content translation via gettext catalogs.
|
||||
# Usually you set "language" from the command line for these cases.
|
||||
language = "en"
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
# This patterns also effect to html_static_path and html_extra_path
|
||||
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "shared/*.rst"]
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = "sphinx"
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
# keep_warnings = False
|
||||
|
||||
# If true, `todo` and `todoList` produce output, else they produce nothing.
|
||||
todo_include_todos = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = "sphinx_rtd_theme"
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents.
|
||||
# "<project> v<release> documentation" by default.
|
||||
# html_title = u'PostgREST v0.4.0.0'
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (relative to this directory) to use as a favicon of
|
||||
# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
html_favicon = "_static/favicon.ico"
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ["_static"]
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
# html_extra_path = []
|
||||
|
||||
# If not None, a 'Last updated on:' timestamp is inserted at every page
|
||||
# bottom, using the given strftime format.
|
||||
# The empty string is equivalent to '%b %d, %Y'.
|
||||
# html_last_updated_fmt = None
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# Language to be used for generating the HTML full-text search index.
|
||||
# Sphinx supports the following languages:
|
||||
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
|
||||
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh'
|
||||
# html_search_language = 'en'
|
||||
|
||||
# A dictionary with options for the search language support, empty by default.
|
||||
# 'ja' uses this config value.
|
||||
# 'zh' user can custom change `jieba` dictionary path.
|
||||
# html_search_options = {'type': 'default'}
|
||||
|
||||
# The name of a javascript file (relative to the configuration directory) that
|
||||
# implements a search results scorer. If empty, the default will be used.
|
||||
# html_search_scorer = 'scorer.js'
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = "PostgRESTdoc"
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
# Latex figure (float) alignment
|
||||
#'figure_align': 'htbp',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
(master_doc, "PostgREST.tex", "PostgREST Documentation", author, "manual"),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [(master_doc, "postgrest", "PostgREST Documentation", [author], 1)]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- Options for Texinfo output -------------------------------------------
|
||||
|
||||
# Grouping the document tree into Texinfo files. List of tuples
|
||||
# (source start file, target name, title, author,
|
||||
# dir menu entry, description, category)
|
||||
texinfo_documents = [
|
||||
(
|
||||
master_doc,
|
||||
"PostgREST",
|
||||
"PostgREST Documentation",
|
||||
author,
|
||||
"PostgREST",
|
||||
"REST API for any PostgreSQL database",
|
||||
"Web",
|
||||
),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
# texinfo_no_detailmenu = False
|
||||
|
||||
# -- Custom setup ---------------------------------------------------------
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.add_css_file("css/custom.css")
|
||||
|
||||
|
||||
# taken from https://github.com/sphinx-doc/sphinx/blob/82dad44e5bd3776ecb6fd8ded656bc8151d0e63d/sphinx/util/requests.py#L42
|
||||
user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0"
|
||||
|
||||
# TODO: stackoverflow only returns 403 right now. We might need to come back later to check whether that's
|
||||
# a permanent issue or not.
|
||||
linkcheck_ignore = [r"https://stackoverflow.com/"]
|
||||
|
||||
# sphinx-tabs configuration
|
||||
sphinx_tabs_disable_tab_closing = True
|
||||
|
||||
# sphinxext-opengraph configuration
|
||||
|
||||
ogp_image = "_images/logo.png"
|
||||
ogp_use_first_image = True
|
||||
ogp_enable_meta_description = True
|
||||
ogp_description_length = 300
|
||||
|
||||
## RTD sets html_baseurl, ensures we use the correct env for canonical URLs
|
||||
## Useful to generate correct meta tags for Open Graph
|
||||
## Refs: https://github.com/readthedocs/readthedocs.org/issues/10226, https://github.com/urllib3/urllib3/pull/3064
|
||||
html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "/")
|
||||
@@ -0,0 +1,109 @@
|
||||
let
|
||||
# Commit of the Nixpkgs repository that we want to use.
|
||||
nixpkgsVersion = {
|
||||
date = "2024-01-06";
|
||||
rev = "4bbf5a2eb6046c54f7a29a0964c642ebfe912cbc";
|
||||
tarballHash = "03p45qdcxqxc41mmzmmyzbkff29vv95vv643z0kd3mf1s2nnsy5b";
|
||||
};
|
||||
|
||||
# Nix files that describe the Nixpkgs repository. We evaluate the expression
|
||||
# using `import` below.
|
||||
pkgs = import
|
||||
(fetchTarball {
|
||||
url = "https://github.com/nixos/nixpkgs/archive/${nixpkgsVersion.rev}.tar.gz";
|
||||
sha256 = nixpkgsVersion.tarballHash;
|
||||
})
|
||||
{ };
|
||||
|
||||
python = pkgs.python3.withPackages (ps: [
|
||||
ps.sphinx
|
||||
ps.sphinx_rtd_theme
|
||||
ps.livereload
|
||||
ps.sphinx-tabs
|
||||
ps.sphinx-copybutton
|
||||
ps.sphinxext-opengraph
|
||||
# TODO: Remove override once new sphinx-intl version (> 2.1.0) is released and available in nixpkgs
|
||||
(ps.sphinx-intl.overrideAttrs (drv: { nativeBuildInputs = drv.nativeBuildInputs ++ [ ps.six ]; }))
|
||||
]);
|
||||
in
|
||||
rec {
|
||||
inherit pkgs;
|
||||
|
||||
build =
|
||||
pkgs.writeShellScriptBin "postgrest-docs-build"
|
||||
''
|
||||
set -euo pipefail
|
||||
cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/docs"
|
||||
|
||||
# build.sh needs to find "sphinx-build"
|
||||
PATH=${python}/bin:$PATH
|
||||
|
||||
./build.sh "$@"
|
||||
'';
|
||||
|
||||
serve =
|
||||
pkgs.writeShellScriptBin "postgrest-docs-serve"
|
||||
''
|
||||
set -euo pipefail
|
||||
cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/docs"
|
||||
|
||||
# livereload_docs.py needs to find "sphinx-build"
|
||||
PATH=${python}/bin:$PATH
|
||||
|
||||
./livereload_docs.py "$@"
|
||||
'';
|
||||
|
||||
spellcheck =
|
||||
pkgs.writeShellScriptBin "postgrest-docs-spellcheck"
|
||||
''
|
||||
set -euo pipefail
|
||||
cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/docs"
|
||||
|
||||
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
|
||||
|
||||
cat $FILES \
|
||||
| grep -v '^\(\.\.\| \)' \
|
||||
| sed 's/`.*`//g' \
|
||||
| ${pkgs.aspell}/bin/aspell -d ${pkgs.aspellDicts.en}/lib/aspell/en_US -p ./postgrest.dict list \
|
||||
| sort -f \
|
||||
| tee misspellings
|
||||
test ! -s misspellings
|
||||
'';
|
||||
|
||||
# dictcheck detects obsolete entries in postgrest.dict, that are not used anymore
|
||||
dictcheck =
|
||||
pkgs.writeShellScriptBin "postgrest-docs-dictcheck"
|
||||
''
|
||||
set -euo pipefail
|
||||
cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/docs"
|
||||
|
||||
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
|
||||
|
||||
cat postgrest.dict \
|
||||
| tail -n+2 \
|
||||
| tr '\n' '\0' \
|
||||
| xargs -0 -n 1 -i \
|
||||
sh -c "grep \"{}\" $FILES > /dev/null || echo \"{}\"" \
|
||||
| tee unuseddict
|
||||
test ! -s unuseddict
|
||||
'';
|
||||
|
||||
linkcheck =
|
||||
pkgs.writeShellScriptBin "postgrest-docs-linkcheck"
|
||||
''
|
||||
set -euo pipefail
|
||||
cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/docs"
|
||||
|
||||
${python}/bin/sphinx-build --color -b linkcheck . _build
|
||||
'';
|
||||
|
||||
check =
|
||||
pkgs.writeShellScriptBin "postgrest-docs-check"
|
||||
''
|
||||
set -euo pipefail
|
||||
${build}/bin/postgrest-docs-build
|
||||
${dictcheck}/bin/postgrest-docs-dictcheck
|
||||
${linkcheck}/bin/postgrest-docs-linkcheck
|
||||
${spellcheck}/bin/postgrest-docs-spellcheck
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
.. _community_tutorials:
|
||||
|
||||
Community Tutorials
|
||||
-------------------
|
||||
|
||||
* `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ -
|
||||
In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.
|
||||
|
||||
* `PostgREST + Auth0: Create REST API in mintutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
|
||||
|
||||
* `PostgREST + PostGIS API tutorial in 5 minutes <https://gis-ops.com/postgrest-postgis-api-tutorial-geospatial-api-in-5-minutes/>`_ -
|
||||
In this tutorial, GIS • OPS shows how to perform PostGIS calculations through PostgREST :ref:`s_procs` interface.
|
||||
|
||||
* `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ -
|
||||
A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service).
|
||||
|
||||
* `How PostgreSQL triggers work when called with a PostgREST PATCH HTTP request <https://blog.fgribreau.com/2020/11/how-postgresql-triggers-works-when.html>`_ - A tutorial to see how the old and new values are set or not when doing a PATCH request to PostgREST.
|
||||
|
||||
* `REST Data Service on YugabyteDB / PostgreSQL <https://dev.to/yugabyte/rest-data-service-on-yugabytedb-postgresql-5f2h>`_
|
||||
|
||||
* `Build data-driven applications with Workers and PostgreSQL <https://developers.cloudflare.com/workers/tutorials/postgres/>`_ - A tutorial on how to integrate with PostgREST and PostgreSQL using Cloudflare Workers.
|
||||
|
||||
* `A poor man's API <https://blog.frankel.ch/poor-man-api>`_ - Shows how to integrate PostgREST with Apache APISIX as an alternative to Nginx.
|
||||
|
||||
.. * `Accessing a PostgreSQL database in Godot 4 via PostgREST <https://peterkingsbury.com/2022/08/16/godot-postgresql-postgrest/>`_
|
||||
|
||||
.. _templates:
|
||||
|
||||
Templates
|
||||
---------
|
||||
|
||||
* `compose-postgrest <https://github.com/mattddowney/compose-postgrest>`_ - docker-compose setup with Nginx and HTML example
|
||||
* `svelte-postgrest-template <https://github.com/guyromm/svelte-postgrest-template>`_ - Svelte/SvelteKit, PostgREST, EveryLayout and social auth
|
||||
|
||||
.. _eco_example_apps:
|
||||
|
||||
Example Apps
|
||||
------------
|
||||
|
||||
* `delibrium-postgrest <https://gitlab.com/delibrium/delibrium-postgrest/>`_ - example school API and front-end in Vue.js
|
||||
* `ETH-transactions-storage <https://github.com/Adamant-im/ETH-transactions-storage>`_ - indexer for Ethereum to get transaction list by ETH address
|
||||
* `general <https://github.com/PierreRochard/general>`_ - example auth back-end
|
||||
* `guild-operators <https://github.com/cardano-community/koios-artifacts/tree/main/files/grest>`_ - example queries and functions that the Cardano Community uses for their Guild Operators' Repository
|
||||
* `PostGUI <https://github.com/priyank-purohit/PostGUI>`_ - React Material UI admin panel
|
||||
* `prospector <https://github.com/sfcta/prospector>`_ - data warehouse and visualization platform
|
||||
|
||||
.. _devops:
|
||||
|
||||
DevOps
|
||||
------
|
||||
|
||||
* `cloudgov-demo-postgrest <https://github.com/GSA/cloudgov-demo-postgrest>`_ - demo for a federally-compliant REST API on cloud.gov
|
||||
* `cloudstark/helm-charts <https://github.com/cloudstark/helm-charts/tree/master/postgrest>`_ - helm chart to deploy PostgREST to a Kubernetes cluster via a Deployment and Service
|
||||
* `cyril-sabourault/postgrest-cloud-run <https://github.com/cyril-sabourault/postgrest-cloud-run>`_ - expose a PostgreSQL database on Cloud SQL using Cloud Run
|
||||
* `eyberg/postgrest <https://repo.ops.city/v2/packages/eyberg/postgrest/10.1.1/x86_64/show>`_ - run PostgREST as a Nanos unikernel
|
||||
* `jbkarle/postgrest <https://github.com/jbkarle/postgrest>`_ - helm chart with a demo database for development and test purposes
|
||||
|
||||
.. _eco_external_notification:
|
||||
|
||||
External Notification
|
||||
---------------------
|
||||
|
||||
These are PostgreSQL bridges that propagate LISTEN/NOTIFY to external queues for further processing. This allows stored procedures to initiate actions outside the database such as sending emails.
|
||||
|
||||
* `pg-notify-webhook <https://github.com/vbalasu/pg-notify-webhook>`_ - trigger webhooks from PostgreSQL's LISTEN/NOTIFY
|
||||
* `pgsql-listen-exchange <https://github.com/gmr/pgsql-listen-exchange>`_ - RabbitMQ
|
||||
* `postgres-websockets <https://github.com/diogob/postgres-websockets>`_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY
|
||||
* `postgresql2websocket <https://github.com/frafra/postgresql2websocket>`_ - Websockets
|
||||
|
||||
|
||||
.. _eco_extensions:
|
||||
|
||||
Extensions
|
||||
----------
|
||||
|
||||
* `aiodata <https://github.com/Exahilosys/aiodata>`_ - Python, event-based proxy and caching client.
|
||||
* `pg-safeupdate <https://github.com/eradman/pg-safeupdate>`_ - prevent full-table updates or deletes
|
||||
* `postgrest-node <https://github.com/seveibar/postgrest-node>`_ - Run a PostgREST server in Node.js via npm module
|
||||
* `PostgREST-writeAPI <https://github.com/ppKrauss/PostgREST-writeAPI>`_ - generate Nginx rewrite rules to fit an OpenAPI spec
|
||||
|
||||
.. _clientside_libraries:
|
||||
|
||||
Client-Side Libraries
|
||||
---------------------
|
||||
|
||||
* `postgrest-csharp <https://github.com/supabase-community/postgrest-csharp>`_ - C#
|
||||
* `postgrest-dart <https://github.com/supabase/postgrest-dart>`_ - Dart
|
||||
* `postgrest-ex <https://github.com/supabase-community/postgrest-ex>`_ - Elixir
|
||||
* `postgrest-go <https://github.com/supabase-community/postgrest-go>`_ - Go
|
||||
* `postgrest-js <https://github.com/supabase/postgrest-js>`_ - TypeScript/JavaScript
|
||||
* `postgrest-kt <https://github.com/supabase-community/postgrest-kt>`_ - Kotlin
|
||||
* `postgrest-py <https://github.com/supabase-community/postgrest-py>`_ - Python
|
||||
* `postgrest-rs <https://github.com/supabase-community/postgrest-rs>`_ - Rust
|
||||
* `postgrest-swift <https://github.com/supabase-community/postgrest-swift>`_ - Swift
|
||||
* `redux-postgrest <https://github.com/andytango/redux-postgrest>`_ - TypeScript/JS, client integrated with (React) Redux.
|
||||
* `vue-postgrest <https://github.com/technowledgy/vue-postgrest>`_ - Vue.js
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
.. _db_authz:
|
||||
|
||||
Database Authorization
|
||||
######################
|
||||
|
||||
Database authorization is the process of granting and verifying database access permissions. PostgreSQL manages permissions using the concept of roles.
|
||||
|
||||
Users and Groups
|
||||
================
|
||||
|
||||
A role can be thought of as either a database user, or a group of database users, depending on how the role is set up.
|
||||
|
||||
Roles for Each Web User
|
||||
-----------------------
|
||||
|
||||
PostgREST can accommodate either viewpoint. If you treat a role as a single user then the :ref:`jwt_impersonation` does most of what you need. When an authenticated user makes a request PostgREST will switch into the database role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable.
|
||||
|
||||
You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example <https://www.2ndquadrant.com/en/blog/application-users-vs-row-level-security/>`_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE TABLE chat (
|
||||
message_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||
message_time TIMESTAMP NOT NULL DEFAULT now(),
|
||||
message_from NAME NOT NULL DEFAULT current_user,
|
||||
message_to NAME NOT NULL,
|
||||
message_subject VARCHAR(64) NOT NULL,
|
||||
message_body TEXT
|
||||
);
|
||||
|
||||
ALTER TABLE chat ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
We want to enforce a policy that ensures a user can see only those messages sent by them or intended for them. Also we want to prevent a user from forging the ``message_from`` column with another person's name.
|
||||
|
||||
PostgreSQL allows us to set this policy with row-level security:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE POLICY chat_policy ON chat
|
||||
USING ((message_to = current_user) OR (message_from = current_user))
|
||||
WITH CHECK (message_from = current_user)
|
||||
|
||||
Anyone accessing the generated API endpoint for the chat table will see exactly the rows they should, without our needing custom imperative server-side coding.
|
||||
|
||||
.. warning::
|
||||
|
||||
Roles are namespaced per-cluster rather than per-database so they may be prone to collision.
|
||||
|
||||
Web Users Sharing Role
|
||||
----------------------
|
||||
|
||||
Alternately database roles can represent groups instead of (or in addition to) individual users. You may choose that all signed-in users for a web app share the role ``webuser``. You can distinguish individual users by including extra claims in the JWT such as email.
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"role": "webuser",
|
||||
"email": "john@doe.com"
|
||||
}
|
||||
|
||||
SQL code can access claims through PostgREST :ref:`tx_settings`. For instance to get the email claim, call this function:
|
||||
|
||||
.. code:: sql
|
||||
|
||||
current_setting('request.jwt.claims', true)::json->>'email';
|
||||
|
||||
.. note::
|
||||
|
||||
For PostgreSQL < 14
|
||||
|
||||
.. code:: sql
|
||||
|
||||
current_setting('request.jwt.claim.email', true);
|
||||
|
||||
This allows JWT generation services to include extra information and your database code to react to it. For instance the RLS example could be modified to use this ``current_setting`` rather than ``current_user``. The second ``'true'`` argument tells ``current_setting`` to return NULL if the setting is missing from the current configuration.
|
||||
|
||||
Hybrid User-Group Roles
|
||||
-----------------------
|
||||
|
||||
You can mix the group and individual role policies. For instance we could still have a webuser role and individual users which inherit from it:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE ROLE webuser NOLOGIN;
|
||||
-- grant this role access to certain tables etc
|
||||
|
||||
CREATE ROLE user000 NOLOGIN;
|
||||
GRANT webuser TO user000;
|
||||
-- now user000 can do whatever webuser can
|
||||
|
||||
GRANT user000 TO authenticator;
|
||||
-- allow authenticator to switch into user000 role
|
||||
-- (the role itself has nologin)
|
||||
|
||||
Schemas
|
||||
=======
|
||||
|
||||
You must explicitly allow roles to access the exposed schemas in :ref:`db-schemas`.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
GRANT USAGE ON SCHEMA api TO webuser;
|
||||
|
||||
Tables
|
||||
======
|
||||
|
||||
To let web users access tables you must grant them privileges for the operations you want them to do.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
GRANT
|
||||
SELECT
|
||||
, INSERT
|
||||
, UPDATE(message_body)
|
||||
, DELETE
|
||||
ON chat TO webuser;
|
||||
|
||||
You can also choose on which table columns the operation is valid. In the above example, the web user can only update the ``message_body`` column.
|
||||
|
||||
.. _func_privs:
|
||||
|
||||
Functions
|
||||
=========
|
||||
|
||||
By default, when a function is created, the privilege to execute it is not restricted by role. The function access is ``PUBLIC`` — executable by all roles (more details at `PostgreSQL Privileges page <https://www.postgresql.org/docs/current/ddl-priv.html>`_). This is not ideal for an API schema. To disable this behavior, you can run the following SQL statement:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC;
|
||||
|
||||
This will change the privileges for all functions created in the future in all schemas. Currently there is no way to limit it to a single schema. In our opinion it's a good practice anyway.
|
||||
|
||||
.. note::
|
||||
|
||||
It is however possible to limit the effect of this clause only to functions you define. You can put the above statement at the beginning of the API schema definition, and then at the end reverse it with:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO PUBLIC;
|
||||
|
||||
This will work because the :code:`alter default privileges` statement has effect on function created *after* it is executed. See `PostgreSQL alter default privileges <https://www.postgresql.org/docs/current/sql-alterdefaultprivileges.html>`_ for more details.
|
||||
|
||||
After that, you'll need to grant EXECUTE privileges on functions explicitly:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
GRANT EXECUTE ON FUNCTION login TO anonymous;
|
||||
GRANT EXECUTE ON FUNCTION signup TO anonymous;
|
||||
|
||||
You can also grant execute on all functions in a schema to a higher privileged role:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA api TO web_user;
|
||||
|
||||
Security definer
|
||||
----------------
|
||||
|
||||
A function is executed with the privileges of the user who calls it. This means that the user has to have all permissions to do the operations the procedure performs.
|
||||
If the function accesses private database objects, your :ref:`API roles <roles>` won't be able to successfully execute the function.
|
||||
|
||||
Another option is to define the function with the :code:`SECURITY DEFINER` option. Then only one permission check will take place, the permission to call the function, and the operations in the function will have the authority of the user who owns the function itself.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- login as a user wich has privileges on the private schemas
|
||||
|
||||
-- create a sample function
|
||||
create or replace function login(email text, pass text) returns jwt_token as $$
|
||||
begin
|
||||
-- access to a private schema called 'auth'
|
||||
select auth.user_role(email, pass) into _role;
|
||||
-- other operations
|
||||
-- ...
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
|
||||
Note the ``SECURITY DEFINER`` keywords at the end of the function. See `PostgreSQL documentation <https://www.postgresql.org/docs/current/sql-createfunction.html#SQL-CREATEFUNCTION-SECURITY>`_ for more details.
|
||||
|
||||
Views
|
||||
=====
|
||||
|
||||
Views are invoked with the privileges of the view owner, much like stored procedures with the ``SECURITY DEFINER`` option. When created by a SUPERUSER role, all `row-level security <https://www.postgresql.org/docs/current/ddl-rowsecurity.html>`_ policies will be bypassed.
|
||||
|
||||
If you're on PostgreSQL >= 15, this behavior can be changed by specifying the ``security_invoker`` option.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE VIEW sample_view WITH (security_invoker = true) AS
|
||||
SELECT * FROM sample_table;
|
||||
|
||||
On PostgreSQL < 15, you can create a non-SUPERUSER role and make this role the view's owner.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE ROLE api_views_owner NOSUPERUSER NOBYPASSRLS;
|
||||
ALTER VIEW sample_view OWNER TO api_views_owner;
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
.. _install:
|
||||
|
||||
Installation
|
||||
############
|
||||
|
||||
The release page has `pre-compiled binaries for macOS, Windows, Linux and FreeBSD <https://github.com/PostgREST/postgrest/releases/latest>`_ .
|
||||
The Linux binary is a static executable that can be run on any Linux distribution.
|
||||
|
||||
You can also use your OS package manager.
|
||||
|
||||
.. include:: ../shared/installation.rst
|
||||
|
||||
.. _pg-dependency:
|
||||
|
||||
Supported PostgreSQL versions
|
||||
=============================
|
||||
|
||||
=============== =================================
|
||||
**Supported** PostgreSQL >= 9.6
|
||||
=============== =================================
|
||||
|
||||
PostgREST works with all PostgreSQL versions starting from 9.6.
|
||||
|
||||
Running PostgREST
|
||||
=================
|
||||
|
||||
If you downloaded PostgREST from the release page, first extract the compressed file to obtain the executable.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# For UNIX platforms
|
||||
tar Jxf postgrest-[version]-[platform].tar.xz
|
||||
|
||||
# On Windows you should unzip the file
|
||||
|
||||
Now you can run PostgREST with the :code:`--help` flag to see usage instructions:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Running postgrest binary
|
||||
./postgrest --help
|
||||
|
||||
# Running postgrest installed from a package manager
|
||||
postgrest --help
|
||||
|
||||
# You should see a usage help message
|
||||
|
||||
The PostgREST server reads a configuration file as its only argument:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
postgrest /path/to/postgrest.conf
|
||||
|
||||
# You can also generate a sample config file with
|
||||
# postgrest -e > postgrest.conf
|
||||
# You'll need to edit this file and remove the usage parts for postgrest to read it
|
||||
|
||||
For a complete reference of the configuration file, see :ref:`configuration`.
|
||||
|
||||
.. note::
|
||||
|
||||
If you see a dialog box like this on Windows, it may be that the :code:`pg_config` program is not in your system path.
|
||||
|
||||
.. image:: ../_static/win-err-dialog.png
|
||||
|
||||
It usually lives in :code:`C:\Program Files\PostgreSQL\<version>\bin`. See this `article <https://www.howtogeek.com/118594/how-to-edit-your-system-path-for-easy-command-line-access/>`_ about how to modify the system path.
|
||||
|
||||
To test that the system path is set correctly, run ``pg_config`` from the command line. You should see it output a list of paths.
|
||||
|
||||
Docker
|
||||
======
|
||||
|
||||
You can get the `official PostgREST Docker image <https://hub.docker.com/r/postgrest/postgrest>`_ with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
docker pull postgrest/postgrest
|
||||
|
||||
To configure the container image, use :ref:`env_variables_config`.
|
||||
|
||||
There are two ways to run the PostgREST container: with an existing external database, or through docker-compose.
|
||||
|
||||
Containerized PostgREST with native PostgreSQL
|
||||
----------------------------------------------
|
||||
|
||||
The first way to run PostgREST in Docker is to connect it to an existing native database on the host.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run the server
|
||||
docker run --rm --net=host \
|
||||
-e PGRST_DB_URI="postgres://app_user:password@localhost/postgres" \
|
||||
postgrest/postgrest
|
||||
|
||||
The database connection string above is just an example. Adjust the role and password as necessary. You may need to edit PostgreSQL's :code:`pg_hba.conf` to grant the user local login access.
|
||||
|
||||
.. note::
|
||||
|
||||
Docker on Mac does not support the :code:`--net=host` flag. Instead you'll need to create an IP address alias to the host. Requests for the IP address from inside the container are unable to resolve and fall back to resolution by the host.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo ifconfig lo0 10.0.0.10 alias
|
||||
|
||||
You should then use 10.0.0.10 as the host in your database connection string. Also remember to include the IP address in the :code:`listen_address` within postgresql.conf. For instance:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
listen_addresses = 'localhost,10.0.0.10'
|
||||
|
||||
You might also need to add a new IPv4 local connection within pg_hba.conf. For instance:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
host all all 10.0.0.10/32 trust
|
||||
|
||||
The docker command will then look like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Run the server
|
||||
docker run --rm -p 3000:3000 \
|
||||
-e PGRST_DB_URI="postgres://app_user:password@10.0.0.10/postgres" \
|
||||
postgrest/postgrest
|
||||
|
||||
.. _pg-in-docker:
|
||||
|
||||
Containerized PostgREST *and* db with docker-compose
|
||||
----------------------------------------------------
|
||||
|
||||
To avoid having to install the database at all, you can run both it and the server in containers and link them together with docker-compose. Use this configuration:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
# docker-compose.yml
|
||||
|
||||
version: '3'
|
||||
services:
|
||||
server:
|
||||
image: postgrest/postgrest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
PGRST_DB_URI: postgres://app_user:password@db:5432/app_db
|
||||
PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000
|
||||
depends_on:
|
||||
- db
|
||||
db:
|
||||
image: postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_DB: app_db
|
||||
POSTGRES_USER: app_user
|
||||
POSTGRES_PASSWORD: password
|
||||
# Uncomment this if you want to persist the data.
|
||||
# volumes:
|
||||
# - "./pgdata:/var/lib/postgresql/data"
|
||||
|
||||
Go into the directory where you saved this file and run :code:`docker-compose up`. You will see the logs of both the database and PostgREST, and be able to access the latter on port 3000.
|
||||
|
||||
If you want to have a visual overview of your API in your browser you can add swagger-ui to your :code:`docker-compose.yml`:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
swagger:
|
||||
image: swaggerapi/swagger-ui
|
||||
ports:
|
||||
- "8080:8080"
|
||||
expose:
|
||||
- "8080"
|
||||
environment:
|
||||
API_URL: http://localhost:3000/
|
||||
|
||||
With this you can see the swagger-ui in your browser on port 8080.
|
||||
|
||||
.. _build_source:
|
||||
|
||||
Building from Source
|
||||
====================
|
||||
|
||||
When a pre-built binary does not exist for your system you can build the project from source.
|
||||
|
||||
.. note::
|
||||
|
||||
We discourage building and using PostgREST on **Alpine Linux** because of a reported GHC memory leak on that platform.
|
||||
|
||||
You can build PostgREST from source with `Stack <https://github.com/commercialhaskell/stack>`_. It will install any necessary Haskell dependencies on your system.
|
||||
|
||||
* `Install Stack <https://docs.haskellstack.org/en/stable/README/#how-to-install-stack>`_ for your platform
|
||||
* Install Library Dependencies
|
||||
|
||||
===================== =======================================
|
||||
Operating System Dependencies
|
||||
===================== =======================================
|
||||
Ubuntu/Debian libpq-dev, libgmp-dev, zlib1g-dev
|
||||
CentOS/Fedora/Red Hat postgresql-devel, zlib-devel, gmp-devel
|
||||
BSD postgresql12-client
|
||||
macOS libpq, gmp
|
||||
===================== =======================================
|
||||
|
||||
* Build and install binary
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone https://github.com/PostgREST/postgrest.git
|
||||
cd postgrest
|
||||
|
||||
# adjust local-bin-path to taste
|
||||
stack build --install-ghc --copy-bins --local-bin-path /usr/local/bin
|
||||
|
||||
.. note::
|
||||
|
||||
- If building fails and your system has less than 1GB of memory, try adding a swap file.
|
||||
- `--install-ghc` flag is only needed for the first build and can be omitted in the subsequent builds.
|
||||
|
||||
* Check that the server is installed: :code:`postgrest --help`.
|
||||
@@ -0,0 +1,102 @@
|
||||
.. _nginx:
|
||||
|
||||
Nginx
|
||||
=====
|
||||
|
||||
PostgREST is a fast way to construct a RESTful API. Its default behavior is great for scaffolding in development. When it's time to go to production it works great too, as long as you take precautions.
|
||||
PostgREST is a small sharp tool that focuses on performing the API-to-database mapping. We rely on a reverse proxy like Nginx for additional safeguards.
|
||||
|
||||
The first step is to create an Nginx configuration file that proxies requests to an underlying PostgREST server.
|
||||
|
||||
.. code-block:: nginx
|
||||
|
||||
http {
|
||||
# ...
|
||||
# upstream configuration
|
||||
upstream postgrest {
|
||||
server localhost:3000;
|
||||
}
|
||||
# ...
|
||||
server {
|
||||
# ...
|
||||
# expose to the outside world
|
||||
location /api/ {
|
||||
default_type application/json;
|
||||
proxy_hide_header Content-Location;
|
||||
add_header Content-Location /api/$upstream_http_content_location;
|
||||
proxy_set_header Connection "";
|
||||
proxy_http_version 1.1;
|
||||
proxy_pass http://postgrest/;
|
||||
}
|
||||
# ...
|
||||
}
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
For ubuntu, if you already installed nginx through :code:`apt` you can add this to the config file in
|
||||
:code:`/etc/nginx/sites-enabled/default`.
|
||||
|
||||
.. _https:
|
||||
|
||||
HTTPS
|
||||
-----
|
||||
|
||||
PostgREST aims to do one thing well: add an HTTP interface to a PostgreSQL database. To keep the code small and focused we do not implement HTTPS. Use a reverse proxy such as NGINX to add this, `here's how <https://nginx.org/en/docs/http/configuring_https_servers.html>`_.
|
||||
|
||||
Rate Limiting
|
||||
-------------
|
||||
|
||||
Nginx supports "leaky bucket" rate limiting (see `official docs <https://nginx.org/en/docs/http/ngx_http_limit_req_module.html>`_). Using standard Nginx configuration, routes can be grouped into *request zones* for rate limiting. For instance we can define a zone for login attempts:
|
||||
|
||||
.. code-block:: nginx
|
||||
|
||||
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
|
||||
|
||||
This creates a shared memory zone called "login" to store a log of IP addresses that access the rate limited urls. The space reserved, 10 MB (:code:`10m`) will give us enough space to store a history of 160k requests. We have chosen to allow only allow one request per second (:code:`1r/s`).
|
||||
|
||||
Next we apply the zone to certain routes, like a hypothetical stored procedure called :code:`login`.
|
||||
|
||||
.. code-block:: nginx
|
||||
|
||||
location /rpc/login/ {
|
||||
# apply rate limiting
|
||||
limit_req zone=login burst=5;
|
||||
}
|
||||
|
||||
The burst argument tells Nginx to start dropping requests if more than five queue up from a specific IP.
|
||||
|
||||
Nginx rate limiting is general and indiscriminate. To rate limit each authenticated request individually you will need to add logic in a :ref:`Custom Validation <custom_validation>` function.
|
||||
|
||||
Alternate URL Structure
|
||||
-----------------------
|
||||
|
||||
As discussed in :ref:`singular_plural`, there are no special URL forms for singular resources in PostgREST, only operators for filtering. Thus there are no URLs like :code:`/people/1`. It would be specified instead as
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?id=eq.1" \
|
||||
-H "Accept: application/vnd.pgrst.object+json"
|
||||
|
||||
This allows compound primary keys and makes the intent for singular response independent of a URL convention.
|
||||
|
||||
Nginx rewrite rules allow you to simulate the familiar URL convention. The following example adds a rewrite rule for all table endpoints, but you'll want to restrict it to those tables that have a numeric simple primary key named "id."
|
||||
|
||||
.. code-block:: nginx
|
||||
|
||||
# support /endpoint/:id url style
|
||||
location ~ ^/([a-z_]+)/([0-9]+) {
|
||||
|
||||
# make the response singular
|
||||
proxy_set_header Accept 'application/vnd.pgrst.object+json';
|
||||
|
||||
# assuming an upstream named "postgrest"
|
||||
proxy_pass http://postgrest/$1?id=eq.$2;
|
||||
|
||||
}
|
||||
|
||||
.. TODO
|
||||
.. Administration
|
||||
.. API Versioning
|
||||
.. HTTP Caching
|
||||
.. Upgrading
|
||||
@@ -0,0 +1,15 @@
|
||||
.. note::
|
||||
|
||||
This page is a work in progress.
|
||||
|
||||
.. _schema_isolation:
|
||||
|
||||
Schema Isolation
|
||||
================
|
||||
|
||||
A PostgREST instance exposes all the tables, views, and stored procedures of a single `PostgreSQL schema <https://www.postgresql.org/docs/current/ddl-schemas.html>`_ (a namespace of database objects). This means private data or implementation details can go inside different private schemas and be invisible to HTTP clients.
|
||||
|
||||
It is recommended that you don't expose tables on your API schema. Instead expose views and stored procedures which insulate the internal details from the outside world.
|
||||
This allows you to change the internals of your schema and maintain backwards compatibility. It also keeps your code easier to refactor, and provides a natural way to do API versioning.
|
||||
|
||||
.. image:: ../_static/db.png
|
||||
@@ -0,0 +1,201 @@
|
||||
.. _create_soap_endpoint:
|
||||
|
||||
Create a SOAP endpoint
|
||||
======================
|
||||
|
||||
:author: `fjf2002 <https://github.com/fjf2002>`_
|
||||
|
||||
PostgREST supports :ref:`custom_media`. With a bit of work, SOAP endpoints become possible.
|
||||
|
||||
Minimal Example
|
||||
---------------
|
||||
|
||||
This example will simply return the request body, inside a tag ``therequestbodywas``.
|
||||
|
||||
Add the following function to your PostgreSQL database:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "text/xml" as pg_catalog.xml;
|
||||
|
||||
CREATE OR REPLACE FUNCTION my_soap_endpoint(xml) RETURNS "text/xml" AS $$
|
||||
DECLARE
|
||||
nsarray CONSTANT text[][] := ARRAY[
|
||||
ARRAY['soapenv', 'http://schemas.xmlsoap.org/soap/envelope/']
|
||||
];
|
||||
BEGIN
|
||||
RETURN xmlelement(
|
||||
NAME "soapenv:Envelope",
|
||||
XMLATTRIBUTES('http://schemas.xmlsoap.org/soap/envelope/' AS "xmlns:soapenv"),
|
||||
xmlelement(NAME "soapenv:Header"),
|
||||
xmlelement(
|
||||
NAME "soapenv:Body",
|
||||
xmlelement(
|
||||
NAME theRequestBodyWas,
|
||||
(xpath('/soapenv:Envelope/soapenv:Body', $1, nsarray))[1]
|
||||
)
|
||||
)
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
Do not forget to refresh the :ref:`PostgREST schema cache <schema_reloading>`.
|
||||
|
||||
Use ``curl`` for a first test:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl http://localhost:3000/rpc/my_soap_endpoint \
|
||||
--header 'Content-Type: text/xml' \
|
||||
--header 'Accept: text/xml' \
|
||||
--data-binary @- <<XML
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<mySOAPContent>
|
||||
My SOAP Content
|
||||
</mySOAPContent>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
XML
|
||||
|
||||
The output should contain the original request body within the ``therequestbodywas`` entity,
|
||||
and should roughly look like:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<therequestbodywas>
|
||||
<soapenv:Body xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<mySOAPContent>
|
||||
My SOAP Content
|
||||
</mySOAPContent>
|
||||
</soapenv:Body>
|
||||
</therequestbodywas>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
|
||||
A more elaborate example
|
||||
------------------------
|
||||
|
||||
Here we have a SOAP service that converts a fraction to a decimal value,
|
||||
with pass-through of PostgreSQL errors to the SOAP response.
|
||||
Please note that in production you probably should not pass through plain database errors
|
||||
potentially disclosing internals to the client, but instead handle the errors directly.
|
||||
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- helper function
|
||||
CREATE OR REPLACE FUNCTION _soap_envelope(body xml)
|
||||
RETURNS xml
|
||||
LANGUAGE sql
|
||||
AS $function$
|
||||
SELECT xmlelement(
|
||||
NAME "soapenv:Envelope",
|
||||
XMLATTRIBUTES('http://schemas.xmlsoap.org/soap/envelope/' AS "xmlns:soapenv"),
|
||||
xmlelement(NAME "soapenv:Header"),
|
||||
xmlelement(NAME "soapenv:Body", body)
|
||||
);
|
||||
$function$;
|
||||
|
||||
-- helper function
|
||||
CREATE OR REPLACE FUNCTION _soap_exception(
|
||||
faultcode text,
|
||||
faultstring text
|
||||
)
|
||||
RETURNS xml
|
||||
LANGUAGE sql
|
||||
AS $function$
|
||||
SELECT _soap_envelope(
|
||||
xmlelement(NAME "soapenv:Fault",
|
||||
xmlelement(NAME "faultcode", faultcode),
|
||||
xmlelement(NAME "faultstring", faultstring)
|
||||
)
|
||||
);
|
||||
$function$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION fraction_to_decimal(xml)
|
||||
RETURNS "text/xml"
|
||||
LANGUAGE plpgsql
|
||||
AS $function$
|
||||
DECLARE
|
||||
nsarray CONSTANT text[][] := ARRAY[
|
||||
ARRAY['soapenv', 'http://schemas.xmlsoap.org/soap/envelope/']
|
||||
];
|
||||
exc_msg text;
|
||||
exc_detail text;
|
||||
exc_hint text;
|
||||
exc_sqlstate text;
|
||||
BEGIN
|
||||
-- simulating a statement that results in an exception:
|
||||
RETURN _soap_envelope(xmlelement(
|
||||
NAME "decimalValue",
|
||||
(
|
||||
(xpath('/soapenv:Envelope/soapenv:Body/fraction/numerator/text()', $1, nsarray))[1]::text::int
|
||||
/
|
||||
(xpath('/soapenv:Envelope/soapenv:Body/fraction/denominator/text()', $1, nsarray))[1]::text::int
|
||||
)::text::xml
|
||||
));
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
GET STACKED DIAGNOSTICS
|
||||
exc_msg := MESSAGE_TEXT,
|
||||
exc_detail := PG_EXCEPTION_DETAIL,
|
||||
exc_hint := PG_EXCEPTION_HINT,
|
||||
exc_sqlstate := RETURNED_SQLSTATE;
|
||||
RAISE WARNING USING
|
||||
MESSAGE = exc_msg,
|
||||
DETAIL = exc_detail,
|
||||
HINT = exc_hint;
|
||||
RETURN _soap_exception(faultcode => exc_sqlstate, faultstring => concat(exc_msg, ', DETAIL: ', exc_detail, ', HINT: ', exc_hint));
|
||||
END
|
||||
$function$;
|
||||
|
||||
Let's test the ``fraction_to_decimal`` service with illegal values:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl http://localhost:3000/rpc/fraction_to_decimal \
|
||||
--header 'Content-Type: text/xml' \
|
||||
--header 'Accept: text/xml' \
|
||||
--data-binary @- <<XML
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<fraction>
|
||||
<numerator>42</numerator>
|
||||
<denominator>0</denominator>
|
||||
</fraction>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
XML
|
||||
|
||||
The output should roughly look like:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
|
||||
<soapenv:Header/>
|
||||
<soapenv:Body>
|
||||
<soapenv:Fault>
|
||||
<faultcode>22012</faultcode>
|
||||
<faultstring>division by zero, DETAIL: , HINT: </faultstring>
|
||||
</soapenv:Fault>
|
||||
</soapenv:Body>
|
||||
</soapenv:Envelope>
|
||||
|
||||
References
|
||||
----------
|
||||
|
||||
For more information concerning PostgREST, cf.
|
||||
|
||||
- :ref:`s_proc_single_unnamed`
|
||||
- :ref:`custom_media`. See :ref:`any_handler`, if you need to support an ``application/soap+xml`` media type or if you want to respond with XML without sending a media type.
|
||||
- :ref:`Nginx reverse proxy <nginx>`
|
||||
|
||||
For SOAP reference, visit
|
||||
|
||||
- the specification at https://www.w3.org/TR/soap/
|
||||
- shorter more practical advice is available at https://www.w3schools.com/xml/xml_soap.asp
|
||||
@@ -0,0 +1,326 @@
|
||||
|
||||
.. _providing_html_htmx:
|
||||
|
||||
Providing HTML Content Using Htmx
|
||||
=================================
|
||||
|
||||
:author: `Laurence Isla <https://github.com/laurenceisla>`_
|
||||
|
||||
This how-to shows a way to return HTML content and use the `htmx library <https://htmx.org/>`_ to handle the AJAX requests.
|
||||
Htmx expects an HTML response and uses it to replace an element inside the DOM (see the `htmx introduction <https://htmx.org/docs/#introduction>`_ in the docs).
|
||||
|
||||
.. image:: ../_static/how-tos/htmx-demo.gif
|
||||
|
||||
.. warning::
|
||||
|
||||
This is a proof of concept showing what can be achieved using both technologies.
|
||||
We are working on `plmustache <https://github.com/PostgREST/plmustache>`_ which will further improve the HTML aspect of this how-to.
|
||||
|
||||
Preparatory Configuration
|
||||
-------------------------
|
||||
|
||||
We will make a to-do app based on the :ref:`tut0`, so make sure to complete it before continuing.
|
||||
|
||||
To simplify things, we won't be using authentication, so grant all permissions on the ``todos`` table to the ``web_anon`` user.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
grant all on api.todos to web_anon;
|
||||
grant usage, select on sequence api.todos_id_seq to web_anon;
|
||||
|
||||
Next, add the ``text/html`` as a :ref:`custom_media`. With this, PostgREST can identify the request made by your web browser (with the ``Accept: text/html`` header)
|
||||
and return a raw HTML document file.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "text/html" as text;
|
||||
|
||||
Creating an HTML Response
|
||||
-------------------------
|
||||
|
||||
Let's create a function that returns a basic HTML file, using `Pico CSS <https://picocss.com>`_ for styling and
|
||||
`Ionicons <https://ionic.io/ionicons>`_ to show some icons later.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function api.index() returns "text/html" as $$
|
||||
select $html$
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PostgREST + HTMX To-Do List</title>
|
||||
<!-- Pico CSS for CSS styling -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/@picocss/pico@next/css/pico.min.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<main class="container">
|
||||
<article>
|
||||
<h5 style="text-align: center;">
|
||||
PostgREST + HTMX To-Do List
|
||||
</h5>
|
||||
</article>
|
||||
</main>
|
||||
<!-- Script for Ionicons icons -->
|
||||
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
|
||||
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
$html$;
|
||||
$$ language sql;
|
||||
|
||||
The web browser will open the web page at ``http://localhost:3000/rpc/index``.
|
||||
|
||||
.. image:: ../_static/how-tos/htmx-simple.jpg
|
||||
|
||||
.. _html_htmx_list_create:
|
||||
|
||||
Listing and Creating To-Dos
|
||||
---------------------------
|
||||
|
||||
Now, let's show a list of the to-dos already inserted in the database.
|
||||
For that, we'll also need a function to help us sanitize the HTML content that may be present in the task.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function api.sanitize_html(text) returns text as $$
|
||||
select replace(replace(replace(replace(replace($1, '&', '&'), '"', '"'),'>', '>'),'<', '<'), '''', ''')
|
||||
$$ language sql;
|
||||
|
||||
create or replace function api.html_todo(api.todos) returns text as $$
|
||||
select format($html$
|
||||
<div>
|
||||
<%2$s>
|
||||
%3$s
|
||||
</%2$s>
|
||||
</div>
|
||||
$html$,
|
||||
$1.id,
|
||||
case when $1.done then 's' else 'span' end,
|
||||
api.sanitize_html($1.task)
|
||||
);
|
||||
$$ language sql stable;
|
||||
|
||||
create or replace function api.html_all_todos() returns text as $$
|
||||
select coalesce(
|
||||
string_agg(api.html_todo(t), '<hr/>' order by t.id),
|
||||
'<p><em>There is nothing else to do.</em></p>'
|
||||
)
|
||||
from api.todos t;
|
||||
$$ language sql;
|
||||
|
||||
These two functions are used to build the to-do list template. We won't use them as PostgREST endpoints.
|
||||
|
||||
- The ``api.html_todo`` function uses the table ``api.todos`` as a parameter and formats each item into a list element ``<li>``.
|
||||
The PostgreSQL `format <https://www.postgresql.org/docs/current/functions-string.html#FUNCTIONS-STRING-FORMAT>`_ is useful to that end.
|
||||
It replaces the values according to the position in the template, e.g. ``%1$s`` will be replaced with the value of ``$1.id`` (the first parameter).
|
||||
|
||||
- The ``api.html_all_todos`` function returns the ``<ul>`` wrapper for all the list elements.
|
||||
It uses `string_arg <https://www.postgresql.org/docs/current/functions-aggregate.html>`_ to concatenate all the to-dos in a single text value.
|
||||
It also returns an alternative message, instead of a list, when the ``api.todos`` table is empty.
|
||||
|
||||
Next, let's add an endpoint to register a to-do in the database and modify the ``/rpc/index`` page accordingly.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function api.add_todo(_task text) returns "text/html" as $$
|
||||
insert into api.todos(task) values (_task);
|
||||
select api.html_all_todos();
|
||||
$$ language sql;
|
||||
|
||||
create or replace function api.index() returns "text/html" as $$
|
||||
select $html$
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>PostgREST + HTMX To-Do List</title>
|
||||
<!-- Pico CSS for CSS styling -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/@picocss/pico@next/css/pico.min.css" rel="stylesheet"/>
|
||||
<!-- htmx for AJAX requests -->
|
||||
<script src="https://unpkg.com/htmx.org"></script>
|
||||
</head>
|
||||
<body>
|
||||
<main class="container"
|
||||
style="max-width: 600px"
|
||||
hx-headers='{"Accept": "text/html"}'>
|
||||
<article>
|
||||
<h5 style="text-align: center;">
|
||||
PostgREST + HTMX To-Do List
|
||||
</h5>
|
||||
<form hx-post="/rpc/add_todo"
|
||||
hx-target="#todo-list-area"
|
||||
hx-trigger="submit"
|
||||
hx-on="htmx:afterRequest: this.reset()">
|
||||
<input type="text" name="_task" placeholder="Add a todo...">
|
||||
</form>
|
||||
<div id="todo-list-area">
|
||||
$html$
|
||||
|| api.html_all_todos() ||
|
||||
$html$
|
||||
<div>
|
||||
</article>
|
||||
</main>
|
||||
<!-- Script for Ionicons icons -->
|
||||
<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
|
||||
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
$html$;
|
||||
$$ language sql;
|
||||
|
||||
- The ``/rpc/add_todo`` endpoint allows us to add a new to-do using the ``_task`` parameter and returns an ``html`` with all the to-dos in the database.
|
||||
|
||||
- The ``/rpc/index`` now adds the ``hx-headers='{"Accept": "text/html"}'`` tag to the ``<body>``.
|
||||
This will make sure that all htmx elements inside the body send this header, otherwise PostgREST won't recognize it as HTML.
|
||||
|
||||
There is also a ``<form>`` element that uses the htmx library. Let's break it down:
|
||||
|
||||
+ ``hx-post="/rpc/add_todo"``: sends an AJAX POST request to the ``/rpc/add_todo`` endpoint, with the value of the ``_task`` from the ``<input>`` element.
|
||||
|
||||
+ ``hx-target="#todo-list-area"``: the HTML content returned from the request will go inside ``<div id="todo-list-area"></div>`` (which is the list of to-dos).
|
||||
|
||||
+ ``hx-trigger="submit"``: htmx will do this request when submitting the form (by pressing enter while inside the ``<input>``).
|
||||
|
||||
+ ``hx-on="htmx:afterRequest: this.reset()">``: this is a Javascript command that clears the form `after the request is done <https://htmx.org/events/#htmx:afterRequest>`_.
|
||||
|
||||
With this, the ``http://localhost:3000/rpc/index`` page lists all the todos and adds new ones by submitting tasks in the input element.
|
||||
Don't forget to refresh the :ref:`schema cache <schema_reloading>`.
|
||||
|
||||
.. image:: ../_static/how-tos/htmx-insert.gif
|
||||
|
||||
Editing and Deleting To-Dos
|
||||
---------------------------
|
||||
|
||||
Now, let's modify ``api.html_todo`` and make it more functional.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function api.html_todo(api.todos) returns text as $$
|
||||
select format($html$
|
||||
<div class="grid">
|
||||
<div id="todo-edit-area-%1$s">
|
||||
<form id="edit-task-state-%1$s"
|
||||
hx-post="/rpc/change_todo_state"
|
||||
hx-vals='{"_id": %1$s, "_done": %4$s}'
|
||||
hx-target="#todo-list-area"
|
||||
hx-trigger="click">
|
||||
<%2$s style="cursor: pointer">
|
||||
%3$s
|
||||
</%2$s>
|
||||
</form>
|
||||
</div>
|
||||
<div style="text-align: right">
|
||||
<button class="outline"
|
||||
hx-get="/rpc/html_editable_task"
|
||||
hx-vals='{"_id": "%1$s"}'
|
||||
hx-target="#todo-edit-area-%1$s"
|
||||
hx-trigger="click">
|
||||
<span>
|
||||
<ion-icon name="create"></ion-icon>
|
||||
</span>
|
||||
</button>
|
||||
<button class="outline contrast"
|
||||
hx-post="/rpc/delete_todo"
|
||||
hx-vals='{"_id": %1$s}'
|
||||
hx-target="#todo-list-area"
|
||||
hx-trigger="click">
|
||||
<span>
|
||||
<ion-icon name="trash" style="color: #f87171"></ion-icon>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
$html$,
|
||||
$1.id,
|
||||
case when $1.done then 's' else 'span' end,
|
||||
api.sanitize_html($1.task),
|
||||
(not $1.done)::text
|
||||
);
|
||||
$$ language sql stable;
|
||||
|
||||
Let's deconstruct the new htmx features added:
|
||||
|
||||
- The ``<form>`` element is configured as follows:
|
||||
|
||||
+ ``hx-post="/rpc/change_todo_state"``: does an AJAX POST request to that endpoint. It will toggle the ``done`` state of the to-do.
|
||||
|
||||
+ ``hx-vals='{"_id": %1$s, "_done": %4$s}'``: adds the parameters to the request.
|
||||
This is an alternative to using hidden inputs inside the ``<form>``.
|
||||
|
||||
+ ``hx-trigger="click"``: htmx does the request after clicking on the element.
|
||||
|
||||
- For the first ``<button>``:
|
||||
|
||||
+ ``hx-get="/rpc/html_editable_task"``: it does an AJAX GET request to that endpoint.
|
||||
It returns an HTML with an input that will allow us to edit the task.
|
||||
|
||||
+ ``hx-target="#todo-edit-area"``: the returned HTML will replace the element with this id.
|
||||
In this case, this replaces an individual task, not the whole list.
|
||||
|
||||
+ ``hx-vals='{"id": "eq.%1$s"}'``: adds the query parameters to the GET request.
|
||||
Note that this needs the ``eq.`` operator because it represents a table column not a function parameter.
|
||||
|
||||
- For the second ``<button>``:
|
||||
|
||||
+ ``hx-post="/rpc/delete_todo"``: this post request will delete the corresponding to-do.
|
||||
|
||||
Clicking on the first button will enable the task editing.
|
||||
That's why we create the ``api.html_editable_task`` function as an endpoint:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function api.html_editable_task(_id int) returns "text/html" as $$
|
||||
select format ($html$
|
||||
<form id="edit-task-%1$s"
|
||||
hx-post="/rpc/change_todo_task"
|
||||
hx-headers='{"Accept": "text/html"}'
|
||||
hx-vals='{"_id": %1$s}'
|
||||
hx-target="#todo-list-area"
|
||||
hx-trigger="submit,focusout">
|
||||
<input id="task-%1$s" type="text" name="_task" value="%2$s" autofocus>
|
||||
</form>
|
||||
$html$,
|
||||
id,
|
||||
api.sanitize_html(task)
|
||||
)
|
||||
from api.todos
|
||||
where id = _id;
|
||||
$$ language sql;
|
||||
|
||||
In this example, this will return an input field that allows us to edit the corresponding to-do task.
|
||||
|
||||
Finally, let's add the endpoints that will modify and delete the to-dos in the database.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function api.change_todo_state(_id int, _done boolean) returns "text/html" as $$
|
||||
update api.todos set done = _done where id = _id;
|
||||
select api.html_all_todos();
|
||||
$$ language sql;
|
||||
|
||||
create or replace function api.change_todo_task(_id int, _task text) returns "text/html" as $$
|
||||
update api.todos set task = _task where id = _id;
|
||||
select api.html_all_todos();
|
||||
$$ language sql;
|
||||
|
||||
create or replace function api.delete_todo(_id int) returns "text/html" as $$
|
||||
delete from api.todos where id = _id;
|
||||
select api.html_all_todos();
|
||||
$$ language sql;
|
||||
|
||||
All of those functions return an HTML list of to-dos that will replace the outdated one:
|
||||
|
||||
- The ``api.change_todo_state`` function updates the ``done`` column using the ``_id`` and the ``_done`` values from the request.
|
||||
|
||||
- The ``api.delete_todo`` function deletes a to-do using the ``_id`` value from the request.
|
||||
|
||||
- The ``api.change_todo_task`` function modifies the ``task`` column using the ``_id`` and the ``_task`` value from the request.
|
||||
|
||||
After refreshing the :ref:`schema cache <schema_reloading>`, the page at ``http://localhost:3000/rpc/index`` will allow us to edit, delete and complete any to-do.
|
||||
|
||||
.. image:: ../_static/how-tos/htmx-edit-delete.gif
|
||||
|
||||
With that, we completed the to-do list functionality.
|
||||
@@ -0,0 +1,122 @@
|
||||
.. _providing_img:
|
||||
|
||||
Providing images for ``<img>``
|
||||
==============================
|
||||
|
||||
:author: `pkel <https://github.com/pkel>`_
|
||||
|
||||
In this how-to, you will learn how to create an endpoint for providing images to HTML :code:`<img>` tags without client side JavaScript. In fact, the presented technique is suitable for providing not only images, but arbitrary files.
|
||||
|
||||
We will start with a minimal example that highlights the general concept.
|
||||
Afterwards we present a more detailed solution that fixes a few shortcomings of the first approach.
|
||||
|
||||
.. warning::
|
||||
|
||||
Be careful when saving binaries in the database, having a separate storage service for these is preferable in most cases. See `Storing Binary files in the Database <https://wiki.postgresql.org/wiki/BinaryFilesInDB>`_.
|
||||
|
||||
Minimal Example
|
||||
---------------
|
||||
|
||||
First, we need a public table for storing the files.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create table files(
|
||||
id int primary key
|
||||
, blob bytea
|
||||
);
|
||||
|
||||
Let's assume this table contains an image of two cute kittens with id 42. We can retrieve this image in binary format from our PostgREST API by using :ref:`custom_media`:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "application/octet-stream" as bytea;
|
||||
|
||||
create or replace function file(id int) returns "application/octet-stream" as $$
|
||||
select blob from files where id = file.id;
|
||||
$$ language sql;
|
||||
|
||||
Now we can request the RPC endpoint :code:`/rpc/file?id=42` with the :code:`Accept: application/octet-stream` header.
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "localhost:3000/rpc/file?id=42" -H "Accept: application/octet-stream"
|
||||
|
||||
|
||||
Unfortunately, putting the URL into the :code:`src` of an :code:`<img>` tag will not work. That's because browsers do not send the required :code:`Accept: application/octet-stream` header.
|
||||
Instead, the :code:`Accept: image/webp` header is sent by many web browsers by default.
|
||||
|
||||
Luckily we can change the accepted media type in the function like so:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "image/webp" as bytea;
|
||||
|
||||
create or replace function file(id int) returns "image/webp" as $$
|
||||
select blob from files where id = file.id;
|
||||
$$ language sql;
|
||||
|
||||
Now, the image will be displayed in the HTML page:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<img src="http://localhost:3000/file?id=42" alt="Cute Kittens"/>
|
||||
|
||||
Improved Version
|
||||
----------------
|
||||
|
||||
The basic solution has some shortcomings:
|
||||
|
||||
1. The response :code:`Content-Type` header is set to :code:`image/webp`.
|
||||
This might be a problem if you want to specify a different format for the file.
|
||||
2. Download requests (e.g. Right Click -> Save Image As) to :code:`/files?select=blob&id=eq.42` will propose :code:`files` as filename.
|
||||
This might confuse users.
|
||||
3. Requests to the binary endpoint are not cached.
|
||||
This will cause unnecessary load on the database.
|
||||
|
||||
The following improved version addresses these problems.
|
||||
First, in addition to the minimal example, we need to store the media types and names of our files in the database.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
alter table files
|
||||
add column type text,
|
||||
add column name text;
|
||||
|
||||
Next, we set modify the function to set the content type and filename.
|
||||
We use this opportunity to configure some basic, client-side caching.
|
||||
For production, you probably want to configure additional caches, e.g. on the :ref:`reverse proxy <nginx>`.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "*/*" as bytea;
|
||||
|
||||
create function file(id int) returns "*/*" as
|
||||
$$
|
||||
declare headers text;
|
||||
declare blob bytea;
|
||||
begin
|
||||
select format(
|
||||
'[{"Content-Type": "%s"},'
|
||||
'{"Content-Disposition": "inline; filename=\"%s\""},'
|
||||
'{"Cache-Control": "max-age=259200"}]'
|
||||
, files.type, files.name)
|
||||
from files where files.id = file.id into headers;
|
||||
perform set_config('response.headers', headers, true);
|
||||
select files.blob from files where files.id = file.id into blob;
|
||||
if FOUND -- special var, see https://www.postgresql.org/docs/current/plpgsql-statements.html#PLPGSQL-STATEMENTS-DIAGNOSTICS
|
||||
then return(blob);
|
||||
else raise sqlstate 'PT404' using
|
||||
message = 'NOT FOUND',
|
||||
detail = 'File not found',
|
||||
hint = format('%s seems to be an invalid file id', file.id);
|
||||
end if;
|
||||
end
|
||||
$$ language plpgsql;
|
||||
|
||||
With this, we can obtain the cat image from :code:`/rpc/file?id=42`. Thus, the resulting HTML will be:
|
||||
|
||||
.. code-block:: html
|
||||
|
||||
<img src="http://localhost:3000/rpc/file?id=42" alt="Cute Kittens"/>
|
||||
@@ -0,0 +1,318 @@
|
||||
.. _sql-user-management-using-postgres-users-and-passwords:
|
||||
|
||||
SQL User Management using postgres' users and passwords
|
||||
=======================================================
|
||||
|
||||
:author: `fjf2002 <https://github.com/fjf2002>`_
|
||||
|
||||
|
||||
This is an alternative to chapter :ref:`sql_user_management`, solely using the PostgreSQL built-in table `pg_catalog.pg_authid <https://www.postgresql.org/docs/current/catalog-pg-authid.html>`_ for user management. This means
|
||||
|
||||
- no dedicated user table (aside from :code:`pg_authid`) is required
|
||||
|
||||
- PostgreSQL's users and passwords (i. e. the stuff in :code:`pg_authid`) are also used at the PostgREST level.
|
||||
|
||||
.. note::
|
||||
Only PostgreSQL users with SCRAM-SHA-256 password hashes (the default since PostgreSQL v14) are supported.
|
||||
|
||||
.. warning::
|
||||
|
||||
This is experimental. We can't give you any guarantees, especially concerning security. Use at your own risk.
|
||||
|
||||
|
||||
|
||||
Working with pg_authid and SCRAM-SHA-256 hashes
|
||||
-----------------------------------------------
|
||||
|
||||
As in :ref:`sql_user_management`, we create a :code:`basic_auth` schema:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- We put things inside the basic_auth schema to hide
|
||||
-- them from public view. Certain public procs/views will
|
||||
-- refer to helpers and tables inside.
|
||||
CREATE SCHEMA IF NOT EXISTS basic_auth;
|
||||
|
||||
|
||||
As in :ref:`sql_user_management`, we create the :code:`pgcrypto` and :code:`pgjwt` extensions. Here we prefer to put the extensions in its own schemas:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE SCHEMA ext_pgcrypto;
|
||||
ALTER SCHEMA ext_pgcrypto OWNER TO postgres;
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto WITH SCHEMA ext_pgcrypto;
|
||||
|
||||
|
||||
Concerning the `pgjwt extension <https://github.com/michelp/pgjwt>`_, please cf. to :ref:`client_auth`.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE SCHEMA ext_pgjwt;
|
||||
ALTER SCHEMA ext_pgjwt OWNER TO postgres;
|
||||
CREATE EXTENSION IF NOT EXISTS pgjwt WITH SCHEMA ext_pgjwt;
|
||||
|
||||
|
||||
In order to be able to work with postgres' SCRAM-SHA-256 password hashes, we also need the PBKDF2 key derivation function. Luckily there is `a PL/pgSQL implementation on stackoverflow <https://stackoverflow.com/a/72805848>`_:
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
CREATE FUNCTION basic_auth.pbkdf2(salt bytea, pw text, count integer, desired_length integer, algorithm text) RETURNS bytea
|
||||
LANGUAGE plpgsql IMMUTABLE
|
||||
AS $$
|
||||
DECLARE
|
||||
hash_length integer;
|
||||
block_count integer;
|
||||
output bytea;
|
||||
the_last bytea;
|
||||
xorsum bytea;
|
||||
i_as_int32 bytea;
|
||||
i integer;
|
||||
j integer;
|
||||
k integer;
|
||||
BEGIN
|
||||
algorithm := lower(algorithm);
|
||||
CASE algorithm
|
||||
WHEN 'md5' then
|
||||
hash_length := 16;
|
||||
WHEN 'sha1' then
|
||||
hash_length = 20;
|
||||
WHEN 'sha256' then
|
||||
hash_length = 32;
|
||||
WHEN 'sha512' then
|
||||
hash_length = 64;
|
||||
ELSE
|
||||
RAISE EXCEPTION 'Unknown algorithm "%"', algorithm;
|
||||
END CASE;
|
||||
--
|
||||
block_count := ceil(desired_length::real / hash_length::real);
|
||||
--
|
||||
FOR i in 1 .. block_count LOOP
|
||||
i_as_int32 := E'\\000\\000\\000'::bytea || chr(i)::bytea;
|
||||
i_as_int32 := substring(i_as_int32, length(i_as_int32) - 3);
|
||||
--
|
||||
the_last := salt::bytea || i_as_int32;
|
||||
--
|
||||
xorsum := ext_pgcrypto.HMAC(the_last, pw::bytea, algorithm);
|
||||
the_last := xorsum;
|
||||
--
|
||||
FOR j IN 2 .. count LOOP
|
||||
the_last := ext_pgcrypto.HMAC(the_last, pw::bytea, algorithm);
|
||||
|
||||
-- xor the two
|
||||
FOR k IN 1 .. length(xorsum) LOOP
|
||||
xorsum := set_byte(xorsum, k - 1, get_byte(xorsum, k - 1) # get_byte(the_last, k - 1));
|
||||
END LOOP;
|
||||
END LOOP;
|
||||
--
|
||||
IF output IS NULL THEN
|
||||
output := xorsum;
|
||||
ELSE
|
||||
output := output || xorsum;
|
||||
END IF;
|
||||
END LOOP;
|
||||
--
|
||||
RETURN substring(output FROM 1 FOR desired_length);
|
||||
END $$;
|
||||
|
||||
ALTER FUNCTION basic_auth.pbkdf2(salt bytea, pw text, count integer, desired_length integer, algorithm text) OWNER TO postgres;
|
||||
|
||||
|
||||
Analogous to :ref:`sql_user_management` creates the function :code:`basic_auth.user_role`, we create a helper function to check the user's password, here with another name and signature (since we want the username, not an email address).
|
||||
But contrary to :ref:`sql_user_management`, this function does not use a dedicated :code:`users` table with passwords, but instead utilizes the built-in table `pg_catalog.pg_authid <https://www.postgresql.org/docs/current/catalog-pg-authid.html>`_:
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
CREATE FUNCTION basic_auth.check_user_pass(username text, password text) RETURNS name
|
||||
LANGUAGE sql
|
||||
AS
|
||||
$$
|
||||
SELECT rolname AS username
|
||||
FROM pg_authid
|
||||
-- regexp-split scram hash:
|
||||
CROSS JOIN LATERAL regexp_match(rolpassword, '^SCRAM-SHA-256\$(.*):(.*)\$(.*):(.*)$') AS rm
|
||||
-- identify regexp groups with sane names:
|
||||
CROSS JOIN LATERAL (SELECT rm[1]::integer AS iteration_count, decode(rm[2], 'base64') as salt, decode(rm[3], 'base64') AS stored_key, decode(rm[4], 'base64') AS server_key, 32 AS digest_length) AS stored_password_part
|
||||
-- calculate pbkdf2-digest:
|
||||
CROSS JOIN LATERAL (SELECT basic_auth.pbkdf2(salt, check_user_pass.password, iteration_count, digest_length, 'sha256')) AS digest_key(digest_key)
|
||||
-- based on that, calculate hashed passwort part:
|
||||
CROSS JOIN LATERAL (SELECT ext_pgcrypto.digest(ext_pgcrypto.hmac('Client Key', digest_key, 'sha256'), 'sha256') AS stored_key, ext_pgcrypto.hmac('Server Key', digest_key, 'sha256') AS server_key) AS check_password_part
|
||||
WHERE rolpassword IS NOT NULL
|
||||
AND pg_authid.rolname = check_user_pass.username
|
||||
-- verify password:
|
||||
AND check_password_part.stored_key = stored_password_part.stored_key
|
||||
AND check_password_part.server_key = stored_password_part.server_key;
|
||||
$$;
|
||||
|
||||
ALTER FUNCTION basic_auth.check_user_pass(username text, password text) OWNER TO postgres;
|
||||
|
||||
|
||||
|
||||
Public User Interface
|
||||
---------------------
|
||||
|
||||
Analogous to :ref:`sql_user_management`, we create a login function which takes a username and password and returns a JWT if the credentials match a user in the internal table.
|
||||
Here we use the username instead of the email address to identify a user.
|
||||
|
||||
|
||||
Logins
|
||||
~~~~~~
|
||||
|
||||
As described in :ref:`client_auth`, we'll create a JWT token inside our login function. Note that you'll need to adjust the secret key which is hard-coded in this example to a secure (at least thirty-two character) secret of your choosing.
|
||||
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
CREATE TYPE basic_auth.jwt_token AS (
|
||||
token text
|
||||
);
|
||||
|
||||
-- if you are not using psql, you need to replace :DBNAME with the current database's name.
|
||||
ALTER DATABASE :DBNAME SET "app.jwt_secret" to 'reallyreallyreallyreallyverysafe';
|
||||
|
||||
|
||||
CREATE FUNCTION public.login(username text, password text) RETURNS basic_auth.jwt_token
|
||||
LANGUAGE plpgsql security definer
|
||||
AS $$
|
||||
DECLARE
|
||||
_role name;
|
||||
result basic_auth.jwt_token;
|
||||
BEGIN
|
||||
-- check email and password
|
||||
SELECT basic_auth.check_user_pass(username, password) INTO _role;
|
||||
IF _role IS NULL THEN
|
||||
RAISE invalid_password USING message = 'invalid user or password';
|
||||
END IF;
|
||||
--
|
||||
SELECT ext_pgjwt.sign(
|
||||
row_to_json(r), current_setting('app.jwt_secret')
|
||||
) AS token
|
||||
FROM (
|
||||
SELECT login.username as role,
|
||||
extract(epoch FROM now())::integer + 60*60 AS exp
|
||||
) r
|
||||
INTO result;
|
||||
RETURN result;
|
||||
END;
|
||||
$$;
|
||||
|
||||
ALTER FUNCTION public.login(username text, password text) OWNER TO postgres;
|
||||
|
||||
|
||||
|
||||
Permissions
|
||||
~~~~~~~~~~~
|
||||
|
||||
Analogous to :ref:`sql_user_management`:
|
||||
Your database roles need access to the schema, tables, views and functions in order to service HTTP requests.
|
||||
Recall from the :ref:`roles` that PostgREST uses special roles to process requests, namely the authenticator and
|
||||
anonymous roles. Below is an example of permissions that allow anonymous users to attempt to log in.
|
||||
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE ROLE anon NOINHERIT;
|
||||
CREATE role authenticator NOINHERIT LOGIN PASSWORD 'secret';
|
||||
GRANT anon TO authenticator;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.login(username text, password text) TO anon;
|
||||
|
||||
|
||||
Since the above :code:`login` function is defined as `security definer <https://www.postgresql.org/docs/current/sql-createfunction.html#id-1.9.3.67.10.2>`_,
|
||||
the anonymous user :code:`anon` doesn't need permission to access the table :code:`pg_catalog.pg_authid` .
|
||||
:code:`grant execute on function` is included for clarity but it might not be needed, see :ref:`func_privs` for more details.
|
||||
|
||||
Choose a secure password for role :code:`authenticator`.
|
||||
Do not forget to configure PostgREST to use the :code:`authenticator` user to connect, and to use the :code:`anon` user as anonymous user.
|
||||
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
Let us create a sample user:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE ROLE foo PASSWORD 'bar';
|
||||
|
||||
|
||||
Test at the SQL level
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Execute:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
SELECT * FROM public.login('foo', 'bar');
|
||||
|
||||
|
||||
This should return a single scalar field like:
|
||||
|
||||
::
|
||||
|
||||
token
|
||||
-----------------------------------------------------------------------------------------------------------------------------
|
||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZm9vIiwiZXhwIjoxNjY4MTg4ODQ3fQ.idBBHuDiQuN_S7JJ2v3pBOr9QypCliYQtCgwYOzAqEk
|
||||
(1 row)
|
||||
|
||||
|
||||
Test at the REST level
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
An API request to call this function would look like:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/login" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '{ "username": "foo", "password": "bar" }'
|
||||
|
||||
The response would look like the snippet below. Try decoding the token at `jwt.io <https://jwt.io/>`_. (It was encoded with a secret of :code:`reallyreallyreallyreallyverysafe` as specified in the SQL code above. You'll want to change this secret in your app!)
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VwcCIsImV4cCI6MTY2ODE4ODQzN30.WSytcouNMQe44ZzOQit2AQsqTKFD5mIvT3z2uHwdoYY"
|
||||
}
|
||||
|
||||
|
||||
|
||||
A more sophisticated test at the REST level
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Let's add a table, intended for the :code:`foo` user:
|
||||
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE TABLE public.foobar(foo int, bar text, baz float);
|
||||
ALTER TABLE public.foobar owner TO postgres;
|
||||
|
||||
|
||||
Now try to get the table's contents with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/foobar"
|
||||
|
||||
|
||||
This should fail --- of course, we haven't specified the user, thus PostgREST falls back to the :code:`anon` user and denies access.
|
||||
Add an :code:`Authorization` header. Please use the token value from the login function call above instead of the one provided below.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/foobar" \
|
||||
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZm9vIiwiZXhwIjoxNjY4MTkyMjAyfQ.zzdHCBjfkqDQLQ8D7CHO3cIALF6KBCsfPTWgwhCiHCY"
|
||||
|
||||
|
||||
This will fail again --- we get :code:`Permission denied to set role`. We forgot to allow the authenticator role to switch into this user by executing:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
GRANT foo TO authenticator;
|
||||
|
||||
|
||||
Re-execute the last REST request. We fail again --- we also forgot to grant permissions for :code:`foo` on the table. Execute:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
GRANT SELECT ON TABLE public.foobar TO foo;
|
||||
|
||||
Now the REST request should succeed. An empty JSON array :code:`[]` is returned.
|
||||
@@ -0,0 +1,220 @@
|
||||
.. _sql_user_management:
|
||||
|
||||
SQL User Management
|
||||
===================
|
||||
|
||||
As mentioned on :ref:`jwt_generation`, an external service can provide user management and coordinate with the PostgREST server using JWT. It’s also possible to support logins entirely through SQL. It’s a fair bit of work, so get ready.
|
||||
|
||||
Storing Users and Passwords
|
||||
---------------------------
|
||||
|
||||
The following table, functions, and triggers will live in a :code:`basic_auth` schema that you shouldn't expose publicly in the API. The public views and functions will live in a different schema which internally references this internal information.
|
||||
|
||||
First we'll need a table to keep track of our users:
|
||||
|
||||
.. code:: sql
|
||||
|
||||
-- We put things inside the basic_auth schema to hide
|
||||
-- them from public view. Certain public procs/views will
|
||||
-- refer to helpers and tables inside.
|
||||
create schema if not exists basic_auth;
|
||||
|
||||
create table if not exists
|
||||
basic_auth.users (
|
||||
email text primary key check ( email ~* '^.+@.+\..+$' ),
|
||||
pass text not null check (length(pass) < 512),
|
||||
role name not null check (length(role) < 512)
|
||||
);
|
||||
|
||||
We would like the role to be a foreign key to actual database roles, however PostgreSQL does not support these constraints against the :code:`pg_roles` table. We'll use a trigger to manually enforce it.
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
create or replace function
|
||||
basic_auth.check_role_exists() returns trigger as $$
|
||||
begin
|
||||
if not exists (select 1 from pg_roles as r where r.rolname = new.role) then
|
||||
raise foreign_key_violation using message =
|
||||
'unknown database role: ' || new.role;
|
||||
return null;
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$ language plpgsql;
|
||||
|
||||
drop trigger if exists ensure_user_role_exists on basic_auth.users;
|
||||
create constraint trigger ensure_user_role_exists
|
||||
after insert or update on basic_auth.users
|
||||
for each row
|
||||
execute procedure basic_auth.check_role_exists();
|
||||
|
||||
Next we'll use the pgcrypto extension and a trigger to keep passwords safe in the :code:`users` table.
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
create extension if not exists pgcrypto;
|
||||
|
||||
create or replace function
|
||||
basic_auth.encrypt_pass() returns trigger as $$
|
||||
begin
|
||||
if tg_op = 'INSERT' or new.pass <> old.pass then
|
||||
new.pass = crypt(new.pass, gen_salt('bf'));
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$ language plpgsql;
|
||||
|
||||
drop trigger if exists encrypt_pass on basic_auth.users;
|
||||
create trigger encrypt_pass
|
||||
before insert or update on basic_auth.users
|
||||
for each row
|
||||
execute procedure basic_auth.encrypt_pass();
|
||||
|
||||
With the table in place we can make a helper to check a password against the encrypted column. It returns the database role for a user if the email and password are correct.
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
create or replace function
|
||||
basic_auth.user_role(email text, pass text) returns name
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
return (
|
||||
select role from basic_auth.users
|
||||
where users.email = user_role.email
|
||||
and users.pass = crypt(user_role.pass, users.pass)
|
||||
);
|
||||
end;
|
||||
$$;
|
||||
|
||||
.. _public_ui:
|
||||
|
||||
Public User Interface
|
||||
---------------------
|
||||
|
||||
In the previous section we created an internal table to store user information. Here we create a login function which takes an email address and password and returns JWT if the credentials match a user in the internal table.
|
||||
|
||||
Permissions
|
||||
~~~~~~~~~~~
|
||||
|
||||
Your database roles need access to the schema, tables, views and functions in order to service HTTP requests.
|
||||
Recall from the :ref:`roles` that PostgREST uses special roles to process requests, namely the authenticator and
|
||||
anonymous roles. Below is an example of permissions that allow anonymous users to create accounts and attempt to log in.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create role anon noinherit;
|
||||
create role authenticator noinherit;
|
||||
grant anon to authenticator;
|
||||
|
||||
Then, add ``db-anon-role`` to the configuration file to allow anonymous requests.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
db-anon-role = "anon"
|
||||
|
||||
JWT from SQL
|
||||
~~~~~~~~~~~~
|
||||
|
||||
You can create JWT tokens in SQL using the `pgjwt extension <https://github.com/michelp/pgjwt>`_. It's simple and requires only pgcrypto. If you're on an environment like Amazon RDS which doesn't support installing new extensions, you can still manually run the `SQL inside pgjwt <https://github.com/michelp/pgjwt/blob/master/pgjwt--0.1.1.sql>`_ (you'll need to replace ``@extschema@`` with another schema or just delete it) which creates the functions you will need.
|
||||
|
||||
Next write a stored procedure that returns the token. The one below returns a token with a hard-coded role, which expires five minutes after it was issued. Note this function has a hard-coded secret as well.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE TYPE jwt_token AS (
|
||||
token text
|
||||
);
|
||||
|
||||
CREATE FUNCTION jwt_test() RETURNS public.jwt_token AS $$
|
||||
SELECT public.sign(
|
||||
row_to_json(r), 'reallyreallyreallyreallyverysafe'
|
||||
) AS token
|
||||
FROM (
|
||||
SELECT
|
||||
'my_role'::text as role,
|
||||
extract(epoch from now())::integer + 300 AS exp
|
||||
) r;
|
||||
$$ LANGUAGE sql;
|
||||
|
||||
PostgREST exposes this function to clients via a POST request to ``/rpc/jwt_test``.
|
||||
|
||||
.. note::
|
||||
|
||||
To avoid hard-coding the secret in stored procedures, save it as a property of the database.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- run this once
|
||||
ALTER DATABASE mydb SET "app.jwt_secret" TO 'reallyreallyreallyreallyverysafe';
|
||||
|
||||
-- then all functions can refer to app.jwt_secret
|
||||
SELECT sign(
|
||||
row_to_json(r), current_setting('app.jwt_secret')
|
||||
) AS token
|
||||
FROM ...
|
||||
|
||||
Logins
|
||||
~~~~~~
|
||||
|
||||
As described in `JWT from SQL`_, we'll create a JWT inside our login function. Note that you'll need to adjust the secret key which is hard-coded in this example to a secure (at least thirty-two character) secret of your choosing.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- add type
|
||||
CREATE TYPE basic_auth.jwt_token AS (
|
||||
token text
|
||||
);
|
||||
|
||||
-- login should be on your exposed schema
|
||||
create or replace function
|
||||
login(email text, pass text) returns basic_auth.jwt_token as $$
|
||||
declare
|
||||
_role name;
|
||||
result basic_auth.jwt_token;
|
||||
begin
|
||||
-- check email and password
|
||||
select basic_auth.user_role(email, pass) into _role;
|
||||
if _role is null then
|
||||
raise invalid_password using message = 'invalid user or password';
|
||||
end if;
|
||||
|
||||
select sign(
|
||||
row_to_json(r), 'reallyreallyreallyreallyverysafe'
|
||||
) as token
|
||||
from (
|
||||
select _role as role, login.email as email,
|
||||
extract(epoch from now())::integer + 60*60 as exp
|
||||
) r
|
||||
into result;
|
||||
return result;
|
||||
end;
|
||||
$$ language plpgsql security definer;
|
||||
|
||||
grant execute on function login(text,text) to anon;
|
||||
|
||||
Since the above :code:`login` function is defined as `security definer <https://www.postgresql.org/docs/current/sql-createfunction.html#id-1.9.3.67.10.2>`_,
|
||||
the anonymous user :code:`anon` doesn't need permission to read the :code:`basic_auth.users` table. It doesn't even need permission to access the :code:`basic_auth` schema.
|
||||
:code:`grant execute on function` is included for clarity but it might not be needed, see :ref:`func_privs` for more details.
|
||||
|
||||
An API request to call this function would look like:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/login" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '{ "email": "foo@bar.com", "pass": "foobar" }'
|
||||
|
||||
The response would look like the snippet below. Try decoding the token at `jwt.io <https://jwt.io/>`_. (It was encoded with a secret of :code:`reallyreallyreallyreallyverysafe` as specified in the SQL code above. You'll want to change this secret in your app!)
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicGFzcyI6ImZvb2JhciJ9.37066TTRlh-1hXhnA9oO9Pj6lgL6zFuJU0iCHhuCFno"
|
||||
}
|
||||
|
||||
|
||||
Alternatives
|
||||
~~~~~~~~~~~~
|
||||
|
||||
See the how-to :ref:`sql-user-management-using-postgres-users-and-passwords` for a similar way that completely avoids the table :code:`basic_auth.users`.
|
||||
@@ -0,0 +1,563 @@
|
||||
.. _working_with_types:
|
||||
|
||||
Working with PostgreSQL data types
|
||||
==================================
|
||||
|
||||
:author: `Laurence Isla <https://github.com/laurenceisla>`_
|
||||
|
||||
PostgREST makes use of PostgreSQL string representations to work with data types. Thanks to this, you can use special values, such as ``now`` for timestamps, ``yes`` for booleans or time values including the time zones. This page describes how you can take advantage of these string representations to perform operations on different PostgreSQL data types.
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
:depth: 1
|
||||
|
||||
Timestamps
|
||||
----------
|
||||
|
||||
You can use the **time zone** to filter or send data if needed.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create table reports (
|
||||
id int primary key
|
||||
, due_date timestamptz
|
||||
);
|
||||
|
||||
Suppose you are located in Sydney and want create a report with the date in the local time zone. Your request should look like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/reports" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '[{ "id": 1, "due_date": "2022-02-24 11:10:15 Australia/Sydney" },{ "id": 2, "due_date": "2022-02-27 22:00:00 Australia/Sydney" }]'
|
||||
|
||||
Someone located in Cairo can retrieve the data using their local time, too:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/reports?due_date=eq.2022-02-24+02:10:15+Africa/Cairo"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"due_date": "2022-02-23T19:10:15-05:00"
|
||||
}
|
||||
]
|
||||
|
||||
The response has the date in the time zone configured by the server: ``UTC -05:00`` (see :ref:`prefer_timezone`).
|
||||
|
||||
You can use other comparative filters and also all the `PostgreSQL special date/time input values <https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-DATETIME-SPECIAL-TABLE>`_ as illustrated in this example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/reports?or=(and(due_date.gte.today,due_date.lte.tomorrow),and(due_date.gt.-infinity,due_date.lte.epoch))"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"id": 2,
|
||||
"due_date": "2022-02-27T06:00:00-05:00"
|
||||
}
|
||||
]
|
||||
|
||||
JSON
|
||||
----
|
||||
|
||||
To work with a ``json`` type column, you can handle the value as a JSON object.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create table products (
|
||||
id int primary key,
|
||||
name text unique,
|
||||
extra_info json
|
||||
);
|
||||
|
||||
You can insert a new product using a JSON object for the ``extra_info`` column:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/products" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
{
|
||||
"id": 1,
|
||||
"name": "Canned fish",
|
||||
"extra_info": {
|
||||
"expiry_date": "2025-12-31",
|
||||
"exportable": true
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
To query and filter the data see :ref:`json_columns` for a complete reference.
|
||||
|
||||
Arrays
|
||||
------
|
||||
|
||||
To handle `array types <https://www.postgresql.org/docs/current/arrays.html>`_ you can use string representation or JSON array format.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create table movies (
|
||||
id int primary key,
|
||||
title text not null,
|
||||
tags text[],
|
||||
performance_times time[]
|
||||
);
|
||||
|
||||
You can insert a new value using string representation.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/movies" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Paddington",
|
||||
"tags": "{family,comedy,not streamable}",
|
||||
"performance_times": "{12:40,15:00,20:00}"
|
||||
}
|
||||
EOF
|
||||
|
||||
Or you could send the same data using JSON array format:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/movies" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Paddington",
|
||||
"tags": ["family", "comedy", "not streamable"],
|
||||
"performance_times": ["12:40", "15:00", "20:00"]
|
||||
}
|
||||
EOF
|
||||
|
||||
To query the data you can use arrow operators. See :ref:`composite_array_columns`.
|
||||
|
||||
Multidimensional Arrays
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Similarly to one-dimensional arrays, both the string representation and JSON array format are allowed.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- This new column stores the cinema, floor and auditorium numbers in that order
|
||||
alter table movies
|
||||
add column cinema_floor_auditorium int[][][];
|
||||
|
||||
You can now update the item using JSON array format:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/movies?id=eq.1" \
|
||||
-X PATCH -H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
{
|
||||
"cinema_floor_auditorium": [ [ [1,2], [6,7] ], [ [3,5], [8,9] ] ]
|
||||
}
|
||||
EOF
|
||||
|
||||
Then, for example, to query the auditoriums that are located in the first cinema (position 0 in the array) and on the second floor (position 1 in the next inner array), we can use the arrow operators this way:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/movies?select=title,auditorium:cinema_floor_auditorium->0->1&id=eq.1"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"title": "Paddington",
|
||||
"auditorium": [6,7]
|
||||
}
|
||||
]
|
||||
|
||||
Composite Types
|
||||
---------------
|
||||
|
||||
With PostgREST, you have two options to handle `composite type columns <https://www.postgresql.org/docs/current/rowtypes.html>`_.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create type dimension as (
|
||||
length decimal(6,2),
|
||||
width decimal (6,2),
|
||||
height decimal (6,2),
|
||||
unit text
|
||||
);
|
||||
|
||||
create table products (
|
||||
id int primary key,
|
||||
size dimension
|
||||
);
|
||||
|
||||
insert into products (id, size)
|
||||
values (1, '(5.0,5.0,10.0,"cm")');
|
||||
|
||||
On one hand you can insert values using string representation.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/products" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
{ "id": 2, "size": "(0.7,0.5,1.8,\"m\")" }
|
||||
EOF
|
||||
|
||||
Or you could insert the same data in JSON format.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/products" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
{
|
||||
"id": 2,
|
||||
"size": {
|
||||
"length": 0.7,
|
||||
"width": 0.5,
|
||||
"height": 1.8,
|
||||
"unit": "m"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
You can also query the data using arrow operators. See :ref:`composite_array_columns`.
|
||||
|
||||
Ranges
|
||||
------
|
||||
|
||||
PostgREST allows you to handle `ranges <https://www.postgresql.org/docs/current/rangetypes.html>`_.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create table events (
|
||||
id int primary key,
|
||||
name text unique,
|
||||
duration tsrange
|
||||
);
|
||||
|
||||
To insert a new event, specify the ``duration`` value as a string representation of the ``tsrange`` type:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/events" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
{
|
||||
"id": 1,
|
||||
"name": "New Year's Party",
|
||||
"duration": "['2022-12-31 11:00','2023-01-01 06:00']"
|
||||
}
|
||||
EOF
|
||||
|
||||
You can use range :ref:`operators <operators>` to filter the data. But, in this case, requesting a filter like ``events?duration=cs.2023-01-01`` will return an error, because PostgreSQL needs an explicit cast from string to timestamp. A workaround is to use a range starting and ending in the same date:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/events?duration=cs.\[2023-01-01,2023-01-01\]"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "New Year's Party",
|
||||
"duration": "[\"2022-12-31 11:00:00\",\"2023-01-01 06:00:00\"]"
|
||||
}
|
||||
]
|
||||
|
||||
.. _casting_range_to_json:
|
||||
|
||||
Casting a Range to a JSON Object
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As you may have noticed, the ``tsrange`` value is returned as a string literal. To return it as a JSON value, first you need to create a function that will do the conversion from a ``tsrange`` type:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function tsrange_to_json(tsrange) returns json as $$
|
||||
select json_build_object(
|
||||
'lower', lower($1)
|
||||
, 'upper', upper($1)
|
||||
, 'lower_inc', lower_inc($1)
|
||||
, 'upper_inc', upper_inc($1)
|
||||
);
|
||||
$$ language sql;
|
||||
|
||||
Then, create the cast using this function:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create cast (tsrange as json) with function tsrange_to_json(tsrange) as assignment;
|
||||
|
||||
Finally, do the request :ref:`casting the range column <casting_columns>`:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/events?select=id,name,duration::json"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"name": "New Year's Party",
|
||||
"duration": {
|
||||
"lower": "2022-12-31T11:00:00",
|
||||
"upper": "2023-01-01T06:00:00",
|
||||
"lower_inc": true,
|
||||
"upper_inc": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
.. note::
|
||||
|
||||
If you don't want to modify casts for built-in types, an option would be to `create a custom type <https://www.postgresql.org/docs/current/sql-createtype.html>`_
|
||||
for your own ``tsrange`` and add its own cast.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create type mytsrange as range (subtype = timestamp, subtype_diff = tsrange_subdiff);
|
||||
|
||||
-- define column types and casting function analogously to the above example
|
||||
-- ...
|
||||
|
||||
create cast (mytsrange as json) with function mytsrange_to_json(mytsrange) as assignment;
|
||||
|
||||
Bytea
|
||||
-----
|
||||
|
||||
To send raw binary to PostgREST you need a function with a single unnamed parameter of `bytea type <https://www.postgresql.org/docs/current/datatype-binary.html>`_.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create table files (
|
||||
id int primary key generated always as identity,
|
||||
file bytea
|
||||
);
|
||||
|
||||
create function upload_binary(bytea) returns void as $$
|
||||
insert into files (file) values ($1);
|
||||
$$ language sql;
|
||||
|
||||
Let's download the PostgREST logo for our test.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "https://postgrest.org/en/latest/_images/logo.png" -o postgrest-logo.png
|
||||
|
||||
Now, to send the file ``postgrest-logo.png`` we need to set the ``Content-Type: application/octet-stream`` header in the request:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/upload_binary" \
|
||||
-X POST -H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@postgrest-logo.png"
|
||||
|
||||
To get the image from the database, use :ref:`custom_media` like so:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "image/png" as bytea;
|
||||
|
||||
create or replace get_image(id int) returns "image/png" as $$
|
||||
select file from files where id = $1;
|
||||
$$ language sql;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/get_image?id=1" \
|
||||
-H "Accept: image/png"
|
||||
|
||||
See :ref:`providing_img` for a step-by-step example on how to handle images in HTML.
|
||||
|
||||
.. warning::
|
||||
|
||||
Be careful when saving binaries in the database, having a separate storage service for these is preferable in most cases. See `Storing Binary files in the Database <https://wiki.postgresql.org/wiki/BinaryFilesInDB>`_.
|
||||
|
||||
hstore
|
||||
------
|
||||
|
||||
You can work with data types belonging to additional supplied modules such as `hstore <https://www.postgresql.org/docs/current/hstore.html>`_.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- Activate the hstore module in the current database
|
||||
create extension if not exists hstore;
|
||||
|
||||
create table countries (
|
||||
id int primary key,
|
||||
name hstore unique
|
||||
);
|
||||
|
||||
The ``name`` column will have the name of the country in different formats. You can insert values using the string representation for that data type:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/countries" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
[
|
||||
{ "id": 1, "name": "common => Egypt, official => \"Arab Republic of Egypt\", native => مصر" },
|
||||
{ "id": 2, "name": "common => Germany, official => \"Federal Republic of Germany\", native => Deutschland" }
|
||||
]
|
||||
EOF
|
||||
|
||||
Notice that the use of ``"`` in the value of the ``name`` column needs to be escaped using a backslash ``\``.
|
||||
|
||||
You can also query and filter the value of a ``hstore`` column using the arrow operators, as you would do for a :ref:`JSON column<json_columns>`. For example, if you want to get the native name of Egypt:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/countries?select=name->>native&name->>common=like.Egypt"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[{ "native": "مصر" }]
|
||||
|
||||
.. _ww_postgis:
|
||||
|
||||
PostGIS
|
||||
-------
|
||||
|
||||
You can use the string representation for `PostGIS <https://postgis.net/>`_ data types such as ``geometry`` or ``geography`` (you need to `install PostGIS <https://postgis.net/documentation/getting_started/>`_ first).
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- Activate the postgis module in the current database
|
||||
create extension if not exists postgis;
|
||||
|
||||
create table coverage (
|
||||
id int primary key,
|
||||
name text unique,
|
||||
area geometry
|
||||
);
|
||||
|
||||
To add areas in polygon format, you can use string representation:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/coverage" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
[
|
||||
{ "id": 1, "name": "small", "area": "SRID=4326;POLYGON((0 0, 1 0, 1 1, 0 1, 0 0))" },
|
||||
{ "id": 2, "name": "big", "area": "SRID=4326;POLYGON((0 0, 10 0, 10 10, 0 10, 0 0))" }
|
||||
]
|
||||
EOF
|
||||
|
||||
Now, when you request the information, PostgREST will automatically cast the ``area`` column into a ``Polygon`` geometry type. Although this is useful, you may need the whole output to be in `GeoJSON <https://geojson.org/>`_ format out of the box, which can be done by including the ``Accept: application/geo+json`` in the request. This will work for PostGIS versions 3.0.0 and up and will return the output as a `FeatureCollection Object <https://www.rfc-editor.org/rfc/rfc7946#section-3.3>`_:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/coverage" \
|
||||
-H "Accept: application/geo+json"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"id": 1,
|
||||
"name": "small"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"id": 2,
|
||||
"name": "big"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
If you need to add an extra property, like the area in square units by using ``st_area(area)``, you could add a generated column to the table and it will appear in the ``properties`` key of each ``Feature``.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
alter table coverage
|
||||
add square_units double precision generated always as ( st_area(area) ) stored;
|
||||
|
||||
In the case that you are using older PostGIS versions, then creating a function is your best option:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function coverage_geo_collection() returns json as $$
|
||||
select
|
||||
json_build_object(
|
||||
'type', 'FeatureCollection',
|
||||
'features', json_agg(
|
||||
json_build_object(
|
||||
'type', 'Feature',
|
||||
'geometry', st_AsGeoJSON(c.area)::json,
|
||||
'properties', json_build_object('id', c.id, 'name', c.name)
|
||||
)
|
||||
)
|
||||
)
|
||||
from coverage c;
|
||||
$$ language sql;
|
||||
|
||||
Now this query will return the same results:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/coverage_geo_collection"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"type": "FeatureCollection",
|
||||
"features": [
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[[0,0],[1,0],[1,1],[0,1],[0,0]]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"id": 1,
|
||||
"name": "small"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "Feature",
|
||||
"geometry": {
|
||||
"type": "Polygon",
|
||||
"coordinates": [
|
||||
[[0,0],[10,0],[10,10],[0,10],[0,0]]
|
||||
]
|
||||
},
|
||||
"properties": {
|
||||
"id": 2,
|
||||
"name": "big"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
.. title:: PostgREST Documentation
|
||||
|
||||
PostgREST Documentation
|
||||
=======================
|
||||
|
||||
.. container:: image-container
|
||||
|
||||
.. figure:: _static/logo.png
|
||||
|
||||
.. image:: https://img.shields.io/github/stars/postgrest/postgrest.svg?style=social
|
||||
:target: https://github.com/PostgREST/postgrest
|
||||
|
||||
.. image:: https://img.shields.io/github/v/release/PostgREST/postgrest.svg
|
||||
:target: https://github.com/PostgREST/postgrest/releases
|
||||
|
||||
.. image:: https://img.shields.io/docker/pulls/postgrest/postgrest.svg
|
||||
:target: https://hub.docker.com/r/postgrest/postgrest/
|
||||
|
||||
.. image:: https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg
|
||||
:target: https://gitter.im/begriffs/postgrest
|
||||
|
||||
.. image:: https://img.shields.io/badge/Donate-Patreon-orange.svg?colorB=F96854
|
||||
:target: https://www.patreon.com/postgrest
|
||||
|
||||
.. image:: https://img.shields.io/badge/Donate-PayPal-green.svg
|
||||
:target: https://www.paypal.com/paypalme/postgrest
|
||||
|
||||
|
|
||||
|
||||
PostgREST is a standalone web server that turns your PostgreSQL database directly into a RESTful API. The structural constraints and permissions in the database determine the API endpoints and operations.
|
||||
|
||||
Sponsors
|
||||
--------
|
||||
|
||||
.. container:: image-container
|
||||
|
||||
.. image:: _static/cybertec-new.png
|
||||
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: _static/gnuhost.png
|
||||
:target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: _static/neon.jpg
|
||||
:target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
|
|
||||
|
||||
.. image:: _static/code-build.webp
|
||||
:target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. image:: _static/supabase.png
|
||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||
:width: 13em
|
||||
|
||||
.. image:: _static/tembo.png
|
||||
:target: https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
||||
:width: 13em
|
||||
|
||||
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
|
||||
to create space and center the logos. It's not easy to layout with restructuredText.
|
||||
|
||||
.. .. image:: _static/empty.png
|
||||
:target: #sponsors
|
||||
:width: 13em
|
||||
|
||||
|
|
||||
|
||||
Database as Single Source of Truth
|
||||
----------------------------------
|
||||
|
||||
Using PostgREST is an alternative to manual CRUD programming. Custom API servers suffer problems. Writing business logic often duplicates, ignores or hobbles database structure. Object-relational mapping is a leaky abstraction leading to slow imperative code. The PostgREST philosophy establishes a single declarative source of truth: the data itself.
|
||||
|
||||
Declarative Programming
|
||||
-----------------------
|
||||
|
||||
It's easier to ask PostgreSQL to join data for you and let its query planner figure out the details than to loop through rows yourself. It's easier to assign permissions to database objects than to add guards in controllers. (This is especially true for cascading permissions in data dependencies.) It's easier to set constraints than to litter code with sanity checks.
|
||||
|
||||
Leak-proof Abstraction
|
||||
----------------------
|
||||
|
||||
There is no ORM involved. Creating new views happens in SQL with known performance implications. A database administrator can now create an API from scratch with no custom programming.
|
||||
|
||||
One Thing Well
|
||||
--------------
|
||||
|
||||
PostgREST has a focused scope. It works well with other tools like Nginx. This forces you to cleanly separate the data-centric CRUD operations from other concerns. Use a collection of sharp tools rather than building a big ball of mud.
|
||||
|
||||
Getting Support
|
||||
----------------
|
||||
|
||||
The project has a friendly and growing community. For discussions, use the Github `discussions page <https://github.com/PostgREST/postgrest/discussions>`_ or join our `chat room <https://gitter.im/begriffs/postgrest>`_. You can also report or search for bugs/features on the Github `issues <https://github.com/PostgREST/postgrest/issues>`_ page.
|
||||
|
||||
Release Notes
|
||||
-------------
|
||||
|
||||
The release notes are published on `PostgREST's GitHub release page <https://github.com/PostgREST/postgrest/releases>`_.
|
||||
|
||||
Tutorials
|
||||
---------
|
||||
|
||||
Are you new to PostgREST? This is the place to start!
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
:caption: Tutorials
|
||||
:maxdepth: 1
|
||||
|
||||
tutorials/*
|
||||
|
||||
Also have a look at :ref:`install` and :ref:`community_tutorials`.
|
||||
|
||||
References
|
||||
----------
|
||||
|
||||
Technical references for PostgREST's functionality.
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
:caption: References
|
||||
:name: references
|
||||
:maxdepth: 1
|
||||
|
||||
references/auth.rst
|
||||
references/api.rst
|
||||
references/transactions.rst
|
||||
references/connection_pool.rst
|
||||
references/schema_cache.rst
|
||||
references/errors.rst
|
||||
references/configuration.rst
|
||||
references/observability.rst
|
||||
references/health_check.rst
|
||||
|
||||
Explanations
|
||||
------------
|
||||
|
||||
Key concepts in PostgREST.
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
:caption: Explanations
|
||||
:name: explanations
|
||||
:maxdepth: 1
|
||||
|
||||
explanations/*
|
||||
|
||||
How-tos
|
||||
-------
|
||||
|
||||
Recipes that'll help you address specific use-cases.
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
:caption: How-to guides
|
||||
:name: how-tos
|
||||
:maxdepth: 1
|
||||
|
||||
how-tos/sql-user-*
|
||||
how-tos/working-*
|
||||
how-tos/*
|
||||
|
||||
.. _intgrs:
|
||||
|
||||
Integrations
|
||||
------------
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
:caption: Integrations
|
||||
:name: integrations
|
||||
:maxdepth: 1
|
||||
|
||||
integrations/*
|
||||
|
||||
Ecosystem
|
||||
---------
|
||||
|
||||
PostgREST has a growing ecosystem of examples, libraries, and experiments. Here is a selection.
|
||||
|
||||
.. toctree::
|
||||
:caption: Ecosystem
|
||||
:name: ecosystem
|
||||
:maxdepth: 1
|
||||
|
||||
ecosystem.rst
|
||||
|
||||
In Production
|
||||
-------------
|
||||
|
||||
Here are some companies that use PostgREST in production.
|
||||
|
||||
* `Catarse <https://www.catarse.me>`_
|
||||
* `Datrium <https://www.datrium.com>`_
|
||||
* `Drip Depot <https://www.dripdepot.com>`_
|
||||
* `Image-charts <https://www.image-charts.com>`_
|
||||
* `Moat <https://www.oracle.com/advertising/measurement/>`_
|
||||
* `Netwo <https://www.netwo.io>`_
|
||||
* `Nimbus <https://www.nimbusfacility.com/sg/home>`_
|
||||
- See how Nimbus uses PostgREST in `Paul Copplestone's blog post <https://paul.copplest.one/blog/nimbus-tech-2019-04.html>`_.
|
||||
* `OpenBooking <https://openbooking.ch>`_
|
||||
* `Redsmin <https://www.redsmin.com>`_
|
||||
* `Sompani <https://www.sompani.com>`_
|
||||
* `Supabase <https://supabase.com>`_
|
||||
|
||||
.. Failing links
|
||||
* `eGull <http://www.egull.co>`_
|
||||
* `MotionDynamic - Fast highly dynamic video generation at scale <https://motiondynamic.tech>`_
|
||||
|
||||
Testimonials
|
||||
------------
|
||||
|
||||
"It's so fast to develop, it feels like cheating!"
|
||||
|
||||
-- François-Guillaume Ribreau
|
||||
|
||||
"I just have to say that, the CPU/Memory usage compared to our
|
||||
Node.js/Waterline ORM based API is ridiculous. It's hard to even push
|
||||
it over 60/70 MB while our current API constantly hits 1GB running on 6
|
||||
instances (dynos)."
|
||||
|
||||
-- Louis Brauer
|
||||
|
||||
"I really enjoyed the fact that all of a sudden I was writing
|
||||
microservices in SQL DDL (and v8 JavaScript functions). I dodged so
|
||||
much boilerplate. The next thing I knew, we pulled out a full rewrite
|
||||
of a Spring+MySQL legacy app in 6 months. Literally 10x faster, and
|
||||
code was super concise. The old one took 3 years and a team of 4
|
||||
people to develop."
|
||||
|
||||
-- Simone Scarduzio
|
||||
|
||||
"I like the fact that PostgREST does one thing, and one thing well.
|
||||
While PostgREST takes care of bridging the gap between our HTTP server
|
||||
and PostgreSQL database, we can focus on the development of our API in
|
||||
a single language: SQL. This puts the database in the center of our
|
||||
architecture, and pushed us to improve our skills in SQL programming
|
||||
and database design."
|
||||
|
||||
-- Eric Bréchemier, Data Engineer, eGull SAS
|
||||
|
||||
"PostgREST is performant, stable, and transparent. It allows us to
|
||||
bootstrap projects really fast, and to focus on our data and application
|
||||
instead of building out the ORM layer. In our k8s cluster, we run a few
|
||||
pods per schema we want exposed, and we scale up/down depending on demand.
|
||||
Couldn't be happier."
|
||||
|
||||
-- Anupam Garg, Datrium, Inc.
|
||||
|
||||
Contributing
|
||||
------------
|
||||
|
||||
Please see the `Contributing guidelines <https://github.com/PostgREST/postgrest/blob/main/.github/CONTRIBUTING.md>`_ in the main PostgREST repository.
|
||||
@@ -0,0 +1,6 @@
|
||||
Greenplum
|
||||
#########
|
||||
|
||||
`Greenplum <https://greenplum.org/>`_ has been reported to work by adding ``LOGIN`` to the :ref:`anonymous and user roles <roles>`.
|
||||
|
||||
For more details, see https://github.com/PostgREST/postgrest/issues/2021.
|
||||
@@ -0,0 +1,31 @@
|
||||
.. _external_jwt:
|
||||
|
||||
External JWT Generation
|
||||
-----------------------
|
||||
|
||||
JWT from Auth0
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
An external service like `Auth0 <https://auth0.com/>`_ can do the hard work transforming OAuth from Github, Twitter, Google etc into a JWT suitable for PostgREST. Auth0 can also handle email signup and password reset flows.
|
||||
|
||||
To use Auth0, create `an application <https://auth0.com/docs/get-started/applications>`_ for your app and `an API <https://auth0.com/docs/get-started/apis>`_ for your PostgREST server. Auth0 supports both HS256 and RS256 scheme for the issued tokens for APIs. For simplicity, you may first try HS256 scheme while creating your API on Auth0. Your application should use your PostgREST API's `API identifier <https://auth0.com/docs/get-started/apis/api-settings>`_ by setting it with the `audience parameter <https://auth0.com/docs/secure/tokens/access-tokens/get-access-tokens#control-access-token-audience>`_ during the authorization request. This will ensure that Auth0 will issue an access token for your PostgREST API. For PostgREST to verify the access token, you will need to set ``jwt-secret`` on PostgREST config file with your API's signing secret.
|
||||
|
||||
.. note::
|
||||
|
||||
Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata <https://auth0.com/docs/manage-users/user-accounts/metadata/manage-metadata-rules>`_. Then, you will need to write `a rule <https://auth0.com/docs/customize/rules>`_ that will extract the role from the user's app_metadata and set it as a `custom claim <https://auth0.com/docs/get-started/apis/scopes/sample-use-cases-scopes-and-claims#add-custom-claims-to-a-token>`_ in the access token. Note that, you may use Auth0's `core authorization feature <https://auth0.com/docs/manage-users/access-control/rbac>`_ for more complex use cases. Metadata solution is mentioned here for simplicity.
|
||||
|
||||
.. code:: javascript
|
||||
|
||||
function (user, context, callback) {
|
||||
|
||||
// Follow the documentations at
|
||||
// https://postgrest.org/en/latest/configuration.html#db-role-claim-key
|
||||
// to set a custom role claim on PostgREST
|
||||
// and use it as custom claim attribute in this rule
|
||||
const myRoleClaim = 'https://myapp.com/role';
|
||||
|
||||
user.app_metadata = user.app_metadata || {};
|
||||
context.accessToken[myRoleClaim] = user.app_metadata.role;
|
||||
callback(null, user, context);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
pg-safeupdate
|
||||
#############
|
||||
|
||||
.. _block_fulltable:
|
||||
|
||||
Block Full-Table Operations
|
||||
---------------------------
|
||||
|
||||
If the :ref:`active role <user_impersonation>` can delete table rows then the DELETE verb is allowed for clients. Here's an API request to delete old rows from a hypothetical logs table:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/logs?time=lt.1991-08-06" -X DELETE
|
||||
|
||||
Note that it's very easy to delete the **entire table** by omitting the query parameter!
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/logs" -X DELETE
|
||||
|
||||
This can happen accidentally such as by switching a request from a GET to a DELETE. To protect against accidental operations use the `pg-safeupdate <https://github.com/eradman/pg-safeupdate>`_ PostgreSQL extension. It raises an error if UPDATE or DELETE are executed without specifying conditions. To install it you can use the `PGXN <https://pgxn.org/>`_ network:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo -E pgxn install safeupdate
|
||||
|
||||
# then add this to postgresql.conf:
|
||||
# shared_preload_libraries='safeupdate';
|
||||
|
||||
This does not protect against malicious actions, since someone can add a url parameter that does not affect the result set. To prevent this you must turn to database permissions, forbidding the wrong people from deleting rows, and using `row-level security <https://www.postgresql.org/docs/current/ddl-rowsecurity.html>`_ if finer access control is required.
|
||||
@@ -0,0 +1,59 @@
|
||||
systemd
|
||||
=======
|
||||
|
||||
For Linux distributions that use **systemd** (Ubuntu, Debian, Archlinux) you can create a daemon in the following way.
|
||||
|
||||
First, create postgrest configuration in ``/etc/postgrest/config``
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
db-uri = "postgres://<your_user>:<your_password>@localhost:5432/<your_db>"
|
||||
db-schemas = "<your_exposed_schema>"
|
||||
db-anon-role = "<your_anon_role>"
|
||||
jwt-secret = "<your_secret>"
|
||||
|
||||
Create a dedicated ``postgrest`` user with:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
sudo useradd -M -U -d /nonexistent -s /usr/sbin/nologin postgrest
|
||||
|
||||
Then create the systemd service file in ``/etc/systemd/system/postgrest.service``
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[Unit]
|
||||
Description=REST API for any PostgreSQL database
|
||||
After=postgresql.service
|
||||
|
||||
[Service]
|
||||
User=postgrest
|
||||
Group=postgrest
|
||||
ExecStart=/bin/postgrest /etc/postgrest/config
|
||||
ExecReload=/bin/kill -SIGUSR1 $MAINPID
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
After that, you can enable the service at boot time and start it with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
systemctl enable postgrest
|
||||
systemctl start postgrest
|
||||
|
||||
## For reloading the service
|
||||
## systemctl restart postgrest
|
||||
|
||||
.. _file_descriptors:
|
||||
|
||||
File Descriptors
|
||||
----------------
|
||||
|
||||
File descriptors are kernel resources that are used by HTTP connections (among others). File descriptors are limited per process. The kernel default limit is 1024, which is increased in some Linux distributions.
|
||||
When under heavy traffic, PostgREST can reach this limit and start showing ``No file descriptors available`` errors. To clear these errors, you can increase the process' file descriptor limit.
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[Service]
|
||||
LimitNOFILE=10000
|
||||
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env python
|
||||
import sys
|
||||
from livereload import Server, shell
|
||||
from subprocess import call
|
||||
|
||||
if len(sys.argv) == 1:
|
||||
locale = "default"
|
||||
build = "./build.sh"
|
||||
else:
|
||||
locale = sys.argv[1]
|
||||
build = f"./build.sh {locale}"
|
||||
|
||||
call(build, shell=True)
|
||||
|
||||
server = Server()
|
||||
server.watch("**/*.rst", shell(build))
|
||||
server.watch(f"locales/{locale}/LC_MESSAGES/*.po", shell(build))
|
||||
server.serve(root=f"_build/html/{locale}")
|
||||
@@ -0,0 +1,189 @@
|
||||
personal_ws-1.1 en 0 utf-8
|
||||
api
|
||||
API's
|
||||
APISIX
|
||||
Archlinux
|
||||
aud
|
||||
Auth
|
||||
auth
|
||||
authenticator
|
||||
backoff
|
||||
booleans
|
||||
BOM
|
||||
Bytea
|
||||
Cardano
|
||||
casted
|
||||
cd
|
||||
centric
|
||||
coercible
|
||||
conf
|
||||
Cloudflare
|
||||
config
|
||||
cors
|
||||
CORS
|
||||
cryptographically
|
||||
CSV
|
||||
durations
|
||||
DDL
|
||||
DOM
|
||||
DevOps
|
||||
dockerize
|
||||
eq
|
||||
ETH
|
||||
Ethereum
|
||||
EveryLayout
|
||||
filename
|
||||
FreeBSD
|
||||
fts
|
||||
GeoJSON
|
||||
GHC
|
||||
Github
|
||||
Google
|
||||
grantor
|
||||
GraphQL
|
||||
Greenplum
|
||||
gte
|
||||
GUC
|
||||
Haskell
|
||||
HMAC
|
||||
htmx
|
||||
Htmx
|
||||
Homebrew
|
||||
hstore
|
||||
HTTP
|
||||
HTTPS
|
||||
HV
|
||||
Inlining
|
||||
inlined
|
||||
Integrations
|
||||
idletime
|
||||
IDLETIME
|
||||
ilike
|
||||
imatch
|
||||
io
|
||||
IP
|
||||
isdistinct
|
||||
JS
|
||||
js
|
||||
JSON
|
||||
JWK
|
||||
JWT
|
||||
jwt
|
||||
Kubernetes
|
||||
localhost
|
||||
login
|
||||
lookups
|
||||
Logins
|
||||
LIBPQ
|
||||
logins
|
||||
lon
|
||||
lt
|
||||
lte
|
||||
macOS
|
||||
misprediction
|
||||
multi
|
||||
namespace
|
||||
namespaced
|
||||
Nanos
|
||||
neq
|
||||
nginx
|
||||
nixpkgs
|
||||
npm
|
||||
nxl
|
||||
nxr
|
||||
OAuth
|
||||
Observability
|
||||
OpenAPI
|
||||
openapi
|
||||
ORM
|
||||
ov
|
||||
passphrase
|
||||
PBKDF
|
||||
PgBouncer
|
||||
pgcrypto
|
||||
pgjwt
|
||||
pgrst
|
||||
pgrstX
|
||||
PGRSTX
|
||||
pgSQL
|
||||
authid
|
||||
phfts
|
||||
phraseto
|
||||
plainto
|
||||
plfts
|
||||
poolers
|
||||
PostGIS
|
||||
PostgreSQL
|
||||
PostgreSQL's
|
||||
PostgREST
|
||||
postgres
|
||||
postgrest
|
||||
PostgREST's
|
||||
pre
|
||||
preflight
|
||||
plpgsql
|
||||
psql
|
||||
RabbitMQ
|
||||
RDS
|
||||
reallyreallyreallyreallyverysafe
|
||||
Redux
|
||||
refactor
|
||||
reloadable
|
||||
Reloadable
|
||||
requester's
|
||||
RESTful
|
||||
RLS
|
||||
RPC
|
||||
RSA
|
||||
safeupdate
|
||||
savepoint
|
||||
schemas
|
||||
schema's
|
||||
SHA
|
||||
signup
|
||||
SIGUSR
|
||||
sl
|
||||
spreaded
|
||||
Spreaded
|
||||
SQL
|
||||
sql
|
||||
sr
|
||||
SSL
|
||||
stateful
|
||||
stdout
|
||||
supervisees
|
||||
SvelteKit
|
||||
syslog
|
||||
systemd
|
||||
todo
|
||||
todos
|
||||
tos
|
||||
tsquery
|
||||
tx
|
||||
TypeScript
|
||||
UI
|
||||
ui
|
||||
unicode
|
||||
unikernel
|
||||
unix
|
||||
updatable
|
||||
unfulfillable
|
||||
Untyped
|
||||
UPSERT
|
||||
Upsert
|
||||
upsert
|
||||
uri
|
||||
url
|
||||
urlencoded
|
||||
urls
|
||||
variadic
|
||||
verifier
|
||||
versioning
|
||||
Vondra
|
||||
Vue
|
||||
webhooks
|
||||
websearch
|
||||
Websockets
|
||||
webuser
|
||||
wfts
|
||||
www
|
||||
@@ -0,0 +1,124 @@
|
||||
.. _api:
|
||||
|
||||
API
|
||||
###
|
||||
|
||||
PostgREST exposes three database objects of a schema as resources: tables, views and stored procedures.
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
:maxdepth: 1
|
||||
|
||||
api/tables_views.rst
|
||||
api/stored_procedures.rst
|
||||
api/schemas.rst
|
||||
api/computed_fields.rst
|
||||
api/domain_representations.rst
|
||||
api/pagination_count.rst
|
||||
api/resource_embedding.rst
|
||||
api/resource_representation.rst
|
||||
api/media_type_handlers.rst
|
||||
api/aggregate_functions.rst
|
||||
api/openapi.rst
|
||||
api/preferences.rst
|
||||
api/*
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript">
|
||||
let hash = window.location.hash;
|
||||
|
||||
const redirects = {
|
||||
// Tables and Views
|
||||
'#horizontal-filtering-rows': 'api/tables_views.html#horizontal-filtering-rows',
|
||||
'#operators': 'api/tables_views.html#operators',
|
||||
'#logical-operators': 'api/tables_views.html#logical-operators',
|
||||
'#pattern-matching': 'api/tables_views.html#pattern-matching',
|
||||
'#full-text-search': 'api/tables_views.html#full-text-search',
|
||||
'#vertical-filtering-columns': 'api/tables_views.html#vertical-filtering-columns',
|
||||
'#renaming-columns': 'api/tables_views.html#renaming-columns',
|
||||
'#casting-columns': 'api/tables_views.html#casting-columns',
|
||||
'#json-columns': 'api/tables_views.html#json-columns',
|
||||
'#composite-array-columns': 'api/tables_views.html#composite-array-columns',
|
||||
'#computed-virtual-columns': 'api/computed_fields.html#computed-fields',
|
||||
'#ordering': 'api/tables_views.html#ordering',
|
||||
'#limits-and-pagination': 'api/tables_views.html#limits-and-pagination',
|
||||
'#exact-count': 'api/tables_views.html#exact-count',
|
||||
'#planned-count': 'api/tables_views.html#planned-count',
|
||||
'#estimated-count': 'api/tables_views.html#estimated-count',
|
||||
'#updates': 'api/tables_views.html#update',
|
||||
'#insertions': 'api/tables_views.html#insert',
|
||||
'#bulk-insert': 'api/tables_views.html#bulk-insert',
|
||||
'#specifying-columns': 'api/tables_views.html#specifying-columns',
|
||||
'#upsert': 'api/tables_views.html#upsert',
|
||||
'#on-conflict': 'api/tables_views.html#on-conflict',
|
||||
'#put': 'api/tables_views.html#put',
|
||||
'#deletions': 'api/tables_views.html#delete',
|
||||
'#limited-updates-deletions': 'api/tables_views.html#limited-update-delete',
|
||||
// Stored procedures
|
||||
'#stored-procedures': 'api/stored_procedures.html#stored-procedures',
|
||||
'#calling-functions-with-a-single-json-parameter': 'api/stored_procedures.html#functions-with-a-single-json-parameter',
|
||||
'#calling-functions-with-a-single-unnamed-parameter': 'api/stored_procedures.html#functions-with-a-single-unnamed-parameter',
|
||||
'#calling-functions-with-array-parameters': 'api/stored_procedures.html#functions-with-array-parameters',
|
||||
'#calling-variadic-functions': 'api/stored_procedures.html#variadic-functions',
|
||||
'#scalar-functions': 'api/stored_procedures.html#scalar-functions',
|
||||
'#function-filters': 'api/stored_procedures.html#table-valued-functions',
|
||||
'#overloaded-functions': 'api/stored_procedures.html#overloaded-functions',
|
||||
// Schemas
|
||||
'#switching-schemas': 'api/schemas.html',
|
||||
// Resource Embedding
|
||||
'#resource-embedding': 'api/resource_embedding.html#resource-embedding',
|
||||
'#many-to-one-relationships': 'api/resource_embedding.html#many-to-one-relationships',
|
||||
'#one-to-many-relationships': 'api/resource_embedding.html#one-to-many-relationships',
|
||||
'#many-to-many-relationships': 'api/resource_embedding.html#many-to-many-relationships',
|
||||
'#one-to-one-relationships': 'api/resource_embedding.html#one-to-one-relationships',
|
||||
'#computed-relationships': 'api/resource_embedding.html#computed-relationships',
|
||||
'#nested-embedding': 'api/resource_embedding.html#nested-embedding',
|
||||
'#embedded-filters': 'api/resource_embedding.html#embedded-filters',
|
||||
'#embedding-with-top-level-filtering': 'api/resource_embedding.html#top-level-filtering',
|
||||
'#embedding-partitioned-tables': 'api/resource_embedding.html#embedding-partitioned-tables',
|
||||
'#embedding-views': 'api/resource_embedding.html#embedding-views',
|
||||
'#embedding-chains-of-views': 'api/resource_embedding.html#embedding-chains-of-views',
|
||||
'#embedding-on-stored-procedures': 'api/resource_embedding.html#embedding-on-stored-procedures',
|
||||
'#embedding-after-insertions-updates-deletions': 'api/resource_embedding.html#embedding-after-insertions-updates-deletions',
|
||||
'#embedding-disambiguation': 'api/resource_embedding.html#embedding-disambiguation',
|
||||
'#target-disambiguation': 'api/resource_embedding.html#target-disambiguation',
|
||||
'#hint-disambiguation': 'api/resource_embedding.html#hint-disambiguation',
|
||||
"#embedding-through-join-tables": "api/resource_embedding.html#many-to-many-relationships",
|
||||
// OpenAPI
|
||||
'#openapi-support': 'api/openapi.html',
|
||||
// Resource Representation
|
||||
'#response-format': 'api/resource_representation.html#response-format',
|
||||
'#singular-or-plural': 'api/resource_representation.html#singular-or-plural',
|
||||
'#response-formats-for-scalar-responses': 'api/resource_representation.html#scalar-function-response-format',
|
||||
// CORS
|
||||
'#cors': 'api/cors.html',
|
||||
// OPTIONS
|
||||
'#options': 'api/options.html',
|
||||
// URL Grammar
|
||||
'#custom-queries': 'api/url_grammar.html#custom-queries',
|
||||
'#unicode-support': 'api/url_grammar.html#unicode-support',
|
||||
'#table-columns-with-spaces': 'api/url_grammar.html#table-columns-with-spaces',
|
||||
'#reserved-characters': 'api/url_grammar.html#reserved-characters',
|
||||
// Transactions
|
||||
'#immutable-and-stable-functions': 'transactions.html#access-mode',
|
||||
'#http-context': 'transactions.html#transaction-scoped-settings',
|
||||
'#accessing-request-headers-cookies-and-jwt-claims': 'transactions.html#request-headers-cookies-and-jwt-claims',
|
||||
'#legacy-guc-variable-names': 'transactions.html#transaction-scoped-settings',
|
||||
'#accessing-request-path-and-method': 'transactions.html#request-path-and-method',
|
||||
'#setting-response-headers': 'transactions.html#response-headers',
|
||||
'#setting-headers-via-pre-request': 'transactions.html#setting-headers-via-pre-request',
|
||||
'#setting-response-status-code': 'transactions.html#response-status-code',
|
||||
'#raise-errors-with-http-status-codes': 'transactions.html#raise-errors-with-http-status-codes',
|
||||
// Admin
|
||||
'#execution-plan': 'admin.html#execution-plan',
|
||||
// Deprecated
|
||||
'#bulk-call': '../releases/v11.0.1.html#breaking-changes',
|
||||
};
|
||||
|
||||
let willRedirectTo = redirects[hash];
|
||||
|
||||
if (willRedirectTo) {
|
||||
window.location.href = willRedirectTo;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,288 @@
|
||||
.. _aggregate_functions:
|
||||
|
||||
Aggregate Functions
|
||||
###################
|
||||
|
||||
Aggregate functions allow you to summarize data by performing calculations across groups of rows. For instance, if you have an ``orders`` table that has an ``amount`` column, you could use an aggregate function to get the sum of the ``amount`` column, either for all rows, or for each group of rows that share specific values, for instance all rows that share the same ``order_date``.
|
||||
|
||||
.. note::
|
||||
Aggregate functions are *disabled* by default in PostgREST, as without appropriate safeguards, aggregate functions can create performance problems. See :ref:`db-aggregates-enabled` for further details.
|
||||
|
||||
PostgREST supports the following aggregate functions: ``avg()``, ``count()``, ``max()``, ``min()``, and ``sum()``. Please refer to the `section on aggregate functions in the PostgreSQL documentation <https://www.postgresql.org/docs/current/functions-aggregate.html>`_ for a detailed explanation of these functions.
|
||||
|
||||
To use an aggregate function, you append the function to a value in the ``select`` parameter, like so:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/orders?select=amount.sum()"
|
||||
|
||||
With the above query, PostgREST will return a single row with a single column named ``sum`` that contains the sum of all the values in the ``amount`` column:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"sum": 1234.56
|
||||
}
|
||||
]
|
||||
|
||||
You can use multiple aggregate functions by just adding more columns with aggregate functions to the ``select`` parameter.
|
||||
|
||||
To group by other columns, you simply add those columns to the ``select`` parameter. For instance:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/orders?select=amount.sum(),amount.avg(),order_date"
|
||||
|
||||
This will return a row for each unique value in the ``order_date`` column, with the sum and average of the ``amount`` column for all rows that share the same ``order_date``:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"sum": 1234.56,
|
||||
"avg": 123.45,
|
||||
"order_date": "2023-01-01"
|
||||
},
|
||||
{
|
||||
"sum": 2345.67,
|
||||
"avg": 234.56,
|
||||
"order_date": "2023-01-02"
|
||||
}
|
||||
]
|
||||
|
||||
.. note::
|
||||
Aggregate functions work alongside other PostgREST features, like :ref:`h_filter`, :ref:`json_columns`, and :ref:`ordering`. Please note at this time aggregate functions are not compatible with :ref:`domain_reps`. Additionally, PostgreSQL's ``HAVING`` clause and ordering by aggregated columns are not yet supported.
|
||||
|
||||
The Case of ``count()``
|
||||
===========================
|
||||
|
||||
.. note::
|
||||
Before the addition of aggregate functions, it was possible to count by adding ``count`` (without parentheses) to the ``select`` parameter. While this is still supported, it may be deprecated in the future, and thus use of this legacy feature is **not recommended.** Please use ``count()`` (with parentheses) instead.
|
||||
|
||||
|
||||
``count()`` is treated specially, as it can be used without an associated column. Take for example the following query:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/orders?select=count(),order_date"
|
||||
|
||||
This would return a row for each unique value in the ``order_date`` column, with the count of all rows that share the same ``order_date``:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"count": 4,
|
||||
"order_date": "2023-01-01"
|
||||
},
|
||||
{
|
||||
"count": 2,
|
||||
"order_date": "2023-01-02"
|
||||
}
|
||||
]
|
||||
|
||||
When ``count()`` is used with an associated column, its behaviour is slightly different: It will return the count of all values that are not ``NULL``. This is due to how PostgreSQL itself implements the ``count()`` function.
|
||||
|
||||
Renaming and Casting
|
||||
====================
|
||||
|
||||
Renaming Aggregates
|
||||
-------------------
|
||||
|
||||
Just like with other columns, you can rename aggregated columns too. See :ref:`renaming_columns` for details.
|
||||
|
||||
Renaming columns is especially helpful in the context of aggregate functions, as by default a column with an aggregate function applied will take on the name of the applied aggregate function. You may want to provide a more semantically meaningful name or prevent collisions when using multiple aggregate functions of the same type.
|
||||
|
||||
Casting Aggregates
|
||||
------------------
|
||||
|
||||
When applying an aggregate function to a column, you are able to cast both the value of the input to the aggregate function *and* the value of the output from the aggregate function. In both cases, the syntax works as described in :ref:`casting_columns`, with the only difference being the placement of the cast.
|
||||
|
||||
Casting the Value of the Input
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For instance, imagine that the ``orders`` table has a JSON column, ``order_details``, and this column contains a JSON object that has a key, ``tax_amount``. Let's say you want to get the sum of the tax amount for every order. You can use the ``->`` or ``->>`` operators to extract the value with this key (see :ref:`json_columns`), but these operators will return values of the types JSON and ``text`` respectively, and neither of these types can be used with ``sum()``.
|
||||
|
||||
Therefore, you will need to first cast the input value to a type that is compatible with ``sum()`` (e.g. ``numeric``). Casting the input value is done in exactly the same way as casting any other value:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/orders?select=order_details->tax_amount::numeric.sum()"
|
||||
|
||||
With this, you will receive the sum of the casted ``tax_amount`` value:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"sum": 1234.56
|
||||
}
|
||||
]
|
||||
|
||||
Casting the Value of the Output
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Now let's return to an example involving the ``amount`` column of the ``orders`` table. Imagine that we want to get the rounded average of the ``amount`` column. One way to do this is to use the ``avg()`` aggregate function and then to cast the output value of the function to ``int``. To cast the value of the output of the function, we simply place the cast *after* the aggregate function:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/orders?select=amount.avg()::int"
|
||||
|
||||
You will then receive the rounded average as the result:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"avg": 201
|
||||
}
|
||||
]
|
||||
|
||||
Of course, you can use both input and output casts at the same time, if you so desire.
|
||||
|
||||
|
||||
Using Aggregate Functions with Resource Embedding
|
||||
=================================================
|
||||
|
||||
Aggregate functions can be used in conjunction with :ref:`resource_embedding`. You can use embedded resources as grouping columns, use aggregate functions within the context of an embedded resource, or use columns from a spreaded resource as grouping columns or as inputs to aggregate functions.
|
||||
|
||||
Using Embedded Resources as Grouping Columns
|
||||
--------------------------------------------
|
||||
|
||||
Using an embedded resource as a grouping column allows you to use data from an association to group the results of an aggregation.
|
||||
|
||||
For example, imagine that the ``orders`` table from the examples above is related to a ``customers`` table. If you want to get the sum of the ``amount`` column grouped by the ``name`` column from the ``customers`` table, you can include the customer name, using the standard :ref:`resource_embedding` syntax, and perform a sum on the ``amount`` column.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/orders?select=amount.sum(),customers(name)"
|
||||
|
||||
You will then get the summed amount, along with the embedded customer resource:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"sum": 100,
|
||||
"customers": {
|
||||
"name": "Customer A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"sum": 200,
|
||||
"customers": {
|
||||
"name": "Customer B"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
.. note::
|
||||
The previous example uses a has-one association to demonstrate this functionality, but you may also use has-many associations as grouping columns, although there are few obvious use cases for this.
|
||||
|
||||
Using Aggregate Functions Within the Context of an Embedded Resource
|
||||
--------------------------------------------------------------------
|
||||
|
||||
When embedding a resource, you can apply aggregate functions to columns from the associated resource to perform aggregations within the context of an embedded resource.
|
||||
|
||||
Continuing with the example relationship between ``orders`` and ``customers`` from the previous section, imagine that you want to fetch the ``name``, ``city``, and ``state`` for each customer, along with the sum of amount of the customer's orders, grouped by the order date. This can be done in the following way:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/customers?select=name,city,state,orders(amount.sum(),order_date)"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"name": "Customer A",
|
||||
"city": "New York",
|
||||
"state": "NY",
|
||||
"orders": [
|
||||
{
|
||||
"sum": 215.22,
|
||||
"order_date": "2023-09-01"
|
||||
},
|
||||
{
|
||||
"sum": 905.73,
|
||||
"order_date": "2023-09-02"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Customer B",
|
||||
"city": "Los Angeles",
|
||||
"state": "CA",
|
||||
"orders": [
|
||||
{
|
||||
"sum": 329.71,
|
||||
"order_date": "2023-09-01"
|
||||
},
|
||||
{
|
||||
"sum": 425.87,
|
||||
"order_date": "2023-09-03"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
In this example, the ``amount`` column is summed and grouped by the ``order_date`` *within* the context of the embedded resource. That is, the ``name``, ``city``, and ``state`` from the ``customers`` table have no bearing on the aggregation performed in the context of the ``orders`` association; instead, each aggregation can be seen as being performed independently on just the orders belonging to a particular customer, using only the data from the embedded resource for both grouping and aggregation.
|
||||
|
||||
Using Columns from a Spreaded Resource
|
||||
--------------------------------------
|
||||
|
||||
When you :ref:`spread an embedded resource <spread_embed>`, the columns from the spreaded resource are treated as if they were columns of the top-level resource, both when using them as grouping columns and when applying aggregate functions to them.
|
||||
|
||||
Grouping with Columns from a Spreaded Resource
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
For instance, assume you want to sum the ``amount`` column from the ``orders`` table, using the ``city`` and ``state`` columns from the ``customers`` table as grouping columns. To achieve this, you may select these two columns from the ``customers`` table and spread them; they will then be used as grouping columns:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/orders?select=amount.sum(),...customers(city,state)
|
||||
|
||||
The result will be the same as if ``city`` and ``state`` were columns from the ``orders`` table:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"sum": 2000.29,
|
||||
"city": "New York",
|
||||
"state": "NY"
|
||||
},
|
||||
{
|
||||
"sum": 9241.21,
|
||||
"city": "Los Angeles",
|
||||
"state": "CA"
|
||||
}
|
||||
]
|
||||
|
||||
Aggregate Functions with Columns from a Spreaded Resource
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Now imagine that the ``customers`` table has a ``joined_date`` column that represents the date that the customer joined. You want to get both the most recent and the oldest ``joined_date`` for customers that placed an order on every distinct order date. This can be expressed as follows:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/orders?select=order_date,...customers(joined_date.max(),joined_date.min())
|
||||
|
||||
As columns from a spreaded resource are treated as if they were columns from the top-level resource, the ``max()`` and ``min()`` are applied *within* the context of the top-level, rather than within the context of the embedded resource, as in the previous section.
|
||||
|
||||
The result will be the same as if the aggregations were applied to columns from the top-level:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"order_date": "2023-11-01",
|
||||
"max": "2023-10-15",
|
||||
"min": "2013-10-01"
|
||||
},
|
||||
{
|
||||
"order_date": "2023-11-02",
|
||||
"max": "2023-10-30",
|
||||
"min": "2016-02-11"
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,75 @@
|
||||
.. _computed_cols:
|
||||
|
||||
Computed Fields
|
||||
###############
|
||||
|
||||
Computed fields are virtual columns that are not stored in a table. PostgreSQL makes it possible to implement them using functions on table types.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE TABLE people (
|
||||
first_name text
|
||||
, last_name text
|
||||
, job text
|
||||
);
|
||||
|
||||
-- a computed field that combines data from two columns
|
||||
CREATE FUNCTION full_name(people)
|
||||
RETURNS text AS $$
|
||||
SELECT $1.first_name || ' ' || $1.last_name;
|
||||
$$ LANGUAGE SQL;
|
||||
|
||||
Horizontal Filtering on Computed Fields
|
||||
=======================================
|
||||
|
||||
:ref:`h_filter` can be applied to computed fields. For example, we can do a :ref:`fts` on :code:`full_name`:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- (optional) you can add an index on the computed field to speed up the query
|
||||
CREATE INDEX people_full_name_idx ON people
|
||||
USING GIN (to_tsvector('english', full_name(people)));
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?full_name=fts.Beckett"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{"first_name": "Samuel", "last_name": "Beckett", "job": "novelist"}
|
||||
]
|
||||
|
||||
Vertical Filtering on Computed Fields
|
||||
=====================================
|
||||
|
||||
Computed fields won't appear on the response by default but you can use :ref:`v_filter` to include them:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?select=full_name,job"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{"full_name": "Samuel Beckett", "job": "novelist"}
|
||||
]
|
||||
|
||||
Ordering on Computed Fields
|
||||
===========================
|
||||
|
||||
:ref:`ordering` on computed fields is also possible:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?order=full_name.desc"
|
||||
|
||||
.. important::
|
||||
|
||||
Computed columns must be created in the :ref:`exposed schema <db-schemas>` or in a schema in the :ref:`extra search path <db-extra-search-path>` to be used in this way. When placing the computed column in the :ref:`exposed schema <db-schemas>` you can use an **unnamed** parameter, as in the example above, to prevent it from being exposed as an :ref:`RPC <s_procs>` under ``/rpc``.
|
||||
|
||||
.. note::
|
||||
|
||||
- PostgreSQL 12 introduced `generated columns <https://www.postgresql.org/docs/12/ddl-generated-columns.html>`_, which can also compute a value based on other columns. However they're stored, not virtual.
|
||||
- "computed fields" are documented on https://www.postgresql.org/docs/current/rowtypes.html#ROWTYPES-USAGE (search for "computed fields")
|
||||
- On previous PostgREST versions this feature was documented with the name of "computed columns".
|
||||
@@ -0,0 +1,40 @@
|
||||
.. _cors:
|
||||
|
||||
CORS
|
||||
####
|
||||
|
||||
By default, PostgREST sets highly permissive cross origin resource sharing, that is why it accepts Ajax requests from any domain. This behavior can be configured by using :ref:`server_cors_allowed_origins`.
|
||||
|
||||
|
||||
It also handles `preflight requests <https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request>`_ done by the browser, which are cached using the returned ``Access-Control-Max-Age: 86400`` header (86400 seconds = 24 hours). This is useful to reduce the latency of the subsequent requests.
|
||||
|
||||
A ``POST`` preflight request would look like this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/items" \
|
||||
-X OPTIONS \
|
||||
-H "Origin: http://example.com" \
|
||||
-H "Access-Control-Request-Method: POST" \
|
||||
-H "Access-Control-Request-Headers: Content-Type"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Access-Control-Allow-Origin: http://example.com
|
||||
Access-Control-Allow-Credentials: true
|
||||
Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD
|
||||
Access-Control-Allow-Headers: Authorization, Content-Type, Accept, Accept-Language, Content-Language
|
||||
Access-Control-Max-Age: 86400
|
||||
|
||||
.. _allowed_origins:
|
||||
|
||||
Allowed Origins
|
||||
===============
|
||||
|
||||
With the following config setting, PostgREST will accept CORS requests from domains :code:`http://example.com` and :code:`http://example2.com`.
|
||||
|
||||
|
||||
.. code-block::
|
||||
|
||||
server-cors-allowed-origins="http://example.com, http://example2.com"
|
||||
@@ -0,0 +1,176 @@
|
||||
.. _domain_reps:
|
||||
|
||||
Domain Representations
|
||||
######################
|
||||
|
||||
Domain Representations separates "how the data is presented" from "how the data is stored". It works by creating `domains <https://www.postgresql.org/docs/current/sql-createdomain.html>`_ and `casts <https://www.postgresql.org/docs/current/sql-createcast.html>`_, the latter act on the former to present and receive the data in different formats.
|
||||
|
||||
.. contents::
|
||||
:depth: 1
|
||||
:local:
|
||||
:backlinks: none
|
||||
|
||||
Custom Domain
|
||||
=============
|
||||
|
||||
Suppose you want to use a ``uuid`` type for a primary key and want to present it shortened to web users.
|
||||
|
||||
For this, let's create a domain based on ``uuid``.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain app_uuid as uuid;
|
||||
|
||||
-- and use it as our table PK.
|
||||
create table profiles(
|
||||
id app_uuid
|
||||
, name text
|
||||
);
|
||||
|
||||
-- some data for the example
|
||||
insert into profiles values ('846c4ffd-92ce-4de7-8d11-8e29929f4ec4', 'John Doe');
|
||||
|
||||
Domain Response Format
|
||||
======================
|
||||
|
||||
We can shorten the ``uuid`` with ``base64`` encoding. Let's use JSON as our response format for this example.
|
||||
|
||||
To change the domain format for JSON, create a function that converts ``app_uuid`` to ``json``.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- the name of the function is arbitrary
|
||||
CREATE OR REPLACE FUNCTION json(app_uuid) RETURNS json AS $$
|
||||
select to_json(encode(uuid_send($1),'base64'));
|
||||
$$ LANGUAGE SQL IMMUTABLE;
|
||||
|
||||
-- check it works
|
||||
select json('846c4ffd-92ce-4de7-8d11-8e29929f4ec4'::app_uuid);
|
||||
json
|
||||
----------------------------
|
||||
"hGxP/ZLOTeeNEY4pkp9OxA=="
|
||||
|
||||
Then create a CAST to tell PostgREST to convert it automatically whenever a JSON response is requested.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE CAST (app_uuid AS json) WITH FUNCTION json(app_uuid) AS IMPLICIT;
|
||||
|
||||
With this you can obtain the data in the shortened format.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/profiles" \
|
||||
-H "Accept: application/json"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[{"id":"hGxP/ZLOTeeNEY4pkp9OxA==","name":"John Doe"}]
|
||||
|
||||
.. note::
|
||||
|
||||
- Casts on domains are ignored by PostgreSQL, their interpretation is left to the application. We're discussing the possibility of including the Domain Representations behavior on `pgsql-hackers <https://www.postgresql.org/message-id/flat/CAGRrpzZKa%2BGu91j1SOvN3tM1f-7Gh_w441c5nAX1QqdH3Q31Lg%40mail.gmail.com>`_.
|
||||
- It would make more sense to use ``base58`` encoding as it's URL friendly but for simplicity we use ``base64`` (supported natively in PostgreSQL).
|
||||
|
||||
.. important::
|
||||
|
||||
After creating a cast over a domain, you must refresh PostgREST schema cache. See :ref:`schema_reloading`.
|
||||
|
||||
Domain Filter Format
|
||||
====================
|
||||
|
||||
For :ref:`h_filter` to work with the shortened format, you need a different conversion.
|
||||
|
||||
PostgREST considers the URL query string to be, in the most generic sense, ``text``. So let's create a function that converts ``text`` to ``app_uuid``.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- the name of the function is arbitrary
|
||||
CREATE OR REPLACE FUNCTION app_uuid(text) RETURNS app_uuid AS $$
|
||||
select substring(decode($1,'base64')::text from 3)::uuid;
|
||||
$$ LANGUAGE SQL IMMUTABLE;
|
||||
|
||||
-- plus a CAST to tell PostgREST to use this function
|
||||
CREATE CAST (text AS app_uuid) WITH FUNCTION app_uuid(text) AS IMPLICIT;
|
||||
|
||||
Now you can filter as usual.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/profiles?id=eq.ZLOTeeNEY4pkp9OxA==" \
|
||||
-H "Accept: application/json"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[{"id":"hGxP/ZLOTeeNEY4pkp9OxA==","name":"John Doe"}]
|
||||
|
||||
.. note::
|
||||
|
||||
If there's no CAST from ``text`` to ``app_uuid`` defined, the filter will still work with the native uuid format (``846c4ffd-92ce-4de7-8d11-8e29929f4ec4``).
|
||||
|
||||
Domain Request Body Format
|
||||
==========================
|
||||
|
||||
To accept the shortened format in a JSON request body, for example when creating a new record, define a ``json`` to ``app_uuid`` conversion.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- the name of the function is arbitrary
|
||||
CREATE OR REPLACE FUNCTION app_uuid(json) RETURNS public.app_uuid AS $$
|
||||
-- here we reuse the previous app_uuid(text) function
|
||||
select app_uuid($1 #>> '{}');
|
||||
$$ LANGUAGE SQL IMMUTABLE;
|
||||
|
||||
CREATE CAST (json AS public.app_uuid) WITH FUNCTION app_uuid(json) AS IMPLICIT;
|
||||
|
||||
Now we can :ref:`insert` (or :ref:`update`) as usual.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/profiles" \
|
||||
-H "Prefer: return=representation" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- <<JSON
|
||||
|
||||
{"id":"zH7HbFJUTfy/GZpwuirpuQ==","name":"Jane Doe"}
|
||||
|
||||
JSON
|
||||
|
||||
The response:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[{"id":"zH7HbFJUTfy/GZpwuirpuQ==","name":"Jane Doe"}]
|
||||
|
||||
Note that on the database side we have our regular ``uuid`` format.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
select * from profiles;
|
||||
|
||||
id | name
|
||||
--------------------------------------+----------
|
||||
846c4ffd-92ce-4de7-8d11-8e29929f4ec4 | John Doe
|
||||
cc7ec76c-5254-4dfc-bf19-9a70ba2ae9b9 | Jane Doe
|
||||
(2 rows)
|
||||
|
||||
.. note::
|
||||
|
||||
If there's no CAST from ``json`` to ``app_uuid`` defined, the request body will still work with the native uuid format (``cc7ec76c-5254-4dfc-bf19-9a70ba2ae9b9``).
|
||||
|
||||
Advantages over Views
|
||||
=====================
|
||||
|
||||
`Views <https://www.postgresql.org/docs/current/sql-createview.html>`_ also allow us to change the format of the underlying type. However they come with drawbacks that increase complexity.
|
||||
|
||||
1) Formatting the column in the view makes it `non-updatable <https://www.postgresql.org/docs/current/sql-createview.html#SQL-CREATEVIEW-UPDATABLE-VIEWS>`_ since Postgres doesn't know how to reverse the transform. This can be worked around using INSTEAD OF triggers.
|
||||
2) When filtering by this column, we get full table scans for the same reason (also applies to :ref:`computed_cols`) . The performance loss here can be avoided with a computed index, or using a materialized generated column.
|
||||
3) If the formatted column is used as a foreign key, PostgREST can no longer detect that relationship and :ref:`resource_embedding` breaks. This can be worked around with :ref:`computed_relationships`.
|
||||
|
||||
Domain Representations avoid all the above drawbacks. Their only drawback is that for existing tables, you have to change the column types. But this should be a fast operation since domains are binary coercible with their underlying types. A table rewrite won't be required.
|
||||
|
||||
.. note::
|
||||
|
||||
Why not create a `base type <https://www.postgresql.org/docs/current/sql-createtype.html#id-1.9.3.94.5.8>`_ instead? ``CREATE TYPE app_uuid (INTERNALLENGTH = 22, INPUT = app_uuid_parser, OUTPUT = app_uuid_formatter)``.
|
||||
|
||||
Creating base types need superuser, which is restricted on cloud hosted databases. Additionally this way lets “how the data is presented” dictate “how the data is stored” which would be backwards.
|
||||
@@ -0,0 +1,318 @@
|
||||
.. _custom_media:
|
||||
|
||||
Media Type Handlers
|
||||
###################
|
||||
|
||||
Media Type Handlers allow PostgREST to deliver custom media types. These handlers extend the :ref:`builtin ones <builtin_media>` and can also override them.
|
||||
|
||||
Media types are expressed as type aliases using `domains <https://www.postgresql.org/docs/current/sql-createdomain.html>`_ and their name must comply to `RFC 6838 requirements <https://datatracker.ietf.org/doc/html/rfc6838#section-4.2>`_.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE DOMAIN "application/json" AS json;
|
||||
|
||||
Using these domains, :ref:`functions <s_procs>` can become handlers and `user-defined aggregates <https://www.postgresql.org/docs/current/xaggr.html>`_ can serve as handlers for :ref:`tables_views` and :ref:`table_functions`.
|
||||
|
||||
.. important::
|
||||
|
||||
- PostgREST vendor media types (``application/vnd.pgrst.plan``, ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``) cannot be overriden.
|
||||
- Long media types like ``application/vnd.openxmlformats-officedocument.wordprocessingml.document`` cannot be expressed as domains since they surpass `PostgreSQL identifier length <https://www.postgresql.org/docs/current/limits.html#LIMITS-TABLE>`_.
|
||||
For these you can use the :ref:`any_handler`.
|
||||
|
||||
Handler Function
|
||||
================
|
||||
|
||||
As an example, let's obtain the `TWKB <https://postgis.net/docs/ST_AsTWKB.html>`_ compressed binary format for a PostGIS geometry.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create extension postgis;
|
||||
|
||||
create table lines (
|
||||
id int primary key
|
||||
, name text
|
||||
, geom geometry(LINESTRING, 4326)
|
||||
);
|
||||
|
||||
insert into lines values (1, 'line-1', 'LINESTRING(1 1,5 5)'::geometry), (2, 'line-2', 'LINESTRING(2 2,6 6)'::geometry);
|
||||
|
||||
For this you can create a vendor media type.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "application/vnd.twkb" as bytea;
|
||||
|
||||
And use it as a return type on a function, to make it a handler.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function get_line (id int)
|
||||
returns "application/vnd.twkb" as $$
|
||||
select st_astwkb(geom) from lines where id = get_line.id;
|
||||
$$ language sql;
|
||||
|
||||
.. note::
|
||||
|
||||
For PostgreSQL <= 12, you'll need a cast on the function body :code:`st_astwkb(geom)::"application/vnd.twkb"`.
|
||||
|
||||
Now you can request the ``TWKB`` output like so:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/rpc/get_line?id=1' -i \
|
||||
-H "Accept: application/vnd.twkb"
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/vnd.twkb
|
||||
|
||||
# binary output
|
||||
|
||||
Note that PostgREST will automatically set the ``Content-Type`` to ``application/vnd.twkb``.
|
||||
|
||||
Handlers for Tables/Views
|
||||
=========================
|
||||
|
||||
To benefit from a compressed format like ``TWKB``, it makes more sense to obtain many rows instead of one. Let's allow that by adding a handler for the table.
|
||||
|
||||
User-defined aggregates can be turned into handlers by using domain media types as the return type of their transition or final functions.
|
||||
|
||||
Let's create a transition function for this example.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function twkb_handler_transition (state bytea, next lines)
|
||||
returns "application/vnd.twkb" as $$
|
||||
select state || st_astwkb(next.geom);
|
||||
$$ language sql;
|
||||
|
||||
Now we'll use it on a new aggregate defined for the ``lines`` table.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace aggregate twkb_agg (lines) (
|
||||
initcond = ''
|
||||
, stype = "application/vnd.twkb"
|
||||
, sfunc = twkb_handler_transition
|
||||
);
|
||||
|
||||
.. note::
|
||||
|
||||
You can test see this aggregate working with:
|
||||
|
||||
.. code-block:: psql
|
||||
|
||||
SELECT twkb_agg(l) from lines l;
|
||||
|
||||
twkb_agg
|
||||
---------------------------------------------------------------
|
||||
\xa20002c09a0cc09a0c80ea3080ea30a2000280b51880b51880ea3080ea30
|
||||
(1 row)
|
||||
|
||||
Now you can request the table endpoint with the ``twkb`` media type:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/lines' -i \
|
||||
-H "Accept: application/vnd.twkb"
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/vnd.twkb
|
||||
|
||||
# binary output
|
||||
|
||||
If you have a table-valued function returning the same table type, the handler can also act upon on it.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function get_lines ()
|
||||
returns setof lines as $$
|
||||
select * from lines;
|
||||
$$ language sql;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/get_lines' -i \
|
||||
-H "Accept: application/vnd.twkb"
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/vnd.twkb
|
||||
|
||||
# binary output
|
||||
|
||||
Overriding a Builtin Handler
|
||||
============================
|
||||
|
||||
Let's override the existing ``text/csv`` handler for the table to provide a more complex CSV output.
|
||||
It'll include a `Byte order mark (BOM) <https://en.wikipedia.org/wiki/Byte_order_mark>`_ plus a ``Content-Disposition`` header to set a name for the downloaded file.
|
||||
|
||||
Create a domain for the standard ``text/csv`` media type.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "text/csv" as text;
|
||||
|
||||
And a transition function that returns the domain.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function bom_csv_trans (state text, next lines)
|
||||
returns "text/csv" as $$
|
||||
select state || next.id::text || ',' || next.name || ',' || next.geom::text || E'\n';
|
||||
$$ language sql;
|
||||
|
||||
This time we'll add a final function. This will add the CSV header, the BOM and the ``Content-Disposition`` header.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function bom_csv_final (data "text/csv")
|
||||
returns "text/csv" as $$
|
||||
-- set the Content-Disposition header
|
||||
select set_config('response.headers', '[{"Content-Disposition": "attachment; filename=\"lines.csv\""}]', true);
|
||||
select
|
||||
-- EFBBBF is the BOM in UTF8 https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
|
||||
convert_from (decode (E'EFBBBF', 'hex'),'UTF8') ||
|
||||
-- the header for the CSV
|
||||
(E'id,name,geom\n' || data);
|
||||
$$ language sql;
|
||||
|
||||
Now use the transition and final function as part of the new aggregate.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace aggregate bom_csv_agg (lines) (
|
||||
initcond = ''
|
||||
, stype = "text/csv"
|
||||
, sfunc = bom_csv_trans
|
||||
, finalfunc = bom_csv_final
|
||||
);
|
||||
|
||||
.. note::
|
||||
|
||||
You can test this with:
|
||||
|
||||
.. code-block:: psql
|
||||
|
||||
select bom_csv_agg(l) from lines l;
|
||||
bom_csv_agg
|
||||
-----------------------------------------------------------------------------------------------------
|
||||
id,name,geom +
|
||||
1,line-1,0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440+
|
||||
2,line-2,0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840+
|
||||
|
||||
(1 row)
|
||||
|
||||
And request it like:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/lines' -i \
|
||||
-H "Accept: text/csv"
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/csv
|
||||
Content-Disposition: attachment; filename="lines.csv"
|
||||
|
||||
id,name,geom
|
||||
1,line-1,0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440
|
||||
2,line-2,0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840
|
||||
|
||||
.. _any_handler:
|
||||
|
||||
The "Any" Handler
|
||||
=================
|
||||
|
||||
For more flexibility, you can also define a catch-all handler by using a domain named ``*/*`` (any media type). This handler obeys the following rules:
|
||||
|
||||
- It responds to all media types and even to requests that don't include an ``Accept`` header.
|
||||
- It sets the ``Content-Type`` header to ``application/octet-stream`` by default, but this can be overridden inside the function with :ref:`guc_resp_hdrs`.
|
||||
- It overrides all other handlers (:ref:`builtin <builtin_media>` or custom), so it's better to do it for an isolated function or view.
|
||||
|
||||
Let's define an any handler for a view that will always respond with ``XML`` output. It will accept ``text/xml``, ``application/xml``, ``*/*`` and reject other media types.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create domain "*/*" as bytea;
|
||||
|
||||
-- we'll use an .xml suffix for the view to be clear its output is always XML
|
||||
create view "lines.xml" as
|
||||
select * from lines;
|
||||
|
||||
-- transition function
|
||||
create or replace function lines_xml_trans (state "*/*", next "lines.xml")
|
||||
returns "*/*" as $$
|
||||
select state || xmlelement(name line, xmlattributes(next.id as id, next.name as name), next.geom)::text::bytea || E'\n' ;
|
||||
$$ language sql;
|
||||
|
||||
-- final function
|
||||
create or replace function lines_xml_final (data "*/*")
|
||||
returns "*/*" as $$
|
||||
declare
|
||||
-- get the Accept header
|
||||
req_accept text := current_setting('request.headers', true)::json->>'accept';
|
||||
begin
|
||||
-- when we need to override the default Content-Type (application/octet-stream) set by PostgREST
|
||||
if req_accept = '*/*' then
|
||||
perform set_config('response.headers', json_build_array(json_build_object('Content-Type', 'text/xml'))::text, true);
|
||||
elsif req_accept IN ('application/xml', 'text/xml') then
|
||||
perform set_config('response.headers', json_build_array(json_build_object('Content-Type', req_accept))::text, true);
|
||||
else
|
||||
-- we'll reject other non XML media types, we need to reject manually since */* will command PostgREST to accept all media types
|
||||
raise sqlstate 'PT415' using message = 'Unsupported Media Type';
|
||||
end if;
|
||||
|
||||
return data;
|
||||
end; $$ language plpgsql;
|
||||
|
||||
-- new aggregate
|
||||
create or replace aggregate lines_xml_agg ("lines.xml") (
|
||||
stype = "*/*"
|
||||
, sfunc = lines_xml_trans
|
||||
, finalfunc = lines_xml_final
|
||||
);
|
||||
|
||||
Test it on SQL:
|
||||
|
||||
.. code-block:: psql
|
||||
|
||||
select (encode(lines_xml_agg(x), 'escape'))::xml from "lines.xml" x;
|
||||
encode
|
||||
------------------------------------------------------------------------------------------------------------------------------
|
||||
<line id="1" name="line-1">0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440</line>+
|
||||
<line id="2" name="line-2">0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840</line>+
|
||||
|
||||
Now we can omit the ``Accept`` header and it will respond with XML.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/lines.xml' -i
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/xml
|
||||
|
||||
<line id="1" name="line-1">0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440</line>
|
||||
<line id="2" name="line-2">0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840</line>
|
||||
|
||||
And it will accept only XML media types.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/lines.xml' -i \
|
||||
-H "Accept: text/xml"
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/xml
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/lines.xml' -i \
|
||||
-H "Accept: application/xml"
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/xml
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl 'localhost:3000/lines.xml' -i \
|
||||
-H "Accept: unknown/media"
|
||||
|
||||
HTTP/1.1 415 Unsupported Media Type
|
||||
@@ -0,0 +1,98 @@
|
||||
.. _open-api:
|
||||
|
||||
OpenAPI
|
||||
=======
|
||||
|
||||
PostgREST automatically serves a full `OpenAPI <https://www.openapis.org/>`_ description on the root path. This provides a list of all endpoints (tables, foreign tables, views, functions), along with supported HTTP verbs and example payloads.
|
||||
|
||||
.. note::
|
||||
|
||||
By default, this output depends on the permissions of the role that is contained in the JWT role claim (or the :ref:`db-anon-role` if no JWT is sent). If you need to show all the endpoints disregarding the role's permissions, set the :ref:`openapi-mode` config to :code:`ignore-privileges`.
|
||||
|
||||
For extra customization, the OpenAPI output contains a "description" field for every `SQL comment <https://www.postgresql.org/docs/current/sql-comment.html>`_ on any database object. For instance,
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
COMMENT ON SCHEMA mammals IS
|
||||
'A warm-blooded vertebrate animal of a class that is distinguished by the secretion of milk by females for the nourishment of the young';
|
||||
|
||||
COMMENT ON TABLE monotremes IS
|
||||
'Freakish mammals lay the best eggs for breakfast';
|
||||
|
||||
COMMENT ON COLUMN monotremes.has_venomous_claw IS
|
||||
'Sometimes breakfast is not worth it';
|
||||
|
||||
These unsavory comments will appear in the generated JSON as the fields, ``info.description``, ``definitions.monotremes.description`` and ``definitions.monotremes.properties.has_venomous_claw.description``.
|
||||
|
||||
Also if you wish to generate a ``summary`` field you can do it by having a multiple line comment, the ``summary`` will be the first line and the ``description`` the lines that follow it:
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
COMMENT ON TABLE entities IS
|
||||
$$Entities summary
|
||||
|
||||
Entities description that
|
||||
spans
|
||||
multiple lines$$;
|
||||
|
||||
Similarly, you can override the API title by commenting the schema.
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
COMMENT ON SCHEMA api IS
|
||||
$$FooBar API
|
||||
|
||||
A RESTful API that serves FooBar data.$$;
|
||||
|
||||
If you need to include the ``security`` and ``securityDefinitions`` options, set the :ref:`openapi-security-active` configuration to ``true``.
|
||||
|
||||
You can use a tool like `Swagger UI <https://swagger.io/tools/swagger-ui/>`_ to create beautiful documentation from the description and to host an interactive web-based dashboard. The dashboard allows developers to make requests against a live PostgREST server, and provides guidance with request headers and example request bodies.
|
||||
|
||||
.. important::
|
||||
|
||||
The OpenAPI information can go out of date as the schema changes under a running server. See :ref:`schema_reloading`.
|
||||
|
||||
.. _override_openapi:
|
||||
|
||||
Overriding Full OpenAPI Response
|
||||
--------------------------------
|
||||
|
||||
You can override the whole default response with a function result. To do this, set the function on :ref:`db-root-spec`.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
db-root-spec = "root"
|
||||
|
||||
.. code:: postgres
|
||||
|
||||
create or replace function root() returns json as $_$
|
||||
declare
|
||||
openapi json = $$
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info":{
|
||||
"title":"Overridden",
|
||||
"description":"This is a my own API"
|
||||
}
|
||||
}
|
||||
$$;
|
||||
begin
|
||||
return openapi;
|
||||
end
|
||||
$_$ language plpgsql;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl http://localhost:3000
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info":{
|
||||
"title":"Overridden",
|
||||
"description":"This is a my own API"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
.. _options_requests:
|
||||
|
||||
OPTIONS method
|
||||
==============
|
||||
|
||||
You can verify which HTTP methods are allowed on endpoints for tables and views by using an OPTIONS request. These methods are allowed depending on what operations *can* be done on the table or view, not on the database permissions assigned to them.
|
||||
|
||||
For a table named ``people``, OPTIONS would show:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people" -X OPTIONS -i
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Allow: OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE
|
||||
|
||||
For a view, the methods are determined by the presence of INSTEAD OF TRIGGERS.
|
||||
|
||||
.. table::
|
||||
:widths: auto
|
||||
|
||||
+--------------------+-------------------------------------------------------------------------------------------------+
|
||||
| Method allowed | View's requirements |
|
||||
+====================+=================================================================================================+
|
||||
| OPTIONS, GET, HEAD | None (Always allowed) |
|
||||
+--------------------+-------------------------------------------------------------------------------------------------+
|
||||
| POST | INSTEAD OF INSERT TRIGGER |
|
||||
+--------------------+-------------------------------------------------------------------------------------------------+
|
||||
| PUT | INSTEAD OF INSERT TRIGGER, INSTEAD OF UPDATE TRIGGER, also requires the presence of a |
|
||||
| | primary key |
|
||||
+--------------------+-------------------------------------------------------------------------------------------------+
|
||||
| PATCH | INSTEAD OF UPDATE TRIGGER |
|
||||
+--------------------+-------------------------------------------------------------------------------------------------+
|
||||
| DELETE | INSTEAD OF DELETE TRIGGER |
|
||||
+--------------------+-------------------------------------------------------------------------------------------------+
|
||||
| All the above methods are allowed for |
|
||||
| `auto-updatable views <https://www.postgresql.org/docs/current/sql-createview.html#SQL-CREATEVIEW-UPDATABLE-VIEWS>`_ |
|
||||
+--------------------+-------------------------------------------------------------------------------------------------+
|
||||
|
||||
For functions, the methods depend on their volatility. ``VOLATILE`` functions allow only ``OPTIONS,POST``, whereas the rest also permit ``GET,HEAD``.
|
||||
|
||||
.. important::
|
||||
|
||||
Whenever you add or remove tables or views, or modify a view's INSTEAD OF TRIGGERS on the database, you must refresh PostgREST's schema cache for OPTIONS requests to work properly. See the section :ref:`schema_reloading`.
|
||||
@@ -0,0 +1,144 @@
|
||||
Pagination and Count
|
||||
####################
|
||||
|
||||
Pagination controls the number of rows returned for an :doc:`API resource <../api>` response. Combined with the count, you can traverse all the rows of a response.
|
||||
|
||||
.. _limits:
|
||||
|
||||
Limits and Pagination
|
||||
---------------------
|
||||
|
||||
PostgREST uses HTTP range headers to describe the size of results. Every response contains the current range and, if requested, the total number of results:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Range-Unit: items
|
||||
Content-Range: 0-14/*
|
||||
|
||||
Here items zero through fourteen are returned. This information is available in every response and can help you render pagination controls on the client. This is an RFC7233-compliant solution that keeps the response JSON cleaner.
|
||||
|
||||
Query Parameters
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
One way to request limits and offsets is by using query parameters. For example:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?limit=15&offset=30"
|
||||
|
||||
This method is also useful for embedded resources, which we will cover in another section. The server always responds with range headers even if you use query parameters to limit the query.
|
||||
|
||||
Range Header
|
||||
~~~~~~~~~~~~
|
||||
|
||||
You can use headers to specify the range of rows desired.
|
||||
This request gets the first twenty people:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people" -i \
|
||||
-H "Range-Unit: items" \
|
||||
-H "Range: 0-19"
|
||||
|
||||
Note that the server may respond with fewer if unable to meet your request:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Range-Unit: items
|
||||
Content-Range: 0-17/*
|
||||
|
||||
You may also request open-ended ranges for an offset with no limit, e.g. :code:`Range: 10-`.
|
||||
|
||||
.. _prefer_count:
|
||||
|
||||
Counting
|
||||
--------
|
||||
|
||||
In order to obtain the total size of the table (such as when rendering the last page link in a pagination control), you can specify a ``Prefer: count=<value>`` header. The values can be ``exact``, ``planned`` and ``estimated``.
|
||||
|
||||
This also works on views and :ref:`table_functions`.
|
||||
|
||||
|
||||
.. _exact_count:
|
||||
|
||||
Exact Count
|
||||
~~~~~~~~~~~
|
||||
|
||||
To get the exact count, use ``Prefer: count=exact``.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/bigtable" -I \
|
||||
-H "Range-Unit: items" \
|
||||
-H "Range: 0-24" \
|
||||
-H "Prefer: count=exact"
|
||||
|
||||
Note that the larger the table the slower this query runs in the database. The server will respond with the selected range and total
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 206 Partial Content
|
||||
Range-Unit: items
|
||||
Content-Range: 0-24/3573458
|
||||
|
||||
.. _planned_count:
|
||||
|
||||
Planned Count
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
To avoid the shortcomings of :ref:`exact count <exact_count>`, PostgREST can leverage PostgreSQL statistics and get a fairly accurate and fast count.
|
||||
To do this, specify the ``Prefer: count=planned`` header.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/bigtable?limit=25" -I \
|
||||
-H "Prefer: count=planned"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 206 Partial Content
|
||||
Content-Range: 0-24/3572000
|
||||
|
||||
Note that the accuracy of this count depends on how up-to-date are the PostgreSQL statistics tables.
|
||||
For example in this case, to increase the accuracy of the count you can do ``ANALYZE bigtable``.
|
||||
See `ANALYZE <https://www.postgresql.org/docs/current/sql-analyze.html>`_ for more details.
|
||||
|
||||
.. _estimated_count:
|
||||
|
||||
Estimated Count
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
When you are interested in the count, the relative error is important. If you have a :ref:`planned count <planned_count>` of 1000000 and the exact count is
|
||||
1001000, the error is small enough to be ignored. But with a planned count of 7, an exact count of 28 would be a huge misprediction.
|
||||
|
||||
In general, when having smaller row-counts, the estimated count should be as close to the exact count as possible.
|
||||
|
||||
To help with these cases, PostgREST can get the exact count up until a threshold and get the planned count when
|
||||
that threshold is surpassed. To use this behavior, you can specify the ``Prefer: count=estimated`` header. The **threshold** is
|
||||
defined by :ref:`db-max-rows`.
|
||||
|
||||
Here's an example. Suppose we set ``db-max-rows=1000`` and ``smalltable`` has 321 rows, then we'll get the exact count:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/smalltable?limit=25" -I \
|
||||
-H "Prefer: count=estimated"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 206 Partial Content
|
||||
Content-Range: 0-24/321
|
||||
|
||||
If we make a similar request on ``bigtable``, which has 3573458 rows, we would get the planned count:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/bigtable?limit=25" -I \
|
||||
-H "Prefer: count=estimated"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 206 Partial Content
|
||||
Content-Range: 0-24/3572000
|
||||
@@ -0,0 +1,254 @@
|
||||
.. _preferences:
|
||||
|
||||
Prefer Header
|
||||
#############
|
||||
|
||||
PostgREST honors the Prefer HTTP header specified on `RFC 7240 <https://www.rfc-editor.org/rfc/rfc7240.html>`_. It allows clients to specify required and optional behaviors for their requests.
|
||||
|
||||
The following preferences are supported.
|
||||
|
||||
- ``Prefer: handling``. See :ref:`prefer_handling`.
|
||||
- ``Prefer: timezone``. See :ref:`prefer_timezone`.
|
||||
- ``Prefer: return``. See :ref:`prefer_return`.
|
||||
- ``Prefer: count``. See :ref:`prefer_count`.
|
||||
- ``Prefer: resolution``. See :ref:`prefer_resolution`.
|
||||
- ``Prefer: missing``. See :ref:`bulk_insert_default`.
|
||||
- ``Prefer: max-affected``, See :ref:`prefer_max_affected`.
|
||||
- ``Prefer: tx``. See :ref:`prefer_tx`.
|
||||
- ``Prefer: params``. See :ref:`prefer_params`.
|
||||
|
||||
.. _prefer_handling:
|
||||
|
||||
Strict or Lenient Handling
|
||||
==========================
|
||||
|
||||
The server ignores unrecognized or unfulfillable preferences by default. You can control this behavior with the ``handling`` preference. It can take two values: ``lenient`` (the default) or ``strict``.
|
||||
|
||||
``handling=strict`` will throw an error if you specify invalid preferences. For instance:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/projects" \
|
||||
-H "Prefer: handling=strict, foo, bar"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"code": "PGRST122",
|
||||
"message": "Invalid preferences given with handling=strict",
|
||||
"details": "Invalid preferences: foo, bar",
|
||||
"hint": null
|
||||
}
|
||||
|
||||
|
||||
``handling=lenient`` ignores invalid preferences.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/projects" \
|
||||
-H "Prefer: handling=lenient, foo, bar"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
.. _prefer_timezone:
|
||||
|
||||
Timezone
|
||||
========
|
||||
|
||||
The ``timezone`` preference allows you to change the `PostgreSQL timezone <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-TIMEZONE>`_. It accepts all timezones in `pg_timezone_names <https://www.postgresql.org/docs/current/view-pg-timezone-names.html>`_.
|
||||
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/timestamps" \
|
||||
-H "Prefer: timezone=America/Los_Angeles"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Preference-Applied: timezone=America/Los_Angeles
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{"t":"2023-10-18T05:37:59.611-07:00"},
|
||||
{"t":"2023-10-18T07:37:59.611-07:00"},
|
||||
{"t":"2023-10-18T09:37:59.611-07:00"}
|
||||
]
|
||||
|
||||
For an invalid timezone, PostgREST returns values with the default timezone (configured on ``postgresql.conf`` or as a setting on the :ref:`authenticator <roles>`).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/timestamps" \
|
||||
-H "Prefer: timezone=Jupiter/Red_Spot"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{"t":"2023-10-18T12:37:59.611+00:00"},
|
||||
{"t":"2023-10-18T14:37:59.611+00:00"},
|
||||
{"t":"2023-10-18T16:37:59.611+00:00"}
|
||||
]
|
||||
|
||||
Note that there's no ``Preference-Applied`` in the response.
|
||||
|
||||
However, with ``handling=strict``, an invalid timezone preference will throw an :ref:`error <pgrst122>`.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/timestamps" \
|
||||
-H "Prefer: handling=strict, timezone=Jupiter/Red_Spot"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
|
||||
.. _prefer_return:
|
||||
|
||||
Return Representation
|
||||
=====================
|
||||
|
||||
The ``return`` preference can be used to obtain information about affected resource when it's :ref:`inserted <insert>`, :ref:`updated <update>` or :ref:`deleted <delete>`.
|
||||
This helps avoid a subsequent GET request.
|
||||
|
||||
Minimal
|
||||
-------
|
||||
|
||||
With ``Prefer: return=minimal``, no response body will be returned. This is the default mode for all write requests.
|
||||
|
||||
Headers Only
|
||||
------------
|
||||
|
||||
If the table has a primary key, the response can contain a :code:`Location` header describing where to find the new object by including the header :code:`Prefer: return=headers-only` in the request. Make sure that the table is not write-only, otherwise constructing the :code:`Location` header will cause a permissions error.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/projects" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: return=headers-only" \
|
||||
-d '{"id":33, "name": "x"}'
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Location: /projects?id=eq.34
|
||||
Preference-Applied: return=headers-only
|
||||
|
||||
Full
|
||||
----
|
||||
|
||||
On the other end of the spectrum you can get the full created object back in the response to your request by including the header :code:`Prefer: return=representation`. That way you won't have to make another HTTP call to discover properties that may have been filled in on the server side. You can also apply the standard :ref:`v_filter` to these results.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/projects" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: return=representation" \
|
||||
-d '{"id":33, "name": "x"}'
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
Preference-Applied: return=representation
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"id": 33,
|
||||
"name": "x"
|
||||
}
|
||||
]
|
||||
|
||||
.. _prefer_tx:
|
||||
|
||||
Transaction End Preference
|
||||
==========================
|
||||
|
||||
The ``tx`` preference can be set to specify if the :ref:`transaction <transactions>` will end in a COMMIT or ROLLBACK. This preference is not enabled by default but can be activated with :ref:`db-tx-end`.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/projects" -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: tx=rollback, return=representation" \
|
||||
-d '{"name": "Project X"}'
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Preference-Applied: tx=rollback, return=representation
|
||||
|
||||
{"id": 35, "name": "Project X"}
|
||||
|
||||
|
||||
.. _prefer_max_affected:
|
||||
|
||||
Max Affected
|
||||
============
|
||||
|
||||
You can set a limit to the amount of resources affected in a request by sending ``max-affected`` preference. This feature works in combination with ``handling=strict`` preference. ``max-affected`` would be ignored with lenient handling. The "affected resources" are the number of rows returned by ``DELETE`` and ``PATCH`` requests. This is also supported through ``RPC`` calls.
|
||||
|
||||
To illustrate the use of this preference, consider the following scenario where the ``items`` table contains 14 rows.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -i "http://localhost:3000/items?id=lt.15 -X DELETE \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: handling=strict, max-affected=10"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"code": "PGRST124",
|
||||
"message": "Query result exceeds max-affected preference constraint",
|
||||
"details": "The query affects 14 rows",
|
||||
"hint": null
|
||||
}
|
||||
|
||||
.. _prefer_params:
|
||||
|
||||
Single JSON object as Function Parameter
|
||||
----------------------------------------
|
||||
|
||||
.. warning::
|
||||
|
||||
Using this preference is **deprecated** in favor of :ref:`s_proc_single_json`.
|
||||
|
||||
:code:`Prefer: params=single-object` allows sending the JSON request body as the single argument of a :ref:`function <s_procs>`.
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
CREATE FUNCTION mult_them(param json) RETURNS int AS $$
|
||||
SELECT (param->>'x')::int * (param->>'y')::int
|
||||
$$ LANGUAGE SQL;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/mult_them" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-H "Prefer: params=single-object" \
|
||||
-d '{ "x": 4, "y": 2 }'
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
8
|
||||
@@ -0,0 +1,147 @@
|
||||
Resource Representation
|
||||
#######################
|
||||
|
||||
PostgREST uses proper HTTP content negotiation (`RFC7231 <https://datatracker.ietf.org/doc/html/rfc7231#section-5.3>`_) to deliver a resource representation.
|
||||
That is to say the same API endpoint can respond in different formats like JSON or CSV depending on the request.
|
||||
|
||||
.. _res_format:
|
||||
|
||||
Response Format
|
||||
===============
|
||||
|
||||
Use the Accept request header to specify the acceptable format (or formats) for the response:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people" \
|
||||
-H "Accept: application/json"
|
||||
|
||||
.. _builtin_media:
|
||||
|
||||
Builtin Media Type Handlers
|
||||
===========================
|
||||
|
||||
Builtin handlers are offered for common standard media types.
|
||||
|
||||
* ``text/csv`` and ``application/json``, for all API endpoints. See :ref:`tables_views` and :ref:`s_procs`.
|
||||
* ``application/openapi+json``, for the root endpoint. See :ref:`open-api`.
|
||||
* ``application/geo+json``, see :ref:`ww_postgis`.
|
||||
* ``*/*``, resolves to ``application/json`` for API endpoints and to ``application/openapi+json`` for the root endpoint.
|
||||
|
||||
The following vendor media types handlers are also supported.
|
||||
|
||||
* ``application/vnd.pgrst.plan``, see :ref:`explain_plan`.
|
||||
* ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``, see :ref:`singular_plural` and :ref:`stripped_nulls`.
|
||||
|
||||
Any unrecognized media type will throw an error.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people" \
|
||||
-H "Accept: unknown/unknown"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 415 Unsupported Media Type
|
||||
|
||||
{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: unknown/unknown"}
|
||||
|
||||
To extend the accepted media types, you can use :ref:`custom_media`.
|
||||
|
||||
.. _singular_plural:
|
||||
|
||||
Singular or Plural
|
||||
------------------
|
||||
|
||||
By default PostgREST returns all JSON results in an array, even when there is only one item. For example, requesting :code:`/items?id=eq.1` returns
|
||||
|
||||
.. code:: json
|
||||
|
||||
[
|
||||
{ "id": 1 }
|
||||
]
|
||||
|
||||
This can be inconvenient for client code. To return the first result as an object unenclosed by an array, specify :code:`vnd.pgrst.object` as part of the :code:`Accept` header
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/items?id=eq.1" \
|
||||
-H "Accept: application/vnd.pgrst.object+json"
|
||||
|
||||
This returns
|
||||
|
||||
.. code:: json
|
||||
|
||||
{ "id": 1 }
|
||||
|
||||
with a :code:`Content-Type: application/vnd.pgrst.object+json`.
|
||||
|
||||
When a singular response is requested but no entries are found, the server responds with an error message and 406 Not Acceptable status code rather than the usual empty array and 200 status:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"message": "JSON object requested, multiple (or no) rows returned",
|
||||
"details": "Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row",
|
||||
"hint": null,
|
||||
"code": "PGRST505"
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
Many APIs distinguish plural and singular resources using a special nested URL convention e.g. `/stories` vs `/stories/1`. Why do we use `/stories?id=eq.1`? The answer is because a singular resource is (for us) a row determined by a primary key, and primary keys can be compound (meaning defined across more than one column). The more familiar nested urls consider only a degenerate case of simple and overwhelmingly numeric primary keys. These so-called artificial keys are often introduced automatically by Object Relational Mapping libraries.
|
||||
|
||||
Admittedly PostgREST could detect when there is an equality condition holding on all columns constituting the primary key and automatically convert to singular. However this could lead to a surprising change of format that breaks unwary client code just by filtering on an extra column. Instead we allow manually specifying singular vs plural to decouple that choice from the URL format.
|
||||
|
||||
.. _stripped_nulls:
|
||||
|
||||
Stripped Nulls
|
||||
--------------
|
||||
|
||||
By default PostgREST returns all JSON null values. For example, requesting ``/projects?id=gt.10`` returns
|
||||
|
||||
.. code:: json
|
||||
|
||||
[
|
||||
{ "id": 11, "name": "OSX", "client_id": 1, "another_col": "val" },
|
||||
{ "id": 12, "name": "ProjectX", "client_id": null, "another_col": null },
|
||||
{ "id": 13, "name": "Y", "client_id": null, "another_col": null }
|
||||
]
|
||||
|
||||
On large result sets, the unused keys with ``null`` values can waste bandwith unnecessarily. To remove them, specify ``nulls=stripped`` as a parameter of ``application/vnd.pgrst.array``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/projects?id=gt.10" \
|
||||
-H "Accept: application/vnd.pgrst.array+json;nulls=stripped"
|
||||
|
||||
This returns
|
||||
|
||||
.. code:: json
|
||||
|
||||
[
|
||||
{ "id": 11, "name": "OSX", "client_id": 1, "another_col": "val" },
|
||||
{ "id": 12, "name": "ProjectX" },
|
||||
{ "id": 13, "name": "Y"}
|
||||
]
|
||||
|
||||
.. _req_body:
|
||||
|
||||
Request Body
|
||||
============
|
||||
|
||||
The server handles the following request body media types:
|
||||
|
||||
* ``application/json``
|
||||
* ``application/x-www-form-urlencoded``
|
||||
* ``text/csv``
|
||||
|
||||
For :ref:`tables_views` this works on ``POST``, ``PATCH`` and ``PUT`` methods. For :ref:`s_procs`, it works on ``POST`` methods.
|
||||
|
||||
For stored procedures there are three additional types:
|
||||
|
||||
* ``application/octet-stream``
|
||||
* ``text/plain``
|
||||
* ``text/xml``
|
||||
|
||||
See :ref:`s_proc_single_unnamed`.
|
||||
@@ -0,0 +1,131 @@
|
||||
.. _schemas:
|
||||
|
||||
Schemas
|
||||
=======
|
||||
|
||||
PostgREST can expose a single or multiple schema's tables, views and functions. The :ref:`active database role <roles>` must have the usage privilege on the schemas to access them.
|
||||
|
||||
Single schema
|
||||
-------------
|
||||
|
||||
To expose a single schema, specify a single value in :ref:`db-schemas`.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
db-schemas = "api"
|
||||
|
||||
This schema is added to the `search_path <https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH>`_ of every request using :ref:`tx_settings`.
|
||||
|
||||
.. _multiple-schemas:
|
||||
|
||||
Multiple schemas
|
||||
----------------
|
||||
|
||||
To expose multiple schemas, specify a comma-separated list on :ref:`db-schemas`:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
db-schemas = "tenant1, tenant2"
|
||||
|
||||
To switch schemas, use the ``Accept-Profile`` and ``Content-Profile`` headers.
|
||||
|
||||
If you don't specify a Profile header, the first schema in the list(``tenant1`` here) is selected as the default schema.
|
||||
|
||||
Only the selected schema gets added to the `search_path <https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH>`_ of every request.
|
||||
|
||||
.. note::
|
||||
|
||||
These headers are based on the "Content Negotiation by Profile" spec: https://www.w3.org/TR/dx-prof-conneg
|
||||
|
||||
GET/HEAD
|
||||
~~~~~~~~
|
||||
|
||||
For GET or HEAD, select the schema with ``Accept-Profile``.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/items" \
|
||||
-H "Accept-Profile: tenant2"
|
||||
|
||||
Other methods
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
For POST, PATCH, PUT and DELETE, select the schema with ``Content-Profile``.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/items" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-H "Content-Profile: tenant2" \
|
||||
-d '{...}'
|
||||
|
||||
You can also select the schema for :ref:`s_procs` and :ref:`open-api`.
|
||||
|
||||
Restricted schemas
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can only switch to a schema included in :ref:`db-schemas`. Using another schema will result in an error:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/items" \
|
||||
-H "Accept-Profile: tenant3"
|
||||
|
||||
.. code-block::
|
||||
|
||||
{
|
||||
"code":"PGRST106",
|
||||
"details":null,
|
||||
"hint":null,
|
||||
"message":"The schema must be one of the following: tenant1, tenant2"
|
||||
}
|
||||
|
||||
|
||||
Dynamic schemas
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
To add schemas dynamically, you can use :ref:`in_db_config` plus :ref:`config reloading <config_reloading_notify>` and :ref:`schema cache reloading <schema_reloading_notify>`. Here are some options for how to do this:
|
||||
|
||||
- If the schemas' names have a pattern, like a ``tenant_`` prefix, do:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
create or replace function postgrest.pre_config()
|
||||
returns void as $$
|
||||
select
|
||||
set_config('pgrst.db_schemas', string_agg(nspname, ','), true)
|
||||
from pg_namespace
|
||||
where nspname like 'tenant_%';
|
||||
$$ language sql;
|
||||
|
||||
- If there's no name pattern but they're created with a particular role (``CREATE SCHEMA mine AUTHORIZATION joe``), do:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
create or replace function postgrest.pre_config()
|
||||
returns void as $$
|
||||
select
|
||||
set_config('pgrst.db_schemas', string_agg(nspname, ','), true)
|
||||
from pg_namespace
|
||||
where nspowner = 'joe'::regrole;
|
||||
$$ language sql;
|
||||
|
||||
- Otherwise, you might need to create a table that stores the allowed schemas.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
create table postgrest.config (schemas text);
|
||||
|
||||
create or replace function postgrest.pre_config()
|
||||
returns void as $$
|
||||
select
|
||||
set_config('pgrst.db_schemas', schemas, true)
|
||||
from postgrest.config;
|
||||
$$ language sql;
|
||||
|
||||
Then each time you add an schema, do:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
NOTIFY pgrst, 'reload config';
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
@@ -0,0 +1,340 @@
|
||||
.. _s_procs:
|
||||
|
||||
Stored Procedures
|
||||
=================
|
||||
|
||||
*"A single resource can be the equivalent of a database stored procedure, with the power to abstract state changes over any number of storage items"* -- `Roy T. Fielding <http://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven#comment-743>`_
|
||||
|
||||
Procedures can perform any operations allowed by PostgreSQL (read data, modify data, :ref:`raise errors <raise_error>`, and even DDL operations). Every stored procedure in the :ref:`exposed schema <schemas>` and accessible by the :ref:`active database role <roles>` is executable under the :code:`/rpc` prefix.
|
||||
|
||||
If they return table types, Stored Procedures can:
|
||||
|
||||
- Use all the same :ref:`read filters as Tables and Views <read>` (horizontal/vertical filtering, counts, limits, etc.).
|
||||
- Use :ref:`Resource Embedding <s_proc_embed>`, if the returned table type has relationships to other tables.
|
||||
|
||||
.. note::
|
||||
|
||||
Why the ``/rpc`` prefix? PostgreSQL allows a table or view to have the same name as a function. The prefix allows us to avoid routes collisions.
|
||||
|
||||
Calling with POST
|
||||
-----------------
|
||||
|
||||
To supply arguments in an API call, include a JSON object in the request payload. Each key/value of the object will become an argument.
|
||||
|
||||
For instance, assume we have created this function in the database.
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
CREATE FUNCTION add_them(a integer, b integer)
|
||||
RETURNS integer AS $$
|
||||
SELECT a + b;
|
||||
$$ LANGUAGE SQL IMMUTABLE;
|
||||
|
||||
.. important::
|
||||
|
||||
Whenever you create or change a function you must refresh PostgREST's schema cache. See the section :ref:`schema_reloading`.
|
||||
|
||||
The client can call it by posting an object like
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/add_them" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '{ "a": 1, "b": 2 }'
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
3
|
||||
|
||||
.. note::
|
||||
|
||||
PostgreSQL converts identifier names to lowercase unless you quote them like:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE FUNCTION "someFunc"("someParam" text) ...
|
||||
|
||||
Calling with GET
|
||||
----------------
|
||||
|
||||
If the function doesn't modify the database, it will also run under the GET method (see :ref:`access_mode`).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/add_them?a=1&b=2"
|
||||
|
||||
The function parameter names match the JSON object keys in the POST case, for the GET case they match the query parameters ``?a=1&b=2``.
|
||||
|
||||
.. _s_proc_single_json:
|
||||
|
||||
Functions with a single unnamed JSON parameter
|
||||
----------------------------------------------
|
||||
|
||||
If you want the JSON request body to be sent as a single argument, you can create a function with a single unnamed ``json`` or ``jsonb`` parameter.
|
||||
For this the ``Content-Type: application/json`` header must be included in the request.
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
CREATE FUNCTION mult_them(json) RETURNS int AS $$
|
||||
SELECT ($1->>'x')::int * ($1->>'y')::int
|
||||
$$ LANGUAGE SQL;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/mult_them" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '{ "x": 4, "y": 2 }'
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
8
|
||||
|
||||
.. note::
|
||||
|
||||
If an overloaded function has a single ``json`` or ``jsonb`` unnamed parameter, PostgREST will call this function as a fallback provided that no other overloaded function is found with the parameters sent in the POST request.
|
||||
|
||||
.. warning::
|
||||
|
||||
Sending the JSON request body as a single argument is also possible with :ref:`Prefer: params=single-object <prefer_params>` but this method is **deprecated**.
|
||||
|
||||
.. _s_proc_single_unnamed:
|
||||
|
||||
Functions with a single unnamed parameter
|
||||
-----------------------------------------
|
||||
|
||||
You can make a POST request to a function with a single unnamed parameter to send raw ``bytea``, ``text`` or ``xml`` data.
|
||||
|
||||
To send raw XML, the parameter type must be ``xml`` and the header ``Content-Type: text/xml`` must be included in the request.
|
||||
|
||||
To send raw binary, the parameter type must be ``bytea`` and the header ``Content-Type: application/octet-stream`` must be included in the request.
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
CREATE TABLE files(blob bytea);
|
||||
|
||||
CREATE FUNCTION upload_binary(bytea) RETURNS void AS $$
|
||||
INSERT INTO files(blob) VALUES ($1);
|
||||
$$ LANGUAGE SQL;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/upload_binary" \
|
||||
-X POST -H "Content-Type: application/octet-stream" \
|
||||
--data-binary "@file_name.ext"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
|
||||
[ ... ]
|
||||
|
||||
To send raw text, the parameter type must be ``text`` and the header ``Content-Type: text/plain`` must be included in the request.
|
||||
|
||||
.. _s_procs_array:
|
||||
|
||||
Functions with array parameters
|
||||
-------------------------------
|
||||
|
||||
You can call a function that takes an array parameter:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create function plus_one(arr int[]) returns int[] as $$
|
||||
SELECT array_agg(n + 1) FROM unnest($1) AS n;
|
||||
$$ language sql;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/plus_one" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '{"arr": [1,2,3,4]}'
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[2,3,4,5]
|
||||
|
||||
For calling the function with GET, you can pass the array as an `array literal <https://www.postgresql.org/docs/current/arrays.html#ARRAYS-INPUT>`_,
|
||||
as in ``{1,2,3,4}``. Note that the curly brackets have to be urlencoded(``{`` is ``%7B`` and ``}`` is ``%7D``).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/plus_one?arr=%7B1,2,3,4%7D'"
|
||||
|
||||
.. note::
|
||||
|
||||
For versions prior to PostgreSQL 10, to pass a PostgreSQL native array on a POST payload, you need to quote it and use an array literal:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/plus_one" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '{ "arr": "{1,2,3,4}" }'
|
||||
|
||||
In these versions we recommend using function parameters of type JSON to accept arrays from the client.
|
||||
|
||||
.. _s_procs_variadic:
|
||||
|
||||
Variadic functions
|
||||
------------------
|
||||
|
||||
You can call a variadic function by passing a JSON array in a POST request:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create function plus_one(variadic v int[]) returns int[] as $$
|
||||
SELECT array_agg(n + 1) FROM unnest($1) AS n;
|
||||
$$ language sql;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/plus_one" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '{"v": [1,2,3,4]}'
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[2,3,4,5]
|
||||
|
||||
In a GET request, you can repeat the same parameter name:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/plus_one?v=1&v=2&v=3&v=4"
|
||||
|
||||
Repeating also works in POST requests with ``Content-Type: application/x-www-form-urlencoded``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/plus_one" \
|
||||
-X POST -H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d 'v=1&v=2&v=3&v=4'
|
||||
|
||||
.. _table_functions:
|
||||
|
||||
Table-Valued Functions
|
||||
----------------------
|
||||
|
||||
A function that returns a table type can be filtered using the same filters as :ref:`tables and views <tables_views>`. They can also use :ref:`Resource Embedding <s_proc_embed>`.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE FUNCTION best_films_2017() RETURNS SETOF films ..
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/best_films_2017?select=title,director:directors(*)"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/best_films_2017?rating=gt.8&order=title.desc"
|
||||
|
||||
.. _function_inlining:
|
||||
|
||||
Function Inlining
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
A function that follows the `rules for inlining <https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions#Inlining_conditions_for_table_functions>`_ will also inline :ref:`filters <h_filter>`, :ref:`order <ordering>` and :ref:`limits <limits>`.
|
||||
|
||||
For example, for the following function:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create function getallprojects() returns setof projects
|
||||
language sql stable
|
||||
as $$
|
||||
select * from projects;
|
||||
$$;
|
||||
|
||||
Let's get its :ref:`explain_plan` when calling it with filters applied:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/getallprojects?id=eq.1" \
|
||||
-H "Accept: application/vnd.pgrst.plan"
|
||||
|
||||
.. code-block:: psql
|
||||
|
||||
Aggregate (cost=8.18..8.20 rows=1 width=112)
|
||||
-> Index Scan using projects_pkey on projects (cost=0.15..8.17 rows=1 width=40)
|
||||
Index Cond: (id = 1)
|
||||
|
||||
Notice there's no "Function Scan" node in the plan, which tells us it has been inlined.
|
||||
|
||||
.. _scalar_functions:
|
||||
|
||||
Scalar functions
|
||||
----------------
|
||||
|
||||
PostgREST will detect if the function is scalar or table-valued and will shape the response format accordingly:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/add_them?a=1&b=2"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
3
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/best_films_2017"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{ "title": "Okja", "rating": 7.4},
|
||||
{ "title": "Call me by your name", "rating": 8},
|
||||
{ "title": "Blade Runner 2049", "rating": 8.1}
|
||||
]
|
||||
|
||||
To manually choose a return format such as binary, see :ref:`custom_media`.
|
||||
|
||||
.. _untyped_functions:
|
||||
|
||||
Untyped functions
|
||||
-----------------
|
||||
|
||||
Functions that return ``record`` or ``SETOF record`` are supported:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create function projects_setof_record() returns setof record as $$
|
||||
select * from projects;
|
||||
$$ language sql;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/projects_setof_record"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[{"id":1,"name":"Windows 7","client_id":1},
|
||||
{"id":2,"name":"Windows 10","client_id":1},
|
||||
{"id":3,"name":"IOS","client_id":2}]
|
||||
|
||||
However note that they will fail when trying to use :ref:`v_filter` and :ref:`h_filter` on them.
|
||||
|
||||
So while they can be used for quick tests, it's recommended to always choose a strict return type for the function.
|
||||
|
||||
Overloaded functions
|
||||
--------------------
|
||||
|
||||
You can call overloaded functions with different number of arguments.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE FUNCTION rental_duration(customer_id integer) ..
|
||||
|
||||
CREATE FUNCTION rental_duration(customer_id integer, from_date date) ..
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/rental_duration?customer_id=232"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/rental_duration?customer_id=232&from_date=2018-07-01"
|
||||
|
||||
.. important::
|
||||
|
||||
Overloaded functions with the same argument names but different types are not supported.
|
||||
@@ -0,0 +1,689 @@
|
||||
.. _tables_views:
|
||||
|
||||
Tables and Views
|
||||
################
|
||||
|
||||
All views and tables of the :ref:`exposed schema <schemas>` and accessible by the :ref:`active database role <roles>` are available for querying. They are exposed in one-level deep routes.
|
||||
|
||||
.. _read:
|
||||
|
||||
Read
|
||||
====
|
||||
|
||||
For instance the full contents of a table `people` is returned at
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people"
|
||||
|
||||
There are no deeply/nested/routes. Each route provides OPTIONS, GET, HEAD, POST, PATCH, and DELETE verbs depending entirely on database permissions.
|
||||
|
||||
.. note::
|
||||
|
||||
Why not provide nested routes? Many APIs allow nesting to retrieve related information, such as :code:`/films/1/director`. We offer a more flexible mechanism (inspired by GraphQL) to embed related information. It can handle one-to-many and many-to-many relationships. This is covered in the section about :ref:`resource_embedding`.
|
||||
|
||||
|
||||
.. _h_filter:
|
||||
|
||||
Horizontal Filtering
|
||||
--------------------
|
||||
|
||||
You can filter result rows by adding conditions on columns. For instance, to return people aged under 13 years old:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?age=lt.13"
|
||||
|
||||
You can evaluate multiple conditions on columns by adding more query string parameters. For instance, to return people who are 18 or older **and** are students:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?age=gte.18&student=is.true"
|
||||
|
||||
.. _operators:
|
||||
|
||||
Operators
|
||||
~~~~~~~~~
|
||||
|
||||
These operators are available:
|
||||
|
||||
============ ======================== ==================================================================================
|
||||
Abbreviation In PostgreSQL Meaning
|
||||
============ ======================== ==================================================================================
|
||||
eq :code:`=` equals
|
||||
gt :code:`>` greater than
|
||||
gte :code:`>=` greater than or equal
|
||||
lt :code:`<` less than
|
||||
lte :code:`<=` less than or equal
|
||||
neq :code:`<>` or :code:`!=` not equal
|
||||
like :code:`LIKE` LIKE operator (to avoid `URL encoding <https://en.wikipedia.org/wiki/Percent-encoding>`_ you can use ``*`` as an alias of the percent sign ``%`` for the pattern)
|
||||
ilike :code:`ILIKE` ILIKE operator (to avoid `URL encoding <https://en.wikipedia.org/wiki/Percent-encoding>`_ you can use ``*`` as an alias of the percent sign ``%`` for the pattern)
|
||||
match :code:`~` ~ operator, see :ref:`pattern_matching`
|
||||
imatch :code:`~*` ~* operator, see :ref:`pattern_matching`
|
||||
in :code:`IN` one of a list of values, e.g. :code:`?a=in.(1,2,3)`
|
||||
– also supports commas in quoted strings like
|
||||
:code:`?a=in.("hi,there","yes,you")`
|
||||
is :code:`IS` checking for exact equality (null,true,false,unknown)
|
||||
isdistinct :code:`IS DISTINCT FROM` not equal, treating :code:`NULL` as a comparable value
|
||||
fts :code:`@@` :ref:`fts` using to_tsquery
|
||||
plfts :code:`@@` :ref:`fts` using plainto_tsquery
|
||||
phfts :code:`@@` :ref:`fts` using phraseto_tsquery
|
||||
wfts :code:`@@` :ref:`fts` using websearch_to_tsquery
|
||||
cs :code:`@>` contains e.g. :code:`?tags=cs.{example, new}`
|
||||
cd :code:`<@` contained in e.g. :code:`?values=cd.{1,2,3}`
|
||||
ov :code:`&&` overlap (have points in common), e.g. :code:`?period=ov.[2017-01-01,2017-06-30]` –
|
||||
also supports array types, use curly braces instead of square brackets e.g.
|
||||
:code: `?arr=ov.{1,3}`
|
||||
sl :code:`<<` strictly left of, e.g. :code:`?range=sl.(1,10)`
|
||||
sr :code:`>>` strictly right of
|
||||
nxr :code:`&<` does not extend to the right of, e.g. :code:`?range=nxr.(1,10)`
|
||||
nxl :code:`&>` does not extend to the left of
|
||||
adj :code:`-|-` is adjacent to, e.g. :code:`?range=adj.(1,10)`
|
||||
not :code:`NOT` negates another operator, see :ref:`logical_operators`
|
||||
or :code:`OR` logical :code:`OR`, see :ref:`logical_operators`
|
||||
and :code:`AND` logical :code:`AND`, see :ref:`logical_operators`
|
||||
all :code:`ALL` comparison matches all the values in the list, see :ref:`modifiers`
|
||||
any :code:`ANY` comparison matches any value in the list, see :ref:`modifiers`
|
||||
============ ======================== ==================================================================================
|
||||
|
||||
For more complicated filters you will have to create a new view in the database, or use a stored procedure. For instance, here's a view to show "today's stories" including possibly older pinned stories:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
CREATE VIEW fresh_stories AS
|
||||
SELECT *
|
||||
FROM stories
|
||||
WHERE pinned = true
|
||||
OR published > now() - interval '1 day'
|
||||
ORDER BY pinned DESC, published DESC;
|
||||
|
||||
The view will provide a new endpoint:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/fresh_stories"
|
||||
|
||||
.. _logical_operators:
|
||||
|
||||
Logical operators
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
Multiple conditions on columns are evaluated using ``AND`` by default, but you can combine them using ``OR`` with the ``or`` operator. For example, to return people under 18 **or** over 21:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?or=(age.lt.18,age.gt.21)"
|
||||
|
||||
To **negate** any operator, you can prefix it with :code:`not` like :code:`?a=not.eq.2` or :code:`?not.and=(a.gte.0,a.lte.100)` .
|
||||
|
||||
You can also apply complex logic to the conditions:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?grade=gte.90&student=is.true&or=(age.eq.14,not.and(age.gte.11,age.lte.17))"
|
||||
|
||||
.. _modifiers:
|
||||
|
||||
Operator Modifiers
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You may further simplify the logic using the ``any/all`` modifiers of ``eq,like,ilike,gt,gte,lt,lte,match,imatch``.
|
||||
|
||||
For instance, to avoid repeating the same column for ``or``, use ``any`` to get people with last names that start with O or P:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -g "http://localhost:3000/people?last_name=like(any).{O*,P*}"
|
||||
|
||||
In a similar way, you can use ``all`` to avoid repeating the same column for ``and``. To get the people with last names that start with O and end with n:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -g "http://localhost:3000/people?last_name=like(all).{O*,*n}"
|
||||
|
||||
.. _pattern_matching:
|
||||
|
||||
Pattern Matching
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The pattern-matching operators (:code:`like`, :code:`ilike`, :code:`match`, :code:`imatch`) exist to support filtering data using patterns instead of concrete strings, as described in the `PostgreSQL docs <https://www.postgresql.org/docs/current/functions-matching.html>`__.
|
||||
|
||||
To ensure best performance on larger data sets, an `appropriate index <https://www.postgresql.org/docs/current/pgtrgm.html#PGTRGM-INDEX>`__ should be used and even then, it depends on the pattern value and actual data statistics whether an existing index will be used by the query planner or not.
|
||||
|
||||
.. _fts:
|
||||
|
||||
Full-Text Search
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
The :code:`fts` filter mentioned above has a number of options to support flexible textual queries, namely the choice of plain vs phrase search and the language used for stemming. Suppose that :code:`tsearch` is a table with column :code:`my_tsv`, of type `tsvector <https://www.postgresql.org/docs/current/datatype-textsearch.html>`_. The following examples illustrate the possibilities.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_tsv=fts(french).amusant"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_tsv=plfts.The%20Fat%20Cats"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/tsearch?my_tsv=not.wfts(french).amusant"
|
||||
|
||||
Using `websearch_to_tsquery` requires PostgreSQL of version at least 11.0 and will raise an error in earlier versions of the database.
|
||||
|
||||
.. _v_filter:
|
||||
|
||||
Vertical Filtering
|
||||
------------------
|
||||
|
||||
When certain columns are wide (such as those holding binary data), it is more efficient for the server to withhold them in a response. The client can specify which columns are required using the :code:`select` parameter.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?select=first_name,age"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{"first_name": "John", "age": 30},
|
||||
{"first_name": "Jane", "age": 20}
|
||||
]
|
||||
|
||||
The default is ``*``, meaning all columns. This value will become more important below in :ref:`resource_embedding`.
|
||||
|
||||
.. _renaming_columns:
|
||||
|
||||
Renaming Columns
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
You can rename the columns by prefixing them with an alias followed by the colon ``:`` operator.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?select=fullName:full_name,birthDate:birth_date"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{"fullName": "John Doe", "birthDate": "04/25/1988"},
|
||||
{"fullName": "Jane Doe", "birthDate": "01/12/1998"}
|
||||
]
|
||||
|
||||
.. _casting_columns:
|
||||
|
||||
Casting Columns
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Casting the columns is possible by suffixing them with the double colon ``::`` plus the desired type.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?select=full_name,salary::text"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{"full_name": "John Doe", "salary": "90000.00"},
|
||||
{"full_name": "Jane Doe", "salary": "120000.00"}
|
||||
]
|
||||
|
||||
.. _json_columns:
|
||||
|
||||
JSON Columns
|
||||
------------
|
||||
|
||||
You can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs <https://www.postgresql.org/docs/current/functions-json.html>`__.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE TABLE people (
|
||||
id int,
|
||||
json_data json
|
||||
);
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?select=id,json_data->>blood_type,json_data->phones"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{ "id": 1, "blood_type": "A-", "phones": [{"country_code": "61", "number": "917-929-5745"}] },
|
||||
{ "id": 2, "blood_type": "O+", "phones": [{"country_code": "43", "number": "512-446-4988"}, {"country_code": "43", "number": "213-891-5979"}] }
|
||||
]
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?select=id,json_data->phones->0->>number"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{ "id": 1, "number": "917-929-5745"},
|
||||
{ "id": 2, "number": "512-446-4988"}
|
||||
]
|
||||
|
||||
This also works with filters:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?select=id,json_data->blood_type&json_data->>blood_type=eq.A-"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{ "id": 1, "blood_type": "A-" },
|
||||
{ "id": 3, "blood_type": "A-" },
|
||||
{ "id": 7, "blood_type": "A-" }
|
||||
]
|
||||
|
||||
Note that ``->>`` is used to compare ``blood_type`` as ``text``. To compare with an integer value use ``->``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?select=id,json_data->age&json_data->age=gt.20"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{ "id": 11, "age": 25 },
|
||||
{ "id": 12, "age": 30 },
|
||||
{ "id": 15, "age": 35 }
|
||||
]
|
||||
.. _composite_array_columns:
|
||||
|
||||
Composite / Array Columns
|
||||
-------------------------
|
||||
|
||||
The arrow operators(``->``, ``->>``) can also be used for accessing composite fields and array elements.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE TYPE coordinates (
|
||||
lat decimal(8,6),
|
||||
long decimal(9,6)
|
||||
);
|
||||
|
||||
CREATE TABLE countries (
|
||||
id int,
|
||||
location coordinates,
|
||||
languages text[]
|
||||
);
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/countries?select=id,location->>lat,location->>long,primary_language:languages->0&location->lat=gte.19"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"id": 5,
|
||||
"lat": "19.741755",
|
||||
"long": "-155.844437",
|
||||
"primary_language": "en"
|
||||
}
|
||||
]
|
||||
|
||||
.. important::
|
||||
|
||||
When using the ``->`` and ``->>`` operators on composite and array columns, PostgREST uses a query like ``to_jsonb(<col>)->'field'``. To make filtering and ordering on those nested fields use an index, the index needs to be created on the same expression, including the ``to_jsonb(...)`` call:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE INDEX ON mytable ((to_jsonb(data) -> 'identification' ->> 'registration_number'));
|
||||
|
||||
.. _ordering:
|
||||
|
||||
Ordering
|
||||
--------
|
||||
|
||||
The reserved word ``order`` reorders the response rows. It uses a comma-separated list of columns and directions:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?order=age.desc,height.asc"
|
||||
|
||||
If no direction is specified it defaults to ascending order:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?order=age"
|
||||
|
||||
If you care where nulls are sorted, add ``nullsfirst`` or ``nullslast``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?order=age.nullsfirst"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?order=age.desc.nullslast"
|
||||
|
||||
You can also sort on fields of :ref:`composite_array_columns` or :ref:`json_columns`.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/countries?order=location->>lat"
|
||||
|
||||
.. _head_req:
|
||||
|
||||
HEAD
|
||||
----
|
||||
|
||||
A HEAD method will behave identically to GET except that no body will be returned (`RFC 2616 <https://datatracker.ietf.org/doc/html/rfc2616#section-9.4>`_) .
|
||||
As an optimization, the generated query won't execute an aggregate (to avoid unnecessary data transfer).
|
||||
|
||||
.. _insert:
|
||||
|
||||
Insert
|
||||
======
|
||||
|
||||
All tables and `auto-updatable views <https://www.postgresql.org/docs/current/sql-createview.html#SQL-CREATEVIEW-UPDATABLE-VIEWS>`_ can be modified through the API, subject to permissions of the requester's database role.
|
||||
|
||||
To create a row in a database table post a JSON object whose keys are the names of the columns you would like to create. Missing properties will be set to default values when applicable.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/table_name" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d '{ "col1": "value1", "col2": "value2" }'
|
||||
|
||||
.. code::
|
||||
|
||||
HTTP/1.1 201 Created
|
||||
|
||||
No response body will be returned by default but you can use :ref:`prefer_return` to get the affected resource.
|
||||
|
||||
x-www-form-urlencoded
|
||||
---------------------
|
||||
|
||||
URL encoded payloads can be posted with ``Content-Type: application/x-www-form-urlencoded``.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people" \
|
||||
-X POST -H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "name=John+Doe&age=50&weight=80"
|
||||
|
||||
.. note::
|
||||
|
||||
When inserting a row you must post a JSON object, not quoted JSON.
|
||||
|
||||
.. code::
|
||||
|
||||
Yes
|
||||
{ "a": 1, "b": 2 }
|
||||
|
||||
No
|
||||
"{ \"a\": 1, \"b\": 2 }"
|
||||
|
||||
Some JavaScript libraries will post the data incorrectly if you're not careful. For best results try one of the :ref:`clientside_libraries` built for PostgREST.
|
||||
|
||||
.. important::
|
||||
|
||||
It's recommended that you `use triggers instead of rules <https://wiki.postgresql.org/wiki/Don%27t_Do_This#Don.27t_use_rules>`_.
|
||||
Insertion on views with complex `rules <https://www.postgresql.org/docs/current/sql-createrule.html>`_ might not work out of the box with PostgREST due to its usage of CTEs.
|
||||
If you want to keep using rules, a workaround is to wrap the view insertion in a stored procedure and call it through the :ref:`s_procs` interface.
|
||||
For more details, see this `github issue <https://github.com/PostgREST/postgrest/issues/1283>`_.
|
||||
|
||||
.. _bulk_insert:
|
||||
|
||||
Bulk Insert
|
||||
-----------
|
||||
|
||||
Bulk insert works exactly like single row insert except that you provide either a JSON array of objects having uniform keys, or lines in CSV format. This not only minimizes the HTTP requests required but uses a single INSERT statement on the back-end for efficiency.
|
||||
|
||||
To bulk insert CSV simply post to a table route with :code:`Content-Type: text/csv` and include the names of the columns as the first row. For instance
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people" \
|
||||
-X POST -H "Content-Type: text/csv" \
|
||||
--data-binary @- << EOF
|
||||
name,age,height
|
||||
J Doe,62,70
|
||||
Jonas,10,55
|
||||
EOF
|
||||
|
||||
An empty field (:code:`,,`) is coerced to an empty string and the reserved word :code:`NULL` is mapped to the SQL null value. Note that there should be no spaces between the column names and commas.
|
||||
|
||||
To bulk insert JSON post an array of objects having all-matching keys
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
[
|
||||
{ "name": "J Doe", "age": 62, "height": 70 },
|
||||
{ "name": "Janus", "age": 10, "height": 55 }
|
||||
]
|
||||
EOF
|
||||
|
||||
.. _bulk_insert_default:
|
||||
|
||||
Bulk Insert with Default Values
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Any missing columns in the payload will be inserted as ``null`` values. To use the ``DEFAULT`` column value instead, use the ``Prefer: missing=default`` header.
|
||||
|
||||
Having:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create table foo (
|
||||
id bigint generated by default as identity primary key
|
||||
, bar text
|
||||
, baz int default 100
|
||||
);
|
||||
|
||||
A request:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/foo?columns=id,bar,baz" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: missing=default, return=representation" \
|
||||
-d @- << EOF
|
||||
[
|
||||
{ "bar": "val1" },
|
||||
{ "bar": "val2", "baz": 15 }
|
||||
]
|
||||
EOF
|
||||
|
||||
Will result in:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{ "id": 1, "bar": "val1", "baz": 100 },
|
||||
{ "id": 2, "bar": "val2", "baz": 15 }
|
||||
]
|
||||
|
||||
.. _specify_columns:
|
||||
|
||||
Specifying Columns
|
||||
------------------
|
||||
|
||||
By using the :code:`columns` query parameter it's possible to specify the payload keys that will be inserted and ignore the rest of the payload.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/datasets?columns=source,publication_date,figure" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-d @- << EOF
|
||||
{
|
||||
"source": "Natural Disaster Prevention and Control",
|
||||
"publication_date": "2015-09-11",
|
||||
"figure": 1100,
|
||||
"location": "...",
|
||||
"comment": "...",
|
||||
"extra": "...",
|
||||
"stuff": "..."
|
||||
}
|
||||
EOF
|
||||
|
||||
In this case, only **source**, **publication_date** and **figure** will be inserted. The rest of the JSON keys will be ignored.
|
||||
|
||||
Using this also has the side-effect of being more efficient for :ref:`bulk_insert` since PostgREST will not process the JSON and
|
||||
it'll send it directly to PostgreSQL.
|
||||
|
||||
.. _update:
|
||||
|
||||
Update
|
||||
======
|
||||
|
||||
To update a row or rows in a table, use the PATCH verb. Use :ref:`h_filter` to specify which record(s) to update. Here is an example query setting the :code:`category` column to child for all people below a certain age.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people?age=lt.13" \
|
||||
-X PATCH -H "Content-Type: application/json" \
|
||||
-d '{ "category": "child" }'
|
||||
|
||||
Updates also support :ref:`prefer_return` plus :ref:`v_filter`.
|
||||
|
||||
.. warning::
|
||||
|
||||
Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`.
|
||||
|
||||
.. _prefer_resolution:
|
||||
|
||||
.. _upsert:
|
||||
|
||||
Upsert
|
||||
======
|
||||
|
||||
You can make an upsert with :code:`POST` and the :code:`Prefer: resolution=merge-duplicates` header:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/employees" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-H "Prefer: resolution=merge-duplicates" \
|
||||
-d @- << EOF
|
||||
[
|
||||
{ "id": 1, "name": "Old employee 1", "salary": 30000 },
|
||||
{ "id": 2, "name": "Old employee 2", "salary": 42000 },
|
||||
{ "id": 3, "name": "New employee 3", "salary": 50000 }
|
||||
]
|
||||
EOF
|
||||
|
||||
By default, upsert operates based on the primary key columns, you must specify all of them. You can also choose to ignore the duplicates with :code:`Prefer: resolution=ignore-duplicates`. This works best when the primary key is natural, but it's also possible to use it if the primary key is surrogate (example: "id serial primary key"). For more details read `this issue <https://github.com/PostgREST/postgrest/issues/1118>`_.
|
||||
|
||||
.. important::
|
||||
After creating a table or changing its primary key, you must refresh PostgREST schema cache for upsert to work properly. To learn how to refresh the cache see :ref:`schema_reloading`.
|
||||
|
||||
.. _on_conflict:
|
||||
|
||||
On Conflict
|
||||
-----------
|
||||
|
||||
By specifying the ``on_conflict`` query parameter, you can make upsert work on a column(s) that has a UNIQUE constraint.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/employees?on_conflict=name" \
|
||||
-X POST -H "Content-Type: application/json" \
|
||||
-H "Prefer: resolution=merge-duplicates" \
|
||||
-d @- << EOF
|
||||
[
|
||||
{ "name": "Old employee 1", "salary": 40000 },
|
||||
{ "name": "Old employee 2", "salary": 52000 },
|
||||
{ "name": "New employee 3", "salary": 60000 }
|
||||
]
|
||||
EOF
|
||||
|
||||
.. _upsert_put:
|
||||
|
||||
PUT
|
||||
---
|
||||
|
||||
A single row upsert can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost/employees?id=eq.4" \
|
||||
-X PUT -H "Content-Type: application/json" \
|
||||
-d '{ "id": 4, "name": "Sara B.", "salary": 60000 }'
|
||||
|
||||
All the columns must be specified in the request body, including the primary key columns.
|
||||
|
||||
.. _delete:
|
||||
|
||||
Delete
|
||||
======
|
||||
|
||||
To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instance deleting inactive users:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/user?active=is.false" -X DELETE
|
||||
|
||||
Deletions also support :ref:`prefer_return` plus :ref:`v_filter`.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/user?id=eq.1" -X DELETE \
|
||||
-H "Prefer: return=representation"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{"id": 1, "email": "johndoe@email.com"}
|
||||
|
||||
.. warning::
|
||||
|
||||
Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`.
|
||||
|
||||
.. _limited_update_delete:
|
||||
|
||||
Limited Update/Delete
|
||||
=====================
|
||||
|
||||
You can limit the amount of affected rows by :ref:`update` or :ref:`delete` with the ``limit`` query parameter. For this, you must add an explicit ``order`` on a unique column(s).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -X PATCH "/users?limit=10&order=id&last_login=lt.2020-01-01" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{ "status": "inactive" }'
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -X DELETE "http://localhost:3000/users?limit=10&order=id&status=eq.inactive"
|
||||
|
||||
If your table has no unique columns, you can use the `ctid <https://www.postgresql.org/docs/current/ddl-system-columns.html>`_ system column.
|
||||
|
||||
Using ``offset`` to target a different subset of rows is also possible.
|
||||
|
||||
.. note::
|
||||
|
||||
There is no native ``UPDATE...LIMIT`` or ``DELETE...LIMIT`` support in PostgreSQL; the generated query simulates that behavior and is based on `this Crunchy Data blog post <https://www.crunchydata.com/blog/simulating-update-or-delete-with-limit-in-postgres-ctes-to-the-rescue>`_.
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript">
|
||||
let hash = window.location.hash;
|
||||
|
||||
const redirects = {
|
||||
// Tables and Views
|
||||
'#computed-virtual-columns': 'computed_fields.html#computed-fields',
|
||||
'#limits-and-pagination': 'pagination_count.html#limits-and-pagination',
|
||||
'#exact-count': 'pagination_count.html#exact-count',
|
||||
'#planned-count': 'pagination_count.html#planned-count',
|
||||
'#estimated-count': 'pagination_count.html#estimated-count',
|
||||
'#prefer-return-headers-only': 'preferences.html#headers-only',
|
||||
'#prefer-return-representation': 'preferences.html#full',
|
||||
};
|
||||
|
||||
let willRedirectTo = redirects[hash];
|
||||
|
||||
if (willRedirectTo) {
|
||||
window.location.href = willRedirectTo;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,79 @@
|
||||
.. note::
|
||||
|
||||
This page is a work in progress.
|
||||
|
||||
.. _url_grammar:
|
||||
|
||||
URL Grammar
|
||||
===========
|
||||
|
||||
.. _custom_queries:
|
||||
|
||||
Custom Queries
|
||||
--------------
|
||||
|
||||
The PostgREST URL grammar limits the kinds of queries clients can perform. It prevents arbitrary, potentially poorly constructed and slow client queries. It's good for quality of service, but means database administrators must create custom views and stored procedures to provide richer endpoints. The most common causes for custom endpoints are
|
||||
|
||||
* Table unions
|
||||
* More complicated joins than those provided by :ref:`resource_embedding`.
|
||||
* Geo-spatial queries that require an argument, like "points near (lat,lon)"
|
||||
|
||||
Unicode support
|
||||
---------------
|
||||
|
||||
PostgREST supports unicode in schemas, tables, columns and values. To access a table with unicode name, use percent encoding.
|
||||
|
||||
To request this:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
GET /موارد HTTP/1.1
|
||||
|
||||
Do this:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
|
||||
|
||||
.. _tabs-cols-w-spaces:
|
||||
|
||||
Table / Columns with spaces
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can request table/columns with spaces in them by percent encoding the spaces with ``%20``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/Order%20Items?Unit%20Price=lt.200"
|
||||
|
||||
.. _reserved-chars:
|
||||
|
||||
Reserved characters
|
||||
~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
If filters include PostgREST reserved characters(``,``, ``.``, ``:``, ``()``) you'll have to surround them in percent encoded double quotes ``%22`` for correct processing.
|
||||
|
||||
Here ``Hebdon,John`` and ``Williams,Mary`` are values.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/employees?name=in.(%22Hebdon,John%22,%22Williams,Mary%22)"
|
||||
|
||||
Here ``information.cpe`` is a column name.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/vulnerabilities?%22information.cpe%22=like.*MS*"
|
||||
|
||||
If the value filtered by the ``in`` operator has a double quote (``"``), you can escape it using a backslash ``"\""``. A backslash itself can be used with a double backslash ``"\\"``.
|
||||
|
||||
Here ``Quote:"`` and ``Backslash:\`` are percent-encoded values. Note that ``%5C`` is the percent-encoded backslash.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/marks?name=in.(%22Quote:%5C%22%22,%22Backslash:%5C%5C%22)"
|
||||
|
||||
.. note::
|
||||
|
||||
Some HTTP libraries might encode URLs automatically(e.g. :code:`axios`). In these cases you should use double quotes
|
||||
:code:`""` directly instead of :code:`%22`.
|
||||
@@ -0,0 +1,202 @@
|
||||
.. _authn:
|
||||
|
||||
Authentication
|
||||
==============
|
||||
|
||||
PostgREST is designed to keep the database at the center of API security. All :ref:`authorization happens in the database <db_authz>` . It is PostgREST's job to **authenticate** requests -- i.e. verify that a client is who they say they are -- and then let the database **authorize** client actions.
|
||||
|
||||
.. _roles:
|
||||
|
||||
Overview of role system
|
||||
-----------------------
|
||||
|
||||
There are three types of roles used by PostgREST, the **authenticator**, **anonymous** and **user** roles. The database administrator creates these roles and configures PostgREST to use them.
|
||||
|
||||
.. image:: ../_static/security-roles.png
|
||||
|
||||
The authenticator role is used for connecting to the database and should be configured to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests.
|
||||
|
||||
|
||||
.. code:: sql
|
||||
|
||||
|
||||
CREATE ROLE authenticator LOGIN NOINHERIT NOCREATEDB NOCREATEROLE NOSUPERUSER;
|
||||
CREATE ROLE anonymous NOLOGIN;
|
||||
CREATE ROLE webuser NOLOGIN;
|
||||
|
||||
.. note::
|
||||
|
||||
The names "authenticator" and "anon" names are configurable and not sacred, we simply choose them for clarity. See :ref:`db-uri` and :ref:`db-anon-role`.
|
||||
|
||||
.. _user_impersonation:
|
||||
|
||||
User Impersonation
|
||||
------------------
|
||||
|
||||
The picture below shows how the server handles authentication. If auth succeeds, it switches into the user role specified by the request, otherwise it switches into the anonymous role (if it's set in :ref:`db-anon-role`).
|
||||
|
||||
.. image:: ../_static/security-anon-choice.png
|
||||
|
||||
This role switching mechanism is called **user impersonation**. In PostgreSQL it's done with the ``SET ROLE`` statement.
|
||||
|
||||
.. note::
|
||||
|
||||
The impersonated roles will have their settings applied. See :ref:`impersonated_settings`.
|
||||
|
||||
.. _jwt_impersonation:
|
||||
|
||||
JWT-Based User Impersonation
|
||||
----------------------------
|
||||
|
||||
We use `JSON Web Tokens <https://jwt.io/>`_ to authenticate API requests, this allows us to be stateless and not require database lookups for verification. As you'll recall a JWT contains a list of cryptographically signed claims. All claims are allowed but PostgREST cares specifically about a claim called role.
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"role": "user123"
|
||||
}
|
||||
|
||||
When a request contains a valid JWT with a role claim PostgREST will switch to the database role with that name for the duration of the HTTP request.
|
||||
|
||||
.. code:: sql
|
||||
|
||||
SET LOCAL ROLE user123;
|
||||
|
||||
Note that the database administrator must allow the authenticator role to switch into this user by previously executing
|
||||
|
||||
.. code:: sql
|
||||
|
||||
GRANT user123 TO authenticator;
|
||||
-- similarly for the anonymous role
|
||||
-- GRANT anonymous TO authenticator;
|
||||
|
||||
If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role. The database administrator must set the anonymous role permissions correctly to prevent anonymous users from seeing or changing things they shouldn't.
|
||||
|
||||
.. _jwt_generation:
|
||||
|
||||
JWT Generation
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
You can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_jwt`).
|
||||
|
||||
.. _client_auth:
|
||||
|
||||
Client Auth
|
||||
~~~~~~~~~~~
|
||||
|
||||
To make an authenticated request the client must include an :code:`Authorization` HTTP header with the value :code:`Bearer <jwt>`. For instance:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/foo" \
|
||||
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZSIsImV4cCI6MTQ3NTUxNjI1MH0.GYDZV3yM0gqvuEtJmfpplLBXSGYnke_Pvnl0tbKAjB4"
|
||||
|
||||
The ``Bearer`` header value can be used with or without capitalization(``bearer``).
|
||||
|
||||
.. _jwt_caching:
|
||||
|
||||
JWT Caching
|
||||
-----------
|
||||
|
||||
PostgREST validates ``JWTs`` on every request. We can cache ``JWTs`` to avoid this performance overhead.
|
||||
|
||||
To enable JWT caching, the config :code:`jwt-cache-max-lifetime` is to be set. It is the maximum number of seconds for which the cache stores the JWT validation results. The cache uses the :code:`exp` claim to set the cache entry lifetime. If the JWT does not have an :code:`exp` claim, it uses the config value. See :ref:`jwt-cache-max-lifetime` for more details.
|
||||
|
||||
.. note::
|
||||
|
||||
You can use the :ref:`server-timing_header` to see the effect of JWT caching.
|
||||
|
||||
Symmetric Keys
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Each token is cryptographically signed with a secret key. In the case of symmetric cryptography the signer and verifier share the same secret passphrase, which can be configured with :ref:`jwt-secret`.
|
||||
If it is set to a simple string value like “reallyreallyreallyreallyverysafe” then PostgREST interprets it as an HMAC-SHA256 passphrase.
|
||||
|
||||
.. _asym_keys:
|
||||
|
||||
Asymmetric Keys
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
In asymmetric cryptography the signer uses the private key and the verifier the public key.
|
||||
|
||||
As described in the :ref:`configuration` section, PostgREST accepts a ``jwt-secret`` config file parameter. However you can also specify a literal JSON Web Key (JWK) or set. For example, you can use an RSA-256 public key encoded as a JWK:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"alg":"RS256",
|
||||
"e":"AQAB",
|
||||
"key_ops":["verify"],
|
||||
"kty":"RSA",
|
||||
"n":"9zKNYTaYGfGm1tBMpRT6FxOYrM720GhXdettc02uyakYSEHU2IJz90G_MLlEl4-WWWYoS_QKFupw3s7aPYlaAjamG22rAnvWu-rRkP5sSSkKvud_IgKL4iE6Y2WJx2Bkl1XUFkdZ8wlEUR6O1ft3TS4uA-qKifSZ43CahzAJyUezOH9shI--tirC028lNg767ldEki3WnVr3zokSujC9YJ_9XXjw2hFBfmJUrNb0-wldvxQbFU8RPXip-GQ_JPTrCTZhrzGFeWPvhA6Rqmc3b1PhM9jY7Dur1sjYWYVyXlFNCK3c-6feo5WlRfe1aCWmwZQh6O18eTmLeT4nWYkDzQ"
|
||||
}
|
||||
|
||||
.. note::
|
||||
|
||||
This could also be a JSON Web Key Set (JWKS) if it was contained within an array assigned to a `keys` member, e.g. ``{ keys: [jwk1, jwk2] }``.
|
||||
|
||||
Just pass it in as a single line string, escaping the quotes:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
jwt-secret = "{ \"alg\":\"RS256\", … }"
|
||||
|
||||
To generate such a public/private key pair use a utility like `latchset/jose <https://github.com/latchset/jose>`_.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
jose jwk gen -i '{"alg": "RS256"}' -o rsa.jwk
|
||||
jose jwk pub -i rsa.jwk -o rsa.jwk.pub
|
||||
|
||||
# now rsa.jwk.pub contains the desired JSON object
|
||||
|
||||
You can specify the literal value as we saw earlier, or reference a filename to load the JWK from a file:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
jwt-secret = "@rsa.jwk.pub"
|
||||
|
||||
JWT Claims Validation
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PostgREST honors the :code:`exp` claim for token expiration, rejecting expired tokens.
|
||||
|
||||
JWT Security
|
||||
~~~~~~~~~~~~
|
||||
|
||||
There are at least three types of common critiques against using JWT: 1) against the standard itself, 2) against using libraries with known security vulnerabilities, and 3) against using JWT for web sessions. We'll briefly explain each critique, how PostgREST deals with it, and give recommendations for appropriate user action.
|
||||
|
||||
The critique against the `JWT standard <https://datatracker.ietf.org/doc/html/rfc7519>`_ is voiced in detail `elsewhere on the web <https://web.archive.org/web/20230123041631/https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-bad-standard-that-everyone-should-avoid>`_. The most relevant part for PostgREST is the so-called :code:`alg=none` issue. Some servers implementing JWT allow clients to choose the algorithm used to sign the JWT. In this case, an attacker could set the algorithm to :code:`none`, remove the need for any signature at all and gain unauthorized access. The current implementation of PostgREST, however, does not allow clients to set the signature algorithm in the HTTP request, making this attack irrelevant. The critique against the standard is that it requires the implementation of the :code:`alg=none` at all.
|
||||
|
||||
Critiques against JWT libraries are only relevant to PostgREST via the library it uses. As mentioned above, not allowing clients to choose the signature algorithm in HTTP requests removes the greatest risk. Another more subtle attack is possible where servers use asymmetric algorithms like RSA for signatures. Once again this is not relevant to PostgREST since it is not supported. Curious readers can find more information in `this article <https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/>`_. Recommendations about high quality libraries for usage in API clients can be found on `jwt.io <https://jwt.io/>`_.
|
||||
|
||||
The last type of critique focuses on the misuse of JWT for maintaining web sessions. The basic recommendation is to `stop using JWT for sessions <http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-for-sessions/>`_ because most, if not all, solutions to the problems that arise when you do, `do not work <http://cryto.net/~joepie91/blog/2016/06/19/stop-using-jwt-for-sessions-part-2-why-your-solution-doesnt-work/>`_. The linked articles discuss the problems in depth but the essence of the problem is that JWT is not designed to be secure and stateful units for client-side storage and therefore not suited to session management.
|
||||
|
||||
PostgREST uses JWT mainly for authentication and authorization purposes and encourages users to do the same. For web sessions, using cookies over HTTPS is good enough and well catered for by standard web frameworks.
|
||||
|
||||
.. _custom_validation:
|
||||
|
||||
Custom Validation
|
||||
-----------------
|
||||
|
||||
PostgREST does not enforce any extra constraints besides JWT validation. An example of an extra constraint would be to immediately revoke access for a certain user. Using :ref:`db-pre-request` you can specify a stored procedure to call immediately after :ref:`user_impersonation` and before the main query itself runs.
|
||||
|
||||
.. code:: ini
|
||||
|
||||
db-pre-request = "public.check_user"
|
||||
|
||||
In the function you can run arbitrary code to check the request and raise an exception(see :ref:`raise_error`) to block it if desired. Here you can take advantage of :ref:`guc_req_headers_cookies_claims` for
|
||||
doing custom logic based on the web user info.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
CREATE OR REPLACE FUNCTION check_user() RETURNS void AS $$
|
||||
DECLARE
|
||||
email text := current_setting('request.jwt.claims', true)::json->>'email';
|
||||
BEGIN
|
||||
IF email = 'evil.user@malicious.com' THEN
|
||||
RAISE EXCEPTION 'No, you are evil'
|
||||
USING HINT = 'Stop being so evil and maybe you can log in';
|
||||
END IF;
|
||||
END
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -0,0 +1,879 @@
|
||||
.. _configuration:
|
||||
|
||||
Configuration
|
||||
#############
|
||||
|
||||
Configuration parameters can be provided via:
|
||||
|
||||
- :ref:`file_config`.
|
||||
- :ref:`env_variables_config`, overriding values from the config file.
|
||||
- :ref:`in_db_config`, overriding values from both the config file and environment variables.
|
||||
|
||||
Using :ref:`config_reloading` you can modify the parameters without restarting the server.
|
||||
|
||||
|
||||
Minimum parameters
|
||||
==================
|
||||
|
||||
The server is able to start without any config parameters, but it won't be able to serve requests unless it has :ref:`a role to serve anonymous requests with <db-anon-role>` - or :ref:`a secret to use for JWT authentication <jwt-secret>`.
|
||||
|
||||
.. _file_config:
|
||||
|
||||
Config File
|
||||
===========
|
||||
|
||||
There is no predefined location for the config file, you must specify the file path as the one and only argument to the server:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
./postgrest /path/to/postgrest.conf
|
||||
|
||||
The configuration file must contain a set of key value pairs:
|
||||
|
||||
.. code::
|
||||
|
||||
# postgrest.conf
|
||||
|
||||
# The standard connection URI format, documented at
|
||||
# https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
|
||||
db-uri = "postgres://user:pass@host:5432/dbname"
|
||||
|
||||
# The database role to use when no client authentication is provided.
|
||||
# Should differ from authenticator
|
||||
db-anon-role = "anon"
|
||||
|
||||
# The secret to verify the JWT for authenticated requests with.
|
||||
# Needs to be 32 characters minimum.
|
||||
jwt-secret = "reallyreallyreallyreallyverysafe"
|
||||
jwt-secret-is-base64 = false
|
||||
|
||||
# Port the postgrest process is listening on for http requests
|
||||
server-port = 3000
|
||||
|
||||
You can run ``postgrest --example`` to display all possible configuration parameters and how to use them in a configuration file.
|
||||
|
||||
.. _env_variables_config:
|
||||
|
||||
Environment Variables
|
||||
=====================
|
||||
|
||||
Environment variables are capitalized, have a ``PGRST_`` prefix, and use underscores. For example: ``PGRST_DB_URI`` corresponds to ``db-uri`` and ``PGRST_APP_SETTINGS_*`` to ``app.settings.*``.
|
||||
|
||||
`libpq environment variables <https://www.postgresql.org/docs/current/libpq-envars.html>`_ are also supported for constructing the connection string, see :ref:`db-uri`.
|
||||
|
||||
See the full list of environment variable names on :ref:`config_full_list`.
|
||||
|
||||
.. _in_db_config:
|
||||
|
||||
In-Database Configuration
|
||||
=========================
|
||||
|
||||
You can also configure the server with database settings by using a :ref:`pre-config <db-pre-config>` function. For example, you can configure :ref:`db-schemas` and :ref:`jwt-secret` like this:
|
||||
|
||||
.. code-block::
|
||||
|
||||
# postgrest.conf
|
||||
|
||||
db-pre-config = "postgrest.pre_config"
|
||||
|
||||
# or env vars
|
||||
|
||||
PGRST_DB_PRE_CONFIG = "postgrest.pre_config"
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
-- create a dedicated schema, hidden from the API
|
||||
create schema postgrest;
|
||||
-- grant usage on this schema to the authenticator
|
||||
grant usage on schema postgrest to authenticator;
|
||||
|
||||
-- the function can configure postgREST by using set_config
|
||||
create or replace function postgrest.pre_config()
|
||||
returns void as $$
|
||||
select
|
||||
set_config('pgrst.db_schemas', 'schema1, schema2', true)
|
||||
, set_config('pgrst.jwt_secret', 'REALLYREALLYREALLYREALLYVERYSAFE', true);
|
||||
$$ language sql;
|
||||
|
||||
Note that underscores(``_``) need to be used instead of dashes(``-``) for the in-database config parameters. See the full list of in-database names on :ref:`config_full_list`.
|
||||
|
||||
You can disable the in-database configuration by setting :ref:`db-config` to ``false``.
|
||||
|
||||
.. note::
|
||||
For backwards compatibility, you can do in-db config by modifying the :ref:`authenticator role <roles>`. This is no longer recommended as it requires SUPERUSER.
|
||||
|
||||
.. code:: postgresql
|
||||
|
||||
ALTER ROLE authenticator SET pgrst.db_schemas = "tenant1, tenant2, tenant3"
|
||||
ALTER ROLE authenticator IN DATABASE <your_database_name> SET pgrst.db_schemas = "tenant4, tenant5" -- database-specific setting, overrides the previous setting
|
||||
|
||||
.. _config_reloading:
|
||||
|
||||
Configuration Reloading
|
||||
=======================
|
||||
|
||||
It's possible to reload PostgREST's configuration without restarting the server. You can do this :ref:`via signal <config_reloading_signal>` or :ref:`via notification <config_reloading_notify>`.
|
||||
|
||||
- Any modification to the :ref:`file_config` will be applied during reload.
|
||||
- Any modification to the :ref:`in_db_config` will be applied during reload.
|
||||
- Not all settings are reloadable, see the reloadable list on :ref:`config_full_list`.
|
||||
- It's not possible to change :ref:`env_variables_config` for a running process, hence reloading a Docker container configuration will not work. In these cases, you can restart the process or use :ref:`in_db_config`.
|
||||
|
||||
.. _config_reloading_signal:
|
||||
|
||||
Reload with signal
|
||||
------------------
|
||||
|
||||
To reload the configuration via signal, send a SIGUSR2 signal to the server process.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
killall -SIGUSR2 postgrest
|
||||
|
||||
.. _config_reloading_notify:
|
||||
|
||||
Reload with NOTIFY
|
||||
------------------
|
||||
|
||||
To reload the configuration from within the database, you can use a NOTIFY command.
|
||||
|
||||
.. code:: postgresql
|
||||
|
||||
NOTIFY pgrst, 'reload config'
|
||||
|
||||
The ``"pgrst"`` notification channel is enabled by default. You can name the channel with :ref:`db-channel` and enable or disable it with :ref:`db-channel-enabled`.
|
||||
|
||||
.. _config_full_list:
|
||||
|
||||
List of parameters
|
||||
==================
|
||||
|
||||
.. _admin-server-port:
|
||||
|
||||
admin-server-port
|
||||
-----------------
|
||||
|
||||
=============== =======================
|
||||
**Type** Int
|
||||
**Default** `n/a`
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_ADMIN_SERVER_PORT
|
||||
**In-Database** `n/a`
|
||||
=============== =======================
|
||||
|
||||
Specifies the port for the :ref:`health_check` endpoints.
|
||||
|
||||
.. _app.settings.*:
|
||||
|
||||
app.settings.*
|
||||
--------------
|
||||
|
||||
=============== =======================
|
||||
**Type** String
|
||||
**Default** `n/a`
|
||||
**Reloadable** &
|
||||
**Environment** PGRST_APP_SETTINGS_*
|
||||
**In-Database** `n/a`
|
||||
=============== =======================
|
||||
|
||||
Arbitrary settings that can be used to pass in secret keys directly as strings, or via OS environment variables. For instance: :code:`app.settings.jwt_secret = "$(MYAPP_JWT_SECRET)"` will take :code:`MYAPP_JWT_SECRET` from the environment and make it available to postgresql functions as :code:`current_setting('app.settings.jwt_secret')`.
|
||||
|
||||
.. _db-aggregates-enabled:
|
||||
|
||||
db-aggregates-enabled
|
||||
---------------------
|
||||
|
||||
=============== =======================
|
||||
**Type** Boolean
|
||||
**Default** False
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_AGGREGATES_ENABLED
|
||||
**In-Database** pgrst.db_aggregates_enabled
|
||||
=============== =======================
|
||||
|
||||
|
||||
When this is set to :code:`true`, the use of :ref:`aggregate_functions` is allowed.
|
||||
|
||||
It is recommended that this be set to ``false`` unless proper safeguards are in place to prevent potential performance problems from arising. For example, it is possible that a user may request the ``max()`` of an unindexed column in a table with millions of rows. At best, this would result in a slow query, and at worst, it could be abused to prevent other users from accessing your API (i.e. a form of denial-of-service attack.)
|
||||
|
||||
Proper safeguards could include:
|
||||
- Use of a statement timeout. See :ref:`impersonated_settings`.
|
||||
- Use of the `pg_plan_filter extension <https://github.com/pgexperts/pg_plan_filter>`_ to block excessively expensive queries.
|
||||
|
||||
.. _db-anon-role:
|
||||
|
||||
db-anon-role
|
||||
------------
|
||||
|
||||
=============== =======================
|
||||
**Type** String
|
||||
**Default** `n/a`
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_ANON_ROLE
|
||||
**In-Database** pgrst.db_anon_role
|
||||
=============== =======================
|
||||
|
||||
The database role to use when executing commands on behalf of unauthenticated clients. For more information, see :ref:`roles`.
|
||||
|
||||
When unset anonymous access will be blocked.
|
||||
|
||||
.. _db-channel:
|
||||
|
||||
db-channel
|
||||
----------
|
||||
|
||||
=============== =======================
|
||||
**Type** String
|
||||
**Default** pgrst
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_CHANNEL
|
||||
**In-Database** `n/a`
|
||||
=============== =======================
|
||||
|
||||
The name of the notification channel that PostgREST uses for :ref:`schema_reloading` and configuration reloading.
|
||||
|
||||
.. _db-channel-enabled:
|
||||
|
||||
db-channel-enabled
|
||||
------------------
|
||||
|
||||
=============== =======================
|
||||
**Type** Boolean
|
||||
**Default** True
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_CHANNEL_ENABLED
|
||||
**In-Database** `n/a`
|
||||
=============== =======================
|
||||
|
||||
When this is set to :code:`true`, the notification channel specified in :ref:`db-channel` is enabled.
|
||||
|
||||
You should set this to ``false`` when using PostgresSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
||||
|
||||
.. _db-config:
|
||||
|
||||
db-config
|
||||
---------
|
||||
|
||||
=============== =======================
|
||||
**Type** Boolean
|
||||
**Default** True
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_CONFIG
|
||||
**In-Database** `n/a`
|
||||
=============== =======================
|
||||
|
||||
Enables the in-database configuration.
|
||||
|
||||
.. _db-pre-config:
|
||||
|
||||
db-pre-config
|
||||
-------------
|
||||
|
||||
=============== =======================
|
||||
**Type** String
|
||||
**Default** `n/a`
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_PRE_CONFIG
|
||||
**In-Database** pgrst.db_pre_config
|
||||
=============== =======================
|
||||
|
||||
Name of the function that does :ref:`in_db_config`.
|
||||
|
||||
.. _db-extra-search-path:
|
||||
|
||||
db-extra-search-path
|
||||
--------------------
|
||||
|
||||
=============== ==========================
|
||||
**Type** String
|
||||
**Default** public
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_EXTRA_SEARCH_PATH
|
||||
**In-Database** pgrst.db_extra_search_path
|
||||
=============== ==========================
|
||||
|
||||
Extra schemas to add to the `search_path <https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH>`_ of every request. These schemas tables, views and stored procedures **don't get API endpoints**, they can only be referred from the database objects inside your :ref:`db-schemas`.
|
||||
|
||||
This parameter was meant to make it easier to use **PostgreSQL extensions** (like PostGIS) that are outside of the :ref:`db-schemas`.
|
||||
|
||||
Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``.
|
||||
|
||||
.. _db-max-rows:
|
||||
|
||||
db-max-rows
|
||||
-----------
|
||||
|
||||
=============== ==========================
|
||||
**Type** Int
|
||||
**Default** ∞
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_MAX_ROWS
|
||||
**In-Database** pgrst.db_max_rows
|
||||
=============== ==========================
|
||||
|
||||
*For backwards compatibility, this config parameter is also available without prefix as "max-rows".*
|
||||
|
||||
A hard limit to the number of rows PostgREST will fetch from a view, table, or stored procedure. Limits payload size for accidental or malicious requests.
|
||||
|
||||
.. _db-plan-enabled:
|
||||
|
||||
db-plan-enabled
|
||||
---------------
|
||||
|
||||
=============== ==========================
|
||||
**Type** Boolean
|
||||
**Default** False
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_PLAN_ENABLED
|
||||
**In-Database** pgrst.db_plan_enabled
|
||||
=============== ==========================
|
||||
|
||||
When this is set to :code:`true`, the execution plan of a request can be retrieved by using the :code:`Accept: application/vnd.pgrst.plan` header. See :ref:`explain_plan`.
|
||||
|
||||
.. _db-pool:
|
||||
|
||||
db-pool
|
||||
-------
|
||||
|
||||
=============== ==========================
|
||||
**Type** Int
|
||||
**Default** 10
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_DB_POOL
|
||||
**In-Database** n/a
|
||||
=============== ==========================
|
||||
|
||||
Number of maximum connections to keep open in PostgREST's database pool.
|
||||
|
||||
.. _db-pool-acquisition-timeout:
|
||||
|
||||
db-pool-acquisition-timeout
|
||||
---------------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Int
|
||||
**Default** 10
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_DB_POOL_ACQUISITION_TIMEOUT
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
Specifies the maximum time in seconds that the request will wait for the pool to free up a connection slot to the database.
|
||||
|
||||
.. _db-pool-max-idletime:
|
||||
|
||||
db-pool-max-idletime
|
||||
--------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Int
|
||||
**Default** 30
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_DB_POOL_MAX_IDLETIME
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
*For backwards compatibility, this config parameter is also available as “db-pool-timeout”.*
|
||||
|
||||
Time in seconds to close idle pool connections.
|
||||
|
||||
.. _db-pool-max-lifetime:
|
||||
|
||||
db-pool-max-lifetime
|
||||
--------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Int
|
||||
**Default** 1800
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_DB_POOL_MAX_LIFETIME
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
Specifies the maximum time in seconds of an existing connection in the pool.
|
||||
|
||||
.. _db-pool-automatic-recovery:
|
||||
|
||||
db-pool-automatic-recovery
|
||||
--------------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Boolean
|
||||
**Default** True
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_POOL_AUTOMATIC_RECOVERY
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
Enables or disables connection retrying.
|
||||
|
||||
When disabled, PostgREST would terminate immediately after connection loss instead of retrying indefinitely. See :ref:`this section <automatic_recovery>` for more information.
|
||||
|
||||
.. _db-pre-request:
|
||||
|
||||
db-pre-request
|
||||
--------------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** `n/a`
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_PRE_REQUEST
|
||||
**In-Database** pgrst.db_pre_request
|
||||
=============== =================================
|
||||
|
||||
*For backwards compatibility, this config parameter is also available without prefix as "pre-request".*
|
||||
|
||||
A schema-qualified stored procedure name to call right after the :ref:`tx_settings` are set. See :ref:`pre-request`.
|
||||
|
||||
.. _db-prepared-statements:
|
||||
|
||||
db-prepared-statements
|
||||
----------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Boolean
|
||||
**Default** True
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_PREPARED_STATEMENTS
|
||||
**In-Database** pgrst.db_prepared_statements
|
||||
=============== =================================
|
||||
|
||||
Enables or disables prepared statements.
|
||||
|
||||
When disabled, the generated queries will be parameterized (invulnerable to SQL injection) but they will not be prepared (cached in the database session). Not using prepared statements will noticeably decrease performance, so it's recommended to always have this setting enabled.
|
||||
|
||||
You should only set this to ``false`` when using PostgresSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section <external_connection_poolers>` for more information.
|
||||
|
||||
.. _db-root-spec:
|
||||
|
||||
db-root-spec
|
||||
------------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** `n/a`
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_ROOT_SPEC
|
||||
**In-Database** pgrst.db_root_spec
|
||||
=============== =================================
|
||||
|
||||
Function to override the OpenAPI response. See :ref:`override_openapi`.
|
||||
|
||||
.. _db-schemas:
|
||||
|
||||
db-schemas
|
||||
----------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** public
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_DB_SCHEMAS
|
||||
**In-Database** pgrst.db_schemas
|
||||
=============== =================================
|
||||
|
||||
*For backwards compatibility, this config parameter is also available in singular as "db-schema".*
|
||||
|
||||
The list of database schemas to expose to clients. See :ref:`schemas`.
|
||||
|
||||
.. _db-tx-end:
|
||||
|
||||
db-tx-end
|
||||
---------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** commit
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_DB_TX_END
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
Specifies how to terminate the database transactions.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
# The transaction is always committed
|
||||
db-tx-end = "commit"
|
||||
|
||||
# The transaction is committed unless a "Prefer: tx=rollback" header is sent
|
||||
db-tx-end = "commit-allow-override"
|
||||
|
||||
# The transaction is always rolled back
|
||||
db-tx-end = "rollback"
|
||||
|
||||
# The transaction is rolled back unless a "Prefer: tx=commit" header is sent
|
||||
db-tx-end = "rollback-allow-override"
|
||||
|
||||
.. _db-uri:
|
||||
|
||||
db-uri
|
||||
------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** postgresql://
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_DB_URI
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
The standard `PostgreSQL connection string <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING>`_, there are different ways to specify it:
|
||||
|
||||
URI Format
|
||||
~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
"postgres://authenticator:mysecretpassword@localhost:5433/postgres?parameters=val"
|
||||
|
||||
- Under this format symbols and unusual characters in the password or other fields should be percent encoded to avoid a parse error.
|
||||
- If enforcing an SSL connection to the database is required you can use `sslmode <https://www.postgresql.org/docs/current/libpq-ssl.html#LIBPQ-SSL-SSLMODE-STATEMENTS>`_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``.
|
||||
- The user with whom PostgREST connects to the database is also known as the ``authenticator`` role. For more information see :ref:`roles`.
|
||||
- When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket <https://en.wikipedia.org/wiki/Unix_domain_socket>`_ and the `Peer Authentication method <https://www.postgresql.org/docs/current/auth-peer.html>`_ as an alternative to TCP/IP communication and authentication with a password, this also grants higher performance. To do this you can omit the host and the password, e.g. ``postgres://user@/dbname``, see the `libpq connection string <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING>`_ documentation for more details.
|
||||
|
||||
Keyword/Value Format
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
"host=localhost port=5433 user=authenticator password=mysecretpassword dbname=postgres"
|
||||
|
||||
LIBPQ Environment Variables
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code::
|
||||
|
||||
PGHOST=localhost PGPORT=5433 PGUSER=authenticator PGDATABASE=postgres
|
||||
|
||||
Any parameter that is not set in the above formats is read from `libpq environment variables <https://www.postgresql.org/docs/current/libpq-envars.html>`_. The default connection string is ``postgresql://``, which reads **all** parameters from the environment.
|
||||
|
||||
External config file
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Choosing a value for this parameter beginning with the at sign such as ``@filename`` (e.g. ``@./configs/my-config``) loads the connection string out of an external file.
|
||||
|
||||
.. _jwt-aud:
|
||||
|
||||
jwt-aud
|
||||
-------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** `n/a`
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_JWT_AUD
|
||||
**In-Database** pgrst.jwt_aud
|
||||
=============== =================================
|
||||
|
||||
Specifies the `JWT audience claim <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3>`_. If this claim is present in the client provided JWT then you must set this to the same value as in the JWT, otherwise verifying the JWT will fail.
|
||||
|
||||
.. _jwt-role-claim-key:
|
||||
|
||||
jwt-role-claim-key
|
||||
------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** .role
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_JWT_ROLE_CLAIM_KEY
|
||||
**In-Database** pgrst.jwt_role_claim_key
|
||||
=============== =================================
|
||||
|
||||
*For backwards compatibility, this config parameter is also available without prefix as "role-claim-key".*
|
||||
|
||||
A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. This can be used to consume a JWT provided by a third party service like Auth0, Okta or Keycloak. Usage examples:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
# {"postgrest":{"roles": ["other", "author"]}}
|
||||
# the DSL accepts characters that are alphanumerical or one of "_$@" as keys
|
||||
jwt-role-claim-key = ".postgrest.roles[1]"
|
||||
|
||||
# {"https://www.example.com/role": { "key": "author }}
|
||||
# non-alphanumerical characters can go inside quotes(escaped in the config value)
|
||||
jwt-role-claim-key = ".\"https://www.example.com/role\".key"
|
||||
|
||||
.. _jwt-secret:
|
||||
|
||||
jwt-secret
|
||||
----------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** `n/a`
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_JWT_SECRET
|
||||
**In-Database** pgrst.jwt_secret
|
||||
=============== =================================
|
||||
|
||||
The secret or `JSON Web Key (JWK) (or set) <https://datatracker.ietf.org/doc/html/rfc7517>`_ used to decode JWT tokens clients provide for authentication. For security the key must be **at least 32 characters long**. If this parameter is not specified then PostgREST refuses authentication requests. Choosing a value for this parameter beginning with the at sign such as :code:`@filename` loads the secret out of an external file. This is useful for automating deployments. Note that any binary secrets must be base64 encoded. Both symmetric and asymmetric cryptography are supported. For more info see :ref:`asym_keys`.
|
||||
|
||||
Choosing a value for this parameter beginning with the at sign such as ``@filename`` (e.g. ``@./configs/my-config``) loads the secret out of an external file.
|
||||
|
||||
.. warning::
|
||||
|
||||
Only when using the :ref:`file_config`, if the ``jwt-secret`` contains a ``$`` character by itself it will give errors. In this case, use ``$$`` and PostgREST will interpret it as a single ``$`` character.
|
||||
|
||||
.. _jwt-secret-is-base64:
|
||||
|
||||
jwt-secret-is-base64
|
||||
--------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Boolean
|
||||
**Default** False
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_JWT_SECRET_IS_BASE64
|
||||
**In-Database** pgrst.jwt_secret_is_base64
|
||||
=============== =================================
|
||||
|
||||
When this is set to :code:`true`, the value derived from :code:`jwt-secret` will be treated as a base64 encoded secret.
|
||||
|
||||
.. _jwt-cache-max-lifetime:
|
||||
|
||||
jwt-cache-max-lifetime
|
||||
----------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Int
|
||||
**Default** 0
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_JWT_CACHE_MAX_LIFETIME
|
||||
**In-Database** pgrst.jwt_cache_max_lifetime
|
||||
=============== =================================
|
||||
|
||||
Maximum number of seconds of lifetime for cached entries. The default :code:`0` disables caching. See :ref:`jwt_caching`.
|
||||
|
||||
.. _log-level:
|
||||
|
||||
log-level
|
||||
---------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** error
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_LOG_LEVEL
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
Specifies the level of information to be logged while running PostgREST.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
# Only startup and db connection recovery messages are logged
|
||||
log-level = "crit"
|
||||
|
||||
# All the "crit" level events plus server errors (status 5xx) are logged
|
||||
log-level = "error"
|
||||
|
||||
# All the "error" level events plus request errors (status 4xx) are logged
|
||||
log-level = "warn"
|
||||
|
||||
# All the "warn" level events plus all requests (every status code) are logged
|
||||
log-level = "info"
|
||||
|
||||
|
||||
Because currently there's no buffering for logging, the levels with minimal logging(``crit/error``) will increase throughput.
|
||||
|
||||
.. _openapi-mode:
|
||||
|
||||
openapi-mode
|
||||
------------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** follow-privileges
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_OPENAPI_MODE
|
||||
**In-Database** pgrst.openapi_mode
|
||||
=============== =================================
|
||||
|
||||
Specifies how the OpenAPI output should be displayed.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
# Follows the privileges of the JWT role claim (or from db-anon-role if the JWT is not sent)
|
||||
# Shows information depending on the permissions that the role making the request has
|
||||
openapi-mode = "follow-privileges"
|
||||
|
||||
# Ignores the privileges of the JWT role claim (or from db-anon-role if the JWT is not sent)
|
||||
# Shows all the exposed information, regardless of the permissions that the role making the request has
|
||||
openapi-mode = "ignore-privileges"
|
||||
|
||||
# Disables the OpenApi output altogether.
|
||||
# Throws a `404 Not Found` error when accessing the API root path
|
||||
openapi-mode = "disabled"
|
||||
|
||||
.. _openapi-security-active:
|
||||
|
||||
openapi-security-active
|
||||
-----------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Boolean
|
||||
**Default** False
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_OPENAPI_SECURITY_ACTIVE
|
||||
**In-Database** pgrst.openapi_security_active
|
||||
=============== =================================
|
||||
|
||||
When this is set to :code:`true`, security options are included in the :ref:`OpenAPI output <open-api>`.
|
||||
|
||||
.. _openapi-server-proxy-uri:
|
||||
|
||||
openapi-server-proxy-uri
|
||||
------------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** `n/a`
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_OPENAPI_SERVER_PROXY_URI
|
||||
**In-Database** pgrst.openapi_server_proxy_uri
|
||||
=============== =================================
|
||||
|
||||
Overrides the base URL used within the OpenAPI self-documentation hosted at the API root path. Use a complete URI syntax :code:`scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]`. Ex. :code:`https://postgrest.com`
|
||||
|
||||
.. code:: json
|
||||
|
||||
{
|
||||
"swagger": "2.0",
|
||||
"info": {
|
||||
"version": "0.4.3.0",
|
||||
"title": "PostgREST API",
|
||||
"description": "This is a dynamic API generated by PostgREST"
|
||||
},
|
||||
"host": "postgrest.com:443",
|
||||
"basePath": "/",
|
||||
"schemes": [
|
||||
"https"
|
||||
]
|
||||
}
|
||||
|
||||
.. _server_cors_allowed_origins:
|
||||
|
||||
server-cors-allowed-origins
|
||||
---------------------------
|
||||
|
||||
=============== ===================================
|
||||
**Type** String
|
||||
**Default** `n/a`
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_SERVER_CORS_ALLOWED_ORIGINS
|
||||
**In-Database** `pgrst.server_cors_allowed_origins`
|
||||
=============== ===================================
|
||||
|
||||
Specifies allowed CORS origins in this config. See :ref:`cors`.
|
||||
|
||||
When this is not set or set to :code:`""`, PostgREST **accepts** CORS requests from any domain.
|
||||
|
||||
.. _server-host:
|
||||
|
||||
server-host
|
||||
-----------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** !4
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_SERVER_HOST
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
Where to bind the PostgREST web server. In addition to the usual address options, PostgREST interprets these reserved addresses with special meanings:
|
||||
|
||||
* :code:`*` - any IPv4 or IPv6 hostname
|
||||
* :code:`*4` - any IPv4 or IPv6 hostname, IPv4 preferred
|
||||
* :code:`!4` - any IPv4 hostname
|
||||
* :code:`*6` - any IPv4 or IPv6 hostname, IPv6 preferred
|
||||
* :code:`!6` - any IPv6 hostname
|
||||
|
||||
.. _server-port:
|
||||
|
||||
server-port
|
||||
-----------
|
||||
|
||||
=============== =================================
|
||||
**Type** Int
|
||||
**Default** 3000
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_SERVER_PORT
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
The TCP port to bind the web server. Use ``0`` to automatically assign a port.
|
||||
|
||||
.. _server-trace-header:
|
||||
|
||||
server-trace-header
|
||||
-------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** `n/a`
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_SERVER_TRACE_HEADER
|
||||
**In-Database** pgrst.server_trace_header
|
||||
=============== =================================
|
||||
|
||||
The header name used to trace HTTP requests. See :ref:`trace_header`.
|
||||
|
||||
.. _server-timing-enabled:
|
||||
|
||||
server-timing-enabled
|
||||
---------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** Boolean
|
||||
**Default** False
|
||||
**Reloadable** Y
|
||||
**Environment** PGRST_SERVER_TIMING_ENABLED
|
||||
**In-Database** pgrst.server_timing_enabled
|
||||
=============== =================================
|
||||
|
||||
Enables the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>`_ header.
|
||||
See :ref:`server-timing_header`.
|
||||
|
||||
.. _server-unix-socket:
|
||||
|
||||
server-unix-socket
|
||||
------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** `n/a`
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_SERVER_UNIX_SOCKET
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
`Unix domain socket <https://en.wikipedia.org/wiki/Unix_domain_socket>`_ where to bind the PostgREST web server.
|
||||
If specified, this takes precedence over :ref:`server-port`. Example:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
server-unix-socket = "/tmp/pgrst.sock"
|
||||
|
||||
.. _server-unix-socket-mode:
|
||||
|
||||
server-unix-socket-mode
|
||||
-----------------------
|
||||
|
||||
=============== =================================
|
||||
**Type** String
|
||||
**Default** 660
|
||||
**Reloadable** N
|
||||
**Environment** PGRST_SERVER_UNIX_SOCKET_MODE
|
||||
**In-Database** `n/a`
|
||||
=============== =================================
|
||||
|
||||
`Unix file mode <https://en.wikipedia.org/wiki/File_system_permissions>`_ to be set for the socket specified in :ref:`server-unix-socket`
|
||||
Needs to be a valid octal between 600 and 777.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
server-unix-socket-mode = "660"
|
||||
@@ -0,0 +1,94 @@
|
||||
.. _connection_pool:
|
||||
|
||||
Connection Pool
|
||||
===============
|
||||
|
||||
A connection pool is a cache of reusable database connections. It allows serving many HTTP requests using few database connections. Every request to an :doc:`API resource <api>` borrows a connection from the pool to start a :doc:`transaction <transactions>`.
|
||||
|
||||
Minimizing connections is paramount to performance. Each PostgreSQL connection creates a process, having too many can exhaust available resources.
|
||||
|
||||
Connection String
|
||||
-----------------
|
||||
|
||||
For connecting to the database, the pool requires a connection string. You can configure it using :ref:`db-uri`.
|
||||
|
||||
.. _pool_growth_limit:
|
||||
.. _dyn_conn_pool:
|
||||
|
||||
Dynamic Connection Pool
|
||||
-----------------------
|
||||
|
||||
To conserve system resources, PostgREST uses a dynamic connection pool. This enables the number of connections in the pool to increase and decrease depending on request traffic.
|
||||
|
||||
- If all the connections are being used, a new connection is added. The pool can grow until it reaches the :ref:`db-pool` size. Note that it’s pointless to set this higher than the ``max_connections`` setting in your database.
|
||||
- If a connection is unused for a period of time (:ref:`db-pool-max-idletime`), it will be released.
|
||||
|
||||
Connection lifetime
|
||||
-------------------
|
||||
|
||||
Long-lived PostgreSQL connections can consume considerable memory (see `here <https://www.postgresql.org/message-id/CAFj8pRCQN2B2vrVMH1-bd-8xtzjytWR%2BAjZ%2BMCj9J2wPxKPa9Q%40mail.gmail.com>`_ for more details).
|
||||
Under a busy system, the :ref:`db-pool-max-idletime` won't be reached and the connection pool can be full of long-lived connections.
|
||||
|
||||
To avoid this problem and save resources, a connection max lifetime (:ref:`db-pool-max-lifetime`) is enforced.
|
||||
After the max lifetime is reached, connections from the pool will be released and new ones will be created. This doesn't affect running requests, only unused connections will be released.
|
||||
|
||||
Acquisition Timeout
|
||||
-------------------
|
||||
|
||||
If all the available connections in the pool are busy, an HTTP request will wait until reaching a timeout (:ref:`db-pool-acquisition-timeout`).
|
||||
|
||||
If the request reaches the timeout, it will be aborted with the following response:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 504 Gateway Timeout
|
||||
|
||||
{"code":"PGRST003",
|
||||
"details":null,
|
||||
"hint":null,
|
||||
"message":"Timed out acquiring connection from connection pool."}
|
||||
|
||||
.. important::
|
||||
|
||||
Getting this error message is an indicator of a performance issue. To solve it, you can:
|
||||
|
||||
- Reduce your queries execution time.
|
||||
|
||||
- Check the request :ref:`explain_plan` to tune your query, this usually means adding indexes.
|
||||
|
||||
- Reduce the amount of requests.
|
||||
|
||||
- Reduce write requests. Do :ref:`bulk_insert` (or :ref:`upsert`) instead of inserting rows one by one.
|
||||
- Reduce read requests. Use :ref:`resource_embedding`. Combine unrelated data into a single request using custom database views or functions.
|
||||
- Use :ref:`s_procs` for combining read and write logic into a single request.
|
||||
|
||||
- Increase the :ref:`db-pool` size.
|
||||
|
||||
- Not a panacea since connections can't grow infinitely. Try the previous recommendations before this.
|
||||
|
||||
.. _automatic_recovery:
|
||||
|
||||
Automatic Recovery
|
||||
------------------
|
||||
|
||||
The server will retry reconnecting to the database if connection loss happens.
|
||||
|
||||
- It will retry forever with exponential backoff, with a maximum backoff time of 32 seconds between retries. Each of these attempts are :ref:`logged <pgrst_logging>`.
|
||||
- It will only stop retrying if the server deems the error to be fatal. This can be a password authentication failure or an internal error.
|
||||
- The retries happen immediately after a connection loss, if :ref:`db-channel-enabled` is set to true (the default). Otherwise they'll happen once a request arrives.
|
||||
- To ensure a valid state, the server reloads the :ref:`schema_cache` and :ref:`configuration` when recovering.
|
||||
- To notify the client of the next retry, the server sends a ``503 Service Unavailable`` status with the ``Retry-After: x`` header. Where ``x`` is the number of seconds programmed for the next retry.
|
||||
- Automatic recovery can be disabled by setting :ref:`db-pool-automatic-recovery` to ``false``.
|
||||
|
||||
.. _external_connection_poolers:
|
||||
|
||||
Using External Connection Poolers
|
||||
---------------------------------
|
||||
|
||||
It's possible to use external connection poolers, such as PgBouncer. Session pooling is compatible, while transaction pooling requires :ref:`db-prepared-statements` set to ``false``. Statement pooling is not compatible with PostgREST.
|
||||
|
||||
Also set :ref:`db-channel-enabled` to ``false`` since ``LISTEN`` is not compatible with transaction pooling. Although it should not give any errors if left enabled.
|
||||
|
||||
.. note::
|
||||
|
||||
It’s not recommended to use an external connection pooler. `Our benchmarks <https://github.com/PostgREST/postgrest/issues/2294#issuecomment-1139148672>`_ indicate it provides much lower performance than PostgREST built-in pool.
|
||||
@@ -0,0 +1,427 @@
|
||||
.. _error_source:
|
||||
|
||||
Errors
|
||||
######
|
||||
|
||||
PostgREST error messages follow the PostgreSQL error structure. It includes ``MESSAGE``, ``DETAIL``, ``HINT``, ``ERRCODE`` and will add an HTTP status code to the response.
|
||||
|
||||
Errors from PostgreSQL
|
||||
======================
|
||||
|
||||
PostgREST will forward errors coming from PostgreSQL. For instance, on a failed constraint:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
POST /projects HTTP/1.1
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 400 Bad Request
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
|
||||
{
|
||||
"code": "23502",
|
||||
"details": "Failing row contains (null, foo, null).",
|
||||
"hint": null,
|
||||
"message": "null value in column \"id\" of relation \"projects\" violates not-null constraint"
|
||||
}
|
||||
|
||||
.. _status_codes:
|
||||
|
||||
HTTP Status Codes
|
||||
-----------------
|
||||
|
||||
PostgREST translates `PostgreSQL error codes <https://www.postgresql.org/docs/current/errcodes-appendix.html>`_ into HTTP status as follows:
|
||||
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| PostgreSQL error code(s) | HTTP status | Error description |
|
||||
+==========================+=========================+=================================+
|
||||
| 08* | 503 | pg connection err |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 09* | 500 | triggered action exception |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 0L* | 403 | invalid grantor |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 0P* | 403 | invalid role specification |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 23503 | 409 | foreign key violation |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 23505 | 409 | uniqueness violation |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 25006 | 405 | read only sql transaction |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 25* | 500 | invalid transaction state |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 28* | 403 | invalid auth specification |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 2D* | 500 | invalid transaction termination |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 38* | 500 | external routine exception |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 39* | 500 | external routine invocation |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 3B* | 500 | savepoint exception |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 40* | 500 | transaction rollback |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 53* | 503 | insufficient resources |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 54* | 413 | too complex |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 55* | 500 | obj not in prerequisite state |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 57* | 500 | operator intervention |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 58* | 500 | system error |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| F0* | 500 | config file error |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| HV* | 500 | foreign data wrapper error |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| P0001 | 400 | default code for "raise" |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| P0* | 500 | PL/pgSQL error |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| XX* | 500 | internal error |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 42883 | 404 | undefined function |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 42P01 | 404 | undefined table |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| 42501 | | if authenticated 403, | insufficient privileges |
|
||||
| | | else 401 | |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
| other | 400 | |
|
||||
+--------------------------+-------------------------+---------------------------------+
|
||||
|
||||
Errors from PostgREST
|
||||
=====================
|
||||
|
||||
Errors that come from PostgREST itself maintain the same structure but differ in the ``PGRST`` prefix in the ``code`` field. For instance, when querying a function that does not exist in the :doc:`schema cache <schema_cache>`:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
POST /rpc/nonexistent_function HTTP/1.1
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 404 Not Found
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"hint": "...",
|
||||
"details": null
|
||||
"code": "PGRST202",
|
||||
"message": "Could not find the api.nonexistent_function() function in the schema cache"
|
||||
}
|
||||
|
||||
|
||||
.. _pgrst_errors:
|
||||
|
||||
PostgREST Error Codes
|
||||
---------------------
|
||||
|
||||
PostgREST error codes have the form ``PGRSTgxx``.
|
||||
|
||||
- ``PGRST`` is the prefix that differentiates the error from a PostgreSQL error.
|
||||
- ``g`` is the error group
|
||||
- ``xx`` is the error identifier in the group.
|
||||
|
||||
.. _pgrst0**:
|
||||
|
||||
Group 0 - Connection
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Related to the connection with the database.
|
||||
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| Code | HTTP status | Description |
|
||||
+===============+=============+=============================================================+
|
||||
| .. _pgrst000: | 503 | Could not connect with the database due to an incorrect |
|
||||
| | | :ref:`db-uri` or due to the PostgreSQL service not running. |
|
||||
| PGRST000 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst001: | 503 | Could not connect with the database due to an internal |
|
||||
| | | error. |
|
||||
| PGRST001 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst002: | 503 | Could not connect with the database when building the |
|
||||
| | | :doc:`Schema Cache <schema_cache>` |
|
||||
| PGRST002 | | due to the PostgreSQL service not running. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst003: | 504 | The request timed out waiting for a pool connection |
|
||||
| | | to be available. See :ref:`db-pool-acquisition-timeout`. |
|
||||
| PGRST003 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
|
||||
.. _pgrst1**:
|
||||
|
||||
Group 1 - Api Request
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Related to the HTTP request elements.
|
||||
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| Code | HTTP status | Description |
|
||||
+===============+=============+=============================================================+
|
||||
| .. _pgrst100: | 400 | Parsing error in the query string parameter. |
|
||||
| | | See :ref:`h_filter`, :ref:`operators` and :ref:`ordering`. |
|
||||
| PGRST100 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst101: | 405 | For :ref:`functions <s_procs>`, only ``GET`` and ``POST`` |
|
||||
| | | verbs are allowed. Any other verb will throw this error. |
|
||||
| PGRST101 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst102: | 400 | An invalid request body was sent(e.g. an empty body or |
|
||||
| | | malformed JSON). |
|
||||
| PGRST102 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst103: | 416 | An invalid range was specified for :ref:`limits`. |
|
||||
| | | |
|
||||
| PGRST103 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst105: | 405 | An invalid :ref:`PUT <upsert_put>` request was done |
|
||||
| | | |
|
||||
| PGRST105 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst106: | 406 | The schema specified when |
|
||||
| | | :ref:`switching schemas <multiple-schemas>` is not present |
|
||||
| PGRST106 | | in the :ref:`db-schemas` configuration variable. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst107: | 415 | The ``Content-Type`` sent in the request is invalid. |
|
||||
| | | |
|
||||
| PGRST107 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst108: | 400 | The filter is applied to a embedded resource that is not |
|
||||
| | | specified in the ``select`` part of the query string. |
|
||||
| PGRST108 | | See :ref:`embed_filters`. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst109: | 400 | Restricting a Deletion or an Update using limits must |
|
||||
| | | include the ordering of a unique column. |
|
||||
| PGRST109 | | See :ref:`limited_update_delete`. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst110: | 400 | When restricting a Deletion or an Update using limits |
|
||||
| | | modifies more rows than the maximum specified in the limit. |
|
||||
| PGRST110 | | See :ref:`limited_update_delete`. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst111: | 500 | An invalid ``response.headers`` was set. |
|
||||
| | | See :ref:`guc_resp_hdrs`. |
|
||||
| PGRST111 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst112: | 500 | The status code must be a positive integer. |
|
||||
| | | See :ref:`guc_resp_status`. |
|
||||
| PGRST112 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst114: | 400 | For an :ref:`UPSERT using PUT <upsert_put>`, when |
|
||||
| | | :ref:`limits and offsets <limits>` are used. |
|
||||
| PGRST114 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst115: | 400 | For an :ref:`UPSERT using PUT <upsert_put>`, when the |
|
||||
| | | primary key in the query string and the body are different. |
|
||||
| PGRST115 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst116: | 406 | More than 1 or no items where returned when requesting |
|
||||
| | | a singular response. See :ref:`singular_plural`. |
|
||||
| PGRST116 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst117: | 405 | The HTTP verb used in the request in not supported. |
|
||||
| | | |
|
||||
| PGRST117 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst118: | 400 | Could not order the result using the related table because |
|
||||
| | | there is no many-to-one or one-to-one relationship between |
|
||||
| PGRST118 | | them. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst119: | 400 | Could not use the spread operator on the related table |
|
||||
| | | because there is no many-to-one or one-to-one relationship |
|
||||
| PGRST119 | | between them. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst120: | 400 | An embedded resource can only be filtered using the |
|
||||
| | | ``is.null`` or ``not.is.null`` :ref:`operators <operators>`.|
|
||||
| PGRST120 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst121: | 400 | PostgREST can't parse the JSON objects in RAISE |
|
||||
| | | ``PGRST`` error. See :ref:`raise headers <raise_headers>`. |
|
||||
| PGRST121 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst122: | 400 | Invalid preferences found in ``Prefer`` header with |
|
||||
| | | ``Prefer: handling=strict``. See :ref:`prefer_handling`. |
|
||||
| PGRST122 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
|
||||
.. _pgrst2**:
|
||||
|
||||
Group 2 - Schema Cache
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Related to a :ref:`stale schema cache <stale_schema>`. Most of the time, these errors are solved by :ref:`reloading the schema cache <schema_reloading>`.
|
||||
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| Code | HTTP status | Description |
|
||||
+===============+=============+=============================================================+
|
||||
| .. _pgrst200: | 400 | Caused by stale foreign key relationships, otherwise any of |
|
||||
| | | the embedding resources or the relationship itself may not |
|
||||
| PGRST200 | | exist in the database. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst201: | 300 | An ambiguous embedding request was made. |
|
||||
| | | See :ref:`complex_rels`. |
|
||||
| PGRST201 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst202: | 404 | Caused by a stale function signature, otherwise |
|
||||
| | | the function may not exist in the database. |
|
||||
| PGRST202 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst203: | 300 | Caused by requesting overloaded functions with the same |
|
||||
| | | argument names but different types, or by using a ``POST`` |
|
||||
| PGRST203 | | verb to request overloaded functions with a ``JSON`` or |
|
||||
| | | ``JSONB`` type unnamed parameter. The solution is to rename |
|
||||
| | | the function or add/modify the names of the arguments. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst204: | 400 | Caused when the :ref:`column specified <specify_columns>` |
|
||||
| | | in the ``columns`` query parameter is not found. |
|
||||
| PGRST204 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
|
||||
.. _pgrst3**:
|
||||
|
||||
Group 3 - JWT
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Related to the authentication process using JWT. You can follow the :ref:`tut1` for an example on how to implement authentication and the :doc:`Authentication page <auth>` for more information on this process.
|
||||
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| Code | HTTP status | Description |
|
||||
+===============+=============+=============================================================+
|
||||
| .. _pgrst300: | 500 | A :ref:`JWT secret <jwt-secret>` is missing from the |
|
||||
| | | configuration. |
|
||||
| PGRST300 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst301: | 401 | Any error related to the verification of the JWT, |
|
||||
| | | which means that the JWT provided is invalid in some way. |
|
||||
| PGRST301 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| .. _pgrst302: | 401 | Attempted to do a request without |
|
||||
| | | :ref:`authentication <client_auth>` when the anonymous role |
|
||||
| PGRST302 | | is disabled by not setting it in :ref:`db-anon-role`. |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
|
||||
.. The Internal Errors Group X** is always at the end
|
||||
|
||||
.. _pgrst_X**:
|
||||
|
||||
Group X - Internal
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Internal errors. If you encounter any of these, you may have stumbled on a PostgREST bug, please `open an issue <https://github.com/PostgREST/postgrest/issues>`_ and we'll be glad to fix it.
|
||||
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
| Code | HTTP status | Description |
|
||||
+===============+=============+=============================================================+
|
||||
| .. _pgrstX00: | 500 | Internal errors related to the library used for connecting |
|
||||
| | | to the database. |
|
||||
| PGRSTX00 | | |
|
||||
+---------------+-------------+-------------------------------------------------------------+
|
||||
|
||||
Custom Errors
|
||||
=============
|
||||
|
||||
You can customize the errors by using the `RAISE statement <https://www.postgresql.org/docs/current/plpgsql-errors-and-messages.html#PLPGSQL-STATEMENTS-RAISE>`_ on functions.
|
||||
|
||||
.. _raise_error:
|
||||
|
||||
RAISE errors with HTTP Status Codes
|
||||
-----------------------------------
|
||||
|
||||
Custom status codes can be done by raising SQL exceptions inside :ref:`functions <s_procs>`. For instance, here's a saucy function that always responds with an error:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
CREATE OR REPLACE FUNCTION just_fail() RETURNS void
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
RAISE EXCEPTION 'I refuse!'
|
||||
USING DETAIL = 'Pretty simple',
|
||||
HINT = 'There is nothing you can do.';
|
||||
END
|
||||
$$;
|
||||
|
||||
Calling the function returns HTTP 400 with the body
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"message":"I refuse!",
|
||||
"details":"Pretty simple",
|
||||
"hint":"There is nothing you can do.",
|
||||
"code":"P0001"
|
||||
}
|
||||
|
||||
One way to customize the HTTP status code is by raising particular exceptions according to the PostgREST :ref:`error to status code mapping <status_codes>`. For example, :code:`RAISE insufficient_privilege` will respond with HTTP 401/403 as appropriate.
|
||||
|
||||
For even greater control of the HTTP status code, raise an exception of the ``PTxyz`` type. For instance to respond with HTTP 402, raise ``PT402``:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
RAISE sqlstate 'PT402' using
|
||||
message = 'Payment Required',
|
||||
detail = 'Quota exceeded',
|
||||
hint = 'Upgrade your plan';
|
||||
|
||||
Returns:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 402 Payment Required
|
||||
Content-Type: application/json; charset=utf-8
|
||||
|
||||
{
|
||||
"message": "Payment Required",
|
||||
"details": "Quota exceeded",
|
||||
"hint": "Upgrade your plan",
|
||||
"code": "PT402"
|
||||
}
|
||||
|
||||
.. _raise_headers:
|
||||
|
||||
Add HTTP Headers with RAISE
|
||||
---------------------------
|
||||
|
||||
For full control over headers and status you can raise a ``PGRST`` SQLSTATE error. You can achieve this by adding the ``code``, ``message``, ``detail`` and ``hint`` in the postgresql error message field as a JSON object. Here, the ``details`` and ``hint`` are optional. Similarly, the ``status`` and ``headers`` must be added to the SQL error detail field as a JSON object. For instance:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
RAISE sqlstate 'PGRST' USING
|
||||
message = '{"code":"123","message":"Payment Required","details":"Quota exceeded","hint":"Upgrade your plan"}',
|
||||
detail = '{"status":402,"headers":{"X-Powered-By":"Nerd Rage"}}';
|
||||
|
||||
Returns:
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 402 Payment Required
|
||||
Content-Type: application/json; charset=utf-8
|
||||
X-Powered-By: Nerd Rage
|
||||
|
||||
{
|
||||
"message": "Payment Required",
|
||||
"details": "Quota exceeded",
|
||||
"hint": "Upgrade your plan",
|
||||
"code": "123"
|
||||
}
|
||||
|
||||
|
||||
For non standard HTTP status, you can optionally add ``status_text`` to describe the status code. For status code ``419`` the detail field may look like this:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
detail = '{"status":419,"status_text":"Page Expired","headers":{"X-Powered-By":"Nerd Rage"}}';
|
||||
|
||||
If PostgREST can't parse the JSON objects ``message`` and ``detail``, it will throw a ``PGRST121`` error. See :ref:`Errors from PostgREST<pgrst1**>`.
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
.. _health_check:
|
||||
|
||||
Health Check
|
||||
############
|
||||
|
||||
You can enable a health check to verify if PostgREST is available for client requests. Also to check the status of its internal state.
|
||||
|
||||
To do this, set the configuration variable :ref:`admin-server-port` to the port number of your preference. Two endpoints ``live`` and ``ready`` will then be available.
|
||||
|
||||
The ``live`` endpoint verifies if PostgREST is running on its configured port. A request will return ``200 OK`` if PostgREST is alive or ``503`` otherwise.
|
||||
|
||||
The ``ready`` endpoint also checks the state of both the Database Connection and the :ref:`schema_cache`. A request will return ``200 OK`` if it is ready or ``503`` if not.
|
||||
|
||||
For instance, to verify if PostgREST is running at ``localhost:3000`` while the ``admin-server-port`` is set to ``3001``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl -I "http://localhost:3001/live"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
|
||||
If you have a machine with multiple network interfaces and multiple PostgREST instances in the same port, you need to specify a unique :ref:`hostname <server-host>` in the configuration of each PostgREST instance for the health check to work correctly. Don't use the special values(``!4``, ``*``, etc) in this case because the health check could report a false positive.
|
||||
@@ -0,0 +1,273 @@
|
||||
.. _observability:
|
||||
|
||||
Observability
|
||||
#############
|
||||
|
||||
.. _pgrst_logging:
|
||||
|
||||
Logging
|
||||
-------
|
||||
|
||||
PostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, and HTTP response status.
|
||||
|
||||
.. code::
|
||||
|
||||
127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 - "" "curl/7.64.0"
|
||||
127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 - "" "curl/7.64.0"
|
||||
|
||||
For diagnostic information about the server itself, PostgREST logs to ``stderr``.
|
||||
|
||||
.. code::
|
||||
|
||||
12/Jun/2021:17:47:39 -0500: Starting PostgREST 11.1.0...
|
||||
12/Jun/2021:17:47:39 -0500: Attempting to connect to the database...
|
||||
12/Jun/2021:17:47:39 -0500: Listening on port 3000
|
||||
12/Jun/2021:17:47:39 -0500: Connection successful
|
||||
12/Jun/2021:17:47:39 -0500: Config re-loaded
|
||||
12/Jun/2021:17:47:40 -0500: Schema cache loaded
|
||||
|
||||
.. note::
|
||||
|
||||
When running it in an SSH session you must detach it from stdout or it will be terminated when the session closes. The easiest technique is redirecting the output to a log file or to the syslog:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
ssh foo@example.com \
|
||||
'postgrest foo.conf </dev/null >/var/log/postgrest.log 2>&1 &'
|
||||
|
||||
# another option is to pipe the output into "logger -t postgrest"
|
||||
|
||||
Currently PostgREST doesn't log the SQL commands executed against the underlying database.
|
||||
|
||||
Database Logs
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
To find the SQL operations, you can watch the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below.
|
||||
|
||||
Find :code:`postgresql.conf` inside your PostgreSQL data directory (to find that, issue the command :code:`show data_directory;`). Either find the settings scattered throughout the file and change them to the following values, or append this block of code to the end of the configuration file.
|
||||
|
||||
.. code:: sql
|
||||
|
||||
# send logs where the collector can access them
|
||||
log_destination = "stderr"
|
||||
|
||||
# collect stderr output to log files
|
||||
logging_collector = on
|
||||
|
||||
# save logs in pg_log/ under the pg data directory
|
||||
log_directory = "pg_log"
|
||||
|
||||
# (optional) new log file per day
|
||||
log_filename = "postgresql-%Y-%m-%d.log"
|
||||
|
||||
# log every kind of SQL statement
|
||||
log_statement = "all"
|
||||
|
||||
Restart the database and watch the log file in real-time to understand how HTTP requests are being translated into SQL commands.
|
||||
|
||||
.. note::
|
||||
|
||||
On Docker you can enable the logs by using a custom ``init.sh``:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
#!/bin/sh
|
||||
echo "log_statement = 'all'" >> /var/lib/postgresql/data/postgresql.conf
|
||||
|
||||
After that you can start the container and check the logs with ``docker logs``.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
docker run -v "$(pwd)/init.sh":"/docker-entrypoint-initdb.d/init.sh" -d postgres
|
||||
docker logs -f <container-id>
|
||||
|
||||
Server Version
|
||||
--------------
|
||||
|
||||
When debugging a problem it's important to verify the running PostgREST version. There are three ways to do this:
|
||||
|
||||
- Look for the :code:`Server` HTTP response header that is returned on every request.
|
||||
|
||||
.. code::
|
||||
|
||||
HEAD /users HTTP/1.1
|
||||
|
||||
Server: postgrest/11.0.1
|
||||
|
||||
- Query ``application_name`` on `pg_stat_activity <https://www.postgresql.org/docs/current/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW>`_.
|
||||
|
||||
.. code-block:: psql
|
||||
|
||||
select distinct application_name
|
||||
from pg_stat_activity
|
||||
where application_name ilike '%postgrest%';
|
||||
|
||||
application_name
|
||||
------------------------------
|
||||
PostgREST 11.1.0
|
||||
|
||||
.. important::
|
||||
|
||||
- The server sets the `fallback_application_name <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-FALLBACK-APPLICATION-NAME>`_ to the connection URI for this query to work. To override the value set ``application_name`` on the connection string.
|
||||
- The version will only be set if it's a valid URI (`RFC 3986 <https://datatracker.ietf.org/doc/html/rfc3986>`_). This means any special characters must be urlencoded.
|
||||
- The version will not be set if the connection string is in `keyword/value format <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-KEYWORD-VALUE>`_.
|
||||
|
||||
- The ``stderr`` logs also contain the version, as noted on :ref:`pgrst_logging`.
|
||||
|
||||
.. _trace_header:
|
||||
|
||||
Trace Header
|
||||
------------
|
||||
|
||||
You can enable tracing HTTP requests by setting :ref:`server-trace-header`. Specify the set header in the request, and the server will include it in the response.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
server-trace-header = "X-Request-Id"
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/users" \
|
||||
-H "X-Request-Id: 123"
|
||||
|
||||
.. code::
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
X-Request-Id: 123
|
||||
|
||||
.. _server-timing_header:
|
||||
|
||||
Server-Timing Header
|
||||
--------------------
|
||||
|
||||
You can enable the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>`_ header by setting :ref:`server-timing-enabled` on.
|
||||
This header communicates metrics of the different phases in the request-response cycle.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/users" -i
|
||||
|
||||
.. code::
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
|
||||
Server-Timing: jwt;dur=14.9, parse;dur=71.1, plan;dur=109.0, transaction;dur=353.2, response;dur=4.4
|
||||
|
||||
- All the durations (``dur``) are in milliseconds.
|
||||
- The ``jwt`` stage is when :ref:`jwt_impersonation` is done. This duration can be lowered with :ref:`jwt_caching`.
|
||||
- On the ``parse`` stage, the :ref:`url_grammar` is parsed.
|
||||
- On the ``plan`` stage, the :ref:`schema_cache` is used to generate the :ref:`main_query` of the transaction.
|
||||
- The ``transaction`` stage corresponds to the database transaction. See :ref:`transactions`.
|
||||
- The ``response`` stage is where the response status and headers are computed.
|
||||
|
||||
.. note::
|
||||
|
||||
We're working on lowering the duration of the ``parse`` and ``plan`` stages on https://github.com/PostgREST/postgrest/issues/2816.
|
||||
|
||||
.. _explain_plan:
|
||||
|
||||
Execution plan
|
||||
--------------
|
||||
|
||||
You can get the `EXPLAIN execution plan <https://www.postgresql.org/docs/current/sql-explain.html>`_ of a request by adding the ``Accept: application/vnd.pgrst.plan`` header.
|
||||
This is enabled by :ref:`db-plan-enabled` (false by default).
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/users?select=name&order=id" \
|
||||
-H "Accept: application/vnd.pgrst.plan"
|
||||
|
||||
.. code-block:: psql
|
||||
|
||||
Aggregate (cost=73.65..73.68 rows=1 width=112)
|
||||
-> Index Scan using users_pkey on users (cost=0.15..60.90 rows=850 width=36)
|
||||
|
||||
The output of the plan is generated in ``text`` format by default but you can change it to JSON by using the ``+json`` suffix.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/users?select=name&order=id" \
|
||||
-H "Accept: application/vnd.pgrst.plan+json"
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"Plan": {
|
||||
"Node Type": "Aggregate",
|
||||
"Strategy": "Plain",
|
||||
"Partial Mode": "Simple",
|
||||
"Parallel Aware": false,
|
||||
"Async Capable": false,
|
||||
"Startup Cost": 73.65,
|
||||
"Total Cost": 73.68,
|
||||
"Plan Rows": 1,
|
||||
"Plan Width": 112,
|
||||
"Plans": [
|
||||
{
|
||||
"Node Type": "Index Scan",
|
||||
"Parent Relationship": "Outer",
|
||||
"Parallel Aware": false,
|
||||
"Async Capable": false,
|
||||
"Scan Direction": "Forward",
|
||||
"Index Name": "users_pkey",
|
||||
"Relation Name": "users",
|
||||
"Alias": "users",
|
||||
"Startup Cost": 0.15,
|
||||
"Total Cost": 60.90,
|
||||
"Plan Rows": 850,
|
||||
"Plan Width": 36
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
By default the plan is assumed to generate the JSON representation of a resource(``application/json``), but you can obtain the plan for the :ref:`different representations that PostgREST supports <res_format>` by adding them to the ``for`` parameter. For instance, to obtain the plan for a ``text/xml``, you would use ``Accept: application/vnd.pgrst.plan; for="text/xml``.
|
||||
|
||||
The other available parameters are ``analyze``, ``verbose``, ``settings``, ``buffers`` and ``wal``, which correspond to the `EXPLAIN command options <https://www.postgresql.org/docs/current/sql-explain.html>`_. To use the ``analyze`` and ``wal`` parameters for example, you would add them like ``Accept: application/vnd.pgrst.plan; options=analyze|wal``.
|
||||
|
||||
Note that akin to the EXPLAIN command, the changes will be committed when using the ``analyze`` option. To avoid this, you can use the :ref:`db-tx-end` and the ``Prefer: tx=rollback`` header.
|
||||
|
||||
Securing the Execution Plan
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
It's recommended to only activate :ref:`db-plan-enabled` on testing environments since it reveals internal database details.
|
||||
However, if you choose to use it in production you can add a :ref:`db-pre-request` to filter the requests that can use this feature.
|
||||
|
||||
For example, to only allow requests from an IP address to get the execution plans:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
-- Assuming a proxy(Nginx, Cloudflare, etc) passes an "X-Forwarded-For" header(https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For)
|
||||
create or replace function filter_plan_requests()
|
||||
returns void as $$
|
||||
declare
|
||||
headers json := current_setting('request.headers', true)::json;
|
||||
client_ip text := coalesce(headers->>'x-forwarded-for', '');
|
||||
accept text := coalesce(headers->>'accept', '');
|
||||
begin
|
||||
if accept like 'application/vnd.pgrst.plan%' and client_ip != '144.96.121.73' then
|
||||
raise insufficient_privilege using
|
||||
message = 'Not allowed to use application/vnd.pgrst.plan';
|
||||
end if;
|
||||
end; $$ language plpgsql;
|
||||
|
||||
-- set this function on your postgrest.conf
|
||||
-- db-pre-request = filter_plan_requests
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<script type="text/javascript">
|
||||
let hash = window.location.hash;
|
||||
|
||||
const redirects = {
|
||||
'#health_check': 'health_check.html',
|
||||
};
|
||||
|
||||
let willRedirectTo = redirects[hash];
|
||||
|
||||
if (willRedirectTo) {
|
||||
window.location.href = willRedirectTo;
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,173 @@
|
||||
.. _schema_cache:
|
||||
|
||||
Schema Cache
|
||||
============
|
||||
|
||||
Some PostgREST features need metadata from the database schema. Getting this metadata requires expensive queries. To avoid repeating this work, PostgREST uses a schema cache.
|
||||
|
||||
+--------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| Feature | Required Metadata |
|
||||
+============================================+===============================================================================+
|
||||
| :ref:`resource_embedding` | Foreign key constraints |
|
||||
+--------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| :ref:`Stored Functions <s_procs>` | Function signature (parameters, return type, volatility and |
|
||||
| | `overloading <https://www.postgresql.org/docs/current/xfunc-overload.html>`_) |
|
||||
+--------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| :ref:`Upserts <upsert>` | Primary keys |
|
||||
+--------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| :ref:`Insertions <insert>` | Primary keys (optional: only if the Location header is requested) |
|
||||
+--------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| :ref:`OPTIONS requests <options_requests>` | View INSTEAD OF TRIGGERS and primary keys |
|
||||
+--------------------------------------------+-------------------------------------------------------------------------------+
|
||||
| :ref:`open-api` | Table columns, primary keys and foreign keys |
|
||||
+ +-------------------------------------------------------------------------------+
|
||||
| | View columns and INSTEAD OF TRIGGERS |
|
||||
+ +-------------------------------------------------------------------------------+
|
||||
| | Function signature |
|
||||
+--------------------------------------------+-------------------------------------------------------------------------------+
|
||||
|
||||
.. _stale_schema:
|
||||
|
||||
Stale Schema Cache
|
||||
------------------
|
||||
|
||||
One operational problem that comes with a cache is that it can go stale. This can happen for PostgREST when you make changes to the metadata before mentioned. Requests that depend on the metadata will fail.
|
||||
|
||||
You can solve this by reloading the cache manually or automatically.
|
||||
|
||||
.. _schema_reloading:
|
||||
|
||||
Schema Cache Reloading
|
||||
----------------------
|
||||
|
||||
To manually reload the cache without restarting the PostgREST server, send a SIGUSR1 signal to the server process.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
killall -SIGUSR1 postgrest
|
||||
|
||||
|
||||
For docker you can do:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
docker kill -s SIGUSR1 <container>
|
||||
|
||||
# or in docker-compose
|
||||
docker-compose kill -s SIGUSR1 <service>
|
||||
|
||||
There’s no downtime when reloading the schema cache. The reloading will happen on a background thread while serving requests.
|
||||
|
||||
.. _schema_reloading_notify:
|
||||
|
||||
Reloading with NOTIFY
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PostgREST also allows you to reload its schema cache through PostgreSQL `NOTIFY <https://www.postgresql.org/docs/current/sql-notify.html>`_.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
NOTIFY pgrst, 'reload schema'
|
||||
|
||||
This is useful in environments where you can’t send the SIGUSR1 Unix Signal. Like on cloud managed containers or on Windows systems.
|
||||
|
||||
The ``pgrst`` notification channel is enabled by default. For configuring the channel, see :ref:`db-channel` and :ref:`db-channel-enabled`.
|
||||
|
||||
.. _auto_schema_reloading:
|
||||
|
||||
Automatic Schema Cache Reloading
|
||||
--------------------------------
|
||||
|
||||
You can do automatic schema cache reloading in a pure SQL way and forget about stale schema cache errors. For this use an `event trigger <https://www.postgresql.org/docs/current/event-trigger-definition.html>`_ and ``NOTIFY``.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
-- Create an event trigger function
|
||||
CREATE OR REPLACE FUNCTION pgrst_watch() RETURNS event_trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
BEGIN
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- This event trigger will fire after every ddl_command_end event
|
||||
CREATE EVENT TRIGGER pgrst_watch
|
||||
ON ddl_command_end
|
||||
EXECUTE PROCEDURE pgrst_watch();
|
||||
|
||||
Now, whenever the ``pgrst_watch`` trigger fires, PostgREST will auto-reload the schema cache.
|
||||
|
||||
To disable auto reloading, drop the trigger.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
DROP EVENT TRIGGER pgrst_watch
|
||||
|
||||
Finer-Grained Event Trigger
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can refine the previous event trigger to only react to the events relevant to the schema cache. This also prevents unnecessary
|
||||
reloading when creating temporary tables inside functions.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
-- watch CREATE and ALTER
|
||||
CREATE OR REPLACE FUNCTION pgrst_ddl_watch() RETURNS event_trigger AS $$
|
||||
DECLARE
|
||||
cmd record;
|
||||
BEGIN
|
||||
FOR cmd IN SELECT * FROM pg_event_trigger_ddl_commands()
|
||||
LOOP
|
||||
IF cmd.command_tag IN (
|
||||
'CREATE SCHEMA', 'ALTER SCHEMA'
|
||||
, 'CREATE TABLE', 'CREATE TABLE AS', 'SELECT INTO', 'ALTER TABLE'
|
||||
, 'CREATE FOREIGN TABLE', 'ALTER FOREIGN TABLE'
|
||||
, 'CREATE VIEW', 'ALTER VIEW'
|
||||
, 'CREATE MATERIALIZED VIEW', 'ALTER MATERIALIZED VIEW'
|
||||
, 'CREATE FUNCTION', 'ALTER FUNCTION'
|
||||
, 'CREATE TRIGGER'
|
||||
, 'CREATE TYPE', 'ALTER TYPE'
|
||||
, 'CREATE RULE'
|
||||
, 'COMMENT'
|
||||
)
|
||||
-- don't notify in case of CREATE TEMP table or other objects created on pg_temp
|
||||
AND cmd.schema_name is distinct from 'pg_temp'
|
||||
THEN
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
END IF;
|
||||
END LOOP;
|
||||
END; $$ LANGUAGE plpgsql;
|
||||
|
||||
-- watch DROP
|
||||
CREATE OR REPLACE FUNCTION pgrst_drop_watch() RETURNS event_trigger AS $$
|
||||
DECLARE
|
||||
obj record;
|
||||
BEGIN
|
||||
FOR obj IN SELECT * FROM pg_event_trigger_dropped_objects()
|
||||
LOOP
|
||||
IF obj.object_type IN (
|
||||
'schema'
|
||||
, 'table'
|
||||
, 'foreign table'
|
||||
, 'view'
|
||||
, 'materialized view'
|
||||
, 'function'
|
||||
, 'trigger'
|
||||
, 'type'
|
||||
, 'rule'
|
||||
)
|
||||
AND obj.is_temporary IS false -- no pg_temp objects
|
||||
THEN
|
||||
NOTIFY pgrst, 'reload schema';
|
||||
END IF;
|
||||
END LOOP;
|
||||
END; $$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE EVENT TRIGGER pgrst_ddl_watch
|
||||
ON ddl_command_end
|
||||
EXECUTE PROCEDURE pgrst_ddl_watch();
|
||||
|
||||
CREATE EVENT TRIGGER pgrst_drop_watch
|
||||
ON sql_drop
|
||||
EXECUTE PROCEDURE pgrst_drop_watch();
|
||||
@@ -0,0 +1,364 @@
|
||||
.. _transactions:
|
||||
|
||||
Transactions
|
||||
============
|
||||
|
||||
After :ref:`user_impersonation`, every request to an :doc:`API resource <api>` runs inside a transaction. The sequence of the transaction is as follows:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
START TRANSACTION; -- <Access Mode> <Isolation Level>
|
||||
-- <Transaction-scoped settings>
|
||||
-- <Main Query>
|
||||
END; -- <Transaction End>
|
||||
|
||||
.. _access_mode:
|
||||
|
||||
Access Mode
|
||||
-----------
|
||||
|
||||
The access mode determines whether the transaction can modify the database or not. There are 2 possible values: READ ONLY and READ WRITE.
|
||||
|
||||
Modifying the database inside READ ONLY transactions is not possible. PostgREST uses this fact to enforce HTTP semantics in GET and HEAD requests. Consider the following:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
CREATE SEQUENCE callcounter_count START 1;
|
||||
|
||||
CREATE VIEW callcounter AS
|
||||
SELECT nextval('callcounter_count');
|
||||
|
||||
Since the ``callcounter`` view modifies the sequence, calling it with GET or HEAD will result in an error:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/callcounter"
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 405 Method Not Allowed
|
||||
|
||||
{"code":"25006","details":null,"hint":null,"message":"cannot execute nextval() in a read-only transaction"}
|
||||
|
||||
Access Mode on Tables and Views
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The access mode on :ref:`tables_views` is determined by the HTTP method.
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 1
|
||||
|
||||
* - HTTP Method
|
||||
- Access Mode
|
||||
* - GET, HEAD
|
||||
- READ ONLY
|
||||
* - POST, PATCH, PUT, DELETE
|
||||
- READ WRITE
|
||||
|
||||
Access Mode on Functions
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
:ref:`s_procs` additionally depend on the function `volatility <https://www.postgresql.org/docs/current/xfunc-volatility.html>`_.
|
||||
|
||||
.. list-table::
|
||||
:header-rows: 2
|
||||
|
||||
* -
|
||||
- Access Mode
|
||||
-
|
||||
-
|
||||
* - HTTP Method
|
||||
- VOLATILE
|
||||
- STABLE
|
||||
- IMMUTABLE
|
||||
* - GET, HEAD
|
||||
- READ ONLY
|
||||
- READ ONLY
|
||||
- READ ONLY
|
||||
* - POST
|
||||
- READ WRITE
|
||||
- READ ONLY
|
||||
- READ ONLY
|
||||
|
||||
.. note::
|
||||
|
||||
- The volatility marker is a promise about the behavior of the function. PostgreSQL will let you mark a function that modifies the database as ``IMMUTABLE`` or ``STABLE`` without failure. But, because of the READ ONLY transaction the function will fail under PostgREST.
|
||||
- The :ref:`options_requests` method doesn't start a transaction, so it's not relevant here.
|
||||
|
||||
.. _isolation_lvl:
|
||||
|
||||
Isolation Level
|
||||
---------------
|
||||
|
||||
Every transaction uses the PostgreSQL default isolation level: READ COMMITTED. Unless you modify `default_transaction_isolation <https://www.postgresql.org/docs/15/runtime-config-client.html#GUC-DEFAULT-TRANSACTION-ISOLATION>`_ for an impersonated role or function.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
ALTER ROLE webuser SET default_transaction_isolation TO 'repeatable read';
|
||||
|
||||
Every ``webuser`` gets its queries executed with ``default_transaction_isolation`` set to REPEATABLE READ.
|
||||
|
||||
Or to change the isolation level per function call.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
CREATE OR REPLACE FUNCTION myfunc()
|
||||
RETURNS text as $$
|
||||
SELECT 'hello';
|
||||
$$
|
||||
LANGUAGE SQL
|
||||
SET default_transaction_isolation TO 'serializable';
|
||||
|
||||
.. _tx_settings:
|
||||
|
||||
Transaction-Scoped Settings
|
||||
---------------------------
|
||||
|
||||
PostgREST uses settings tied to the transaction lifetime. These can be used to get data about the HTTP request. Or to modify the HTTP response.
|
||||
|
||||
You can get these with ``current_setting``
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
-- request settings use the ``request.`` prefix.
|
||||
SELECT
|
||||
current_setting('request.<setting>', true);
|
||||
|
||||
And you can set them with ``set_config``
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
-- response settings use the ``response.`` prefix.
|
||||
SELECT
|
||||
set_config('response.<setting>', 'value1' ,true);
|
||||
|
||||
.. _guc_req_headers_cookies_claims:
|
||||
|
||||
Request Headers, Cookies and JWT claims
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PostgREST stores the headers, cookies and headers as JSON. To get them:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
-- To get all the headers sent in the request
|
||||
SELECT current_setting('request.headers', true)::json;
|
||||
|
||||
-- To get a single header, you can use JSON arrow operators
|
||||
SELECT current_setting('request.headers', true)::json->>'user-agent';
|
||||
|
||||
-- value of sessionId in a cookie
|
||||
SELECT current_setting('request.cookies', true)::json->>'sessionId';
|
||||
|
||||
-- value of the email claim in a jwt
|
||||
SELECT current_setting('request.jwt.claims', true)::json->>'email';
|
||||
|
||||
.. important::
|
||||
|
||||
- The headers names are lowercased. e.g. If the request sends ``User-Agent: x`` this will be obtainable as ``current_setting('request.headers', true)::json->>'user-agent'``.
|
||||
- The ``role`` in ``request.jwt.claims`` defaults to the value of :ref:`db-anon-role`.
|
||||
- Settings don't become NULL after the transaction is committed, instead they're set to a an empty string ``''``.
|
||||
|
||||
+ This is considered expected behavior by PostgreSQL. For more details, see `this discussion <https://www.postgresql.org/message-id/flat/CAB_pDVVa84w7hXhzvyuMTb8f5kKV3bee_p9QTZZ58Rg7zYM7sw%40mail.gmail.com>`_.
|
||||
+ To avoid this inconsistency, you can create a wrapper function like:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
CREATE FUNCTION my_current_setting(text) RETURNS text
|
||||
LANGUAGE SQL AS $$
|
||||
SELECT nullif(current_setting($1, true), '');
|
||||
$$;
|
||||
|
||||
.. _guc_req_path_method:
|
||||
|
||||
Request Path and Method
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The path and method are stored as ``text``.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
SELECT current_setting('request.path', true);
|
||||
|
||||
SELECT current_setting('request.method', true);
|
||||
|
||||
Request Role and Search Path
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Because of :ref:`user_impersonation`, PostgREST sets the standard ``role``. You can get this in different ways:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
SELECT current_role;
|
||||
|
||||
SELECT current_user;
|
||||
|
||||
SELECT current_setting('role', true);
|
||||
|
||||
Additionally it also sets the ``search_path`` based on :ref:`db-schemas` and :ref:`db-extra-search-path`.
|
||||
|
||||
.. _guc_resp_hdrs:
|
||||
|
||||
Response Headers
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
You can set ``response.headers`` to add headers to the HTTP response. For instance, this statement would add caching headers to the response:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
-- tell client to cache response for two days
|
||||
|
||||
SELECT set_config('response.headers',
|
||||
'[{"Cache-Control": "public"}, {"Cache-Control": "max-age=259200"}]', true);
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=utf-8
|
||||
Cache-Control: no-cache, no-store, must-revalidate
|
||||
|
||||
Notice that the ``response.headers`` should be set to an *array* of single-key objects rather than a single multiple-key object. This is because headers such as ``Cache-Control`` or ``Set-Cookie`` need repeating when setting many values. An object would not allow the repeated key.
|
||||
|
||||
.. note::
|
||||
|
||||
PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overriden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you use :ref:`custom_media`.
|
||||
|
||||
.. _guc_resp_status:
|
||||
|
||||
Response Status Code
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can set the ``response.status`` to override the default status code PostgREST provides. For instance, the following function would replace the default ``200`` status code.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create or replace function teapot() returns json as $$
|
||||
begin
|
||||
perform set_config('response.status', '418', true);
|
||||
return json_build_object('message', 'The requested entity body is short and stout.',
|
||||
'hint', 'Tip it over and pour it out.');
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/rpc/teapot" -i
|
||||
|
||||
.. code-block:: http
|
||||
|
||||
HTTP/1.1 418 I'm a teapot
|
||||
|
||||
{
|
||||
"message" : "The requested entity body is short and stout.",
|
||||
"hint" : "Tip it over and pour it out."
|
||||
}
|
||||
|
||||
If the status code is standard, PostgREST will complete the status message(**I'm a teapot** in this example).
|
||||
|
||||
.. _impersonated_settings:
|
||||
|
||||
Impersonated Role Settings
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PostgreSQL applies the connection role (:ref:`authenticator <roles>`) settings. Additionally, PostgREST applies the :ref:`impersonated roles <user_impersonation>` settings as transaction-scoped settings.
|
||||
This allows finer-grained control over actions made by a role.
|
||||
|
||||
For example, consider `statement_timeout <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT>`__. It allows you to abort any statement that takes more than a specified time. It is disabled by default.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
ALTER ROLE authenticator SET statement_timeout TO '10s';
|
||||
ALTER ROLE anonymous SET statement_timeout TO '1s';
|
||||
|
||||
With the above settings, all users get a global statement timeout of 10 seconds and :ref:`anonymous <roles>` users get a timeout of 1 second.
|
||||
|
||||
Settings with privileged context
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Settings that have a context which requires privileges won't be applied by default. This is so we don't cause permission errors.
|
||||
For more details see `Understanding Postgres Parameter Context <https://www.enterprisedb.com/blog/understanding-postgres-parameter-context>`_.
|
||||
|
||||
However, starting from PostgreSQL 15, you can grant privileges for these settings with:
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
GRANT SET ON PARAMETER <setting> TO <authenticator>;
|
||||
|
||||
Function Settings
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
In addition to :ref:`impersonated_settings`, PostgREST will also apply function settings as transaction-scoped settings. This allows functions settings to override
|
||||
the impersonated and connection role settings.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
CREATE OR REPLACE FUNCTION myfunc()
|
||||
RETURNS void as $$
|
||||
SELECT pg_sleep(3); -- simulating some long-running process
|
||||
$$
|
||||
LANGUAGE SQL
|
||||
SET statement_timeout TO '4s';
|
||||
|
||||
When calling the above function (see :ref:`s_procs`), the statement timeout will be 4 seconds.
|
||||
|
||||
.. note::
|
||||
|
||||
Currently, only ``statement_timeout`` is applied for functions.
|
||||
|
||||
.. _main_query:
|
||||
|
||||
Main query
|
||||
----------
|
||||
|
||||
The main query is generated by requesting :ref:`tables_views` or :ref:`s_procs`. All generated queries use prepared statements (:ref:`db-prepared-statements`).
|
||||
|
||||
.. _tx_end:
|
||||
|
||||
Transaction End
|
||||
---------------
|
||||
|
||||
If the transaction doesn't fail, it will always end in a COMMIT. Unless :ref:`db-tx-end` is configured to ROLLBACK in any case or conditionally with the :ref:`prefer_tx`. This is useful for testing purposes.
|
||||
|
||||
Aborting transactions
|
||||
---------------------
|
||||
|
||||
Any database failure(like a failed constraint) will result in a rollback of the transaction. You can also :ref:`RAISE an error inside a function <raise_error>` to cause a rollback.
|
||||
|
||||
.. _pre-request:
|
||||
|
||||
Pre-Request
|
||||
-----------
|
||||
|
||||
The pre-request is a function that can run after the :ref:`tx_settings` are set and before the :ref:`main_query`. It's enabled with :ref:`db-pre-request`.
|
||||
|
||||
This provides an opportunity to modify settings or raise an exception to prevent the request from completing.
|
||||
|
||||
.. _pre_req_headers:
|
||||
|
||||
Setting headers via pre-request
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
As an example, let's add some cache headers for all requests that come from an Internet Explorer(6 or 7) browser.
|
||||
|
||||
.. code-block:: postgresql
|
||||
|
||||
create or replace function custom_headers()
|
||||
returns void as $$
|
||||
declare
|
||||
user_agent text := current_setting('request.headers', true)::json->>'user-agent';
|
||||
begin
|
||||
if user_agent similar to '%MSIE (6.0|7.0)%' then
|
||||
perform set_config('response.headers',
|
||||
'[{"Cache-Control": "no-cache, no-store, must-revalidate"}]', false);
|
||||
end if;
|
||||
end; $$ language plpgsql;
|
||||
|
||||
-- set this function on postgrest.conf
|
||||
-- db-pre-request = custom_headers
|
||||
|
||||
Now when you make a GET request to a table or view, you'll get the cache headers.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl "http://localhost:3000/people" -i \
|
||||
-H "User-Agent: Mozilla/4.01 (compatible; MSIE 6.0; Windows NT 5.1)"
|
||||
@@ -0,0 +1,8 @@
|
||||
docutils==0.17.1
|
||||
sphinx-copybutton
|
||||
sphinx-intl
|
||||
sphinx-rtd-theme>=0.5.1
|
||||
sphinx-tabs>=3.2.0
|
||||
sphinx>=5.0.2
|
||||
sphinxext-opengraph==0.9.0
|
||||
urllib3==2.0.7
|
||||
@@ -0,0 +1,46 @@
|
||||
.. tabs::
|
||||
|
||||
.. group-tab:: macOS
|
||||
|
||||
You can install PostgREST from the `Homebrew official repo <https://formulae.brew.sh/formula/postgrest>`_.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
brew install postgrest
|
||||
|
||||
.. group-tab:: FreeBSD
|
||||
|
||||
You can install PostgREST from the `official ports <https://www.freshports.org/www/hs-postgrest>`_.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pkg install hs-postgrest
|
||||
|
||||
.. group-tab:: Linux
|
||||
|
||||
.. tabs::
|
||||
|
||||
.. tab:: Arch Linux
|
||||
|
||||
You can install PostgREST from the `community repo <https://archlinux.org/packages/extra/x86_64/postgrest/>`_.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pacman -S postgrest
|
||||
|
||||
.. tab:: Nix
|
||||
|
||||
You can install PostgREST from nixpkgs.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
nix-env -i haskellPackages.postgrest
|
||||
|
||||
.. group-tab:: Windows
|
||||
|
||||
You can install PostgREST using `Chocolatey <https://community.chocolatey.org/packages/postgrest>`_ or `Scoop <https://github.com/ScoopInstaller/Scoop>`_.
|
||||
|
||||
.. code:: bash
|
||||
|
||||
choco install postgrest
|
||||
scoop install postgrest
|
||||
@@ -0,0 +1,22 @@
|
||||
let
|
||||
docs =
|
||||
import ./default.nix;
|
||||
|
||||
inherit (docs) pkgs;
|
||||
in
|
||||
pkgs.mkShell {
|
||||
name = "postgrest-docs";
|
||||
|
||||
buildInputs = [
|
||||
docs.build
|
||||
docs.serve
|
||||
docs.spellcheck
|
||||
docs.dictcheck
|
||||
docs.linkcheck
|
||||
docs.check
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
export HISTFILE=.history
|
||||
'';
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
.. _tut0:
|
||||
|
||||
Tutorial 0 - Get it Running
|
||||
===========================
|
||||
|
||||
:author: `begriffs <https://github.com/begriffs>`_
|
||||
|
||||
Welcome to PostgREST! In this pre-tutorial we're going to get things running so you can create your first simple API.
|
||||
|
||||
PostgREST is a standalone web server which turns a PostgreSQL database into a RESTful API. It serves an API that is customized based on the structure of the underlying database.
|
||||
|
||||
.. image:: ../_static/tuts/tut0-request-flow.png
|
||||
|
||||
To make an API we'll simply be building a database. All the endpoints and permissions come from database objects like tables, views, roles, and stored procedures. These tutorials will cover a number of common scenarios and how to model them in the database.
|
||||
|
||||
By the end of this tutorial you'll have a working database, PostgREST server, and a simple single-user todo list API.
|
||||
|
||||
Step 1. Relax, we'll help
|
||||
-------------------------
|
||||
|
||||
As you begin the tutorial, pop open the project `chat room <https://gitter.im/begriffs/postgrest>`_ in another tab. There are a nice group of people active in the project and we'll help you out if you get stuck.
|
||||
|
||||
Step 2. Install PostgreSQL
|
||||
--------------------------
|
||||
|
||||
If you're already familiar with using PostgreSQL and have it installed on your system you can use the existing installation (see :ref:`pg-dependency` for minimum requirements). For this tutorial we'll describe how to use the database in Docker because database configuration is otherwise too complicated for a simple tutorial.
|
||||
|
||||
If Docker is not installed, you can get it `here <https://www.docker.com/get-started>`_. Next, let's pull and start the database image:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo docker run --name tutorial -p 5433:5432 \
|
||||
-e POSTGRES_PASSWORD=mysecretpassword \
|
||||
-d postgres
|
||||
|
||||
This will run the Docker instance as a daemon and expose port 5433 to the host system so that it looks like an ordinary PostgreSQL server to the rest of the system.
|
||||
|
||||
Step 3. Install PostgREST
|
||||
-------------------------
|
||||
|
||||
Using a Package Manager
|
||||
~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can use your OS package manager to install PostgREST.
|
||||
|
||||
.. include:: ../shared/installation.rst
|
||||
|
||||
Then, try running it with:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
postgrest -h
|
||||
|
||||
It should print the help page with its version and the available options.
|
||||
|
||||
Downloading a Pre-Built Binary
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
PostgREST is also distributed as a single binary, with versions compiled for major distributions of macOS, Windows, Linux and FreeBSD. Visit the `latest release <https://github.com/PostgREST/postgrest/releases/latest>`_ for a list of downloads. In the event that your platform is not among those already pre-built, see :ref:`build_source` for instructions how to build it yourself. Also let us know to add your platform in the next release.
|
||||
|
||||
The pre-built binaries for download are :code:`.tar.xz` compressed files (except Windows which is a zip file). To extract the binary, go into the terminal and run
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# download from https://github.com/PostgREST/postgrest/releases/latest
|
||||
|
||||
tar xJf postgrest-<version>-<platform>.tar.xz
|
||||
|
||||
The result will be a file named simply :code:`postgrest` (or :code:`postgrest.exe` on Windows). At this point try running it with
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
./postgrest -h
|
||||
|
||||
If everything is working correctly it will print out its version and the available options. You can continue to run this binary from where you downloaded it, or copy it to a system directory like :code:`/usr/local/bin` on Linux so that you will be able to run it from any directory.
|
||||
|
||||
.. note::
|
||||
|
||||
PostgREST requires libpq, the PostgreSQL C library, to be installed on your system. Without the library you'll get an error like "error while loading shared libraries: libpq.so.5." Here's how to fix it:
|
||||
|
||||
.. raw:: html
|
||||
|
||||
<p>
|
||||
<details>
|
||||
<summary>Ubuntu or Debian</summary>
|
||||
<div class="highlight-bash"><div class="highlight">
|
||||
<pre>sudo apt-get install libpq-dev</pre>
|
||||
</div></div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Fedora, CentOS, or Red Hat</summary>
|
||||
<div class="highlight-bash"><div class="highlight">
|
||||
<pre>sudo yum install postgresql-libs</pre>
|
||||
</div></div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>macOS</summary>
|
||||
<div class="highlight-bash"><div class="highlight">
|
||||
<pre>brew install postgresql</pre>
|
||||
</div></div>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Windows</summary>
|
||||
<p>All of the DLL files that are required to run PostgREST are available in the windows installation of PostgreSQL server.
|
||||
Once installed they are found in the BIN folder, e.g: C:\Program Files\PostgreSQL\10\bin. Add this directory to your PATH
|
||||
variable. Run the following from an administrative command prompt (adjusting the actual BIN path as necessary of course)
|
||||
<pre>setx /m PATH "%PATH%;C:\Program Files\PostgreSQL\10\bin"</pre>
|
||||
</p>
|
||||
</details>
|
||||
</p>
|
||||
|
||||
Step 4. Create Database for API
|
||||
-------------------------------
|
||||
|
||||
Connect to the SQL console (psql) inside the container. To do so, run this from your command line:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo docker exec -it tutorial psql -U postgres
|
||||
|
||||
You should see the psql command prompt:
|
||||
|
||||
::
|
||||
|
||||
psql (9.6.3)
|
||||
Type "help" for help.
|
||||
|
||||
postgres=#
|
||||
|
||||
The first thing we'll do is create a `named schema <https://www.postgresql.org/docs/current/ddl-schemas.html>`_ for the database objects which will be exposed in the API. We can choose any name we like, so how about "api." Execute this and the other SQL statements inside the psql prompt you started.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create schema api;
|
||||
|
||||
Our API will have one endpoint, :code:`/todos`, which will come from a table.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create table api.todos (
|
||||
id serial primary key,
|
||||
done boolean not null default false,
|
||||
task text not null,
|
||||
due timestamptz
|
||||
);
|
||||
|
||||
insert into api.todos (task) values
|
||||
('finish tutorial 0'), ('pat self on back');
|
||||
|
||||
Next make a role to use for anonymous web requests. When a request comes in, PostgREST will switch into this role in the database to run queries.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create role web_anon nologin;
|
||||
|
||||
grant usage on schema api to web_anon;
|
||||
grant select on api.todos to web_anon;
|
||||
|
||||
The :code:`web_anon` role has permission to access things in the :code:`api` schema, and to read rows in the :code:`todos` table.
|
||||
|
||||
It's a good practice to create a dedicated role for connecting to the database, instead of using the highly privileged ``postgres`` role. So we'll do that, name the role ``authenticator`` and also grant it the ability to switch to the ``web_anon`` role :
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
create role authenticator noinherit login password 'mysecretpassword';
|
||||
grant web_anon to authenticator;
|
||||
|
||||
|
||||
Now quit out of psql; it's time to start the API!
|
||||
|
||||
.. code-block:: psql
|
||||
|
||||
\q
|
||||
|
||||
Step 5. Run PostgREST
|
||||
---------------------
|
||||
|
||||
PostgREST can use a configuration file to tell it how to connect to the database. Create a file :code:`tutorial.conf` with this inside:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
db-uri = "postgres://authenticator:mysecretpassword@localhost:5433/postgres"
|
||||
db-schemas = "api"
|
||||
db-anon-role = "web_anon"
|
||||
|
||||
The configuration file has other :ref:`options <configuration>`, but this is all we need.
|
||||
If you are not using Docker, make sure that your port number is correct and replace `postgres` with the name of the database where you added the todos table.
|
||||
|
||||
Now run the server:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Running postgrest installed from a package manager
|
||||
postgrest tutorial.conf
|
||||
|
||||
# Running postgrest binary
|
||||
./postgrest tutorial.conf
|
||||
|
||||
You should see
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Listening on port 3000
|
||||
Attempting to connect to the database...
|
||||
Connection successful
|
||||
|
||||
It's now ready to serve web requests. There are many nice graphical API exploration tools you can use, but for this tutorial we'll use :code:`curl` because it's likely to be installed on your system already. Open a new terminal (leaving the one open that PostgREST is running inside). Try doing an HTTP request for the todos.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl http://localhost:3000/todos
|
||||
|
||||
The API replies:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"done": false,
|
||||
"task": "finish tutorial 0",
|
||||
"due": null
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"done": false,
|
||||
"task": "pat self on back",
|
||||
"due": null
|
||||
}
|
||||
]
|
||||
|
||||
With the current role permissions, anonymous requests have read-only access to the :code:`todos` table. If we try to add a new todo we are not able.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl http://localhost:3000/todos -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"task": "do bad thing"}'
|
||||
|
||||
Response is 401 Unauthorized:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"hint": null,
|
||||
"details": null,
|
||||
"code": "42501",
|
||||
"message": "permission denied for table todos"
|
||||
}
|
||||
|
||||
There we have it, a basic API on top of the database! In the next tutorials we will see how to extend the example with more sophisticated user access controls, and more tables and queries.
|
||||
|
||||
Now that you have PostgREST running, try the next tutorial, :ref:`tut1`
|
||||
@@ -0,0 +1,262 @@
|
||||
.. _tut1:
|
||||
|
||||
Tutorial 1 - The Golden Key
|
||||
===========================
|
||||
|
||||
:author: `begriffs <https://github.com/begriffs>`_
|
||||
|
||||
In :ref:`tut0` we created a read-only API with a single endpoint to list todos. There are many directions we can go to make this API more interesting, but one good place to start would be allowing some users to change data in addition to reading it.
|
||||
|
||||
Step 1. Add a Trusted User
|
||||
--------------------------
|
||||
|
||||
The previous tutorial created a :code:`web_anon` role in the database with which to execute anonymous web requests. Let's make a role called :code:`todo_user` for users who authenticate with the API. This role will have the authority to do anything to the todo list.
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
-- run this in psql using the database created
|
||||
-- in the previous tutorial
|
||||
|
||||
create role todo_user nologin;
|
||||
grant todo_user to authenticator;
|
||||
|
||||
grant usage on schema api to todo_user;
|
||||
grant all on api.todos to todo_user;
|
||||
grant usage, select on sequence api.todos_id_seq to todo_user;
|
||||
|
||||
Step 2. Make a Secret
|
||||
---------------------
|
||||
|
||||
Clients authenticate with the API using JSON Web Tokens. These are JSON objects which are cryptographically signed using a secret known to only us and the server. Because clients do not know this secret, they cannot tamper with the contents of their tokens. PostgREST will detect counterfeit tokens and will reject them.
|
||||
|
||||
Let's create a secret and provide it to PostgREST. Think of a nice long one, or use a tool to generate it. **Your secret must be at least 32 characters long.**
|
||||
|
||||
.. note::
|
||||
|
||||
Unix tools can generate a nice secret for you:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Allow "tr" to process non-utf8 byte sequences
|
||||
export LC_CTYPE=C
|
||||
|
||||
# Read random bytes keeping only alphanumerics and add the secret to the configuration file
|
||||
echo "jwt-secret = \"$(< /dev/urandom tr -dc A-Za-z0-9 | head -c32)\"" >> tutorial.conf
|
||||
|
||||
|
||||
Check that the :code:`tutorial.conf` (created in the previous tutorial) has the secret set in :code:`jwt-secret`:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# THE SECRET MUST BE AT LEAST 32 CHARS LONG
|
||||
cat tutorial.conf
|
||||
|
||||
If the PostgREST server is still running from the previous tutorial, restart it to load the updated configuration file.
|
||||
|
||||
Step 3. Sign a Token
|
||||
--------------------
|
||||
|
||||
Ordinarily your own code in the database or in another server will create and sign authentication tokens, but for this tutorial we will make one "by hand." Go to `jwt.io <https://jwt.io/#debugger-io>`_ and fill in the fields like this:
|
||||
|
||||
.. figure:: ../_static/tuts/tut1-jwt-io.png
|
||||
:alt: jwt.io interface
|
||||
|
||||
How to create a token at https://jwt.io
|
||||
|
||||
**Remember to fill in the secret you generated rather than the word "secret".** After you have filled in the secret and payload, the encoded data on the left will update. Copy the encoded token.
|
||||
|
||||
.. note::
|
||||
|
||||
While the token may look well obscured, it's easy to reverse engineer the payload. The token is merely signed, not encrypted, so don't put things inside that you don't want a determined client to see.
|
||||
|
||||
Step 4. Make a Request
|
||||
----------------------
|
||||
|
||||
Back in the terminal, let's use :code:`curl` to add a todo. The request will include an HTTP header containing the authentication token.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export TOKEN="<paste token here>"
|
||||
|
||||
curl http://localhost:3000/todos -X POST \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"task": "learn how to auth"}'
|
||||
|
||||
And now we have completed all three items in our todo list, so let's set :code:`done` to true for them all with a :code:`PATCH` request.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl http://localhost:3000/todos -X PATCH \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"done": true}'
|
||||
|
||||
A request for the todos shows three of them, and all completed.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl http://localhost:3000/todos
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"done": true,
|
||||
"task": "finish tutorial 0",
|
||||
"due": null
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"done": true,
|
||||
"task": "pat self on back",
|
||||
"due": null
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"done": true,
|
||||
"task": "learn how to auth",
|
||||
"due": null
|
||||
}
|
||||
]
|
||||
|
||||
Step 5. Add Expiration
|
||||
----------------------
|
||||
|
||||
Currently our authentication token is valid for all eternity. The server, as long as it continues using the same JWT secret, will honor the token.
|
||||
|
||||
It's better policy to include an expiration timestamp for tokens using the :code:`exp` claim. This is one of two JWT claims that PostgREST treats specially.
|
||||
|
||||
+--------------+----------------------------------------------------------------+
|
||||
| Claim | Interpretation |
|
||||
+==============+================================================================+
|
||||
| :code:`role` | The database role under which to execute SQL for API request |
|
||||
+--------------+----------------------------------------------------------------+
|
||||
| :code:`exp` | Expiration timestamp for token, expressed in "Unix epoch time" |
|
||||
+--------------+----------------------------------------------------------------+
|
||||
|
||||
.. note::
|
||||
|
||||
Epoch time is defined as the number of seconds that have elapsed since 00:00:00 Coordinated Universal Time (UTC), January 1st 1970, minus the number of leap seconds that have taken place since then.
|
||||
|
||||
To observe expiration in action, we'll add an :code:`exp` claim of five minutes in the future to our previous token. First find the epoch value of five minutes from now. In psql run this:
|
||||
|
||||
.. code-block:: postgres
|
||||
|
||||
select extract(epoch from now() + '5 minutes'::interval) :: integer;
|
||||
|
||||
Go back to jwt.io and change the payload to
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"role": "todo_user",
|
||||
"exp": 123456789
|
||||
}
|
||||
|
||||
**NOTE**: Don't forget to change the dummy epoch value :code:`123456789` in the snippet above to the epoch value returned by the psql command.
|
||||
|
||||
Copy the updated token as before, and save it as a new environment variable.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export NEW_TOKEN="<paste new token>"
|
||||
|
||||
Try issuing this request in curl before and after the expiration time:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
curl http://localhost:3000/todos \
|
||||
-H "Authorization: Bearer $NEW_TOKEN"
|
||||
|
||||
After expiration, the API returns HTTP 401 Unauthorized:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"hint": null,
|
||||
"details": null,
|
||||
"code": "PGRST301",
|
||||
"message": "JWT expired"
|
||||
}
|
||||
|
||||
Bonus Topic: Immediate Revocation
|
||||
---------------------------------
|
||||
|
||||
Even with token expiration there are times when you may want to immediately revoke access for a specific token. For instance, suppose you learn that a disgruntled employee is up to no good and his token is still valid.
|
||||
|
||||
To revoke a specific token we need a way to tell it apart from others. Let's add a custom :code:`email` claim that matches the email of the client issued the token.
|
||||
|
||||
Go ahead and make a new token with the payload
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"role": "todo_user",
|
||||
"email": "disgruntled@mycompany.com"
|
||||
}
|
||||
|
||||
Save it to an environment variable:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
export WAYWARD_TOKEN="<paste new token>"
|
||||
|
||||
PostgREST allows us to specify a stored procedure to run during attempted authentication. The function can do whatever it likes, including raising an exception to terminate the request.
|
||||
|
||||
First make a new schema and add the function:
|
||||
|
||||
.. code-block:: plpgsql
|
||||
|
||||
create schema auth;
|
||||
grant usage on schema auth to web_anon, todo_user;
|
||||
|
||||
create or replace function auth.check_token() returns void
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if current_setting('request.jwt.claims', true)::json->>'email' =
|
||||
'disgruntled@mycompany.com' then
|
||||
raise insufficient_privilege
|
||||
using hint = 'Nope, we are on to you';
|
||||
end if;
|
||||
end
|
||||
$$;
|
||||
|
||||
Next update :code:`tutorial.conf` and specify the new function:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
# add this line to tutorial.conf
|
||||
|
||||
db-pre-request = "auth.check_token"
|
||||
|
||||
Restart PostgREST for the change to take effect. Next try making a request with our original token and then with the revoked one.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# this request still works
|
||||
|
||||
curl http://localhost:3000/todos -X PATCH \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"done": true}'
|
||||
|
||||
# this one is rejected
|
||||
|
||||
curl http://localhost:3000/todos -X PATCH \
|
||||
-H "Authorization: Bearer $WAYWARD_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"task": "AAAHHHH!", "done": false}'
|
||||
|
||||
The server responds with 403 Forbidden:
|
||||
|
||||
.. code-block:: json
|
||||
|
||||
{
|
||||
"hint": "Nope, we are on to you",
|
||||
"details": null,
|
||||
"code": "42501",
|
||||
"message": "insufficient_privilege"
|
||||
}
|
||||