diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f62d5ec24d..2249eded55 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -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 diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000000..2e3eacc143 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,50 @@ +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@v23 + - run: nix-env -f docs/default.nix -iA build + - run: postgrest-docs-build + + spellcheck: + name: Run spellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v23 + - 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@v23 + - 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@v23 + - run: nix-env -f docs/default.nix -iA linkcheck + - run: postgrest-docs-linkcheck + diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..0d7162c8f7 --- /dev/null +++ b/.readthedocs.yaml @@ -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" diff --git a/.test b/.test new file mode 100644 index 0000000000..e69de29bb2 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..be92f4d7a9 --- /dev/null +++ b/CONTRIBUTING.md @@ -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 diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000000..503e5d5247 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,8 @@ +_build +Pipfile.lock +*.aux +*.log +_diagrams/db.pdf +misspellings +unuseddict +.history diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000000..29050fb9a6 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,20 @@ +# 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. diff --git a/docs/_diagrams/README.md b/docs/_diagrams/README.md new file mode 100644 index 0000000000..7014ae3780 --- /dev/null +++ b/docs/_diagrams/README.md @@ -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 +``` diff --git a/docs/_diagrams/boxoffice.er b/docs/_diagrams/boxoffice.er new file mode 100644 index 0000000000..7d6b0a1c77 --- /dev/null +++ b/docs/_diagrams/boxoffice.er @@ -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 diff --git a/docs/_diagrams/db.tex b/docs/_diagrams/db.tex new file mode 100644 index 0000000000..f4580f8915 --- /dev/null +++ b/docs/_diagrams/db.tex @@ -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} diff --git a/docs/_diagrams/employees.er b/docs/_diagrams/employees.er new file mode 100644 index 0000000000..4632f5bac5 --- /dev/null +++ b/docs/_diagrams/employees.er @@ -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 diff --git a/docs/_diagrams/film.er b/docs/_diagrams/film.er new file mode 100644 index 0000000000..cc54c4800c --- /dev/null +++ b/docs/_diagrams/film.er @@ -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 diff --git a/docs/_diagrams/orders.er b/docs/_diagrams/orders.er new file mode 100644 index 0000000000..86f0805e95 --- /dev/null +++ b/docs/_diagrams/orders.er @@ -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 diff --git a/docs/_diagrams/premieres.er b/docs/_diagrams/premieres.er new file mode 100644 index 0000000000..6099e74fd4 --- /dev/null +++ b/docs/_diagrams/premieres.er @@ -0,0 +1,16 @@ +entity {font: "FreeSans"} +relationship {font: "FreeMono"} + +[Premieres] +*id +location +date ++film_id + +[Films] +*id ++director_id +title +`...` + +Premieres *--1 Films diff --git a/docs/_diagrams/presidents.er b/docs/_diagrams/presidents.er new file mode 100644 index 0000000000..ca7cf71ba1 --- /dev/null +++ b/docs/_diagrams/presidents.er @@ -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 diff --git a/docs/_diagrams/users.er b/docs/_diagrams/users.er new file mode 100644 index 0000000000..8ec011302a --- /dev/null +++ b/docs/_diagrams/users.er @@ -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 diff --git a/docs/_static/2ndquadrant.png b/docs/_static/2ndquadrant.png new file mode 100644 index 0000000000..3b6a755891 Binary files /dev/null and b/docs/_static/2ndquadrant.png differ diff --git a/docs/_static/boxoffice.png b/docs/_static/boxoffice.png new file mode 100644 index 0000000000..87249a6659 Binary files /dev/null and b/docs/_static/boxoffice.png differ diff --git a/docs/_static/code-build.webp b/docs/_static/code-build.webp new file mode 100644 index 0000000000..3024afc6b7 Binary files /dev/null and b/docs/_static/code-build.webp differ diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 0000000000..602ef678fc --- /dev/null +++ b/docs/_static/css/custom.css @@ -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; +} diff --git a/docs/_static/cybertec-new.png b/docs/_static/cybertec-new.png new file mode 100644 index 0000000000..15ec8d4abc Binary files /dev/null and b/docs/_static/cybertec-new.png differ diff --git a/docs/_static/cybertec.png b/docs/_static/cybertec.png new file mode 100644 index 0000000000..4bb395027f Binary files /dev/null and b/docs/_static/cybertec.png differ diff --git a/docs/_static/db.png b/docs/_static/db.png new file mode 100644 index 0000000000..a3dd2d85ab Binary files /dev/null and b/docs/_static/db.png differ diff --git a/docs/_static/employees.png b/docs/_static/employees.png new file mode 100644 index 0000000000..b21153cc5a Binary files /dev/null and b/docs/_static/employees.png differ diff --git a/docs/_static/empty.png b/docs/_static/empty.png new file mode 100644 index 0000000000..99fabe47fd Binary files /dev/null and b/docs/_static/empty.png differ diff --git a/docs/_static/favicon.ico b/docs/_static/favicon.ico new file mode 100644 index 0000000000..a9e16d3a8b Binary files /dev/null and b/docs/_static/favicon.ico differ diff --git a/docs/_static/film.png b/docs/_static/film.png new file mode 100644 index 0000000000..03b9b2b749 Binary files /dev/null and b/docs/_static/film.png differ diff --git a/docs/_static/gnuhost.png b/docs/_static/gnuhost.png new file mode 100644 index 0000000000..79a4c9d728 Binary files /dev/null and b/docs/_static/gnuhost.png differ diff --git a/docs/_static/how-tos/htmx-demo.gif b/docs/_static/how-tos/htmx-demo.gif new file mode 100644 index 0000000000..72e1a82dd6 Binary files /dev/null and b/docs/_static/how-tos/htmx-demo.gif differ diff --git a/docs/_static/how-tos/htmx-edit-delete.gif b/docs/_static/how-tos/htmx-edit-delete.gif new file mode 100644 index 0000000000..89ee58e00e Binary files /dev/null and b/docs/_static/how-tos/htmx-edit-delete.gif differ diff --git a/docs/_static/how-tos/htmx-insert.gif b/docs/_static/how-tos/htmx-insert.gif new file mode 100644 index 0000000000..9c678983e5 Binary files /dev/null and b/docs/_static/how-tos/htmx-insert.gif differ diff --git a/docs/_static/how-tos/htmx-simple.jpg b/docs/_static/how-tos/htmx-simple.jpg new file mode 100644 index 0000000000..e15d133495 Binary files /dev/null and b/docs/_static/how-tos/htmx-simple.jpg differ diff --git a/docs/_static/logo.png b/docs/_static/logo.png new file mode 100644 index 0000000000..7d23fffc4a Binary files /dev/null and b/docs/_static/logo.png differ diff --git a/docs/_static/neon.jpg b/docs/_static/neon.jpg new file mode 100644 index 0000000000..5f4819a248 Binary files /dev/null and b/docs/_static/neon.jpg differ diff --git a/docs/_static/oblivious.jpg b/docs/_static/oblivious.jpg new file mode 100644 index 0000000000..955e1a57d3 Binary files /dev/null and b/docs/_static/oblivious.jpg differ diff --git a/docs/_static/orders.png b/docs/_static/orders.png new file mode 100644 index 0000000000..0ac19aacd2 Binary files /dev/null and b/docs/_static/orders.png differ diff --git a/docs/_static/presidents.png b/docs/_static/presidents.png new file mode 100644 index 0000000000..09c4e6f791 Binary files /dev/null and b/docs/_static/presidents.png differ diff --git a/docs/_static/retool.png b/docs/_static/retool.png new file mode 100644 index 0000000000..abf26a1eff Binary files /dev/null and b/docs/_static/retool.png differ diff --git a/docs/_static/security-anon-choice.png b/docs/_static/security-anon-choice.png new file mode 100644 index 0000000000..ea02a237e6 Binary files /dev/null and b/docs/_static/security-anon-choice.png differ diff --git a/docs/_static/security-roles.png b/docs/_static/security-roles.png new file mode 100644 index 0000000000..f45ba8e9e1 Binary files /dev/null and b/docs/_static/security-roles.png differ diff --git a/docs/_static/supabase.png b/docs/_static/supabase.png new file mode 100644 index 0000000000..9c3686bd04 Binary files /dev/null and b/docs/_static/supabase.png differ diff --git a/docs/_static/tembo.png b/docs/_static/tembo.png new file mode 100644 index 0000000000..c3a3875176 Binary files /dev/null and b/docs/_static/tembo.png differ diff --git a/docs/_static/timescaledb.png b/docs/_static/timescaledb.png new file mode 100644 index 0000000000..d6403efa4c Binary files /dev/null and b/docs/_static/timescaledb.png differ diff --git a/docs/_static/tuts/tut0-request-flow.png b/docs/_static/tuts/tut0-request-flow.png new file mode 100644 index 0000000000..24f2986e81 Binary files /dev/null and b/docs/_static/tuts/tut0-request-flow.png differ diff --git a/docs/_static/tuts/tut1-jwt-io.png b/docs/_static/tuts/tut1-jwt-io.png new file mode 100644 index 0000000000..488b87f103 Binary files /dev/null and b/docs/_static/tuts/tut1-jwt-io.png differ diff --git a/docs/_static/users.png b/docs/_static/users.png new file mode 100644 index 0000000000..d94f097fc0 Binary files /dev/null and b/docs/_static/users.png differ diff --git a/docs/_static/win-err-dialog.png b/docs/_static/win-err-dialog.png new file mode 100644 index 0000000000..e60a71c450 Binary files /dev/null and b/docs/_static/win-err-dialog.png differ diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000000..494c1fca2b --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,314 @@ +# -*- 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 = "12.0" +# The full version, including alpha/beta/rc tags. +release = "12.0.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. +# " v 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 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" + +# 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", "/") diff --git a/docs/default.nix b/docs/default.nix new file mode 100644 index 0000000000..8da9118709 --- /dev/null +++ b/docs/default.nix @@ -0,0 +1,100 @@ +let + # Commit of the Nixpkgs repository that we want to use. + nixpkgsVersion = { + date = "2023-03-25"; + rev = "dbf5322e93bcc6cfc52268367a8ad21c09d76fea"; + tarballHash = "0lwk4v9dkvd28xpqch0b0jrac4xl9lwm6snrnzx8k5lby72kmkng"; + }; + + # 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 ]); +in +rec { + inherit pkgs; + + build = + pkgs.writeShellScriptBin "postgrest-docs-build" + '' + set -euo pipefail + cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/docs" + + # clean previous build, otherwise some errors might be supressed + rm -rf _build + + ${python}/bin/sphinx-build --color -W -b html -a -n . _build + ''; + + 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 + + ${python}/bin/python 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 + ''; +} diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst new file mode 100644 index 0000000000..efb68fc39b --- /dev/null +++ b/docs/ecosystem.rst @@ -0,0 +1,95 @@ +.. _community_tutorials: + +Community Tutorials +------------------- + +* `Building a Contacts List with PostgREST and Vue.js `_ - + 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 `_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service. + +* `PostgREST + PostGIS API tutorial 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 `_ - + 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 `_ - 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 `_ + +* `Build data-driven applications with Workers and PostgreSQL `_ - A tutorial on how to integrate with PostgREST and PostgreSQL using Cloudflare Workers. + +.. * `A poor man's API `_ - Shows how to integrate PostgREST with Apache APISIX as an alternative to Nginx. + +.. _templates: + +Templates +--------- + +* `compose-postgrest `_ - docker-compose setup with Nginx and HTML example +* `svelte-postgrest-template `_ - Svelte/SvelteKit, PostgREST, EveryLayout and social auth + +.. _eco_example_apps: + +Example Apps +------------ + +* `delibrium-postgrest `_ - example school API and front-end in Vue.js +* `ETH-transactions-storage `_ - indexer for Ethereum to get transaction list by ETH address +* `general `_ - example auth back-end +* `guild-operators `_ - example queries and functions that the Cardano Community uses for their Guild Operators' Repository +* `PostGUI `_ - React Material UI admin panel +* `prospector `_ - data warehouse and visualization platform + +.. _devops: + +DevOps +------ + +* `cloudgov-demo-postgrest `_ - demo for a federally-compliant REST API on cloud.gov +* `cloudstark/helm-charts `_ - helm chart to deploy PostgREST to a Kubernetes cluster via a Deployment and Service +* `jbkarle/postgrest `_ - helm chart with a demo database for development and test purposes +* `Limezest/postgrest-cloud-run `_ - expose a PostgreSQL database on Cloud SQL using Cloud Run +* `eyberg/postgrest `_ - run PostgREST as a Nanos unikernel + +.. _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 `_ - trigger webhooks from PostgreSQL's LISTEN/NOTIFY +* `pgsql-listen-exchange `_ - RabbitMQ +* `postgres-websockets `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY +* `postgresql2websocket `_ - Websockets + + +.. _eco_extensions: + +Extensions +---------- + +* `aiodata `_ - Python, event-based proxy and caching client. +* `pg-safeupdate `_ - prevent full-table updates or deletes +* `postgrest-node `_ - Run a PostgREST server in Node.js via npm module +* `PostgREST-writeAPI `_ - generate Nginx rewrite rules to fit an OpenAPI spec + +.. _clientside_libraries: + +Client-Side Libraries +--------------------- + +* `postgrest-csharp `_ - C# +* `postgrest-dart `_ - Dart +* `postgrest-ex `_ - Elixir +* `postgrest-go `_ - Go +* `postgrest-js `_ - TypeScript/JavaScript +* `postgrest-kt `_ - Kotlin +* `postgrest-py `_ - Python +* `postgrest-rs `_ - Rust +* `postgrest-swift `_ - Swift +* `redux-postgrest `_ - TypeScript/JS, client integrated with (React) Redux. +* `vue-postgrest `_ - Vue.js + diff --git a/docs/explanations/db_authz.rst b/docs/explanations/db_authz.rst new file mode 100644 index 0000000000..e818428398 --- /dev/null +++ b/docs/explanations/db_authz.rst @@ -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 `_ 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 `_). 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 `_ 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 ` 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 `_ 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 `_ 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; + diff --git a/docs/explanations/install.rst b/docs/explanations/install.rst new file mode 100644 index 0000000000..b2935a9375 --- /dev/null +++ b/docs/explanations/install.rst @@ -0,0 +1,217 @@ +.. _install: + +Installation +############ + +The release page has `pre-compiled binaries for macOS, Windows, Linux and FreeBSD `_ . +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\\bin`. See this `article `_ 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 `_ 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 `_. It will install any necessary Haskell dependencies on your system. + +* `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`. diff --git a/docs/explanations/nginx.rst b/docs/explanations/nginx.rst new file mode 100644 index 0000000000..ccbdebc47a --- /dev/null +++ b/docs/explanations/nginx.rst @@ -0,0 +1,109 @@ +.. _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 `_. Note that some Platforms as a Service like Heroku also add SSL automatically in their load balancer. + +Rate Limiting +------------- + +Nginx supports "leaky bucket" rate limiting (see `official docs `_). 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 ` 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 + +.. tabs:: + + .. code-tab:: http + + GET /people?id=eq.1 HTTP/1.1 + Accept: application/vnd.pgrst.object+json + + .. code-tab:: bash Curl + + 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 diff --git a/docs/explanations/schema_isolation.rst b/docs/explanations/schema_isolation.rst new file mode 100644 index 0000000000..342b15e8fa --- /dev/null +++ b/docs/explanations/schema_isolation.rst @@ -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 `_ (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 diff --git a/docs/how-tos/create-soap-endpoint.rst b/docs/how-tos/create-soap-endpoint.rst new file mode 100644 index 0000000000..1e481d37a1 --- /dev/null +++ b/docs/how-tos/create-soap-endpoint.rst @@ -0,0 +1,201 @@ +.. _create_soap_endpoint: + +Create a SOAP endpoint +====================== + +:author: `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 `. + +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 @- < + + + + My SOAP Content + + + + XML + +The output should contain the original request body within the ``therequestbodywas`` entity, +and should roughly look like: + +.. code-block:: xml + + + + + + + + My SOAP Content + + + + + + +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 @- < + + + + 42 + 0 + + + + XML + +The output should roughly look like: + +.. code-block:: xml + + + + + + 22012 + division by zero, DETAIL: , HINT: + + + + +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 ` + +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 diff --git a/docs/how-tos/providing-html-content-using-htmx.rst b/docs/how-tos/providing-html-content-using-htmx.rst new file mode 100644 index 0000000000..fd52815d87 --- /dev/null +++ b/docs/how-tos/providing-html-content-using-htmx.rst @@ -0,0 +1,321 @@ + +.. _providing_html_htmx: + +Providing HTML Content Using Htmx +================================= + +:author: `Laurence Isla `_ + +This how-to shows a way to return HTML content and use the `htmx library `_ to handle the AJAX requests. +Htmx expects an HTML response and uses it to replace an element inside the DOM (see the `htmx 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 `_ 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 `Tailwind CSS `_ for styling. + +.. code-block:: postgres + + create or replace function api.index() returns "text/html" as $$ + select $html$ + + + + + + PostgREST + HTMX To-Do List + + + + +
+
+
PostgREST + HTMX To-Do List
+
+
+ + + $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$ +
  • + + %3$s + +
  • + $html$, + $1.id, + case when $1.done then 'line-through text-gray-400' else '' 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), '' order by t.id) || + '
    ', + '

    There is nothing else to do.

    ' + ) + 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 ``
  • ``. + The PostgreSQL `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 ``
      `` wrapper for all the list elements. + It uses `string_arg `_ 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$ + + + + + + PostgREST + HTMX To-Do List + + + + + + +
      +
      +
      PostgREST + HTMX To-Do List
      +
      + +
      +
      + $html$ + || api.html_all_todos() || + $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 ````. + 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 ``
      `` 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 ```` element. + + + ``hx-target="#todo-list-area"``: the HTML content returned from the request will go inside ``
      `` (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 ````). + + + ``hx-on="htmx:afterRequest: this.reset()">``: this is a Javascript command that clears the form `after the request is done `_. + +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 `. + +.. 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$ +
    • +
      +
      + + + %3$s + + +
      +
      + + +
      +
      +
    • + $html$, + $1.id, + case when $1.done then 'line-through text-gray-400' else '' end, + api.sanitize_html($1.task), + (not $1.done)::text + ); + $$ language sql stable; + +Let's deconstruct the new htmx features added: + +- The ``
      `` 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 ````. + + + ``hx-trigger="click"``: htmx does the request after clicking on the element. + +- For the first ``