From 7611308b0ef9d525bbce84426ae04e5d1a93476b Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 9 Oct 2016 16:53:41 -0700 Subject: [PATCH 001/549] Sphinx quickstart --- .gitignore | 1 + Makefile | 230 ++++++++++++++++++++++++++++++++++++++++++ conf.py | 286 +++++++++++++++++++++++++++++++++++++++++++++++++++++ index.rst | 22 +++++ 4 files changed, 539 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 conf.py create mode 100644 index.rst diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..e35d8850c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +_build diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..b739a52fff --- /dev/null +++ b/Makefile @@ -0,0 +1,230 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# User-friendly check for sphinx-build +ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) + $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) +endif + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PostgREST.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PostgREST.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/PostgREST" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PostgREST" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/conf.py b/conf.py new file mode 100644 index 0000000000..8c6802b3fe --- /dev/null +++ b/conf.py @@ -0,0 +1,286 @@ +# -*- 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 = [] + +# 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 = u'PostgREST' +copyright = u'2016, Joe Nelson' +author = u'Joe Nelson' + +# 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 = u'0.4' +# The full version, including alpha/beta/rc tags. +release = u'0.4.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 = None + +# 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'] + +# 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 = 'alabaster' + +# 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 = None + +# 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', u'PostgREST Documentation', + u'Joe Nelson', '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', u'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', u'PostgREST Documentation', + author, 'PostgREST', 'One line description of project.', + 'Miscellaneous'), +] + +# 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 diff --git a/index.rst b/index.rst new file mode 100644 index 0000000000..8c055322db --- /dev/null +++ b/index.rst @@ -0,0 +1,22 @@ +.. PostgREST documentation master file, created by + sphinx-quickstart on Sun Oct 9 16:53:00 2016. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to PostgREST's documentation! +===================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + From debf4dc7153f302911e70b0c8e5affbcc8ed7e46 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 9 Oct 2016 22:16:13 -0700 Subject: [PATCH 002/549] Adjustment --- index.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/index.rst b/index.rst index 8c055322db..23b0de3218 100644 --- a/index.rst +++ b/index.rst @@ -10,13 +10,3 @@ Contents: .. toctree:: :maxdepth: 2 - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - From 577cf29f64aa68c5a1c89e1b8d84528ed6a1cd37 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 10 Oct 2016 01:15:33 -0700 Subject: [PATCH 003/549] Intro section --- _static/logo.png | Bin 0 -> 8149 bytes index.rst | 55 +++++++++++++++++++++++----- intro.rst | 92 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 9 deletions(-) create mode 100644 _static/logo.png create mode 100644 intro.rst diff --git a/_static/logo.png b/_static/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..030673cfbaa40c2bc994436ff1aa67cab3a01d0e GIT binary patch literal 8149 zcmXweby$=^_x3I&h$tYiARtl_0=uAqN+YR+gwibC-LZ5xNH>CngrqdPbT>#XNGvJM zF1f^y_kF)_{y1~b{hTv1=ecI)oNFfHvyv>y!)Ffx004=+oRlg6fD69I)r7eB^2*(* z8~^~?eO6GHmUU2wj*h-3|Ni}ZFWcJMj*gD*0RaKQ+1c6r{QU6nFbajb$1N=_C;u(? z`1JJj-@kv`+uQfbySuyhO*9&f!C>y?!NI}){{G>A%RO^%aejWzX_a<)d2w}lNkc(} z#bVhlQ|YK_?nk-rip?VRUhU@Q=I8{&VxDquetdi)Auev8-f%yJ{|^MuE3V1ODPv>f znU!q>904C&%t+68LH}G|Pw(B^w`!qfZmzCx9J4JgEC>k+kw~P1f`Wc@Wmi|1jBglqTz(9|jmWKLzRaMoB@`~8l z*xH(!or4nxdxz-qo{@$1d-scri-!gWQU4B0JEv!7XYYSSZu4kFM8xlb`J9}bzTUof zuDPS5V<90S1^M|tK0YaRgQTQn2~~YvQ!4|rYXQD~e*XS0?w&j2?78=DGrFiMsR5qi z$`Od_0*OwAdDM1Zv{WAcb+pX!81I(KtUl~TwD;?pI+#n; zugD$YEdG)p^$(30{8$o2Qpcjxx{Yr62l6;M^i~iCAitV%Csu*{^~;0X0CTFaY~aP#%a`%MijFCk6<5f&n$>C+{m9Y)wL^emBBns-o?}h8%aX?u;qB==N({Y$mn8{^Iw4m*}z@nL~|#_!y(Y zO3*v2UG{QarL#Jp);IUxou*!7`0dQTLf6l{{1gP--r$;hM~zg5>ib65om$F60B!w0 zN;M2893xk`#)}Y=kd-!jRW`B7>>bOc+H>M%^{|$RKwz86sL&~DckQcn1qG%&kagFOc-;pJ7z9%3&TaC~iLg1~%#>{4$rmUn zGFC%O2&Tj8{l?vOmVB-N#c7)f^EQZDX+DElT)@@1ZM$T7t&8Y$ zQO_dvFwhn&8?m;a4$vBQxr6SXEEJ|J8vl1|$2XAmVOe{W=j@Y`!6qgbhMe=$F$9x{ zBot(goS8-j2^Y%qq9FHmRb^KHnd4NwlE|5h6E$N*&q2yoR19Q@r3I@O z4emZteEZUz+vAkA2PuF`yJXsvwRtlw(LAcM{q_U+sJ>tH1Dhw(QN7}LV=J_+)2Uam z4RkT^HnII9sqZrUt=V~F@smTeX_ouAU{m;(;RQ_T!tMC8-2Aq*sNV$Vk=aSlGpN7b z0QkY%Pd?v|F1JKz%+L3Bx2mc_m5+K<=Aa!H1Bo??F~{;5;G7%9B#TAQK?zZp_4&is zfISL=?)DzXShiU-3cbPLGK{=H9Z&x8HwZng_TvozXSe;iaJ<~t3W`hOAydL&$Q)I6 zdxSKl`|yvDSXIh7N{G=4hdnc0jNt+C9cHLrn!DJ(v?nAHJ_WE4QMlMS`JVk}6%!Bb zV7|@THBJGmlJ$KJJhr{tPw0_|)8To*DU#xe|7-nA(#=i|A)*O9`X}ZEEb3?qUn~vl z)XSG?IEEHp`b`ts}kPOBw+9NaWBCeumz}as51U1)T zR25~2fwOy!QxrBn$lZ$y-A3)iSG9m8lYd3VXoO0LN$%@d)WLXxRXFza=9Ymq1Y$Jq zgPMw+IG^CbfgPXr#ZF$*k1$Cd7cf5p5FeFYbRICw%lTK;g1ILdb04<b$;DcQvz)vy9K7J=WOw}-YLT{Y z!5+8~MK%QLpXLSqD3c^!=i$`SJpiCTz$qfYspW0~zn^Mq(mXoj0O<7tLzQB|~cybWofx4t1q zP=e`?4#h-)89UB3T|5{T$1L`7N(ipPfUfsBe9QaTfoD+cS1~0#<*aWly^^?btFDEDIGoW50xyQ;8FgNY}2%a!C@AVEeLJoUv|n`D=r&2JjTyq z7FJZ*WFuSd@q`9bPHMuhyQWj`_~tJ`-ick(hmZ!B8Tp5+4t=A~{OMta;4i`Y2oKA{ z&oXpj>=@|pIhPJ(9mhxHo1~L$sb@Xg%$^@mL~%|}O0<@mYvvTUpLMhMg|K*>N(WO` ztzFVA#VU({vp}CbXKKe<$xD>zrm0V6S=KzMA6zL>0fx7uHliGS@*GQ_ZC<(X^V4*l z;xdzfZO)~Ir3kabF<-i3?-M&(m&X7QZ!!g2j#!L=z7#vPeiM2m*d@P=Fq4QlvCCwo&Mt} zy|l17N<7MNWb1wgN$pV%^34Sm%#&8AWZ{?lIz9oR(eW^O?QXjiX7G(h$H$k6O?Hc^ zl=%`!;#zLi;LmOI33x&hE=#cF2#EWS5#)eLGv_yJnn*bp%U3L4WA*V-=?ZM~wqax% z2izA%LmL_nCd}?vEGOd+vca@3;KPPJlXLFE1IMo|`^980QMHaxK;q^7-Vjjw`!>n3 ze4=#0<;a|#lI=h_I?GHT4h$%tCS(q_;C(4)W@&d-%-8ch7RK+iH(1NLdR0=s)cZeWlF&8d$VdT zW#Rvvf8Cr2Q)4^;nsSTyf}k}LeJ#`6h^tFCK|i6lUD zEo#QtI<5G_*N)c9ffVo~7SnP(ena?YXui;;s;KT-UG)}D+V59#Z6+ElLoXWn=G zGqynXE<|291jY;5?McB_<6CfRf23P*^Wa$IYA1-jl#WQD(M-T=>j81o zMAt>NU~z&+229_-D?jdu?ktf?n=FG|vp7W7ChNg2n?>a?&Mtv9C(c^UFsesWO|e3l zQ_i_4ceAJ-8EE?#ss2O0tF?!YEf+Xjo*AUI&Nsa5Bt**w#;$y#UvO%%I13j>N`~vZ zO^|v=KB^WIv326cNIFn*^#0kez;$b{oA()CieBMG{!?Dm5rUpl=nMC4U5B%Z*KpQ%vT`b zGV%`)W&R4g%BWE?<;7t9jPU!}?D)DkH!} z*SD#^qN}l-f2~e+)*1-8ZRZi47t^F5ko$Qd+mssQm4GbSd&<18TvwGO@;r9k(t3Gl zd;-H}@p`%pj{GH90P(*ev>UXJNNi+sF-&nIEl#KCxaUNgvlk;Q+6(#Ujr$}-UDQ>V zdydfj7isU0j-wl}5cYoYuzQd2Kt-gw@n-%?WjQr0ZYw(I_LrT9knONp4Z#=c%q)l&tmo7yJRbJ(KOz4wsfePMmT&`;Yi*@D z*t{T4&(B&g+_2aloqD-0OUFT&oJmf6)GV;lOzb>=^b4*K<=R;f^W`qM`+Xs6vfPxk z@??)|d2(~gH)|tm{^j*IhB6baVZ*5|Z*6y8y1}X{0QxbMy0Iuo{6)O0Qx{8@&4 zd_tDdR#3Yb;E*WZ{rx8x6FpbaKRGjt?ssaTI_Gl}+AV!^!^yV1-v}M>E$Hge@%fYq zu8R03zGfCidm-vJm>c8ax=p=;xx$JS#h30838$96aG@)l!f7L)+nC|QtDSeLKY5`}vWdc`PDxlk z=MsleO*rZB4zV2Lp8Z5IFSG0u^)x5@LW=m!w+HxXBa3yXr+t$AM4R*`N=xB7QjUl} zQ~p0PgVhj4V)QT6yg8b`NUN+nS-5ZmE#UDV{r_T!=O{N|yFmVmt9XA7p2`7CD!qHV{$m5H+h`aSr7+G`VpocUIWq#--s zZWB9cPC$U_@2Z&MOT8yX2AXmk+g_`WPX4j0(x1~no?VKIUK+%J%>aCMFzpPk2+8kv zo1#>1Ml-(@^83cF7R966k|-%N*?pS*{9tPEq{`*ke}8=ijujD zH-L=-J{+IZNG!-Y-Ka?%2+qRH;V8n5Id`|?iu;7-4rtc?465sT-G;a@P<}O4sQUAo zOqpR4CV50wU~S+_H>a7KE1()(F-7IX|1@q&3&9HlEOq8KJMa+ZJBPm5wrPOuz@Qc= zsj?#hzg6eHC(9pie8rCTzeB2zMYjl1IuVz`!3kWmBxNo(pQS&7sH4W{p|>%-G-tTw zy$p4>gO0EMQv2BFW(g2f_*X9c>xC>i-3hNetdLuzP&Vt!5F;i|yuuNrJ0Li@R!Bvu zwpOgrl>D`jAYGNh7j4MTiyr?@coj9kwdU&`$IS)(!K73NQ-fZ>RL!T>j7%bX=-##`TuIP6(%ouznM-|d#+W|CE7`c0J!)`K91ys>6ZB?dW#tW7 zS7R&n!fB^@w11sALR(?hi>7mhk6zXCrFrk7%9*%`j1M^##fR998O zzHfXzGEVd@?3oCejXVC%pI%=D(|Yo#bL!#EV_&|aPKpOy#)gKMH3W?*=JhW$1WICm{#0(L0?{xh24r(>vMn5LSok0l zr6<@S0;+wj%0asQ^nngg5^B)he(M~?F;b?0v6H3o*l$F}r*Wi9gkE!-5d<|7Q}g~c zWH;Ks{TEh$)-zhp9J92a_)HH(O-~l6K~n|eWgb_r%G&d`@VsRSi_)K?)_cd1`ZTr5 z`49yYrS!C_24RF-$2dd^Pc9#nY@!q8Z|7(#XQ#3?pnTtF4`shS?m8#O{rkb4Y2rZQ zr6Z17_0sJ@+yzc{iI9reBJRx}1&1)0s)KZtGjE>LPDoQA^SM8NE%>fGO`E!K=0uT) z&y?%1V{`k?cs4p0*UX%r@wHsj7NdKv0{e01Tv5Bi#E&WyT- zs6nf5*fj5J?`qYcOL9jhS6#))L1DRkz0I_gxS8_gu6{LI)pi92`{L(gRbHzb zTGRq{Vi5FfK|w8p;sQrrkjf*&9}`SFe;_|fSMWJ@6{0vM-lSq7l9Qbe``y;@yPGzK zHnN9x9P#?tw%2d|AP^|&*B}0PuHzrAYs{%$IB!v=n!~P zds=R!h{TH@8F9xVO;)m6M2JcAeK*Hn9DCwK<^;7JNwyW_{$BRpA!=r<(t6OgweA^l z-YPy;PVR4i(t7L}UuWoe>EhRva>!%dncT7jH@gs<71Z+c9-VsR)>8D}+**DbfvhXhQ@{vLy{_6|RDbY_w_yEb@&;Wh#=0 zGlMCqY)9AhD4H6x+_}W`73t~S%Q=NdqdcZNfjV0)G1FV<;iCL+!>^29se)$W&C8Qp zezrM2Ev7h6-LJFatnW-t9V-HIoqSPWjVR{#lwY}DRt6u-?#fSwAO-ZIQ!( z#P6i7CCImUc`;O$8{wOyFhZNusFiDs{?+v@K{f*AcC~tdS%Ws@0y~PQti>k>)8)o{ zC+d-`ya((MV)oV0=ClDN!mZN*eia$HB5mUghJ1VpGcg8KSF(^Xs zqh)KCf7uU8NkWF`1kj*$oS-=F2$4ErgNnbUKK^TKX2CBcd2ZV4+f`0=hJlS7KBfe{ z8RMf__>P=Sj!7upC|vKLj0!_5*3CaBQJZ-}+N}~L4a~J3s~hRm{bJVSu)^3-Y%0YY zmWK={;P-C;EG#He(vCbjyf8~?YRL;+X~-4gUrFRD$E;d!^?CM|yuUPw(UC3P=gt;o z{rAcbuk~;M`9_RtL^ybwh2mD^3tA7APl+XWsDFbMfcOD5P?GRMW=ZK>KXsf=+2D&j zAYJL8-~4amWP-pKfG!n0W_Gr4(q9vXkBNnBlU@jT;tQ3{^`2KsACY!L-pNI-!z;BK z+sq98iss+i>{sdc`;MG?C-6%3ifj3fT%wH9@9q-BKDnT0d(g;I#2SLO?b><$RCZXz zSEC4YV)+H(K5!oxhqgXMXX`hPMXS2G>-FYL5O3pSaJ($@3F>4s&v1rimj6cp+x+## z!L=p8k!|n);XKed@pfU%v;WrrvH1^eS3vM)>QLV2oyHX_{{vwT}PyOv@ln z$F*|Z+1=S0!0=zC7xN(j+1U{h_+r%f1qKv=-tO*E0Kwj#f=37YJ>#EEpQQ(2tq`Rv UW`QN$zZL`JrIn<9Ng4$HAD&l41^@s6 literal 0 HcmV?d00001 diff --git a/index.rst b/index.rst index 23b0de3218..c3fbe2bcba 100644 --- a/index.rst +++ b/index.rst @@ -1,12 +1,49 @@ -.. PostgREST documentation master file, created by - sphinx-quickstart on Sun Oct 9 16:53:00 2016. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -Welcome to PostgREST's documentation! -===================================== - -Contents: +.. image:: _static/logo.png .. toctree:: :maxdepth: 2 + +.. toctree:: + :caption: What is PostgREST? + + intro.rst + +.. Installation +.. Binary Release +.. Build from Source +.. Docker +.. API +.. Tables and Views +.. Filtering +.. Ordering +.. Limits and Pagination +.. Counting +.. Response Format +.. Singular or Plural +.. OpenAPI Support +.. Resource Embedding +.. Query Limitations +.. Stored Procedures +.. Insertions / Updates +.. Getting Results +.. Bulk Insert +.. Deletions +.. Authentication +.. Overview of Role System +.. JSON Web Tokens +.. Internal Generation +.. External Generation +.. SSL +.. Custom Validation +.. Schema Isolation +.. User Management +.. Logins +.. Password Reset +.. Administration +.. Block full-table operations +.. Alternate URL structure +.. API Versioning +.. HTTP Caching +.. Database Caching +.. Debugging +.. (viewing db logs) diff --git a/intro.rst b/intro.rst new file mode 100644 index 0000000000..4d070f6fc2 --- /dev/null +++ b/intro.rst @@ -0,0 +1,92 @@ +Motivation +########## + +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. + +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 db objects than to add guards in controllers. (This is especially true for cascading permissions in data dependencies.) It's easier set constraints than to litter code with sanity checks. + +Leakproof 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. + +Embracing the Relational Model +------------------------------ + +In 1970 E. F. Codd criticized the then-dominant hierarchical model of databases in his article A Relational Model of Data for Large Shared Data Banks. Reading the article reveals a striking similarity between hierarchical databases and nested http routes. With PostgREST we attempt to use flexible filtering and embedding rather than nested routes. + +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. + +Shared Improvements +------------------- + +As with any open source project, we all gain from features and fixes in the tool. It's more beneficial than improvements locked inextricably within custom codebases. + +Ecosystem +######### + +PostgREST has a growing ecosystem of examples, and libraries, experiments, and users. Here is a selection. + +Client-Side Libraries +--------------------- + +* `hugomrdias/postgrest-url `_ - JS, just for generating query URLs +* `john-kelly/elm-postgrest `_ - Elm +* `mithril.postgrest `_ - JS, Mithril +* `thejettdurham/postgrest-sharp-client `_ - C#, RestSharp +* `lewisjared/postgrest-request `_ - JS, SuperAgent +* `JarvusInnovations/jarvus-postgrest-apikit `_ - JS, Sencha framework +* `davidthewatson/postgrest_python_requests_client `_ - Python +* `calebmer/postgrest-client `_ - JS + +Extensions +---------- + +* `diogob/postgrest-ws `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY +* `srid/spas `_ - allow file uploads and basic auth +* `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server +* `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware + +Example Apps +------------ + +* `CodeforAustralia/heritage-near-me `_ - Elm and PostgREST with PostGIS +* `timwis/handsontable-postgrest `_ - An excel-like database table editor +* `Recmo/PostgrestSkeleton `_ - Docker Compose, PostgREST, Nginx and Auth0 +* `benoror/ember-postgrest-dynamic-ui `_ - generating Ember forms to edit data +* `ruslantalpa/blogdemo `_ - blog api demo in a vagrant image +* `timwis/ext-postgrest-crud `_ - browser-based spreadsheet +* `srid/chronicle `_ - tracking a tree of personal memories +* `diogob/elm-workshop `_ - building a simple database query UI +* `marmelab/ng-admin-postgrest `_ - automatic database admin panel +* `myfreeweb/moneylog `_ - accounting web app in Polymer + PostgREST +* `tyrchen/goodfilm `_ - example film api +* `begriffs/postgrest-example `_ - sqitch versioning for API + +In Production +------------- + +* `Catarse `_ +* `iAdvize `_ +* `Redsmin `_ +* `Image-charts `_ +* `Drip Depot `_ + +Commercial PaaS +--------------- + +* `Sub0 `_ - Automated GraphQL & REST API with built-in caching (powered by PostgREST) + + +Getting Support +################ + +The project has a friendly and growing community. Join our `chat room `_ for discussion and help. You can also report or search for bugs/features on the Github `issues `_ page. From 0060e8906db0e1608767a447485c86883410a6c2 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 10 Oct 2016 01:22:52 -0700 Subject: [PATCH 004/549] Allow RTD to use its nicer theme --- conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf.py b/conf.py index 8c6802b3fe..bcc453897d 100644 --- a/conf.py +++ b/conf.py @@ -108,7 +108,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'alabaster' +html_theme = 'default' # 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 From 6e82a55d68bf2657ff44aca34801ded8f695a1c5 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 10 Oct 2016 09:53:43 -0700 Subject: [PATCH 005/549] Installation page --- index.rst | 5 ++++ install.rst | 78 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 install.rst diff --git a/index.rst b/index.rst index c3fbe2bcba..7614a96d48 100644 --- a/index.rst +++ b/index.rst @@ -8,6 +8,11 @@ intro.rst +.. toctree:: + :caption: Installation + + install.rst + .. Installation .. Binary Release .. Build from Source diff --git a/install.rst b/install.rst new file mode 100644 index 0000000000..11fcfe2ac7 --- /dev/null +++ b/install.rst @@ -0,0 +1,78 @@ +Binary Release +============== + +The `release page `_ has precompiled binaries for Mac OS X, Windows, and several Linux distros. Extract the tarball and run the binary inside with the :code:`--help` flag to see usage instructions: + +.. code-block:: bash + + # Untar the release (available at https://github.com/begriffs/postgrest/releases/latest) + + $ tar zxf postgrest-[version]-[platform].tar.xz + + # Try running it + $ ./postgrest --help + + # You should see a usage help message + +Build from Source +================= + +When a prebuilt binary does not exist for your system you can build the project from source. You'll also need to do this if you want to help with development. `Stack `_ makes it easy. 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 + CentOS/Fedora/Red Hat postgresql-devel, zlib-devel + BSD postgresql95-server + ===================== ============================ + +* Build and install binary + + .. code-block:: bash + + git clone https://github.com/begriffs/postgrest.git + cd postgrest + stack build --install-ghc + sudo stack install --allow-different-user --local-bin-path /usr/local/bin + +* Check that the server is installed: :code:`postgrest --help`. + +If you want to run the test suite, stack can do that too: :code:`stack test`. + +PostgreSQL dependency +===================== + +To use PostgREST you will need an underlying database (PostgreSQL version 9.3 or greater is required). You can use something like Amazon `RDS `_ but installing your own locally is cheaper and more convenient for development. + +* `Instructions for OS X `_ +* `Instructions for Ubuntu 14.04 `_ +* `Installer for Windows `_ + +Homebrew +======== + +You can use the Homebrew package manager to install PostgREST on Mac + +.. code-block:: bash + + # Ensure brew is up to date + brew update + + # Check for any problems with brew's setup + brew doctor + + # Install the postgrest package + brew install postgrest + +This will automatically install PostgreSQL as a dependency. The process tends to take up to 15 minutes to install the package and its dependencies. + +After installation completes, the tool is added to your $PATH and can be used from anywhere with: + +.. code-block:: bash + + postgrest --help + From c0791a5f212b764622cacb0bf3681f0fd796b0ce Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 10 Oct 2016 10:30:31 -0700 Subject: [PATCH 006/549] WIP: api page --- api.rst | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ index.rst | 25 ++------- 2 files changed, 154 insertions(+), 20 deletions(-) create mode 100644 api.rst diff --git a/api.rst b/api.rst new file mode 100644 index 0000000000..1e3afb42b6 --- /dev/null +++ b/api.rst @@ -0,0 +1,149 @@ +Tables and Views +================ + +All views and tables in the active schema and accessible by the active database role for a request are available for querying. They are exposed in one-level deep routes. For instance the full contents of a table `people` is returned at + +.. code-block:: HTTP + + GET /people + +There are no deeply/nested/routes. Each route provides OPTIONS, GET, 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 Embedding. + +Filtering +--------- + +You can filter result rows by adding conditions on columns, each condition a query string parameter. For instance, to return people aged under 13 years old: + +.. code-block:: http + + GET /people?age=lt.13 + +Adding multiple parameters conjoins the conditions: + +.. code-block:: http + + GET /people?age=gte.18&student=is.true + +These operators are available: + +============ ============================================= +abbreviation meaning +============ ============================================= +eq equals +gte greater than or equal +gt greater than +lte less than or equal +lt less than +neq not equal +like LIKE operator (use * in place of %) +ilike ILIKE operator (use * in place of %) +in one of a list of values e.g. :code:`?a=in.1,2,3` +is checking for exact equality (null,true,false) +@@ full-text search using to_tsquery +@> contains e.g. :code:`?tags=@>.{example, new}` +<@ contained in e.g. :code:`?values=<@{1,2,3}` +not negates another operator, see below +============ ============================================= + + +To negate any operator, prefix it with :code:`not` like :code:`?a=not.eq.2`. + +For more complicated filters (such as those involving condition 1 OR condition 2) you will have to create a new view in the database. + +.. _computed_cols: + +Computed Columns +~~~~~~~~~~~~~~~~ + +Filters may be applied to computed columns as well as actual table/view columns, even though the computed columns will not appear in the output. For example, to search first and last names at once we can create a computed column that will not appear in the output but can be used in a filter: + +.. code-block:: sql + + CREATE TABLE people ( + fname text, + lname text + ); + + CREATE FUNCTION full_name(people) RETURNS text AS $$ + SELECT $1.fname || ' ' || $1.lname; + $$ LANGUAGE SQL; + + # (optional) add an index to speed up anticipated query + CREATE INDEX people_full_name_idx ON people + USING GIN (to_tsvector('english', fname || ' ' || lname)); + +A full-text search on the computed column: + +.. code-block:: http + + GET /people?full_name=@@.Beckett + +Ordering +-------- + +The reserved word :code:`order` reorders the response rows. It uses a comma-separated list of columns and directions: + +.. code-block:: http + + GET /people?order=age.desc,height.asc + +If no direction is specified it defaults to ascending order: + +.. code-block:: http + + GET /people?order=age + +If you care where nulls are sorted, add nullsfirst or nullslast: + +.. code-block:: http + + GET /people?order=age.nullsfirst + GET /people?order=age.desc.nullslast + +To order the embedded items, you need to specify the tree path for the order param like so. + +.. code-block:: http + + GET /projects?select=id,name,tasks{id,name}&order=id.asc&tasks.order=name.asc + +You can also use :ref:`computed_cols` to order the results, even though the computed columns will not appear in the output. + +Limits and Pagination +--------------------- + +Counting +-------- + +Response Format +--------------- + +Singular or Plural +------------------ + +OpenAPI Support +=============== + +Resource Embedding +================== + +Query Limitations +================= + +Stored Procedures +================= + +Insertions / Updates +==================== + +Getting Results +--------------- + +Bulk Insert +----------- + +Deletions +========= diff --git a/index.rst b/index.rst index 7614a96d48..4e6582abf9 100644 --- a/index.rst +++ b/index.rst @@ -13,26 +13,11 @@ install.rst -.. Installation -.. Binary Release -.. Build from Source -.. Docker -.. API -.. Tables and Views -.. Filtering -.. Ordering -.. Limits and Pagination -.. Counting -.. Response Format -.. Singular or Plural -.. OpenAPI Support -.. Resource Embedding -.. Query Limitations -.. Stored Procedures -.. Insertions / Updates -.. Getting Results -.. Bulk Insert -.. Deletions +.. toctree:: + :caption: API + + api.rst + .. Authentication .. Overview of Role System .. JSON Web Tokens From 5cd719bdfffa4e792036701c9207407b9a5d1fc7 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 10 Oct 2016 11:14:11 -0700 Subject: [PATCH 007/549] Limits and offsets --- api.rst | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 1e3afb42b6..7063703223 100644 --- a/api.rst +++ b/api.rst @@ -115,8 +115,56 @@ You can also use :ref:`computed_cols` to order the results, even though the comp Limits and Pagination --------------------- -Counting --------- +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 + + 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. + +There are two ways to apply a limit and offset rows: through request headers or query params. When using headers you specify the range of rows desired. This request gets the first twenty people. + +.. code-block:: http + + GET /people + Range-Unit: items + Range: 0-19 + +Note that the server may respond with fewer if unable to meet your request: + +.. code-block:: http + + 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-`. + +The other way to request a limit or offset is with query pamameters. For example + +.. code-block:: http + + GET /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. + +In order to obtain the total size of the table or view (such as when rendering the last page link in a pagination control), specify your preference in a request header: + + +.. code-block:: http + + GET /bigtable + Range-Unit: items + Range: 0-24 + 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 + + Range-Unit: items + Content-Range: 0-24/3573458 Response Format --------------- From 3025e18b3a278fadd456284a2a106e259702ba09 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 14 Oct 2016 13:37:51 -0700 Subject: [PATCH 008/549] Alpine linux warning --- install.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/install.rst b/install.rst index 11fcfe2ac7..5edde5e553 100644 --- a/install.rst +++ b/install.rst @@ -17,6 +17,10 @@ The `release page `_ has Build from Source ================= +.. note:: + + We discourage building and using PostgREST on **Alpine Linux** because of a reported GHC memory leak on that platform. + When a prebuilt binary does not exist for your system you can build the project from source. You'll also need to do this if you want to help with development. `Stack `_ makes it easy. It will install any necessary Haskell dependencies on your system. * `Install Stack `_ for your platform From 98a776f2dc6760dfcc2356805147cfc4e68d9faf Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 15 Oct 2016 16:54:36 -0700 Subject: [PATCH 009/549] Overview of role system --- _static/security-anon-choice.png | Bin 0 -> 31513 bytes _static/security-roles.png | Bin 0 -> 46364 bytes auth.rst | 61 +++++++++++++++++++++++++++++++ index.rst | 5 +++ 4 files changed, 66 insertions(+) create mode 100644 _static/security-anon-choice.png create mode 100644 _static/security-roles.png create mode 100644 auth.rst diff --git a/_static/security-anon-choice.png b/_static/security-anon-choice.png new file mode 100644 index 0000000000000000000000000000000000000000..ea02a237e6f61dc9430e96e2dff6567322059266 GIT binary patch literal 31513 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�t;Y~_Hb$OFhK3_nUNnF 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 literal 0 HcmV?d00001 diff --git a/auth.rst b/auth.rst new file mode 100644 index 0000000000..9ddbc23a64 --- /dev/null +++ b/auth.rst @@ -0,0 +1,61 @@ +Overview of Role System +======================= + +PostgREST is designed to keep the database at the center of API security. All authorization happens through database roles and permissions. 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. + +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 should be configured in the db to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests. 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. + +.. image:: _static/security-anon-choice.png + +Here are the technical details. We use `JSON Web Tokens `_ to authenticate API requests. 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; + +If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role whos actual database-specific name, like that of with the authenticator role, is specified in the PostgREST server configuration file. + +JSON Web Tokens +=============== + +Internal Generation +------------------- + +External Generation +------------------- + +SSL +=== + +Custom Validation +================= + +Schema Isolation +================ + +User Management +=============== + +Logins +------ + +Password Reset +-------------- diff --git a/index.rst b/index.rst index 4e6582abf9..39bf27b4cb 100644 --- a/index.rst +++ b/index.rst @@ -18,6 +18,11 @@ api.rst +.. toctree:: + :caption: Authentication + + auth.rst + .. Authentication .. Overview of Role System .. JSON Web Tokens From d00bb026b6e3bcf13cdb4746d4877f19cace0f01 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 15 Oct 2016 18:14:50 -0700 Subject: [PATCH 010/549] pre-request function docs --- auth.rst | 52 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/auth.rst b/auth.rst index 9ddbc23a64..cce3492385 100644 --- a/auth.rst +++ b/auth.rst @@ -3,11 +3,11 @@ Overview of Role System PostgREST is designed to keep the database at the center of API security. All authorization happens through database roles and permissions. 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. -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. +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 should be configured in the db to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests. 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. +The authenticator should be configured in the database to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests. 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. .. image:: _static/security-anon-choice.png @@ -31,17 +31,55 @@ Note that the database administrator must allow the authenticator role to switch GRANT user123 TO authenticator; -If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role whos actual database-specific name, like that of with the authenticator role, is specified in the PostgREST server configuration file. +If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role whos actual database-specific name, like that of with the authenticator role, is specified in the PostgREST server configuration file. The database administrator must set anonymous role permissions correctly to prevent anonymous users from seeing or changing things they shouldn't. + +Custom Authentication +--------------------- + +PostgREST honors the `exp` claim for token expiration, rejecting expired tokens. However it does not enforce any extra constraints. An example of an extra constraint would be to immediately revoke access for a certain user. The configuration file paramter `pre-request` specifies a stored procedure to call immediately after the authenticator switches into a new role and before the main query itself runs. + +Here's an example. In the config file specify a stored procedure: + +.. code:: + + pre-request = "public.check_user" + +In the function you can run arbitrary code to check the request and raise an exception to block it if desired. + +.. code:: sql + + CREATE OR REPLACE FUNCTION check_user() RETURNS void + LANGUAGE plpgsql + AS $$ + BEGIN + IF current_role = 'evil_user' THEN + RAISE EXCEPTION 'No, you are evil' + USING HINT = 'Stop being so evil and maybe you can log in'; + END IF; + END + $$; + +Client Auth +=========== + +To make an authenticated request the client must include an `Authorization` HTTP header with the value `Bearer `. For instance: + +.. code:: http + + GET /foo + Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZSIsImV4cCI6MTQ3NTUxNjI1MH0.GYDZV3yM0gqvuEtJmfpplLBXSGYnke_Pvnl0tbKAjB4 + +JWT Generation +============== -JSON Web Tokens -=============== -Internal Generation -------------------- External Generation ------------------- +Internal Generation +------------------- + SSL === From be869d3f54faa7c94073edfb00d1fb489b1a4a40 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 16 Oct 2016 13:13:44 -0700 Subject: [PATCH 011/549] JWT generation --- auth.rst | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/auth.rst b/auth.rst index cce3492385..e77dad5ae8 100644 --- a/auth.rst +++ b/auth.rst @@ -72,13 +72,44 @@ To make an authenticated request the client must include an `Authorization` HTTP JWT Generation ============== +You can create a valid JWT either from inside your database or via an external service. Each token is cryptographically signed with a secret passphrase -- the signer and verifier share the secret. Hence any service that shares a passphrase with a PostgREST server can create valid JWT. (PostgREST currently supports only the HMAC-SHA256 signing algorithm.) +From SQL +-------- -External Generation -------------------- +You can create JWT tokens in SQL using the `pgjwt extension `_. 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 which creates the functions you will need. -Internal Generation -------------------- +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:: sql + + CREATE TYPE jwt_token AS ( + token text + ); + + CREATE FUNCTION jwt_test() RETURNS public.jwt_token + LANGUAGE sql + AS $$ + SELECT jwt.sign( + row_to_json(r), 'mysecret' + ) AS token + FROM ( + SELECT + 'my_role'::text as role, + extract(epoch from now())::integer + 300 AS exp + ) r; + $$; + +PostgREST exposes this function to clients via a POST request to `/rpc/jwt_token`. + +Using Auth0 +----------- + +An external service like `Auth0 `_ can do the hard work transforming Github, Twitter, Google etc OAuth into a JWT suitable for PostgREST. Auth0 can also handle email signup and password reset flows. + +To adapt Auth0 to our uses we need to save the database role in `user metadata `_ and include the metadata in `private claims `_ of the generated JWT. + +**TODO: add details** SSL === From 6e7349d76c666e9ab26dc4974ea5892e6fea0a14 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 16 Oct 2016 13:54:05 -0700 Subject: [PATCH 012/549] SSL --- auth.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/auth.rst b/auth.rst index e77dad5ae8..d02f6affbd 100644 --- a/auth.rst +++ b/auth.rst @@ -114,8 +114,7 @@ To adapt Auth0 to our uses we need to save the database role in `user metadata < SSL === -Custom Validation -================= +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 SSL. 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. Schema Isolation ================ From f8829540190c2e5a53b14e810a653c52c2b3f026 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 24 Oct 2016 14:23:44 -0700 Subject: [PATCH 013/549] SQL user management section --- auth.rst | 406 +++++++++++++++++++++++++++++++++++++++++++++++++++--- intro.rst | 2 +- 2 files changed, 390 insertions(+), 18 deletions(-) diff --git a/auth.rst b/auth.rst index d02f6affbd..9622b5e750 100644 --- a/auth.rst +++ b/auth.rst @@ -7,7 +7,7 @@ There are three types of roles used by PostgREST, the **authenticator**, **anony .. image:: _static/security-roles.png -The authenticator should be configured in the database to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests. 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. +The authenticator should be created `NOINHERIT` and configured in the database to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests. 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. .. image:: _static/security-anon-choice.png @@ -31,10 +31,10 @@ Note that the database administrator must allow the authenticator role to switch GRANT user123 TO authenticator; -If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role whos actual database-specific name, like that of with the authenticator role, is specified in the PostgREST server configuration file. The database administrator must set anonymous role permissions correctly to prevent anonymous users from seeing or changing things they shouldn't. +If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role whose actual database-specific name, like that of with the authenticator role, is specified in the PostgREST server configuration file. The database administrator must set anonymous role permissions correctly to prevent anonymous users from seeing or changing things they shouldn't. -Custom Authentication ---------------------- +Custom Validation +----------------- PostgREST honors the `exp` claim for token expiration, rejecting expired tokens. However it does not enforce any extra constraints. An example of an extra constraint would be to immediately revoke access for a certain user. The configuration file paramter `pre-request` specifies a stored procedure to call immediately after the authenticator switches into a new role and before the main query itself runs. @@ -70,12 +70,12 @@ To make an authenticated request the client must include an `Authorization` HTTP Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZSIsImV4cCI6MTQ3NTUxNjI1MH0.GYDZV3yM0gqvuEtJmfpplLBXSGYnke_Pvnl0tbKAjB4 JWT Generation -============== +-------------- You can create a valid JWT either from inside your database or via an external service. Each token is cryptographically signed with a secret passphrase -- the signer and verifier share the secret. Hence any service that shares a passphrase with a PostgREST server can create valid JWT. (PostgREST currently supports only the HMAC-SHA256 signing algorithm.) -From SQL --------- +JWT from SQL +~~~~~~~~~~~~ You can create JWT tokens in SQL using the `pgjwt extension `_. 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 which creates the functions you will need. @@ -102,28 +102,400 @@ Next write a stored procedure that returns the token. The one below returns a to PostgREST exposes this function to clients via a POST request to `/rpc/jwt_token`. -Using Auth0 ------------ +JWT from Auth0 +~~~~~~~~~~~~~~ -An external service like `Auth0 `_ can do the hard work transforming Github, Twitter, Google etc OAuth into a JWT suitable for PostgREST. Auth0 can also handle email signup and password reset flows. +An external service like `Auth0 `_ 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 adapt Auth0 to our uses we need to save the database role in `user metadata `_ and include the metadata in `private claims `_ of the generated JWT. **TODO: add details** SSL -=== +--- 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 SSL. 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. Schema Isolation ================ -User Management -=============== +A PostgREST instance is configured to expose all the tables, views, and stored procedures of a single schema specified in a server configuration file. Objects -Logins ------- -Password Reset --------------- +SQL User Management +=================== + +Storing Users and Passwords +--------------------------- + +As mentioned, 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. + +The following table, functions, and triggers will live in a `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), + verified boolean not null default false + -- If you like add more columns, or a json column + ); + +We would like the role to be a foreign key to actual database roles, however PostgreSQL does not support these constraints against the `pg_roles` table. We'll use a trigger to manually enforce it. + +.. code:: sql + + create or replace function + basic_auth.check_role_exists() returns trigger + language plpgsql + 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 + $$; + + 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 `users` table. + +.. code:: sql + + create extension if not exists pgcrypto; + + create or replace function + basic_auth.encrypt_pass() returns trigger + language plpgsql + 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 + $$; + + 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:: sql + + 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; + $$; + +Finally we want a helper function to check whether the database user for the current API request has access to see or change a given role. This will become useful in the next section. + +.. code:: sql + + create or replace function + basic_auth.clearance_for_role(u name) returns void as + $$ + declare + ok boolean; + begin + select exists ( + select rolname + from pg_authid + where pg_has_role(current_user, oid, 'member') + and rolname = u + ) into ok; + if not ok then + raise invalid_password using message = + 'current user not member of role ' || u; + end if; + end + $$ LANGUAGE plpgsql; + +Public User Interface +--------------------- + +In the previous section we created an internal place to store user information. Here we create views and functions in a public schema that clients will access through the HTTP API. These public relations allow users view or edit their own information, log in, sign up, etc. + +Logins and Signup +~~~~~~~~~~~~~~~~~ + +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 hardcoded in this example to a secure secret of your choosing. + +.. code:: sql + + create or replace function + login(email text, pass text) returns basic_auth.jwt_token + language plpgsql + as $$ + declare + _role name; + _verified boolean; + _email text; + result basic_auth.jwt_claims; + 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; + -- check verified flag whether users + -- have validated their emails + _email := email; + select verified from basic_auth.users as u where u.email=_email limit 1 into _verified; + if not _verified then + raise invalid_authorization_specification using message = 'user is not verified'; + end if; + + select jwt.sign( + row_to_json(r), 'mysecret' + ) 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; + $$; + +An API request to call this function would look like: + +.. code:: http + + POST /rpc/login + + { "email": "foo@bar.com", "pass": "foobar" } + +The response would look like the snippet below. Try decoding the token at `jwt.io `_. (It was encoded with a secret of `mysecret` as specified in the SQL code above. You'll want to change this secret in your app!) + +.. code:: json + + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicm9sZSI6ImF1dGhvciJ9.fpf3_ERi5qbWOE5NPzvauJgvulm0zkIG9xSm2w5zmdw" + } + +Editing User Info +~~~~~~~~~~~~~~~~~ + +Here is a redacted view for users. It hides passwords and shows only those users whose roles the currently logged in user has database permission to access. + +.. code:: sql + + create or replace view users as + select actual.role as role, + '***'::text as pass, + actual.email as email, + actual.verified as verified + from basic_auth.users as actual, + (select rolname + from pg_authid + where pg_has_role(current_user, oid, 'member') + ) as member_of + where actual.role = member_of.rolname; + -- can also add restriction that current_setting('request.jwt.claim.email') + -- is equal to email so that user can only see themselves + +Using this view a client can see their role and any other users to whose roles the client belongs. This view does not yet support inserts or updates because not all the columns refer directly to underlying columns. Nor do we want it to be auto-updatable because it would allow an escalation of privileges. Someone could update their own row and change their role to become more powerful. We'll handle updates with a trigger: + +.. code:: sql + + create or replace function + update_users() returns trigger + language plpgsql + AS $$ + begin + if tg_op = 'INSERT' then + perform basic_auth.clearance_for_role(new.role); + + insert into basic_auth.users + (role, pass, email, verified) + values ( + new.role, new.pass, new.email, + coalesce(new.verified, false)); + return new; + elsif tg_op = 'UPDATE' then + -- no need to check clearance for old.role because + -- an ineligible row would not have been available to update (http 404) + perform basic_auth.clearance_for_role(new.role); + + update basic_auth.users set + email = new.email, + role = new.role, + pass = new.pass, + verified = coalesce(new.verified, old.verified, false) + where email = old.email; + return new; + elsif tg_op = 'DELETE' then + -- no need to check clearance for old.role (see previous case) + + delete from basic_auth.users + where basic_auth.email = old.email; + return null; + end if; + end + $$; + + drop trigger if exists update_users on users; + create trigger update_users + instead of insert or update or delete on + users for each row execute procedure update_users(); + +Permissions +~~~~~~~~~~~ + +Your database roles need access to the schema, tables, views and functions in order to service HTTP requests. Recall from the `Overview of Role System`_ 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:: sql + + -- the names "anon" and "authenticator" are configurable and not + -- sacred, we simply choose them for clarity + create role anon; + create role authenticator noinherit; + grant anon to authenticator; + + grant usage on schema public, basic_auth to anon; + + -- anon can create new logins + grant insert on table basic_auth.users, basic_auth.tokens to anon; + grant select on table pg_authid, basic_auth.users to anon; + grant execute on function + login(text,text), + signup(text, text) + to anon; + +You may be worried from the above that anonymous users can read everything from the `basic_auth.users` table. However this table is not available for direct queries because it lives in a separate schema. The anonymous role needs access because the public `users` view reads the underlying table with the permissions of the calling user. But we have made sure the view properly restricts access to sensitive information. + +Interacting with Email +---------------------- + +External actions like sending an email or calling 3rd-party services are possible in PostgREST but must be handled with care. Even if there are PostgreSQL extensions to make network requests it is bad practice to do this in SQL. Blocking on the outside world is unhealthy in a database and holds open long-running transactions. The proper approach is for the database to signal an external program to perform the required action and then not block on the result. + +One way to do this is using a table to implement a job queue for external programs. However this approach is `dangerous `_ because of its potential interactions with unrelated long-running queries. However things are improving with PostgreSQL 9.5 which introduces SKIP LOCKED to build reliable work queues, see `this article `_. + +Another way to queue and tasks for external processing is by bridging PostgreSQL's `LISTEN `_/`NOTIFY `_ pubsub with a dedicated external queue system. Two programs to listen for database events and queue them are + +* `aweber/pgsql-listen-exchange `_ for RabbitMQ +* `SpiderOak/skeeter `_ for ZeroMQ + +For experimentation purposes you can also have external programs LISTEN directly for PostgreSQL events. It's less robust than a queuing system but an example Node program might look like this: + +.. code:: js + + var PS = require('pg-pubsub'); + + if(process.argv.length !== 3) { + console.log("USAGE: DB_URL"); + process.exit(2); + } + var url = process.argv[2], + ps = new PS(url); + + // password reset request events + ps.addChannel('reset', console.log); + // email validation required event + ps.addChannel('validate', console.log); + + // modify me to send emails + +To use this LISTEN/NOTIFY approach (with or without a real queue hooked up) we can make our SQL functions issue a NOTIFY to perform external actions. Two such such functions are those to confirm an email address or send a password reset token. Both will use nonces and need a place to store them, so we'll start there. + +.. code:: sql + + create type token_type_enum as enum ('validation', 'reset'); + + create table if not exists + basic_auth.tokens ( + token uuid primary key, + token_type token_type_enum not null, + email text not null references basic_auth.users (email) + on delete cascade on update cascade, + created_at timestamptz not null default current_date + ); + +Here is a password reset function to make public for API requests. The function takes a user email address. + +.. code:: sql + + create or replace function + request_password_reset(email text) returns void + language plpgsql + as $$ + declare + tok uuid; + begin + delete from basic_auth.tokens + where token_type = 'reset' + and tokens.email = request_password_reset.email; + + select gen_random_uuid() into tok; + insert into basic_auth.tokens (token, token_type, email) + values (tok, 'reset', request_password_reset.email); + perform pg_notify('reset', + json_build_object( + 'email', request_password_reset.email, + 'token', tok, + 'token_type', 'reset' + )::text + ); + end; + $$; + +Notice the use of `pg_notify` above. It notifies a channel called `reset` with a JSON object containing details of the email address and token. A worker process would directly LISTEN for this event or would pull it off a queue and do the work to send an email with a friendly human readable message. + +Similar to the password reset request, an email validation function creates a token and then defers to external processing. This one won't be publicly accessible, but rather can be triggered on user account creation. + +.. code:: sql + + create or replace function + basic_auth.send_validation() returns trigger + language plpgsql + as $$ + declare + tok uuid; + begin + select gen_random_uuid() into tok; + insert into basic_auth.tokens (token, token_type, email) + values (tok, 'validation', new.email); + perform pg_notify('validate', + json_build_object( + 'email', new.email, + 'token', tok, + 'token_type', 'validation' + )::text + ); + return new; + end + $$; + + drop trigger if exists send_validation on basic_auth.users; + create trigger send_validation + after insert on basic_auth.users + for each row + execute procedure basic_auth.send_validation(); diff --git a/intro.rst b/intro.rst index 4d070f6fc2..0baee66198 100644 --- a/intro.rst +++ b/intro.rst @@ -83,7 +83,7 @@ In Production Commercial PaaS --------------- -* `Sub0 `_ - Automated GraphQL & REST API with built-in caching (powered by PostgREST) +* `Sub0 `_ - Automated GraphQL & REST API with built-in caching (powered by PostgREST, not affiliated) Getting Support From d1526890380cd7d27c912a3252f85da469edbabb Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 24 Oct 2016 14:50:22 -0700 Subject: [PATCH 014/549] More about schema isolation --- auth.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/auth.rst b/auth.rst index 9622b5e750..89324bdd77 100644 --- a/auth.rst +++ b/auth.rst @@ -119,8 +119,7 @@ PostgREST aims to do one thing well: add an HTTP interface to a PostgreSQL datab Schema Isolation ================ -A PostgREST instance is configured to expose all the tables, views, and stored procedures of a single schema specified in a server configuration file. Objects - +A PostgREST instance is configured to expose all the tables, views, and stored procedures of a single schema specified in a server configuration file. This means private data or implementation details can go inside a private schema and be invisible to HTTP clients. You can then expose views and stored procedures which insulate the internal details from the outside world. It keeps you code easier to refactor, and provides a natural way to do API `versioning`_. For an example of wrapping a private table with a public view see the `Editing User Info`_ section below. SQL User Management =================== @@ -304,7 +303,7 @@ The response would look like the snippet below. Try decoding the token at `jwt.i Editing User Info ~~~~~~~~~~~~~~~~~ -Here is a redacted view for users. It hides passwords and shows only those users whose roles the currently logged in user has database permission to access. +By creating a public wrapper around the internal users table we can allow people to safely edit it through the same auto-generated API that apply to other tables and views. The following view redacts sensitive information. It hides passwords and shows only those users whose roles the currently logged in user has database permission to access. .. code:: sql @@ -399,7 +398,7 @@ External actions like sending an email or calling 3rd-party services are possibl One way to do this is using a table to implement a job queue for external programs. However this approach is `dangerous `_ because of its potential interactions with unrelated long-running queries. However things are improving with PostgreSQL 9.5 which introduces SKIP LOCKED to build reliable work queues, see `this article `_. -Another way to queue and tasks for external processing is by bridging PostgreSQL's `LISTEN `_/`NOTIFY `_ pubsub with a dedicated external queue system. Two programs to listen for database events and queue them are +Another way to queue tasks for external processing is by bridging PostgreSQL's `LISTEN `_/`NOTIFY `_ pubsub with a dedicated external queue system. Two programs to listen for database events and queue them are * `aweber/pgsql-listen-exchange `_ for RabbitMQ * `SpiderOak/skeeter `_ for ZeroMQ From fda3e586199205e548cefd5590533ccb5be91179 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 24 Oct 2016 15:23:44 -0700 Subject: [PATCH 015/549] Fix code block lexing --- api.rst | 36 +++++++++++++++++++++--------------- auth.rst | 34 +++++++++++++++++----------------- 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/api.rst b/api.rst index 7063703223..0e9aa142e1 100644 --- a/api.rst +++ b/api.rst @@ -3,9 +3,9 @@ Tables and Views All views and tables in the active schema and accessible by the active database role for a request are available for querying. They are exposed in one-level deep routes. For instance the full contents of a table `people` is returned at -.. code-block:: HTTP +.. code-block:: http - GET /people + GET /people HTTP/1.1 There are no deeply/nested/routes. Each route provides OPTIONS, GET, POST, PATCH, and DELETE verbs depending entirely on database permissions. @@ -20,13 +20,13 @@ You can filter result rows by adding conditions on columns, each condition a que .. code-block:: http - GET /people?age=lt.13 + GET /people?age=lt.13 HTTP/1.1 Adding multiple parameters conjoins the conditions: .. code-block:: http - GET /people?age=gte.18&student=is.true + GET /people?age=gte.18&student=is.true HTTP/1.1 These operators are available: @@ -61,7 +61,7 @@ Computed Columns Filters may be applied to computed columns as well as actual table/view columns, even though the computed columns will not appear in the output. For example, to search first and last names at once we can create a computed column that will not appear in the output but can be used in a filter: -.. code-block:: sql +.. code-block:: postgres CREATE TABLE people ( fname text, @@ -72,7 +72,7 @@ Filters may be applied to computed columns as well as actual table/view columns, SELECT $1.fname || ' ' || $1.lname; $$ LANGUAGE SQL; - # (optional) add an index to speed up anticipated query + -- (optional) add an index to speed up anticipated query CREATE INDEX people_full_name_idx ON people USING GIN (to_tsvector('english', fname || ' ' || lname)); @@ -80,7 +80,7 @@ A full-text search on the computed column: .. code-block:: http - GET /people?full_name=@@.Beckett + GET /people?full_name=@@.Beckett HTTP/1.1 Ordering -------- @@ -89,26 +89,29 @@ The reserved word :code:`order` reorders the response rows. It uses a comma-sepa .. code-block:: http - GET /people?order=age.desc,height.asc + GET /people?order=age.desc,height.asc HTTP/1.1 If no direction is specified it defaults to ascending order: .. code-block:: http - GET /people?order=age + GET /people?order=age HTTP/1.1 If you care where nulls are sorted, add nullsfirst or nullslast: .. code-block:: http - GET /people?order=age.nullsfirst - GET /people?order=age.desc.nullslast + GET /people?order=age.nullsfirst HTTP/1.1 + +.. code-block:: http + + GET /people?order=age.desc.nullslast HTTP/1.1 To order the embedded items, you need to specify the tree path for the order param like so. .. code-block:: http - GET /projects?select=id,name,tasks{id,name}&order=id.asc&tasks.order=name.asc + GET /projects?select=id,name,tasks{id,name}&order=id.asc&tasks.order=name.asc HTTP/1.1 You can also use :ref:`computed_cols` to order the results, even though the computed columns will not appear in the output. @@ -119,6 +122,7 @@ PostgREST uses HTTP range headers to describe the size of results. Every respons .. code-block:: http + HTTP/1.1 200 OK Range-Unit: items Content-Range: 0-14/* @@ -128,7 +132,7 @@ There are two ways to apply a limit and offset rows: through request headers or .. code-block:: http - GET /people + GET /people HTTP/1.1 Range-Unit: items Range: 0-19 @@ -136,6 +140,7 @@ 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/* @@ -145,7 +150,7 @@ The other way to request a limit or offset is with query pamameters. For example .. code-block:: http - GET /people?limit=15&offset=30 + GET /people?limit=15&offset=30 HTTP/1.1 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. @@ -154,7 +159,7 @@ In order to obtain the total size of the table or view (such as when rendering t .. code-block:: http - GET /bigtable + GET /bigtable HTTP/1.1 Range-Unit: items Range: 0-24 Prefer: count=exact @@ -163,6 +168,7 @@ Note that the larger the table the slower this query runs in the database. The s .. code-block:: http + HTTP/1.1 206 Partial Content Range-Unit: items Content-Range: 0-24/3573458 diff --git a/auth.rst b/auth.rst index 89324bdd77..c5a44e7a76 100644 --- a/auth.rst +++ b/auth.rst @@ -21,13 +21,13 @@ Here are the technical details. We use `JSON Web Tokens `_ to au 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 +.. code:: postgres SET LOCAL ROLE user123; Note that the database administrator must allow the authenticator role to switch into this user by previously executing -.. code:: sql +.. code:: postgres GRANT user123 TO authenticator; @@ -40,13 +40,13 @@ PostgREST honors the `exp` claim for token expiration, rejecting expired tokens. Here's an example. In the config file specify a stored procedure: -.. code:: +.. code:: ini pre-request = "public.check_user" In the function you can run arbitrary code to check the request and raise an exception to block it if desired. -.. code:: sql +.. code:: postgres CREATE OR REPLACE FUNCTION check_user() RETURNS void LANGUAGE plpgsql @@ -81,7 +81,7 @@ You can create JWT tokens in SQL using the `pgjwt extension Date: Mon, 24 Oct 2016 15:26:40 -0700 Subject: [PATCH 016/549] Use plpgsql language type when applicable --- auth.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/auth.rst b/auth.rst index c5a44e7a76..0051127553 100644 --- a/auth.rst +++ b/auth.rst @@ -46,7 +46,7 @@ Here's an example. In the config file specify a stored procedure: In the function you can run arbitrary code to check the request and raise an exception to block it if desired. -.. code:: postgres +.. code:: plpgsql CREATE OR REPLACE FUNCTION check_user() RETURNS void LANGUAGE plpgsql @@ -151,7 +151,7 @@ First we'll need a table to keep track of our users: We would like the role to be a foreign key to actual database roles, however PostgreSQL does not support these constraints against the `pg_roles` table. We'll use a trigger to manually enforce it. -.. code:: postgres +.. code:: plpgsql create or replace function basic_auth.check_role_exists() returns trigger @@ -175,7 +175,7 @@ We would like the role to be a foreign key to actual database roles, however Pos Next we'll use the pgcrypto extension and a trigger to keep passwords safe in the `users` table. -.. code:: postgres +.. code:: plpgsql create extension if not exists pgcrypto; @@ -199,7 +199,7 @@ Next we'll use the pgcrypto extension and a trigger to keep passwords safe in th 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:: postgres +.. code:: plpgsql create or replace function basic_auth.user_role(email text, pass text) returns name @@ -247,7 +247,7 @@ Logins and Signup 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 hardcoded in this example to a secure secret of your choosing. -.. code:: postgres +.. code:: plpgsql create or replace function login(email text, pass text) returns basic_auth.jwt_token @@ -323,7 +323,7 @@ By creating a public wrapper around the internal users table we can allow people Using this view a client can see their role and any other users to whose roles the client belongs. This view does not yet support inserts or updates because not all the columns refer directly to underlying columns. Nor do we want it to be auto-updatable because it would allow an escalation of privileges. Someone could update their own row and change their role to become more powerful. We'll handle updates with a trigger: -.. code:: postgres +.. code:: plpgsql create or replace function update_users() returns trigger @@ -440,7 +440,7 @@ To use this LISTEN/NOTIFY approach (with or without a real queue hooked up) we c Here is a password reset function to make public for API requests. The function takes a user email address. -.. code:: postgres +.. code:: plpgsql create or replace function request_password_reset(email text) returns void @@ -470,7 +470,7 @@ Notice the use of `pg_notify` above. It notifies a channel called `reset` with a Similar to the password reset request, an email validation function creates a token and then defers to external processing. This one won't be publicly accessible, but rather can be triggered on user account creation. -.. code:: postgres +.. code:: plpgsql create or replace function basic_auth.send_validation() returns trigger From 8a606f72237f49536d69b2700df60a8edda505c5 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 28 Oct 2016 17:18:41 -0700 Subject: [PATCH 017/549] Discuss group/user roles --- auth.rst | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/auth.rst b/auth.rst index 0051127553..2c00ae42df 100644 --- a/auth.rst +++ b/auth.rst @@ -1,7 +1,10 @@ Overview of Role System ======================= -PostgREST is designed to keep the database at the center of API security. All authorization happens through database roles and permissions. 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. +PostgREST is designed to keep the database at the center of API security. All authorization happens through database roles and permissions. 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. + +Authentication Sequence +----------------------- 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. @@ -33,6 +36,89 @@ Note that the database administrator must allow the authenticator role to switch If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role whose actual database-specific name, like that of with the authenticator role, is specified in the PostgREST server configuration file. The database administrator must set anonymous role permissions correctly to prevent anonymous users from seeing or changing things they shouldn't. +Users and Groups +---------------- + +PostgreSQL manages database access permissions using the concept of roles. 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 the JWT-based role switching described above does most of what you need. When an authenticated user makes a request PostgREST will switch into the role for that user, which in addition to restricting queries, is available to SQL through the `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:: 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 + ); + +We want to enforce a policy that ensures a user can see only those messages sent by him or intended for him. Also we want to prevent a user from forging the message_from column with anyone else's name. + +PostgreSQL (9.5 and later) allows us to set this policy with row-level security: + +.. code:: 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. + +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 GUC variables set by PostgREST per request. For instance to get the email claim, call this function: + +.. code:: postgres + + current_setting('request.jwt.claim.email') + +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. + +.. note:: + + The current_setting function raises an exception if the setting in question is not present, as when a claim is missing from the JWT. Your SQL functions can either catch the exception, or you can set a default value for the database like this. + + .. code:: postgres + + -- Prevent current_setting('postgrest.claims.email') from raising + -- an exception if the setting is not present. Default it to ''. + ALTER DATABASE your_db_name SET request.claim.email TO ''; + +Hybrid User-Group Roles +~~~~~~~~~~~~~~~~~~~~~~~ + +There is no performance penalty for having many database roles, although roles are namespaced per-cluster rather than per-database so may be prone to collision within the database. You are free to assign a new role for every user in a web application if desired. 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:: 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) + Custom Validation ----------------- From 2c53cb4fae0bc7ae3c35e2d24ed7e7f2174f5f9a Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 28 Oct 2016 17:54:32 -0700 Subject: [PATCH 018/549] Fix highlighting, typo in current_user --- auth.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/auth.rst b/auth.rst index 2c00ae42df..cc1aecbbe1 100644 --- a/auth.rst +++ b/auth.rst @@ -132,13 +132,13 @@ Here's an example. In the config file specify a stored procedure: In the function you can run arbitrary code to check the request and raise an exception to block it if desired. -.. code:: plpgsql +.. code:: postgres CREATE OR REPLACE FUNCTION check_user() RETURNS void LANGUAGE plpgsql AS $$ BEGIN - IF current_role = 'evil_user' THEN + IF current_user = 'evil_user' THEN RAISE EXCEPTION 'No, you are evil' USING HINT = 'Stop being so evil and maybe you can log in'; END IF; @@ -152,7 +152,7 @@ To make an authenticated request the client must include an `Authorization` HTTP .. code:: http - GET /foo + GET /foo HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZSIsImV4cCI6MTQ3NTUxNjI1MH0.GYDZV3yM0gqvuEtJmfpplLBXSGYnke_Pvnl0tbKAjB4 JWT Generation @@ -374,7 +374,7 @@ An API request to call this function would look like: .. code:: http - POST /rpc/login + POST /rpc/login HTTP/1.1 { "email": "foo@bar.com", "pass": "foobar" } From 5919a371972c2f2aadabadc5946c384257536987 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 31 Oct 2016 22:43:41 -0700 Subject: [PATCH 019/549] Remove signups and user editing Too complicated for this section --- auth.rst | 222 +------------------------------------------------------ 1 file changed, 4 insertions(+), 218 deletions(-) diff --git a/auth.rst b/auth.rst index cc1aecbbe1..17ef0d913c 100644 --- a/auth.rst +++ b/auth.rst @@ -231,8 +231,6 @@ First we'll need a table to keep track of our users: email text primary key check ( email ~* '^.+@.+\..+$' ), pass text not null check (length(pass) < 512), role name not null check (length(role) < 512), - verified boolean not null default false - -- If you like add more columns, or a json column ); We would like the role to be a foreign key to actual database roles, however PostgreSQL does not support these constraints against the `pg_roles` table. We'll use a trigger to manually enforce it. @@ -300,36 +298,13 @@ With the table in place we can make a helper to check a password against the enc end; $$; -Finally we want a helper function to check whether the database user for the current API request has access to see or change a given role. This will become useful in the next section. - -.. code:: postgres - - create or replace function - basic_auth.clearance_for_role(u name) returns void as - $$ - declare - ok boolean; - begin - select exists ( - select rolname - from pg_authid - where pg_has_role(current_user, oid, 'member') - and rolname = u - ) into ok; - if not ok then - raise invalid_password using message = - 'current user not member of role ' || u; - end if; - end - $$ LANGUAGE plpgsql; - Public User Interface --------------------- -In the previous section we created an internal place to store user information. Here we create views and functions in a public schema that clients will access through the HTTP API. These public relations allow users view or edit their own information, log in, sign up, etc. +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. -Logins and Signup -~~~~~~~~~~~~~~~~~ +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 hardcoded in this example to a secure secret of your choosing. @@ -341,8 +316,6 @@ As described in `JWT from SQL`_, we'll create a JWT inside our login function. N as $$ declare _role name; - _verified boolean; - _email text; result basic_auth.jwt_claims; begin -- check email and password @@ -350,13 +323,6 @@ As described in `JWT from SQL`_, we'll create a JWT inside our login function. N if _role is null then raise invalid_password using message = 'invalid user or password'; end if; - -- check verified flag whether users - -- have validated their emails - _email := email; - select verified from basic_auth.users as u where u.email=_email limit 1 into _verified; - if not _verified then - raise invalid_authorization_specification using message = 'user is not verified'; - end if; select jwt.sign( row_to_json(r), 'mysecret' @@ -386,72 +352,6 @@ The response would look like the snippet below. Try decoding the token at `jwt.i "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicm9sZSI6ImF1dGhvciJ9.fpf3_ERi5qbWOE5NPzvauJgvulm0zkIG9xSm2w5zmdw" } -Editing User Info -~~~~~~~~~~~~~~~~~ - -By creating a public wrapper around the internal users table we can allow people to safely edit it through the same auto-generated API that apply to other tables and views. The following view redacts sensitive information. It hides passwords and shows only those users whose roles the currently logged in user has database permission to access. - -.. code:: postgres - - create or replace view users as - select actual.role as role, - '***'::text as pass, - actual.email as email, - actual.verified as verified - from basic_auth.users as actual, - (select rolname - from pg_authid - where pg_has_role(current_user, oid, 'member') - ) as member_of - where actual.role = member_of.rolname; - -- can also add restriction that current_setting('request.jwt.claim.email') - -- is equal to email so that user can only see themselves - -Using this view a client can see their role and any other users to whose roles the client belongs. This view does not yet support inserts or updates because not all the columns refer directly to underlying columns. Nor do we want it to be auto-updatable because it would allow an escalation of privileges. Someone could update their own row and change their role to become more powerful. We'll handle updates with a trigger: - -.. code:: plpgsql - - create or replace function - update_users() returns trigger - language plpgsql - AS $$ - begin - if tg_op = 'INSERT' then - perform basic_auth.clearance_for_role(new.role); - - insert into basic_auth.users - (role, pass, email, verified) - values ( - new.role, new.pass, new.email, - coalesce(new.verified, false)); - return new; - elsif tg_op = 'UPDATE' then - -- no need to check clearance for old.role because - -- an ineligible row would not have been available to update (http 404) - perform basic_auth.clearance_for_role(new.role); - - update basic_auth.users set - email = new.email, - role = new.role, - pass = new.pass, - verified = coalesce(new.verified, old.verified, false) - where email = old.email; - return new; - elsif tg_op = 'DELETE' then - -- no need to check clearance for old.role (see previous case) - - delete from basic_auth.users - where basic_auth.email = old.email; - return null; - end if; - end - $$; - - drop trigger if exists update_users on users; - create trigger update_users - instead of insert or update or delete on - users for each row execute procedure update_users(); - Permissions ~~~~~~~~~~~ @@ -466,121 +366,7 @@ Your database roles need access to the schema, tables, views and functions in or grant anon to authenticator; grant usage on schema public, basic_auth to anon; - - -- anon can create new logins - grant insert on table basic_auth.users, basic_auth.tokens to anon; grant select on table pg_authid, basic_auth.users to anon; - grant execute on function - login(text,text), - signup(text, text) - to anon; + grant execute on function login(text,text) to anon; You may be worried from the above that anonymous users can read everything from the `basic_auth.users` table. However this table is not available for direct queries because it lives in a separate schema. The anonymous role needs access because the public `users` view reads the underlying table with the permissions of the calling user. But we have made sure the view properly restricts access to sensitive information. - -Interacting with Email ----------------------- - -External actions like sending an email or calling 3rd-party services are possible in PostgREST but must be handled with care. Even if there are PostgreSQL extensions to make network requests it is bad practice to do this in SQL. Blocking on the outside world is unhealthy in a database and holds open long-running transactions. The proper approach is for the database to signal an external program to perform the required action and then not block on the result. - -One way to do this is using a table to implement a job queue for external programs. However this approach is `dangerous `_ because of its potential interactions with unrelated long-running queries. However things are improving with PostgreSQL 9.5 which introduces SKIP LOCKED to build reliable work queues, see `this article `_. - -Another way to queue tasks for external processing is by bridging PostgreSQL's `LISTEN `_/`NOTIFY `_ pubsub with a dedicated external queue system. Two programs to listen for database events and queue them are - -* `aweber/pgsql-listen-exchange `_ for RabbitMQ -* `SpiderOak/skeeter `_ for ZeroMQ - -For experimentation purposes you can also have external programs LISTEN directly for PostgreSQL events. It's less robust than a queuing system but an example Node program might look like this: - -.. code:: js - - var PS = require('pg-pubsub'); - - if(process.argv.length !== 3) { - console.log("USAGE: DB_URL"); - process.exit(2); - } - var url = process.argv[2], - ps = new PS(url); - - // password reset request events - ps.addChannel('reset', console.log); - // email validation required event - ps.addChannel('validate', console.log); - - // modify me to send emails - -To use this LISTEN/NOTIFY approach (with or without a real queue hooked up) we can make our SQL functions issue a NOTIFY to perform external actions. Two such such functions are those to confirm an email address or send a password reset token. Both will use nonces and need a place to store them, so we'll start there. - -.. code:: postgres - - create type token_type_enum as enum ('validation', 'reset'); - - create table if not exists - basic_auth.tokens ( - token uuid primary key, - token_type token_type_enum not null, - email text not null references basic_auth.users (email) - on delete cascade on update cascade, - created_at timestamptz not null default current_date - ); - -Here is a password reset function to make public for API requests. The function takes a user email address. - -.. code:: plpgsql - - create or replace function - request_password_reset(email text) returns void - language plpgsql - as $$ - declare - tok uuid; - begin - delete from basic_auth.tokens - where token_type = 'reset' - and tokens.email = request_password_reset.email; - - select gen_random_uuid() into tok; - insert into basic_auth.tokens (token, token_type, email) - values (tok, 'reset', request_password_reset.email); - perform pg_notify('reset', - json_build_object( - 'email', request_password_reset.email, - 'token', tok, - 'token_type', 'reset' - )::text - ); - end; - $$; - -Notice the use of `pg_notify` above. It notifies a channel called `reset` with a JSON object containing details of the email address and token. A worker process would directly LISTEN for this event or would pull it off a queue and do the work to send an email with a friendly human readable message. - -Similar to the password reset request, an email validation function creates a token and then defers to external processing. This one won't be publicly accessible, but rather can be triggered on user account creation. - -.. code:: plpgsql - - create or replace function - basic_auth.send_validation() returns trigger - language plpgsql - as $$ - declare - tok uuid; - begin - select gen_random_uuid() into tok; - insert into basic_auth.tokens (token, token_type, email) - values (tok, 'validation', new.email); - perform pg_notify('validate', - json_build_object( - 'email', new.email, - 'token', tok, - 'token_type', 'validation' - )::text - ); - return new; - end - $$; - - drop trigger if exists send_validation on basic_auth.users; - create trigger send_validation - after insert on basic_auth.users - for each row - execute procedure basic_auth.send_validation(); From d38bfc8cf35020c72836b90e33e748a7caa93312 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 31 Oct 2016 22:50:38 -0700 Subject: [PATCH 020/549] Notes for future admin section --- index.rst | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/index.rst b/index.rst index 39bf27b4cb..840f8e27a6 100644 --- a/index.rst +++ b/index.rst @@ -23,22 +23,16 @@ auth.rst -.. Authentication -.. Overview of Role System -.. JSON Web Tokens -.. Internal Generation -.. External Generation -.. SSL -.. Custom Validation -.. Schema Isolation -.. User Management -.. Logins -.. Password Reset .. Administration -.. Block full-table operations +.. Hardening PostgREST +.. Block full-table operation +.. Count header DoS +.. HTTPS +.. Rate limiting .. Alternate URL structure .. API Versioning +.. Schema Reloading .. HTTP Caching -.. Database Caching +.. Upgrading .. Debugging .. (viewing db logs) From e5d46975073640344e9d6c4da33fec166294193d Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Wed, 2 Nov 2016 20:46:10 -0700 Subject: [PATCH 021/549] Config section in admin page --- admin.rst | 148 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ auth.rst | 4 ++ index.rst | 17 ++----- 3 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 admin.rst diff --git a/admin.rst b/admin.rst new file mode 100644 index 0000000000..68f1aafec7 --- /dev/null +++ b/admin.rst @@ -0,0 +1,148 @@ +Configuration +============= + +The PostgREST server reads a configuration file to determine information about the database and how to serve client requests. There is no predefined location for this file, you must specify it with the `-c` option when starting the server: + +.. code:: bash + + postgrest -c /path/to/postgrest.conf + +The file must contain a set of key value pairs. At minimum you must include these keys: + +.. code:: + + # postgrest.conf + + # The standard connection URI format, documented at + # https://www.postgresql.org/docs/current/static/libpq-connect.html#AEN45347 + db-uri = "postgres://user:pass@host:5432/dbname" + + # The name of which database schema to expose to REST clients + db-schema = "api" + + # The database role to use when no client authentication is provided. + # Can (and probably should) differ from user in db-uri + db-anon-role = "anon" + +The user specified in the db-uri is also known as the authenticator role. For more information about the anonymous vs authenticator roles see the :ref:`roles`. + +Here is the full list of configuration parameters. + +================ ====== ======= ======== +Name Type Default Required +================ ====== ======= ======== +db-uri String Y +db-schema String Y +db-anon-role String Y +db-pool Int 10 +server-host String \*4 +server-port Int 3000 +server-proxy-url String +jwt-secret String +max-rows Int ∞ +pre-request String +================ ====== ======= ======== + +db-uri + The standard connection PostgreSQL `URI format `_. Also allows connections over Unix sockets for higher performance. +db-schema + The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. +db-anon-role + The database role to use when executing commands on behalf of unauthenticated clients. +db-pool + Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the `max_connections` GUC in your database. +server-host + Where to bind the PostgREST web server. +server-port + The port to bind the web server. +server-proxy-url + Overrides the base URL used within the OpenAPI self-documentation hosted at the API root path. +jwt-secret + The secret used to decode JWT tokens clients provide for authentication. If this parameter is not specified then PostgREST refuses authentication requests. Choosing a value for this parameter beginning with the at sign such as `@filename` loads the secret out of an external file which is useful for non-UTF-8 binary secrets. +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. +pre-request + A schema-qualified stored procedure name to call right after switching roles for a client request. This provides an opportunity to modify SQL variables or raise an exception to prevent the request from completing. + +Hardening PostgREST +=================== + +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:: + + Nginx code goes here. + +Block Full-Table Operations +--------------------------- + +Each table in the admin-selected schema gets exposed as a top level route. Client requests are executed by certain database roles depending on their authentication. All HTTP verbs are supported that correspond to actions permitted to the role. For instance if the active role can drop rows of the table then the DELETE verb is allowed for clients. Here's an API request to delete old rows from a hypothetical logs table: + +.. code:: http + + DELETE /logs?time=lt.1991-08-06 HTTP/1.1 + +However it's very easy to delete the **entire table** by omitting the query parameter! + +.. code:: http + + DELETE /logs HTTP/1.1 + +This can happen accidentally even just by switching a request from a GET to a DELETE. To protect against accidental operations we can add an Nginx rule to prevent DELETE or PATCH requests which lack a query parameter. + +.. code:: + + Nginx stuff goes here + +This does not protect against malicious actions, since someone can add a url parameter that does not affect the resultset. To prevent this you must turn to database permissions, forbidding the wrong people from deleting rows, and using `row-level security `_ if finer access control is required. + +Count-Header DoS +---------------- + +For convenience to client-side pagination controls PostgREST supports counting and reporting total table size in its response. As described in :ref:`Limits and Pagination`_, responses ordinarily include a range and unspecified total like + +.. code-block:: http + + HTTP/1.1 200 OK + Range-Unit: items + Content-Range: 0-14/* + +However including the request header `Prefer: count=exact` calculates and includes the full count: + +.. code-block:: http + + HTTP/1.1 206 Partial Content + Range-Unit: items + Content-Range: 0-14/3573458 + +This is fine in small tables, but count performance degrades in big tables due to the MVCC architecture of PostgreSQL. For very large tables it can take a very long time to retrieve the results which allows a denial of service attack. The solution is to strip this header from all requests: + +.. code:: + + Nginx stuff. Remove any prefer header which contains the word count + +.. note:: + + In future versions we will support `Prefer: count=estimated` to leverage the PostgreSQL statistics tables for a fast (and fairly accurate) result. + +.. _hardening_https: + +HTTPS +----- + +See the :ref:`ssl` section of the authentication guide. + +Rate Limiting +------------- + + +.. Administration +.. Alternate URL structure +.. API Versioning +.. Schema Reloading +.. HTTP Caching +.. Upgrading +.. Debugging +.. (viewing db logs) diff --git a/auth.rst b/auth.rst index 17ef0d913c..5ea384e7ba 100644 --- a/auth.rst +++ b/auth.rst @@ -1,3 +1,5 @@ +.. _roles: + Overview of Role System ======================= @@ -197,6 +199,8 @@ To adapt Auth0 to our uses we need to save the database role in `user metadata < **TODO: add details** +.. _ssl: + SSL --- diff --git a/index.rst b/index.rst index 840f8e27a6..d42418d559 100644 --- a/index.rst +++ b/index.rst @@ -23,16 +23,7 @@ auth.rst -.. Administration -.. Hardening PostgREST -.. Block full-table operation -.. Count header DoS -.. HTTPS -.. Rate limiting -.. Alternate URL structure -.. API Versioning -.. Schema Reloading -.. HTTP Caching -.. Upgrading -.. Debugging -.. (viewing db logs) +.. toctree:: + :caption: Administration + + admin.rst From 0ae281f15f278fe3562a5ac77eca4ba234846e70 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 13 Nov 2016 17:19:13 -0800 Subject: [PATCH 022/549] Debugging steps (sql logging, ngrep) --- admin.rst | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/admin.rst b/admin.rst index 68f1aafec7..5b1ae7319d 100644 --- a/admin.rst +++ b/admin.rst @@ -137,6 +137,42 @@ See the :ref:`ssl` section of the authentication guide. Rate Limiting ------------- +Foo + +Debugging +========= + +The PostgREST server logs basic request information to stdout, including the requester's IP address and user agent, the URL requested, and HTTP response status. However this provides limited information for debugging server errors. It's helpful to get full information about both client requests and the corresponding SQL commands executed against the underlying database. + +A great way to inspect incoming HTTP requests including headers and query params is to sniff the network traffic on the port where PostgREST is running. For instance on a development server bound to port 3000 on localhost, run this: + +.. code:: bash + + # sudo access is necessary for watching the network + sudo ngrep -d lo0 port 3000 + +The options to ngrep vary depending on the address and host on which you've bound the server. The binding is described in the `Configuration`_ section. The ngrep output isn't particularly pretty, but it's legible. Note the `Server` response header as well which identifies the version of server. This is important when submitting bug reports. + +Once you've verified that requests are as you expect, you can get more information about the server operations by watching the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below. Find `postgresql.conf` inside your PostgreSQL data directory (to find that, issue the command `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. .. Administration .. Alternate URL structure @@ -144,5 +180,3 @@ Rate Limiting .. Schema Reloading .. HTTP Caching .. Upgrading -.. Debugging -.. (viewing db logs) From 8258631b03e32f2651ae1e74a16a37cabc8d5ab7 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 13 Nov 2016 18:11:50 -0800 Subject: [PATCH 023/549] Schema reloading --- admin.rst | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/admin.rst b/admin.rst index 5b1ae7319d..b586e99da8 100644 --- a/admin.rst +++ b/admin.rst @@ -174,9 +174,21 @@ Once you've verified that requests are as you expect, you can get more informati Restart the database and watch the log file in real-time to understand how HTTP requests are being translated into SQL commands. +Schema Reloading +---------------- + +PostgREST's database schema cache is a common source of confusion. Detecting the foreign key relationships between tables (including how those relationships pass through views) is an involved query. To speed up regular API requests the server caches the database schema on startup. However if the schema changes while the server is running it results in a stale cache and failures for :ref:`Resource Embedding`_ in API requests. + +To refresh the cache without restarting the PostgREST server, send its process a SIGHUP signal: + +.. code:: bash + + killall -HUP postgrest + +For the future we're investigating ways to keep the cache updated without an intrusive setup procedure or system resource usage. + .. Administration .. Alternate URL structure .. API Versioning -.. Schema Reloading .. HTTP Caching .. Upgrading From aed5618d285ea2efc0c1789f77f1667746e8dc15 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 13 Nov 2016 22:44:18 -0800 Subject: [PATCH 024/549] Suggest pg-safeupdate rather than nginx --- admin.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/admin.rst b/admin.rst index b586e99da8..6835aa9c14 100644 --- a/admin.rst +++ b/admin.rst @@ -90,11 +90,7 @@ However it's very easy to delete the **entire table** by omitting the query para DELETE /logs HTTP/1.1 -This can happen accidentally even just by switching a request from a GET to a DELETE. To protect against accidental operations we can add an Nginx rule to prevent DELETE or PATCH requests which lack a query parameter. - -.. code:: - - Nginx stuff goes here +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 `_ PostgreSQL extension. It raises an error if UPDATE or DELETE are executed without specifying conditions. This does not protect against malicious actions, since someone can add a url parameter that does not affect the resultset. To prevent this you must turn to database permissions, forbidding the wrong people from deleting rows, and using `row-level security `_ if finer access control is required. From 954252ed164efc2a2de4839691d7a58b02dc2e5a Mon Sep 17 00:00:00 2001 From: Raphael Schmitt Date: Fri, 18 Nov 2016 18:22:51 +0100 Subject: [PATCH 025/549] corrected url (#24) --- auth.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth.rst b/auth.rst index 5ea384e7ba..d98fbc8bba 100644 --- a/auth.rst +++ b/auth.rst @@ -188,7 +188,7 @@ Next write a stored procedure that returns the token. The one below returns a to ) r; $$; -PostgREST exposes this function to clients via a POST request to `/rpc/jwt_token`. +PostgREST exposes this function to clients via a POST request to `/rpc/jwt_test`. JWT from Auth0 ~~~~~~~~~~~~~~ From d1d6001a6db3f357f75c30020444459e4ad02297 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 13 Nov 2016 23:04:54 -0800 Subject: [PATCH 026/549] Alternate url structure --- admin.rst | 16 ++++++++++++++++ api.rst | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/admin.rst b/admin.rst index 6835aa9c14..0151c09af2 100644 --- a/admin.rst +++ b/admin.rst @@ -183,6 +183,22 @@ To refresh the cache without restarting the PostgREST server, send its process a For the future we're investigating ways to keep the cache updated without an intrusive setup procedure or system resource usage. +Alternate URL Structure +======================= + +As discussed in `Singular or Plural`_, there are no special URL forms for singular resources in PostgREST, only operators for filtering. Thus there are no URLs like `/people/1`. It would be specified instead as + +.. code:: http + + GET /people?id=eq.1 + Prefer: plurality=singular + +This allows compound primary keys and makes the intent for singular response independent of a URL convention. However for any table which uses a simple primary key you can use Nginx to simulate the familiar URL convention. + +.. code:: nginx + + nginx code here + .. Administration .. Alternate URL structure .. API Versioning diff --git a/api.rst b/api.rst index 0e9aa142e1..e45952bf8e 100644 --- a/api.rst +++ b/api.rst @@ -11,7 +11,7 @@ There are no deeply/nested/routes. Each route provides OPTIONS, GET, POST, PATCH .. 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 Embedding. + 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 `Resource Embedding`_. Filtering --------- From 79de60db581f2656f933f7143b5e4b5db1f66296 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 20 Nov 2016 11:18:29 -0800 Subject: [PATCH 027/549] The -c param is no more --- admin.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin.rst b/admin.rst index 0151c09af2..bb81338edd 100644 --- a/admin.rst +++ b/admin.rst @@ -1,11 +1,11 @@ Configuration ============= -The PostgREST server reads a configuration file to determine information about the database and how to serve client requests. There is no predefined location for this file, you must specify it with the `-c` option when starting the server: +The PostgREST server reads a configuration file to determine information about the database and how to serve client requests. There is no predefined location for this file, you must specify the file path as the one and only argument to the server: .. code:: bash - postgrest -c /path/to/postgrest.conf + postgrest /path/to/postgrest.conf The file must contain a set of key value pairs. At minimum you must include these keys: From 684a9f95666541cdc7e33c874f56d8ecc50568d0 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 20 Nov 2016 14:22:46 -0800 Subject: [PATCH 028/549] Spell check, WIP Circle CI for sphinx --- admin.rst | 4 +-- api.rst | 2 +- auth.rst | 4 +-- circle.yml | 8 +++++ install.rst | 2 +- intro.rst | 6 ++-- postgrest.dict | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 circle.yml create mode 100644 postgrest.dict diff --git a/admin.rst b/admin.rst index bb81338edd..5ba5d40976 100644 --- a/admin.rst +++ b/admin.rst @@ -92,7 +92,7 @@ However it's very easy to delete the **entire table** by omitting the query para 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 `_ PostgreSQL extension. It raises an error if UPDATE or DELETE are executed without specifying conditions. -This does not protect against malicious actions, since someone can add a url parameter that does not affect the resultset. To prevent this you must turn to database permissions, forbidding the wrong people from deleting rows, and using `row-level security `_ if finer access control is required. +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 `_ if finer access control is required. Count-Header DoS ---------------- @@ -138,7 +138,7 @@ Foo Debugging ========= -The PostgREST server logs basic request information to stdout, including the requester's IP address and user agent, the URL requested, and HTTP response status. However this provides limited information for debugging server errors. It's helpful to get full information about both client requests and the corresponding SQL commands executed against the underlying database. +The PostgREST server logs basic request information to stdout, including the requesting IP address and user agent, the URL requested, and HTTP response status. However this provides limited information for debugging server errors. It's helpful to get full information about both client requests and the corresponding SQL commands executed against the underlying database. A great way to inspect incoming HTTP requests including headers and query params is to sniff the network traffic on the port where PostgREST is running. For instance on a development server bound to port 3000 on localhost, run this: diff --git a/api.rst b/api.rst index e45952bf8e..73fa46ceaa 100644 --- a/api.rst +++ b/api.rst @@ -146,7 +146,7 @@ Note that the server may respond with fewer if unable to meet your request: You may also request open-ended ranges for an offset with no limit, e.g. :code:`Range: 10-`. -The other way to request a limit or offset is with query pamameters. For example +The other way to request a limit or offset is with query parameters. For example .. code-block:: http diff --git a/auth.rst b/auth.rst index d98fbc8bba..f945336468 100644 --- a/auth.rst +++ b/auth.rst @@ -61,7 +61,7 @@ You can use row-level security to flexibly restrict visibility and access for th message_body TEXT ); -We want to enforce a policy that ensures a user can see only those messages sent by him or intended for him. Also we want to prevent a user from forging the message_from column with anyone else's name. +We want to enforce a policy that ensures a user can see only those messages sent by him or intended for him. Also we want to prevent a user from forging the message_from column with another person's name. PostgreSQL (9.5 and later) allows us to set this policy with row-level security: @@ -310,7 +310,7 @@ In the previous section we created an internal table to store user information. 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 hardcoded in this example to a secure secret of your choosing. +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 secret of your choosing. .. code:: plpgsql diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000000..ae7ac312b9 --- /dev/null +++ b/circle.yml @@ -0,0 +1,8 @@ +dependencies: + pre: + - sudo apt-get install aspell + +test: + override: + - cat *.rst | grep -v '^\(\.\.\| \)' | sed 's/`.*`//g' |aspell -d en_US -p ./postgrest.dict list | tee misspellings + - test ! -s misspellings diff --git a/install.rst b/install.rst index 5edde5e553..430161bcfa 100644 --- a/install.rst +++ b/install.rst @@ -21,7 +21,7 @@ Build from Source We discourage building and using PostgREST on **Alpine Linux** because of a reported GHC memory leak on that platform. -When a prebuilt binary does not exist for your system you can build the project from source. You'll also need to do this if you want to help with development. `Stack `_ makes it easy. It will install any necessary Haskell dependencies on your system. +When a pre-built binary does not exist for your system you can build the project from source. You'll also need to do this if you want to help with development. `Stack `_ makes it easy. It will install any necessary Haskell dependencies on your system. * `Install Stack `_ for your platform * Install Library Dependencies diff --git a/intro.rst b/intro.rst index 0baee66198..e90839bfab 100644 --- a/intro.rst +++ b/intro.rst @@ -10,8 +10,8 @@ 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 db objects than to add guards in controllers. (This is especially true for cascading permissions in data dependencies.) It's easier set constraints than to litter code with sanity checks. -Leakproof Abstraction ---------------------- +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. @@ -28,7 +28,7 @@ PostgREST has a focused scope. It works well with other tools like Nginx. This f Shared Improvements ------------------- -As with any open source project, we all gain from features and fixes in the tool. It's more beneficial than improvements locked inextricably within custom codebases. +As with any open source project, we all gain from features and fixes in the tool. It's more beneficial than improvements locked inextricably within custom code-bases. Ecosystem ######### diff --git a/postgrest.dict b/postgrest.dict new file mode 100644 index 0000000000..5966273cbc --- /dev/null +++ b/postgrest.dict @@ -0,0 +1,85 @@ +personal_ws-1.1 en 0 utf-8 +Auth +Codd +DoS +GUC +Github +Google +GraphQL +HMAC +HTTPS +Haskell +Heroku +Homebrew +ILIKE +IP +JS +JSON +JWT +Logins +MVCC +Mithril +NGINX +Nginx +OAuth +ORM +OpenAPI +PaaS +PostGIS +PostgREST +PostgREST's +PostgreSQL +PostgreSQL's +RDS +RESTful +RLS +RestSharp +SHA +SIGHUP +SQL +SSL +Sencha +SuperAgent +UI +Vondra +WAI +api +auth +authenticator +balancer +centric +config +cryptographically +eq +gte +http +ilike +jwt +localhost +login +logins +lt +lte +middleware +namespaced +neq +ngrep +nullsfirst +nullslast +param +params +passphrase +pgcrypto +pgjwt +pre +refactor +signup +sqitch +startup +stdout +tsquery +uri +url +verifier +versioning +webuser From 2a77ff5714aa19a07fe0e6bccd0aa8e8ecd8af8b Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 20 Nov 2016 16:37:18 -0800 Subject: [PATCH 029/549] Schema cache rewrite --- admin.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/admin.rst b/admin.rst index 5ba5d40976..a7d145d7f2 100644 --- a/admin.rst +++ b/admin.rst @@ -173,15 +173,15 @@ Restart the database and watch the log file in real-time to understand how HTTP Schema Reloading ---------------- -PostgREST's database schema cache is a common source of confusion. Detecting the foreign key relationships between tables (including how those relationships pass through views) is an involved query. To speed up regular API requests the server caches the database schema on startup. However if the schema changes while the server is running it results in a stale cache and failures for :ref:`Resource Embedding`_ in API requests. +Users are often confused by PostgREST's database schema cache. It is present because detecting foreign key relationships between tables (including how those relationships pass through views) is necessary, but costly. API requests consult the schema cache as part of :ref:`Resource Embedding`_. However if the schema changes while the server is running it results in a stale cache and leads to errors claiming that no relations are detected between tables. -To refresh the cache without restarting the PostgREST server, send its process a SIGHUP signal: +To refresh the cache without restarting the PostgREST server, send the server process a SIGHUP signal: .. code:: bash killall -HUP postgrest -For the future we're investigating ways to keep the cache updated without an intrusive setup procedure or system resource usage. +In the future we're investigating ways to keep the cache updated without manual intervention. Alternate URL Structure ======================= From 216550e9590e8c55d94b49e3018f0728dfcaaa26 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 20 Nov 2016 21:39:35 -0800 Subject: [PATCH 030/549] Content negotiation --- api.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/api.rst b/api.rst index 73fa46ceaa..1cceabdf1b 100644 --- a/api.rst +++ b/api.rst @@ -175,6 +175,24 @@ Note that the larger the table the slower this query runs in the database. The s Response Format --------------- +PostgREST uses proper HTTP content negotiation (`RFC7231 `_) to deliver the desired representation of a resource. That is to say the same API endpoint can respond respond in different formats like JSON or CSV depending on the client request. + +Use the Accept request header to specify the acceptable format (or formats) for the response: + +.. code-block:: http + + GET /people HTTP/1.1 + Accept: application/json + +The current possibilities are + +* \*/\* +* text/csv +* application/json +* application/openapi+json + +The server will default to JSON for API endpoints and OpenAPI on the root. + Singular or Plural ------------------ From dacf39404e598049e3e1050486d76700b52f17e0 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 20 Nov 2016 22:03:04 -0800 Subject: [PATCH 031/549] Singular or plural --- api.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/api.rst b/api.rst index 1cceabdf1b..d9e6e7398d 100644 --- a/api.rst +++ b/api.rst @@ -196,6 +196,33 @@ The server will default to JSON for API endpoints and OpenAPI on the root. Singular or Plural ------------------ +By default PostgREST returns all JSON results in an array, even when there is only one item. For example, requesting `/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, Include a Prefer request header + +.. code:: http + + GET /items?id=eq.1 HTTP/1.1 + Prefer: plurality=singular + +This returns + +.. code:: json + + { "id": 1 } + +.. 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`? It is because a singlular 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. + OpenAPI Support =============== From 455dc5c8bc631a874d89e490239aa30fba975b56 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 20 Nov 2016 22:41:05 -0800 Subject: [PATCH 032/549] OpenAPI overview --- api.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api.rst b/api.rst index d9e6e7398d..e7bc523983 100644 --- a/api.rst +++ b/api.rst @@ -226,6 +226,10 @@ This returns OpenAPI Support =============== +Every API hosted by PostgREST automatically serves a full `OpenAPI `_ description on the root path. This provides a list of all endpoints, along with supported HTTP verbs and example payloads. + +You can use a tool like `Swagger UI `_ to create beautiful documentation from the description and host an interactive web-based dahsboard. The dashboard allows developers to make requests against a live PostgREST server, provides guidance with request headers and example request bodies. + Resource Embedding ================== From b35ffd4931212b7dc19082c38642657fd4b56993 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 21 Nov 2016 23:04:06 -0800 Subject: [PATCH 033/549] First draft of resource embedding --- _static/film.png | Bin 0 -> 51752 bytes api.rst | 85 ++++++++++++++++++++++++++++++++++++++++++----- erd/film.er | 40 ++++++++++++++++++++++ 3 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 _static/film.png create mode 100644 erd/film.er diff --git a/_static/film.png b/_static/film.png new file mode 100644 index 0000000000000000000000000000000000000000..99843b8709145b1f89bf14ea93b2134a9f2b4a2f GIT binary patch literal 51752 zcmb@ucRbhs-ar1PjgpcyP)d;zDI%j!Q6k9**&|do*{eP+D-yEFY*wLf8KYpECw{uR$`!$}=$K$>pPp^v?&TgdHNkgGfHlCA_Qle0( zWAImN{aXA>QBds`{MT9oxwBG~74n}a1y2Jhls%MlQh%v9Mh>+(Ic}O+UiodZ<+9iM zfVfiz@=~9hlD!{i%hs`OJE170S+AycK~awDDenbY1(t6GD%~#5&n&OWc`NPb+N-ob zqv7%AJ=qnaMUqD+Y~?mHs@1J_&PZ0B61@b*y0yHt~_@iXNC?SonCw$;^^~Tp_ycA~{$q#Oc zALAn5z>)NK7A-!JB6nwdLVbWQiflJ%Q#%BGdO}Id}kpKlp$K&6>e{Wb;I4vc+;s5z^ zoN%4XkD{XFrKIlUY9*`kiaLy#75ptFRk;VhBcLcnJMZzWRqMj5E+wmSh=}ZPaalMa zG5xdd>E%LK5lWV6;{i_2AAbH^>`FVYHEB!r9+rE|lc6qQ+fOICICeT?_ri}a@xH9$ z;a}pZ`Z+e`UG}%8HKKnf?r#ngYA#cdw+dPK6~;c<*&c~dXt-bIntr(G4E~(SLT}s zV_dgv-@cuF-@eO5?&A1?wC@&mN%B%>&e)7~{&h$-#WN>I*lm9DU3vL{pNBC!yR-q` zH~YJH@6K3RS!J;0xXg^K)uWWD>AD$-&34}I9}m%WzuYOYOUR7>dSjY-cHVSdk%z~= z6DL~u=}N9PrfJ_YKl5Ox5vyrSw)vU1w|2B+VQTUV{6U{n*y4%uYAtJZnN$kw+b@by+hW^$=O#&1%Gc&6jH;(3bOwFI|zZQ9hI%j!O>@5d^xp|^CnaF+f4 zCYD#~GoMb3zBK!Or1O{ZLV?SSkjrITTU)2W)ExC33vGQ5DR%bKGuS7U1)iSkUu9-e z{b)$tWnp2#)Mbm?juf@4G>Gxqdhk(L*oKpMu_D&ps}CwfUwMCLgKc5Q`k0CG71VNUlY!)X^068 zmBHH$J7Pq$vN#|~wVs|{ILET}O+a5&NMLAa#pKVrnkjCzwEQTxKu?*B&*CxcN+WJd zQ;q#ICNXYIH|M7Yw{G5CJkpl;@#Dw6Vq%dYI?fy3G~3R0tSmRS4#fLQ#Nh=}szU{C z-?_7zPq&axE6U)x^VBb4v6*0QHJ-n_cUk7&N>1jcV9j=YQ`0rVdZO@XzYQw&***|4 z{~Sx&toikZg9i`VMm>1&V9sr*DdWz)dtWmQ&zAYK)sA(QQ1LilmzI+Cc9xJ%&}u32 zkW^Guq^LZ*c$OMpchrn|*(j zUN~Rbnr+@9BskmGHn72}!)-?2RC7`{k&BC~e@4{qS1)Y<-g>J;hufO$+PXK^J+6bN zPM_W(;d*@Ap~V#{z7l>=1)IqqiddM}+vS}1O${~*7acrsz-E5(n$Iv6 zY4-=Ot@u=1H@!7hc6LU;yGs)i62|5RVgzyb*BerP$*v2 zzm$SjaE^YrmrSmmdAdmmpEgaTGbbOPUw=)+Dz(#Xek>=})6rcizDec!yL5Z;w*~Hj znuz3Y`0Gbo8RIzqWC!uGXuK78hHd?GalXD@oDFqM;}# zD4g+UH6Ixm95l^d>G78&1^RX$xfm1^%E1efo!?Dj67@^&O%68J?!IT){F;ZRu zmXovd5u09_&R>CZ;?9%BA3u7@1|MQzWV|w6#x(7Sa&nF=!9=W)F#1t!hj-l{rw7F+ zss;OJuowd(B7FS)>5G=8ci}Rt8VjX;n5530t$Xs>e1o+fR``%u>~!o4+{zgPEOvSh>3|gt}M;$5Hvm?>wl14 zNz2Svv+d!2b{qPSgR#Dzo-b`U)OOr^7qIUv*6=}L;SkkS4aH>D)Lq%Nm6hGKk}JK~ zrYgy*8yy@Re1^C4Y3-4aXm`!YD=I2-Qn;#^keC?o@L@52{Mp4=YOJt#wYBNRo3&Jc z6()yUW**PvczaT+>}sESR0Tg&JDZl z^|iWtLgvkkRllXAMuP6O{hvVysm}kupcrdw>;3!p>lqpv4l4omtS8$@<+*N=L~d^G zd2yC8;8Yz}-TxIs z8GWh@=EhIFG0J=M=J@;f?~l7on{80xU}t{Pudp>#x4xVBor4=DXvxv-8`ms?7`m)WB#mVOzIsF&N$=CDpr@01=~4O&NwxBe?@S z4W%DGY~H<_w8tOKS=_WtJjMKU+?P1nmE4~XU^PajYUVr&55LZ5+4S-jtGF|Irc}C4 z!ON^Hqw(I4D$kWly)Sut1}Ts`?tidLc+=UlXR|Fk{D3Mb=>^j}c{N_Yw9sc~?|X>1 zm2e)QU{{{~utEJZMdPv6KrwsK!LxP9Hv|D|23^BOUd8z`q^_f*D4`07bA~+Y^CwWn zDE9XDbJ-#yBEdIA@a@rmz8!m+V%GXbpd-8FK7EMQF?RMi@_aO~MqlRS3=G70WUI$S zMb%)Lrm6&c%FHE_FNcSOY>z+dZ#*;7M#m-*$#TMe^R8WTqwNLf6z&-df3&hDR zI6FEr+Ts?JHUpb-38GK^Y|cvl^AaOIuR8mGUo`}I|Jy|$enm%0HrTM-FT-}=LVkXJ zinjM6hq!oKGl!=PS6uWz0sgN!#sH;6`{CyLH0=lgn}4p{Czc(BK|NGCq zeom?~LEL5fr9mkT5Pm2M>z6NIhT01vs8+4*92znk9tR%h#V?i#v;9@irIB^L6n%{# za4S2$N``%B9zK4o$}`aP%7|9bxOxuIia8wt?&5jeDxFZo2_eu$)m7#ZU+NYXwm0nfSEU7r)dmVMd2H|Hiy?XJx2>;c);#0O2PVZEx$- zbz{&ctbcw@bR8BpYbpkt0McR5(b1V4?TEQcyZupMprLj3!-t!vM>}dg8TO5hk9P;0 zW$nUEuhkoA%>_c)f`;_gVf2EM(xXz5Pf;hzpDD&)Mb9~Qz5WTHcZyEIe0G|gR04nf z1eU%m$ivv=iu-mRy=tX-43`urxw06D&e_@1 za~inb)7!f{vsPjsFYf~aSB+Z$V(L;gg;tfWYTB5#TlB_y02v^OGk8BDZu3V^ z-=lk1Q}ck$V=>@qw6nK}Z9l(7>zfk`XBilBAMa&X@?O1e-7fGOVe^TP2VeiKx1Y~GMT(T*bzEzEd(6Bg`&i5Ima1)}I5LLu5(T4dV?9*}3}=ie+TDT$5uK2Sm8BjA%?RFrl`R7+jm zRxk!1yOEI*idMek7cJl`x$D;t+4fiKV=;ith9)HJ{}dysfECO$oqj2qi4ZrN_TTB2 zAr35IVPT|2Gq2r#SSJC+ifMTK{VTk(v2f4UD8u)$fenEgJ<)~TH zXV)VV3KuSHLRBd)ElpJa#KBIDD$O9Uv^Y`wCNB?cwWY5**)U;w<#Qr7z3T}u z`Sq=e-4QK2DC&d*Qh%AA$0lgiITFTHD&Z{QUSH+j@E4L@)Aue)|3< zN1#-<`5V}BCp9#7w&Yj}Ki&E3OZ@%sLY6d7fjJqrZnahWbq!57_|PSG@JH~yfS{lc z=(V42(@V(oRR-re{$}5>VS~esP3zXJGp-3QotqfgarEkDltI0fV6OB8ysFPnHmq^V2*f%}WRt5SxDk~o2 zKF?KhZ{t2r&hZRm_QM*T z^Msc+WLT7a`xbnk&HYUi>39Se1Q=Ki^DouZr(B-kQ-iQ%sSN3jYb^>|1K(d#2`ZoK z@GSqkM0Y7MF)_oqX6KP3N6_{m@KD;&3{=s0H~^{vY(t*KY7%E-*jw7-VWpeEq#1A0cL!>G=M$m4%Ky%2S^E@|WR z^mGHC9Q4D7SX8ZcPZeVN*;Z!Pt;{zTt>3tjgNG-jq8t@q%eiys^2aN=OVwjry*_-9 zG7Z_op6L1KN1z)TS3KN{l``PxZ`WB&1sNn%*iuJ8(bpLBNwh-!B{p&y3c-ze~$&!TrXjk48D^e60J@{BcpyuP^maM|&(g z@oBx$%ibYkv!8Z5-)g<4m-@aeC#JK1VF4LIn~FSc{Wsv_MM!bzaBLGZxuq->FZ= zpLqbBIshPv=imv6qg9pngu_TL+RK$v?`^ZQv!By-B_R&a4rSJoMlHEAS8M(a_eG$) zMVly^HNmWq{Y+2+iS7oOdb~GKK?%>IaAnyA^`kQO^nFmYJ~a6oLrnp7PZb2ORBXoG z%;Hjod6HxYZLXW*{HImMd4TF#C&*dvY?(skB7f6 zDcQIr!VqxE`}4~@@U!&%aT);u0VeT0w12#zyJ)DZLQj9kLPITXFcUVeX6Dn$msy&h z@=0YiF)>NH5as>g!A1&%p&LIw-+KM}D1f8tkMMLOeDU`kkN*$ap@1sf7c}|w^2Sdk zjT{SkTv&||a0m2E2)iG1I@~qQpyyGekZ?@^tOIS*$%h}!u;)_k`8W8*@{9O2w zTmrOcnN0zqc34IHZmd_mn<`TKfOT_bR>AM|x%zq)3g}M;M4MTNK@R}C0H}1EsJ&Aqm|x_7*< z%F)F|5I7`BA?AXO%@MqVEAReZQynAO5w}!uaU$x4S~@Fb<1V42F}}4ZjwK*VR@T;B z*%N>=(3LBFt3nQEV!QO$#?%7u{rnQY4qNYiM@L6~&Py@Tf5Au}SpI>L!mGes1c5(Y z0jc`mq9TOODE1e`{?vIfcj4I8&o|+*OaL4_3Jx~PFmFh?4Dhv;g+<$JU@HUFU4?)n zhIo`i%~?J`ECTjJ>KfT*PIY@5$K#;SsJu||0*`@wCXGfEn0~W4*^4D$b z@pTI`qb305M6<_-w^@biN+mpl(4%zbwMAPvK2;z3?Xc1a7m$qW{LiOE6MXVy59Nhw zY9%y@XX+X3d-v`obucA`=P}RaQvg!hZ*K_9IM~=20HHE(hUavK|lW*fQjvFr$lS)@kKoS2{I6`E!-t+Qj#=^70%2O5w*$VP|X=R7p%q zBJI=m)hyoF*6rItF&A&$zrPMZuC%T$v)C~DxHYw!nwn4S?{1Z*a3AufJs&*i!-HGB zdUa=C-=!aJ``FokwB#KAV;Q~F>QYV!s;H^q;O3?UXfX{jxFo$X#sMTXp5~jySm!%P zipzXz%Hu#Uy+S;VFY_wIKk?H|4}C8IxO}es)K7$td&bZ7{!hy!APR@vEeruJG4G#N zhz|j$rD*2(Z9RCQb96NQzT}AFf8efqXnVQq$V$S?4Fil;gzH7x*i<-xKp234jX!Dd zP(D!N`3*j-Cna|H5%fv#2jq97Thi<5>P`#`0=*e|{rRUqI0ERk^_cS##K3o6x zAa{VfA9r7TlO+c!HG^hjo=?!t>gw~^7HyRcshR+2JYB>_OSM zoabxP2U>3ouHL{GUta}?henv9TNI7o0H31dG`;v8Y^f|c?kp4|^=z}Sb`f{~=NDsD z7Eyd%PClPCCx#npNI7?UzQM3k<=tG{0UP|2X^7$r=*1|iO>eCCf#@3lxM6RfZGSDX z9s10VZ^QM4g+pd-)W{U_gIVz^nI!d*Y?fOw_7>;Fnx^ zST#JF*}fht3mvX(eqF10J47KFY-^7Eg z1FkqBk)&RZ`y=oOc8?(^5M2a;SwA!2(!X}w$f{w-(|)RY1A zR^Od7)tq7fkp(zbiAxS9ViTmA&1hB5lRy4Kugb{!)&?DsUHVd5$x~>WY_4OsawFk( zlN#eX?&Wm2Bf_%q9Ka_*XTaOB-I3P5FaunZ0sI2?D+Qexe#aj0E@9!EmKR;v5sQ;`3b3pdaZ?lW$06vf z$?m|j3J3_;E#~+TIJ0|rSUyI=4M-|Cbwal5rwKOzu4|>;Y(4{|09*(zi|VB#y@QGh zquauCUq!&axoH3c)~>Fu*Wf`H2jDIBjC!n`z=~oT6c-mKN;|I~Df=jZSfG~+dx-sW zK1wtg63+Ht`FJSo4RH~TG@EzqxVE+yOErRi1B>VlPe?mq@9I8&+ECMw*e8wIePLKQbLqrrzTw(hwBRp!4wji|5P9022YInZ z)OLsHS}@;xH(Ipv3I3(5s_Nq`DAx>J+i{2IFI?!yUK5gpV%YC>Fq;2j^ieYvsOrCh2Wc`j4_!v^;fqc_L<| zv#aYbY{x7#W^#E`FBd?Ed8pg7%*@S+F5ooVb^lhfg1GK3N{3_5MlEe^s%J`xG_XJq zMF^Qw>p`dF(aE2^N6%GSR%X-vVFP7xX~_xJ9WmzhpfM2}ueZN{9B?B9Ojzl;5{J(< zw=zE#Ijlk4*_FlF-%#9lQy|5hn6nO|1!}&6?glhZ?cw2p@_QCE=^q;_S24{+f@)}Z zxTZFX>M$4#_G#GZACTMe3DnSy&qK-L@vF&V>M&G@zS8Zw z`;Q-2mlPzC7S*&869`uo)zwEO;mv>e@Zlo1Pv>=dqxP5fva;T}x&X{Ptf!~}YZwd#_BmK)!<6@oM&pv2l3IVePl8PvS9PhvzsBKGJ9YP+l$w#~+XI`&&Cx zMR&Tszk#_vefmUn3Y0g^MNJfanU7qJT@%jOz}O0vaUs?)EhrFMF~KCxKWm~*S^HT7S?zM(<1QEqxVFhDZaJp`Q^+cr%2$b(2|)) zy6vt}pby!En6me2tDlQ^kJoi%Fw2x(5*UJeAkR17eqrzYl`w&z30Y`ZvtJ>sqvX7VdJ_6%S#^Nd z=WAWv+*G5km$&zO$ULCo!7UU%)W)4ZqHC$OPVhvBhgTxAu~uv29Y8<29Y^+eNc^Xx z>q@_G+kSrjhtHmIvbv7Fb8R8U2|5u%I8nKLJUqU?d&+ydyQxna8z+^noR=f+uEIl8rS3AZyiI!Ei|B*-7k)v55)6)7PxuZ1-q)`lo~d_DnPl zJioEer|GvH8iPdU`Hi>nr3*vu+qYi}-CUY;EIrE0Uufo}m2Mpq?&`aKZM<0x03fb4 zj1q}W2raxcEZ+qE0bMo2UPP&EacR`!f5_4WQNR6u!CT(Kf{)m)U{H(DHxUw1&)d&+ z8*k-b19oq8Htl7b8khtO9PR6#5icZY=y4 zaG;Jv8a(8L*N3)sDUWEc+q8Snkt2^FBoR>^`|#U~OPPN_sEt3qBlBOaMI}Cjm7ab! zcNl5)3Jlzet&%_hcg7*oK51p8XMZ-Gg5JUqxln?NN6gR(hMrr?lz+h;^`L=u?v75LQO^M2$mu~a);;*PI%C+ z=#&r~`Yf~_1=*hU+r3v*^zB9Sh@;p3MqwhWTu6xNCZ6C{X6E(91P zczX3e?gXy|Q7|3=F4TPLo0l_<6^q>6h(!V2fz~Yr;OPijgmG=OJhXTqG&&@uz?7+x zMIg}}tX&RtMto4m1Gz2u-5cpxL|_}1RJAxaK{X2v;M%=f4!Or9jX;q= zc9ATxnaUxt7|428fcnwcnQy{>_+uxu6fKU+5p2_K=LWinv|#%y?JHOIL)p{cpmM{+ zeXei2kY)S%t5=`pqG9<#x?$o`FHV#Tu~-P*xbtXdMmeh?v_X`t)g}Bq*=otE^5W6h zM2RY4Lvvc7vcxCdc1T4J9FRi7VQ;Jri698{1_8ATMUj|$9I9$SvIG*txNh59GDv8*@aQ1=M_$z4`Il%AW<6sAeVk^BZV0KXE zk}i+KaNQ#ykRelU=ip$14oN~S@QufcSJ8h)t2x1~ny{X5aiD3-?|eXiFxsfp*oyOl z;jdXkpQeyH1VISVLl(_!vsg4YvH2hI*nLPgp+Xu!IEHH$Kvu$-N4T(gvlLzz&{YD= zDs*z7Lbif_RjCm!BRa5x4C%2?ogtxG<)dDoh zE~YQx?lk8CVz#u6Y7 zxI2qkd^1Bv(vf5KbMu4g5e0X-Ra5k4$GRws3mq#-5R_q4vcB0VVTw>OULu7VaXYMj zvgX8A7AMFZBW{S$1_Gw}04QkCO!V~htS&7ga`kN+3?F!3(wpZp=YwTF*g>EV%W5(qiwT6Ixl zb&5@XKHbmng1|Ejd6hJ+9Z21Ic?sOshQEsVu|8HKSwGMix&{ZU5TzqQ9unRGruzWR zz3jm*R9OxDmqSaIk+JbOY;N^QGe~DKD0~^FjeMYw@7!kENKzp5m?;yv+if!soSd8j zK&dQ41rG0LDnCZ-O0uZ$(eLqUl_y|pro+SIRt$WxZB-&fbnhy;PpnPyXd2<&3SO`0 z{QC7Pn16g};5c3wf6@K>_ai!lEm|wE7|xwPe+3#)|2$MLCb?8J)EDPB=G8*7B3AtY z`S6!|@2ER}txT;znCxFAfjR&Z8rEeCRSi7J3W;dc`FkW|2OAzyJZ~6(k0K*|1 z4+nzEk}0rs8u)l(6s3S!*kU)_A!9(V{+D)G8et-~2e48OZL{1oJ~6SX6HyBSTD4XH zi>XVuN|>9O&0?ut16ahR@7cSzE81hTT(3Rd&ZC|rD8X;m#82c&iwHQ!sCgt0NWO+` zOtFG~m5He9f6uyF`cXtgMIV0vxNKDlqltlYqmR2`qF*=EmKOqb^>enl=AMLY8S%0k(O>SWG`nJtU)C)!uD{`%8}8(l&%v6m6qwv zaMyh{*uO|thIN>Jdtv160Dkz!uC%76W-%H%r@IH%mgCenH7YdHjiwDL@5;*7f~gU3 zqvP1M2CqPsC(GNj^*NR>+W0xJ-yLInJequgY0Jv)q4-9$3=a?gg!EII0e6*v)6&Ds z2(6I@;D zMZp@DgmRLXJn$xaVEjx6m5=6yv^FM0a}WcV1q!bC^hv0tth{_HsvuhDT+zycBmt#4 z#9jSdSuY;5M*Lae_IZ-`;1dQih)fU@6%EHW)XZmF^p{*lK8aYoZR4{!%9{g^*6O>%1n08IU>&NcF_jIJyn z#)QN{^aF^s4G0TauE5VHZ@Xj4bW3id_AUu+ZSBkXjsj4)T$kqt=yo6fY%rgRLNJC~ z(TG3K5a3#=Qfq=(JeDl<-FmI<+qOvq$2`}{JpwjMCxxO&HWt zrvXw_ZYGbOy8rh8HIkrDz)qM-tY$=^&k;C++UT)7caTU!8(Ydge0Ye((q8L4Y=b5C zvn_9iw|1sK!ZKX)bilX;h+w~|8OmT4@|)}7kMh$23RHJEN~;_qiJI6T zKsU%`DaF43RwawPJE(5|q#Q&yrjl_GAm)1khtHppmZq9>VT+HCe-4ERe{wFVwL6Xb zh0Ii~%&!Y6T%I?Dc_usj@>Nt+oBZvAy)ofG&nO>jlWpIf#@=^Hr`80|cMebIX0qkV z_5YC?GKxv}Mqf0(*H(+13y3%`wV|0A4?LJ}4TztfO z-G`l)r*n#^NQNJ8N&&_Uwse11qs}VX#)o;H>SuLkCnqPbfUFjO`;PeUk6S>zBtgN+ zS&^^snp63MmuKrnqXVvZn3TEEnS6U~yfuV|&`(;mu$*6&DvtnpnspS4>sBlx^g}bm z!Rw1_0jsTUp~7HGHQW@L^XHM)VSNV-j5<3G%+!~+7GV@9Xoe79`XdPUJEx$quDGP+ zHa0^H;E#Hy6az!x14SC+p^>_$^j1^u6|Zoe`xZg$;r(b%zS7XVyyPVJ{mF`iCC}*c zIDq;^jO&23+>VKf*&#a$A#^<)sO0S@J!SNH4o}UvvP!zQ=AX?>80sjBfg(Qlco^du z=+cgbKkudGV3Y+RQHzXE<>mJQ)jR!uF$RpI=sW{50SdB zU#oK`Ey{p25&U>AGw`D+Bc9|{JcC5=jqp_;A((QR$ipBGcaBUH2jFX%BZ9*Je;iZ# zx1q&EgGX~1)FHW+|B$#LM-yw>G-NLpuhGTl>thJj;o|c8qp1hk^r();ZzrGct;8S^ zam@5grg04};w2$aP(M?&|E}1GfHnfrH_<7Ga38?V3Cbk3UjhE)$C{c%SZxh_(#wya z>)_2;4j++92qKYTr9^P(1LM)*x92dz$k@>>u+W!9UHMFx(ZBy-+k*Ojc4&bg&ybLGIkWYYlf~%QIOisgqq3NoXt8kV#3S|8^aMiaxwBI%fXP{|$f9YItKSrAD+7 z*VMznOO9EN(}Xbq$yqSYf25e7KR%C)(p>wmOsoa`tHq8VG|f40(2}2A^pNW5=^@C% zy!mx5w;krhpj+%EQB@<{09ECc+5rfbs6DPxHW1y>$ z2_7PhBZcMj?)JJ&kacwMbI9&}w^&}BBUT!owg4bgv}5-MG)53{G;~2A`oQRDKVTa8 zZy7(9wBA_lh4IUN>{u}7bi6Qz6^yzvo>R0`6M6MJKV9McFK)!+%K?;s1@abNC0bf)_D zty@onwj&A?kH^QO{*qF{pP~|r?DVB1ltffK{0uW_aE#k@SZ*OE6G&DyD2S0lSfP&l zoDuxpnnxUR5V}@%2CM1lx92+%yhD6v)Cm$Lg(+*vYKlIbKlxP-lr@N0GyPTzH>p-# zZO<1$3Z3M5;llW%KVoP`6$yA|1jsM~kYgtf>W;il$xuxzYzFi)LwI^T8m}wpUO~7a zum}TQAM=?4#!1gXav;14o+|l;$3;ZGAmjmEh{XTk)nph1L-tfg=drS@XoC)@B}8`{ z*ox9QI7q9-Qi+w>jSZ2#-W2rY{rB%-aBPr7dJn|<4#I??^&&E|@K$RHytj@q!2}-Y z;35WaDE<7iD4{#Voo@uBaf~uM6{MkRerRclfP-2Qw=N5ty*g4v`Sb|8F!HI#;h%pm zbUV@B-cHG%ZZ^ekfB*Kb*Lvij`ib#@!jIBG^Blm!VbZ(j$gv@vTpJ_EOQ)oyRwH7P z05(vcaybxm7!7(Cq6#&L>3aG5KSclyf`P^m8`A4oU!-Z0X)C6ZVeD)Y!bVg?sleIJ z$8BTD9%13ep>s$CmH}fCoImFr7ayOIm6eFM4B`7Fnf9UE@i!Dq={z#W*pS;X(WvWT zghbqK5gVz~p*9N5|C;c>le};3KLh^MT1%Qan5IMhyn6$T7J$j&Y&oa0cnrAipIHJ} zXHZJMK36`d<1%~<{P-7x{^5+KR-p|_XCA)3$tLKpSCK9y@N1=!bjDKy15@oU^-vy zutQcW2$V=H&l&!}4*6 zEdaazFeE;F=p8hw9{hO1V>!Qc9@Bp)7AMfxePUwRkbK$4%{}OM@13E60Y*KCYN{7X zoS?+`*u&L4Mv&W@HQVO2O5Z`sM(-eq5)T!3*oA~b&eV++2!~fB_IHD`uA&#y%UEj7 zwKb-d zd?$32E`rzD+5g5<>gww&JVFL1j>)&6SsgwD`nIu{LI*{;r(mXC5D4iC(j*@# zzUUu2Aa7v%90O~t&IT5b`TcGkoBHnBmX;QZFHC5S6pw}BoSLO^2{L-Cq^w*cME(bg z%>!RwUv}MzIt58{m=(;(IQPHZu=Rj?WDfRA8PCd1}*dP>&3I+QZaovaMGv?rkJeObf`lSub=WtMw_DJNw4v!TU&|SnmY4yH6 zt@ikb?pY1mTa8$~Jvu+X$*0|jOPzp!vpo=}mQ-qavR3`0&CYBAvdxVbKdyTOwv1ep{U7w2gtw`x46kHN4i3l=FNrNY68 z*>~PPwI)~bBDXKXwvZb&dY62|deAm6@WIW4cTewI0h8(HmkA8aAFA?<@Z1B$&E1bmrWl zsV+fKrO2iwo#4>Ws~7Iy5X1*1jz|`QBCoobX)NzE6&)Q-nHy>0@dSK=rVdXCw6qih z>t1ktq4W_zbAHZasLe&|lfz2!Z#B^nl`TiO?(@Y~Uk>f*2vx0D4A($I5b|2X&khBKa~IjWH2WcETUh>^&5Xz*Z(Ag3Mb}3z5$&QzI^EKHp)^S+L z1w$pFp`rYk&Y^&D)FFHLG$`Ly5O$;}nP$|8A^K8Km7H~J-lQ{$=& znK;yf8ePC8Xr92%zaG2-v5@A8D~hi4ND(NYhCfaR2u9iGymRr4M-_xk z_m?80blA~rk2JEceB6??jO6gQabQ$V3{t64O5eXfmE4B+$C;FrG^hHOxcf+V#9k5> z6VrN*&|cx&n@1pB_Tlk*$8mZ`L}=c!gTeGH+YS~$>AQEa#m+EnDjFU1KMmk}$Rszd zStjb{U5#@|M~(BUQZ_2G!|bDC_0x7zX+Pfj^-XE))2HQNbJq6uWIPhCkAbmq878R8 zAnuP%PnRKOgy!@+-DAlIU$nuEZ<*n~vpKH&Wp;KFp4G3Oe31=z1@zyO;{_tq7g2)< z0{o@Xjq!tv!pXUIgG_iz1LR4RB*$Z2VbF}#arXPvk4+xPvm4_#QV4Y*e}4&J3frwFh+`mF$^0=H!@ef?EFuBYDw> zFjFT89}*Ut=G>N^Ct-fS!DEA6*_!VZ2->%bUbt}%W-qOq1OG2{$n7GR7#h>m7Opc+b36s8XbG`C!X(gU=19|tHf&d%a` z7O|bN3;J6e@{ogY1;$W_Vo*mc2H&V$X~Knf9miGq$d%w$L{2nfPC$gv%Cp~i6CjAZ zcM9MKu^xz33Imf&24ai^5v9emZXkm7J2LE!Ta&vwqXE| zf^ASQDVZ=~Ule3JZA1NprkW(Lu ziz)De5Ckg47W(suwB*cDZOAPmMKU=xmBl)cTKYI3;MV>7>&Uf{0nmntg{i@Ee2*NX zCDcz{_c;#0H&kdy2;Q&P6>L7`IZ%gA{%w@4Aa#ic#AR+v7BJD&0vy)QaniJ@J!{vl zy<%h(fcPe?lW*L?pjjFYOZdER$1-Sppf;0a6TM}*!>{^tgN`{CZJ!&7v2q_i9?5uP zJ;yjnP#}hM;rXxABSFQcj8(|PftHrxRE~F`p|>AAkdHOwAUCLssdCYlU7TMJaY9)Cq-Nh!wsC3!Z~{O#W_B?@BW!G}!Q-6Jsa&UtqQ;*wm!Ar~-y*N-ND4Xna&XbGMbIg^H-RgA*A$>9V>H~-o5Nn0ZMj;7z+ z?_oRE{}>#UJofv}qMYD12Q%Y#_UTgnt5;=k_dlC5_Q7$BJmKI6VDbuwbl_QDLw~@- zFSXFc2AtC@aL#($_WJqrYXQrC+iwK(Thf}|grFm7?aJ}wfX$fIta^J}!P%vHT;~ zEG@}oAd2Q^+1<3=KYn=P8es)+dcTL80gdKaLvDXYb~fMS*x%R;WG!^+8=!ZXfm>c= zybIet%b$pTa?x^{xNgWKG8PtTX6fn|++q7uuLQmrs;;TQoadC_U|)ZKW&5}&BYY7| z31GUu(T=mbX#8<3L!4M7LNmf0u3H^LSF=I~;Eqj3fJf9ce?KCG*BVlGAe>HEpf8&w zD-xerkAEOC5(Fs?6A;B=>>_^5>Uccc2BQw4;o+}Pi0pcTad5Ao^D!m%~z$R%8UK z_*_?aT9J`V#U^XH3;!F`1RV?%m5{L&*|0%kE z?;S_|*0**(1bjQDpa_BErK{Ng5;%JF3II0oW3fp|zR=F-v6!*3F~+WU-bAdBEK0C@ z9N)kLh|=&v6F}AOumc4lUOZWxH_(FvQ%L#?&vYD=bsU;|CMJ?mz{?TLbHw=KcsS!DEE9)9!V2SnP8Np@NH#7_!Wj}*h6H1Wk zjJ;(;O4!>G6Kf6*4vf2&fS>U5^FP5r?$K*sR|^Venn>g@y6b56RX*KZdIi7wO#EN* zUmgYqp4QUZ4dfQ^L3#b&Lwnb)yMIdi(l_9PFJBmtcs-S6di5$IXKh>kHPzJtwC*cu zFIRm1suX3GO#8@{Mv;b;ucnt>f(Nf-APLvJXTu9r-R+O}$D9g^$NC|I66gYC_!bcL zh^@~0y&q`%?#fC)SvzHKFN6t~uh^AqzP&P%qobqq^z-Y72&t~F{wy#U(Zl@=yk{}5 z+Jz?eYp4!Ui?yKYI&M>!Sk9^z(zvwy7wMc@uc&pn{-8FjNn*8tdOSv-z|j}avW6c& za|E>iNKU$e00P@T0V{z#V6@S#jEo;}fj3yXX$|q_U11p)mxxDVMC?2A(z)eGhv<$W z#S`RGbn*3s`{dM?Q=X3;V;|Le4*?)xaOHFxwVaA8)^`FSX*h%d%k>Kkk1eAWdua$V z-7V=JiMO?i{@{8NWk3fFG2M+7L(bd)1q?AQa{Ax5uqAjr+ogSS#arlkd|Vl?>?gR^ zyLLr9Z6GBTzQd~}6RzWEIAp<{uZ6YAuzB;vBsZqlf(=l_5Jz{quB7((#Pw~{`D`8q zKr{OfeG9Gy)%}X_9-s%mpkO?z3cBw;-2&8We(3ll7X%nu3{_*?oPIkis~@5YJ0Glt z?ucbyh89rI`)VA18e!G0u3}IvkUnP0ePB{5?!M>MU~yacVuQA58isb_Z+-DI~Jw8RyfKHXI04J^^-%$U?P*Vc~)NxeviIi^e4*w_HK5m=N2ZuwcXz$7Z4yp zBVt?M1k8KxmZqNZH&lEDHyx;`s9R7q>ffRHYd@K{R`AK>XO$d%Ur3hFmf-$M6L=l- zXWOB8!($>sBEpJIILL_v+*#eNVAQa1b90~ZWqyY{CDDs>^74?Gc_}EZ^iaEaczMYo zT4-*1K*3~10L5omb)CxU01?EI4u3&JB@!zxQTlp3zqRSno#+wr*n&GB+`~JiqR)AU zgrFeq6G!yzl#`RQA8Gvvi(x$tjZ#j9vyyL|0Xeb*FOmZeJ`_C$Kq)dN56pA5^^J8b z(>nJrJYXouc#w{aa$6466MEd59UhBHszxw+H>SZ#@!;fFmX~hbci6Lx7-n6*ldY)z zkPNbGNGz{CbLI>YZP6bC(M$xu*GD>vYLR8ZgfhW!coN#lqr@G7QW1uSVS_gd!vn{3 zkmHq5vjc%dGcp9QD%8^5({KnCBq@k<<vND0x1Icn198A&#}V2&baNc%Y;oa3p%q@D zNMorJTR#ZlF!GNOo@L=-1w$%4gl&G^1qwCI6W^W3WO-YPJc{Dd)6fxmkZRg^@%xnRI(xRGatT(n|UG^2^WxO@9uw*s0iyLYFk zJoMlCd-tOmg;FIN$@GV^)-B@yh`d~eE4%LPC6z?kU{}LP;?Ll1!k<~E zR~f{G?x<&El-PiC8Qxb`diVDBQX7VzaNt9O`qlIPwPMg4Hiv;vERcSs0L-(>e30De z>ghSHs>%d`a!J{caa+jq=UnLZM*v28!0fIdmxlvO21Qc_Dsd)>HI63&E7nk3HIX*i zx8&Z}ru1n+ywXJsg~!#+?b|s$l7R*4!2y+9K&P|BF>vSm;6d(G0xxITxDGWR3iANF1kOq@mNR~khF=msA?e|`v!U&S<%j1xIKJGU0PY4W-#Mc$p7 zb=p4tr{ce0%xOEU^@bw0xN7M*pkQkG;eyF6ga;WpZ7@L2c#MBe?)>@voSdEz(iygI zKa1oAu4~Om?RA`8LhinjrtXYlx4RZb+r8r+A|Vg-@!GGCBL^pcxL)^-v*8yKg2CGt zHU&!mbgK5E5fcN$J9HB8&Bv;7>tRf+y?QVP?q4hfmtTh1^9Z!0N(`X6!0FaEH?P4+ zeExiKunK-0&O!Q&z4^3^Q)f36S%?UCh>(V<(y0Qq%YE`q z+*c3xqD4_RtBHaU%R+#nI*yz^7Lmq=P8>LaWYd}WFTD$Hf=7?`q7`2O#iIO?03qa! zJr|IW@FE9ht?k6us?}!T0j#=Mo+iolDb8^+-)G!;!(OR#m%O=SfINyzB+H!GC{E;% z5M-Dw3#L?Kn`LEW_Uze1junJ1pOvP?c%w9azj@I95riu#I5+GAI#HG%lIss2J!0Q_ z@lIuPb2z#!-0CVk-EOR1oP+c1 zw8i-HUQW(_s8iI>JP!WncD}<&3zk(}(goJ%0H1zKjw071Qme5x!!1Jy%^Yvgw1io7 z=|}GbgbKXzBQO|J6Mkbl7{%;w7=FPTyEwe^;0&X`zrtZ#2nB;DsQA|aw&2_oD>xTA zPJO|rqH>hjg+l)Lgi%tD#+V zb`3j8E7m7F{rSH`{B;E`oIjs{CW>PeUZ@?EKY#w_N?Y2ep3%`-JbQ9D zl6M?p^s#n{0_pzc<-z42K8!(3Is#V$R`PiLaoWM(6MRF198#A(eSB8wfsDP~C>0ce zf>r>0LXZ|(FA<3e>IbTUdBY4*V=hfaOrC_wG_npsY9jO)!Uta7S+uw3s;N6MhK_d@ zB)d!6i{u(f&K${=Ti`CRhz^Q~gyH3p)E>@!I0@tl=?C{NX&rjgF$1+5L+>Y%pdlxD z;X~pP^?AD~S!D?=kD`Z`LF_gV0TMvRAs3Y17_G_K-uUvsw*Za6(8X9!O$9E(YH3<~ zp;m%e1x7`sW{Df?bl`Ky52F$bI!|83VlGCLArA7)Xa~;W@j~b|0y@u{*$(r0s1>$= zSvVBwFoJRNpD<`4vAjr-PtY33HJx3bh3A}?i~hcSTWmiZIpxsA zOQ85qpyP3(hSEHtcw?pk!d6@ZXr_E&;v=-*<+)lkH3IR$Ryp?Xm%`(G(R_h4F#v3C z)IMcU#%-%st(yANd|^sOq}RO9Rt$Z!Ote0t4m;oJM7 z9NHB_E<0=ZDzMA(dX;0~PR{fvoA#lh1;IUgPA(Nt7=?v}$%$HO6eD0Q^uS7_Nz+2Z z!nm*-$p3}tybZ@6rY7DS$MIR*(3&-hOo0?talArk`UuKK+SCbch@D3GU=2HXJKIiL z+t?^o^?!;=-X{12Lw{O>Gjo@%naX+ww3`|S&s}-GTXDv`|{|F^7Ul_D}LTC#U4PC^4Ig)*`-$~Z1@L z4zX9ElCZAvxtHb*y~R~7W^wPz@4A~@UTT$f@7|g8raK0BPMXx8J-?2b-<_8CE=}Kk zc#h7o+gku{5dE0A47S#GrSw?@tlE3wo92`^&p^ltY7F3!i_MufEqBk=d8Z$qS+%Nv zLYtk7=`8l9tj)Q#b;l0x>qGti04ZXe-*ZlqUO^u&9?#_wHnKmEV$6DJyZtLp~0XOT_sk2r!aHWN* zp56-Nk8?w8)ts7s2YX(v`=SN34fQSeaaM1ewjmA&dBv>T90lw>bj+JYpYiI|AF~VX zKwq{KJHt({b5G@!6OUyd4!XUNJru{0!{^7wx2e>&4omozrPub)D9dclek6|ZKAA$roR95$%t**+wKS=CyWGFINMOO zslY6`cuYy(^s+k35O!@fT;pn`t&{mJQ)!uh+#%7a{DSQjLpso&qf>HO4)g z0S2%*&>&5ghnNBE6eG3f^;a$05eY3@`v z`NC$6SnKJ4deFJXA^ILSu8t&|=HSD|MKzoOoD@jC=(nSwy?YNb2nTUQkRv%+2#u~|fG8y3*3%=Og!b$D#pR<-+ zHq;4{0rGct#%5~FWY6sREw&p6sCRyBtH$efJjhKxkV)YxP@Ej0piTl#8Vd50X<>PEhWD{1w69cYM@h!>(?Xb#-#drw$6li+OPs zWP9s2ZJs9h&Ck&X8{X8ACFDJK%e8-K0W>^kaloGW`sTEFsFFb+X^Wkvd~X4z z%kwhp!I^}J8Z~R8aCmpy9J|3&Is>z#C%Il%W-ipT=`EQVbieISB!$rHj7O*;Qa&me zl}9gLkkfv7cqw1jc!^w_acZPerq=&WO|Sag!m(;jR{?XQXuVZB5p{xxS>@fQPgB8h z91jfNL<`m#=?%9i9(e}RIej9(*4^N0n0Az>AUtH?ZhS>Uzn`T@QV=-U-mmk;&ZnhL zt5%NmNd?3hvJp<;)yNf)oF$C;&h@7n1ZaeBythEd+QpQl0~J~_el-ZF(EZpnr~T#% zls`W|2VN5aR)cgj3&ngvk@3)dx1=+i=CEy_N_m;4rP>Ug$?{*6O_-2nV; zLACA=n12}fm7hv7j8bUbN$U_10fO8^7t~g3#7+!t7cD9iIOLx7p^;1b(ry1HI1j-r-@L+<8%XRyvLS6%9isQ?F3 zH%dAO9mwuOhdlQ5SpL0YWX05Hc-*-hDH|RF2LYQ*3$G}?RmNOHmlt* zHf8-0!7#+SA~Erimk& z)Jzrz+LULkIb&zlyXfy^_rg$d_}DRtrLx(b6Q9|>eS1M2sq`Cj-Np0Fff#htkmohV#P<06V-fyEhX4{|hl=P<-VLGwyH2`X zSHeY6TE|{6Zuiq0^(%g$DK4IO@4C+fr2)j9ihXtUtZYDgj;EuL85H-DxQPGqd`5=q zqD9>gupsH`1Ne3g+u9ZR0iZRp^zDXUsO_wFZx@!9-{{+M&gj@zs{;bUdR2zi9%LP4 zpZT8!DT|0ZF6bimedM}V*(Su4#qVQFkQ_H;epzqYm>xlaw_dbCU}`CYJv>^0QV_CU z2c$pb-V}l1u#5rKfjgN$oG}AM=BXmL5Ej*1q?7FZjlw{5dF%W4TTpq8I5_4iyy@Dp znSfaoY!qT zbsAQ>h|*zlWh=cXt)I_nG5yZ(FK_D60PcOTeA%~>DZlxUqg9^L9XwwmED%e8yj{x6 zYsB!2E`0;DULjjhO026E1NPRoXg+*IbNVv&mt2goIH2rUlEn|t`CVPv$U4ltvga^0 zr|EeK!v_s9b8ybg$XJPl;@w^ELv}@O>TE#4XU|%3P2Ha@);f96+P=jYaIJd%6Y{;-9SQ7wP4t;qQAh%^`nG2chk+1}nh_^y_97^LQE4ePLW zK&o=``c<7Bv3Oi4!7R5Le(H?0rDokG?=IH1jVxi!xuKg2*#Hx9n?=Vdhu&Q5|y5aNi6AfV+q~hHxEq zi!tw~f(&wKgxj}*@e%($C18a8lA^1!z}1`%{x{-*o9=DSc4D^xYVcSLor3u6Qf_YO z%a<=3UNW0sDk4M!gH=2UKUJavGr|Q&_nvb4usjxM2JO2&AEmv4HKV4NvoD7LvFjVs0W(H7GlI7^EQ1n{yc* zr+9c2+@p>C{HpXXy?pWnA4)4amq{>*gwfu_r$Ok*=tp~NqjLKE4f1lxH5kv`1SpfxW1rIxSt@_fwho7+L-y(b27Y!qm1HJy)ZmG`P)T6*KQ4!JQZ4zu^iSA*+A!~xn!qSnEF9$V3RSV*o0!AG=2J2y6vW_3-vOfwwE$5xN|@aFz#s`C-0cv?-V+e2`d zj@SVy;HtDWG#rRZirQ_3sxeZN0ak%Lv?#G~V_}dV$=?AYI#JogjdFMQ%Bv_Va{}HB z$AFgT379I~coDtEq3e`QDQ0G7gH^PFLA%f5&Dq)y%;N=`{#!tM?b4_aXVY_U^<=~4 z0SdoNO`Gy@=Z(|hEvW1#Pu}DHhI$Hg6MuqP`0^n`hHL|iOnp6zt@`Ed1TBHIGBcAZ zMDvcGpX=@A2DZ!pB8u@&NjP-q28VOJ6$Xn)6?(G3I~UJD;i@!<+^u?Y-i!LceX$6G zV4=qym^yiK0J>8Iy(K+Pq_#eBf3-#%|m01)q#BkV^5kjZsiiNq>0hqc6M}K@xyd~2@PHqd~YtgsJ>O- zG|*zdif*J$d<33L#0$#Kk-wBa7JFg%U!=7VcmMwFIOi zsoKyfcXZ?8n)avKRS?WLPtAP6lG9_73VgA1>7n?^*M9Z`rUz(ICIT{po#!QU(j`qY zDUa|nZ6Np*QYcQn>20Gpn4a+f$K7$Hn2=!$lzM1Cyjvh4wc%jz)7-%jJJ%!GccG4xsX*H zON3|eAnm>Qy-Cxio&A0+CC^e6r3~SD!FGVWU$aS*pwp-O2u!`Yt7*$qbolb-*_`@B z2sh?QA&CB9#12%c1TuH8oWF2kCZ;~9KRGfPZ1ZlLT4dU20jv)VB~IyXwyY1l9W z1^@=esrj>tAN;Bj$kDkUU)B=f_^7BTw!^rpc=iw0l9Shx%Q|g3civ2z5sG=tV+q^U z3>l|`&s4wT+^TL!XNbCB+(ifk1{-o)uS5T!`K5QU?Ao;}l_fb(OT5y~c&+Bt^$5}7;6Hjn zljs1BUq_j%R;Nz(r!#N@9HKeXx^c2~Mf?eNI{@x)geANomlxWJR#<2B=)&aQU@P(I zQn|R80*#5|bH@Et*Q9Si+nhEkJpQ61z(>ScCC3ldoj~tWShKGaN8l7q6KjQ=`6_7( z^1*;;pRnA7utmuo?Y_)rzyQD$KZ&&^Gx#^q?yP5j&`IfUrV?{XI0K|ATq+yb^l(4) zw92~Xy&L+|*6}8y1IC*-9w=TC;3Zj;EuG}TYUw;bojY}src0hjk=g(xacTcLyKKqX zADC1is0`qBF&2cE70`%X`U!hDckbEqg``m!_f8*Q+?XeqQ}5wvTX-~8|2{1 zr-M9n2f{J~hbmf3$<nN_e#c7(b7t$Z*W#UPR@(5*C?5 zUYw>9?|_-(rvP_08*dk@K6<8+cz*mF&i-ngMfX22xNwu)p`XD#6YE|XbZ^|8b4(UMsii^Jjf5i(vwp1%fV5z-Ue{)H~e|W8J2bO zJiybO-+wnQ?+o(4Ve&8YMF2@JUFSU?TruijH@&gn(X5KS=Mhp7B=4J$?6TI6HM~Jc zpv2Hg*|qmY>x*k!&$dhE&q{hVbZSDNY2})Bjlu`_YG@sHiZAxSuWA{<^fri9x_p=X zyJqc>b6tka9@}F&2ti3_1nD>vnFP><5A_t@6!+x{E)tpNa~3Y##~v$KA$TjK?=_nB z;PvZ00n;U7nU4TYWKUqzwx~exs2+2lap|4|odv+m(&z6(k#MH`&pu6>gnf^W=3Bq# zp}2(`kuCa=&oxTlUy5K@G&KcIWnHtL8#rW;S_iU^JX~5{Pdup?E>x2k+M|ogCc*`f z&y;}?ghk(_uMQ8gnLNm0+vxNcynS$;OPn_+0;R&U9wz&s8I&{MKNi+#8BOZ2wDFWl zcq5BSO196=U4=gab11=04PleE_m~+%Go2XI-AAa9+*^}3u=)80!Kij6VMG#Z z2*?zp!Gi}6p6Ono3%TylF>TMd6f;pGg5fOo>VqMY8A(Ue=OVyqn8|U~v7xkap0Vso z;*Mo$vdQ%ESfnvB=WR{l>E{st>RBaB)C!lC~bFaF|-yC_rrJu={4FNO`|o{ z4j#iDu-Fo7Oj1rLkibe2bF%SaY>Nk}*;RODbOTP2qx&y{u3y_>Z5sgkz2?E>6GDgIJQPpu{OG`1>Db($~S^W}li^DMzkYMTed;gv4 zXn^^NWc`ucFh`%NAIt6>9P7a2O&=)vOasQXSx%iH+D(7@49ZFe&@3!j3)}+C0^*&1m!K#K0dimX|Jl??P4quCH3zTEi3UrfiR1xTA0FNaYR{-w zudOmdhB*gdAd6n7OBd%A9Hr;HdEJ@nBI*(Qv@}#Er|A zF4d-aeI}2Q+rWWqCQhBYuUDTwNuI#GA#}!AP-uz_t zt-b42jlnO6W7&b?3qzb92TBqmrj`2{mv`n*89zS89pY=R5<;!5dxB2{Q*q~%aY!td zcWad})jAVgiAvCqdMCX^O7f83Jw z)Jy`hnwC*7clFTso;kB#KQAw@%};8^58qdxV-I)A$5w|T7uH{V`U<4mKld@53<@$6 zvyZ1DYRV=ia0*wY;@rmKu%3dBBw!$rB z-ich2jIERs6l*&Xf=ZsYY&jg?2=>FuPfP zWt+v>51Te?Hi_^xpKJed#vk2x-02S6b5V^5;A+BsdJ$tF8a)8R)cST15yHVyRU!7{ z+!EiAB?4@V1F8Q@t7GwSUR2;ohgcdD1(GP?=#z5L4m2Dqw{3Gvs2JHi`F5yA$9PH_ zsqScBxe&WKZhpOK@jq!Ks=s057Qt828*T!+^eGT?UT)x7c}Kz)JEVsP#$FuTkhC9} z%L$HvGUkBK3`##(zH%Pe2zS3rdX&Z;w-WnVjxBx%_LL`&9{B_Qh$Adv{W4Ug$+y9~ zIGXGUG2rJ&;)_7%lS3SssPJjR>4X&-0gEm`{BUf-cJ}r~^du$D{qg%&OaD(mcvtyJ zhm0#%f`H~^Y!B*@-pjxBk~mDZ1)u`plVUJ8lRuowuEMB{YfWJx$tidV&}Utcv#@j% zg^w+e!e{bu1a2p?MuCjj3D-Dj)~pS{P@uW=c9SMdIFIILEq}nSHL_6HKpIZ3_r-7C zNDv{OYWzmq;cJS!y$OB)@uMNRK)^qVPoC7PTUYX~xSF>rKQFg@mb3XVZP{x$i_BjN z_m|%`I?hSM(WZ2H{IYjw&&Re_Lv7}0fBMeASX+YPu(G0cclo{pMfDZlJ*Dwl1`zyf zBV~=nraWcw>$~0gGRJ1EjXciXa{lL*$veT!X{7qOw?B2!9XG*f49M35YfZo^zl+Vb zMnvIXe@zQ#>tl@PnjJd|hZkNOeR&#Z!0M0*;7{WD*rZw2@odKBvCiyj4D^U|DKxA1 zlSa||25XEK$Rq8&X+Zp$mT3d3y793+o7+8W!`_TQ18w3ctK z{4cWK#LRzl_ACE|R?F*=xvuH%yp6xC#8{}-BYbAf_XF{7%4)(+;_idrMA9)}{a>KQ zo5~_4*r6GB(5H_DXO`xbaK3E}PbgaqsOnRO_Qr8YC)<8AAS+3jBz{E%7nEcw67ujr z!Gi45V)vjwVFOQnb(P)gecNCedny9DHEV>Um65QNxH20BY)ed%$~Od^R2n)|KZp`m zK?{gA8VMYyZW(AsB7P;f$1A77-qi%km!V5HbD*J{kN;3^YAE3DW!10z zhiG$R#RM-e(a+vq9h*e43Rz@pZ!a_Q03292S5a{sfK_Bi1{T}1b*dq!Sfo^pF81X9 zkx@7z=^FF{Yjx$ym9l>WIs?%Va<{)jhp{eT<*2A^lFVk`o5?=LK~%lvo6>J5so@$W zaff$P(3O~1caqeQ4s)8ZY|H-0;+xtxI@Q~03~ga$*8kK8j@iF_@Q@+Viv~Msvp?4v z_Gs&$XY7VrpSePRf&F1q_xbHMQPf1dto-@P0@6_J(eqtt^~5=B(onAJld#GI34v*%(qS(~Hsb|1PEMW(`@%?-1Kox?0YFu^1dkqDOYh>^5>ED_ zz@+;1!v3KJAQl?Npk>KrKi z(AI~6EETQRQ6!Z?1UFiee5+jlmnW!oD%UBC0B-IAt|M`>-O;<*oBsiIeJg8P zd}-|RVkba!TIg!9nS@~PH;iImLVI7nZS7z9+&l$&F*gjJ`fQ4qm#DJE?8C+&Y6Ct^ zGQFg9eP~8@mui3vjrj>{dd}dnmT4stQpbinVb-jnTng}=K6I8jK8u8HqvDBOXB=V; z!p-A05h7#m>hc;>pdJX$zRbS^!9qDp-Y?g%K66-px~`a4CDICRLEMf2V{MwLF4`KT z&UGEkxnM6dxqmfx(wFhj4xxmc-y#A`uIwJwXOJ@pgaelqFU@Zr9?`VDJ3*t8vOh~* zTBOH8V_bu;C!ZVqCzlEvyX*WGq4EM_;2(pW#{&Q_d7BnUX2Ze$nqYlV-g3&=w<$Ag z#iipJ>f)KDE#|CJsKB2c?TRZOw*&qlzTd`PI5tpE{qLLOUXhu=-$$aI zoqi@b`x48w0Az^cHnsk<)%lnbpj>D%&PHuH1eY`RTzi(XZSd^Vj2S6M0iVNunHx0d zOlv@ki@<6=EiQMxf6gx&bIm+by{Q|qt6g9N%AZhM>hBQ=>ni8Zn|I;Xt)tzxH|!Yn z;`_%xXne6{iCG_jj8goEeOh_A}dXp|hxS=OOEq%v5 zX;`#aqjBSb)cQ!!^L&iCJ1vLvl055T&S}K}_M#UrA`e9o$4U94a4=}l(kItHFieIl z9Om@@DOfpZH;z`BuD^!NdSz~ih0Yw;$>LsPt0Qz}7^zER0}ZmS0IoAJF;?i^vgW3r z3g2KLpNL9C#BB@n8~q9B224;x60F<_WJu-qOVkz}KFmP)&u-}Ein)$f5H{N^m-GOX zHF$Wy+W;Nsa=f{2KZ7=v^%JV>zz?7y_ z?B*Wa6R_E~x6Xj#F>lA6>wE7({yv8RnV*)tKlyOh+rk5%N`1%Wm->v&J3aMKeueK? zv@jQu@2Ef=sOkNV9~QKwA+^^_>II5~_I*Z-8`qMg%<7M>-kRlt9B?h2mx@9UtU~$0 z9{-3#ehunl!0;Tw5N}{vX-HonnM`;Qb5guV(jnC5$aEfz0fBMqlV7yb)RaJ0ChjI@ zEO33{oA-1KTE#Ww7o!F^k69XBd=raBogk{S){opreuRW$(hLPjW4(8MU;r zOh;xVUBcFPlkbVTpeEtxL&uKw(vO|0)xCN@8fw>*-Mz!a{GgR zTRU>g7K6eCB(24DBdR`b$J#Q$S-c~F>||iA;9|FCxEZf z1qVyU94h(4Va}W*xSFS(@|iY$dN7ap$j@Eb89>Zq8!!JBH66deM4)7Sd`8CMS>^}U z-kyq{aw7lUSXcJ(V`GTU%@0~xEgdz@+nb}WPG=-AoiQFE=WnY70oPfFl#(n-kcc)P zt}%_Q7L=k6w@1~Voc+yt1EyKYYz=GP0RY|;=o7<3vKR(0NuZPb!D=nuq&HZ)%uNj7x+9fy!rhES@IzxcfGQxjDP`+mW znF9z87yM$|>%>1R68}7f1j@sdac(+@SG5VU3A@BBz!9J4?b^_H1jm!-kIlPuJbXfL zQ01sETeht1-Q)fVW*s{^()V6%c!9@4U73iQFm}%#8;(Th1Z|q$493KT3^xrgA`(!O zdPu)_Z#Pr=n#kq(iQAWsIKJLBVF|~#sz1ieW7;E!4Qoxk(UxM}#@F{t*F%#$J6E=e zHB}q&byZ@dnVWvSjwHJ@vX6Vetqhe<#G3H0bQR>UjY*wf8!YT4gDL%u`aeOw?|JGg z@NRS1SjzrO)ckSpVY~GhM9k4hBxXMZO>r~9ZCuTb;+$nn8`>(gI%9j3f1|n2&1Ze( z=Hs*KN!huE4u3ojY|o;FukWs=wC!*DHaD)0{0(Tm<H@sz+n7Fta(5a}< zi^KyHb+7R}^uJ3{CBt5CJ2o5fP3Os1Hkr?yN3C^NQTp2I zeLMD5Ad;GhmXNWzAAgPScO}|#*+r?V6TZ=ia>X!lfoW+e<0npB$qC?42u!`_5CaI- zfg?OYP;*hJT7|arB9(32)?>$xndXgkpE>4GR+K~WzAAJfO_@yC410t>I$CKW39INK zGNRY>^b&^vj)-&@txPA(bWkmtFdEGIblx@!SzQxS4YOR5=7FD7^Pj(Q{^CyuNL$OB z-mKl|g&e*C8kn9gbKp-t#6lRbz)WA?$9c*7+7^`!9@3g965chv=-Mqwiw+HKs5tr> zK(9_Nq_8u&AK<}*{H9jIs#U8#q@<;#eMkYYeRb-7oYCX3$S_MWqj|n{$RD0M^%KEt ztDuY}q5xe_EGq#?RVJ4s$;D%U*ArjYk7J;J+Adp$^@mM69Xm4lE$Jw|cE!cD9kM9C z9fgFmqjt_wS|Dw>67JE5hhrZ-Z3No_%WWu;!=zh z5yk5}b?-g^w(WQ9g(eT!#~Ke$4LlfoXhqd`CzIKQZ-IKPqc6HDc|2D{D3Q1{FZEbl|=+a65_L>E1hTE}{82aI}fm z2!wA!E$v+DGwk~uXa#}-q~Nn1JUDD%HDx^dvpn;?CDGz4i~$Huo|$O0F~L|{y*N4} z7rkj>-sP-2uV1~Yt4yOW+qUhaeO$wht@=KnX=1ZSQ@6OXxy(WiE!i(PAtnKh35p__ZPF!{@V;*U(8LkY_1-UTs|9Uk*z&{ z<|^+;n3{5XFQCpK6k>DeIpi_<_AN?M$B~3}8L+Z5;bh2c)EQj<7u+sJqV*J5wsW{o zhdyXLEvLMh07i?npg&nk?~vR`s%t{{q~YK1pi;#hEMvIYQU}0Z11Ya#GmNDj=Fy9& zupS{(Q_2V|p`n@AeSO{2Nl2es3g?XQ?^I#Rv19Lu8yHXT ztoXB~{pRS{fG^M=$0$%q+eb z@}Rh73H#%*S>68$7mFS;H=qVGX!h7xLRB@ap`dFHGuGCyPQQ9J;abLDc`{T8>Rt^S z&AW<<10N2k*rab6_XS141d4%N1F!aGW``L?0uOWk;lozI0Ip~Amr$^<|6c$nF9VCT z{p7TL+qMaS-ZBD_hdz+DHiJ!3G7@2*o8Z;nw83d-j}gb;0On&qoXGx7cyLVxIjLxc z85HWYbLkyTP0Nu(biAz_+`2iZR;3~{4jUnLLFv4F0O50LKzt%<}xg-Ci8EpfA!8LX2i8S2rs0JxG^siV2$CFTTu($;in$GI+4N z`*t{<@Vv3jX6_$+AKn6ElQku-Q6&;NmwfH%cE4swTadam0(DXRsnQJ)9uvtRLp8N^D9s5iP^w1HK5V*0 z^|p_=AH|bnJF}gcS=gJxl;q?L!RkRw^5W|^qWkl^^u)aQ61YP!TlR(P*N5DBGL`=T zZ*wUg#Z`Vukzn=pRaag!#Dup(J8XjhEBn=?nKRdOd;1uPRZ7mN%wf81@nHaeS}b5B4$F?u4Q6j zq9bN@7NwZO>K(PvFD`oh)B zYT)5rC7N!T!eE0P$_Df^ZsTSKL7)OQP}C4)^5I(-j&pX_ry~<9{OY8nBnDyoJ?~_a z;NcT^-@6cl(na>WOB5@n+Ul!e-5Miokob&>)Hm;c|7&MZV(ZVVODgw#*_~*~{@fPs zat$Tehrfohm(!w6f`rJuxIQa4KY!4K33?zz48Ob5#WDk-g0-F9)vmK^>s|kR;=|I2 zh;7?y)A~-Eo=`Q^aLAnl8Bvb2#QMu8=G2)0lBr}_{kVly0BL}T zAj3E8+r4`gTXYO{hKy6-k@1vG?{lr4K|%5L-yLd~rs3ka%H+Ft+ecTuYNu~8GItHf zp@?r4vE(|SLtRemsn?s4=WcW3;?phT)To$m-np}e`@Mfm_oN>{n|bC3ne%sS{8_S< zQ$k1aq$OGyMt`v8igq|LrsDHZvTClajpGZ|Exs{M(1;GHToM?k;`0eVMT^HcLW|l}B9uBUCY+2ZJ{hj?is+s-KLnDE;%!;CjYM{k6OE&}TdZ@;!1b^s{)@f>Z>BJM*1qk$U; zpRsM~JyP3rS(z8e8M(b+mP>a~0s6_?^Sg3uwr^dUTW1oi*3F!p^ZdX~rZNeIOl{hh zbdaBrcj8)OHV=qgYG;`J5g!27*JA)Wp4Ezgnwre%-LS7TyX{JoR(7ut;)*#%E6?gDviZ>he3S0f`gDdjls>m03=_cIo< z@DPjL8L{JK+f9*?foPEaG&SNyPrWO6!~L<@W0lh7)en4QE?&l4Y}@|YD01Z61leBy ztK3e0#_PhTPl4gXudrGmQ2UHx)HWUax^;O$%k(6a2`)dhKP6 zd|7#QYWpup4v+9NZf;Z$coCRP^;rE$*4mt~#ABWa{bX0rb(b*?6*acpa1Y(FV=Zwk zt=afT92{mW1p>pY_h+2kH>q1f@=TKs9WJ9Kc#I}vZv-F3X<5vXOz()Wuzn7um8WKV zKIzup(6FX`T=L*9cPOx^JIlAtam=(BvNQkD;+alj_JMUM!@rWS%a!+Z@rdu9)Q6$& z*a7OuwQABN9Ss%-m1hwYUm39J`3DbLqSu;6&=?QcDRQ?f=`{xyduCC{+ct;QFN_0UAGbY9Jlo6PhNITM`pHiDf9S4J;*2 zI03J7KQgpKj(R1^>|_-oD0YQNQH^IN86TWK*!7T%Iok|9Phf;_U&vfd*nOOa4SRR( z?SKJU-r49`I-}41 zj446qME-3Pc``t_6oqQ!>9c3x@vYe4Qg0`@QCZ>vs*U0)vD2I4t%>7S!N!vRT#fDi zGs*D~N&Zwe<4GBI{n0W!KZ5e|0!;%zZ-0&-N&La}H4))Dd)^v0ukcbvf5b*^jn`9{ z+VXN+XBL<>a(dCYYwPGZJd=qCN^8C;x@Zv;STlYcd~G0mph*WSt8sKNaxqc)l3UbJ z2?!4lr;`oU&SzIw=_dpul_-Bp<=j|(wKeKEhmbnz>ii_A4b@U;-fr4Os%E5SI0FR2 z5`1nmh1c3@t-%=e!n*_Z5(4eE;6dVo9}la(d#q-8yHx0s0T#s7GI;}{q3Vw`X3Btv z8`LfDM{r=OQ`J6x`?e!z|DKaPJ#`d%r_)(J@$qMC@eF)$u06uxhNy!kaK+H1_2WC6 zeyXUr#G}l5t)-}eN*z4d;m6m9RatN5%s{BBtzLtMUQu#4y=7NIhOK|;8AbN!(aq?w z)$=nyR`zaZ5uos{|bi1jRc{m6Mod#D4?I6U2UX9iiKV1d>A{8Zxa{JyC8IMu6!sssAY+Gf*( z!cqCTwFEiIZrfwVudA=l@0 zYHG$`f1S)YncF$uh z7E>7ztl`ggn4XI*`gPC2jdw#Ol;ik+Osg`u6p(|AuFA!msN3%AXzibr6iutf&o3%=aedKPq03@Mr#= zJB`7KJdayu-|vI-1FFB263z|%#PTRdFcGDyS)!}V0l}56S8r_6o7va0vO+Hr zyzz*A`zK4ZcStj?e}A;6SC?W_LvIG~yXRASRYS{gq(x%1-Ed8Lx5Q`7g=nk2B@gb=cw`r0EDS**Q2!LC zvQx_K#qTQ-FenNx+#tY6=_n~JaDmq$r${CwfdL)CHSRwHsns#tKUW8ZR) z`u1X!9Z5l56GXE|onzYd+5t{Vf@mNSbeWFW0I8zv8{(`3&aD1!a*JVr5;N|d)MPPQ zlY+=Wb`xu$R1X_7&5Vs1A#5nP0JaK;5st+RB6L2@A3WsHdBhfM(Ia2$eag*rS@w|n zEs#%)h;lq9+Gp11Kc2XRz2(oGg5x6a6LvV3l2Sua=|_!XE6NbhB1)Oom*U5vXwl+K zalQsIYJ@|`7~AKbWI!i8hHbgArMhwif-PY`@{L$DEJZv6I-nrVmi$K}{5Z+q zSFKzbzyYld+-^Yec99s($#Xb{RsBJW;`Z+(?plNPsL|zco#FSrTL)-B8)zvu)22=9 z{c!cvc8A&n&WEM!28b0ILEgdL-8!WF+y>Q0AZ8j8I+DUPCMIUC(bcesEn8OcbLX#J zt1B7NT?+=a1wVq+1?RV>3XChbpQ6V@%k$EtyO*{+2#ZgofP$0a0fr~pfFP=rI zE-@GGg3R%p$Rj8YZShxc%kkKGGWASN*Kp2r2C8RBW0^E&ICs2Bm&s~gpPw)*#HM4! zP9Jj%n=SqSp#^vhCZtXw8Y)@i4S;!;zc~xN(rfAG-wzxIko&R1IYBmT!gCDT$pFNt z)8|UgI{PGC?S9g?!Yl5htFbo9nMH4oKEX$!p`~Te4vweY!X6e#lwHapa5&_F`Zlo);p;eGs;WxRAK!?imIcS4S>q1#vg|0j^5`6TzQe>UPZyH6WTU3w1K&D^_x3@0^OwlKj3_jg=)_bya#?YW{y=vstof7b| zv~mwX@>+0oMm(U|mD&(*o9er#pSrebBV(1qCc+~ByflEJneZ2nU%grji&_ZWo<%K0w<4k#jG-6UUJj*w)J1$%lbXZC5Ku$xoaNjP|Fkr? znHj`caF(;4IE*aIDjpF%Gqbvg zRoJmsvSHS2(6TNV#oQ;s@lg$bk-?Td55nVtFRPQviOIzjL2FD*Kb(7`m>n@Jq)zSH zn#x5OZST9sOquek%|_Lj(rVF^F-}gKiNINbIQ5V1?j!ovs8J)>{d3uFzgo#7g1Z^)yVo<15^G4iR~F`_;5h zdiAiG{+E$;Mih3-#a%=_{)9F;%&-9|krP0y=utLmGi|emBA$cWdEJzLJma14HzEGA z?D_`O22jFv$=9HgE&P6QDM2DGO+IlwinwFF5U*G<4@OCsRrmV!dN4c1pQQo{7qf3) ztw(lmC(_yMiTZONbdfYUE1}I@_NG?N2kO2_p!6m2(h*+haw-$&g*N4Q>vq}M+5K_5 zu-xL3IY1}CGhfNhuBLF-oM-W-Z?PLGl5v3ZY6>!t&bUR*UK3iZhWpp;?aNz=qXhNJV5Li!s&{VpL$+a1`m5RYc-iV zKtw7{R3pLC(4>2%$V_HN#je}`iXIrHfHBtI6+ChixhA>}8-h!=prW?*NLrrX&YQz8 z_vRdO<)a@=J(dKfle{jObQ=A!f5 z>K2vvOdWswO@;W4l?X12D1N6E9zSsGH5Ey%e*p8&l?*!Zr&fHz^^y`mA+ArP+|?C{ z)n$lOp!P%NM2Q#=iYe*cc9*HGVxH$aC%EWaSTu)gXXHfW_1KO(4@WMKbwgcGa!N42 zFOEp(N@OfQ{&7`@)A>Szt%-D0%Qk#Zkt}s5Ia1sAlHCMl(0N z0b+e?7L?Fah!!6lb95O37e|jD{(uT#r0>IT-utv~3gA)cpEQWH0+i9kITxugp#4#MIiUMj|u>XD4*K2890T;xy71Nt5MRyloQSl2>41kZD0{H*a3utsVDl=#d>}fQsD4m4VFA$gEh3T=~*QuzQ;k&;;3=`mRp7cOIDD9w`cN4 z;QU^U-k{CL5d97v$wUrn@`Co~l^ewRbxirN^!bYyPk1Dfca`vLJ?8uJtcGPpwagun z=8-%(_*MUoefzco$(02~?751?DL)rNXTRJ?Gj*wmzuSeGh#WaZg8VdeR78&J`Pv}1 z#KdyXGL+%!Px$0gH;Bdx?mXEh1vVph*%nhu?P<5;H-9*L=Hl`B9mfptsOC6uqUqhe zOh+(YGMlw}`coGkLB8{r2e`L>e#Vg#ChR(9$+36Ox5RqpnCyL1Ge%Bkm!SsU)IPjz zKstSnINDibu8Ta{Yv%R!va@LOY5^YR@xQ)bue)NpXYt1;iNHA52{aB2z^GLA*h&zZ?5^<>jkrR?LQ01g@YkxlZPo`>4l# zySKO)ekb`?-6OB^TVe;FaSp6~Y4li2f1O#}mD#WLaePi+0_u!0Z`kW6S~+V6w7)ua zaqwQ#$0CN>zaIrY1m%_o=l*Xzq4JcEOWZO_GZ3=N0GW!#sR>onK}V`7ZViXnxPF2x zwmorBl!9nNQ}*xER1g{J-EWr2qrlwh^Eo+Ay7x7#Ta&nP+_Cr*vY_N~S5k-hhBi>} zdyo2Z7ex#%k7S##h*o4!tJFG_=^0nA))t(ONIHyPS=WvoawtiS__u_1`|JL-{D~Tk z{OYJCcaMsl4vBr{_k{~1v$%aZ?03_Xo!HLVrvnkfs!F&lbknfs=N^ZIUMn$-PV{v- zI@Hy1jLB@h=g4Pa2(sQZHHb|Fcf7zLw8P{mDCwBJq$?MrFFVl%st3=b(NiIv+uD09 zEvrFUP)$MGrptaOI~gSBcrf`~D?|i`4reAOnwtL!_@IWC;|zh|fTEj+V`jXa#O@t;CuPa@#FlwwF0Gb3I*z%Ecz{b;d1i9GUW9rOFIbnd z0k`7?ZWc}V&G|>;*RZn%&#HgxRdg8LY`6$jH@C0o%E5qvqlMf=iZj_%A__>z zN^@`L;j$<8V)os;x=7_VU7uSk>VtM-#GKr`yh|(v;?08r31m?VE_ew7TfwUv`QY@) zlj+DZVa=qgknk-WTR@h3GkT1e+RA5)1NtHO>H7TeAYeYu!dej3NCO>8qg3Pb-s)(6 zKb=>3SRM@kaZ`uJoN!K*UsIa2H11Onf2QS7UZ7x^3p}*wkx~NhIpmlBL~i~S2g%*O ztohuLefeRTIR?lrqj)jXhfvuG{!P|L_>jM1bOOP4xoGPc=EdiZzqSCvQvebW(7t<~ z1^F8VyEB;wL?i3f?qSAqT!JJpZJ>&F$j|Rtr{s#opSU;7ym^<}|R*x2Cpv z+;B9w5K|&}F zgQ-g0KhCq+K5|>8x7CmYmk>&zAP}Ems?T5u$PkX97jGiN!`tGsF`+{^ccR{Ss@>X5z$HNZ)vu zuZc%DYLuLcpuBE~$Wp&ouTiL2eqLefO&8y)7vIzLVs)&s+wFVv{*eApWTVRmt%=6j4^aoR#KGO-5B&%>0C)2EN+uE2cwI$;q^A++DflI;JM46{hAN8Clgq!GW9wN#?}~^> znt5>Omk-bNmCEY+o)N~{;2#3S4s&+S@aqDE;WZ3R$;gw>;$UyXzjY)2K1g51g;y9;u2lQRaH z=4M}i_|U^wI}Rnnn$i7jJ`^R4ylRx^ffP_>*hjZq)H=A7&NGT2u6x6FvBQi(UaIj- z1N(D&)de@OCb>Z{TS{2_kUa9;t{*mZHksX9AZLpoK5;r3?93|tKH6G6y4q!M`j*ih z60;xz;e1yU%W?L{7jH@R@ti0p5NGZL3dfVgg1j#;Ps;HXfNa7Iqcjb~61xr@sK-v_ zcFOC1zr0CJlCsbSAib+ovwm|m508gkv^Ei3CKWuTfF5TB@TfF%2-8Vn7f!zTac%D6 zho#?=;ONQzn49f-V`Yc5n1!yDpFc~aHs*Xi;q!}4+|##2jc}dqjk^+!8sJ?$Im|^r zjwoa!W&Fs~vxF3b^7Yl@$my5h0yv-$a_xMoq;ou_L3eXnm2znqUfn;f1ClLxPWD9+ zL0!LI53Q6U^>d2FfI!$stH+jT3{a)~tp<2c&8@^QexCq<%@}Z86Vfws|9#(B2?=3y z(t_ToJeWWp=yMM7h#KjD2&h0+w?UXLm~SwNPoz<`=JImz+SKA(mYk zcL^rd1iZ@#smu%Ad>qstViUrbSOqd@8%W^xCXyow=n+H8BRrvzuOFh{XPA4;h{A(ti!m~g4J6^A?8Fmq zK-;HxjE zt~AKYnnH}?7bk69))$4l)u)!X{8rJ9=y&K)A4%%=S?YOR*>*D^xMZ}ASE`AP%d$S+ zaW8H$ng#^!;QIbK4s5VZ=tPq4oaMVTGK}h1ncc`+AZrdTpo$q%nVd6p5aDDw;HN$9|icNHJ zwB>6M;fp~-S}p)S;uPNT99_75TRVE``{2Dh&{K5W0<{f1d&Mzqg{l2uzHNTOFy8(D) zFr|cLO*#1%z3Eow=is)g!9rO|D598z5xi)3CA37+Cg|qRBZLqJs~fU?4(@?NhyK~M z^dVIf@nRdKEx%N(nYV;yfyO#MZxTm-Q(*wZ2MiiC3A4xV3lRzHhDr8oH4$ zETgf`$f#M$yg(8ugzqin;+5gb$C;oF`Lqfo$+~}kNkC-de2fA^<=xXCM}?(e&w-s5 zK#51K41mV~7zpQLhZApz2a2C#=YAzq zNC;xAwEnocX#?MZ`$HY=A6m#mUm~ATA^l#V9}~a}gx9%}xj5Ik zPOi_f5#B-oDiJLL7XEl!^`ike)EeqOXtJyW^VoEnAXyf0Ou&7XlaKEPn~uqH7V;GW zp);3%ecdx9zb{rp+$9Da6-)q3XPIg=YP6biA>{-f=aLBbP)B7H232a`$({iCps>gG zFWe1MK!%`Tc9(Q@btl69f~FuzY4zmUv(;?rq+L2RK8>J5Oi7@7pJ`NpKs*Lb9pEC< zK2c>l8f@_w(L5ZQ_%R^qoEL){b_NVdIl+0rdmA=)8$DWhRL?AuV>d-bZNQ8D7!?DC z|A9z`sPS-Vc3his>4v zA0>vO&+~Xfl&2%=qCj_Ft9B)ex%3j`jsCuIOnkf=yX$2N2O0Qb_|fgc^EjinB6gAG zLh1zDV<0$w{f%s;){^mpmS$U=!o3oFg>nc zPm27X=4BI;o%j%TbzDZfx#bN!8XO-%zoMeB??|ZomE&y*fQ&)?eUQL+}rURsQhACX@B9lC3fAzp}kS;-KQjO zu!9$IssoUuBN!nIaxLAz*SDr0_@rOEMJz=gJRbM1NP$G>1}!d=SOFI^knPa4K7R9N z+-`H=Bk`?`9Xl42SWCdmgK>AXoMyTwPp&PN0%4I@?G%Cia81hyu^~OzQv|y0d|1Ai zm<{z4bd_pwRMzliDDxlTy=otKn-Ohti;z7=hfO6xL$U)u`~W}joX9xJ?mgB<24#Kv z@b(zRD$z3=1#yJk83gLNXUT7r3*B>UsMrj2{f?jJisC06JL?pQJ2l;HYB-v{X>H`9s;oDPw)yZjMtAxZG@VDyY4if&%ah!{qW#*;#_D7#ET&9< z5eKH?&zJ}?8KQ;nfa=9NB?^puJlIx>?_dOYM>eak1CZW)SAL#6sc9OV-FjBsp4(u*_0hBUqamQ^7MTm3dJ#Ok=wvbD)l(2HXNoV179aUy zT}0@T361$ysujEq1I>l37JCRoSRPZr4Pq?LhUUFjvfBt|fMc^hR)sZuCQ;N$Cw1&t z)VC^eGt7VM*1zsHMX4QbXY-rOgBECR0@Tb&V{{4Fm#jOCJ za1CV+ENQ}iT6$5mu@?t}L3xd-oj6mm88&RwPx@W!+JuFaO8xuyzqYfF4*D78zgXb> zfj=3Iq|1?$yBr$hZ_nJTMO2Ey_U`#4l(rJXK|GCUYnKOX14??|s&Ca4TKRuA$jzZH z`X_^&yTv^Emu7+YgKZ7yn@|O|Zr8PE&svIj$`sDLe|zQ>=0kMxV!2E5jzn*`b>~ja zcWr`e_(SHY>1~4RrFy?I(Y5v)AQ-Rt2dV4(zTz2OkDP7)?>jV9mBUo^GEkJEdHtN6 zG}&p^^K4$o&Q7`}nd#43I{0`@9Uzc^^i#s=rQv(;4D2F6^v7dVS-nMqTV>BJ&cwgT z?6URv^C1{)3&nZhT><)5O#w4CxxQhflWF)OoCEk*hj~|UK)w3)yP)Ur2`g}7nw23c zGNj@0IuOOLQx@w|{MbO^&M0L2B!6K;e0;on(#D{do|$ZAKf(bA>Vb;k7?y6;cJSbf zoR$BO7GcO{CB)CM4A`}xHANW1$oFe1-7{GdqcPM^UIor56+0`qL-k`Rh0* z5jh+~i2!3{dK3tyl_aL%Ls3xx&4e2y1p=+4a3ZfBo;!DKx!B(kmxxpjyxR@|#vwLf4=2XdPyrE`KR61AH0PuryP7Jt0oXwD000yl$0D*`qbiRb9s9GHB@2Y12ZhhHD zQ`m4X!=xSY$}^SQ$(e6M35)+pN0EV>N(_wxhQ_2B9yohgIg${CzsL&Mxi;o6O5O}M z%aGB>rfvQ*lw!nmy=651QLc%G^>{cleCXXofI3s`I>_(k^h6PP^`m%3Sb2!%SEI_{ zK}^rg92BvLQd6b`c=gc{z>mjpk0w>VKX(0NGeV=IqmKKKQyxLkt$iKVCYSU9+rYuKd+PJaIZDF@~^!6=7cRzJW+lOGSH?^hhB#LDf1=+=% zQ-ysipK3`ebJe+>eFi!;2~zF!t@`LB7yWVl4yR~q9?~dNiiVu=k9$=6_=)33uQfH> zKQnh~p9#N3)VLq%`TcB{x{u?UE??8h)pfww{LU@^&-#-sWa=V=r=^~G{&~eJtzUoZ zdfXPjD6tY0G@O0*QQ7Xh*XQ{yzyDK!q0XmZLyXDjRzf|p=0hHLcT%#*O zM~r{Ixa-F+a~SsY{b0G2yZ!c#xb=lLa>?6oyKc|cW~vY`t5LXHx4(bay5zla$I~`v zR?9F5b^0sZeOi6^rX$yUCXO>+4e84g7M_;6mGfg$qgTe~ HDWM4f7(Sx2 literal 0 HcmV?d00001 diff --git a/api.rst b/api.rst index e7bc523983..02817aeb44 100644 --- a/api.rst +++ b/api.rst @@ -13,8 +13,8 @@ There are no deeply/nested/routes. Each route provides OPTIONS, GET, POST, PATCH 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 `Resource Embedding`_. -Filtering ---------- +Horizontal Filtering (Rows) +--------------------------- You can filter result rows by adding conditions on columns, each condition a query string parameter. For instance, to return people aged under 13 years old: @@ -54,6 +54,17 @@ To negate any operator, prefix it with :code:`not` like :code:`?a=not.eq.2`. For more complicated filters (such as those involving condition 1 OR condition 2) you will have to create a new view in the database. +Vertical Filtering (Columns) +---------------------------- + +When certain columns are wide (such as those holding binary data), it is more efficient for the server to withold them in a response. The client can specify which columns are required using the `select` parameter. + +.. code-block:: http + + GET /people?select=fname,age + +The default is `*`, meaning all columns. This value will become more important below in :ref:`Resource Embedding`_. + .. _computed_cols: Computed Columns @@ -107,12 +118,6 @@ If you care where nulls are sorted, add nullsfirst or nullslast: GET /people?order=age.desc.nullslast HTTP/1.1 -To order the embedded items, you need to specify the tree path for the order param like so. - -.. code-block:: http - - GET /projects?select=id,name,tasks{id,name}&order=id.asc&tasks.order=name.asc HTTP/1.1 - You can also use :ref:`computed_cols` to order the results, even though the computed columns will not appear in the output. Limits and Pagination @@ -219,7 +224,7 @@ This returns .. 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`? It is because a singlular 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. + 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 singlular 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. @@ -233,6 +238,68 @@ You can use a tool like `Swagger UI `_ to create Resource Embedding ================== +In addition to providing RESTful routes for each table and view, PostgREST allows related resources to be included together in a single API call. This reduces the need for multiple API requests. The server uses foreign keys to determine which tables and views can be returned together. For example, consider a database of films and their awards: + +.. image:: _static/film.png + +As seen above in `vertical_filtering`_ we can request the titles of all films like this: + +.. code-block:: http + + GET /films?select=title HTTP/1.1 + +This might return something like + +.. code-block:: json + + [ + { "title": "Workers Leaving The Lumière Factory In Lyon" }, + { "title": "The Dickson Experimental Sound Film" }, + { "title": "The Haunted Castle" } + ] + +However because a foreign key constraint exists between Films and Directors, we can request this information be included: + +.. code-block:: http + + GET /films?select=title,directors{last_name} HTTP/1.1 + +Which would return + +.. code-block:: json + + [ + { "title": "Workers Leaving The Lumière Factory In Lyon", + "directors": { + "last_name": "Lumière" + } + }, + { "title": "The Dickson Experimental Sound Film", + "directors": { + "last_name": "Dickson" + } + }, + { "title": "The Haunted Castle", + "directors": { + "last_name": "Méliès" + } + } + ] + +PostgREST can also detect relations going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). You can also reverse the direction of inclusion, asking for all Directories with each including the list of their Films. + +To order the embedded items, you need to specify the tree path in the order parameter. For instance + +.. code-block:: http + + GET /films?select=*,actors{*}&actors.order=last_name,first_name HTTP/1.1 + +Note this does not change the order of the Films, but of the list of Actors in each Film. + +.. note:: + + Whenever foreign key relations change in the database schema you must refresh PostgREST's schema cache to allow resource embedding to work properly. See the section :ref:`Schema Reloading`_. + Query Limitations ================= diff --git a/erd/film.er b/erd/film.er new file mode 100644 index 0000000000..d19fdf61b5 --- /dev/null +++ b/erd/film.er @@ -0,0 +1,40 @@ +[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 + +Roles *--1 Actors +Roles *--1 Films + +Nominations *--1 Competitions +Nominations *--1 Films + +Films *--1 Directors From 6a2ba25414a975e90f070418008f16ce33cd132d Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 25 Nov 2016 15:01:52 -0800 Subject: [PATCH 034/549] Custom CSS --- _static/css/custom.css | 3 +++ conf.py | 5 +++++ 2 files changed, 8 insertions(+) create mode 100644 _static/css/custom.css diff --git a/_static/css/custom.css b/_static/css/custom.css new file mode 100644 index 0000000000..ee869c6da7 --- /dev/null +++ b/_static/css/custom.css @@ -0,0 +1,3 @@ +div.wy-menu.rst-pro { + display: none !important; +} diff --git a/conf.py b/conf.py index bcc453897d..8479fd3019 100644 --- a/conf.py +++ b/conf.py @@ -284,3 +284,8 @@ # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False + +# -- Custom setup --------------------------------------------------------- + +def setup(app): + app.add_stylesheet('css/custom.css') From 5a31320a8b7906ebfbcbf75d1adcb2519b0903c5 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 25 Nov 2016 15:02:00 -0800 Subject: [PATCH 035/549] TeX description --- conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf.py b/conf.py index 8479fd3019..489726976e 100644 --- a/conf.py +++ b/conf.py @@ -269,8 +269,8 @@ # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'PostgREST', u'PostgREST Documentation', - author, 'PostgREST', 'One line description of project.', - 'Miscellaneous'), + author, 'PostgREST', 'REST API for any PostgreSQL database', + 'Web'), ] # Documents to append as an appendix to all manuals. From 5b95d22e2ad44343cf9b6eb5cd47b8137ea3cdc1 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 27 Nov 2016 12:28:10 -0800 Subject: [PATCH 036/549] Base nginx conf from Ruslan --- admin.rst | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/admin.rst b/admin.rst index a7d145d7f2..b5bcb35216 100644 --- a/admin.rst +++ b/admin.rst @@ -71,9 +71,30 @@ PostgREST is a fast way to construct a RESTful API. Its default behavior is grea The first step is to create an Nginx configuration file that proxies requests to an underlying PostgREST server. -.. code:: +.. code:: nginx - Nginx code goes here. + http { + ... + # upstream configuration + upstream postgrest { + server localhost:3000; + keepalive 64; + } + ... + 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/; + } + ... + } + } Block Full-Table Operations --------------------------- From 8c2a95b98e3d3bf7202ae0b39b12898bb4dbd577 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Wed, 30 Nov 2016 21:21:57 -0800 Subject: [PATCH 037/549] Test suite instructions from @dsimunic --- install.rst | 143 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 122 insertions(+), 21 deletions(-) diff --git a/install.rst b/install.rst index 430161bcfa..4bf888f48e 100644 --- a/install.rst +++ b/install.rst @@ -14,6 +14,39 @@ The `release page `_ has # You should see a usage help message +Homebrew +======== + +You can use the Homebrew package manager to install PostgREST on Mac + +.. code-block:: bash + + # Ensure brew is up to date + brew update + + # Check for any problems with brew's setup + brew doctor + + # Install the postgrest package + brew install postgrest + +This will automatically install PostgreSQL as a dependency. The process tends to take up to 15 minutes to install the package and its dependencies. + +After installation completes, the tool is added to your $PATH and can be used from anywhere with: + +.. code-block:: bash + + postgrest --help + +PostgreSQL dependency +===================== + +To use PostgREST you will need an underlying database (PostgreSQL version 9.3 or greater is required). You can use something like Amazon `RDS `_ but installing your own locally is cheaper and more convenient for development. + +* `Instructions for OS X `_ +* `Instructions for Ubuntu 14.04 `_ +* `Installer for Windows `_ + Build from Source ================= @@ -45,38 +78,106 @@ When a pre-built binary does not exist for your system you can build the project * Check that the server is installed: :code:`postgrest --help`. -If you want to run the test suite, stack can do that too: :code:`stack test`. +PostgREST Test Suite +-------------------- -PostgreSQL dependency -===================== +Creating the Test Database +~~~~~~~~~~~~~~~~~~~~~~~~~~ -To use PostgREST you will need an underlying database (PostgreSQL version 9.3 or greater is required). You can use something like Amazon `RDS `_ but installing your own locally is cheaper and more convenient for development. +To properly run postgrest tests one needs to create a database. To do so, use the test creation script `create_test_database` in the `test/` folder. -* `Instructions for OS X `_ -* `Instructions for Ubuntu 14.04 `_ -* `Installer for Windows `_ +The script expects the following parameters: -Homebrew -======== +.. code:: bash -You can use the Homebrew package manager to install PostgREST on Mac + test/create_test_db connection_uri database_name [test_db_user] [test_db_user_password] -.. code-block:: bash +Use the `connection URI `_ to specify the user, password, host, and port. Do not provide the database in the connection URI. The Postgres role you are using to connect must be capable of creating new databases. - # Ensure brew is up to date - brew update +The `database_name` is the name of the database that `stack test` will connect to. If the database of the same name already exists on the server, the script will first drop it and then re-create it. - # Check for any problems with brew's setup - brew doctor +Optionally, specify the database user `stack test` will use. The user will be given necessary permissions to reset the database after every test run. - # Install the postgrest package - brew install postgrest +If the user is not specified, the script will generate the role name `postgrest_test_` suffixed by the chosen database name, and will generate a random password for it. -This will automatically install PostgreSQL as a dependency. The process tends to take up to 15 minutes to install the package and its dependencies. +Optionally, if specifying an existing user to be used for the test connection, one can specify the password the user has. -After installation completes, the tool is added to your $PATH and can be used from anywhere with: +The script will return the db uri to use in the tests--this uri corresponds to the `db-uri` parameter in the configuration file that one would use in production. -.. code-block:: bash +Generating the user and the password allows one to create the database and run the tests against any postgres server without any modifications to the server. (Such as allowing accounts without a passoword or setting up trust authentication, or requiring the server to be on the same localhost the tests are run from). - postgrest --help +Running the Tests +~~~~~~~~~~~~~~~~~ + +To run the tests, one must supply the database uri in the environment variable `POSTGREST_TEST_CONNECTION`. + +Typically, one would create the database and run the test in the same command line, using the `postgres` superuser: + +.. code:: bash + + POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@database-host" test_db) stack test + +For repeated runs on the same database, one should export the connection variable: + +.. code:: bash + + export POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@database-host" test_db) + stack test + stack test + ... + +If the environment variable is empty or not specified, then the test runner will default to connection uri + +.. code:: bash + + postgres://postgrest_test@localhost/postgrest_test + +This connection assumes the test server on the `localhost` with the user `postgrest_test` without the password and the database of the same name. + +Destroying the Database +~~~~~~~~~~~~~~~~~~~~~~~ + +The test database will remain after the test, together with four new roles created on the postgres server. To permanently erase the created database and the roles, run the script `test/delete_test_database`, using the same superuser role used for creating the database: + +.. code:: bash + + test/destroy_test_db connection_uri database_name + +Testing with Docker +~~~~~~~~~~~~~~~~~~~ + +The ability to connect to non-local PostgreSQL simplifies the test setup. One elegant way of testing is to use a disposable PostgreSQL in docker. + +For example, if local development is on a mac with Docker for Mac installed: + +.. code:: bash + + $ docker run --name db-scripting-test -e POSTGRES_PASSWORD=pwd -p 5434:5432 -d postgres + $ POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@localhost:5434" test_db) stack test + +Additionally, if one creates a docker container to run stack test (this is necessary on MacOS Sierra with GHC below 8.0.1, where `stack test` fails), one can run PostgreSQL in a separate linked container, or use the locally installed Postgres.app. + +Build the test container with `test/Dockerfile.test`: + +.. code:: bash + + $ docker build -t pgst-test - < text/Dockerfile.test + $ mkdir .stack-work-docker ~/.stack-linux + +The first run of the test container will take a long time while the dependencies get cached. Creating the `~/.stack-linux` folder and mapping it as a volume into the container ensures that we can run the container in disposable mode and not worry about subsequent runs being slow. `.stack-work-docker` is also mapped into the container and must be specified when using stack from Linux, not to interfere with the `.stack-work` for local development. (On Sierra, `stack build` works, while `stack test` fails with GHC 8.0.1). + +Linked containers: + +.. code:: bash + + $ docker run --name pg -e POSTGRES_PASSWORD=pwd -d postgres + $ docker run --rm -it -v `pwd`:`pwd` -v ~/.stack-linux:/root/.stack --link pg:pg -w="`pwd`" -v `pwd`/.stack-work-docker:`pwd`/.stack-work pgst-test bash -c "POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@pg" test_db) stack test" + +Stack test in Docker for Mac, Postgres.app on mac: + +.. code:: bash + $ host_ip=$(ifconfig en0 | grep 'inet ' | cut -f 2 -d' ') + $ export POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres@$HOST" test_db) + $ docker run --rm -it -v `pwd`:`pwd` -v ~/.stack-linux:/root/.stack -v `pwd`/.stack-work-docker:`pwd`/.stack-work -e "HOST=$host_ip" -e "POSTGREST_TEST_CONNECTION=$POSTGREST_TEST_CONNECTION" -w="`pwd`" pgst-test bash -c "stack test" + $ test/destroy_test_db "postgres://postgres@localhost" test_db From 1f12dedce6b0d3fe10e95c5ba91351d87d22f382 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 2 Dec 2016 10:15:12 -0800 Subject: [PATCH 038/549] Include Ruslan's env_var helper function in note --- auth.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/auth.rst b/auth.rst index f945336468..2430a50092 100644 --- a/auth.rst +++ b/auth.rst @@ -103,6 +103,28 @@ This allows JWT generation services to include extra information and your databa -- an exception if the setting is not present. Default it to ''. ALTER DATABASE your_db_name SET request.claim.email TO ''; + If you are unable to issue an ALTER DATABASE statement (for instance on Amazon RDS), you can create a helper function to read environment variables and swallow exceptions. + + .. code:: plpgsql + + create function env_var(v text) returns text as $$ + declare + result text; + begin + begin + select current_setting(v) into result; + exception + when undefined_object then + return null; + end; + + return result; + end; + $$ stable language plpgsql; + + -- now you can call call for instance + -- SELECT env_var('request.claim.email') + Hybrid User-Group Roles ~~~~~~~~~~~~~~~~~~~~~~~ From 2c1eb9ab13324ebdea285cbad71387de5ebc720e Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 2 Dec 2016 15:50:02 -0800 Subject: [PATCH 039/549] Add queueing bridges to docs --- intro.rst | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/intro.rst b/intro.rst index e90839bfab..575b99fe70 100644 --- a/intro.rst +++ b/intro.rst @@ -47,13 +47,14 @@ Client-Side Libraries * `davidthewatson/postgrest_python_requests_client `_ - Python * `calebmer/postgrest-client `_ - JS -Extensions ----------- +External Notification +--------------------- -* `diogob/postgrest-ws `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY -* `srid/spas `_ - allow file uploads and basic auth -* `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server -* `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware +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. + +* `matthewmueller/pg-sns-bridge `_ - Amazon SNS +* `aweber/pgsql-listen-exchange `_ - RabbitMQ +* `SpiderOak/skeeter `_ - ZeroMQ Example Apps ------------ @@ -80,6 +81,14 @@ In Production * `Image-charts `_ * `Drip Depot `_ +Extensions +---------- + +* `diogob/postgrest-ws `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY +* `srid/spas `_ - allow file uploads and basic auth +* `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server +* `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware + Commercial PaaS --------------- From 564c51fe7999eaf89145c2b54ec2c7c547c68c6b Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 4 Dec 2016 13:34:33 -0800 Subject: [PATCH 040/549] Stored procs --- api.rst | 50 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/api.rst b/api.rst index 02817aeb44..7480539973 100644 --- a/api.rst +++ b/api.rst @@ -233,7 +233,11 @@ OpenAPI Support Every API hosted by PostgREST automatically serves a full `OpenAPI `_ description on the root path. This provides a list of all endpoints, along with supported HTTP verbs and example payloads. -You can use a tool like `Swagger UI `_ to create beautiful documentation from the description and host an interactive web-based dahsboard. The dashboard allows developers to make requests against a live PostgREST server, provides guidance with request headers and example request bodies. +You can use a tool like `Swagger UI `_ to create beautiful documentation from the description and to host an interactive web-based dahsboard. The dashboard allows developers to make requests against a live PostgREST server, provides guidance with request headers and example request bodies. + +.. note:: + + The OpenAPI information can go out of date as the schema changes under a running server. To learn how to refresh the cache see :ref:`Schema Reloading`_. Resource Embedding ================== @@ -300,12 +304,52 @@ Note this does not change the order of the Films, but of the list of Actors in e Whenever foreign key relations change in the database schema you must refresh PostgREST's schema cache to allow resource embedding to work properly. See the section :ref:`Schema Reloading`_. -Query Limitations -================= +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 and OR-conditions in the where clause +* More complicated joins than those provided by `Resource Embedding`_ +* Geospatial queries that require an argument, like "points near (lat,lon)" +* More sophisticated full-text search than a simple use of the `@@` filter Stored Procedures ================= +Every stored procedure in the API-exposed database schema is accessible under the `/rpc` prefix. The API endpoint supports only POST which executes the function. + +.. code:: http + + POST /rpc/function_name HTTP/1.1 + +Procedures must used `named arguments `_. To supply arguments in an API call, include a JSON object in the request payload and each key/value of the object will become an argument. + +For instance, assume we have created this function in the database. + +.. code:: plpgsql + + CREATE FUNCTION add_them(a integer, b integer) + RETURNS integer AS $$ + SELECT $1 + $2; + $$ LANGUAGE SQL IMMUTABLE STRICT; + +The client can call it by posting an object like + +.. code:: http + + POST /rpc/add_them HTTP/1.1 + { "a": 1, "b": 2} + +The keys of the object match the parameter names. Note that PostgreSQL converts parameter names to lowercase unless you quote them like `CREATE FUNCTION foo("mixedCase" text) ...`. + +.. note:: + + Why the `/rpc` prefix? One reason is to avoid name collisions between views and procedures. It also helps emphasize to API consumers that these functions are not normal restful things. The functions can have arbitrary and surprising behavior, not the standard "post creates a resource" thing that users expect from the other routes. + + We are considering allowing GET requests for functions that are marked non-volatile. Allowing GET is important for HTTP caching. However we still must decide how to pass function parameters since request bodies are not allowed. Also some query string arguments are already reserved for shaping/filtering the output. + + Insertions / Updates ==================== From 6b255e98065d01b5c47e0e8627fc50e5f4610379 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 4 Dec 2016 13:59:24 -0800 Subject: [PATCH 041/549] Mark inline code as code, not italics --- admin.rst | 16 ++++++++-------- api.rst | 20 ++++++++++++-------- auth.rst | 18 +++++++++--------- install.rst | 22 +++++++++++----------- 4 files changed, 40 insertions(+), 36 deletions(-) diff --git a/admin.rst b/admin.rst index b5bcb35216..7946471503 100644 --- a/admin.rst +++ b/admin.rst @@ -50,7 +50,7 @@ db-schema db-anon-role The database role to use when executing commands on behalf of unauthenticated clients. db-pool - Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the `max_connections` GUC in your database. + Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the :code:`max_connections` GUC in your database. server-host Where to bind the PostgREST web server. server-port @@ -58,7 +58,7 @@ server-port server-proxy-url Overrides the base URL used within the OpenAPI self-documentation hosted at the API root path. jwt-secret - The secret used to decode JWT tokens clients provide for authentication. If this parameter is not specified then PostgREST refuses authentication requests. Choosing a value for this parameter beginning with the at sign such as `@filename` loads the secret out of an external file which is useful for non-UTF-8 binary secrets. + The secret used to decode JWT tokens clients provide for authentication. 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 which is useful for non-UTF-8 binary secrets. 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. pre-request @@ -118,7 +118,7 @@ This does not protect against malicious actions, since someone can add a url par Count-Header DoS ---------------- -For convenience to client-side pagination controls PostgREST supports counting and reporting total table size in its response. As described in :ref:`Limits and Pagination`_, responses ordinarily include a range and unspecified total like +For convenience to client-side pagination controls PostgREST supports counting and reporting total table size in its response. As described in :ref:`Limits and Pagination`_, responses ordinarily include a range but leave the total unspecified like .. code-block:: http @@ -126,7 +126,7 @@ For convenience to client-side pagination controls PostgREST supports counting a Range-Unit: items Content-Range: 0-14/* -However including the request header `Prefer: count=exact` calculates and includes the full count: +However including the request header :code:`Prefer: count=exact` calculates and includes the full count: .. code-block:: http @@ -142,7 +142,7 @@ This is fine in small tables, but count performance degrades in big tables due t .. note:: - In future versions we will support `Prefer: count=estimated` to leverage the PostgreSQL statistics tables for a fast (and fairly accurate) result. + In future versions we will support :code:`Prefer: count=estimated` to leverage the PostgreSQL statistics tables for a fast (and fairly accurate) result. .. _hardening_https: @@ -168,9 +168,9 @@ A great way to inspect incoming HTTP requests including headers and query params # sudo access is necessary for watching the network sudo ngrep -d lo0 port 3000 -The options to ngrep vary depending on the address and host on which you've bound the server. The binding is described in the `Configuration`_ section. The ngrep output isn't particularly pretty, but it's legible. Note the `Server` response header as well which identifies the version of server. This is important when submitting bug reports. +The options to ngrep vary depending on the address and host on which you've bound the server. The binding is described in the `Configuration`_ section. The ngrep output isn't particularly pretty, but it's legible. Note the :code:`Server` response header as well which identifies the version of server. This is important when submitting bug reports. -Once you've verified that requests are as you expect, you can get more information about the server operations by watching the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below. Find `postgresql.conf` inside your PostgreSQL data directory (to find that, issue the command `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. +Once you've verified that requests are as you expect, you can get more information about the server operations by watching 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 @@ -207,7 +207,7 @@ In the future we're investigating ways to keep the cache updated without manual Alternate URL Structure ======================= -As discussed in `Singular or Plural`_, there are no special URL forms for singular resources in PostgREST, only operators for filtering. Thus there are no URLs like `/people/1`. It would be specified instead as +As discussed in `Singular or 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:: http diff --git a/api.rst b/api.rst index 7480539973..bb5aa845fd 100644 --- a/api.rst +++ b/api.rst @@ -1,3 +1,6 @@ +.. role:: sql(code) + :language: sql + Tables and Views ================ @@ -57,13 +60,13 @@ For more complicated filters (such as those involving condition 1 OR condition 2 Vertical Filtering (Columns) ---------------------------- -When certain columns are wide (such as those holding binary data), it is more efficient for the server to withold them in a response. The client can specify which columns are required using the `select` parameter. +When certain columns are wide (such as those holding binary data), it is more efficient for the server to withold them in a response. The client can specify which columns are required using the :sql:`select` parameter. .. code-block:: http - GET /people?select=fname,age + GET /people?select=fname,age HTTP/1.1 -The default is `*`, meaning all columns. This value will become more important below in :ref:`Resource Embedding`_. +The default is :sql:`*`, meaning all columns. This value will become more important below in :ref:`Resource Embedding`_. .. _computed_cols: @@ -96,7 +99,7 @@ A full-text search on the computed column: Ordering -------- -The reserved word :code:`order` reorders the response rows. It uses a comma-separated list of columns and directions: +The reserved word :sql:`order` reorders the response rows. It uses a comma-separated list of columns and directions: .. code-block:: http @@ -201,7 +204,7 @@ The server will default to JSON for API endpoints and OpenAPI on the root. Singular or Plural ------------------ -By default PostgREST returns all JSON results in an array, even when there is only one item. For example, requesting `/items?id=eq.1` returns +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 @@ -312,12 +315,12 @@ The PostgREST url grammar limits the kinds of queries clients can perform. It pr * Table unions and OR-conditions in the where clause * More complicated joins than those provided by `Resource Embedding`_ * Geospatial queries that require an argument, like "points near (lat,lon)" -* More sophisticated full-text search than a simple use of the `@@` filter +* More sophisticated full-text search than a simple use of the :sql:`@@` filter Stored Procedures ================= -Every stored procedure in the API-exposed database schema is accessible under the `/rpc` prefix. The API endpoint supports only POST which executes the function. +Every stored procedure in the API-exposed database schema is accessible under the :code:`/rpc` prefix. The API endpoint supports only POST which executes the function. .. code:: http @@ -339,9 +342,10 @@ The client can call it by posting an object like .. code:: http POST /rpc/add_them HTTP/1.1 + { "a": 1, "b": 2} -The keys of the object match the parameter names. Note that PostgreSQL converts parameter names to lowercase unless you quote them like `CREATE FUNCTION foo("mixedCase" text) ...`. +The keys of the object match the parameter names. Note that PostgreSQL converts parameter names to lowercase unless you quote them like :sql:`CREATE FUNCTION foo("mixedCase" text) ...`. .. note:: diff --git a/auth.rst b/auth.rst index 2430a50092..7a8ce23168 100644 --- a/auth.rst +++ b/auth.rst @@ -12,7 +12,7 @@ There are three types of roles used by PostgREST, the **authenticator**, **anony .. image:: _static/security-roles.png -The authenticator should be created `NOINHERIT` and configured in the database to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests. 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. +The authenticator should be created :code:`NOINHERIT` and configured in the database to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests. 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. .. image:: _static/security-anon-choice.png @@ -46,7 +46,7 @@ PostgreSQL manages database access permissions using the concept of roles. A rol Roles for Each Web User ~~~~~~~~~~~~~~~~~~~~~~~ -PostgREST can accommodate either viewpoint. If you treat a role as a single user then the the JWT-based role switching described above does most of what you need. When an authenticated user makes a request PostgREST will switch into the role for that user, which in addition to restricting queries, is available to SQL through the `current_user` variable. +PostgREST can accommodate either viewpoint. If you treat a role as a single user then the the JWT-based role switching described above does most of what you need. When an authenticated user makes a request PostgREST will switch into the 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. @@ -146,7 +146,7 @@ There is no performance penalty for having many database roles, although roles a Custom Validation ----------------- -PostgREST honors the `exp` claim for token expiration, rejecting expired tokens. However it does not enforce any extra constraints. An example of an extra constraint would be to immediately revoke access for a certain user. The configuration file paramter `pre-request` specifies a stored procedure to call immediately after the authenticator switches into a new role and before the main query itself runs. +PostgREST honors the :code:`exp` claim for token expiration, rejecting expired tokens. However it does not enforce any extra constraints. An example of an extra constraint would be to immediately revoke access for a certain user. The configuration file paramter :code:`pre-request` specifies a stored procedure to call immediately after the authenticator switches into a new role and before the main query itself runs. Here's an example. In the config file specify a stored procedure: @@ -172,7 +172,7 @@ In the function you can run arbitrary code to check the request and raise an exc Client Auth =========== -To make an authenticated request the client must include an `Authorization` HTTP header with the value `Bearer `. For instance: +To make an authenticated request the client must include an :code:`Authorization` HTTP header with the value :code:`Bearer `. For instance: .. code:: http @@ -241,7 +241,7 @@ Storing Users and Passwords As mentioned, 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. -The following table, functions, and triggers will live in a `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. +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: @@ -259,7 +259,7 @@ First we'll need a table to keep track of our users: 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 `pg_roles` table. We'll use a trigger to manually enforce it. +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:: plpgsql @@ -283,7 +283,7 @@ We would like the role to be a foreign key to actual database roles, however Pos 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 `users` table. +Next we'll use the pgcrypto extension and a trigger to keep passwords safe in the :code:`users` table. .. code:: plpgsql @@ -370,7 +370,7 @@ An API request to call this function would look like: { "email": "foo@bar.com", "pass": "foobar" } -The response would look like the snippet below. Try decoding the token at `jwt.io `_. (It was encoded with a secret of `mysecret` as specified in the SQL code above. You'll want to change this secret in your app!) +The response would look like the snippet below. Try decoding the token at `jwt.io `_. (It was encoded with a secret of :code:`mysecret` as specified in the SQL code above. You'll want to change this secret in your app!) .. code:: json @@ -395,4 +395,4 @@ Your database roles need access to the schema, tables, views and functions in or grant select on table pg_authid, basic_auth.users to anon; grant execute on function login(text,text) to anon; -You may be worried from the above that anonymous users can read everything from the `basic_auth.users` table. However this table is not available for direct queries because it lives in a separate schema. The anonymous role needs access because the public `users` view reads the underlying table with the permissions of the calling user. But we have made sure the view properly restricts access to sensitive information. +You may be worried from the above that anonymous users can read everything from the :code:`basic_auth.users` table. However this table is not available for direct queries because it lives in a separate schema. The anonymous role needs access because the public :code:`users` view reads the underlying table with the permissions of the calling user. But we have made sure the view properly restricts access to sensitive information. diff --git a/install.rst b/install.rst index 4bf888f48e..b300cfd5c0 100644 --- a/install.rst +++ b/install.rst @@ -84,7 +84,7 @@ PostgREST Test Suite Creating the Test Database ~~~~~~~~~~~~~~~~~~~~~~~~~~ -To properly run postgrest tests one needs to create a database. To do so, use the test creation script `create_test_database` in the `test/` folder. +To properly run postgrest tests one needs to create a database. To do so, use the test creation script :code:`create_test_database` in the :code:`test/` folder. The script expects the following parameters: @@ -94,22 +94,22 @@ The script expects the following parameters: Use the `connection URI `_ to specify the user, password, host, and port. Do not provide the database in the connection URI. The Postgres role you are using to connect must be capable of creating new databases. -The `database_name` is the name of the database that `stack test` will connect to. If the database of the same name already exists on the server, the script will first drop it and then re-create it. +The :code:`database_name` is the name of the database that :code:`stack test` will connect to. If the database of the same name already exists on the server, the script will first drop it and then re-create it. -Optionally, specify the database user `stack test` will use. The user will be given necessary permissions to reset the database after every test run. +Optionally, specify the database user :code:`stack test` will use. The user will be given necessary permissions to reset the database after every test run. -If the user is not specified, the script will generate the role name `postgrest_test_` suffixed by the chosen database name, and will generate a random password for it. +If the user is not specified, the script will generate the role name :code:`postgrest_test_` suffixed by the chosen database name, and will generate a random password for it. Optionally, if specifying an existing user to be used for the test connection, one can specify the password the user has. -The script will return the db uri to use in the tests--this uri corresponds to the `db-uri` parameter in the configuration file that one would use in production. +The script will return the db uri to use in the tests--this uri corresponds to the :code:`db-uri` parameter in the configuration file that one would use in production. Generating the user and the password allows one to create the database and run the tests against any postgres server without any modifications to the server. (Such as allowing accounts without a passoword or setting up trust authentication, or requiring the server to be on the same localhost the tests are run from). Running the Tests ~~~~~~~~~~~~~~~~~ -To run the tests, one must supply the database uri in the environment variable `POSTGREST_TEST_CONNECTION`. +To run the tests, one must supply the database uri in the environment variable :code:`POSTGREST_TEST_CONNECTION`. Typically, one would create the database and run the test in the same command line, using the `postgres` superuser: @@ -132,12 +132,12 @@ If the environment variable is empty or not specified, then the test runner will postgres://postgrest_test@localhost/postgrest_test -This connection assumes the test server on the `localhost` with the user `postgrest_test` without the password and the database of the same name. +This connection assumes the test server on the :code:`localhost:code:` with the user `postgrest_test` without the password and the database of the same name. Destroying the Database ~~~~~~~~~~~~~~~~~~~~~~~ -The test database will remain after the test, together with four new roles created on the postgres server. To permanently erase the created database and the roles, run the script `test/delete_test_database`, using the same superuser role used for creating the database: +The test database will remain after the test, together with four new roles created on the postgres server. To permanently erase the created database and the roles, run the script :code:`test/delete_test_database`, using the same superuser role used for creating the database: .. code:: bash @@ -155,16 +155,16 @@ For example, if local development is on a mac with Docker for Mac installed: $ docker run --name db-scripting-test -e POSTGRES_PASSWORD=pwd -p 5434:5432 -d postgres $ POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@localhost:5434" test_db) stack test -Additionally, if one creates a docker container to run stack test (this is necessary on MacOS Sierra with GHC below 8.0.1, where `stack test` fails), one can run PostgreSQL in a separate linked container, or use the locally installed Postgres.app. +Additionally, if one creates a docker container to run stack test (this is necessary on MacOS Sierra with GHC below 8.0.1, where :code:`stack test` fails), one can run PostgreSQL in a separate linked container, or use the locally installed Postgres.app. -Build the test container with `test/Dockerfile.test`: +Build the test container with :code:`test/Dockerfile.test`: .. code:: bash $ docker build -t pgst-test - < text/Dockerfile.test $ mkdir .stack-work-docker ~/.stack-linux -The first run of the test container will take a long time while the dependencies get cached. Creating the `~/.stack-linux` folder and mapping it as a volume into the container ensures that we can run the container in disposable mode and not worry about subsequent runs being slow. `.stack-work-docker` is also mapped into the container and must be specified when using stack from Linux, not to interfere with the `.stack-work` for local development. (On Sierra, `stack build` works, while `stack test` fails with GHC 8.0.1). +The first run of the test container will take a long time while the dependencies get cached. Creating the :code:`~/.stack-linux` folder and mapping it as a volume into the container ensures that we can run the container in disposable mode and not worry about subsequent runs being slow. :code:`.stack-work-docker` is also mapped into the container and must be specified when using stack from Linux, not to interfere with the :code:`.stack-work` for local development. (On Sierra, :code:`stack build` works, while :code:`stack test` fails with GHC 8.0.1). Linked containers: From e84b4cbcfe530dccd849b750c3aaabdc51a7ac10 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 4 Dec 2016 18:34:35 -0800 Subject: [PATCH 042/549] Fill out the API ops --- api.rst | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/api.rst b/api.rst index bb5aa845fd..380a09f1cd 100644 --- a/api.rst +++ b/api.rst @@ -96,6 +96,12 @@ A full-text search on the computed column: GET /people?full_name=@@.Beckett HTTP/1.1 +As mentioned, computed columns do not appear in the output by default. However you can include them by listing them in the vertical filtering :code:`select` param: + +.. code-block:: HTTP + + GET /people?select=*,full_name HTTP/1.1 + Ordering -------- @@ -310,7 +316,7 @@ Note this does not change the order of the Films, but of the list of Actors in e 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 +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 and OR-conditions in the where clause * More complicated joins than those provided by `Resource Embedding`_ @@ -357,11 +363,87 @@ The keys of the object match the parameter names. Note that PostgreSQL converts Insertions / Updates ==================== -Getting Results ---------------- +All tables and `auto-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:: HTTP + + POST /table_name HTTP/1.1 + + { "col1": "value1", "col2": "value2" } + +The response will include a :code:`Location` header describing where to find the new object. If the table is write-only then constructing the Location header will cause a permissions error. To successfully insert an item to a write-only table you will need to suppress the Location response header by including the request header :code:`Prefer: return=minimal`. + +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 `Vertical Filtering (Columns)`_ to these results. + +.. 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:`Client-Side Libraries`_ built for PostgREST. + +To update a row or rows in a table, use the PATCH verb. Use :ref:`Horizontal Filtering (Rows)`_ to specify which record(s) to update. Here is an exmaple query setting the :code:`category` column to child for all people below a certain age. + +.. code:: HTTP + + PATCH /people?age=lt.13 HTTP/1.1 + + { "category": "child" } + +Updates also support :code:`Prefer: return=representation` plus `Vertical Filtering (Columns)`_. + +.. note:: + + Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`Block Full-Table Operations`_. 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 backend for efficiency. Note that using CSV requires less parsing on the server and is much faster. + +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:: HTTP + + POST /people HTTP/1.1 + Content-Type: text/csv + + name,age,height + J Doe,62,70 + Jonas,10,55 + +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:: HTTP + + POST /people HTTP/1.1 + Content-Type: application/json + + [ + { "name": "J Doe", "age": 62, "height": 70 }, + { "name": "Janus", "age": 10, "height": 55 } + ] + Deletions ========= + +To delete rows in a table, use the DELETE verb plus :ref:`Horizontal Filtering (Rows)`_. For instance deleting inactive users: + +.. code-block:: HTTP + + DELETE /user?active=is.false HTTP/1.1 + +.. note:: + + Beware of accidentally delting all rows in a table. To learn to prevent that see :ref:`Block Full-Table Operations`_. From adb852227662a83274b61cd59124fec16ded94a0 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 5 Dec 2016 16:14:28 -0800 Subject: [PATCH 043/549] Sorting embedded resources --- admin.rst | 2 +- api.rst | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/admin.rst b/admin.rst index 7946471503..4de4615fb5 100644 --- a/admin.rst +++ b/admin.rst @@ -220,8 +220,8 @@ This allows compound primary keys and makes the intent for singular response ind nginx code here +.. TODO .. Administration -.. Alternate URL structure .. API Versioning .. HTTP Caching .. Upgrading diff --git a/api.rst b/api.rst index 380a09f1cd..5f5f12378b 100644 --- a/api.rst +++ b/api.rst @@ -301,17 +301,27 @@ Which would return PostgREST can also detect relations going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). You can also reverse the direction of inclusion, asking for all Directories with each including the list of their Films. -To order the embedded items, you need to specify the tree path in the order parameter. For instance +.. note:: + + Whenever foreign key relations change in the database schema you must refresh PostgREST's schema cache to allow resource embedding to work properly. See the section :ref:`Schema Reloading`_. + +Embedded Filters and Order +-------------------------- + +Embedded tables can be filtered and ordered similarly to their top-level counterparts. To to do so prefix the query parameters with the name of the embedded table. For instance to order the actors in each film: .. code-block:: http GET /films?select=*,actors{*}&actors.order=last_name,first_name HTTP/1.1 -Note this does not change the order of the Films, but of the list of Actors in each Film. +This sorts the list of actors in each film but does *not* change the order of the films themselves. To filter the roles returned with each film: -.. note:: +.. code-block:: http + + GET /films?select=*,roles{*}&roles.character=in.Chico,Harpo,Groucho HTTP/1.1 + +Once again, this restricts the roles included to certain characters but does not filter the films in any way. Films without any of those characters would be included along with empty character lists. - Whenever foreign key relations change in the database schema you must refresh PostgREST's schema cache to allow resource embedding to work properly. See the section :ref:`Schema Reloading`_. Custom Queries ============== From 349726cdc0e0720d56cbfefbb105724df2bfd438 Mon Sep 17 00:00:00 2001 From: Trevor Basinger Date: Sun, 11 Dec 2016 10:56:39 -0600 Subject: [PATCH 044/549] Documentation b64 option #772 (#25) --- admin.rst | 5 ++++- auth.rst | 28 ++++++++++++++++++++++++++-- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/admin.rst b/admin.rst index 4de4615fb5..51b91acdaa 100644 --- a/admin.rst +++ b/admin.rst @@ -39,6 +39,7 @@ server-host String \*4 server-port Int 3000 server-proxy-url String jwt-secret String +secret-is-base64 Bool False max-rows Int ∞ pre-request String ================ ====== ======= ======== @@ -58,7 +59,9 @@ server-port server-proxy-url Overrides the base URL used within the OpenAPI self-documentation hosted at the API root path. jwt-secret - The secret used to decode JWT tokens clients provide for authentication. 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 which is useful for non-UTF-8 binary secrets. + The secret used to decode JWT tokens clients provide for authentication. 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. +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. 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. pre-request diff --git a/auth.rst b/auth.rst index 7a8ce23168..db8259b664 100644 --- a/auth.rst +++ b/auth.rst @@ -217,9 +217,33 @@ JWT from Auth0 An external service like `Auth0 `_ 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 adapt Auth0 to our uses we need to save the database role in `user metadata `_ and include the metadata in `private claims `_ of the generated JWT. +By default, Auth0 generates binary base64URL encoded secrets. You can find the secret in the client settings of the Auth0 management console. Copy the client secret into your PostgREST configuration file as the :code:`jwt-secret`. Then set :code:`secret-is-base64` to :code:`true`. + +To adapt Auth0 to our uses we need to save the database role in `user metadata `_. Then, you will need to write a rule that will extract the role from the user metadata and include a :code:`role` claim in the payload of our user object. Afterwards, in your Auth0Lock code, include the :code:`role` claim in your `scope param `_. + + +.. code:: javascript + + // Example Auth0 rule + function (user, context, callback) { + var role = user.user_metadata.role; + user.role = role; + callback(null, user, context); + } + + +.. code:: javascript + + // Example using Auth0Lock with role claim in scope + new Auth0Lock ( AUTH0_CLIENTID, AUTH0_DOMAIN, { + container: 'lock-container', + auth: { + params: { scope: 'openid role' }, + redirectUrl: FQDN + '/login', // Replace with your redirect url + responseType: 'token' + } + }) -**TODO: add details** .. _ssl: From 4d8272fa0d79519b0182637c2c690b173a7293c3 Mon Sep 17 00:00:00 2001 From: J Phani Mahesh Date: Sat, 17 Dec 2016 22:46:03 +0530 Subject: [PATCH 045/549] fix typo in install.rst: s/text/text/ (#27) --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index b300cfd5c0..20d41f90db 100644 --- a/install.rst +++ b/install.rst @@ -161,7 +161,7 @@ Build the test container with :code:`test/Dockerfile.test`: .. code:: bash - $ docker build -t pgst-test - < text/Dockerfile.test + $ docker build -t pgst-test - < test/Dockerfile.test $ mkdir .stack-work-docker ~/.stack-linux The first run of the test container will take a long time while the dependencies get cached. Creating the :code:`~/.stack-linux` folder and mapping it as a volume into the container ensures that we can run the container in disposable mode and not worry about subsequent runs being slow. :code:`.stack-work-docker` is also mapped into the container and must be specified when using stack from Linux, not to interfere with the :code:`.stack-work` for local development. (On Sierra, :code:`stack build` works, while :code:`stack test` fails with GHC 8.0.1). From b3fb9ff21578f7a391167a0dd15919dcc1568762 Mon Sep 17 00:00:00 2001 From: Nikolay Date: Tue, 20 Dec 2016 21:14:05 -0800 Subject: [PATCH 046/549] more about stored procedures (#26) --- api.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 5f5f12378b..4e7b439238 100644 --- a/api.rst +++ b/api.rst @@ -336,7 +336,7 @@ The PostgREST URL grammar limits the kinds of queries clients can perform. It pr Stored Procedures ================= -Every stored procedure in the API-exposed database schema is accessible under the :code:`/rpc` prefix. The API endpoint supports only POST which executes the function. +Every stored procedure in the API-exposed database schema is accessible under the :code:`/rpc` prefix. The API endpoint supports only POST which executes the function. Such function can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). .. code:: http @@ -363,6 +363,10 @@ The client can call it by posting an object like The keys of the object match the parameter names. Note that PostgreSQL converts parameter names to lowercase unless you quote them like :sql:`CREATE FUNCTION foo("mixedCase" text) ...`. +PostgreSQL has four procedural languages that are part of the core distribution: PL/pgSQL, PL/Tcl, PL/Perl, and PL/Python. There are many other procedural languages distributed as additional extensions. Also, plain SQL can be used to write functions (as shown in the example above). + +By default, a function is to be executed with the privileges of the user that calls it. This means, that the user has to have all permissions to do all operations the procedure performs. But if the function was defined with :code:`SECURITY DEFINER` options, only one permission check will take place – the permission to call the function. See `PostgreSQL documentation `_ for more details. + .. note:: Why the `/rpc` prefix? One reason is to avoid name collisions between views and procedures. It also helps emphasize to API consumers that these functions are not normal restful things. The functions can have arbitrary and surprising behavior, not the standard "post creates a resource" thing that users expect from the other routes. From f52fb70e3d10d540d5aa4601ef1d7950f5c55290 Mon Sep 17 00:00:00 2001 From: Jeffrey Date: Mon, 26 Dec 2016 12:40:23 -0500 Subject: [PATCH 047/549] Suggest swap file on systems with under 1GB of ram (#30) --- install.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/install.rst b/install.rst index 20d41f90db..22b9fbdded 100644 --- a/install.rst +++ b/install.rst @@ -75,6 +75,10 @@ When a pre-built binary does not exist for your system you can build the project cd postgrest stack build --install-ghc sudo stack install --allow-different-user --local-bin-path /usr/local/bin + +.. note:: + + If building fails and your system has less than 1GB of memory, try adding a swap file. * Check that the server is installed: :code:`postgrest --help`. From 8706779b42cc704009f6741f5682083b456c3543 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 16 Jan 2017 20:34:39 -0800 Subject: [PATCH 048/549] Document SSH session termination precautions --- admin.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/admin.rst b/admin.rst index 51b91acdaa..239c4d1a67 100644 --- a/admin.rst +++ b/admin.rst @@ -67,6 +67,20 @@ max-rows pre-request A schema-qualified stored procedure name to call right after switching roles for a client request. This provides an opportunity to modify SQL variables or raise an exception to prevent the request from completing. +Running the Server +------------------ + +PostgREST outputs basic request logging to stdout. 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 logfile or to the syslog: + +.. code-block:: bash + + ssh foo@example.com \ + 'postgrest foo.conf /var/log/postgrest.log 2>&1 &' + + # another option is to pipe the output into "logger -t postgrest" + +(Avoid :code:`nohup postgrest` because the HUP signal is used for manual `Schema Reloading`_.) + Hardening PostgREST =================== From 360c90afe312be4e781a30869e34ced8f81a685f Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 16 Jan 2017 20:35:01 -0800 Subject: [PATCH 049/549] Auth0 no longer base64 encodes secrets --- auth.rst | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/auth.rst b/auth.rst index db8259b664..f9c4f02671 100644 --- a/auth.rst +++ b/auth.rst @@ -212,15 +212,29 @@ Next write a stored procedure that returns the token. The one below returns a to 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 '!!secret!!'; + + -- then all functions can refer to app.jwt_secret + SELECT jwt.sign( + row_to_json(r), current_setting('app.jwt_secret') + ) AS token + FROM ... + JWT from Auth0 ~~~~~~~~~~~~~~ An external service like `Auth0 `_ 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. -By default, Auth0 generates binary base64URL encoded secrets. You can find the secret in the client settings of the Auth0 management console. Copy the client secret into your PostgREST configuration file as the :code:`jwt-secret`. Then set :code:`secret-is-base64` to :code:`true`. - -To adapt Auth0 to our uses we need to save the database role in `user metadata `_. Then, you will need to write a rule that will extract the role from the user metadata and include a :code:`role` claim in the payload of our user object. Afterwards, in your Auth0Lock code, include the :code:`role` claim in your `scope param `_. +To use Auth0, copy its client secret into your PostgREST configuration file as the :code:`jwt-secret`. (Old-style Auth0 secrets are Base64 encoded. For these secrets set :code:`secret-is-base64` to :code:`true`, or just refresh the Auth0 secret.) You can find the secret in the client settings of the Auth0 management console. +Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `user metadata `_. Then, you will need to write a rule that will extract the role from the user metadata and include a :code:`role` claim in the payload of our user object. Afterwards, in your Auth0Lock code, include the :code:`role` claim in your `scope param `_. .. code:: javascript @@ -244,7 +258,6 @@ To adapt Auth0 to our uses we need to save the database role in `user metadata < } }) - .. _ssl: SSL From b3dfde5ecfa4f7e2cf0c36d40a57dbe554a893c3 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 16 Jan 2017 20:35:17 -0800 Subject: [PATCH 050/549] Move admin section higher in TOC --- index.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/index.rst b/index.rst index d42418d559..f8bb6e4146 100644 --- a/index.rst +++ b/index.rst @@ -13,6 +13,11 @@ install.rst +.. toctree:: + :caption: Administration + + admin.rst + .. toctree:: :caption: API @@ -22,8 +27,3 @@ :caption: Authentication auth.rst - -.. toctree:: - :caption: Administration - - admin.rst From e0e604aa9f7e90f9b30ecb042707705c88ec4c36 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 16 Jan 2017 20:35:42 -0800 Subject: [PATCH 051/549] Fix install problems reported by @tnhu --- install.rst | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/install.rst b/install.rst index 22b9fbdded..34d0ae7ecd 100644 --- a/install.rst +++ b/install.rst @@ -59,13 +59,14 @@ When a pre-built binary does not exist for your system you can build the project * `Install Stack `_ for your platform * Install Library Dependencies - ===================== ============================ + ===================== ======================================= Operating System Dependencies - ===================== ============================ - Ubuntu/Debian libpq-dev - CentOS/Fedora/Red Hat postgresql-devel, zlib-devel + ===================== ======================================= + Ubuntu/Debian libpq-dev, libgmp-dev + CentOS/Fedora/Red Hat postgresql-devel, zlib-devel, gmp-devel BSD postgresql95-server - ===================== ============================ + OS X postgresql, gmp + ===================== ======================================= * Build and install binary @@ -73,9 +74,10 @@ When a pre-built binary does not exist for your system you can build the project git clone https://github.com/begriffs/postgrest.git cd postgrest - stack build --install-ghc - sudo stack install --allow-different-user --local-bin-path /usr/local/bin - + + # 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. From 58503af43277d1be92ee11ec891e114f3acca164 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 16 Jan 2017 20:47:01 -0800 Subject: [PATCH 052/549] English and RST adjustments --- admin.rst | 2 +- api.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/admin.rst b/admin.rst index 239c4d1a67..9f36dc4b9d 100644 --- a/admin.rst +++ b/admin.rst @@ -135,7 +135,7 @@ This does not protect against malicious actions, since someone can add a url par Count-Header DoS ---------------- -For convenience to client-side pagination controls PostgREST supports counting and reporting total table size in its response. As described in :ref:`Limits and Pagination`_, responses ordinarily include a range but leave the total unspecified like +For convenience to client-side pagination controls PostgREST supports counting and reporting total table size in its response. As described in `Limits and Pagination`_, responses ordinarily include a range but leave the total unspecified like .. code-block:: http diff --git a/api.rst b/api.rst index 4e7b439238..779e85bcfe 100644 --- a/api.rst +++ b/api.rst @@ -365,7 +365,7 @@ The keys of the object match the parameter names. Note that PostgreSQL converts PostgreSQL has four procedural languages that are part of the core distribution: PL/pgSQL, PL/Tcl, PL/Perl, and PL/Python. There are many other procedural languages distributed as additional extensions. Also, plain SQL can be used to write functions (as shown in the example above). -By default, a function is to be executed with the privileges of the user that calls it. This means, that the user has to have all permissions to do all operations the procedure performs. But if the function was defined with :code:`SECURITY DEFINER` options, only one permission check will take place – the permission to call the function. See `PostgreSQL documentation `_ for more details. +By default, 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. Another option is to define the function with 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. See `PostgreSQL documentation `_ for more details. .. note:: From e37a2a569c79dd4b5d17bb3d4dcb17e87dd17c52 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 21 Jan 2017 20:16:21 -0800 Subject: [PATCH 053/549] Document rate limiting via nginx --- admin.rst | 21 ++++++++++++++++++++- auth.rst | 2 ++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/admin.rst b/admin.rst index 9f36dc4b9d..e2fadd380c 100644 --- a/admin.rst +++ b/admin.rst @@ -171,7 +171,26 @@ See the :ref:`ssl` section of the authentication guide. Rate Limiting ------------- -Foo +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 indescriminate. To rate limit each authenticated request individually you will need to add logic in a :ref:`Custom Validation ` function. Debugging ========= diff --git a/auth.rst b/auth.rst index f9c4f02671..3c42c9113a 100644 --- a/auth.rst +++ b/auth.rst @@ -143,6 +143,8 @@ There is no performance penalty for having many database roles, although roles a -- allow authenticator to switch into user000 role -- (the role itself has nologin) +.. _custom_validation: + Custom Validation ----------------- From e1e1ee2d31efaefd7869eb9d31d4eb489d4a82ba Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 21 Jan 2017 23:00:13 -0800 Subject: [PATCH 054/549] pg-sns-bridge rename --- intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intro.rst b/intro.rst index 575b99fe70..ffec7d75cd 100644 --- a/intro.rst +++ b/intro.rst @@ -52,7 +52,7 @@ 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. -* `matthewmueller/pg-sns-bridge `_ - Amazon SNS +* `matthewmueller/pg-bridge `_ - Amazon SNS * `aweber/pgsql-listen-exchange `_ - RabbitMQ * `SpiderOak/skeeter `_ - ZeroMQ From 71816e64f835c5b02a4fcbae167d1a989155ff65 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 22 Jan 2017 17:32:38 -0800 Subject: [PATCH 055/549] Fix typos in auth section Fixes #29 --- auth.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/auth.rst b/auth.rst index 3c42c9113a..6ae84a0aae 100644 --- a/auth.rst +++ b/auth.rst @@ -99,9 +99,9 @@ This allows JWT generation services to include extra information and your databa .. code:: postgres - -- Prevent current_setting('postgrest.claims.email') from raising + -- Prevent current_setting('request.jwt.claim.email') from raising -- an exception if the setting is not present. Default it to ''. - ALTER DATABASE your_db_name SET request.claim.email TO ''; + ALTER DATABASE your_db_name SET request.jwt.claim.email TO ''; If you are unable to issue an ALTER DATABASE statement (for instance on Amazon RDS), you can create a helper function to read environment variables and swallow exceptions. @@ -123,7 +123,7 @@ This allows JWT generation services to include extra information and your databa $$ stable language plpgsql; -- now you can call call for instance - -- SELECT env_var('request.claim.email') + -- SELECT env_var('request.jwt.claim.email') Hybrid User-Group Roles ~~~~~~~~~~~~~~~~~~~~~~~ @@ -381,7 +381,7 @@ As described in `JWT from SQL`_, we'll create a JWT inside our login function. N as $$ declare _role name; - result basic_auth.jwt_claims; + result basic_auth.jwt_token; begin -- check email and password select basic_auth.user_role(email, pass) into _role; From 19768064dde76c92bbeff43b5cf99533f182c04d Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 22 Jan 2017 20:16:57 -0800 Subject: [PATCH 056/549] More prominent link to downloads --- install.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 34d0ae7ecd..50c6ba8158 100644 --- a/install.rst +++ b/install.rst @@ -1,7 +1,9 @@ Binary Release ============== -The `release page `_ has precompiled binaries for Mac OS X, Windows, and several Linux distros. Extract the tarball and run the binary inside with the :code:`--help` flag to see usage instructions: +[ `Download from release page `_ ] + +The release page has precompiled binaries for Mac OS X, Windows, and several Linux distros. Extract the tarball and run the binary inside with the :code:`--help` flag to see usage instructions: .. code-block:: bash From e56ffe7b5e88532e70c711ff20a5f761faf59c1c Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 22 Jan 2017 20:36:27 -0800 Subject: [PATCH 057/549] Note json array vs native array args --- api.rst | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 779e85bcfe..f0f3af22a6 100644 --- a/api.rst +++ b/api.rst @@ -359,10 +359,26 @@ The client can call it by posting an object like POST /rpc/add_them HTTP/1.1 - { "a": 1, "b": 2} + { "a": 1, "b": 2 } The keys of the object match the parameter names. Note that PostgreSQL converts parameter names to lowercase unless you quote them like :sql:`CREATE FUNCTION foo("mixedCase" text) ...`. +.. note:: + + We recommend using function arguments of type json to accept arrays from the client. To pass a PostgreSQL native array you'll need to quote it as a string: + + .. code:: http + + POST /rpc/native_array_func HTTP/1.1 + + { "arg": "{1,2,3}" } + + .. code:: http + + POST /rpc/json_array_func HTTP/1.1 + + { "arg": [1,2,3] } + PostgreSQL has four procedural languages that are part of the core distribution: PL/pgSQL, PL/Tcl, PL/Perl, and PL/Python. There are many other procedural languages distributed as additional extensions. Also, plain SQL can be used to write functions (as shown in the example above). By default, 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. Another option is to define the function with 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. See `PostgreSQL documentation `_ for more details. From 3262607b5b9fddb5c3ce4681a6895cfb8b3b0b5b Mon Sep 17 00:00:00 2001 From: Leon du Toit Date: Mon, 23 Jan 2017 17:34:39 +0100 Subject: [PATCH 058/549] Document connect strings for older versions of libpq (#11) (#31) --- admin.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin.rst b/admin.rst index e2fadd380c..7518300180 100644 --- a/admin.rst +++ b/admin.rst @@ -45,7 +45,7 @@ pre-request String ================ ====== ======= ======== db-uri - The standard connection PostgreSQL `URI format `_. Also allows connections over Unix sockets for higher performance. + The standard connection PostgreSQL `URI format `_. On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. Also allows connections over Unix sockets for higher performance. db-schema The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. db-anon-role From 404fcb6b2795d76c00eb69b500417c3b765e2c05 Mon Sep 17 00:00:00 2001 From: phil Date: Thu, 26 Jan 2017 10:32:27 -0500 Subject: [PATCH 059/549] Fix the plurality header docs for v0.4 (#32) --- admin.rst | 2 +- api.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/admin.rst b/admin.rst index 7518300180..aec3d6c39f 100644 --- a/admin.rst +++ b/admin.rst @@ -248,7 +248,7 @@ As discussed in `Singular or Plural`_, there are no special URL forms for singul .. code:: http GET /people?id=eq.1 - Prefer: plurality=singular + Accept: application/vnd.pgrst.object+json This allows compound primary keys and makes the intent for singular response independent of a URL convention. However for any table which uses a simple primary key you can use Nginx to simulate the familiar URL convention. diff --git a/api.rst b/api.rst index f0f3af22a6..6ec65039f8 100644 --- a/api.rst +++ b/api.rst @@ -218,12 +218,12 @@ By default PostgREST returns all JSON results in an array, even when there is on { "id": 1 } ] -This can be inconvenient for client code. To return the first result as an object unenclosed by an array, Include a Prefer request header +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:: http GET /items?id=eq.1 HTTP/1.1 - Prefer: plurality=singular + Accept: application/vnd.pgrst.object+json This returns From 892f4a8491224616f778beae49a427d15cbbd5e7 Mon Sep 17 00:00:00 2001 From: Lucas Desgouilles Date: Thu, 2 Feb 2017 17:02:43 +0100 Subject: [PATCH 060/549] Override document title for index.rst (#34) The index page has ` -- PostgREST 0.4.0.0 Documentation` for HTML title, using the `title` directive, we can force it to something nicer. I'm not sure if this fixes it at the top of the page. http://docutils.sourceforge.net/docs/ref/rst/directives.html#metadata-document-title http://www.sphinx-doc.org/en/stable/rest.html#directives --- index.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/index.rst b/index.rst index f8bb6e4146..85935f0686 100644 --- a/index.rst +++ b/index.rst @@ -1,3 +1,5 @@ +.. title:: PostgREST Documentation + .. image:: _static/logo.png .. toctree:: From e508f071b4c3ba58e4b19b2dca430324619f156f Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 3 Feb 2017 16:11:19 -0800 Subject: [PATCH 061/549] Bump copyright year --- conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf.py b/conf.py index 489726976e..4792328945 100644 --- a/conf.py +++ b/conf.py @@ -46,7 +46,7 @@ # General information about the project. project = u'PostgREST' -copyright = u'2016, Joe Nelson' +copyright = u'2017, Joe Nelson' author = u'Joe Nelson' # The version info for the project you're documenting, acts as replacement for From 588b0bc7e1c1a7c7cb38b916f65ec80d190c3197 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 3 Feb 2017 16:37:24 -0800 Subject: [PATCH 062/549] Fix internal links --- admin.rst | 10 +++++++--- api.rst | 30 +++++++++++++++++++----------- intro.rst | 2 ++ 3 files changed, 28 insertions(+), 14 deletions(-) diff --git a/admin.rst b/admin.rst index aec3d6c39f..24ce3df234 100644 --- a/admin.rst +++ b/admin.rst @@ -79,7 +79,7 @@ PostgREST outputs basic request logging to stdout. When running it in an SSH ses # another option is to pipe the output into "logger -t postgrest" -(Avoid :code:`nohup postgrest` because the HUP signal is used for manual `Schema Reloading`_.) +(Avoid :code:`nohup postgrest` because the HUP signal is used for manual :ref:`schema_reloading`.) Hardening PostgREST =================== @@ -113,6 +113,8 @@ The first step is to create an Nginx configuration file that proxies requests to } } +.. _block_fulltable: + Block Full-Table Operations --------------------------- @@ -135,7 +137,7 @@ This does not protect against malicious actions, since someone can add a url par Count-Header DoS ---------------- -For convenience to client-side pagination controls PostgREST supports counting and reporting total table size in its response. As described in `Limits and Pagination`_, responses ordinarily include a range but leave the total unspecified like +For convenience to client-side pagination controls PostgREST supports counting and reporting total table size in its response. As described in :ref:`limits`, responses ordinarily include a range but leave the total unspecified like .. code-block:: http @@ -227,10 +229,12 @@ Once you've verified that requests are as you expect, you can get more informati Restart the database and watch the log file in real-time to understand how HTTP requests are being translated into SQL commands. +.. _schema_reloading: + Schema Reloading ---------------- -Users are often confused by PostgREST's database schema cache. It is present because detecting foreign key relationships between tables (including how those relationships pass through views) is necessary, but costly. API requests consult the schema cache as part of :ref:`Resource Embedding`_. However if the schema changes while the server is running it results in a stale cache and leads to errors claiming that no relations are detected between tables. +Users are often confused by PostgREST's database schema cache. It is present because detecting foreign key relationships between tables (including how those relationships pass through views) is necessary, but costly. API requests consult the schema cache as part of :ref:`resource_embedding`. However if the schema changes while the server is running it results in a stale cache and leads to errors claiming that no relations are detected between tables. To refresh the cache without restarting the PostgREST server, send the server process a SIGHUP signal: diff --git a/api.rst b/api.rst index 6ec65039f8..edf2b8f4f5 100644 --- a/api.rst +++ b/api.rst @@ -14,7 +14,9 @@ There are no deeply/nested/routes. Each route provides OPTIONS, GET, POST, PATCH .. 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 `Resource Embedding`_. + 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 (Rows) --------------------------- @@ -57,6 +59,8 @@ To negate any operator, prefix it with :code:`not` like :code:`?a=not.eq.2`. For more complicated filters (such as those involving condition 1 OR condition 2) you will have to create a new view in the database. +.. _v_filter: + Vertical Filtering (Columns) ---------------------------- @@ -66,7 +70,7 @@ When certain columns are wide (such as those holding binary data), it is more ef GET /people?select=fname,age HTTP/1.1 -The default is :sql:`*`, meaning all columns. This value will become more important below in :ref:`Resource Embedding`_. +The default is :sql:`*`, meaning all columns. This value will become more important below in :ref:`resource_embedding`. .. _computed_cols: @@ -129,6 +133,8 @@ If you care where nulls are sorted, add nullsfirst or nullslast: You can also use :ref:`computed_cols` to order the results, even though the computed columns will not appear in the output. +.. _limits: + Limits and Pagination --------------------- @@ -246,7 +252,9 @@ You can use a tool like `Swagger UI `_ to create .. note:: - The OpenAPI information can go out of date as the schema changes under a running server. To learn how to refresh the cache see :ref:`Schema Reloading`_. + The OpenAPI information can go out of date as the schema changes under a running server. To learn how to refresh the cache see :ref:`schema_reloading`. + +.. _resource_embedding: Resource Embedding ================== @@ -303,7 +311,7 @@ PostgREST can also detect relations going through join tables. Thus you can requ .. note:: - Whenever foreign key relations change in the database schema you must refresh PostgREST's schema cache to allow resource embedding to work properly. See the section :ref:`Schema Reloading`_. + Whenever foreign key relations change in the database schema you must refresh PostgREST's schema cache to allow resource embedding to work properly. See the section :ref:`schema_reloading`. Embedded Filters and Order -------------------------- @@ -405,7 +413,7 @@ To create a row in a database table post a JSON object whose keys are the names The response will include a :code:`Location` header describing where to find the new object. If the table is write-only then constructing the Location header will cause a permissions error. To successfully insert an item to a write-only table you will need to suppress the Location response header by including the request header :code:`Prefer: return=minimal`. -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 `Vertical Filtering (Columns)`_ to these results. +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. .. note:: @@ -419,9 +427,9 @@ On the other end of the spectrum you can get the full created object back in the 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:`Client-Side Libraries`_ built for PostgREST. + 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. -To update a row or rows in a table, use the PATCH verb. Use :ref:`Horizontal Filtering (Rows)`_ to specify which record(s) to update. Here is an exmaple query setting the :code:`category` column to child for all people below a certain age. +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 exmaple query setting the :code:`category` column to child for all people below a certain age. .. code:: HTTP @@ -429,11 +437,11 @@ To update a row or rows in a table, use the PATCH verb. Use :ref:`Horizontal Fil { "category": "child" } -Updates also support :code:`Prefer: return=representation` plus `Vertical Filtering (Columns)`_. +Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. .. note:: - Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`Block Full-Table Operations`_. + Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`. Bulk Insert ----------- @@ -468,7 +476,7 @@ To bulk insert JSON post an array of objects having all-matching keys Deletions ========= -To delete rows in a table, use the DELETE verb plus :ref:`Horizontal Filtering (Rows)`_. For instance deleting inactive users: +To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instance deleting inactive users: .. code-block:: HTTP @@ -476,4 +484,4 @@ To delete rows in a table, use the DELETE verb plus :ref:`Horizontal Filtering ( .. note:: - Beware of accidentally delting all rows in a table. To learn to prevent that see :ref:`Block Full-Table Operations`_. + Beware of accidentally delting all rows in a table. To learn to prevent that see :ref:`block_fulltable`. diff --git a/intro.rst b/intro.rst index ffec7d75cd..9ed102b53c 100644 --- a/intro.rst +++ b/intro.rst @@ -35,6 +35,8 @@ Ecosystem PostgREST has a growing ecosystem of examples, and libraries, experiments, and users. Here is a selection. +.. _clientside_libraries: + Client-Side Libraries --------------------- From 07777ea76b8e68d61022b42db335af8b49b3bbb1 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 3 Feb 2017 16:56:25 -0800 Subject: [PATCH 063/549] Note websockets postgres integration From https://github.com/begriffs/postgrest/issues/278 --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index 9ed102b53c..3c7c9d7b72 100644 --- a/intro.rst +++ b/intro.rst @@ -54,6 +54,7 @@ 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. +* `frafra/postgresql2websocket `_ - Websockets * `matthewmueller/pg-bridge `_ - Amazon SNS * `aweber/pgsql-listen-exchange `_ - RabbitMQ * `SpiderOak/skeeter `_ - ZeroMQ From b237a4943c3d319d452f89d60d54b5e40903f3d0 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 6 Feb 2017 10:29:45 -0800 Subject: [PATCH 064/549] More client side libs --- intro.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/intro.rst b/intro.rst index 3c7c9d7b72..6d94d44da3 100644 --- a/intro.rst +++ b/intro.rst @@ -40,6 +40,8 @@ PostgREST has a growing ecosystem of examples, and libraries, experiments, and u Client-Side Libraries --------------------- +* `PierreRochard/angular2-postgrest `_ - JS, Angular 2 +* `tomberek/aor-postgrest-client `_ - JS, admin-on-rest * `hugomrdias/postgrest-url `_ - JS, just for generating query URLs * `john-kelly/elm-postgrest `_ - Elm * `mithril.postgrest `_ - JS, Mithril From 7742891c26f14919b51a1c041b652bd75ab3008e Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 6 Feb 2017 10:56:41 -0800 Subject: [PATCH 065/549] safeupdate via PGXN --- admin.rst | 9 ++++++++- intro.rst | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/admin.rst b/admin.rst index 24ce3df234..d157fc4d9e 100644 --- a/admin.rst +++ b/admin.rst @@ -130,7 +130,14 @@ However it's very easy to delete the **entire table** by omitting the query para DELETE /logs HTTP/1.1 -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 `_ PostgreSQL extension. It raises an error if UPDATE or DELETE are executed without specifying conditions. +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 `_ PostgreSQL extension. It raises an error if UPDATE or DELETE are executed without specifying conditions. To install it you can use the `PGXN `_ 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 `_ if finer access control is required. diff --git a/intro.rst b/intro.rst index 6d94d44da3..b87681d723 100644 --- a/intro.rst +++ b/intro.rst @@ -93,6 +93,7 @@ Extensions * `srid/spas `_ - allow file uploads and basic auth * `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server * `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware +* `pg-safeupdate `_ - Prevent full-table updates or deletes Commercial PaaS --------------- From 01def809373bd4a87894050600f8742e7b1669d0 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 6 Feb 2017 11:05:45 -0800 Subject: [PATCH 066/549] Google translate example --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index b87681d723..813b994466 100644 --- a/intro.rst +++ b/intro.rst @@ -64,6 +64,7 @@ These are PostgreSQL bridges that propagate LISTEN/NOTIFY to external queues for Example Apps ------------ +* `NikolayS/postgrest-google-translate `_ - Calling to external translation service * `CodeforAustralia/heritage-near-me `_ - Elm and PostgREST with PostGIS * `timwis/handsontable-postgrest `_ - An excel-like database table editor * `Recmo/PostgrestSkeleton `_ - Docker Compose, PostgREST, Nginx and Auth0 From 6e1e8f02de9532ae0f443b4a086b844df169b19a Mon Sep 17 00:00:00 2001 From: Hugo Dias Date: Fri, 10 Feb 2017 17:49:56 +0000 Subject: [PATCH 067/549] fix: fix server-proxy-uri config (#35) --- admin.rst | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/admin.rst b/admin.rst index d157fc4d9e..95fd11b680 100644 --- a/admin.rst +++ b/admin.rst @@ -37,7 +37,7 @@ db-anon-role String Y db-pool Int 10 server-host String \*4 server-port Int 3000 -server-proxy-url String +server-proxy-uri String jwt-secret String secret-is-base64 Bool False max-rows Int ∞ @@ -56,8 +56,25 @@ server-host Where to bind the PostgREST web server. server-port The port to bind the web server. -server-proxy-url - Overrides the base URL used within the OpenAPI self-documentation hosted at the API root path. +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.0.0", + "title": "PostgREST API", + "description": "This is a dynamic API generated by PostgREST" + }, + "host": "postgrest.com:443", + "basePath": "/", + "schemes": [ + "https" + ] + } + jwt-secret The secret used to decode JWT tokens clients provide for authentication. 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. secret-is-base64 From 022e1101e78abde563ef8cfb36feb7e8e8c8fe38 Mon Sep 17 00:00:00 2001 From: Tony Adams Date: Tue, 14 Feb 2017 23:35:05 -0600 Subject: [PATCH 068/549] fix a missing infinitive (typo) in intro.rst (#36) --- intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intro.rst b/intro.rst index 813b994466..352838f3a8 100644 --- a/intro.rst +++ b/intro.rst @@ -8,7 +8,7 @@ Using PostgREST is an alternative to manual CRUD programming. Custom API servers 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 db objects than to add guards in controllers. (This is especially true for cascading permissions in data dependencies.) It's easier set constraints than to litter code with sanity checks. +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 db 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 ---------------------- From d461e3387f7366f57fca50c2f207ac754c18235b Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 19 Feb 2017 18:11:40 -0800 Subject: [PATCH 069/549] Note the need for percent encoding in passwords --- admin.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin.rst b/admin.rst index 95fd11b680..f70cf4d66c 100644 --- a/admin.rst +++ b/admin.rst @@ -45,7 +45,7 @@ pre-request String ================ ====== ======= ======== db-uri - The standard connection PostgreSQL `URI format `_. On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. Also allows connections over Unix sockets for higher performance. + The standard connection PostgreSQL `URI format `_. Symbols and unusual characters in the password or other fields should be percent encoded to avoid a parse error. On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. Also allows connections over Unix sockets for higher performance. db-schema The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. db-anon-role From ecbf49ad7f3d8634d6ff3ffb93564183acf7a27a Mon Sep 17 00:00:00 2001 From: Peter Date: Mon, 20 Feb 2017 19:33:37 -0300 Subject: [PATCH 070/549] replace code:postgres by code:sql (#43) Code blocks with code:postgres are not highlighted (see site navigation), not works. Replaced by code:sql that works "fine" --- auth.rst | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/auth.rst b/auth.rst index 6ae84a0aae..0d1f842142 100644 --- a/auth.rst +++ b/auth.rst @@ -26,13 +26,13 @@ Here are the technical details. We use `JSON Web Tokens `_ to au 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:: postgres +.. 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:: postgres +.. code:: sql GRANT user123 TO authenticator; @@ -50,7 +50,7 @@ PostgREST can accommodate either viewpoint. If you treat a role as a single user 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:: postgres +.. code:: sql CREATE TABLE chat ( message_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), @@ -65,7 +65,7 @@ We want to enforce a policy that ensures a user can see only those messages sent PostgreSQL (9.5 and later) allows us to set this policy with row-level security: -.. code:: postgres +.. code:: sql CREATE POLICY chat_policy ON chat USING ((message_to = current_user) OR (message_from = current_user)) @@ -87,7 +87,7 @@ Alternately database roles can represent groups instead of (or in addition to) i SQL code can access claims through GUC variables set by PostgREST per request. For instance to get the email claim, call this function: -.. code:: postgres +.. code:: sql current_setting('request.jwt.claim.email') @@ -97,7 +97,7 @@ This allows JWT generation services to include extra information and your databa The current_setting function raises an exception if the setting in question is not present, as when a claim is missing from the JWT. Your SQL functions can either catch the exception, or you can set a default value for the database like this. - .. code:: postgres + .. code:: sql -- Prevent current_setting('request.jwt.claim.email') from raising -- an exception if the setting is not present. Default it to ''. @@ -105,7 +105,7 @@ This allows JWT generation services to include extra information and your databa If you are unable to issue an ALTER DATABASE statement (for instance on Amazon RDS), you can create a helper function to read environment variables and swallow exceptions. - .. code:: plpgsql + .. code:: sql create function env_var(v text) returns text as $$ declare @@ -130,7 +130,7 @@ Hybrid User-Group Roles There is no performance penalty for having many database roles, although roles are namespaced per-cluster rather than per-database so may be prone to collision within the database. You are free to assign a new role for every user in a web application if desired. 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:: postgres +.. code:: sql CREATE ROLE webuser NOLOGIN; -- grant this role access to certain tables etc @@ -158,7 +158,7 @@ Here's an example. In the config file specify a stored procedure: In the function you can run arbitrary code to check the request and raise an exception to block it if desired. -.. code:: postgres +.. code:: sql CREATE OR REPLACE FUNCTION check_user() RETURNS void LANGUAGE plpgsql @@ -193,7 +193,7 @@ You can create JWT tokens in SQL using the `pgjwt extension Date: Sun, 26 Feb 2017 22:23:26 +0545 Subject: [PATCH 071/549] Added a / after location /api (#44) --- admin.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin.rst b/admin.rst index f70cf4d66c..6b290105eb 100644 --- a/admin.rst +++ b/admin.rst @@ -118,10 +118,10 @@ The first step is to create an Nginx configuration file that proxies requests to server { ... # expose to the outside world - location /api { + location /api/ { default_type application/json; proxy_hide_header Content-Location; - add_header Content-Location /api$upstream_http_content_location; + add_header Content-Location /api/$upstream_http_content_location; proxy_set_header Connection ""; proxy_http_version 1.1; proxy_pass http://postgrest/; From b8f2ffc0a306be9ec283566f817d2aa03ccb952a Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 27 Feb 2017 09:38:21 -0800 Subject: [PATCH 072/549] Instructions for building docs locally --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000000..bb9afe28e1 --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +#### Sphinx source files for PostgREST documentation + +To generate HTML version: + +1. Install Sphinx from the [sphinx website](http://sphinx-doc.org/latest/install.html) +2. Clone this repository +4. Generate HTML + ```bash + cd postgrest-docs + sphinx-build -b html -a -n . _build + + # open _build/index.html in your browser + ``` + +--- + +**Sphinx Installation Notes:** + +* If you're on OSX you might want to install the Python from homebrew - then a simple `pip install sphinx` does the trick. +* For an easier time refreshing your local preview of docs as you change it, try [sphinx-autobuild](https://github.com/GaretJax/sphinx-autobuild). From 8b170afe4948e74b725547ce11e01384ad506c6f Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 13 Mar 2017 09:51:12 -0700 Subject: [PATCH 073/549] More extensions, example apps, client side libs (#45) --- intro.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/intro.rst b/intro.rst index 352838f3a8..644bd20a35 100644 --- a/intro.rst +++ b/intro.rst @@ -50,6 +50,7 @@ Client-Side Libraries * `JarvusInnovations/jarvus-postgrest-apikit `_ - JS, Sencha framework * `davidthewatson/postgrest_python_requests_client `_ - Python * `calebmer/postgrest-client `_ - JS +* `clesiemo3/postgrestR `_ - R External Notification --------------------- @@ -77,6 +78,7 @@ Example Apps * `myfreeweb/moneylog `_ - accounting web app in Polymer + PostgREST * `tyrchen/goodfilm `_ - example film api * `begriffs/postgrest-example `_ - sqitch versioning for API +* `SMRxT/postgrest-demo `_ - multi-tenant logging system In Production ------------- @@ -90,11 +92,12 @@ In Production Extensions ---------- +* `ppKrauss/PostgREST-writeAPI `_ - generate Nginx rewrite rules to fit an OpenAPI spec * `diogob/postgrest-ws `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY +* `pg-safeupdate `_ - Prevent full-table updates or deletes * `srid/spas `_ - allow file uploads and basic auth * `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server * `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware -* `pg-safeupdate `_ - Prevent full-table updates or deletes Commercial PaaS --------------- From 708e0cd0b6eee10dbcfceea18af5a6dfa89b019a Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Thu, 16 Mar 2017 00:33:32 -0700 Subject: [PATCH 074/549] Another site in production --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index 644bd20a35..50bbaacb80 100644 --- a/intro.rst +++ b/intro.rst @@ -88,6 +88,7 @@ In Production * `Redsmin `_ * `Image-charts `_ * `Drip Depot `_ +* `OpenBooking `_ Extensions ---------- From 8e6410e55d95aa905b35dfb3ed9c17bf42ec19d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20Ch=C3=A1vez?= Date: Fri, 24 Mar 2017 16:14:03 -0500 Subject: [PATCH 075/549] Add section for binary output (#46) --- api.rst | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/api.rst b/api.rst index edf2b8f4f5..46b9411ee9 100644 --- a/api.rst +++ b/api.rst @@ -210,6 +210,7 @@ The current possibilities are * text/csv * application/json * application/openapi+json +* application/octet-stream The server will default to JSON for API endpoints and OpenAPI on the root. @@ -243,16 +244,20 @@ This returns 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. -OpenAPI Support -=============== +Binary output +------------- -Every API hosted by PostgREST automatically serves a full `OpenAPI `_ description on the root path. This provides a list of all endpoints, along with supported HTTP verbs and example payloads. +If you want to return raw binary data from a :code:`bytea` column, you must specify :code:`application/octet-stream` as part of the :code:`Accept` header +and select a single column :code:`?select=bin_data`. -You can use a tool like `Swagger UI `_ to create beautiful documentation from the description and to host an interactive web-based dahsboard. The dashboard allows developers to make requests against a live PostgREST server, provides guidance with request headers and example request bodies. +.. code:: http + + GET /items?select=bin_data&id=eq.1 HTTP/1.1 + Accept: application/octet-stream .. note:: - The OpenAPI information can go out of date as the schema changes under a running server. To learn how to refresh the cache see :ref:`schema_reloading`. + If more than one row would be returned the binary results will be concatenated with no delimiter. .. _resource_embedding: @@ -350,7 +355,7 @@ Every stored procedure in the API-exposed database schema is accessible under th POST /rpc/function_name HTTP/1.1 -Procedures must used `named arguments `_. To supply arguments in an API call, include a JSON object in the request payload and each key/value of the object will become an argument. +Procedures must be used with `named arguments `_. To supply arguments in an API call, include a JSON object in the request payload and each key/value of the object will become an argument. For instance, assume we have created this function in the database. @@ -485,3 +490,14 @@ To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instanc .. note:: Beware of accidentally delting all rows in a table. To learn to prevent that see :ref:`block_fulltable`. + +OpenAPI Support +=============== + +Every API hosted by PostgREST automatically serves a full `OpenAPI `_ description on the root path. This provides a list of all endpoints, along with supported HTTP verbs and example payloads. + +You can use a tool like `Swagger UI `_ to create beautiful documentation from the description and to host an interactive web-based dahsboard. The dashboard allows developers to make requests against a live PostgREST server, provides guidance with request headers and example request bodies. + +.. note:: + + The OpenAPI information can go out of date as the schema changes under a running server. To learn how to refresh the cache see :ref:`schema_reloading`. From e54fb2a2643621cf572c8453f4890f6a287baa9e Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 25 Mar 2017 10:58:30 -0700 Subject: [PATCH 076/549] Misc improvements How to pass a single json object to stored proc List HTTP codes for db errors Point out 404 semantics of singular responses OR query workaround Include nginx example for singular rewrite --- admin.rst | 15 ++++++++-- api.rst | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 99 insertions(+), 4 deletions(-) diff --git a/admin.rst b/admin.rst index 6b290105eb..e3cd251689 100644 --- a/admin.rst +++ b/admin.rst @@ -278,11 +278,22 @@ As discussed in `Singular or Plural`_, there are no special URL forms for singul GET /people?id=eq.1 Accept: application/vnd.pgrst.object+json -This allows compound primary keys and makes the intent for singular response independent of a URL convention. However for any table which uses a simple primary key you can use Nginx to simulate the familiar URL convention. +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:: nginx - nginx code here + # 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 diff --git a/api.rst b/api.rst index 46b9411ee9..7595c33fa1 100644 --- a/api.rst +++ b/api.rst @@ -57,7 +57,26 @@ not negates another operator, see below To negate any operator, prefix it with :code:`not` like :code:`?a=not.eq.2`. -For more complicated filters (such as those involving condition 1 OR condition 2) you will have to create a new view in the database. +For more complicated filters (such as those involving disjunctions) 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:: http + + GET /fresh_stories HTTP/1.1 + +.. note:: + + We're working to extend the PostgREST query grammar to allow more complicated boolean logic, while continuing to prevent performance problems from arbitrary client queries. .. _v_filter: @@ -238,6 +257,8 @@ This returns { "id": 1 } +When a singular response is requested but no entries are found, the server responds with an empty body and 404 status code rather than the usual empty array and 200 status. + .. 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 singlular 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. @@ -374,7 +395,7 @@ The client can call it by posting an object like { "a": 1, "b": 2 } -The keys of the object match the parameter names. Note that PostgreSQL converts parameter names to lowercase unless you quote them like :sql:`CREATE FUNCTION foo("mixedCase" text) ...`. +The keys of the object match the parameter names. Note that PostgreSQL converts parameter names to lowercase unless you quote them like :sql:`CREATE FUNCTION foo("mixedCase" text) ...`. You can also call a function that takes a single parameter of type json by sending the header :code:`Prefer: params=single-object` with your request. That way the JSON request body will be used as the single argument. .. note:: @@ -501,3 +522,66 @@ You can use a tool like `Swagger UI `_ to create .. note:: The OpenAPI information can go out of date as the schema changes under a running server. To learn how to refresh the cache see :ref:`schema_reloading`. + +HTTP Status Codes +================= + +PostgREST translates `PostgreSQL error codes `_ 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 | ++--------------------------+-------------------------+---------------------------------+ +| 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 prereq state | ++--------------------------+-------------------------+---------------------------------+ +| 57* | 500 | operator intervention | ++--------------------------+-------------------------+---------------------------------+ +| 58* | 500 | system error | ++--------------------------+-------------------------+---------------------------------+ +| F0* | 500 | conf 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 authed 403, else 401 | insufficient privileges | ++--------------------------+-------------------------+---------------------------------+ +| other | 500 | | ++--------------------------+-------------------------+---------------------------------+ From 0f871373878751f3cf10e323a6298671726bafa1 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 26 Mar 2017 00:16:25 -0700 Subject: [PATCH 077/549] More improvements When to use RAISE in procs Fix broken links --- admin.rst | 2 +- api.rst | 35 ++++++++++++++++++++++++++++++++++- auth.rst | 4 +++- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/admin.rst b/admin.rst index e3cd251689..a1abf9cc90 100644 --- a/admin.rst +++ b/admin.rst @@ -271,7 +271,7 @@ In the future we're investigating ways to keep the cache updated without manual Alternate URL Structure ======================= -As discussed in `Singular or 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 +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:: http diff --git a/api.rst b/api.rst index 7595c33fa1..29fc3e1115 100644 --- a/api.rst +++ b/api.rst @@ -233,6 +233,8 @@ The current possibilities are The server will default to JSON for API endpoints and OpenAPI on the root. +.. _singular_plural: + Singular or Plural ------------------ @@ -289,7 +291,7 @@ In addition to providing RESTful routes for each table and view, PostgREST allow .. image:: _static/film.png -As seen above in `vertical_filtering`_ we can request the titles of all films like this: +As seen above in :ref:`v_filter` we can request the titles of all films like this: .. code-block:: http @@ -423,6 +425,35 @@ By default, a function is executed with the privileges of the user who calls it. We are considering allowing GET requests for functions that are marked non-volatile. Allowing GET is important for HTTP caching. However we still must decide how to pass function parameters since request bodies are not allowed. Also some query string arguments are already reserved for shaping/filtering the output. +Raising Errors +-------------- + +Stored procedures can return non-200 HTTP status codes by raising SQL exceptions. For instance, here's a saucy function that always errors: + +.. 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" + } + +You can customize the HTTP status code by raising particular exceptions according to the PostgREST :ref:`error to status code mapping `. For example, :code:`RAISE insufficient_privilege` will respond with HTTP 401/403 as appropriate. Insertions / Updates ==================== @@ -523,6 +554,8 @@ You can use a tool like `Swagger UI `_ to create The OpenAPI information can go out of date as the schema changes under a running server. To learn how to refresh the cache see :ref:`schema_reloading`. +.. _status_codes: + HTTP Status Codes ================= diff --git a/auth.rst b/auth.rst index 0d1f842142..f9abc431d6 100644 --- a/auth.rst +++ b/auth.rst @@ -270,7 +270,7 @@ PostgREST aims to do one thing well: add an HTTP interface to a PostgreSQL datab Schema Isolation ================ -A PostgREST instance is configured to expose all the tables, views, and stored procedures of a single schema specified in a server configuration file. This means private data or implementation details can go inside a private schema and be invisible to HTTP clients. You can then expose views and stored procedures which insulate the internal details from the outside world. It keeps you code easier to refactor, and provides a natural way to do API `versioning`_. For an example of wrapping a private table with a public view see the `Editing User Info`_ section below. +A PostgREST instance is configured to expose all the tables, views, and stored procedures of a single schema specified in a server configuration file. This means private data or implementation details can go inside a private schema and be invisible to HTTP clients. You can then expose views and stored procedures which insulate the internal details from the outside world. It keeps you code easier to refactor, and provides a natural way to do API versioning. For an example of wrapping a private table with a public view see the :ref:`public_ui` section below. SQL User Management =================== @@ -363,6 +363,8 @@ With the table in place we can make a helper to check a password against the enc end; $$; +.. _public_ui: + Public User Interface --------------------- From 27bcd8074ff4a5cd1e273d54f28e4e1afa16f9ca Mon Sep 17 00:00:00 2001 From: Dan Kamenov Date: Sun, 26 Mar 2017 21:26:14 -0600 Subject: [PATCH 078/549] Fix duplicate wording (#48) --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 29fc3e1115..56c97991b9 100644 --- a/api.rst +++ b/api.rst @@ -214,7 +214,7 @@ Note that the larger the table the slower this query runs in the database. The s Response Format --------------- -PostgREST uses proper HTTP content negotiation (`RFC7231 `_) to deliver the desired representation of a resource. That is to say the same API endpoint can respond respond in different formats like JSON or CSV depending on the client request. +PostgREST uses proper HTTP content negotiation (`RFC7231 `_) to deliver the desired representation of a resource. That is to say the same API endpoint can respond in different formats like JSON or CSV depending on the client request. Use the Accept request header to specify the acceptable format (or formats) for the response: From 1cab8cf82d292663d4468b83778782e38b42c648 Mon Sep 17 00:00:00 2001 From: Dan Kamenov Date: Wed, 29 Mar 2017 20:48:36 -0600 Subject: [PATCH 079/549] Fix Bad SQL coding practices in "Computed Columns" example (#51) --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 56c97991b9..c66ac80604 100644 --- a/api.rst +++ b/api.rst @@ -111,7 +111,7 @@ Filters may be applied to computed columns as well as actual table/view columns, -- (optional) add an index to speed up anticipated query CREATE INDEX people_full_name_idx ON people - USING GIN (to_tsvector('english', fname || ' ' || lname)); + USING GIN (to_tsvector('english', full_name(people))); A full-text search on the computed column: From 8a6b429c167b4819ff38ded1e11f7a66535c79be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20Ch=C3=A1vez?= Date: Tue, 4 Apr 2017 23:24:57 -0500 Subject: [PATCH 080/549] Add example for complex boolean logic (#52) --- api.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/api.rst b/api.rst index c66ac80604..e24963da10 100644 --- a/api.rst +++ b/api.rst @@ -425,6 +425,25 @@ By default, a function is executed with the privileges of the user who calls it. We are considering allowing GET requests for functions that are marked non-volatile. Allowing GET is important for HTTP caching. However we still must decide how to pass function parameters since request bodies are not allowed. Also some query string arguments are already reserved for shaping/filtering the output. +Complex boolean logic +--------------------- + +For complex boolean logic you can use stored procedures, an example: + +.. code-block:: postgresql + + CREATE FUNCTION key_customers(country TEXT, company TEXT, salary FLOAT) RETURNS SETOF customers AS $$ + SELECT * FROM customers WHERE (country = $1 AND company = $2) OR salary = $3; + $$ LANGUAGE SQL; + +Then you can query by doing: + +.. code-block:: http + + POST /rpc/key_customers HTTP/1.1 + + { "country": "Germany", "company": "Volkswagen", salary": 120000.00 } + Raising Errors -------------- From 42dd4ab607daf31ff62c6045a67aa18bacfa6f54 Mon Sep 17 00:00:00 2001 From: Ruslan Talpa Date: Thu, 6 Apr 2017 06:57:48 +0300 Subject: [PATCH 081/549] subzero domain change (#47) --- intro.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/intro.rst b/intro.rst index 50bbaacb80..dfa30970e2 100644 --- a/intro.rst +++ b/intro.rst @@ -100,10 +100,10 @@ Extensions * `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server * `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware -Commercial PaaS +Commercial --------------- -* `Sub0 `_ - Automated GraphQL & REST API with built-in caching (powered by PostgREST, not affiliated) +* `subZero `_ - Automated GraphQL & REST API with built-in caching (powered by PostgREST, not affiliated) Getting Support From 625f2ce8d77d85c9f8e6dbbff628c3d3a6e4fc49 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Wed, 5 Apr 2017 23:07:30 -0500 Subject: [PATCH 082/549] Update subzero message --- intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intro.rst b/intro.rst index dfa30970e2..b741071566 100644 --- a/intro.rst +++ b/intro.rst @@ -103,7 +103,7 @@ Extensions Commercial --------------- -* `subZero `_ - Automated GraphQL & REST API with built-in caching (powered by PostgREST, not affiliated) +* `subZero `_ - Automated GraphQL & REST API with built-in caching (powered in part by PostgREST) Getting Support From f52829d16536ce6212b70dc2a14c592e0195abaa Mon Sep 17 00:00:00 2001 From: Will O'Brien Date: Fri, 21 Apr 2017 17:46:15 -0400 Subject: [PATCH 083/549] App metadata instead of user metadata (#55) From the [docs](https://auth0.com/docs/metadata): > An authenticated user can modify data in their profile's user_metadata, but not in their app_metadata. Seems role-type information should be stored in `app_metadata`. --- auth.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/auth.rst b/auth.rst index f9abc431d6..ab271a11c9 100644 --- a/auth.rst +++ b/auth.rst @@ -236,14 +236,14 @@ An external service like `Auth0 `_ can do the hard work tran To use Auth0, copy its client secret into your PostgREST configuration file as the :code:`jwt-secret`. (Old-style Auth0 secrets are Base64 encoded. For these secrets set :code:`secret-is-base64` to :code:`true`, or just refresh the Auth0 secret.) You can find the secret in the client settings of the Auth0 management console. -Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `user metadata `_. Then, you will need to write a rule that will extract the role from the user metadata and include a :code:`role` claim in the payload of our user object. Afterwards, in your Auth0Lock code, include the :code:`role` claim in your `scope param `_. +Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata `_. Then, you will need to write a rule that will extract the role from the user metadata and include a :code:`role` claim in the payload of our user object. Afterwards, in your Auth0Lock code, include the :code:`role` claim in your `scope param `_. .. code:: javascript // Example Auth0 rule function (user, context, callback) { - var role = user.user_metadata.role; - user.role = role; + user.app_metadata = user.app_metadata || {}; + user.role = user.app_metadata.role; callback(null, user, context); } From bfe6bd1165dc8506e8fee39949bb575a563b0820 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 23 Apr 2017 20:10:42 -0500 Subject: [PATCH 084/549] Confirmed more production users --- intro.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/intro.rst b/intro.rst index b741071566..a1dfc3f4f3 100644 --- a/intro.rst +++ b/intro.rst @@ -89,6 +89,8 @@ In Production * `Image-charts `_ * `Drip Depot `_ * `OpenBooking `_ +* `Convene `_ by Thomson-Reuters +* `eGull `_ Extensions ---------- From 2ce053b69eaae38fb07fd649218c9909e467e119 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 23 Apr 2017 20:25:44 -0500 Subject: [PATCH 085/549] Add some things people have said --- intro.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/intro.rst b/intro.rst index a1dfc3f4f3..74e8d622fd 100644 --- a/intro.rst +++ b/intro.rst @@ -107,6 +107,28 @@ Commercial * `subZero `_ - Automated GraphQL & REST API with built-in caching (powered in part by PostgREST) +Testimonials +############ + + "It's so fast to develop, it feels like cheating!" + + -- François-G. 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 Getting Support ################ From 8f5a3f50c25ab826e380f9a56669a2ada686cb2d Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 23 Apr 2017 21:45:47 -0500 Subject: [PATCH 086/549] Another queue bridge --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index 74e8d622fd..860b59197e 100644 --- a/intro.rst +++ b/intro.rst @@ -61,6 +61,7 @@ These are PostgreSQL bridges that propagate LISTEN/NOTIFY to external queues for * `matthewmueller/pg-bridge `_ - Amazon SNS * `aweber/pgsql-listen-exchange `_ - RabbitMQ * `SpiderOak/skeeter `_ - ZeroMQ +* `FGRibreau/postgresql-to-amqp `_ - AMQP Example Apps ------------ From f2f392b0f42b8b2cf07f0bcd83997ccaae5c9c7c Mon Sep 17 00:00:00 2001 From: Steve Phillips Date: Mon, 10 Apr 2017 08:47:18 -0500 Subject: [PATCH 087/549] api.rst typo: dahsboard -> dashboard (#53) --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index e24963da10..6c01c9358f 100644 --- a/api.rst +++ b/api.rst @@ -567,7 +567,7 @@ OpenAPI Support Every API hosted by PostgREST automatically serves a full `OpenAPI `_ description on the root path. This provides a list of all endpoints, along with supported HTTP verbs and example payloads. -You can use a tool like `Swagger UI `_ to create beautiful documentation from the description and to host an interactive web-based dahsboard. The dashboard allows developers to make requests against a live PostgREST server, provides guidance with request headers and example request bodies. +You can use a tool like `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, provides guidance with request headers and example request bodies. .. note:: From fc65694c0a4c1386bcdeeec5f5469f5160fb0213 Mon Sep 17 00:00:00 2001 From: Steve Phillips Date: Mon, 10 Apr 2017 08:47:59 -0500 Subject: [PATCH 088/549] api.rst typo: delting -> deleting (#54) --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 6c01c9358f..bed4863625 100644 --- a/api.rst +++ b/api.rst @@ -560,7 +560,7 @@ To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instanc .. note:: - Beware of accidentally delting all rows in a table. To learn to prevent that see :ref:`block_fulltable`. + Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`. OpenAPI Support =============== From 1a5cadb8550992f928f0623b23c5677f4c79d095 Mon Sep 17 00:00:00 2001 From: Matt Bretl Date: Wed, 17 May 2017 04:01:28 +0100 Subject: [PATCH 089/549] auth.rst: minor fixes to example scripts (#68) --- auth.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/auth.rst b/auth.rst index ab271a11c9..6b490b2db2 100644 --- a/auth.rst +++ b/auth.rst @@ -202,7 +202,7 @@ Next write a stored procedure that returns the token. The one below returns a to CREATE FUNCTION jwt_test() RETURNS public.jwt_token LANGUAGE sql AS $$ - SELECT jwt.sign( + SELECT sign( row_to_json(r), 'mysecret' ) AS token FROM ( @@ -224,7 +224,7 @@ PostgREST exposes this function to clients via a POST request to `/rpc/jwt_test` ALTER DATABASE mydb SET "app.jwt_secret" TO '!!secret!!'; -- then all functions can refer to app.jwt_secret - SELECT jwt.sign( + SELECT sign( row_to_json(r), current_setting('app.jwt_secret') ) AS token FROM ... @@ -295,7 +295,7 @@ First we'll need a table to keep track of our users: 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), + 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. @@ -391,7 +391,7 @@ As described in `JWT from SQL`_, we'll create a JWT inside our login function. N raise invalid_password using message = 'invalid user or password'; end if; - select jwt.sign( + select sign( row_to_json(r), 'mysecret' ) as token from ( From 3cb2c0f297ac02373fe3ef7ea0e84007ed278963 Mon Sep 17 00:00:00 2001 From: James Dalton Date: Wed, 24 May 2017 20:51:27 -0700 Subject: [PATCH 090/549] switch tar command for xz instead of gzip (#73) --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 50c6ba8158..f1bb74f776 100644 --- a/install.rst +++ b/install.rst @@ -9,7 +9,7 @@ The release page has precompiled binaries for Mac OS X, Windows, and several Lin # Untar the release (available at https://github.com/begriffs/postgrest/releases/latest) - $ tar zxf postgrest-[version]-[platform].tar.xz + $ tar Jxf postgrest-[version]-[platform].tar.xz # Try running it $ ./postgrest --help From 6bb81b5d4b1a4e4a431baebea59d1f2870c3f0a4 Mon Sep 17 00:00:00 2001 From: Steve Phillips Date: Sat, 27 May 2017 08:22:35 -0700 Subject: [PATCH 091/549] api.rst typo: Directories -> Directors (#74) --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index bed4863625..ed30a7239a 100644 --- a/api.rst +++ b/api.rst @@ -335,7 +335,7 @@ Which would return } ] -PostgREST can also detect relations going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). You can also reverse the direction of inclusion, asking for all Directories with each including the list of their Films. +PostgREST can also detect relations going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). You can also reverse the direction of inclusion, asking for all Directors with each including the list of their Films. .. note:: From e35f2d28c771e53f1a88617099cdff114f92dc6c Mon Sep 17 00:00:00 2001 From: Michel Pelletier Date: Wed, 24 May 2017 20:55:31 -0700 Subject: [PATCH 092/549] Use current_setting optional param to suppress raise (#70) --- auth.rst | 36 ++---------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/auth.rst b/auth.rst index 6b490b2db2..f953d5aa66 100644 --- a/auth.rst +++ b/auth.rst @@ -89,41 +89,9 @@ SQL code can access claims through GUC variables set by PostgREST per request. F .. code:: sql - current_setting('request.jwt.claim.email') + 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. - -.. note:: - - The current_setting function raises an exception if the setting in question is not present, as when a claim is missing from the JWT. Your SQL functions can either catch the exception, or you can set a default value for the database like this. - - .. code:: sql - - -- Prevent current_setting('request.jwt.claim.email') from raising - -- an exception if the setting is not present. Default it to ''. - ALTER DATABASE your_db_name SET request.jwt.claim.email TO ''; - - If you are unable to issue an ALTER DATABASE statement (for instance on Amazon RDS), you can create a helper function to read environment variables and swallow exceptions. - - .. code:: sql - - create function env_var(v text) returns text as $$ - declare - result text; - begin - begin - select current_setting(v) into result; - exception - when undefined_object then - return null; - end; - - return result; - end; - $$ stable language plpgsql; - - -- now you can call call for instance - -- SELECT env_var('request.jwt.claim.email') +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 ~~~~~~~~~~~~~~~~~~~~~~~ From 49af7dcbc2d47037ca1d69519dd5bc160812d321 Mon Sep 17 00:00:00 2001 From: Steve Phillips Date: Mon, 29 May 2017 08:17:21 -0700 Subject: [PATCH 093/549] api.rst: Added example alluded to in docs (#77) --- api.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index ed30a7239a..461afae7ac 100644 --- a/api.rst +++ b/api.rst @@ -335,7 +335,11 @@ Which would return } ] -PostgREST can also detect relations going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). You can also reverse the direction of inclusion, asking for all Directors with each including the list of their Films. +PostgREST can also detect relations going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). You can also reverse the direction of inclusion, asking for all Directors with each including the list of their Films: + +.. code-block:: http + + GET /directors?select=films{title,year} HTTP/1.1 .. note:: From cb90e3e5577140b27af14afe5bd1f0436a2450ae Mon Sep 17 00:00:00 2001 From: Steve Phillips Date: Mon, 29 May 2017 09:46:12 -0700 Subject: [PATCH 094/549] api.rst: de-duped a word, added commas (#78) --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 461afae7ac..2df2c91edd 100644 --- a/api.rst +++ b/api.rst @@ -348,7 +348,7 @@ PostgREST can also detect relations going through join tables. Thus you can requ Embedded Filters and Order -------------------------- -Embedded tables can be filtered and ordered similarly to their top-level counterparts. To to do so prefix the query parameters with the name of the embedded table. For instance to order the actors in each film: +Embedded tables can be filtered and ordered similarly to their top-level counterparts. To do so, prefix the query parameters with the name of the embedded table. For instance, to order the actors in each film: .. code-block:: http From 174ccadb15590f5c4163ff1c851c44d362eb9f71 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Mon, 29 May 2017 18:08:38 -0500 Subject: [PATCH 095/549] Update ecosystem --- intro.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/intro.rst b/intro.rst index 860b59197e..2f3b0e0df7 100644 --- a/intro.rst +++ b/intro.rst @@ -40,17 +40,17 @@ PostgREST has a growing ecosystem of examples, and libraries, experiments, and u Client-Side Libraries --------------------- -* `PierreRochard/angular2-postgrest `_ - JS, Angular 2 * `tomberek/aor-postgrest-client `_ - JS, admin-on-rest * `hugomrdias/postgrest-url `_ - JS, just for generating query URLs * `john-kelly/elm-postgrest `_ - Elm * `mithril.postgrest `_ - JS, Mithril -* `thejettdurham/postgrest-sharp-client `_ - C#, RestSharp * `lewisjared/postgrest-request `_ - JS, SuperAgent * `JarvusInnovations/jarvus-postgrest-apikit `_ - JS, Sencha framework * `davidthewatson/postgrest_python_requests_client `_ - Python * `calebmer/postgrest-client `_ - JS * `clesiemo3/postgrestR `_ - R +* `PierreRochard/postgrest-angular `_ - TypeScript, generate UI from API description +* `thejettdurham/postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp External Notification --------------------- @@ -66,6 +66,7 @@ These are PostgreSQL bridges that propagate LISTEN/NOTIFY to external queues for Example Apps ------------ +* `https://github.com/subzerocloud/postgrest-starter-kit `_ - Boilerplate for new project * `NikolayS/postgrest-google-translate `_ - Calling to external translation service * `CodeforAustralia/heritage-near-me `_ - Elm and PostgREST with PostGIS * `timwis/handsontable-postgrest `_ - An excel-like database table editor @@ -80,6 +81,7 @@ Example Apps * `tyrchen/goodfilm `_ - example film api * `begriffs/postgrest-example `_ - sqitch versioning for API * `SMRxT/postgrest-demo `_ - multi-tenant logging system +* `PierreRochard/postgrest-boilerplate `_ - example auth backend In Production ------------- From f4c6a99adbe3457adb3a3dc84a58462599a80284 Mon Sep 17 00:00:00 2001 From: Leon du Toit Date: Thu, 8 Jun 2017 08:55:42 +0200 Subject: [PATCH 096/549] Address JWT security in docs, related to postgrest issue #842 (#79) --- auth.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/auth.rst b/auth.rst index f953d5aa66..d76ddcfe87 100644 --- a/auth.rst +++ b/auth.rst @@ -228,6 +228,19 @@ Our code requires a database role in the JWT. To add it you need to save the dat } }) +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 `_ is voiced in detail `elsewhere on the web `_. 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 `_. Recommendations about high quality libraries for usage in API clients can be found on `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 `_ because most, if not all, solutions to the problems that arise when you do, `do not 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. + .. _ssl: SSL From 83e4ad41fe02c0630e4f28e46c4bc351947de5db Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 18 Jun 2017 11:49:08 -0500 Subject: [PATCH 097/549] Deprecate {} for resource embedding --- api.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/api.rst b/api.rst index 2df2c91edd..0128e0b229 100644 --- a/api.rst +++ b/api.rst @@ -311,7 +311,7 @@ However because a foreign key constraint exists between Films and Directors, we .. code-block:: http - GET /films?select=title,directors{last_name} HTTP/1.1 + GET /films?select=title,directors(last_name) HTTP/1.1 Which would return @@ -335,11 +335,15 @@ Which would return } ] +.. note:: + + As of PostgREST v4.1, parens :code:`()` are used rather than brackets :code:`{}` for the list of embedded columns. Brackets are still supported, but are deprecated and will be removed in v5. + PostgREST can also detect relations going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). You can also reverse the direction of inclusion, asking for all Directors with each including the list of their Films: .. code-block:: http - GET /directors?select=films{title,year} HTTP/1.1 + GET /directors?select=films(title,year) HTTP/1.1 .. note:: @@ -352,13 +356,13 @@ Embedded tables can be filtered and ordered similarly to their top-level counter .. code-block:: http - GET /films?select=*,actors{*}&actors.order=last_name,first_name HTTP/1.1 + GET /films?select=*,actors(*)&actors.order=last_name,first_name HTTP/1.1 This sorts the list of actors in each film but does *not* change the order of the films themselves. To filter the roles returned with each film: .. code-block:: http - GET /films?select=*,roles{*}&roles.character=in.Chico,Harpo,Groucho HTTP/1.1 + GET /films?select=*,roles(*)&roles.character=in.Chico,Harpo,Groucho HTTP/1.1 Once again, this restricts the roles included to certain characters but does not filter the films in any way. Films without any of those characters would be included along with empty character lists. From 127fbb41d5d31a2eef87fdfb0e37c5edafe9c790 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 18 Jun 2017 12:06:21 -0500 Subject: [PATCH 098/549] Note function volatility in RPC Fixes #64 --- api.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 0128e0b229..52b0e714b9 100644 --- a/api.rst +++ b/api.rst @@ -380,12 +380,14 @@ The PostgREST URL grammar limits the kinds of queries clients can perform. It pr Stored Procedures ================= -Every stored procedure in the API-exposed database schema is accessible under the :code:`/rpc` prefix. The API endpoint supports only POST which executes the function. Such function can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). +Every stored procedure in the API-exposed database schema is accessible under the :code:`/rpc` prefix. The API endpoint supports only POST which executes the function. .. code:: http POST /rpc/function_name HTTP/1.1 +Such functions can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). However procedures in PostgreSQL marked with :code:`stable` or :code:`immutable` `volatility `_ can only read, not modify, the database and PostgREST executes them in a read-only transaction compatible for read-replicas. + Procedures must be used with `named arguments `_. To supply arguments in an API call, include a JSON object in the request payload and each key/value of the object will become an argument. For instance, assume we have created this function in the database. From e8cdd4463b8fc74811d25cd3aa3472eb5b777a02 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 18 Jun 2017 21:52:53 -0500 Subject: [PATCH 099/549] Example of quoted comma in IN operator --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 52b0e714b9..51716b41ff 100644 --- a/api.rst +++ b/api.rst @@ -46,7 +46,7 @@ lt less than neq not equal like LIKE operator (use * in place of %) ilike ILIKE operator (use * in place of %) -in one of a list of values e.g. :code:`?a=in.1,2,3` +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 checking for exact equality (null,true,false) @@ full-text search using to_tsquery @> contains e.g. :code:`?tags=@>.{example, new}` From cee92cac05fa5f70b627b4c0c292448a53f868d5 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 18 Jun 2017 22:19:46 -0500 Subject: [PATCH 100/549] How to read headers/cookies from a proc --- api.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api.rst b/api.rst index 51716b41ff..cd10a588e1 100644 --- a/api.rst +++ b/api.rst @@ -435,6 +435,15 @@ By default, a function is executed with the privileges of the user who calls it. We are considering allowing GET requests for functions that are marked non-volatile. Allowing GET is important for HTTP caching. However we still must decide how to pass function parameters since request bodies are not allowed. Also some query string arguments are already reserved for shaping/filtering the output. +Accessing Request Headers/Cookies +--------------------------------- + +Stored procedures can access request headers and cookies by reading GUC variables set by PostgREST per request. They are named :code:`request.header.XYZ` and :code:`request.cookie.XYZ`. For example, to read the value of the Origin request header: + +.. code-block:: postgresql + + SELECT current_setting('request.header.origin', true); + Complex boolean logic --------------------- From 6382b597fad39f4bff404260e0659accefffb92a Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 2 Jul 2017 21:17:49 -0500 Subject: [PATCH 101/549] Zeroeth tutorial --- _static/tuts/tut0-request-flow.png | Bin 0 -> 16696 bytes admin.rst | 2 + conf.py | 4 +- index.rst | 5 + install.rst | 2 + tutorials/tut0.rst | 214 +++++++++++++++++++++++++++++ 6 files changed, 225 insertions(+), 2 deletions(-) create mode 100644 _static/tuts/tut0-request-flow.png create mode 100644 tutorials/tut0.rst diff --git a/_static/tuts/tut0-request-flow.png b/_static/tuts/tut0-request-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..24f2986e81ae431f8e0ab1366ca65d6266ffcad9 GIT binary patch literal 16696 zcmb_^Ra{$7)GaQ>p~bbhyE`rJ?(Po79g2H#cPZ`~+}+(B3c=lT^S@8`K74Oql9QP; zCzG7t=-z9sok%4GNt7@6UmzeLP^6`Psz5+MGJx|=2yo!{VhMS9Z~|i~DlZBFQ6G== zZVU@PCpDE)0hfDIK|lnCKtQ~Livs^aK)A6$K%5ytK=7qQK;SrK1C#~87vM}}C4WMI z{(I(k{Y?UwAUaA&xPVg(@PQ9e7@I>6&cM4$%ZtPB!9$^OAhb%!9RBw?cPCLx@a-6Y z4|s@)Lh^A42oX%_pQ37>>lZm5+3G_tpdFX#_74YUyMm|h@*=PdM$zB+@ni9m%IXI* zR|zyYb_VkpsYD4^$L3Je>Gbz}`nu zJoe=S5Up+_=Ewf_5Y#-|7`wRKVaUhECM{QKH#U`(SuJ5m685ba25fC9D>mZ%X{npH z!=a?43=!K)PEM5KNKE;iJ{ww!G;I|ne#QFNpq_`HALx$gKhfa7FgxpXpIojwlpsmG z_W#uG`FyB70ypG&b#*x}SBf0=ql4DHMAOw^s2E@Ps!E)!6`x?(#gnG#!Y_K__&lRh z6?g{2ZeIs4FE4XbQ&SH~;VEJTo&WCRZ;m^Dp5NnIDF6W2uD3eWT5!G%Gf$uS`3sJl zcb*sdFa(aet&~Um#?|FVHCFsceEf)spNy=mFjDxygPWV%R!#_q4~uo5OryTQ+j%20 zArFi3zt=?1@2mpYvBZv&S(tc&Z=f|~3mmD9jEH1Y0`C|dJUhK7M`{dUTFi(7r#ySD zMBMsg#@b%#QPdSi^~4 zDCc>_aFA0|)z5RBUKv1PiW(!PbEcVDnW$xzmAahn=c`MZrDOMl^BfyLz`fv_2>|fj z-rk-kCMF(%+q4Vw=J}>ChQYCCTO3sN^zD8w;eg^^QHxgb_|MIZg$A>{c+uQJvAv>1 z1oQsQbJ+U2j>Tm9+?KR-V>l_`zp3I|s*ociKupr;`*Pd!SFX|Zde#_ch3GZfe9r3K zFwCfoHu6nic+x;UPq^Xu1Bc1h#;%PQ_j=}ZsdD8gv$Wcv&B@q2)2KJl(8AnYz58KV7d)8! zQlTBQL%CB1Z>2QRVZ=mKzOW@(Ip^KmNY(!IPVht*ll+d3pjk(O2~Wm|P8%YHC@IT$ zBjwU|N(veBWzE!@f-|XEHmc0`sO=)Ia&9Xl#p257Xgo!M=BxMI$z9Io<|0SCWz}OH z-@p^|NYuwH^&rF#r7FZ1k-NdW+NrXxr_$1}brDt7g;Ve%bMzw&xxG;_PiXD|xZ>f# z_K1ht{ce3;7Yq>+gGJkXqkiT;dmHjcu(&u4XN`ZaNK2$3gb;lVp+Ar$2-2#?j2A(L z)I@|_g-gnK1+}p(`FCuuH{ub#8c>A`FU%N?P0-W@cds+@A0IZ<4mzMeEu1vm)F&1D z`}@>BPnV2&0iQ3I3d06d$-vK$ay%)NVn#Rl2q@Y?^kPD!7)UtC5)m$Rh)#YdUU(@8 z<@_&N@C=aHq}mi5mVRQZ%6SK{1vfW-Cl2hr;3;^@Rw|!03~&R&RcqCow3utV6;x#> zHBM)GiT1NyEriZO5dMiH&%1*cOUnec+pCzmEn&5oOMquhtq|z_&hYJiLGgmu<=E9j z<&lqS`}u1oISXfyBwvOKo5YtmbSD~?ucPK8gXKk1$$@^*3=rFpHwB9^ZvCwGd;f5C zBAzUB0ZSx*^Ds+K*;1Je6&SD^?I!r94;~)yH+y!F2wX zQHel$Wfopl?YzGTM}xDnvQAP`Qv6(|c~4jzw>vKtrVR!Tfq7~J@?vH-qo#ob7<%Np zJo&=t7W~a3GLa)n!p?`sWEF9wO8r_8F;H<(rC(0J{;rxMDq5Z|Y?wL3T4V8eZ1v9u zF9G`Fi{+|*k!H7Y3+`~1$H!e->KxlQ*0-D3eNeM7Vd&?Mr=Oms4ubfVdU;X9g$pKg zR(ZLCKK=l+u|&Z7`ue59xIterL9SR*+#fLlv`y4^Odg2QVytbQ;$&RFaQ?i20-q5= zi5H$4x43k1)wIMMQZht%AP>ycT%dZ~vGgG_UtC2fb1OgZg@FYL6*V=y{?$5HWqrN! zunB81xhnAhOTw>8`&!7)Wb2R4s*?h0vq$`ANkf)*F$zpxlaJSjRy16ed`ZiioV2tN z))B%56yA29U$gQ7l7V~wUafl#1MdRgr$Z=9yqTw`c4Ni{+7Hv6k?F>cLJ))7uggBh zrVw_eP8IzAeT8F@=>H%kM(6SSOPek*L zSw$y}T{fm{hAt7o9PE(Mj8mzBO2)_g6u0=Fp{ayXJn-oC61>K)5WR5POdKtLeW-wP}CTSLXbZx>}o&5!Tye9EvC)|O=W>WQSndfhGV;9pta zEYVPs996X4$jt){lV7{@e{zGy(xs|rIHWGADuegYx$H~QIphL+k1oxbDeWJ?#6MoD;{a$5LIHk<2 zzBF6<7aM)bE~G}D(_XSg$^FikA)BaQ{n;+7QsBh5r7(uOTT9;FrL&Q-05uzR-(^>H zaY2HVQR|HZ8V)&~D1Jn^+1@9dk%27a-cumFRUQdfZb z_+LU>@ZiZ{N;57|93;krB9_C6Hi8QfB_3!G^T`BR8MDqxak0Yh3ptpB)w9k#9q0JG z&BA%@=UJj$`h7cn$z{3JTxS2|E{gBlRm7i1Xf-SCE2wQv!2nUq`6&2y%Os2W@)6xu z-Pcz`Yz!DTQ*y;xihS?qB>4%4``}3?LlChBU1kJ5y#4$>%0ES%3RY%6XUk6HQcAt5 znE00IXJP#1`7F6Zy+L>R1;M#8B`SY4)HUSl?cZvCpb6gJQs?zyNNfGGyGPo{Y@D|60O9QIaK6_kI=AmVpc%6BA_sO~gV8=d4S!qN-$`H1} zDr$$xSVE@>5FCj`z5n#)!(`a~9%eRvel;@+5EeQs_J|k?C`Wv}vMOIq451w&iF)|w zSI@W#=_oAxzP4q(g(b})Y0ZZGD++0rkJV2c0wL&vN2`S* zp`k3A!YYOm;iZdE&>aG@w$CSe`R4VcvMhd(+hfWjSwteeiWnAm`_ZHW2n)0*fAvvz z?Ti_vF?uZ{&i0fdfg@f_+RYajMHB0V1}NPLCvx$|BFZdtvzr}9B5Fl*g`CV`)oq?Sk1z31`9Lu)vyzfKu34C5TgcvL~^2)A9(reQ2N@s#Nd*yL9x(Re@F`hT9x;I%(%T#}7-Y%*MQ&CcHzEE}jp zu0wBm{QJ@28e$r4Q8PY`51hg_-;P(NBiE0~g=PLs{0)&FBaW3OqNt;aj zQZ0JT;f;_dkLuu$RaiU(TSs9U+eOzWH!FWuqCdmeAwQ#C)7DMkY~*1wLRT<{8>V} zkb6D^FJG*P5b^d1*Le)al7X7qKLzY=tURF_4`^rR5 z-@!zzC^tPh?$7r}^H#~P?Bs@cq+JikwG1&a@N`sMw8zrown!U(kbG(d4mwC43@reF zQ`|e7Ib4jJH6@}s`4G>Lkt`!xHc-%6lCZ$|GajikpFRDfipRvH>K0t8}<_1-F~1zV`azkgwIH3U{+ zaiz8h^0t1rtxNTE^=nfdnUDjPsv1XUgx4HB*E#W5UB@mr88&kR9E7m(x5o>= znNnI!0yg3A5vP` z!#UO5yXm1hyU2UCblWao7h>h7iBe?N_;ZX4bLm(5GW4%BRDdC~D2t~V8dJ9wVf)Zt zPSy=Ekpe-SP)~MJZWP;y&7n2GLiwsC0s_>EHE@MUcxqsYaV<37@R~`4JAu#dejed` zFq|XfL$-d=sZWxpO?&}sLP15w2UnE5DSbxCrdT%ccS ztK$HEIDtK0oHzpR>4GGE7TOX*HnY(Yv53}xfSN_yOIynu&E(b@m^wmFcu(l(YzN#T zn9cg4M~%2-(qh*g>$_`(w&gV2(B}H#WYOh^;r{7w3`mL+s0)e&)9KEX{KR^aqiFrS zMWZh}+}W)>)BJkhxo@9|g&{~F9YkU^89hMT#>+MO{Ob)>dHit)JIn2<=An!mkB>BT zUazCpmYp|==6MCOUQx?^CESdwN=G-IhN&sQCtDkmg+W_Pe3kq=xyf}0uCYdf3#?ZSS(H90X|7gD9xg{F#dlz$p>%)Nwsw8x8f&hNaCWTK);^y^KA7WDsM zE32r?ueLhwl;-?V!N@^OPcl;naZu zHgHZgdc8Z3&kn|EUH}eV@wLpzoPdeRMA8cDn1}5Z+i0gVM3u zBw1Y{O^|C{GlSC>J5BGG^ii2|(WL>v)%q9=QcQE$pX>ps{r$y<75zO@tf@-sV=oIJ zpk}S8_EAY0Mc4l@c~K|mr|S~i$E3%ZJGL6RS>7=@t%!1QtgJY!u1>2;o?KWm z=dPwyRK+V-$(TFgod{$yZm842Q&-XvKueDrzl)C;PQ%{d$`Sr~$ShH`( zXj62f57w{5OY#y>H>DP5T&WMzlvvvKJwRU>IkqUa_QS-RbC1Qq>RR}F0vmrdk2!WM z#tF%QPMCK;(M{Stjq+06;x&g{|5s~lCcV+yqlt;v2cU;@QzM|0i+r~Fh z-%lWse5Yx^$R-)$AorC#X-Ez5)BoeuZJgX9)82GLL4$=~)eQT%z$ZplzU!>Qob7_w1(YwT8O=ZwK_B1Wqkz@j9twM9gWEb5$oQ-J9p zL&2*paVm=fz=m;H$BJ@%GFz%pw_9(rHr?s=a~z+T$byxNq)-7V=DFo4J_oN9qr0?F z>0tg0w!T<#+$8({H=hL?Ic)grw{IxIr^QJCerX@dxmSte1N7;!?|;kC18C~J9MoDD zKdb-!+@QruwVqeg|6#AqYwQ% z)8TKbTIlC-$sRGEyexM#-k@XLnvO$=T>PCp?E)2tU;D@J4lOJ}Xl7SoVPVd; z%T=eB6c;HRON$te8m7zLN`dEvP;9p@H*mgI{9EIz8qYOOGIgp{bFXkV+^Gv%`oTGD zbArSTUy#NaXsn(pAA>Jj$5Oin6EJ)|Pd6Cwa=h4n{4nsw$2X>RR^fVJP1-BT@N_@9 zbnbOh7 zls5-TIl7OR@Cso#Yv0meJj^#63L|n=J`lZt=Y~8o-$N5g6X@U3PZ#vzni04|2Cu9% zS8I(YC@M04ySqQLqbklMzZiA_$8Sv?yXNau6+IKMbbqOY@2h=x7Gvo-MCxS%jI=C|H&JTi~iyf|juUV`g` zRn!)9djpv2<(-JfN|!_TNq3y;D@LR$(fw7_d6ktqc`W*r{uVj6DK5`_oE!(v zb~h(AVOFV96hlbI8&TmXoDIeQjnCgn?zR$aB&|ysL(|ok>XO`DN?B6R6g7^^vjyFJ z%RS~JR-KrftBa_MqGBbcZQTjBM!-dogDHhNP%11b+6=4Ok{(MG$=0{)(|21^pG;>B zHlIlS&&NziOKYXVj7+cBI*$lj2ZX=%fM%B>i)sPn2~vW^ywggM2tf%A7p6(7wVwS? z@rIv5Had#QDJdWK*8^w)Gr$L~&YNL^4mLz0aR|Xc=%9l@T9JUoM^!XekHePG6gKOs zs=gR%1Pk;18z1i4mj!(^lxIIF+Jv}Z*TvL3MiVlto5kx3%g*tFA|1d%u z=YHVSpV#St%#6yODZr5t{u|8ADZaOS%y_JqZ(_tPT`a?q#;L23EGsh$$}{VDA>HP_ z52C;%cG523kIk;iqRxQ+aFU|X*!AAPfa!2dh5?wuIQaS9S!A5!pw}jV#?JHovjA_o zGeFb(v4<@o8I53+4C>m(B!Dr6hh9nTHO4xNgoB%#q#Mj%vCr@UtAwXB*hi!(_*$hE zW^x7bg#86TFEarEy2!O&NSZwL@S;t4*mYkL2&RImZZG{w_+CqWj6be~Ch{06krbaw z=m9(tdk^#O4n6lN=-v- zFnN|Lmr>u@*{SIrGBC5KrU_zZ*+*AS<)m3xN+24YSP??-ysyM8^-BvP{EPHXJHe+* z?7vjGhvV?t^OJ$Ar2_DJ#=Ya~5_-Us*8ojS&}rb9UDNm>8>kwD6*yn*OXAeCqutTE zJSYAe_A(;e9Lq5-F^c;7+1C?FNIyUJj%lN-I40D-3V zFW@srLtPgc6Stet-}jNG^h(xbNo>dN!N%B1NbWM$L&k7;<~;&_TQ71%=GLPofve|Z zI>uG$(q6MrFDwR50VYno9qk5UCSPZA3$ABJSYE~pV}qAu#+lGsJGIgK;fy%5CjR;v z%Fqn1pZ(V`$%pnrevu5@)L~?7sb5=0-LHPA5Dwc9D?@4hsunw53FraxLC+1jO(mz> zr4?;vf#;6b^|K8eW`lR?i3Sa?E(5Zb9qvf5`M23nWvh&BsaXfphG|ZuDDDP7vS1^+ z0%@5~czJ>BzD@@*l2Du|&9FdW-o&%Y&LUu+{nY$idnR`WcEe(T){893t;#DM<$E`6 zs8DX``#yxv_PqZtE$=@m-w#g?Zmw)i9`rpqIH%p-jFY)2+War^Q}=;RvN+;ik?**k zFr2&o0vo3&u9_^K*SlwCY%e7TJR2owD&rUg+ztZE9P<5mc%ZTAi5@cNdz8pCwOf3X zId+tPr#PN|{^r|Ar|7IXBwzky>XLH?F%epNH>Z9m8%vMn+0%2@w*X$Bm1vR;)zt2* z&EBu+tJquS8OEQm-2m7eCe zPrs~tVybUTKWJPPWeKLVV6;!|d2FkuE|X7Uz&{PI>RVQNP-g~`!{=YD1P{#pYlIdp zS@k*~?1-Vx_54jKn=0qv5SMM~`7y!v3(3L2tC!iAnE#$k#J`z}CuYw9(L&J-9Muz5EoLyN>O}F38{!m(&7+HY7zmTS$v$zMf z@RUQ~F*m{spwDnA_qSsBK8t99YTZG>#hMwIB`~Si3Rq~MqvNvo799AK!oTV{tNT_kw*&p36*VpBVR;E?Ua8*@{I|vwB1QVMqu^^b-FTEE zeh0rr2XiS3Co5)L;{p}@CF{>Z<*61Ta&q$E_xWw_@8*tAM!$c?w)0)ejxYbkAQqMK z$VT#u`;0g#-sna(v&bPp2o;tuQ8`&&Toe#A8H^a<-whCojTRat;zX;`8os#6B4)EXLQPb31;pF0CQYe{3oURbR7ZU5-A$`I7 zedWq#CW~2WCfdtlAcJ_YBv-4NQc5As zyL7U+-G5*hV`?hlcZ`##zOwS1ArJ^$_y|HJht3FX=ZX74hrL6);o!zAW~9r7@-1aB zi<~$>qHRluDEOAF~#^2qEO=*7itH`Q~;2x%8J5}<4gP<P8 znOT)}+)sr`W*PyCOTJJU00$iUK5LJ=yybe#+Wm_+IW|cgO0*}wn&kb|HvKz;4un~3I zEdp;{QUD^XGq0wr*uV?eh)&Irng#nfO$7-?8dP2-7yA9UR_T$r0WTw(=bGq; zRkCQkQqkB9wL$R5^Us$$#C>L{n$T$O9#w!e?M4gjGz0I`>=b%U146}2>QL>nRa@pLtPp>jgQ#8ledog3#Z$@9D$oWWw;4?-SzZfh@hReZL#!kycBzQaQ+&Z|)C1j)^U6 zx)==6$my87-BXKEAC!!YjK<>fGSP;X?vv@;hp_&?Jl9M1IM>VBbx(gM*RE+RJ=235 z!!pFV*0QpH?Pq6a;n)mXvmq5i!-e9}_(3zS`rHD1l(n zUqZM2p0+%E>SKLmUd%D4{Cj$0wicnSJa$*PTQu#4jr?o4E#hxog5Z>Mz0)2JtGn=Wo9YHD00$L(I1iF<-F=9 zjYSxAGu7hUx1E@~ZJexhp~643rqTZdDe|1G;l3YgjD*2d+4dUf$RFTjX|;Ezu9(u^ zqMeV4n?~JuyYI)86qnrguE6U4kXg)sJ-EwgwSx8_Ef<>)ls8v~tp76h$alR_6d_SG z;?uR=;i~`SII?fKwz&A!oly5@CVm`UnYND!=^@DWhl^d5rjx5I&3u9<2hrBa0^vq! zXQwY63llD~!_r1#a^mZ&Hy&zu^t-=ssWlX`gPoo4cU)w5t4+EDsY%23Uq_*3=Iqvb zgc}9zH3@10(lq>7hBSetKwN+3u$iJ|ly%e=dvbgD5o_(x&XNwixyy^UgmLXWV4Wf# zo$p9P9V6063-{XuW@gyR6?JlwJPkk6z}psaybdhT05VNxlpJIHFt|c~veG7hC)+J% z`jMUEuwbz$flRd24nH%oDwaY^CW@Jrc3|XBTAIr!D>PnLfb$6Zg8Sxi3-tHa7UJAb zOlszc=NsIgZH>9vdfiolpsb0op>G1cjh)une+emN5WgWl+SEhQ(9&w#;tuPoj9=h4 ze-#^jeSI}@b9<GB3gO6A(kj2)yL+8jBI~4w|`R6C(uPc(Q@P zEbf(-i!a?h;}gGsLwS((oL>`%smNqVQ3#b)S8tt=#MynLl^#mcvLM=yAPJxe7W zUl$_c>B{lL(FL&CiQvtCAuuKN2X#JGYLAo%vSsPg8}kV9H`lf-H~#|`)D+A0^9KxD zKTJOvavoZN^^WaU^VOGkg`BT~hGHc9y@~S>vRn>7YoB(14D>=VsQi?M4ci@bBSqk- z^WCHRybpib^czuYh~j6_rLrDClKo=~bdJLh6rwPEF>vf)Vl@9@4s+}eJ;QXI<+04d z&n=QyJcP!C8%@n!cLB63IZxL_ee#of>9B2^iM+gtj@RU@gJo&dJk-zZ)nhDEg_Ww3K~De-g+g`NYM=w{ogG7{`Tk)wb%A zrN3j296asc(2}TU78Kc^@*VSUL}PQ0Z#es{G{@}S?BBdy!$Euc3b_u5EHq8+wKx{7 z8}7bcO=?p3UVfIEtMwKv)w*D&pTgbIBl7XKuWddau-*l3`UKM9x96`;c-S4$F|$k^5Fn(>N=29vghNv7fA}@E=+rS*4@ZVuz=$t}d83c8GQJ z(l*H`k%q`3H+c=cZOL^7`3YH`6{Zr1l3watCII73VUd&{?7ifcvt0$#R{{%ujXl3_gX?8U#YL2J$6mp=J);- zo;h#9DHawk{_DH4Ykjo87Y7vUBZ^kGA-mT-Ijj~R$GaUI;SPTw1REOLZvR2*T^A{T zm8S{q3_QE#r76%+Mw($J`@9T^56P?C0?2rG8V*YRMgXQ(acS8)?yhMFuH(LJs_Se~t!4$tO&@-9B zg|)8DV+X5}=qL~LPA&YthlVHUeUCw}S>rqDp1oN(Z@>+!(EajIaeSB)wyP&^WR=Oq zKQJQxx`Fm#^#O2e%_>)u=OOIo`}(CzPe}<(*}vcYCtI6nfdy`iEbVw#D$;j-v|kXu zSnCj+_~}N(ZVyN1*=m>H>H1nWzifO|HHXaok|$dx9o!4pvHtB=*}*bN!SGJF#T-r& z$7Iq!>wD|+?umu{2!PA%sWN(**D;71@=EPk>#ZhrMeru|W_M!cj>+WD+ZBK4gl>GYJRe%#%b-LupF%S{T&KAI+*zF@umy&+EG!##< z3XlwNMc_p9y?Z5LE~~2Qq|K08gbVJ=q6Vu*e86aEJs1d0$jRB-8e0SYi!t)69V$W$ zDQy+rCWH64?@GN?_`TF$jp;=l0UR3`fEbXCRHU}zO_xr>ms|c*h>V8)d-(%&nQ&%p zzM-`Q74Ij>;h2Vttu^I|sPsGj)0jkjO{n3WxHC1FNE*{oO2w4&1unW-Kt!;$s0FQ! z46KA02P0KrOP^JvO3PW@-u^=0>UCu(RFJ8XZ$IpzpY`hDbYB35c7WxQSYpXAb;kca z8SErw!G?SY?U;s!hQ@Pqa3rg1Xt@8U%LK^OtplVq9OC#%wJvvbaFCX#GmgE|M5GY? zZMxu^@eHmB#`mq4No#`cA>SY^n=oRtT2)0X5USoNt%^g20r_ceYyP7*mHK&%k}sBN zB3MS=fx3%V^R6hesSIT32wDdt@t}!LuRCuIO-(};ecii`LXWGCrrS|nN0~L})1|?9 z`w4(?L9XLrw(k}eCCJZ2^@sY<@C>eb49#8H@(l6xs)`FhWM#1m~P8S*otBUCXnF?Bm049A*p z5rMie$R3{&o2FF8A3y5={1VoWsGqlKJzyEC0K-HD0XRS*roaF9NQo(0d1d*l>_Zja z$g8F=OCt^Y-TppofUHABRhuTCLi~BvdHAp*E@&An+uGWi+mXc}fh4-seylch_gMUv zhalPHlAZB?^{8M5sJy18CXd5<-X0wLu%c6*tXg0G-hHfS*s107Aq09=pQy32u_6RZ zVmaxVxvvx}HEZ4d?~6#>Jer~M1DJc)q z`U3L`Reab#GrLlW1--LPGmpQi$mHaBCLxiT(WTJEN))|>8Eb(-24HcrepeCL9m{xm z_>bT7K8WRhyze_^Dw%20opYOVg2CdDe~X-LGUb$~-)hI!nDC{Wn~qvPlk<>|M$&&1 z!&+Y;HCz<TxC|%iamXN_{Dr9a2?sDhwnK>=QYTGcaW*Vphjmg_(qYVN3Mr zYR`==_m=&h1pX1|*4Lu{^p`)2=4uH?UMe~c=G4v1%>E=TP;O9M>PO&cO(5N8{DPD) z*hxQHb&1Ufu_xX>iLqdlQ9-^zLk9^YgHnhblzwJ`64FJmezWb`?qWga1#0a*gyGB6 zhvC&b{Y(tV@f+ppmrJ6W5*iQ{ensJ=m}3k|MkUT$`2!fm5WAw~V3k^E>JwXdwaxrT z$x*Ge(1cuI4Ow9AFTc_$h82dMNyQ;=1)vI@EiOgW{Rg1sNLUO>MbE@ua;-M7(#b$K z>6&d)2S+2!y7^U8mD^1+M}%dav2Z|>5$1wU3y7k8z_Am>YnooKqG40@7^yJ&`G|i2 zz2%Qzis1c!;6Sld*6R6(BYE!6_QdvKH8oF=|=Q7emtGeKt~GOCv4RV+aKmbf{v5Ec+|p zT5z-Lq5@jw6rEPkd@{irwyzBG;qs;x{tfnTJlgbt2|;9`$4MmS15i zwkbAe@GEld+S$D}0GM!Wfe5qb#7OyPEKSRMi~+6Ojpp6jWo+jTz9OgKpB;#rzZcpZp zq=fhmtsZxvbfgw%(_7;-;5YQqp}*Grtx7`8d=2dxol*z{CsC-=$&f6<0QH(*RsEe? zG(gNg$_fGpaP`nz)ceZF#tF#)o7xtj+w02hazMjLeCD4UsH&m8on?$f}vxuS+jM?N?Mo>abX@IhoQ{It|Fy3H4*6K{7`~+oLfDEJb z^en9ObSqwVYXjvUkNM8pyGFyxi)Y@xTo`h?Lz9pdhS{2?^t7v}?A0@eLLCOEwGI68 zl&!nD^9UCwhNu%w*}WV)!rSZ9gioGjO|OMoQK$g*E9kR~$(TgVHa%3O13{*bK9FCj z`n3779zsSbS0ZxEyf?Oq8_2Hot&y-~m|V%?(|Z?F4au8Z#bX_&d&?JWUE-n&nV1O&8@cVQ;V|8Qu@hm%{|FN7)c2D zg7FSabJ$w($SIDes|=bXe<~T$(xS7ua1pfR{H@UOHyq{P9xT^9bn%MfDXl>jmhxqQ zKau!}&y3ZnBIMU~1k3cjA2h6bho4<7^7uN#PWPA7lln0~kGAS*oX6aSg<~-*7L=bD zdhN@85e1Le;+6d(iNySqLId=GIx|QQ!c3A>GXMG$g=8}fAn7Q=sRR<=dP^(h*kdmH zB@FFW+lA5@&xS1b#Aly@mVIIX)cx^O_wxOnvVx5`q9H`1aPl$Ip%A+e=0s#My|;gYi+8j<-_PS%)SHBAQY1(5|b3^nXKa5#eqR(ZI&`{^EyDmwzbZ_ z4SSpqc!fQvl;7*;w_Ex|wTq=jSu5#gS`o;m_H>|m`NTZOm)^n5ioJxtQ6^Dd(svq+ zuc;G_zasEOy@f!j^y6>YUf`GH(N=-M(^$;3W}0X>(hK-?)7}D_i8C=ZUbCX{BhzH1 zpoX@hvdgL9vRl09imdp2his1y{yP9(BpZW**pcm5PO4`@8Kuu~n?cCq!prK=vfGa3 zkIx^;vy#fc-8s$-K$z+dByCqv>EVF*;e6y6M>|wjNoT;;iT&7X|?Tvh?dy8 zV{JJ9Le+MhFuAdpmAth>QKo{mtgUSl6PEFBt;fUW#4@-zMIn})XU7|8JcK-Ol)j9L zNtsI3qv**m{8(+7%d8!Kp#g<3kUES9LQTvgsWYs}xG18WTu+6=1=)<#w)L(p3?r>u zzz-~QK@fV|g5ck6!EGs@IMK-MqB0p_q&9e-#3m!_GM{HUwKVO92cMFQ7m~J0B0{zSUuh_cv_wKnOKQWh} z^kQv{Puj2Ol}ub>-rh=RTv|wEpva8h`dQ0ymICh7)?`@v~|T46vGcQ)SC`C3@@z#f!Wp!4uUdEkp-YhcjvPM~d)sZptL=IBZ5OzEo&7#C?G{st+dL|oEkZ(^46c!?hB-{%ModZ(khAA;18T7lk6}XH^yY~R{SM5!8O0U(6ixxW&WR* z4uqveYW}F>XB%m7TRuf0)~DDb@@l-zTBR9f4yz`Me|qs~kZd7sXh`GEWCoQ?W8XS< zS~;LV5=$xH%DkZ~CSBD&p7GTsOqI=jAZ!>)5Y1EU#bR5tBLIlo5^mhOxkly%_-jwr z%}|f9P-T~^Z)I#&6Wo_ie~ZqZI?8;?;M6rFi^gx`%YL0kVm9L_Kb=L={2(J(Qm_57 z6@I65)QQd0he90zIMFB|t|0sk_=_fr!v2A(o@!X|^AtH*xqb|VnqVmy7+L}Swx9#w z9lE=rFP`J5Ysyz~`_jTwS_GpA%B>sH=)Y~^r5>2tY{4yCesM|;{pue7wuQXjp#IQ) zW9uX5GJx8`OSS2uJCEal_1G<;TWM*FTPE;e=PVF1zS44Z<`%CXZ6k=ZHG>9NVR?4X zvGJ0tnR|`syr{JAx^Ht{YslUG+&0?zG0Vqss~=*O6E-g<+wQ$VxS-_ak6@$M3S<8B z9ezp5%D1DYDS2%9o*dfE(4Nu3UyaCxM)3zCyWGC~3k&D|2#altwq;Bs z!pbSEQ@KBw7n8a*w^le^i=5sAps;vOELZ!#ga0ys(p6l`)y&w{oX_-^IXHn}V`1fH zWC6d})L1$BINA7Eco`_ * `Installer for Windows `_ +.. _build_source: + Build from Source ================= diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst new file mode 100644 index 0000000000..3050953558 --- /dev/null +++ b/tutorials/tut0.rst @@ -0,0 +1,214 @@ +Tutorial 0 - Get it Running +=========================== + +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 `_ 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 +-------------------------- + +You'll need a modern copy of the database running on your system, either natively or in a Docker instance. We require PostgreSQL 9.3 or greater, but recommend at least 9.5 for row-level security features that we'll use in future tutorials. + +If you're already familiar with using PostgreSQL and have it installed on your system you can use the existing installation. 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 `_. Next, let's pull and start the database image: + +.. code-block:: bash + + sudo docker run --name tutorial -p 5432:5432 \ + -e POSTGRES_PASSWORD=mysecretpassword \ + -d postgres + +This will run the Docker instance as a daemon and expose port 5432 to the host system so that it looks like an ordinary PostgreSQL server to the rest of the system. + +Step 3. Install PostgREST +------------------------- + +PostgREST is distributed as a single binary, with versions compiled for major distributions of Linux/BSD/Windows. Visit the `latest release `_ 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/begriffs/postgrest/releases/latest + + tar xfJ postgrest--.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 + +If everything is working correctly it will print out its version and information about configuration. 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 + +

+

+ Ubuntu or Debian +
+
sudo apt-get install libpq-dev
+
+
+
+ Fedora, CentOS, or Red Hat +
+
sudo yum install postgresql-libs
+
+
+
+ OS X +
+
brew install postgresql
+
+
+
+ Windows +

It isn't fun. Learn more here.

+

It might be easier to execute PostgREST in its own Docker image as well.

+
+

+ +Step 4. Create Database for API +------------------------------- + +Connect to to 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 `_ 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 web_anon to postgres; + + 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. + +Now quit out of psql; it's time to start the API! + +.. code-block:: psql + + \q + +Step 5. Run PostgREST +--------------------- + +PostgREST uses 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://postgres:mysecretpassword@localhost/postgres" + db-schema = "api" + db-anon-role = "web_anon" + +The configuration file has other :ref:`options `, but this is all we need. Now run the server: + +.. code-block:: bash + + ./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 relation 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. From 71da89fdcc057fe6a62591419c3b27b789e2df49 Mon Sep 17 00:00:00 2001 From: Richard Fox Date: Wed, 5 Jul 2017 00:48:54 +0200 Subject: [PATCH 102/549] intro.rst: Correct first example app link (#87) --- intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intro.rst b/intro.rst index 2f3b0e0df7..dbafcfab77 100644 --- a/intro.rst +++ b/intro.rst @@ -66,7 +66,7 @@ These are PostgreSQL bridges that propagate LISTEN/NOTIFY to external queues for Example Apps ------------ -* `https://github.com/subzerocloud/postgrest-starter-kit `_ - Boilerplate for new project +* `subzerocloud/postgrest-starter-kit `_ - Boilerplate for new project * `NikolayS/postgrest-google-translate `_ - Calling to external translation service * `CodeforAustralia/heritage-near-me `_ - Elm and PostgREST with PostGIS * `timwis/handsontable-postgrest `_ - An excel-like database table editor From cb4c54074a6915e33207c4fa6daca1554949912b Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Tue, 4 Jul 2017 22:25:16 -0500 Subject: [PATCH 103/549] Tutorial 1 --- _static/tuts/tut1-jwt-io.png | Bin 0 -> 60572 bytes index.rst | 1 + tutorials/tut0.rst | 2 + tutorials/tut1.rst | 245 +++++++++++++++++++++++++++++++++++ 4 files changed, 248 insertions(+) create mode 100644 _static/tuts/tut1-jwt-io.png create mode 100644 tutorials/tut1.rst diff --git a/_static/tuts/tut1-jwt-io.png b/_static/tuts/tut1-jwt-io.png new file mode 100644 index 0000000000000000000000000000000000000000..488b87f1034660f5325c69c1d657427416c50b49 GIT binary patch literal 60572 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 provides an easy way to generate a secure password. If you have it installed, run + + .. code-block:: bash + + openssl rand -base64 32 + +Open the :code:`postgrest.conf` (created in the previous tutorial) and add a line with the password: + +.. code-block:: ini + + # add this line to postgrest.conf + + jwt-secret = "" + +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 `_ 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 password you generated rather than the word :code:`secret`. After you have filled in the password 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="" + + 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 4. Add Expiration +---------------------- + +Currently our authentication token is valid for all eternity. As long as the server continues using the same password to verify the JWT signature it will honor this token and use the :code:`todo_user` role to perform requests including it. + +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": "" + } + +Copy the updated token as before, and save it as a new environment variable. + +.. code-block:: bash + + export 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 + + {"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="" + +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.claim.email', true) = + '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 postgrest.conf + + 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 \ + -H "Authorization: Bearer $TOKEN" + + # this one is rejected + + curl http://localhost:3000/todos \ + -H "Authorization: Bearer $WAYWARD_TOKEN" + +The server responds with 403 Forbidden: + +.. code-block:: json + + { + "hint": "Nope, we are on to you", + "details": null, + "code": "42501", + "message": "insufficient_privilege" + } From aa6e3afc0e49dcf87aa538a23dcbca8aaf6aa4ef Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Tue, 4 Jul 2017 22:46:03 -0500 Subject: [PATCH 104/549] Small edit and correct config file name --- tutorials/tut1.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tutorials/tut1.rst b/tutorials/tut1.rst index 91c8976e16..6ea26786cf 100644 --- a/tutorials/tut1.rst +++ b/tutorials/tut1.rst @@ -37,11 +37,11 @@ Let's create a password and provide it to PostgREST. Think of a nice long one, o openssl rand -base64 32 -Open the :code:`postgrest.conf` (created in the previous tutorial) and add a line with the password: +Open the :code:`tutorial.conf` (created in the previous tutorial) and add a line with the password: .. code-block:: ini - # add this line to postgrest.conf + # add this line to tutorial.conf jwt-secret = "" @@ -118,7 +118,7 @@ A request for the todos shows three of them, and all completed. Step 4. Add Expiration ---------------------- -Currently our authentication token is valid for all eternity. As long as the server continues using the same password to verify the JWT signature it will honor this token and use the :code:`todo_user` role to perform requests including it. +Currently our authentication token is valid for all eternity. The server, as long as it continues using the same JWT password, 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. @@ -215,7 +215,7 @@ Next update :code:`tutorial.conf` and specify the new function: .. code-block:: ini - # add this line to postgrest.conf + # add this line to tutorial.conf pre-request = "auth.check_token" From 49a62772c077b61aa8f804cc4c24cde67ac70319 Mon Sep 17 00:00:00 2001 From: Chris Stryczynski Date: Sat, 8 Jul 2017 06:14:30 +0100 Subject: [PATCH 105/549] Single quotes give an error (#88) of postgrest: ParseError "postgrest.config" "endOfInput" --- admin.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/admin.rst b/admin.rst index ae7b1ab209..2e3e7ba23a 100644 --- a/admin.rst +++ b/admin.rst @@ -239,19 +239,19 @@ Once you've verified that requests are as you expect, you can get more informati .. code:: sql # send logs where the collector can access them - log_destination = 'stderr' + 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' + log_directory = "pg_log" # (optional) new log file per day - log_filename = 'postgresql-%Y-%m-%d.log' + log_filename = "postgresql-%Y-%m-%d.log" # log every kind of SQL statement - log_statement = 'all' + 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. From cdf5dece651a057f91134a2584b7020db2cb9c12 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Tue, 18 Jul 2017 22:32:13 -0500 Subject: [PATCH 106/549] Include link to next tutorial --- tutorials/tut0.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index a762bc7d1c..6348f8e187 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -214,3 +214,5 @@ Response is 401 Unauthorized: } 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` From 4be05649f4ca45954ca28f04d8c96b8d4be5455c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20Ch=C3=A1vez?= Date: Thu, 20 Jul 2017 00:06:57 -0500 Subject: [PATCH 107/549] Add documentation for binary output in rpc, Fix #84 (#92) --- api.rst | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/api.rst b/api.rst index cd10a588e1..b785dc59f4 100644 --- a/api.rst +++ b/api.rst @@ -278,6 +278,29 @@ and select a single column :code:`?select=bin_data`. GET /items?select=bin_data&id=eq.1 HTTP/1.1 Accept: application/octet-stream +You can also request binary output when calling stored procedures and since they can return a scalar value you are not forced to use :code:`select` +for this case. + +.. code:: sql + + CREATE FUNCTION closest_point(..) RETURNS bytea .. + +.. code:: http + + POST /rpc/closest_point HTTP/1.1 + Accept: application/octet-stream + +If the stored procedure returns non-scalar values, you need to do a :code:`select` in the same way as for GET binary output. + +.. code:: sql + + CREATE FUNCTION overlapping_regions(..) RETURNS SETOF TABLE(geom_twkb bytea, ..) .. + +.. code:: http + + POST /rpc/overlapping_regions?select=geom_twkb HTTP/1.1 + Accept: application/octet-stream + .. note:: If more than one row would be returned the binary results will be concatenated with no delimiter. From 84bfa2d9ca04b29b3019aa47b09348928f8881ba Mon Sep 17 00:00:00 2001 From: Priyank Purohit Date: Sat, 5 Aug 2017 21:42:31 -0400 Subject: [PATCH 108/549] Updated docs for complex logic API capability (#94) --- api.rst | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/api.rst b/api.rst index b785dc59f4..6033ba46c3 100644 --- a/api.rst +++ b/api.rst @@ -27,12 +27,24 @@ You can filter result rows by adding conditions on columns, each condition a que GET /people?age=lt.13 HTTP/1.1 -Adding multiple parameters conjoins the conditions: +Multiple parameters can be logically conjoined by: .. code-block:: http GET /people?age=gte.18&student=is.true HTTP/1.1 +Multiple parameters can be logically disjoined by: + +.. code-block:: http + + GET /people?or=(age.gte.14,age.lte.18) HTTP/1.1 + +Complex logic can also be applied: + +.. code-block:: http + + GET /people?and=(grade.gte.90,student.is.true,or(age.gte.14,age.is.null)) HTTP/1.1 + These operators are available: ============ ============================================= @@ -55,9 +67,9 @@ not negates another operator, see below ============ ============================================= -To negate any operator, prefix it with :code:`not` like :code:`?a=not.eq.2`. +To negate any operator, prefix it with :code:`not` like :code:`?a=not.eq.2` or :code:`?not.and=(a.gte.0,a.lte.100)` . -For more complicated filters (such as those involving disjunctions) 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: +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 @@ -74,10 +86,6 @@ The view will provide a new endpoint: GET /fresh_stories HTTP/1.1 -.. note:: - - We're working to extend the PostgREST query grammar to allow more complicated boolean logic, while continuing to prevent performance problems from arbitrary client queries. - .. _v_filter: Vertical Filtering (Columns) @@ -395,7 +403,7 @@ 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 and OR-conditions in the where clause +* Table unions * More complicated joins than those provided by `Resource Embedding`_ * Geospatial queries that require an argument, like "points near (lat,lon)" * More sophisticated full-text search than a simple use of the :sql:`@@` filter @@ -467,25 +475,6 @@ Stored procedures can access request headers and cookies by reading GUC variable SELECT current_setting('request.header.origin', true); -Complex boolean logic ---------------------- - -For complex boolean logic you can use stored procedures, an example: - -.. code-block:: postgresql - - CREATE FUNCTION key_customers(country TEXT, company TEXT, salary FLOAT) RETURNS SETOF customers AS $$ - SELECT * FROM customers WHERE (country = $1 AND company = $2) OR salary = $3; - $$ LANGUAGE SQL; - -Then you can query by doing: - -.. code-block:: http - - POST /rpc/key_customers HTTP/1.1 - - { "country": "Germany", "company": "Volkswagen", salary": 120000.00 } - Raising Errors -------------- From aedaca129045015e0519544fa3e2d9204018eb1c Mon Sep 17 00:00:00 2001 From: Russell Davies Date: Sun, 13 Aug 2017 17:39:18 +0100 Subject: [PATCH 109/549] Clarify resource embedding constraints (#97) --- api.rst | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 6033ba46c3..6c59acab27 100644 --- a/api.rst +++ b/api.rst @@ -342,7 +342,7 @@ However because a foreign key constraint exists between Films and Directors, we .. code-block:: http - GET /films?select=title,directors(last_name) HTTP/1.1 + GET /films?select=title,directors(id,last_name) HTTP/1.1 Which would return @@ -351,21 +351,36 @@ Which would return [ { "title": "Workers Leaving The Lumière Factory In Lyon", "directors": { + "id": 2, "last_name": "Lumière" } }, { "title": "The Dickson Experimental Sound Film", "directors": { + "id": 1, "last_name": "Dickson" } }, { "title": "The Haunted Castle", "directors": { + "id": 3, "last_name": "Méliès" } } ] +The primary key of the table of the resource being embedded must be specified, +either explicitly, like in the example above, or implicitly through a wildcard. + +In this example, since the relationship is a forward relationship, there is +only one director associated with a film. As the table name is plural it might +be preferable for it to be singular instead. An table name alias can accomplish +this: + +.. code-block:: http + + GET /films?select=title,director:directors(id,last_name) HTTP/1.1 + .. note:: As of PostgREST v4.1, parens :code:`()` are used rather than brackets :code:`{}` for the list of embedded columns. Brackets are still supported, but are deprecated and will be removed in v5. @@ -376,6 +391,9 @@ PostgREST can also detect relations going through join tables. Thus you can requ GET /directors?select=films(title,year) HTTP/1.1 +Here it is not necessary to specify the table's primary key of the embedded +resource. + .. note:: Whenever foreign key relations change in the database schema you must refresh PostgREST's schema cache to allow resource embedding to work properly. See the section :ref:`schema_reloading`. From 66ce8e8b183292b4ea344da943da9beb34b77836 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 20 Aug 2017 14:15:59 -0500 Subject: [PATCH 110/549] Exp claim must be an int, not a string Pointed out by @nileshtrivedi --- tutorials/tut1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/tut1.rst b/tutorials/tut1.rst index 6ea26786cf..983ac150f9 100644 --- a/tutorials/tut1.rst +++ b/tutorials/tut1.rst @@ -146,7 +146,7 @@ Go back to jwt.io and change the payload to { "role": "todo_user", - "exp": "" + "exp": } Copy the updated token as before, and save it as a new environment variable. From 3fdf6acc45ed9eb3c44eebc3dba3a1d46aa5d43c Mon Sep 17 00:00:00 2001 From: Russell Davies Date: Sun, 20 Aug 2017 20:49:27 +0100 Subject: [PATCH 111/549] Range operators and deprecation of symbol operators (#98) --- api.rst | 58 ++++++++++++++++++++++++++++++++++++--------------------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/api.rst b/api.rst index 6c59acab27..5b45ab3c80 100644 --- a/api.rst +++ b/api.rst @@ -47,25 +47,41 @@ Complex logic can also be applied: These operators are available: -============ ============================================= -abbreviation meaning -============ ============================================= -eq equals -gte greater than or equal -gt greater than -lte less than or equal -lt less than -neq not equal -like LIKE operator (use * in place of %) -ilike ILIKE operator (use * in place of %) -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 checking for exact equality (null,true,false) -@@ full-text search using to_tsquery -@> contains e.g. :code:`?tags=@>.{example, new}` -<@ contained in e.g. :code:`?values=<@{1,2,3}` -not negates another operator, see below -============ ============================================= +============ =============================================== =================== +Abbreviation Meaning Postgres Equivalent +============ =============================================== =================== +eq equals :code:`=` +gt greater than :code:`>` +gte greater than or equal :code:`>=` +lt less than :code:`<` +lte less than or equal :code:`<=` +neq not equal :code:`<>` or :code:`!=` +like LIKE operator (use * in place of %) :code:`LIKE` +ilike ILIKE operator (use * in place of %) :code:`ILIKE` +in one of a list of values e.g. :code:`IN` + :code:`?a=in.1,2,3` – also supports commas + in quoted strings like + :code:`?a=in."hi,there","yes,you"` +is checking for exact equality (null,true,false) :code:`IS` +fts full-text search using to_tsquery :code:`@@` +cs contains e.g. :code:`?tags=cs.{example, new}` :code:`@>` +cd contained in e.g. :code:`?values=cd.{1,2,3}` :code:`<@` +ov overlap (have points in common), :code:`&&` + e.g. :code:`?period=ov.[2017-01-01,2017-06-30]` +sl strictly left of, e.g. :code:`?range=sl.(1,10)` :code:`<<` +sr strictly right of :code:`>>` +nxr does not extend to the right of, :code:`&<` + e.g. :code:`?range=nxr.(1,10)` +nxl does not extend to the left of :code:`&>` +adj is adjacent to, e.g. :code:`?range=adj.(1,10)` :code:`-|-` +not negates another operator, see below :code:`NOT` +============ =============================================== =================== +.. note:: + + As of PostgREST v0.4.3.0, the symbol operators :code:`@@, @>, <@` have been + deprecated in lieu of their mnemonic equivalents. They are still supported + but will be removed in v0.5.0.0. To negate any operator, prefix it with :code:`not` like :code:`?a=not.eq.2` or :code:`?not.and=(a.gte.0,a.lte.100)` . @@ -125,7 +141,7 @@ A full-text search on the computed column: .. code-block:: http - GET /people?full_name=@@.Beckett HTTP/1.1 + GET /people?full_name=fts.Beckett HTTP/1.1 As mentioned, computed columns do not appear in the output by default. However you can include them by listing them in the vertical filtering :code:`select` param: @@ -383,7 +399,7 @@ this: .. note:: - As of PostgREST v4.1, parens :code:`()` are used rather than brackets :code:`{}` for the list of embedded columns. Brackets are still supported, but are deprecated and will be removed in v5. + As of PostgREST v0.4.1.0, parens :code:`()` are used rather than brackets :code:`{}` for the list of embedded columns. Brackets are still supported, but are deprecated and will be removed in v0.5.0.0. PostgREST can also detect relations going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). You can also reverse the direction of inclusion, asking for all Directors with each including the list of their Films: @@ -424,7 +440,7 @@ The PostgREST URL grammar limits the kinds of queries clients can perform. It pr * Table unions * More complicated joins than those provided by `Resource Embedding`_ * Geospatial queries that require an argument, like "points near (lat,lon)" -* More sophisticated full-text search than a simple use of the :sql:`@@` filter +* More sophisticated full-text search than a simple use of the :sql:`fts` filter Stored Procedures ================= From 4b1e594270670bb2ebc292b05a33a7b0c0592fe8 Mon Sep 17 00:00:00 2001 From: Will O'Brien Date: Tue, 5 Sep 2017 23:25:43 -0400 Subject: [PATCH 112/549] Add notice about reverting OIDC default (#99) --- auth.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/auth.rst b/auth.rst index d76ddcfe87..52e490db31 100644 --- a/auth.rst +++ b/auth.rst @@ -204,6 +204,16 @@ An external service like `Auth0 `_ can do the hard work tran To use Auth0, copy its client secret into your PostgREST configuration file as the :code:`jwt-secret`. (Old-style Auth0 secrets are Base64 encoded. For these secrets set :code:`secret-is-base64` to :code:`true`, or just refresh the Auth0 secret.) You can find the secret in the client settings of the Auth0 management console. +.. note:: + + Make sure OIDC-conformant is toggled off. + + A recent Auth0 change sets it on by default. Turn it `off` here: + + Clients > `Your App` > Settings > Show Advanced Settings > OAuth > OIDC Conformant + + Ensure also that your client application does not pass in any `audience` configuration. + Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata `_. Then, you will need to write a rule that will extract the role from the user metadata and include a :code:`role` claim in the payload of our user object. Afterwards, in your Auth0Lock code, include the :code:`role` claim in your `scope param `_. .. code:: javascript From 973e7cb90ee4014778a15396b5b210d8804a7b55 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 9 Sep 2017 15:40:49 -0500 Subject: [PATCH 113/549] How to use docker --- install.rst | 90 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 19 deletions(-) diff --git a/install.rst b/install.rst index 1004963141..ebd7beb028 100644 --- a/install.rst +++ b/install.rst @@ -16,38 +16,90 @@ The release page has precompiled binaries for Mac OS X, Windows, and several Lin # You should see a usage help message -Homebrew -======== +PostgreSQL dependency +===================== -You can use the Homebrew package manager to install PostgREST on Mac +To use PostgREST you will need an underlying database (PostgreSQL version 9.3 or greater is required). You can use something like Amazon `RDS `_ but installing your own locally is cheaper and more convenient for development. -.. code-block:: bash +* `Instructions for OS X `_ +* `Instructions for Ubuntu 14.04 `_ +* `Installer for Windows `_ - # Ensure brew is up to date - brew update +Docker +====== - # Check for any problems with brew's setup - brew doctor +The official PostgREST Docker image consults an internal :code:`/etc/postgrest.conf` file. To customize this file you can either mount a replacement configuration file into the container, or use environment variables. The environment variables will be interpolated into the default config file. - # Install the postgrest package - brew install postgrest +These variables match the options shown in our :ref:`configuration` section, except they are capitalized, have a prefix, and use underscores. To get a list of the available environment variables, run this: + +.. code-block:: bash -This will automatically install PostgreSQL as a dependency. The process tends to take up to 15 minutes to install the package and its dependencies. + docker inspect -f "{{.Config.Env}}" postgrest/postgrest -After installation completes, the tool is added to your $PATH and can be used from anywhere with: +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 - postgrest --help + # Pull the official image + docker pull postgrest/postgrest -PostgreSQL dependency -===================== + # Run the server + docker run --rm --net=host -p 3000:3000 \ + -e PGRST_DB_URI="postgres://postgres@localhost/postgres" \ + -e PGRST_DB_ANON_ROLE="postgres" \ + postgrest/postgrest -To use PostgREST you will need an underlying database (PostgreSQL version 9.3 or greater is required). You can use something like Amazon `RDS `_ but installing your own locally is cheaper and more convenient for development. +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. -* `Instructions for OS X `_ -* `Instructions for Ubuntu 14.04 `_ -* `Installer for Windows `_ +.. 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' + +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 + + server: + image: postgrest/postgrest + ports: + - "3000:3000" + links: + - db:db + environment: + PGRST_DB_URI: postgres://app_user:password@db:5432/app_db + PGRST_DB_SCHEMA: public + PGRST_DB_ANON_ROLE: app_user + + db: + image: postgres + ports: + - "5432:5432" + environment: + POSTGRES_DB: app_db + POSTGRES_USER: app_user + POSTGRES_PASSWORD: password + +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. .. _build_source: From 5fc48057f8c4d01f520bb4009051f8aa267cda15 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 9 Sep 2017 15:41:05 -0500 Subject: [PATCH 114/549] Actually pg 9.5 is needed, not 9.3 --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index ebd7beb028..b3a4ca428a 100644 --- a/install.rst +++ b/install.rst @@ -19,7 +19,7 @@ The release page has precompiled binaries for Mac OS X, Windows, and several Lin PostgreSQL dependency ===================== -To use PostgREST you will need an underlying database (PostgreSQL version 9.3 or greater is required). You can use something like Amazon `RDS `_ but installing your own locally is cheaper and more convenient for development. +To use PostgREST you will need an underlying database (PostgreSQL version 9.5 or greater is required). You can use something like Amazon `RDS `_ but installing your own locally is cheaper and more convenient for development. * `Instructions for OS X `_ * `Instructions for Ubuntu 14.04 `_ From e4741d43e488e5c8d96750671f7c55d8f6b4dc59 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 9 Sep 2017 19:27:51 -0500 Subject: [PATCH 115/549] Note for windows users --- _static/win-err-dialog.png | Bin 0 -> 27093 bytes install.rst | 9 +++++++++ 2 files changed, 9 insertions(+) create mode 100644 _static/win-err-dialog.png diff --git a/_static/win-err-dialog.png b/_static/win-err-dialog.png new file mode 100644 index 0000000000000000000000000000000000000000..e60a71c4507e157076b36b550c7e83f0242249f2 GIT binary patch literal 27093 zcmV)^K!CrAP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 a=;{>o|2A;5en5jW0BES_q8gQKBmM_-;3@h5 literal 0 HcmV?d00001 diff --git a/install.rst b/install.rst index b3a4ca428a..7594ebc301 100644 --- a/install.rst +++ b/install.rst @@ -16,6 +16,15 @@ The release page has precompiled binaries for Mac OS X, Windows, and several Lin # You should see a usage help message +.. 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. + + PostgreSQL dependency ===================== From 0da8cb230fd5b62dcc26334ace3251c7e7decb62 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 9 Sep 2017 19:49:42 -0500 Subject: [PATCH 116/549] Proper error code for failed content negotiation Fixes #95 --- api.rst | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 5b45ab3c80..9b800d016d 100644 --- a/api.rst +++ b/api.rst @@ -283,11 +283,18 @@ This returns { "id": 1 } -When a singular response is requested but no entries are found, the server responds with an empty body and 404 status code rather than the usual empty array and 200 status. +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" + } .. 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 singlular 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. + 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. From 5185a9279824122883d21775bb157ae81a668043 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 9 Sep 2017 20:21:46 -0500 Subject: [PATCH 117/549] Move config section to install page Document special host binding addresses Fixes #83 --- admin.rst | 102 ------------------------------------------------ conf.py | 4 +- install.rst | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+), 104 deletions(-) diff --git a/admin.rst b/admin.rst index 2e3e7ba23a..f4b291bd1b 100644 --- a/admin.rst +++ b/admin.rst @@ -1,105 +1,3 @@ -.. _configuration: - -Configuration -============= - -The PostgREST server reads a configuration file to determine information about the database and how to serve client requests. There is no predefined location for this file, you must specify the file path as the one and only argument to the server: - -.. code:: bash - - postgrest /path/to/postgrest.conf - -The file must contain a set of key value pairs. At minimum you must include these keys: - -.. code:: - - # postgrest.conf - - # The standard connection URI format, documented at - # https://www.postgresql.org/docs/current/static/libpq-connect.html#AEN45347 - db-uri = "postgres://user:pass@host:5432/dbname" - - # The name of which database schema to expose to REST clients - db-schema = "api" - - # The database role to use when no client authentication is provided. - # Can (and probably should) differ from user in db-uri - db-anon-role = "anon" - -The user specified in the db-uri is also known as the authenticator role. For more information about the anonymous vs authenticator roles see the :ref:`roles`. - -Here is the full list of configuration parameters. - -================ ====== ======= ======== -Name Type Default Required -================ ====== ======= ======== -db-uri String Y -db-schema String Y -db-anon-role String Y -db-pool Int 10 -server-host String \*4 -server-port Int 3000 -server-proxy-uri String -jwt-secret String -secret-is-base64 Bool False -max-rows Int ∞ -pre-request String -================ ====== ======= ======== - -db-uri - The standard connection PostgreSQL `URI format `_. Symbols and unusual characters in the password or other fields should be percent encoded to avoid a parse error. On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. Also allows connections over Unix sockets for higher performance. -db-schema - The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. -db-anon-role - The database role to use when executing commands on behalf of unauthenticated clients. -db-pool - Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the :code:`max_connections` GUC in your database. -server-host - Where to bind the PostgREST web server. -server-port - The port to bind the web server. -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.0.0", - "title": "PostgREST API", - "description": "This is a dynamic API generated by PostgREST" - }, - "host": "postgrest.com:443", - "basePath": "/", - "schemes": [ - "https" - ] - } - -jwt-secret - The secret used to decode JWT tokens clients provide for authentication. 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. -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. -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. -pre-request - A schema-qualified stored procedure name to call right after switching roles for a client request. This provides an opportunity to modify SQL variables or raise an exception to prevent the request from completing. - -Running the Server ------------------- - -PostgREST outputs basic request logging to stdout. 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 logfile or to the syslog: - -.. code-block:: bash - - ssh foo@example.com \ - 'postgrest foo.conf /var/log/postgrest.log 2>&1 &' - - # another option is to pipe the output into "logger -t postgrest" - -(Avoid :code:`nohup postgrest` because the HUP signal is used for manual :ref:`schema_reloading`.) - Hardening PostgREST =================== diff --git a/conf.py b/conf.py index 415ce36c25..394a145849 100644 --- a/conf.py +++ b/conf.py @@ -54,9 +54,9 @@ # built documents. # # The short X.Y version. -version = u'4.1' +version = u'4.3' # The full version, including alpha/beta/rc tags. -release = u'4.1.0' +release = u'4.3.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/install.rst b/install.rst index 7594ebc301..979dc3c1ee 100644 --- a/install.rst +++ b/install.rst @@ -34,6 +34,116 @@ To use PostgREST you will need an underlying database (PostgreSQL version 9.5 or * `Instructions for Ubuntu 14.04 `_ * `Installer for Windows `_ +.. _configuration: + +Configuration +============= + +The PostgREST server reads a configuration file to determine information about the database and how to serve client requests. There is no predefined location for this file, you must specify the file path as the one and only argument to the server: + +.. code:: bash + + postgrest /path/to/postgrest.conf + +The file must contain a set of key value pairs. At minimum you must include these keys: + +.. code:: + + # postgrest.conf + + # The standard connection URI format, documented at + # https://www.postgresql.org/docs/current/static/libpq-connect.html#AEN45347 + db-uri = "postgres://user:pass@host:5432/dbname" + + # The name of which database schema to expose to REST clients + db-schema = "api" + + # The database role to use when no client authentication is provided. + # Can (and probably should) differ from user in db-uri + db-anon-role = "anon" + +The user specified in the db-uri is also known as the authenticator role. For more information about the anonymous vs authenticator roles see the :ref:`roles`. + +Here is the full list of configuration parameters. + +================ ====== ======= ======== +Name Type Default Required +================ ====== ======= ======== +db-uri String Y +db-schema String Y +db-anon-role String Y +db-pool Int 10 +server-host String \*4 +server-port Int 3000 +server-proxy-uri String +jwt-secret String +secret-is-base64 Bool False +max-rows Int ∞ +pre-request String +================ ====== ======= ======== + +db-uri + The standard connection PostgreSQL `URI format `_. Symbols and unusual characters in the password or other fields should be percent encoded to avoid a parse error. On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. Also allows connections over Unix sockets for higher performance. +db-schema + The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. +db-anon-role + The database role to use when executing commands on behalf of unauthenticated clients. +db-pool + Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the :code:`max_connections` GUC in your database. +server-host + 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 + The port to bind the web server. +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" + ] + } + +jwt-secret + The secret used to decode JWT tokens clients provide for authentication. 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. +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. +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. +pre-request + A schema-qualified stored procedure name to call right after switching roles for a client request. This provides an opportunity to modify SQL variables or raise an exception to prevent the request from completing. + +Running the Server +------------------ + +PostgREST outputs basic request logging to stdout. 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 logfile or to the syslog: + +.. code-block:: bash + + ssh foo@example.com \ + 'postgrest foo.conf /var/log/postgrest.log 2>&1 &' + + # another option is to pipe the output into "logger -t postgrest" + +(Avoid :code:`nohup postgrest` because the HUP signal is used for manual :ref:`schema_reloading`.) + + Docker ====== From 45ddafc2a8372ceaf90962242854b34ded33be94 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 9 Sep 2017 20:32:59 -0500 Subject: [PATCH 118/549] How to determine server version Fixes #63 --- admin.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/admin.rst b/admin.rst index f4b291bd1b..fdb075f2cb 100644 --- a/admin.rst +++ b/admin.rst @@ -121,6 +121,14 @@ Nginx rate limiting is general and indescriminate. To rate limit each authentica Debugging ========= +Server Version +-------------- + +When debugging a problem it's important to verify the PostgREST version. At any time you can make a request to the running server and determine exactly which version is deployed. Look for the :code:`Server` HTTP response header, which contains the version number. + +HTTP Requests +------------- + The PostgREST server logs basic request information to stdout, including the requesting IP address and user agent, the URL requested, and HTTP response status. However this provides limited information for debugging server errors. It's helpful to get full information about both client requests and the corresponding SQL commands executed against the underlying database. A great way to inspect incoming HTTP requests including headers and query params is to sniff the network traffic on the port where PostgREST is running. For instance on a development server bound to port 3000 on localhost, run this: @@ -130,7 +138,10 @@ A great way to inspect incoming HTTP requests including headers and query params # sudo access is necessary for watching the network sudo ngrep -d lo0 port 3000 -The options to ngrep vary depending on the address and host on which you've bound the server. The binding is described in the `Configuration`_ section. The ngrep output isn't particularly pretty, but it's legible. Note the :code:`Server` response header as well which identifies the version of server. This is important when submitting bug reports. +The options to ngrep vary depending on the address and host on which you've bound the server. The binding is described in the :ref:`configuration` section. The ngrep output isn't particularly pretty, but it's legible. + +Database Logs +------------- Once you've verified that requests are as you expect, you can get more information about the server operations by watching 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. From a93ddbee5af8f016b4bcd7337176632aebcab1aa Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 9 Sep 2017 20:52:12 -0500 Subject: [PATCH 119/549] Unicode Fixes #62 --- api.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/api.rst b/api.rst index 9b800d016d..7ba158dbb4 100644 --- a/api.rst +++ b/api.rst @@ -336,6 +336,23 @@ If the stored procedure returns non-scalar values, you need to do a :code:`selec If more than one row would be returned the binary results will be concatenated with no delimiter. +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:: html + + http://localhost:3000/موارد + +Do this: + +.. code-block:: html + + http://localhost:3000/%D9%85%D9%88%D8%A7%D8%B1%D8%AF + .. _resource_embedding: Resource Embedding From edefd3040c95accdbebcb273216785179e770bcb Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 9 Sep 2017 21:27:36 -0500 Subject: [PATCH 120/549] Section about new FTS features Fixes #61 --- api.rst | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 7ba158dbb4..74e55f5851 100644 --- a/api.rst +++ b/api.rst @@ -63,7 +63,7 @@ in one of a list of values e.g. :code:`IN` in quoted strings like :code:`?a=in."hi,there","yes,you"` is checking for exact equality (null,true,false) :code:`IS` -fts full-text search using to_tsquery :code:`@@` +fts :ref:`fts` using to_tsquery :code:`@@` cs contains e.g. :code:`?tags=cs.{example, new}` :code:`@>` cd contained in e.g. :code:`?values=cd.{1,2,3}` :code:`<@` ov overlap (have points in common), :code:`&&` @@ -102,6 +102,30 @@ The view will provide a new endpoint: GET /fresh_stories HTTP/1.1 +.. _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:`ts_vector`, unsurprisingly of type `tsvector `_. The follow examples illustrate the possibilities. + +.. code-block:: http + + # Use language in fts query + GET /tsearch?ts_vector=french.fts.amusant + + # Use plainto_tsquery and phraseto_tsquery + GET /tsearch?ts_vector=plain.fts.The%20Fat%20Cats + GET /tsearch?ts_vector=phrase.fts.The%20Fat%20Rats + + # Combine both + GET /tsearch?ts_vector=phrase.english.fts.The%20Fat%20Cats + + # "not" also working + GET /tsearch?ts_vector=not.phrase.english.fts.The%20Fat%20Cats + +Using phrase search mode requires PostgreSQL of version at least 9.6 and will raise an error in earlier versions of the database. + .. _v_filter: Vertical Filtering (Columns) From 922ae0e0c984113be05cceb2bd58e24d3e0945bf Mon Sep 17 00:00:00 2001 From: Russell Davies Date: Fri, 29 Sep 2017 06:53:03 +0100 Subject: [PATCH 121/549] Update JWT configuration section for JWK and aud (#106) --- install.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 979dc3c1ee..7b7e45edff 100644 --- a/install.rst +++ b/install.rst @@ -77,6 +77,7 @@ server-host String \*4 server-port Int 3000 server-proxy-uri String jwt-secret String +jwt-aud String secret-is-base64 Bool False max-rows Int ∞ pre-request String @@ -121,7 +122,9 @@ server-proxy-uri } jwt-secret - The secret used to decode JWT tokens clients provide for authentication. 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. + The secret or `JSON Web Key (JWK) `_ used to decode JWT tokens clients provide for authentication. 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. +jwt-aud + Specifies the `JWT audience claim `_. 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. 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. max-rows From e7ddea0acfe29f05071353f41edfa3ae1f4692df Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 7 Oct 2017 13:03:30 -0500 Subject: [PATCH 122/549] Document long jwt secrets as required (#109) * Use long jwt secrets as required * Fix spelling and adjust custom dictionary --- admin.rst | 2 +- api.rst | 21 +++++++++++---------- auth.rst | 12 ++++++------ install.rst | 16 ++++++++-------- intro.rst | 2 +- postgrest.dict | 40 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 67 insertions(+), 26 deletions(-) diff --git a/admin.rst b/admin.rst index fdb075f2cb..d6408aa9f5 100644 --- a/admin.rst +++ b/admin.rst @@ -116,7 +116,7 @@ Next we apply the zone to certain routes, like a hypothetical stored procedure c 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 indescriminate. To rate limit each authenticated request individually you will need to add logic in a :ref:`Custom Validation ` function. +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. Debugging ========= diff --git a/api.rst b/api.rst index 74e55f5851..c05547b07f 100644 --- a/api.rst +++ b/api.rst @@ -47,9 +47,9 @@ Complex logic can also be applied: These operators are available: -============ =============================================== =================== -Abbreviation Meaning Postgres Equivalent -============ =============================================== =================== +============ =============================================== ===================== +Abbreviation Meaning PostgreSQL Equivalent +============ =============================================== ===================== eq equals :code:`=` gt greater than :code:`>` gte greater than or equal :code:`>=` @@ -75,7 +75,7 @@ nxr does not extend to the right of, :code:`&<` nxl does not extend to the left of :code:`&>` adj is adjacent to, e.g. :code:`?range=adj.(1,10)` :code:`-|-` not negates another operator, see below :code:`NOT` -============ =============================================== =================== +============ =============================================== ===================== .. note:: @@ -131,7 +131,7 @@ Using phrase search mode requires PostgreSQL of version at least 9.6 and will ra Vertical Filtering (Columns) ---------------------------- -When certain columns are wide (such as those holding binary data), it is more efficient for the server to withold them in a response. The client can specify which columns are required using the :sql:`select` parameter. +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 :sql:`select` parameter. .. code-block:: http @@ -434,7 +434,7 @@ Which would return ] The primary key of the table of the resource being embedded must be specified, -either explicitly, like in the example above, or implicitly through a wildcard. +either explicitly, like in the example above, or implicitly through a wild card. In this example, since the relationship is a forward relationship, there is only one director associated with a film. As the table name is plural it might @@ -487,7 +487,7 @@ The PostgREST URL grammar limits the kinds of queries clients can perform. It pr * Table unions * More complicated joins than those provided by `Resource Embedding`_ -* Geospatial queries that require an argument, like "points near (lat,lon)" +* Geo-spatial queries that require an argument, like "points near (lat,lon)" * More sophisticated full-text search than a simple use of the :sql:`fts` filter Stored Procedures @@ -635,7 +635,7 @@ Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. 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 backend for efficiency. Note that using CSV requires less parsing on the server and is much faster. +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. Note that using CSV requires less parsing on the server and is much faster. 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 @@ -726,7 +726,7 @@ PostgREST translates `PostgreSQL error codes `_. (It was encoded with a secret of :code:`mysecret` as specified in the SQL code above. You'll want to change this secret in your app!) +The response would look like the snippet below. Try decoding the token at `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.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicm9sZSI6ImF1dGhvciJ9.fpf3_ERi5qbWOE5NPzvauJgvulm0zkIG9xSm2w5zmdw" + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicGFzcyI6ImZvb2JhciJ9.37066TTRlh-1hXhnA9oO9Pj6lgL6zFuJU0iCHhuCFno" } Permissions diff --git a/install.rst b/install.rst index 7b7e45edff..7030616cb5 100644 --- a/install.rst +++ b/install.rst @@ -3,7 +3,7 @@ Binary Release [ `Download from release page `_ ] -The release page has precompiled binaries for Mac OS X, Windows, and several Linux distros. Extract the tarball and run the binary inside with the :code:`--help` flag to see usage instructions: +The release page has pre-compiled binaries for Mac OS X, Windows, and several Linux distributions. Extract the tarball and run the binary inside with the :code:`--help` flag to see usage instructions: .. code-block:: bash @@ -122,7 +122,7 @@ server-proxy-uri } jwt-secret - The secret or `JSON Web Key (JWK) `_ used to decode JWT tokens clients provide for authentication. 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. + The secret or `JSON Web Key (JWK) `_ used to decode JWT tokens clients provide for authentication. For security the key must be at least thirty-two 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. jwt-aud Specifies the `JWT audience claim `_. 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. secret-is-base64 @@ -135,7 +135,7 @@ pre-request Running the Server ------------------ -PostgREST outputs basic request logging to stdout. 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 logfile or to the syslog: +PostgREST outputs basic request logging to stdout. 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 @@ -276,7 +276,7 @@ The script expects the following parameters: test/create_test_db connection_uri database_name [test_db_user] [test_db_user_password] -Use the `connection URI `_ to specify the user, password, host, and port. Do not provide the database in the connection URI. The Postgres role you are using to connect must be capable of creating new databases. +Use the `connection URI `_ to specify the user, password, host, and port. Do not provide the database in the connection URI. The PostgreSQL role you are using to connect must be capable of creating new databases. The :code:`database_name` is the name of the database that :code:`stack test` will connect to. If the database of the same name already exists on the server, the script will first drop it and then re-create it. @@ -288,7 +288,7 @@ Optionally, if specifying an existing user to be used for the test connection, o The script will return the db uri to use in the tests--this uri corresponds to the :code:`db-uri` parameter in the configuration file that one would use in production. -Generating the user and the password allows one to create the database and run the tests against any postgres server without any modifications to the server. (Such as allowing accounts without a passoword or setting up trust authentication, or requiring the server to be on the same localhost the tests are run from). +Generating the user and the password allows one to create the database and run the tests against any PostgreSQL server without any modifications to the server. (Such as allowing accounts without a password or setting up trust authentication, or requiring the server to be on the same localhost the tests are run from). Running the Tests ~~~~~~~~~~~~~~~~~ @@ -321,7 +321,7 @@ This connection assumes the test server on the :code:`localhost:code:` with the Destroying the Database ~~~~~~~~~~~~~~~~~~~~~~~ -The test database will remain after the test, together with four new roles created on the postgres server. To permanently erase the created database and the roles, run the script :code:`test/delete_test_database`, using the same superuser role used for creating the database: +The test database will remain after the test, together with four new roles created on the PostgreSQL server. To permanently erase the created database and the roles, run the script :code:`test/delete_test_database`, using the same superuser role used for creating the database: .. code:: bash @@ -339,7 +339,7 @@ For example, if local development is on a mac with Docker for Mac installed: $ docker run --name db-scripting-test -e POSTGRES_PASSWORD=pwd -p 5434:5432 -d postgres $ POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@localhost:5434" test_db) stack test -Additionally, if one creates a docker container to run stack test (this is necessary on MacOS Sierra with GHC below 8.0.1, where :code:`stack test` fails), one can run PostgreSQL in a separate linked container, or use the locally installed Postgres.app. +Additionally, if one creates a docker container to run stack test (this is necessary on Mac OS Sierra with GHC below 8.0.1, where :code:`stack test` fails), one can run PostgreSQL in a separate linked container, or use the locally installed PostgreSQL app. Build the test container with :code:`test/Dockerfile.test`: @@ -357,7 +357,7 @@ Linked containers: $ docker run --name pg -e POSTGRES_PASSWORD=pwd -d postgres $ docker run --rm -it -v `pwd`:`pwd` -v ~/.stack-linux:/root/.stack --link pg:pg -w="`pwd`" -v `pwd`/.stack-work-docker:`pwd`/.stack-work pgst-test bash -c "POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@pg" test_db) stack test" -Stack test in Docker for Mac, Postgres.app on mac: +Stack test in Docker for Mac, PostgreSQL app on mac: .. code:: bash diff --git a/intro.rst b/intro.rst index dbafcfab77..272c8d9ef6 100644 --- a/intro.rst +++ b/intro.rst @@ -81,7 +81,7 @@ Example Apps * `tyrchen/goodfilm `_ - example film api * `begriffs/postgrest-example `_ - sqitch versioning for API * `SMRxT/postgrest-demo `_ - multi-tenant logging system -* `PierreRochard/postgrest-boilerplate `_ - example auth backend +* `PierreRochard/postgrest-boilerplate `_ - example auth back-end In Production ------------- diff --git a/postgrest.dict b/postgrest.dict index 5966273cbc..1143d7ca74 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -1,13 +1,19 @@ personal_ws-1.1 en 0 utf-8 +AMQP Auth +Bool +CSV Codd +DDL DoS +GHC GUC Github Google GraphQL HMAC HTTPS +HV Haskell Heroku Homebrew @@ -33,53 +39,87 @@ PostgreSQL's RDS RESTful RLS +RSA +RabbitMQ RestSharp SHA SIGHUP +SNS SQL SSL Sencha SuperAgent +Tcl +TypeScript UI Vondra WAI +Websockets +ZeroMQ api +aud auth authenticator balancer +cd centric +conf config cryptographically +csv +disjoined eq +fts +grantor gte http ilike +json jwt localhost login logins +lon lt lte middleware +multi namespaced neq ngrep nullsfirst nullslast +nxl +nxr +openapi +ov param params passphrase +postgrest +pgSQL pgcrypto pgjwt pre refactor +requester's +savepoint +schemas signup +sl sqitch +sql +sr startup +stateful stdout +syslog tsquery +unicode uri url +urls verifier versioning webuser +wildcard From 9598e3948d8eaf08903ebef96e933cea13d4895e Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 7 Oct 2017 19:59:30 -0500 Subject: [PATCH 123/549] Show how to use a JWK literal in the config file (#110) --- auth.rst | 40 +++++++++++++++++++++++++++++++++++++++- install.rst | 2 +- postgrest.dict | 3 +++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/auth.rst b/auth.rst index 9468a4efd5..7957cb9b8b 100644 --- a/auth.rst +++ b/auth.rst @@ -152,7 +152,7 @@ To make an authenticated request the client must include an :code:`Authorization JWT Generation -------------- -You can create a valid JWT either from inside your database or via an external service. Each token is cryptographically signed with a secret passphrase -- the signer and verifier share the secret. Hence any service that shares a passphrase with a PostgREST server can create valid JWT. (PostgREST currently supports only the HMAC-SHA256 signing algorithm.) +You can create a valid JWT either from inside your database or via an external service. Each token is cryptographically signed with a secret key. In the case of symmetric cryptography the signer and verifier share the same secret passphrase. In asymmetric cryptography the signer uses the private key and the verifier the public key. PostgREST supports both symmetric and asymmetric cryptography. JWT from SQL ~~~~~~~~~~~~ @@ -238,6 +238,44 @@ Our code requires a database role in the JWT. To add it you need to save the dat } }) +.. _asym_keys: + +Asymmetric Keys +~~~~~~~~~~~~~~~ + +As described in the :ref:`configuration` section, PostgREST accepts a ``jwt-secret`` config file parameter. If it is set to a simple string value like "reallyreallyreallyreallyverysafe" then PostgREST interprets it as an HMAC-SHA256 passphrase. However you can also specify a literal JWT key JSON value. For example, you can use an RSA-256 public key such as: + +.. 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" + } + +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 `_. + +.. 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 security ~~~~~~~~~~~~ diff --git a/install.rst b/install.rst index 7030616cb5..a7ab1bd4e2 100644 --- a/install.rst +++ b/install.rst @@ -122,7 +122,7 @@ server-proxy-uri } jwt-secret - The secret or `JSON Web Key (JWK) `_ used to decode JWT tokens clients provide for authentication. For security the key must be at least thirty-two 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. + The secret or `JSON Web Key (JWK) `_ used to decode JWT tokens clients provide for authentication. For security the key must be at least thirty-two 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`. jwt-aud Specifies the `JWT audience claim `_. 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. secret-is-base64 diff --git a/postgrest.dict b/postgrest.dict index 1143d7ca74..65b3387f77 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -21,6 +21,7 @@ ILIKE IP JS JSON +JWK JWT Logins MVCC @@ -69,6 +70,7 @@ cryptographically csv disjoined eq +filename fts grantor gte @@ -101,6 +103,7 @@ pgSQL pgcrypto pgjwt pre +reallyreallyreallyreallyverysafe refactor requester's savepoint From a3d87edda35c585b82c2d1a65ed2d5cf2f51b093 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 8 Oct 2017 12:38:46 -0500 Subject: [PATCH 124/549] Explain how sql comments appear in the openapi output (#111) --- api.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index c05547b07f..2a4480f93d 100644 --- a/api.rst +++ b/api.rst @@ -678,9 +678,19 @@ To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instanc OpenAPI Support =============== -Every API hosted by PostgREST automatically serves a full `OpenAPI `_ description on the root path. This provides a list of all endpoints, along with supported HTTP verbs and example payloads. +Every API hosted by PostgREST automatically serves a full `OpenAPI `_ description on the root path. This provides a list of all endpoints, along with supported HTTP verbs and example payloads. For extra customization, the OpenAPI output contains a "description" field for every `SQL comment `_ on a table, column, or function. For instance, -You can use a tool like `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, provides guidance with request headers and example request bodies. +.. code-block:: sql + + 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 ``definitions.monotremes.description`` and ``definitions.monotremes.properties.has_venomous_claw.description``. + +You can use a tool like `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. .. note:: From 59ab74b86139a7c00ea16ac5dc4a829059547584 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 8 Oct 2017 13:35:35 -0500 Subject: [PATCH 125/549] Add items to ecosystem --- intro.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/intro.rst b/intro.rst index 272c8d9ef6..c6509574b1 100644 --- a/intro.rst +++ b/intro.rst @@ -62,6 +62,7 @@ These are PostgreSQL bridges that propagate LISTEN/NOTIFY to external queues for * `aweber/pgsql-listen-exchange `_ - RabbitMQ * `SpiderOak/skeeter `_ - ZeroMQ * `FGRibreau/postgresql-to-amqp `_ - AMQP +* `daurnimator/pg-kinesis-bridge `_ - Amazon Kinesis Example Apps ------------ @@ -86,6 +87,7 @@ Example Apps In Production ------------- +* `Moat `_ * `Catarse `_ * `iAdvize `_ * `Redsmin `_ @@ -98,12 +100,13 @@ In Production Extensions ---------- -* `ppKrauss/PostgREST-writeAPI `_ - generate Nginx rewrite rules to fit an OpenAPI spec * `diogob/postgrest-ws `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY * `pg-safeupdate `_ - Prevent full-table updates or deletes * `srid/spas `_ - allow file uploads and basic auth * `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server * `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware +* `criles25/postgrest-auth `_ - email based auth/signup +* `ppKrauss/PostgREST-writeAPI `_ - generate Nginx rewrite rules to fit an OpenAPI spec Commercial --------------- From 82d2d781be33aefce997fa6764cd785e77422f75 Mon Sep 17 00:00:00 2001 From: Brian Thorne Date: Sat, 4 Nov 2017 16:59:04 +1100 Subject: [PATCH 126/549] Fix typo in api.rst (#113) --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 2a4480f93d..11453fe51a 100644 --- a/api.rst +++ b/api.rst @@ -618,7 +618,7 @@ On the other end of the spectrum you can get the full created object back in the 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. -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 exmaple query setting the :code:`category` column to child for all people below a certain age. +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:: HTTP From e48a206075cb7379732652d80b1fc182e8a852c0 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 4 Nov 2017 23:17:39 -0500 Subject: [PATCH 127/549] Use parens for IN operator Paren-free is deprecated --- api.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api.rst b/api.rst index 11453fe51a..14775ac4a1 100644 --- a/api.rst +++ b/api.rst @@ -59,9 +59,9 @@ neq not equal :code:`<>` or :co like LIKE operator (use * in place of %) :code:`LIKE` ilike ILIKE operator (use * in place of %) :code:`ILIKE` in one of a list of values e.g. :code:`IN` - :code:`?a=in.1,2,3` – also supports commas + :code:`?a=in.(1,2,3)` – also supports commas in quoted strings like - :code:`?a=in."hi,there","yes,you"` + :code:`?a=in.("hi,there","yes,you")` is checking for exact equality (null,true,false) :code:`IS` fts :ref:`fts` using to_tsquery :code:`@@` cs contains e.g. :code:`?tags=cs.{example, new}` :code:`@>` @@ -475,7 +475,7 @@ This sorts the list of actors in each film but does *not* change the order of th .. code-block:: http - GET /films?select=*,roles(*)&roles.character=in.Chico,Harpo,Groucho HTTP/1.1 + GET /films?select=*,roles(*)&roles.character=in.(Chico,Harpo,Groucho) HTTP/1.1 Once again, this restricts the roles included to certain characters but does not filter the films in any way. Films without any of those characters would be included along with empty character lists. From f93e2723e31dbc8363b88ddefe35ff0dad75ce85 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 25 Nov 2017 22:00:55 -0600 Subject: [PATCH 128/549] Add note that windows people need postgres on path (#117) --- install.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install.rst b/install.rst index a7ab1bd4e2..90e612bbb8 100644 --- a/install.rst +++ b/install.rst @@ -34,6 +34,8 @@ To use PostgREST you will need an underlying database (PostgreSQL version 9.5 or * `Instructions for Ubuntu 14.04 `_ * `Installer for Windows `_ +On Windows, PostgREST will fail to run unless the PostgreSQL binaries are on the system path. To test whether this is the case, run ``pg_config`` from the command line. You should see it output a list of paths. + .. _configuration: Configuration From d9e7620160a69e14e12a6b2ffe4fe1b097124ed3 Mon Sep 17 00:00:00 2001 From: Captain Justin Date: Wed, 29 Nov 2017 07:48:31 +0200 Subject: [PATCH 129/549] Update intro.rst (#118) --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index c6509574b1..9754d528bc 100644 --- a/intro.rst +++ b/intro.rst @@ -51,6 +51,7 @@ Client-Side Libraries * `clesiemo3/postgrestR `_ - R * `PierreRochard/postgrest-angular `_ - TypeScript, generate UI from API description * `thejettdurham/postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp +* `team142/ng-postgrest `_ - Angular app for browsing, editing data exposed over Postgrest. External Notification --------------------- From f0bf51efc2b4b5854deecd815b6f6b5d5ba4e913 Mon Sep 17 00:00:00 2001 From: Damien Bry Date: Thu, 28 Dec 2017 04:56:04 +0100 Subject: [PATCH 130/549] Added Elyios as using PostgREST in production (#119) --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index 9754d528bc..426c625cf1 100644 --- a/intro.rst +++ b/intro.rst @@ -97,6 +97,7 @@ In Production * `OpenBooking `_ * `Convene `_ by Thomson-Reuters * `eGull `_ +* `Elyios `_ Extensions ---------- From e10599d0659f5b89028de82964600800f7a34b9d Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Thu, 18 Jan 2018 10:48:34 -0600 Subject: [PATCH 131/549] Changes in 4.4 release (#121) * Updated full-text search * Calling RPC with GET * Customizing HTTP status codes and headers * Move the admin section to bottom of TOC * Revise JWT signing section of tutorial * Selecting pkeys in embedding no longer required --- api.rst | 74 +++++++++++++++++++++++++++++++++------------- index.rst | 10 +++---- postgrest.dict | 5 +++- tutorials/tut1.rst | 17 +++++++---- 4 files changed, 73 insertions(+), 33 deletions(-) diff --git a/api.rst b/api.rst index 14775ac4a1..78d51da1a7 100644 --- a/api.rst +++ b/api.rst @@ -64,6 +64,8 @@ in one of a list of values e.g. :code:`IN` :code:`?a=in.("hi,there","yes,you")` is checking for exact equality (null,true,false) :code:`IS` fts :ref:`fts` using to_tsquery :code:`@@` +plfts :ref:`fts` using plainto_tsquery :code:`@@` +phfts :ref:`fts` using phraseto_tsquery :code:`@@` cs contains e.g. :code:`?tags=cs.{example, new}` :code:`@>` cd contained in e.g. :code:`?values=cd.{1,2,3}` :code:`<@` ov overlap (have points in common), :code:`&&` @@ -107,22 +109,22 @@ The view will provide a new endpoint: 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:`ts_vector`, unsurprisingly of type `tsvector `_. The follow examples illustrate the possibilities. +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 `_. The follow examples illustrate the possibilities. .. code-block:: http # Use language in fts query - GET /tsearch?ts_vector=french.fts.amusant + GET /tsearch?my_tsv=fts(french).amusant # Use plainto_tsquery and phraseto_tsquery - GET /tsearch?ts_vector=plain.fts.The%20Fat%20Cats - GET /tsearch?ts_vector=phrase.fts.The%20Fat%20Rats + GET /tsearch?my_tsv=plfts.The%20Fat%20Cats + GET /tsearch?my_tsv=phfts.The%20Fat%20Rats # Combine both - GET /tsearch?ts_vector=phrase.english.fts.The%20Fat%20Cats + GET /tsearch?my_tsv=phfts(english).The%20Fat%20Cats # "not" also working - GET /tsearch?ts_vector=not.phrase.english.fts.The%20Fat%20Cats + GET /tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats Using phrase search mode requires PostgreSQL of version at least 9.6 and will raise an error in earlier versions of the database. @@ -433,9 +435,6 @@ Which would return } ] -The primary key of the table of the resource being embedded must be specified, -either explicitly, like in the example above, or implicitly through a wild card. - In this example, since the relationship is a forward relationship, there is only one director associated with a film. As the table name is plural it might be preferable for it to be singular instead. An table name alias can accomplish @@ -455,9 +454,6 @@ PostgREST can also detect relations going through join tables. Thus you can requ GET /directors?select=films(title,year) HTTP/1.1 -Here it is not necessary to specify the table's primary key of the embedded -resource. - .. note:: Whenever foreign key relations change in the database schema you must refresh PostgREST's schema cache to allow resource embedding to work properly. See the section :ref:`schema_reloading`. @@ -493,13 +489,13 @@ The PostgREST URL grammar limits the kinds of queries clients can perform. It pr Stored Procedures ================= -Every stored procedure in the API-exposed database schema is accessible under the :code:`/rpc` prefix. The API endpoint supports only POST which executes the function. +Every stored procedure in the API-exposed database schema is accessible under the :code:`/rpc` prefix. The API endpoint supports POST (and in some cases GET) to execute the function. .. code:: http POST /rpc/function_name HTTP/1.1 -Such functions can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). However procedures in PostgreSQL marked with :code:`stable` or :code:`immutable` `volatility `_ can only read, not modify, the database and PostgREST executes them in a read-only transaction compatible for read-replicas. +Such functions can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). However procedures in PostgreSQL marked with :code:`stable` or :code:`immutable` `volatility `_ can only read, not modify, the database and PostgREST executes them in a read-only transaction compatible for read-replicas. Stable and immutable functions can be called with the HTTP GET verb if desired. Procedures must be used with `named arguments `_. To supply arguments in an API call, include a JSON object in the request payload and each key/value of the object will become an argument. @@ -520,7 +516,13 @@ The client can call it by posting an object like { "a": 1, "b": 2 } -The keys of the object match the parameter names. Note that PostgreSQL converts parameter names to lowercase unless you quote them like :sql:`CREATE FUNCTION foo("mixedCase" text) ...`. You can also call a function that takes a single parameter of type json by sending the header :code:`Prefer: params=single-object` with your request. That way the JSON request body will be used as the single argument. +Because ``add_them`` is declared IMMUTABLE, we can alternately call the function with a GET request: + +.. code:: http + + GET /rpc/add_them?a=1&b=2 HTTP/1.1 + +For POST and GET the keys of the object match the parameter names. Note that PostgreSQL converts parameter names to lowercase unless you quote them like :sql:`CREATE FUNCTION foo("mixedCase" text) ...`. You can also call a function that takes a single parameter of type json by sending the header :code:`Prefer: params=single-object` with your request. That way the JSON request body will be used as the single argument. .. note:: @@ -546,8 +548,6 @@ By default, a function is executed with the privileges of the user who calls it. Why the `/rpc` prefix? One reason is to avoid name collisions between views and procedures. It also helps emphasize to API consumers that these functions are not normal restful things. The functions can have arbitrary and surprising behavior, not the standard "post creates a resource" thing that users expect from the other routes. - We are considering allowing GET requests for functions that are marked non-volatile. Allowing GET is important for HTTP caching. However we still must decide how to pass function parameters since request bodies are not allowed. Also some query string arguments are already reserved for shaping/filtering the output. - Accessing Request Headers/Cookies --------------------------------- @@ -557,10 +557,10 @@ Stored procedures can access request headers and cookies by reading GUC variable SELECT current_setting('request.header.origin', true); -Raising Errors --------------- +Errors and HTTP Status Codes +---------------------------- -Stored procedures can return non-200 HTTP status codes by raising SQL exceptions. For instance, here's a saucy function that always errors: +Stored procedures can return non-200 HTTP status codes by raising SQL exceptions. For instance, here's a saucy function that always responds with an error: .. code-block:: postgresql @@ -585,7 +585,39 @@ Calling the function returns HTTP 400 with the body "code":"P0001" } -You can customize the HTTP status code by raising particular exceptions according to the PostgREST :ref:`error to status code mapping `. For example, :code:`RAISE insufficient_privilege` will respond with HTTP 401/403 as appropriate. +One way to customize the HTTP status code is by raising particular exceptions according to the PostgREST :ref:`error to status code mapping `. 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 + + {"hint":"Upgrade your plan","details":"Quota exceeded"} + +Setting Response Headers +------------------------ + +PostgREST reads the ``response.headers`` SQL variable to add extra headers to the HTTP response. Stored procedures can modify this variable. For instance, this statement would add caching headers to the response: + +.. code-block:: sql + + -- tell client to cache response for two days + + SET LOCAL "response.headers" = + '[{"Cache-Control": "public"}, {"Cache-Control": "max-age=259200"}]'; + +Notice that the variable 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 to be repeated when setting multiple values and an object would not allow the repeated key. Insertions / Updates ==================== diff --git a/index.rst b/index.rst index b3ab8b26fa..c59042d724 100644 --- a/index.rst +++ b/index.rst @@ -21,11 +21,6 @@ install.rst -.. toctree:: - :caption: Administration - - admin.rst - .. toctree:: :caption: API @@ -35,3 +30,8 @@ :caption: Authentication auth.rst + +.. toctree:: + :caption: Administration + + admin.rst diff --git a/postgrest.dict b/postgrest.dict index 65b3387f77..bab9af1efa 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -23,6 +23,7 @@ JS JSON JWK JWT +Kinesis Logins MVCC Mithril @@ -98,10 +99,12 @@ ov param params passphrase -postgrest pgSQL pgcrypto pgjwt +phfts +plfts +postgrest pre reallyreallyreallyreallyverysafe refactor diff --git a/tutorials/tut1.rst b/tutorials/tut1.rst index 983ac150f9..84e357a381 100644 --- a/tutorials/tut1.rst +++ b/tutorials/tut1.rst @@ -27,23 +27,28 @@ Step 2. Make a Secret Clients authenticate with the API using JSON Web Tokens. These are JSON objects which are cryptographically signed using a password known to only us and the server. Because clients do not know the password, they cannot tamper with the contents of their tokens. PostgREST will detect counterfeit tokens and will reject them. -Let's create a password and provide it to PostgREST. Think of a nice long one, or use a tool to generate it. +Let's create a password and provide it to PostgREST. Think of a nice long one, or use a tool to generate it. **Your password must be at least 32 characters long.** .. note:: - The `OpenSSL toolkit `_ provides an easy way to generate a secure password. If you have it installed, run + Unix tools can generate a nice password for you: .. code-block:: bash - openssl rand -base64 32 + # Allow "tr" to process non-utf8 byte sequences + export LC_CTYPE=C + + # read random bytes and keep only alphanumerics + < /dev/urandom tr -dc A-Za-z0-9 | head -c32 Open the :code:`tutorial.conf` (created in the previous tutorial) and add a line with the password: .. code-block:: ini - # add this line to tutorial.conf + # PASSWORD MUST BE AT LEAST 32 CHARS LONG + # add this line to tutorial.conf: - jwt-secret = "" + jwt-secret = "" If the PostgREST server is still running from the previous tutorial, restart it to load the updated configuration file. @@ -57,7 +62,7 @@ Ordinarily your own code in the database or in another server will create and si How to create a token at https://jwt.io -Remember to fill in the password you generated rather than the word :code:`secret`. After you have filled in the password and payload, the encoded data on the left will update. Copy the encoded token. +**Remember to fill in the password you generated rather than the word "secret".** After you have filled in the password and payload, the encoded data on the left will update. Copy the encoded token. .. note:: From 362ee5a4ed47040dea9ff705463a98b8d607476c Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Thu, 18 Jan 2018 10:53:51 -0600 Subject: [PATCH 132/549] Bump version --- conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf.py b/conf.py index 394a145849..2a9c6b8579 100644 --- a/conf.py +++ b/conf.py @@ -54,9 +54,9 @@ # built documents. # # The short X.Y version. -version = u'4.3' +version = u'4.4' # The full version, including alpha/beta/rc tags. -release = u'4.3.0' +release = u'4.4.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 7dbd5f799e5897ea717808a1e15c29466a849951 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Fri, 2 Feb 2018 19:57:24 -0800 Subject: [PATCH 133/549] Update name of diogob's websocket adapter --- intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intro.rst b/intro.rst index 426c625cf1..78178b6f1d 100644 --- a/intro.rst +++ b/intro.rst @@ -58,6 +58,7 @@ 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. +* `diogob/postgres-websockets `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY * `frafra/postgresql2websocket `_ - Websockets * `matthewmueller/pg-bridge `_ - Amazon SNS * `aweber/pgsql-listen-exchange `_ - RabbitMQ @@ -102,7 +103,6 @@ In Production Extensions ---------- -* `diogob/postgrest-ws `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY * `pg-safeupdate `_ - Prevent full-table updates or deletes * `srid/spas `_ - allow file uploads and basic auth * `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server From 911417471e99b893232fa824241b438f28e8d15a Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sat, 3 Feb 2018 13:55:32 -0800 Subject: [PATCH 134/549] Add Eric's testimonial --- intro.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/intro.rst b/intro.rst index 78178b6f1d..317c5faaf8 100644 --- a/intro.rst +++ b/intro.rst @@ -138,6 +138,15 @@ Testimonials -- 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 + Getting Support ################ From 7151e624995ce95733d21147c2fa59bcafcce05e Mon Sep 17 00:00:00 2001 From: Camille Roussel Date: Sun, 4 Feb 2018 00:52:51 -0500 Subject: [PATCH 135/549] Update tut0.rst with improved instructions for windows installs (#120) --- tutorials/tut0.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index 6348f8e187..1979de8122 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -83,8 +83,11 @@ If everything is working correctly it will print out its version and information
Windows -

It isn't fun. Learn more here.

-

It might be easier to execute PostgREST in its own Docker image as well.

+

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) +

setx /m PATH "%PATH%;C:\Program Files\PostgreSQL\10\bin"
+

From 16cf1ef45c32d309691c014085392118a2b29d31 Mon Sep 17 00:00:00 2001 From: Christopher Bowman Date: Fri, 9 Mar 2018 20:42:01 -0500 Subject: [PATCH 136/549] Replace 'to to ' with 'to the ' (#122) --- tutorials/tut0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index 1979de8122..dc3d6b1017 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -94,7 +94,7 @@ If everything is working correctly it will print out its version and information Step 4. Create Database for API ------------------------------- -Connect to to SQL console (psql) inside the container. To do so, run this from your command line: +Connect to the SQL console (psql) inside the container. To do so, run this from your command line: .. code-block:: bash From 7b2ab948bebc30cb3c6eff4aeb4943340c496a84 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 11 Mar 2018 12:26:16 -0500 Subject: [PATCH 137/549] Schema auto-reload instructions (#124) --- admin.rst | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/admin.rst b/admin.rst index d6408aa9f5..b47e93853e 100644 --- a/admin.rst +++ b/admin.rst @@ -177,7 +177,29 @@ To refresh the cache without restarting the PostgREST server, send the server pr killall -HUP postgrest -In the future we're investigating ways to keep the cache updated without manual intervention. +The above is the manual way to do it. To automate the schema reloads, use a database trigger like this: + +.. code-block:: postgresql + + CREATE OR REPLACE FUNCTION public.notify_ddl_postgrest() + RETURNS event_trigger + LANGUAGE plpgsql + AS $$ + BEGIN + NOTIFY ddl_command_end; + END; + $$; + + CREATE EVENT TRIGGER ddl_postgrest ON ddl_command_end + EXECUTE PROCEDURE public.notify_ddl_postgrest(); + +Then run the `pg_listen `_ utility to monitor for that event and send a SIGHUP when it occurs: + +.. code-block:: bash + + pg_listen ddl_command_end "killall -HUP postgrest" + +Now, whenever the structure of the database schema changes, PostgreSQL will notify the ``ddl_command_end`` channel, which will cause ``pg_listen`` to send PostgREST the signal to reload its cache. Alternate URL Structure ======================= From 0bae76f05ecfa2a1befb7c2728cc75337577112d Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 14 Mar 2018 18:12:03 +0100 Subject: [PATCH 138/549] Remove iAdvize from In Production list (#125) --- intro.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/intro.rst b/intro.rst index 317c5faaf8..a8e78ed557 100644 --- a/intro.rst +++ b/intro.rst @@ -91,7 +91,6 @@ In Production * `Moat `_ * `Catarse `_ -* `iAdvize `_ * `Redsmin `_ * `Image-charts `_ * `Drip Depot `_ From 1fb9e34c9cd9091531a922b76bc9237b619f93b3 Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Thu, 29 Mar 2018 00:40:54 -0500 Subject: [PATCH 139/549] Add new oauth server --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index a8e78ed557..0e2307b5b9 100644 --- a/intro.rst +++ b/intro.rst @@ -105,6 +105,7 @@ Extensions * `pg-safeupdate `_ - Prevent full-table updates or deletes * `srid/spas `_ - allow file uploads and basic auth * `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server +* `wildsurfer/postgrest-oauth-server `_ - OAuth2 server * `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware * `criles25/postgrest-auth `_ - email based auth/signup * `ppKrauss/PostgREST-writeAPI `_ - generate Nginx rewrite rules to fit an OpenAPI spec From 88e3c8c4b6291fb6f3c9ad522234f7b4d88bb158 Mon Sep 17 00:00:00 2001 From: Hasan Pekdemir Date: Fri, 4 May 2018 16:15:02 +0200 Subject: [PATCH 140/549] add triggerfs to production apps in examples --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index 0e2307b5b9..ac0da6e35e 100644 --- a/intro.rst +++ b/intro.rst @@ -89,6 +89,7 @@ Example Apps In Production ------------- +* `triggerFS - A realtime messaging and distributed trigger system `_ * `Moat `_ * `Catarse `_ * `Redsmin `_ From 28c39e976c1d792d6c7d4f8353ba5d557fea1b1f Mon Sep 17 00:00:00 2001 From: Ken Fehling Date: Mon, 21 May 2018 05:04:20 -0400 Subject: [PATCH 141/549] Update docker-compose example to version 3 Adds "depends_on" --- install.rst | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/install.rst b/install.rst index 90e612bbb8..95152e191a 100644 --- a/install.rst +++ b/install.rst @@ -203,25 +203,28 @@ To avoid having to install the database at all, you can run both it and the serv # docker-compose.yml - server: - image: postgrest/postgrest - ports: - - "3000:3000" - links: - - db:db - environment: - PGRST_DB_URI: postgres://app_user:password@db:5432/app_db - PGRST_DB_SCHEMA: public - PGRST_DB_ANON_ROLE: app_user - - db: - image: postgres - ports: - - "5432:5432" - environment: - POSTGRES_DB: app_db - POSTGRES_USER: app_user - POSTGRES_PASSWORD: password + version: '3' + services: + server: + image: postgrest/postgrest + ports: + - "3000:3000" + links: + - db:db + environment: + PGRST_DB_URI: postgres://app_user:password@db:5432/app_db + PGRST_DB_SCHEMA: public + PGRST_DB_ANON_ROLE: app_user + depends_on: + - db + db: + image: postgres + ports: + - "5432:5432" + environment: + POSTGRES_DB: app_db + POSTGRES_USER: app_user + POSTGRES_PASSWORD: password 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. From e0e4d68185dd56880b36c2349570f6874e33f66f Mon Sep 17 00:00:00 2001 From: agent3bood Date: Wed, 23 May 2018 17:43:28 +0400 Subject: [PATCH 142/549] update PGRST_DB_ANON_ROLE in docker-compose anon should not be the default db user --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 95152e191a..2cb7186fff 100644 --- a/install.rst +++ b/install.rst @@ -214,7 +214,7 @@ To avoid having to install the database at all, you can run both it and the serv environment: PGRST_DB_URI: postgres://app_user:password@db:5432/app_db PGRST_DB_SCHEMA: public - PGRST_DB_ANON_ROLE: app_user + PGRST_DB_ANON_ROLE: app_user #In production this role should not be the same as the one used for the connection depends_on: - db db: From b32ba7a409a984d1ea3e614dc7ae958b47d7a8c7 Mon Sep 17 00:00:00 2001 From: agent3bood Date: Wed, 23 May 2018 17:46:13 +0400 Subject: [PATCH 143/549] Add volume to docker-compose --- install.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install.rst b/install.rst index 2cb7186fff..c54ebdd175 100644 --- a/install.rst +++ b/install.rst @@ -225,6 +225,9 @@ To avoid having to install the database at all, you can run both it and the serv 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. From 736a5020b034b29bf2fd30c5423789bcd2fb8302 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 23 May 2018 13:00:00 -0500 Subject: [PATCH 144/549] Add app.settings.* config value feature --- install.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install.rst b/install.rst index c54ebdd175..c5c81e17ac 100644 --- a/install.rst +++ b/install.rst @@ -83,6 +83,7 @@ jwt-aud String secret-is-base64 Bool False max-rows Int ∞ pre-request String +app.settings.* String ================ ====== ======= ======== db-uri @@ -133,6 +134,8 @@ 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. pre-request A schema-qualified stored procedure name to call right after switching roles for a client request. This provides an opportunity to modify SQL variables or raise an exception to prevent the request from completing. +app.settings.* + Arbitrary settings that will become database session settings. This 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')`. Running the Server ------------------ From e56313196ba147a2fee858489c6ab1709f9a19e0 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 23 May 2018 13:49:06 -0500 Subject: [PATCH 145/549] Add role-claim-key feature --- install.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/install.rst b/install.rst index c5c81e17ac..f8cae8bb86 100644 --- a/install.rst +++ b/install.rst @@ -84,6 +84,7 @@ secret-is-base64 Bool False max-rows Int ∞ pre-request String app.settings.* String +role-claim-key String .role ================ ====== ======= ======== db-uri @@ -136,6 +137,18 @@ pre-request A schema-qualified stored procedure name to call right after switching roles for a client request. This provides an opportunity to modify SQL variables or raise an exception to prevent the request from completing. app.settings.* Arbitrary settings that will become database session settings. This 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')`. +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. Some examples: + +.. code:: bash + + # {"postgrest":{"roles": ["other", "author"]}} + # the DSL accepts characters that are alphanumerical or one of "_$@" as keys + role-claim-key = ".postgrest.roles[1]" + + # {"https://www.example.com/role": { "key": "author }} + # non-alphanumerical characters can go inside quotes(escaped in config value) + role-claim-key = ".\"https://www.example.com/role\".key" Running the Server ------------------ From 6f0a25520a42296417142b71ccaadf479a8daa2c Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 24 May 2018 10:12:23 -0500 Subject: [PATCH 146/549] Add UPSERT feature --- api.rst | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/api.rst b/api.rst index 78d51da1a7..778c0d44f9 100644 --- a/api.rst +++ b/api.rst @@ -694,6 +694,38 @@ To bulk insert JSON post an array of objects having all-matching keys { "name": "Janus", "age": 10, "height": 55 } ] +Upsert +------ + +You can make an UPSERT with :code:`POST` and the :code:`Prefer: resolution=merge-duplicates` header: + +.. code:: HTTP + + POST /employees HTTP/1.1 + Prefer: resolution=merge-duplicates + + [ + { "id": 1, "name": "Old employee 1", "salary": 30000 }, + { "id": 2, "name": "Old employee 2" , "salary": 42000 }, + { "id": 3, "name": "New employee 3" , "salary": 50000 } + ] + +UPSERT merging 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`. + +A single row UPSERT can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: + +.. code:: HTTP + + PUT /employees?id=eq.4 HTTP/1.1 + + { "id": 4, "name": "Sara B.", "salary": 60000 } + +All the columns must be specified in the request body, including the primary key columns. + +.. note:: + + This feature is only available starting from PostgreSQL 9.5 since it uses the `ON CONFLICT clause `_. + Deletions ========= From 2f699fa115abb32d3c911fb28b16661d623bad15 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 24 May 2018 11:37:00 -0500 Subject: [PATCH 147/549] Add foreign tables in OpenAPI feature --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 778c0d44f9..7367227344 100644 --- a/api.rst +++ b/api.rst @@ -742,7 +742,7 @@ To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instanc OpenAPI Support =============== -Every API hosted by PostgREST automatically serves a full `OpenAPI `_ description on the root path. This provides a list of all endpoints, along with supported HTTP verbs and example payloads. For extra customization, the OpenAPI output contains a "description" field for every `SQL comment `_ on a table, column, or function. For instance, +Every API hosted by PostgREST automatically serves a full `OpenAPI `_ 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. For extra customization, the OpenAPI output contains a "description" field for every `SQL comment `_ on a table, column, or function. For instance, .. code-block:: sql From dbec5e08aa29c43eafb608f54ec2768defabd9d4 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 24 May 2018 12:06:00 -0500 Subject: [PATCH 148/549] Fix #131, SCHEMA comment and summary/description --- api.rst | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 7367227344..952f873407 100644 --- a/api.rst +++ b/api.rst @@ -742,17 +742,31 @@ To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instanc OpenAPI Support =============== -Every API hosted by PostgREST automatically serves a full `OpenAPI `_ 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. For extra customization, the OpenAPI output contains a "description" field for every `SQL comment `_ on a table, column, or function. For instance, +Every API hosted by PostgREST automatically serves a full `OpenAPI `_ 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. For extra customization, the OpenAPI output contains a "description" field for every `SQL comment `_ 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 ``definitions.monotremes.description`` and ``definitions.monotremes.properties.has_venomous_claw.description``. +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:: sql + + COMMENT ON TABLE entities IS + $$Entities summary + + Entities description that + spans + multiple lines$$; You can use a tool like `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. From c0f8e64bbb404886221385bc63482701ccd2fa28 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 24 May 2018 12:47:52 -0500 Subject: [PATCH 149/549] Add limit/offset and and/or to embedded resources --- api.rst | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/api.rst b/api.rst index 952f873407..c42d7a7efa 100644 --- a/api.rst +++ b/api.rst @@ -458,10 +458,10 @@ PostgREST can also detect relations going through join tables. Thus you can requ Whenever foreign key relations change in the database schema you must refresh PostgREST's schema cache to allow resource embedding to work properly. See the section :ref:`schema_reloading`. -Embedded Filters and Order --------------------------- +Operations on Embedded Resources +-------------------------------- -Embedded tables can be filtered and ordered similarly to their top-level counterparts. To do so, prefix the query parameters with the name of the embedded table. For instance, to order the actors in each film: +Embedded resources rows can be shaped similarly to their top-level counterparts. To do so, prefix the query parameters with the name of the embedded resource. For instance, to order the actors in each film: .. code-block:: http @@ -475,6 +475,17 @@ This sorts the list of actors in each film but does *not* change the order of th Once again, this restricts the roles included to certain characters but does not filter the films in any way. Films without any of those characters would be included along with empty character lists. +An ``or`` filter can also be used for a similar operation: + +.. code-block:: http + + GET /films?select=*,roles(*)&roles.or=(character.eq.Gummo,character.eq.Zeppo) HTTP/1.1 + +Limit and offset operations are also possible: + +.. code-block:: http + + GET /films?select=*,actors(*)&actors.limit=10&actors.offset=2 HTTP/1.1 Custom Queries ============== From 1108aede5bd73597562fe4b2b6071401c04fd124 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 24 May 2018 12:51:00 -0500 Subject: [PATCH 150/549] Install only necessary deps for BSD and OSX --- install.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.rst b/install.rst index f8cae8bb86..1e3a58ebdd 100644 --- a/install.rst +++ b/install.rst @@ -266,8 +266,8 @@ When a pre-built binary does not exist for your system you can build the project ===================== ======================================= Ubuntu/Debian libpq-dev, libgmp-dev CentOS/Fedora/Red Hat postgresql-devel, zlib-devel, gmp-devel - BSD postgresql95-server - OS X postgresql, gmp + BSD postgresql95-client + OS X libpq, gmp ===================== ======================================= * Build and install binary From 79487307708f8559b4fab007f4627567ba096eb9 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 24 May 2018 13:10:17 -0500 Subject: [PATCH 151/549] Add embeds alias feature --- api.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api.rst b/api.rst index c42d7a7efa..861af8dcf5 100644 --- a/api.rst +++ b/api.rst @@ -487,6 +487,12 @@ Limit and offset operations are also possible: GET /films?select=*,actors(*)&actors.limit=10&actors.offset=2 HTTP/1.1 +You can also alias the embedded resources and apply filters on the aliases: + +.. code-block:: http + + GET /films?select=*,90_comps:competitions(name),91_comps:competitions(name)&90_comps.year=eq.1990&91_comps.year=eq.1991 HTTP/1.1 + Custom Queries ============== From 5e856a22a85f3e8b7721a205fdfd0045d0fe5d5a Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 25 May 2018 11:36:45 -0500 Subject: [PATCH 152/549] Fix #132, remove deprecation notices --- api.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/api.rst b/api.rst index 861af8dcf5..752b7c7de5 100644 --- a/api.rst +++ b/api.rst @@ -79,12 +79,6 @@ adj is adjacent to, e.g. :code:`?range=adj.(1,10)` :code:`-|-` not negates another operator, see below :code:`NOT` ============ =============================================== ===================== -.. note:: - - As of PostgREST v0.4.3.0, the symbol operators :code:`@@, @>, <@` have been - deprecated in lieu of their mnemonic equivalents. They are still supported - but will be removed in v0.5.0.0. - To negate any operator, prefix it with :code:`not` like :code:`?a=not.eq.2` or :code:`?not.and=(a.gte.0,a.lte.100)` . 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: @@ -444,10 +438,6 @@ this: GET /films?select=title,director:directors(id,last_name) HTTP/1.1 -.. note:: - - As of PostgREST v0.4.1.0, parens :code:`()` are used rather than brackets :code:`{}` for the list of embedded columns. Brackets are still supported, but are deprecated and will be removed in v0.5.0.0. - PostgREST can also detect relations going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). You can also reverse the direction of inclusion, asking for all Directors with each including the list of their Films: .. code-block:: http From 6e7a2ca26ceef686a0897b0f7ec0315833846723 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 25 May 2018 12:02:23 -0500 Subject: [PATCH 153/549] Fix #135, clarify usage of GET/POST RPC --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 752b7c7de5..979874e56a 100644 --- a/api.rst +++ b/api.rst @@ -529,7 +529,7 @@ Because ``add_them`` is declared IMMUTABLE, we can alternately call the function GET /rpc/add_them?a=1&b=2 HTTP/1.1 -For POST and GET the keys of the object match the parameter names. Note that PostgreSQL converts parameter names to lowercase unless you quote them like :sql:`CREATE FUNCTION foo("mixedCase" text) ...`. You can also call a function that takes a single parameter of type json by sending the header :code:`Prefer: params=single-object` with your request. That way the JSON request body will be used as the single argument. +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``. Note that PostgreSQL converts parameter names to lowercase unless you quote them like :sql:`CREATE FUNCTION foo("mixedCase" text) ...`. You can also call a function that takes a single parameter of type json by sending the header :code:`Prefer: params=single-object` with your request. That way the JSON request body will be used as the single argument. .. note:: From 7255f026108c242e5c8ebe0cbda3d8bbc6295462 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 25 May 2018 12:18:32 -0500 Subject: [PATCH 154/549] Fix #126, update note about arrays --- api.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 979874e56a..270ccfd0fc 100644 --- a/api.rst +++ b/api.rst @@ -533,7 +533,7 @@ The function parameter names match the JSON object keys in the POST case, for th .. note:: - We recommend using function arguments of type json to accept arrays from the client. To pass a PostgreSQL native array you'll need to quote it as a string: + For versions prior to PostgreSQL 10, to pass a PostgreSQL native array you'll need to quote it as a string: .. code:: http @@ -541,12 +541,16 @@ The function parameter names match the JSON object keys in the POST case, for th { "arg": "{1,2,3}" } + In these versions we recommend using function arguments of type json to accept arrays from the client: + .. code:: http POST /rpc/json_array_func HTTP/1.1 { "arg": [1,2,3] } + Starting from PostgreSQL 10, a json array from the client gets mapped normally to a PostgreSQL native array. + PostgreSQL has four procedural languages that are part of the core distribution: PL/pgSQL, PL/Tcl, PL/Perl, and PL/Python. There are many other procedural languages distributed as additional extensions. Also, plain SQL can be used to write functions (as shown in the example above). By default, 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. Another option is to define the function with 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. See `PostgreSQL documentation `_ for more details. From 121f192b9321c37e6865358a7e343fafa53c7449 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 25 May 2018 13:27:33 -0500 Subject: [PATCH 155/549] Fix #127, RPC cache dependency --- api.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/api.rst b/api.rst index 270ccfd0fc..0130d4c48c 100644 --- a/api.rst +++ b/api.rst @@ -551,6 +551,28 @@ The function parameter names match the JSON object keys in the POST case, for th Starting from PostgreSQL 10, a json array from the client gets mapped normally to a PostgreSQL native array. +PostgREST will detect if the function is scalar or table-valued and will shape the response format accordingly: + +.. code:: http + + GET /rpc/add_them?a=1&b=2 HTTP/1.1 + + 3 + +.. code:: http + + GET /rpc/best_films_2017 HTTP/1.1 + + [ + { "title": "Okja", "rating": 7.4}, + { "title": "Call me by your name", "rating": 8}, + { "title": "Blade Runner 2049", "rating": 8.1} + ] + +.. note:: + + Whenever the function definition changes you must refresh PostgREST's schema for this to work properly. See the section :ref:`schema_reloading`. + PostgreSQL has four procedural languages that are part of the core distribution: PL/pgSQL, PL/Tcl, PL/Perl, and PL/Python. There are many other procedural languages distributed as additional extensions. Also, plain SQL can be used to write functions (as shown in the example above). By default, 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. Another option is to define the function with 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. See `PostgreSQL documentation `_ for more details. From ed028234ccff257d1e2b5ffd73a82beaa13eb3dc Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 25 May 2018 13:39:01 -0500 Subject: [PATCH 156/549] Add example of filtering function response --- api.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api.rst b/api.rst index 0130d4c48c..0bf4153e5e 100644 --- a/api.rst +++ b/api.rst @@ -573,6 +573,12 @@ PostgREST will detect if the function is scalar or table-valued and will shape t Whenever the function definition changes you must refresh PostgREST's schema for this to work properly. See the section :ref:`schema_reloading`. +A function response can be shaped using the same filters as the ones used for tables and views: + +.. code:: http + + GET /rpc/top_rated_films?select=title,director:directors(*)&year=eq.1990&order=title.desc HTTP/1.1 + PostgreSQL has four procedural languages that are part of the core distribution: PL/pgSQL, PL/Tcl, PL/Perl, and PL/Python. There are many other procedural languages distributed as additional extensions. Also, plain SQL can be used to write functions (as shown in the example above). By default, 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. Another option is to define the function with 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. See `PostgreSQL documentation `_ for more details. From 036cc1c5d74ba6a6732b2105cc1ef30b7126c089 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 25 May 2018 14:10:54 -0500 Subject: [PATCH 157/549] Fix #107, Alias column with `:` --- api.rst | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 0bf4153e5e..9fff46ea50 100644 --- a/api.rst +++ b/api.rst @@ -44,7 +44,7 @@ Complex logic can also be applied: .. code-block:: http GET /people?and=(grade.gte.90,student.is.true,or(age.gte.14,age.is.null)) HTTP/1.1 - + These operators are available: ============ =============================================== ===================== @@ -131,10 +131,25 @@ When certain columns are wide (such as those holding binary data), it is more ef .. code-block:: http - GET /people?select=fname,age HTTP/1.1 + GET /people?select=first_name,age HTTP/1.1 + + [ + {"first_name": "John", "age": 30}, + {"first_name": "Jane", "age": 20} + ] The default is :sql:`*`, meaning all columns. This value will become more important below in :ref:`resource_embedding`. +You can rename the columns by prefixing them with an alias followed by the colon ``:`` operator. + +.. code-block:: http + + GET /people?select=fullName:full_name,birthDate:birth_date HTTP/1.1 + [ + {"fullName": "John Doe", "birthDate": "04/25/1988"}, + {"fullName": "Jane Doe", "birthDate": "01/12/1998"} + ] + .. _computed_cols: Computed Columns From 338e8a1ae39fe8197b9329473b6114c898c8c58f Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 25 May 2018 14:25:01 -0500 Subject: [PATCH 158/549] Add casting example --- api.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api.rst b/api.rst index 9fff46ea50..9afb86a873 100644 --- a/api.rst +++ b/api.rst @@ -150,6 +150,16 @@ You can rename the columns by prefixing them with an alias followed by the colon {"fullName": "Jane Doe", "birthDate": "01/12/1998"} ] +Casting the columns is possible by suffixing them with the double colon ``::`` plus the desired type. + +.. code-block:: http + + GET /people?select=full_name,salary::text HTTP/1.1 + [ + {"fullName": "John Doe", "salary": "90000.00"}, + {"fullName": "Jane Doe", "salary": "120000.00"} + ] + .. _computed_cols: Computed Columns From 625a0b2393e22efccbc049e300092699f03b955a Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 25 May 2018 17:31:28 -0500 Subject: [PATCH 159/549] Fix #136, sslmode on db-uri --- install.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 1e3a58ebdd..5355d3b8c8 100644 --- a/install.rst +++ b/install.rst @@ -88,7 +88,9 @@ role-claim-key String .role ================ ====== ======= ======== db-uri - The standard connection PostgreSQL `URI format `_. Symbols and unusual characters in the password or other fields should be percent encoded to avoid a parse error. On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. Also allows connections over Unix sockets for higher performance. + The standard connection PostgreSQL `URI 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 `_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``. + + On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. db-schema The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. db-anon-role From b11f9ef38666e9d257f75ac92b28883a962d219a Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 25 May 2018 17:54:45 -0500 Subject: [PATCH 160/549] Fix #137, unix socket connection --- api.rst | 2 ++ install.rst | 2 ++ 2 files changed, 4 insertions(+) diff --git a/api.rst b/api.rst index 9afb86a873..3422e194d7 100644 --- a/api.rst +++ b/api.rst @@ -145,6 +145,7 @@ You can rename the columns by prefixing them with an alias followed by the colon .. code-block:: http GET /people?select=fullName:full_name,birthDate:birth_date HTTP/1.1 + [ {"fullName": "John Doe", "birthDate": "04/25/1988"}, {"fullName": "Jane Doe", "birthDate": "01/12/1998"} @@ -155,6 +156,7 @@ Casting the columns is possible by suffixing them with the double colon ``::`` p .. code-block:: http GET /people?select=full_name,salary::text HTTP/1.1 + [ {"fullName": "John Doe", "salary": "90000.00"}, {"fullName": "Jane Doe", "salary": "120000.00"} diff --git a/install.rst b/install.rst index 5355d3b8c8..d50e7286ab 100644 --- a/install.rst +++ b/install.rst @@ -90,6 +90,8 @@ role-claim-key String .role db-uri The standard connection PostgreSQL `URI 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 `_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``. + When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ and the `Peer Authentication method `_ as an alternative to TCP/IP communication and authentication with a password, this also grants higher performance. According to the documentation on the `libpq connection string `_ the empty host resolves to the Unix socket and the password can be omitted in this case, so the ``db-uri`` would be reduced to ``postgres://user@/dbname``. + On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. db-schema The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. From c71bbba979082240c7ab9cfacb52e43173d3681d Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 28 May 2018 17:25:21 -0500 Subject: [PATCH 161/549] Some corrections and better wording --- api.rst | 24 ++++++++++++------------ install.rst | 6 +++--- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/api.rst b/api.rst index 3422e194d7..827eddd73f 100644 --- a/api.rst +++ b/api.rst @@ -158,8 +158,8 @@ Casting the columns is possible by suffixing them with the double colon ``::`` p GET /people?select=full_name,salary::text HTTP/1.1 [ - {"fullName": "John Doe", "salary": "90000.00"}, - {"fullName": "Jane Doe", "salary": "120000.00"} + {"full_name": "John Doe", "salary": "90000.00"}, + {"full_name": "Jane Doe", "salary": "120000.00"} ] .. _computed_cols: @@ -475,10 +475,10 @@ PostgREST can also detect relations going through join tables. Thus you can requ Whenever foreign key relations change in the database schema you must refresh PostgREST's schema cache to allow resource embedding to work properly. See the section :ref:`schema_reloading`. -Operations on Embedded Resources --------------------------------- +Embedded Operations +------------------- -Embedded resources rows can be shaped similarly to their top-level counterparts. To do so, prefix the query parameters with the name of the embedded resource. For instance, to order the actors in each film: +Embedded resources can be shaped similarly to their top-level counterparts. To do so, prefix the query parameters with the name of the embedded resource. For instance, to order the actors in each film: .. code-block:: http @@ -492,19 +492,19 @@ This sorts the list of actors in each film but does *not* change the order of th Once again, this restricts the roles included to certain characters but does not filter the films in any way. Films without any of those characters would be included along with empty character lists. -An ``or`` filter can also be used for a similar operation: +An ``or`` filter can be used for a similar operation: .. code-block:: http GET /films?select=*,roles(*)&roles.or=(character.eq.Gummo,character.eq.Zeppo) HTTP/1.1 -Limit and offset operations are also possible: +Limit and offset operations are possible: .. code-block:: http GET /films?select=*,actors(*)&actors.limit=10&actors.offset=2 HTTP/1.1 -You can also alias the embedded resources and apply filters on the aliases: +Embedded resources can be aliased and filters can be applied on these aliases: .. code-block:: http @@ -560,7 +560,7 @@ The function parameter names match the JSON object keys in the POST case, for th .. note:: - For versions prior to PostgreSQL 10, to pass a PostgreSQL native array you'll need to quote it as a string: + For versions prior to PostgreSQL 10, to pass a PostgreSQL native array you need to quote it as a string: .. code:: http @@ -772,11 +772,11 @@ You can make an UPSERT with :code:`POST` and the :code:`Prefer: resolution=merge [ { "id": 1, "name": "Old employee 1", "salary": 30000 }, - { "id": 2, "name": "Old employee 2" , "salary": 42000 }, - { "id": 3, "name": "New employee 3" , "salary": 50000 } + { "id": 2, "name": "Old employee 2", "salary": 42000 }, + { "id": 3, "name": "New employee 3", "salary": 50000 } ] -UPSERT merging 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`. +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`. A single row UPSERT can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: diff --git a/install.rst b/install.rst index d50e7286ab..36ad6fb9f9 100644 --- a/install.rst +++ b/install.rst @@ -90,7 +90,7 @@ role-claim-key String .role db-uri The standard connection PostgreSQL `URI 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 `_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``. - When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ and the `Peer Authentication method `_ as an alternative to TCP/IP communication and authentication with a password, this also grants higher performance. According to the documentation on the `libpq connection string `_ the empty host resolves to the Unix socket and the password can be omitted in this case, so the ``db-uri`` would be reduced to ``postgres://user@/dbname``. + When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ and the `Peer Authentication method `_ 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 `_ documentation for more details. On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. db-schema @@ -142,7 +142,7 @@ pre-request app.settings.* Arbitrary settings that will become database session settings. This 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')`. 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. Some examples: + 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 @@ -151,7 +151,7 @@ role-claim-key role-claim-key = ".postgrest.roles[1]" # {"https://www.example.com/role": { "key": "author }} - # non-alphanumerical characters can go inside quotes(escaped in config value) + # non-alphanumerical characters can go inside quotes(escaped in the config value) role-claim-key = ".\"https://www.example.com/role\".key" Running the Server From d03770d352a748941cacf3fe84efe7b4c10385d7 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 28 May 2018 17:37:11 -0500 Subject: [PATCH 162/549] Bump version to v5.0.0 --- conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf.py b/conf.py index 2a9c6b8579..05837e17db 100644 --- a/conf.py +++ b/conf.py @@ -54,9 +54,9 @@ # built documents. # # The short X.Y version. -version = u'4.4' +version = u'5.0' # The full version, including alpha/beta/rc tags. -release = u'4.4.0' +release = u'5.0.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From f84b79ce4132389814f285ff6f1939b29fe7d12b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 31 May 2018 09:03:24 -0500 Subject: [PATCH 163/549] Add stored function explicit qualification --- api.rst | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 827eddd73f..d109081408 100644 --- a/api.rst +++ b/api.rst @@ -608,12 +608,26 @@ A function response can be shaped using the same filters as the ones used for ta PostgreSQL has four procedural languages that are part of the core distribution: PL/pgSQL, PL/Tcl, PL/Perl, and PL/Python. There are many other procedural languages distributed as additional extensions. Also, plain SQL can be used to write functions (as shown in the example above). -By default, 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. Another option is to define the function with 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. See `PostgreSQL documentation `_ for more details. +By default, 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. 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. See `PostgreSQL documentation `_ for more details. .. note:: Why the `/rpc` prefix? One reason is to avoid name collisions between views and procedures. It also helps emphasize to API consumers that these functions are not normal restful things. The functions can have arbitrary and surprising behavior, not the standard "post creates a resource" thing that users expect from the other routes. +Explicit Qualification +---------------------- + +As of ``v5.0``, PostgREST executes a ``SET SCHEMA `` on each request, this overrides the `search_path `_ and it means that for functions to work properly, explicit qualification is needed for any database object that is not in the ````. However, this can be cumbersome when working with extensions such as PostGIS, to avoid it, you can add a search path to the function: + +.. code-block:: plpgsql + + CREATE FUNCTION api.line() RETURNS json AS $$ + SELECT ST_AsGeoJSON('LINESTRING(1 2 3, 4 5 6)')::json; + $$ LANGUAGE sql SET search_path = public; + + -- existing functions can be altered to add a search_path + ALTER FUNCTION api.make_point() SET search_path = public; + Accessing Request Headers/Cookies --------------------------------- @@ -825,7 +839,7 @@ These unsavory comments will appear in the generated JSON as the fields, ``info. 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:: sql +.. code-block:: plpgsql COMMENT ON TABLE entities IS $$Entities summary From 92cc815e9de626798348303075162b41cc791610 Mon Sep 17 00:00:00 2001 From: Francois-Guillaume Ribreau Date: Sat, 9 Jun 2018 10:49:29 +0200 Subject: [PATCH 164/549] Add MotionDynamic (PostgREST + SubZero) --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index ac0da6e35e..20ac027398 100644 --- a/intro.rst +++ b/intro.rst @@ -94,6 +94,7 @@ In Production * `Catarse `_ * `Redsmin `_ * `Image-charts `_ +* `MotionDynamic - Fast highly dynamic video generation at scale `_ * `Drip Depot `_ * `OpenBooking `_ * `Convene `_ by Thomson-Reuters From 0e16f34eef97c62fad64fcd22c7d1ff9dbf9e585 Mon Sep 17 00:00:00 2001 From: ASVBPREAUBV Date: Thu, 14 Jun 2018 16:09:13 +0200 Subject: [PATCH 165/549] Make swagger-gui an optional step in installation --- install.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/install.rst b/install.rst index 36ad6fb9f9..58341baeee 100644 --- a/install.rst +++ b/install.rst @@ -251,6 +251,21 @@ To avoid having to install the database at all, you can run both it and the serv 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: Build from Source From c7c6ee40e4fcada5a1b7376b83a9d3baff9583bb Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 14 Jun 2018 09:29:12 -0500 Subject: [PATCH 166/549] Add entries to the dict --- postgrest.dict | 3 +++ 1 file changed, 3 insertions(+) diff --git a/postgrest.dict b/postgrest.dict index bab9af1efa..83af75b114 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -129,3 +129,6 @@ verifier versioning webuser wildcard +Upsert +UPSERT +ui From be039152f2b6a73fe4ffa1e71fafb45d0c111af6 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 15 Jun 2018 11:03:59 -0500 Subject: [PATCH 167/549] Improve explicit qualification section --- api.rst | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/api.rst b/api.rst index d109081408..ab0c16ffc6 100644 --- a/api.rst +++ b/api.rst @@ -617,16 +617,31 @@ By default, a function is executed with the privileges of the user who calls it. Explicit Qualification ---------------------- -As of ``v5.0``, PostgREST executes a ``SET SCHEMA `` on each request, this overrides the `search_path `_ and it means that for functions to work properly, explicit qualification is needed for any database object that is not in the ````. However, this can be cumbersome when working with extensions such as PostGIS, to avoid it, you can add a search path to the function: +As of ``v5.0``, PostgREST executes a ``SET SCHEMA `` on each request, since this overrides the `search_path `_, function bodies need qualified schema names for any database object that is not in your exposed schema. .. code-block:: plpgsql - CREATE FUNCTION api.line() RETURNS json AS $$ - SELECT ST_AsGeoJSON('LINESTRING(1 2 3, 4 5 6)')::json; - $$ LANGUAGE sql SET search_path = public; + -- Assuming that: + -- exposed schema is "api" + -- ST_AsGeoJSON is in the "public" schema + -- streets is in the "api" schema + CREATE FUNCTION api.sample() RETURNS json AS $$ + SELECT public.ST_AsGeoJSON(geom)::json FROM streets LIMIT 1; + -- Notice streets doesn't need the schema prefix while ST_AsGeoJSON does + $$ LANGUAGE sql; + +To avoid having to qualify many database objects, you can add a ``search_path`` to the function: + +.. code-block:: plpgsql + + CREATE FUNCTION api.sample_distance() RETURNS float8 AS $$ + SELECT ST_MakePoint(1, 1) <-> ST_MakePoint(10, 10); + -- If the search_path is not specified, this would have to be: + -- SELECT public.ST_MakePoint(1, 1) operator(public.<->) public.ST_MakePoint(10, 10); + $$ LANGUAGE sql SET search_path = public, api; -- existing functions can be altered to add a search_path - ALTER FUNCTION api.make_point() SET search_path = public; + ALTER FUNCTION api.make_point(float8, float8) SET search_path = public, api; Accessing Request Headers/Cookies --------------------------------- From cfaf588bba9a4d3ee6909042a5d5d669567e2cd9 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 19 Jun 2018 12:33:49 -0500 Subject: [PATCH 168/549] Add json operators to vertical filtering section --- api.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/api.rst b/api.rst index ab0c16ffc6..11ac6ae51c 100644 --- a/api.rst +++ b/api.rst @@ -162,6 +162,26 @@ Casting the columns is possible by suffixing them with the double colon ``::`` p {"full_name": "Jane Doe", "salary": "120000.00"} ] +You can specify a json path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `_. + +.. code-block:: http + + GET /people?select=id,json_data->>blood_type,json_data->phones HTTP/1.1 + + [ + { "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:: http + + GET /people?select=id,json_data->phones->0->>number HTTP/1.1 + + [ + { "id": 1, "number": "917-929-5745"}, + { "id": 2, "number": "512-446-4988"} + ] + .. _computed_cols: Computed Columns From a7fd694f19a853021c08303debafae18cb1f3bff Mon Sep 17 00:00:00 2001 From: PJLindsay Date: Fri, 13 Jul 2018 12:06:04 -0600 Subject: [PATCH 169/549] Update tut0.rst (#159) * Update tut0.rst Add hint about port mapping for users with pre-existing PostgreSQL DB --- tutorials/tut0.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index dc3d6b1017..8ff68e6e57 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -35,6 +35,16 @@ If Docker is not installed, you can get it `here Date: Sun, 5 Aug 2018 08:24:05 -0700 Subject: [PATCH 170/549] auth.rst typo: paramter -> parameter --- auth.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth.rst b/auth.rst index 7957cb9b8b..203d046c3c 100644 --- a/auth.rst +++ b/auth.rst @@ -116,7 +116,7 @@ There is no performance penalty for having many database roles, although roles a Custom Validation ----------------- -PostgREST honors the :code:`exp` claim for token expiration, rejecting expired tokens. However it does not enforce any extra constraints. An example of an extra constraint would be to immediately revoke access for a certain user. The configuration file paramter :code:`pre-request` specifies a stored procedure to call immediately after the authenticator switches into a new role and before the main query itself runs. +PostgREST honors the :code:`exp` claim for token expiration, rejecting expired tokens. However it does not enforce any extra constraints. An example of an extra constraint would be to immediately revoke access for a certain user. The configuration file parameter :code:`pre-request` specifies a stored procedure to call immediately after the authenticator switches into a new role and before the main query itself runs. Here's an example. In the config file specify a stored procedure: From 02f7cba3d2586f0e309af0b213634496825d6d1b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 6 Sep 2018 13:24:42 -0500 Subject: [PATCH 171/549] Fix install broken link and add rst cheatsheet --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bb9afe28e1..4a2bb4f58d 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -#### Sphinx source files for PostgREST documentation +# PostgREST documentation + +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 generate HTML version: -1. Install Sphinx from the [sphinx website](http://sphinx-doc.org/latest/install.html) +1. Install Sphinx from the [sphinx website](http://www.sphinx-doc.org/en/stable/install.html) 2. Clone this repository -4. Generate HTML +3. Generate HTML ```bash cd postgrest-docs sphinx-build -b html -a -n . _build From 512862918f510c751514e9c9c8fa1dbc18739237 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 10 Sep 2018 09:55:45 -0500 Subject: [PATCH 172/549] Change default theme to readthedocs --- conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf.py b/conf.py index 05837e17db..f1f263a9eb 100644 --- a/conf.py +++ b/conf.py @@ -108,7 +108,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +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 From bba1d35ade3577d318761fce57875e5853eb0380 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 10 Sep 2018 10:46:38 -0500 Subject: [PATCH 173/549] Fix #178, Add livereload script --- README.md | 1 - reload_docs.py | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100755 reload_docs.py diff --git a/README.md b/README.md index 4a2bb4f58d..30be737ccb 100644 --- a/README.md +++ b/README.md @@ -19,4 +19,3 @@ To generate HTML version: **Sphinx Installation Notes:** * If you're on OSX you might want to install the Python from homebrew - then a simple `pip install sphinx` does the trick. -* For an easier time refreshing your local preview of docs as you change it, try [sphinx-autobuild](https://github.com/GaretJax/sphinx-autobuild). diff --git a/reload_docs.py b/reload_docs.py new file mode 100755 index 0000000000..4ec06803f3 --- /dev/null +++ b/reload_docs.py @@ -0,0 +1,6 @@ +#!/usr/bin/env python +from livereload import Server, shell +server = Server() +server.watch('*.rst', shell('sphinx-build -b html -a -n . _build')) +server.watch('tutorials/*.rst', shell('sphinx-build -b html -a -n . _build')) +server.serve(root='_build/') From 54436e2dc6cf742182629d6ab201e5d1a02c7c1d Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 10 Sep 2018 10:47:03 -0500 Subject: [PATCH 174/549] Add default.nix and udpate README --- README.md | 8 ++++++++ default.nix | 14 ++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 default.nix diff --git a/README.md b/README.md index 30be737ccb..c8c5fb53d8 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,14 @@ To generate HTML version: # open _build/index.html in your browser ``` +If you use [nix](https://nixos.org/nix/), you can just run: + +``` + nix-shell +``` + +This will build the docs and start a livereload server on `http://localhost:5500`. + --- **Sphinx Installation Notes:** diff --git a/default.nix b/default.nix new file mode 100644 index 0000000000..6d32f96097 --- /dev/null +++ b/default.nix @@ -0,0 +1,14 @@ +with import {}; + +stdenv.mkDerivation { + name = "postgrest-docs"; + buildInputs = [ + python36Full + python36Packages.sphinx + python36Packages.sphinx_rtd_theme + python36Packages.livereload ]; + shellHook = '' + sphinx-build -b html -a -n . _build + python reload_docs.py && exit + ''; +} From c4e926af2d065a5744c87dd6f1b14f5b047bb755 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 10 Sep 2018 11:05:05 -0500 Subject: [PATCH 175/549] Update postgrest repo links --- install.rst | 6 +++--- tutorials/tut0.rst | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/install.rst b/install.rst index 58341baeee..278d94a14e 100644 --- a/install.rst +++ b/install.rst @@ -1,13 +1,13 @@ Binary Release ============== -[ `Download from release page `_ ] +[ `Download from release page `_ ] The release page has pre-compiled binaries for Mac OS X, Windows, and several Linux distributions. Extract the tarball and run the binary inside with the :code:`--help` flag to see usage instructions: .. code-block:: bash - # Untar the release (available at https://github.com/begriffs/postgrest/releases/latest) + # Untar the release (available at https://github.com/PostgREST/postgrest/releases/latest) $ tar Jxf postgrest-[version]-[platform].tar.xz @@ -293,7 +293,7 @@ When a pre-built binary does not exist for your system you can build the project .. code-block:: bash - git clone https://github.com/begriffs/postgrest.git + git clone https://github.com/PostgREST/postgrest.git cd postgrest # adjust local-bin-path to taste diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index 8ff68e6e57..dea41538c7 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -48,13 +48,13 @@ This will run the Docker instance as a daemon and expose port 5432 to the host s Step 3. Install PostgREST ------------------------- -PostgREST is distributed as a single binary, with versions compiled for major distributions of Linux/BSD/Windows. Visit the `latest release `_ 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. +PostgREST is distributed as a single binary, with versions compiled for major distributions of Linux/BSD/Windows. Visit the `latest release `_ 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/begriffs/postgrest/releases/latest + # download from https://github.com/PostgREST/postgrest/releases/latest tar xfJ postgrest--.tar.xz From 24f796a329caf3a2be5dea81a89cf4d7fd0efd4b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 10 Sep 2018 11:27:57 -0500 Subject: [PATCH 176/549] Remove app.settings mention of being session scoped --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 278d94a14e..c62d85d8bc 100644 --- a/install.rst +++ b/install.rst @@ -140,7 +140,7 @@ max-rows pre-request A schema-qualified stored procedure name to call right after switching roles for a client request. This provides an opportunity to modify SQL variables or raise an exception to prevent the request from completing. app.settings.* - Arbitrary settings that will become database session settings. This 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')`. + 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')`. 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: From 48ab4e91be292c09ee18bbdbf16fe94ca040a9cc Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 10 Sep 2018 11:40:55 -0500 Subject: [PATCH 177/549] Add header for JSON select feature --- api.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 11ac6ae51c..ea919b4be2 100644 --- a/api.rst +++ b/api.rst @@ -162,7 +162,10 @@ Casting the columns is possible by suffixing them with the double colon ``::`` p {"full_name": "Jane Doe", "salary": "120000.00"} ] -You can specify a json path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `_. +JSON Columns +~~~~~~~~~~~~ + +You can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `_. .. code-block:: http From 99d878b13edc58aa6ecfcfe87ec6d0bc9c25cb6f Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 10 Sep 2018 12:09:06 -0500 Subject: [PATCH 178/549] Fix #175, add important note for computed column --- api.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index ea919b4be2..0d438a9339 100644 --- a/api.rst +++ b/api.rst @@ -4,7 +4,7 @@ Tables and Views ================ -All views and tables in the active schema and accessible by the active database role for a request are available for querying. They are exposed in one-level deep routes. For instance the full contents of a table `people` is returned at +All views and tables in the exposed schema and accessible by the active database role for a request are available for querying. They are exposed in one-level deep routes. For instance the full contents of a table `people` is returned at .. code-block:: http @@ -219,6 +219,10 @@ As mentioned, computed columns do not appear in the output by default. However y GET /people?select=*,full_name HTTP/1.1 +.. important:: + + Computed columns must be created under the exposed schema to be used in this way. + Ordering -------- From 38610e0349f6d2ff23b32da5ec9f958ceb7f62f6 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 10 Sep 2018 12:11:49 -0500 Subject: [PATCH 179/549] Change type of notes for reloading and deleting/updating --- api.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/api.rst b/api.rst index 0d438a9339..7180ad8b75 100644 --- a/api.rst +++ b/api.rst @@ -498,7 +498,7 @@ PostgREST can also detect relations going through join tables. Thus you can requ GET /directors?select=films(title,year) HTTP/1.1 -.. note:: +.. important:: Whenever foreign key relations change in the database schema you must refresh PostgREST's schema cache to allow resource embedding to work properly. See the section :ref:`schema_reloading`. @@ -623,7 +623,7 @@ PostgREST will detect if the function is scalar or table-valued and will shape t { "title": "Blade Runner 2049", "rating": 8.1} ] -.. note:: +.. important:: Whenever the function definition changes you must refresh PostgREST's schema for this to work properly. See the section :ref:`schema_reloading`. @@ -782,7 +782,7 @@ To update a row or rows in a table, use the PATCH verb. Use :ref:`h_filter` to s Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. -.. note:: +.. warning:: Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`. @@ -857,7 +857,7 @@ To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instanc DELETE /user?active=is.false HTTP/1.1 -.. note:: +.. warning:: Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`. @@ -892,7 +892,7 @@ Also if you wish to generate a ``summary`` field you can do it by having a multi You can use a tool like `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. -.. note:: +.. important:: The OpenAPI information can go out of date as the schema changes under a running server. To learn how to refresh the cache see :ref:`schema_reloading`. From c9816f166133b1b650f1772bb40aa6ff94138ff1 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 10 Sep 2018 13:49:22 -0500 Subject: [PATCH 180/549] Hyperlink config settings --- install.rst | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/install.rst b/install.rst index c62d85d8bc..b0df82b49c 100644 --- a/install.rst +++ b/install.rst @@ -87,19 +87,43 @@ app.settings.* String role-claim-key String .role ================ ====== ======= ======== +.. _db-uri: + db-uri +------ + The standard connection PostgreSQL `URI 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 `_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``. When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ and the `Peer Authentication method `_ 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 `_ documentation for more details. On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. + +.. _db-schema: + db-schema +--------- + The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. + +.. _db-anon-role: + db-anon-role +------------ + The database role to use when executing commands on behalf of unauthenticated clients. + +.. _db-pool: + db-pool +------- + Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the :code:`max_connections` GUC in your database. + +.. _server-host: + server-host +----------- + 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 @@ -108,9 +132,18 @@ server-host * :code:`*6` - any IPv4 or IPv6 hostname, IPv6 preferred * :code:`!6` - any IPv6 hostname +.. _server-port: + server-port +----------- + The port to bind the web server. + +.. _server-proxy-uri: + 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 @@ -129,19 +162,53 @@ server-proxy-uri ] } +.. _jwt-secret: + jwt-secret +---------- + The secret or `JSON Web Key (JWK) `_ used to decode JWT tokens clients provide for authentication. For security the key must be at least thirty-two 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`. + +.. _jwt-aud: + jwt-aud +------- + Specifies the `JWT audience claim `_. 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. + +.. _secret-is-base64: + 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. + +.. _max-rows: + 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. + +.. _pre-request: + pre-request +----------- + A schema-qualified stored procedure name to call right after switching roles for a client request. This provides an opportunity to modify SQL variables or raise an exception to prevent the request from completing. + +.. _app.settings.*: + app.settings.* +-------------- + 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')`. + +.. _role-claim-key: + 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 From a924c4b12f0b676c3aa51df65115f1120d3c670c Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 10 Sep 2018 13:59:51 -0500 Subject: [PATCH 181/549] Fix #173, add ref for jwt claim access --- api.rst | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/api.rst b/api.rst index 7180ad8b75..d9344c790c 100644 --- a/api.rst +++ b/api.rst @@ -670,14 +670,23 @@ To avoid having to qualify many database objects, you can add a ``search_path`` -- existing functions can be altered to add a search_path ALTER FUNCTION api.make_point(float8, float8) SET search_path = public, api; -Accessing Request Headers/Cookies ---------------------------------- +Accessing Request Headers, Cookies and JWT claims +------------------------------------------------- -Stored procedures can access request headers and cookies by reading GUC variables set by PostgREST per request. They are named :code:`request.header.XYZ` and :code:`request.cookie.XYZ`. For example, to read the value of the Origin request header: +Stored procedures can access request headers, cookies and jwt claims by reading GUC variables set by PostgREST per request. They are named :code:`request.header.XYZ`, :code:`request.cookie.XYZ` and :code:`request.jwt.claim.XYZ`. .. code-block:: postgresql + -- To read the value of the Origin request header: SELECT current_setting('request.header.origin', true); + -- To read the value of sessionId in a cookie: + SELECT current_setting('request.cookie.sessionId', true); + -- To read the value of the email claim in a jwt: + SELECT current_setting('request.jwt.claim.email', true); + +.. note:: + + ``request.jwt.claim.role`` defaults to the value of :ref:`db-anon-role`. Errors and HTTP Status Codes ---------------------------- From bca1ddf0d613fc6bf31fed0ecc0814b4c7928984 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 11 Sep 2018 09:51:24 -0500 Subject: [PATCH 182/549] Fix #168, Rename SSL to HTTPS --- auth.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/auth.rst b/auth.rst index 203d046c3c..a8c012dd91 100644 --- a/auth.rst +++ b/auth.rst @@ -289,12 +289,12 @@ The last type of critique focuses on the misuse of JWT for maintaining web sessi 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. -.. _ssl: +.. _https: -SSL ---- +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 SSL. 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. +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. Schema Isolation ================ From 5e2f3b8d596819a2086fef1f607a5f3efc865997 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 11 Sep 2018 10:13:41 -0500 Subject: [PATCH 183/549] Fix #166, reorder docker instructions --- install.rst | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/install.rst b/install.rst index b0df82b49c..ea6ab76f19 100644 --- a/install.rst +++ b/install.rst @@ -239,9 +239,15 @@ PostgREST outputs basic request logging to stdout. When running it in an SSH ses Docker ====== -The official PostgREST Docker image consults an internal :code:`/etc/postgrest.conf` file. To customize this file you can either mount a replacement configuration file into the container, or use environment variables. The environment variables will be interpolated into the default config file. +You can get the `official PostgREST Docker image `_ with: -These variables match the options shown in our :ref:`configuration` section, except they are capitalized, have a prefix, and use underscores. To get a list of the available environment variables, run this: +.. code-block:: bash + + docker pull postgrest/postgrest + +The image consults an internal ``/etc/postgrest.conf`` file. To customize this file you can either mount a replacement configuration file into the container, or use environment variables. The environment variables will be interpolated into the default config file. + +These variables match the options shown in our :ref:`configuration` section, except they are capitalized, have a ``PGRST_`` prefix, and use underscores. To get a list of the available environment variables, run this: .. code-block:: bash @@ -256,9 +262,6 @@ The first way to run PostgREST in Docker is to connect it to an existing native .. code-block:: bash - # Pull the official image - docker pull postgrest/postgrest - # Run the server docker run --rm --net=host -p 3000:3000 \ -e PGRST_DB_URI="postgres://postgres@localhost/postgres" \ From d78862996ff3405d6fd91c4c7c53b3a470dffe90 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 11 Sep 2018 11:20:46 -0500 Subject: [PATCH 184/549] Fix #165, add schemas for sign/login --- auth.rst | 47 +++++++++++++++++++---------------------------- 1 file changed, 19 insertions(+), 28 deletions(-) diff --git a/auth.rst b/auth.rst index a8c012dd91..ee254594a4 100644 --- a/auth.rst +++ b/auth.rst @@ -126,18 +126,16 @@ Here's an example. In the config file specify a stored procedure: In the function you can run arbitrary code to check the request and raise an exception to block it if desired. -.. code:: sql +.. code-block:: postgres - CREATE OR REPLACE FUNCTION check_user() RETURNS void - LANGUAGE plpgsql - AS $$ + CREATE OR REPLACE FUNCTION check_user() RETURNS void AS $$ BEGIN IF current_user = 'evil_user' THEN RAISE EXCEPTION 'No, you are evil' USING HINT = 'Stop being so evil and maybe you can log in'; END IF; END - $$; + $$ LANGUAGE plpgsql; Client Auth =========== @@ -157,20 +155,18 @@ You can create a valid JWT either from inside your database or via an external s JWT from SQL ~~~~~~~~~~~~ -You can create JWT tokens in SQL using the `pgjwt extension `_. 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 which creates the functions you will need. +You can create JWT tokens in SQL using the `pgjwt extension `_. 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 `_ (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:: sql +.. code-block:: postgres CREATE TYPE jwt_token AS ( token text ); - CREATE FUNCTION jwt_test() RETURNS public.jwt_token - LANGUAGE sql - AS $$ - SELECT sign( + CREATE FUNCTION jwt_test() RETURNS public.jwt_token AS $$ + SELECT public.sign( row_to_json(r), 'reallyreallyreallyreallyverysafe' ) AS token FROM ( @@ -178,7 +174,7 @@ Next write a stored procedure that returns the token. The one below returns a to '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`. @@ -329,12 +325,10 @@ First we'll need a table to keep track of our users: 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:: plpgsql +.. code-block:: plpgsql create or replace function - basic_auth.check_role_exists() returns trigger - language plpgsql - as $$ + 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 = @@ -343,7 +337,7 @@ We would like the role to be a foreign key to actual database roles, however Pos 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 @@ -353,21 +347,19 @@ We would like the role to be a foreign key to actual database roles, however Pos Next we'll use the pgcrypto extension and a trigger to keep passwords safe in the :code:`users` table. -.. code:: plpgsql +.. code-block:: plpgsql create extension if not exists pgcrypto; create or replace function - basic_auth.encrypt_pass() returns trigger - language plpgsql - as $$ + 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 @@ -377,7 +369,7 @@ Next we'll use the pgcrypto extension and a trigger to keep passwords safe in th 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:: plpgsql +.. code-block:: plpgsql create or replace function basic_auth.user_role(email text, pass text) returns name @@ -404,12 +396,11 @@ 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:: plpgsql +.. code-block:: postgres + -- login should be on your exposed schema create or replace function - login(email text, pass text) returns basic_auth.jwt_token - language plpgsql - as $$ + login(email text, pass text) returns basic_auth.jwt_token as $$ declare _role name; result basic_auth.jwt_token; @@ -430,7 +421,7 @@ As described in `JWT from SQL`_, we'll create a JWT inside our login function. N into result; return result; end; - $$; + $$ language plpgsql; An API request to call this function would look like: From 9337f63823d9277cf76d389b4998e4f9bded5f3b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 11 Sep 2018 13:11:01 -0500 Subject: [PATCH 185/549] Fix #153, add systemd service file --- admin.rst | 62 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/admin.rst b/admin.rst index b47e93853e..934f2d5d3b 100644 --- a/admin.rst +++ b/admin.rst @@ -5,18 +5,18 @@ PostgREST is a fast way to construct a RESTful API. Its default behavior is grea The first step is to create an Nginx configuration file that proxies requests to an underlying PostgREST server. -.. code:: nginx +.. code-block:: nginx http { - ... + # ... # upstream configuration upstream postgrest { server localhost:3000; keepalive 64; } - ... + # ... server { - ... + # ... # expose to the outside world location /api/ { default_type application/json; @@ -26,7 +26,7 @@ The first step is to create an Nginx configuration file that proxies requests to proxy_http_version 1.1; proxy_pass http://postgrest/; } - ... + # ... } } @@ -37,13 +37,13 @@ Block Full-Table Operations Each table in the admin-selected schema gets exposed as a top level route. Client requests are executed by certain database roles depending on their authentication. All HTTP verbs are supported that correspond to actions permitted to the role. For instance if the active role can drop rows of the table then the DELETE verb is allowed for clients. Here's an API request to delete old rows from a hypothetical logs table: -.. code:: http +.. code-block:: http DELETE /logs?time=lt.1991-08-06 HTTP/1.1 However it's very easy to delete the **entire table** by omitting the query parameter! -.. code:: http +.. code-block:: http DELETE /logs HTTP/1.1 @@ -92,7 +92,7 @@ This is fine in small tables, but count performance degrades in big tables due t HTTPS ----- -See the :ref:`ssl` section of the authentication guide. +See the :ref:`https` section of the authentication guide. Rate Limiting ------------- @@ -201,6 +201,50 @@ Then run the `pg_listen `_ utility to mon Now, whenever the structure of the database schema changes, PostgreSQL will notify the ``ddl_command_end`` channel, which will cause ``pg_listen`` to send PostgREST the signal to reload its cache. +Daemonizing +=========== + +For linux distros 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://:@localhost:5432/" + db-schema = "" + db-anon-role = "" + db-pool = 10 + + server-host = "127.0.0.1" + server-port = 3000 + + jwt-secret = "" + +Then create the systemd service file in ``/etc/systemd/system/postgrest.service`` + +.. code-block:: ini + + [Unit] + Description=REST API for any Postgres database + After=postgresql.service + + [Service] + ExecStart=/bin/postgrest /etc/postgrest/config + ExecReload=/bin/kill -HUP $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 + Alternate URL Structure ======================= @@ -215,7 +259,7 @@ This allows compound primary keys and makes the intent for singular response ind 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:: nginx +.. code-block:: nginx # support /endpoint/:id url style location ~ ^/([a-z_]+)/([0-9]+) { From 8670ce6ffea246f6fa165feee748e57bda20a279 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 11 Sep 2018 13:17:45 -0500 Subject: [PATCH 186/549] Highlight 32 chars long and fix indentation --- install.rst | 46 +++++++++++++++++++++++----------------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/install.rst b/install.rst index ea6ab76f19..a2805432ea 100644 --- a/install.rst +++ b/install.rst @@ -146,28 +146,28 @@ 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" - ] - } + .. 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" + ] + } .. _jwt-secret: jwt-secret ---------- - The secret or `JSON Web Key (JWK) `_ used to decode JWT tokens clients provide for authentication. For security the key must be at least thirty-two 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`. + The secret or `JSON Web Key (JWK) `_ 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`. .. _jwt-aud: @@ -211,15 +211,15 @@ 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 + .. code:: bash - # {"postgrest":{"roles": ["other", "author"]}} - # the DSL accepts characters that are alphanumerical or one of "_$@" as keys - role-claim-key = ".postgrest.roles[1]" + # {"postgrest":{"roles": ["other", "author"]}} + # the DSL accepts characters that are alphanumerical or one of "_$@" as keys + 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) - role-claim-key = ".\"https://www.example.com/role\".key" + # {"https://www.example.com/role": { "key": "author }} + # non-alphanumerical characters can go inside quotes(escaped in the config value) + role-claim-key = ".\"https://www.example.com/role\".key" Running the Server ------------------ From 0ec963b295415390f0aa30666753265ddcef7221 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 11 Sep 2018 13:28:04 -0500 Subject: [PATCH 187/549] Fix #105, correct command for running postgrest --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index a2805432ea..36905b5cbb 100644 --- a/install.rst +++ b/install.rst @@ -45,7 +45,7 @@ The PostgREST server reads a configuration file to determine information about t .. code:: bash - postgrest /path/to/postgrest.conf + ./postgrest /path/to/postgrest.conf The file must contain a set of key value pairs. At minimum you must include these keys: From 83183ec8c6e7f9b2eed50025dd106adcfaf77486 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 11 Sep 2018 14:01:44 -0500 Subject: [PATCH 188/549] Fix #171, postgres logging in docker --- admin.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/admin.rst b/admin.rst index 934f2d5d3b..7c5a4978aa 100644 --- a/admin.rst +++ b/admin.rst @@ -164,6 +164,22 @@ Once you've verified that requests are as you expect, you can get more informati 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 + .. _schema_reloading: Schema Reloading From cf220c5a91128d1f94684ff020dd885571176234 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 12 Sep 2018 14:35:18 -0500 Subject: [PATCH 189/549] Fix #164, overloaded functions/named parameters --- api.rst | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/api.rst b/api.rst index d9344c790c..f20a15c75e 100644 --- a/api.rst +++ b/api.rst @@ -552,26 +552,36 @@ Stored Procedures Every stored procedure in the API-exposed database schema is accessible under the :code:`/rpc` prefix. The API endpoint supports POST (and in some cases GET) to execute the function. -.. code:: http +.. code-block:: http POST /rpc/function_name HTTP/1.1 Such functions can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). However procedures in PostgreSQL marked with :code:`stable` or :code:`immutable` `volatility `_ can only read, not modify, the database and PostgREST executes them in a read-only transaction compatible for read-replicas. Stable and immutable functions can be called with the HTTP GET verb if desired. -Procedures must be used with `named arguments `_. To supply arguments in an API call, include a JSON object in the request payload and each key/value of the object will become an argument. +To supply arguments in an API call, include a JSON object in the request payload and each key/value of the object will become an argument. For instance, assume we have created this function in the database. -.. code:: plpgsql +.. code-block:: plpgsql CREATE FUNCTION add_them(a integer, b integer) RETURNS integer AS $$ - SELECT $1 + $2; + SELECT a + b; $$ LANGUAGE SQL IMMUTABLE STRICT; +.. note:: + + Procedures must be declared with named parameters, procedures declared like: + + .. code-block:: plpgsql + + CREATE FUNCTION non_named_args(integer, text, integer) ... + + Can not be called with PostgREST, since we use `named notation `_ internally. + The client can call it by posting an object like -.. code:: http +.. code-block:: http POST /rpc/add_them HTTP/1.1 @@ -579,7 +589,7 @@ The client can call it by posting an object like Because ``add_them`` is declared IMMUTABLE, we can alternately call the function with a GET request: -.. code:: http +.. code-block:: http GET /rpc/add_them?a=1&b=2 HTTP/1.1 @@ -641,6 +651,25 @@ By default, a function is executed with the privileges of the user who calls it. Why the `/rpc` prefix? One reason is to avoid name collisions between views and procedures. It also helps emphasize to API consumers that these functions are not normal restful things. The functions can have arbitrary and surprising behavior, not the standard "post creates a resource" thing that users expect from the other routes. +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:: http + + GET /rpc/rental_duration?customer_id=232 HTTP/1.1 + +.. code-block:: http + + GET /rpc/rental_duration?customer_id=232&from_date=2018-07-01 HTTP/1.1 + Explicit Qualification ---------------------- From 7052446abfc2c89f2bb427f225f4a2a2182c0eaf Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 13 Sep 2018 11:47:50 -0500 Subject: [PATCH 190/549] Fix #160, reorder reloading note for functions --- api.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api.rst b/api.rst index f20a15c75e..b00821e6fe 100644 --- a/api.rst +++ b/api.rst @@ -569,6 +569,10 @@ For instance, assume we have created this function in the database. SELECT a + b; $$ LANGUAGE SQL IMMUTABLE STRICT; +.. important:: + + Whenever you create or change a function you must refresh PostgREST's schema. See the section :ref:`schema_reloading`. + .. note:: Procedures must be declared with named parameters, procedures declared like: @@ -633,10 +637,6 @@ PostgREST will detect if the function is scalar or table-valued and will shape t { "title": "Blade Runner 2049", "rating": 8.1} ] -.. important:: - - Whenever the function definition changes you must refresh PostgREST's schema for this to work properly. See the section :ref:`schema_reloading`. - A function response can be shaped using the same filters as the ones used for tables and views: .. code:: http From efb39e5e122e5402f8e6dfd23397d243d8dade77 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 13 Sep 2018 12:32:06 -0500 Subject: [PATCH 191/549] Add SIGHUP deprecation notice --- admin.rst | 14 +++++++++----- postgrest.dict | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/admin.rst b/admin.rst index 7c5a4978aa..79585a3958 100644 --- a/admin.rst +++ b/admin.rst @@ -187,11 +187,11 @@ Schema Reloading Users are often confused by PostgREST's database schema cache. It is present because detecting foreign key relationships between tables (including how those relationships pass through views) is necessary, but costly. API requests consult the schema cache as part of :ref:`resource_embedding`. However if the schema changes while the server is running it results in a stale cache and leads to errors claiming that no relations are detected between tables. -To refresh the cache without restarting the PostgREST server, send the server process a SIGHUP signal: +To refresh the cache without restarting the PostgREST server, send the server process a SIGUSR1 signal: .. code:: bash - killall -HUP postgrest + killall -SIGUSR1 postgrest The above is the manual way to do it. To automate the schema reloads, use a database trigger like this: @@ -209,14 +209,18 @@ The above is the manual way to do it. To automate the schema reloads, use a data CREATE EVENT TRIGGER ddl_postgrest ON ddl_command_end EXECUTE PROCEDURE public.notify_ddl_postgrest(); -Then run the `pg_listen `_ utility to monitor for that event and send a SIGHUP when it occurs: +Then run the `pg_listen `_ utility to monitor for that event and send a SIGUSR1 when it occurs: .. code-block:: bash - pg_listen ddl_command_end "killall -HUP postgrest" + pg_listen ddl_command_end "killall -SIGUSR1 postgrest" Now, whenever the structure of the database schema changes, PostgreSQL will notify the ``ddl_command_end`` channel, which will cause ``pg_listen`` to send PostgREST the signal to reload its cache. +.. important:: + + As of PostgREST v5.1 reloading with SIGHUP is deprecated, it's still supported but will be removed in v6.0 + Daemonizing =========== @@ -246,7 +250,7 @@ Then create the systemd service file in ``/etc/systemd/system/postgrest.service` [Service] ExecStart=/bin/postgrest /etc/postgrest/config - ExecReload=/bin/kill -HUP $MAINPID + ExecReload=/bin/kill -SIGUSR1 $MAINPID [Install] WantedBy=multi-user.target diff --git a/postgrest.dict b/postgrest.dict index 83af75b114..563e11994a 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -46,6 +46,7 @@ RabbitMQ RestSharp SHA SIGHUP +SIGUSR1 SNS SQL SSL From d0c0312e35df809c28f4a1b0f3def87a4e4fa791 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 13 Sep 2018 12:41:55 -0500 Subject: [PATCH 192/549] Change server-host default --- install.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/install.rst b/install.rst index 36905b5cbb..7e09df3f33 100644 --- a/install.rst +++ b/install.rst @@ -68,14 +68,14 @@ The user specified in the db-uri is also known as the authenticator role. For mo Here is the full list of configuration parameters. -================ ====== ======= ======== -Name Type Default Required -================ ====== ======= ======== -db-uri String Y -db-schema String Y -db-anon-role String Y +================ ====== ========= ======== +Name Type Default Required +================ ====== ========= ======== +db-uri String Y +db-schema String Y +db-anon-role String Y db-pool Int 10 -server-host String \*4 +server-host String 127.0.0.1 server-port Int 3000 server-proxy-uri String jwt-secret String @@ -85,7 +85,7 @@ max-rows Int ∞ pre-request String app.settings.* String role-claim-key String .role -================ ====== ======= ======== +================ ====== ========= ======== .. _db-uri: From d5365b6ead2ffabf0b8ed98fb7d730317999474f Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 15 Sep 2018 13:24:33 -0500 Subject: [PATCH 193/549] Reorder stored procedures section --- api.rst | 90 +++++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/api.rst b/api.rst index b00821e6fe..9099dcc30d 100644 --- a/api.rst +++ b/api.rst @@ -556,7 +556,7 @@ Every stored procedure in the API-exposed database schema is accessible under th POST /rpc/function_name HTTP/1.1 -Such functions can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). However procedures in PostgreSQL marked with :code:`stable` or :code:`immutable` `volatility `_ can only read, not modify, the database and PostgREST executes them in a read-only transaction compatible for read-replicas. Stable and immutable functions can be called with the HTTP GET verb if desired. +Such functions can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). To supply arguments in an API call, include a JSON object in the request payload and each key/value of the object will become an argument. @@ -573,31 +573,49 @@ For instance, assume we have created this function in the database. Whenever you create or change a function you must refresh PostgREST's schema. See the section :ref:`schema_reloading`. -.. note:: +The client can call it by posting an object like - Procedures must be declared with named parameters, procedures declared like: +.. code-block:: http - .. code-block:: plpgsql + POST /rpc/add_them HTTP/1.1 - CREATE FUNCTION non_named_args(integer, text, integer) ... + { "a": 1, "b": 2 } - Can not be called with PostgREST, since we use `named notation `_ internally. + 3 -The client can call it by posting an object like +You can also call a function that takes a single parameter of type json by sending the header :code:`Prefer: params=single-object` with your request. That way the JSON request body will be used as the single argument. + +.. code-block:: plpgsql + + CREATE FUNCTION mult_them(param json) RETURNS int AS $$ + SELECT (param->>'x')::int * (param->>'y')::int + $$ LANGUAGE SQL; .. code-block:: http - POST /rpc/add_them HTTP/1.1 + POST /rpc/mult_them HTTP/1.1 + Prefer: params=single-object - { "a": 1, "b": 2 } + { "x": 4, "y": 2 } -Because ``add_them`` is declared IMMUTABLE, we can alternately call the function with a GET request: + 8 -.. code-block:: http - GET /rpc/add_them?a=1&b=2 HTTP/1.1 +Procedures must be declared with named parameters, procedures declared like: + +.. code-block:: plpgsql + + CREATE FUNCTION non_named_args(integer, text, integer) ... + +Can not be called with PostgREST, since we use `named notation `_ internally. + +Note that PostgreSQL converts identifier names to lowercase unless you quote them like: -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``. Note that PostgreSQL converts parameter names to lowercase unless you quote them like :sql:`CREATE FUNCTION foo("mixedCase" text) ...`. You can also call a function that takes a single parameter of type json by sending the header :code:`Prefer: params=single-object` with your request. That way the JSON request body will be used as the single argument. +.. code-block:: postgres + + CREATE FUNCTION "someFunc"("someParam" text) ... + +PostgreSQL has four procedural languages that are part of the core distribution: PL/pgSQL, PL/Tcl, PL/Perl, and PL/Python. There are many other procedural languages distributed as additional extensions. Also, plain SQL can be used to write functions (as shown in the example above). .. note:: @@ -619,15 +637,35 @@ The function parameter names match the JSON object keys in the POST case, for th Starting from PostgreSQL 10, a json array from the client gets mapped normally to a PostgreSQL native array. +.. note:: + + Why the `/rpc` prefix? One reason is to avoid name collisions between views and procedures. It also helps emphasize to API consumers that these functions are not normal restful things. The functions can have arbitrary and surprising behavior, not the standard "post creates a resource" thing that users expect from the other routes. + +Immutable and stable functions +------------------------------ + +Procedures in PostgreSQL marked with :code:`stable` or :code:`immutable` `volatility `_ can only read, not modify, the database and PostgREST executes them in a read-only transaction compatible for read-replicas. Stable and immutable functions can be called with the HTTP GET verb if desired. + +Because ``add_them`` is declared IMMUTABLE, we can alternately call the function with a GET request: + +.. code-block:: http + + GET /rpc/add_them?a=1&b=2 HTTP/1.1 + +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``. + +Scalar functions +---------------- + PostgREST will detect if the function is scalar or table-valued and will shape the response format accordingly: -.. code:: http +.. code-block:: http GET /rpc/add_them?a=1&b=2 HTTP/1.1 3 -.. code:: http +.. code-block:: http GET /rpc/best_films_2017 HTTP/1.1 @@ -637,19 +675,27 @@ PostgREST will detect if the function is scalar or table-valued and will shape t { "title": "Blade Runner 2049", "rating": 8.1} ] -A function response can be shaped using the same filters as the ones used for tables and views: +Function filters +---------------- -.. code:: http +A function that returns a table type response can be shaped using the same filters as the ones used for tables and views: - GET /rpc/top_rated_films?select=title,director:directors(*)&year=eq.1990&order=title.desc HTTP/1.1 +.. code-block:: postgres -PostgreSQL has four procedural languages that are part of the core distribution: PL/pgSQL, PL/Tcl, PL/Perl, and PL/Python. There are many other procedural languages distributed as additional extensions. Also, plain SQL can be used to write functions (as shown in the example above). + CREATE FUNCTION best_films_2017() RETURNS SETOF films .. -By default, 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. 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. See `PostgreSQL documentation `_ for more details. +.. code-block:: http -.. note:: + GET /rpc/best_films_2017?select=title,director:directors(*) HTTP/1.1 - Why the `/rpc` prefix? One reason is to avoid name collisions between views and procedures. It also helps emphasize to API consumers that these functions are not normal restful things. The functions can have arbitrary and surprising behavior, not the standard "post creates a resource" thing that users expect from the other routes. +.. code-block:: http + + GET /rpc/best_films_2017?rating=gt.8&order=title.desc HTTP/1.1 + +Function privileges +------------------- + +By default, 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. 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. See `PostgreSQL documentation `_ for more details. Overloaded functions -------------------- From 7cff51c002854263a71b83ffba476895000b85fb Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 15 Sep 2018 14:17:28 -0500 Subject: [PATCH 194/549] Fix some code highlighting and add computed col ref --- api.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/api.rst b/api.rst index 9099dcc30d..705cfc31e6 100644 --- a/api.rst +++ b/api.rst @@ -221,7 +221,7 @@ As mentioned, computed columns do not appear in the output by default. However y .. important:: - Computed columns must be created under the exposed schema to be used in this way. + Computed columns must be created under the :ref:`exposed schema ` to be used in this way. Ordering -------- @@ -378,30 +378,30 @@ Binary output If you want to return raw binary data from a :code:`bytea` column, you must specify :code:`application/octet-stream` as part of the :code:`Accept` header and select a single column :code:`?select=bin_data`. -.. code:: http +.. code-block:: http GET /items?select=bin_data&id=eq.1 HTTP/1.1 Accept: application/octet-stream -You can also request binary output when calling stored procedures and since they can return a scalar value you are not forced to use :code:`select` +You can also request binary output when calling `Stored Procedures`_ and since they can return a scalar value you are not forced to use :code:`select` for this case. -.. code:: sql +.. code-block:: postgres CREATE FUNCTION closest_point(..) RETURNS bytea .. -.. code:: http +.. code-block:: http POST /rpc/closest_point HTTP/1.1 Accept: application/octet-stream If the stored procedure returns non-scalar values, you need to do a :code:`select` in the same way as for GET binary output. -.. code:: sql +.. code-block:: sql CREATE FUNCTION overlapping_regions(..) RETURNS SETOF TABLE(geom_twkb bytea, ..) .. -.. code:: http +.. code-block:: http POST /rpc/overlapping_regions?select=geom_twkb HTTP/1.1 Accept: application/octet-stream @@ -502,8 +502,8 @@ PostgREST can also detect relations going through join tables. Thus you can requ Whenever foreign key relations change in the database schema you must refresh PostgREST's schema cache to allow resource embedding to work properly. See the section :ref:`schema_reloading`. -Embedded Operations -------------------- +Embedded Filters +---------------- Embedded resources can be shaped similarly to their top-level counterparts. To do so, prefix the query parameters with the name of the embedded resource. For instance, to order the actors in each film: @@ -832,7 +832,7 @@ All tables and `auto-updatable views Date: Tue, 18 Sep 2018 13:28:25 -0500 Subject: [PATCH 195/549] Fix #104, default privileges on functions --- api.rst | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 705cfc31e6..3c941b20a0 100644 --- a/api.rst +++ b/api.rst @@ -695,7 +695,26 @@ A function that returns a table type response can be shaped using the same filte Function privileges ------------------- -By default, 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. 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. See `PostgreSQL documentation `_ for more details. +By default, 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. + +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. See `PostgreSQL documentation `_ for more details. + +.. warning:: + + Unlike tables/views, functions privileges work as a blacklist, so they're executable for all the roles by default. You can workaround this by revoking the PUBLIC privileges of the function and then granting privileges to specific roles: + + .. code-block:: postgres + + REVOKE ALL PRIVILEGES ON FUNCTION private_func() FROM PUBLIC; + GRANT EXECUTE ON FUNCTION private_func() TO a_role; + + Also to avoid doing ``REVOKE`` on every function you can enable this behavior by default with: + + .. code-block:: postgres + + ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; + + See `PostgreSQL alter default privileges `_ for more details. Overloaded functions -------------------- From a1d4b3ed3475ba40c50fe8f112a0eb9d2178a0e3 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 19 Sep 2018 13:34:28 -0500 Subject: [PATCH 196/549] Add #fff background for snippets --- _static/css/custom.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/_static/css/custom.css b/_static/css/custom.css index ee869c6da7..75207887ce 100644 --- a/_static/css/custom.css +++ b/_static/css/custom.css @@ -1,3 +1,7 @@ div.wy-menu.rst-pro { display: none !important; } + +div.highlight { + background: #fff !important; +} From 518b465ffafea319e09eaac327d69b364f5d19f7 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 20 Sep 2018 10:24:22 -0500 Subject: [PATCH 197/549] Fix highlighting and move running the server header --- api.rst | 23 ++++++++++------------- install.rst | 2 +- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/api.rst b/api.rst index 3c941b20a0..dac2e18b55 100644 --- a/api.rst +++ b/api.rst @@ -103,22 +103,19 @@ The view will provide a new endpoint: 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 `_. The follow examples illustrate the possibilities. +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 `_. The following examples illustrate the possibilities. .. code-block:: http - # Use language in fts query - GET /tsearch?my_tsv=fts(french).amusant + GET /tsearch?my_tsv=fts(french).amusant HTTP/1.1 - # Use plainto_tsquery and phraseto_tsquery - GET /tsearch?my_tsv=plfts.The%20Fat%20Cats - GET /tsearch?my_tsv=phfts.The%20Fat%20Rats +.. code-block:: http + + GET /tsearch?my_tsv=plfts.The%20Fat%20Cats HTTP/1.1 - # Combine both - GET /tsearch?my_tsv=phfts(english).The%20Fat%20Cats +.. code-block:: http - # "not" also working - GET /tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats + GET /tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats HTTP/1.1 Using phrase search mode requires PostgreSQL of version at least 9.6 and will raise an error in earlier versions of the database. @@ -346,7 +343,7 @@ By default PostgREST returns all JSON results in an array, even when there is on 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:: http +.. code-block:: http GET /items?id=eq.1 HTTP/1.1 Accept: application/vnd.pgrst.object+json @@ -621,7 +618,7 @@ PostgreSQL has four procedural languages that are part of the core distribution: For versions prior to PostgreSQL 10, to pass a PostgreSQL native array you need to quote it as a string: - .. code:: http + .. code-block:: http POST /rpc/native_array_func HTTP/1.1 @@ -629,7 +626,7 @@ PostgreSQL has four procedural languages that are part of the core distribution: In these versions we recommend using function arguments of type json to accept arrays from the client: - .. code:: http + .. code-block:: http POST /rpc/json_array_func HTTP/1.1 diff --git a/install.rst b/install.rst index 7e09df3f33..3b7a23fd05 100644 --- a/install.rst +++ b/install.rst @@ -222,7 +222,7 @@ role-claim-key role-claim-key = ".\"https://www.example.com/role\".key" Running the Server ------------------- +================== PostgREST outputs basic request logging to stdout. 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: From 73e59a1643a7962c67aefbb23b2153c3f89c8381 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 21 Sep 2018 13:46:53 -0500 Subject: [PATCH 198/549] Add pipenv instructions --- .gitignore | 1 + Pipfile | 14 ++++++++++++++ README.md | 27 ++++++++------------------- default.nix | 3 +-- reload_docs.py => livereload_docs.py | 3 +++ 5 files changed, 27 insertions(+), 21 deletions(-) create mode 100644 Pipfile rename reload_docs.py => livereload_docs.py (68%) diff --git a/.gitignore b/.gitignore index e35d8850c9..bf70826eac 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ _build +Pipfile.lock diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000000..52ea333a4d --- /dev/null +++ b/Pipfile @@ -0,0 +1,14 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +sphinx = "*" +sphinx-rtd-theme = "*" +livereload = "*" + +[dev-packages] + +[requires] +python_version = "3.6" diff --git a/README.md b/README.md index c8c5fb53d8..5e705d82bf 100644 --- a/README.md +++ b/README.md @@ -2,28 +2,17 @@ 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 generate HTML version: +You can use [pipenv](https://pipenv.readthedocs.io) to build the docs locally: -1. Install Sphinx from the [sphinx website](http://www.sphinx-doc.org/en/stable/install.html) -2. Clone this repository -3. Generate HTML - ```bash - cd postgrest-docs - sphinx-build -b html -a -n . _build - - # open _build/index.html in your browser - ``` +```bash + pipenv install + pipenv run python livereload_docs.py +``` -If you use [nix](https://nixos.org/nix/), you can just run: +Or if you use [nix](https://nixos.org/nix/), you can just run: -``` +```bash nix-shell ``` -This will build the docs and start a livereload server on `http://localhost:5500`. - ---- - -**Sphinx Installation Notes:** - -* If you're on OSX you might want to install the Python from homebrew - then a simple `pip install sphinx` does the trick. +Both of these options will build the docs and start a livereload server on `http://localhost:5500`. diff --git a/default.nix b/default.nix index 6d32f96097..c70878de80 100644 --- a/default.nix +++ b/default.nix @@ -8,7 +8,6 @@ stdenv.mkDerivation { python36Packages.sphinx_rtd_theme python36Packages.livereload ]; shellHook = '' - sphinx-build -b html -a -n . _build - python reload_docs.py && exit + python livereload_docs.py && exit ''; } diff --git a/reload_docs.py b/livereload_docs.py similarity index 68% rename from reload_docs.py rename to livereload_docs.py index 4ec06803f3..9983589aa5 100755 --- a/reload_docs.py +++ b/livereload_docs.py @@ -1,5 +1,8 @@ #!/usr/bin/env python from livereload import Server, shell +from subprocess import call +## Build docs at startup +call(['sphinx-build', '-b', 'html', '-a', '-n', '.', '_build']) server = Server() server.watch('*.rst', shell('sphinx-build -b html -a -n . _build')) server.watch('tutorials/*.rst', shell('sphinx-build -b html -a -n . _build')) From dc9a2858252cc94dc04007cd93edbbfec0951951 Mon Sep 17 00:00:00 2001 From: Kyle Johnson <1007162+kyle-johnson@users.noreply.github.com> Date: Sun, 23 Sep 2018 03:08:24 -0700 Subject: [PATCH 199/549] Note Postgres' (current) RLS limitations for views --- api.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api.rst b/api.rst index dac2e18b55..d0acc499fa 100644 --- a/api.rst +++ b/api.rst @@ -98,6 +98,10 @@ The view will provide a new endpoint: GET /fresh_stories HTTP/1.1 +.. important:: + + Views bypass all row-level security features and are invoked as the role which created the view, much like stored procedures with the "SECURITY DEFINER" option. + .. _fts: Full-Text Search From 87520699343a927a6d8619cb8b2d40fa415d236f Mon Sep 17 00:00:00 2001 From: Kyle Johnson <1007162+kyle-johnson@users.noreply.github.com> Date: Sun, 23 Sep 2018 11:38:34 -0700 Subject: [PATCH 200/549] Tweak view RLS warning --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index d0acc499fa..08110d422d 100644 --- a/api.rst +++ b/api.rst @@ -100,7 +100,7 @@ The view will provide a new endpoint: .. important:: - Views bypass all row-level security features and are invoked as the role which created the view, much like stored procedures with the "SECURITY DEFINER" option. + 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 will be bypassed unless a different owner is specified. .. _fts: From f2e81062a0166728d07aba29032122e9fc72b8c4 Mon Sep 17 00:00:00 2001 From: Kyle Johnson <1007162+kyle-johnson@users.noreply.github.com> Date: Sun, 23 Sep 2018 11:42:38 -0700 Subject: [PATCH 201/549] Further clarify view RLS interaction --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 08110d422d..cac1eea1ed 100644 --- a/api.rst +++ b/api.rst @@ -100,7 +100,7 @@ The view will provide a new endpoint: .. important:: - 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 will be bypassed unless a different owner is specified. + 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 will be bypassed unless a different, non-SUPERUSER owner is specified. .. _fts: From 89c62aa7e38481004dfd5c8a9a0a5e2457e5ef05 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 24 Sep 2018 09:09:24 -0500 Subject: [PATCH 202/549] Add a workaround for RLS in views --- api.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index cac1eea1ed..6d27c0b86d 100644 --- a/api.rst +++ b/api.rst @@ -100,7 +100,15 @@ The view will provide a new endpoint: .. important:: - 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 will be bypassed unless a different, non-SUPERUSER owner is specified. + 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 `_ will be bypassed unless a different, non-SUPERUSER owner is specified. + + .. code-block:: postgres + + -- Workaround: + -- non-SUPERUSER role to be used as the owner of the views + CREATE ROLE api_views_owner; + -- alter the view owner so RLS can work normally + ALTER VIEW sample_view OWNER TO api_views_owner; .. _fts: From 215bc80ff128caa091ad3b1007e59483fd5efddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20Ch=C3=A1vez?= Date: Sun, 21 Oct 2018 17:54:21 -0500 Subject: [PATCH 203/549] Move intro to index and add shields (#184) * Add supporting development section --- _static/css/custom.css | 4 + index.rst | 196 +++++++++++++++++++++++++++++++++++++++-- intro.rst | 155 -------------------------------- 3 files changed, 194 insertions(+), 161 deletions(-) delete mode 100644 intro.rst diff --git a/_static/css/custom.css b/_static/css/custom.css index 75207887ce..588d99d4cf 100644 --- a/_static/css/custom.css +++ b/_static/css/custom.css @@ -5,3 +5,7 @@ div.wy-menu.rst-pro { div.highlight { background: #fff !important; } + +div.line-block { + margin-bottom: 0px !important; +} diff --git a/index.rst b/index.rst index c59042d724..32e5a42248 100644 --- a/index.rst +++ b/index.rst @@ -1,37 +1,221 @@ .. title:: PostgREST Documentation -.. image:: _static/logo.png +.. figure:: _static/logo.png -.. toctree:: - :maxdepth: 2 +.. image:: https://img.shields.io/github/stars/postgrest/postgrest.svg?style=social + :target: https://github.com/PostgREST/postgrest -.. toctree:: - :caption: What is PostgREST? +.. image:: https://img.shields.io/github/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.me/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. + +Motivation +---------- + +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 db 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. - intro.rst +Embracing the Relational Model +------------------------------ + +In 1970 E. F. Codd criticized the then-dominant hierarchical model of databases in his article A Relational Model of Data for Large Shared Data Banks. Reading the article reveals a striking similarity between hierarchical databases and nested http routes. With PostgREST we attempt to use flexible filtering and embedding rather than nested routes. + +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. + +Shared Improvements +------------------- + +As with any open source project, we all gain from features and fixes in the tool. It's more beneficial than improvements locked inextricably within custom code-bases. + +Getting Support +---------------- + +The project has a friendly and growing community. Join our `chat room `_ for discussion and help. You can also report or search for bugs/features on the Github `issues `_ page. + +Supporting development +---------------------- + +You can help PostgREST ongoing maintenance and development by: + +- Making a regular donation through `Patreon `_ + +- Alternatively, you can make a one-time donation via `Paypal `_ + +Every donation will be spent on making PostgREST better for the whole community. .. toctree:: :caption: Tutorials + :titlesonly: tutorials/tut0.rst tutorials/tut1.rst .. toctree:: :caption: Installation + :titlesonly: install.rst .. toctree:: :caption: API + :titlesonly: api.rst .. toctree:: :caption: Authentication + :titlesonly: auth.rst .. toctree:: :caption: Administration + :titlesonly: admin.rst + +Ecosystem +--------- + +PostgREST has a growing ecosystem of examples, and libraries, experiments, and users. Here is a selection. + +Example Apps +------------ + +* `subzerocloud/postgrest-starter-kit `_ - Boilerplate for new project +* `NikolayS/postgrest-google-translate `_ - Calling to external translation service +* `CodeforAustralia/heritage-near-me `_ - Elm and PostgREST with PostGIS +* `timwis/handsontable-postgrest `_ - An excel-like database table editor +* `Recmo/PostgrestSkeleton `_ - Docker Compose, PostgREST, Nginx and Auth0 +* `benoror/ember-postgrest-dynamic-ui `_ - generating Ember forms to edit data +* `ruslantalpa/blogdemo `_ - blog api demo in a vagrant image +* `timwis/ext-postgrest-crud `_ - browser-based spreadsheet +* `srid/chronicle `_ - tracking a tree of personal memories +* `diogob/elm-workshop `_ - building a simple database query UI +* `marmelab/ng-admin-postgrest `_ - automatic database admin panel +* `myfreeweb/moneylog `_ - accounting web app in Polymer + PostgREST +* `tyrchen/goodfilm `_ - example film api +* `begriffs/postgrest-example `_ - sqitch versioning for API +* `SMRxT/postgrest-demo `_ - multi-tenant logging system +* `PierreRochard/postgrest-boilerplate `_ - example auth back-end + + +.. _clientside_libraries: + +Client-Side Libraries +--------------------- + +* `tomberek/aor-postgrest-client `_ - JS, admin-on-rest +* `hugomrdias/postgrest-url `_ - JS, just for generating query URLs +* `john-kelly/elm-postgrest `_ - Elm +* `mithril.postgrest `_ - JS, Mithril +* `lewisjared/postgrest-request `_ - JS, SuperAgent +* `JarvusInnovations/jarvus-postgrest-apikit `_ - JS, Sencha framework +* `davidthewatson/postgrest_python_requests_client `_ - Python +* `calebmer/postgrest-client `_ - JS +* `clesiemo3/postgrestR `_ - R +* `PierreRochard/postgrest-angular `_ - TypeScript, generate UI from API description +* `thejettdurham/postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp +* `team142/ng-postgrest `_ - Angular app for browsing, editing data exposed over Postgrest. + +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. + +* `diogob/postgres-websockets `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY +* `frafra/postgresql2websocket `_ - Websockets +* `matthewmueller/pg-bridge `_ - Amazon SNS +* `aweber/pgsql-listen-exchange `_ - RabbitMQ +* `SpiderOak/skeeter `_ - ZeroMQ +* `FGRibreau/postgresql-to-amqp `_ - AMQP +* `daurnimator/pg-kinesis-bridge `_ - Amazon Kinesis + +Extensions +---------- + +* `pg-safeupdate `_ - Prevent full-table updates or deletes +* `srid/spas `_ - allow file uploads and basic auth +* `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server +* `wildsurfer/postgrest-oauth-server `_ - OAuth2 server +* `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware +* `criles25/postgrest-auth `_ - email based auth/signup +* `ppKrauss/PostgREST-writeAPI `_ - generate Nginx rewrite rules to fit an OpenAPI spec + +Commercial +--------------- + +* `subZero `_ - Automated GraphQL & REST API with built-in caching (powered in part by PostgREST) + +In Production +------------- + +* `triggerFS - A realtime messaging and distributed trigger system `_ +* `Moat `_ +* `Catarse `_ +* `Redsmin `_ +* `Image-charts `_ +* `MotionDynamic - Fast highly dynamic video generation at scale `_ +* `Drip Depot `_ +* `OpenBooking `_ +* `Convene `_ by Thomson-Reuters +* `eGull `_ +* `Elyios `_ + +Testimonials +------------ + + "It's so fast to develop, it feels like cheating!" + + -- François-G. 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 diff --git a/intro.rst b/intro.rst deleted file mode 100644 index 20ac027398..0000000000 --- a/intro.rst +++ /dev/null @@ -1,155 +0,0 @@ -Motivation -########## - -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. - -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 db 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. - -Embracing the Relational Model ------------------------------- - -In 1970 E. F. Codd criticized the then-dominant hierarchical model of databases in his article A Relational Model of Data for Large Shared Data Banks. Reading the article reveals a striking similarity between hierarchical databases and nested http routes. With PostgREST we attempt to use flexible filtering and embedding rather than nested routes. - -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. - -Shared Improvements -------------------- - -As with any open source project, we all gain from features and fixes in the tool. It's more beneficial than improvements locked inextricably within custom code-bases. - -Ecosystem -######### - -PostgREST has a growing ecosystem of examples, and libraries, experiments, and users. Here is a selection. - -.. _clientside_libraries: - -Client-Side Libraries ---------------------- - -* `tomberek/aor-postgrest-client `_ - JS, admin-on-rest -* `hugomrdias/postgrest-url `_ - JS, just for generating query URLs -* `john-kelly/elm-postgrest `_ - Elm -* `mithril.postgrest `_ - JS, Mithril -* `lewisjared/postgrest-request `_ - JS, SuperAgent -* `JarvusInnovations/jarvus-postgrest-apikit `_ - JS, Sencha framework -* `davidthewatson/postgrest_python_requests_client `_ - Python -* `calebmer/postgrest-client `_ - JS -* `clesiemo3/postgrestR `_ - R -* `PierreRochard/postgrest-angular `_ - TypeScript, generate UI from API description -* `thejettdurham/postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp -* `team142/ng-postgrest `_ - Angular app for browsing, editing data exposed over Postgrest. - -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. - -* `diogob/postgres-websockets `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY -* `frafra/postgresql2websocket `_ - Websockets -* `matthewmueller/pg-bridge `_ - Amazon SNS -* `aweber/pgsql-listen-exchange `_ - RabbitMQ -* `SpiderOak/skeeter `_ - ZeroMQ -* `FGRibreau/postgresql-to-amqp `_ - AMQP -* `daurnimator/pg-kinesis-bridge `_ - Amazon Kinesis - -Example Apps ------------- - -* `subzerocloud/postgrest-starter-kit `_ - Boilerplate for new project -* `NikolayS/postgrest-google-translate `_ - Calling to external translation service -* `CodeforAustralia/heritage-near-me `_ - Elm and PostgREST with PostGIS -* `timwis/handsontable-postgrest `_ - An excel-like database table editor -* `Recmo/PostgrestSkeleton `_ - Docker Compose, PostgREST, Nginx and Auth0 -* `benoror/ember-postgrest-dynamic-ui `_ - generating Ember forms to edit data -* `ruslantalpa/blogdemo `_ - blog api demo in a vagrant image -* `timwis/ext-postgrest-crud `_ - browser-based spreadsheet -* `srid/chronicle `_ - tracking a tree of personal memories -* `diogob/elm-workshop `_ - building a simple database query UI -* `marmelab/ng-admin-postgrest `_ - automatic database admin panel -* `myfreeweb/moneylog `_ - accounting web app in Polymer + PostgREST -* `tyrchen/goodfilm `_ - example film api -* `begriffs/postgrest-example `_ - sqitch versioning for API -* `SMRxT/postgrest-demo `_ - multi-tenant logging system -* `PierreRochard/postgrest-boilerplate `_ - example auth back-end - -In Production -------------- - -* `triggerFS - A realtime messaging and distributed trigger system `_ -* `Moat `_ -* `Catarse `_ -* `Redsmin `_ -* `Image-charts `_ -* `MotionDynamic - Fast highly dynamic video generation at scale `_ -* `Drip Depot `_ -* `OpenBooking `_ -* `Convene `_ by Thomson-Reuters -* `eGull `_ -* `Elyios `_ - -Extensions ----------- - -* `pg-safeupdate `_ - Prevent full-table updates or deletes -* `srid/spas `_ - allow file uploads and basic auth -* `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server -* `wildsurfer/postgrest-oauth-server `_ - OAuth2 server -* `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware -* `criles25/postgrest-auth `_ - email based auth/signup -* `ppKrauss/PostgREST-writeAPI `_ - generate Nginx rewrite rules to fit an OpenAPI spec - -Commercial ---------------- - -* `subZero `_ - Automated GraphQL & REST API with built-in caching (powered in part by PostgREST) - -Testimonials -############ - - "It's so fast to develop, it feels like cheating!" - - -- François-G. 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 - -Getting Support -################ - -The project has a friendly and growing community. Join our `chat room `_ for discussion and help. You can also report or search for bugs/features on the Github `issues `_ page. From 9475f53fd73da8e1c1a4816292e0308ad273e6c0 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 8 Nov 2018 10:42:51 -0500 Subject: [PATCH 204/549] Clarify important note about SIGHUP --- admin.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin.rst b/admin.rst index 79585a3958..f8bfd1a958 100644 --- a/admin.rst +++ b/admin.rst @@ -219,7 +219,7 @@ Now, whenever the structure of the database schema changes, PostgreSQL will noti .. important:: - As of PostgREST v5.1 reloading with SIGHUP is deprecated, it's still supported but will be removed in v6.0 + As of PostgREST v5.1 reloading with SIGHUP is deprecated, it's still supported but will be removed in v6.0. SIGUSR1 should be used instead. Daemonizing =========== From 6d1adaaa54c47029624f5d3a707eef5d4dd4c960 Mon Sep 17 00:00:00 2001 From: Russell Davies Date: Wed, 14 Nov 2018 13:09:16 +0000 Subject: [PATCH 205/549] Clarify config and auth sections on JWKS. See PostgREST/postgrest#1205. --- auth.rst | 6 +++++- install.rst | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/auth.rst b/auth.rst index ee254594a4..a0ddc2ee58 100644 --- a/auth.rst +++ b/auth.rst @@ -239,7 +239,7 @@ Our code requires a database role in the JWT. To add it you need to save the dat Asymmetric Keys ~~~~~~~~~~~~~~~ -As described in the :ref:`configuration` section, PostgREST accepts a ``jwt-secret`` config file parameter. If it is set to a simple string value like "reallyreallyreallyreallyverysafe" then PostgREST interprets it as an HMAC-SHA256 passphrase. However you can also specify a literal JWT key JSON value. For example, you can use an RSA-256 public key such as: +As described in the :ref:`configuration` section, PostgREST accepts a ``jwt-secret`` config file parameter. If it is set to a simple string value like "reallyreallyreallyreallyverysafe" then PostgREST interprets it as an HMAC-SHA256 passphrase. 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 @@ -251,6 +251,10 @@ As described in the :ref:`configuration` section, PostgREST accepts a ``jwt-secr "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 diff --git a/install.rst b/install.rst index 3b7a23fd05..e641340f46 100644 --- a/install.rst +++ b/install.rst @@ -167,7 +167,7 @@ server-proxy-uri jwt-secret ---------- - The secret or `JSON Web Key (JWK) `_ 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`. + The secret or `JSON Web Key (JWK) (or set) `_ 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`. .. _jwt-aud: From eb81bbc464b14fb0a71bd6b12ece3884a1a4136d Mon Sep 17 00:00:00 2001 From: Lee Johnson Date: Fri, 16 Nov 2018 21:50:39 -0500 Subject: [PATCH 206/549] Update to include Heroku deploy steps --- install.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/install.rst b/install.rst index e641340f46..ee1c05f899 100644 --- a/install.rst +++ b/install.rst @@ -336,6 +336,19 @@ If you want to have a visual overview of your API in your browser you can add sw With this you can see the swagger-ui in your browser on port 8080. +Deploying to Heroku +=================== +Assuming your making modifications locally and then pushing to GitHub, it's easy to deploy to Heroku. + +1. Create a new app on Heroku +2. In Settings add the following buildpack :code:`https://github.com/PostgREST/postgrest-heroku` +3. Add the require Config Vars in Heroku (see above) +4. Modify your postgres.conf file as required to match your Config Vars in Heroku +5. Create your :code:`Procfile` and add :code:`./env-to-config ./postgrest postgres.conf` +6. Push your changes to GitHub +7. Set Heroku to automatically deploy from Master and then manually deploy the branch for the first build + + .. _build_source: Build from Source From fed4c8c9af4aed652f94e8e7c7fea6cb808d2154 Mon Sep 17 00:00:00 2001 From: Lee Johnson Date: Thu, 22 Nov 2018 14:19:34 -0500 Subject: [PATCH 207/549] Added link to variables settings --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index ee1c05f899..56545bda0f 100644 --- a/install.rst +++ b/install.rst @@ -342,7 +342,7 @@ Assuming your making modifications locally and then pushing to GitHub, it's easy 1. Create a new app on Heroku 2. In Settings add the following buildpack :code:`https://github.com/PostgREST/postgrest-heroku` -3. Add the require Config Vars in Heroku (see above) +3. Add the require Config Vars in Heroku (see https://github.com/PostgREST/postgrest/blob/master/app.json#L7-L57 for more details) 4. Modify your postgres.conf file as required to match your Config Vars in Heroku 5. Create your :code:`Procfile` and add :code:`./env-to-config ./postgrest postgres.conf` 6. Push your changes to GitHub From 3220373803c68c85dd702f04af46db2d9cf3cbcf Mon Sep 17 00:00:00 2001 From: Lee Johnson Date: Thu, 22 Nov 2018 14:34:47 -0500 Subject: [PATCH 208/549] renamed to postgrest.conf --- install.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.rst b/install.rst index 56545bda0f..64bd488856 100644 --- a/install.rst +++ b/install.rst @@ -343,8 +343,8 @@ Assuming your making modifications locally and then pushing to GitHub, it's easy 1. Create a new app on Heroku 2. In Settings add the following buildpack :code:`https://github.com/PostgREST/postgrest-heroku` 3. Add the require Config Vars in Heroku (see https://github.com/PostgREST/postgrest/blob/master/app.json#L7-L57 for more details) -4. Modify your postgres.conf file as required to match your Config Vars in Heroku -5. Create your :code:`Procfile` and add :code:`./env-to-config ./postgrest postgres.conf` +4. Modify your postgrest.conf file as required to match your Config Vars in Heroku +5. Create your :code:`Procfile` and add :code:`./env-to-config ./postgrest postgrest.conf` 6. Push your changes to GitHub 7. Set Heroku to automatically deploy from Master and then manually deploy the branch for the first build From 93c3c59134b3b597e33c02dfc85dcde13bf3ab34 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 31 Dec 2018 21:46:25 -0500 Subject: [PATCH 209/549] Fix #190, Add option for db-uri from file --- install.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/install.rst b/install.rst index 64bd488856..aab99d31a6 100644 --- a/install.rst +++ b/install.rst @@ -98,6 +98,8 @@ db-uri On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. + 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. + .. _db-schema: db-schema From 702e055d435bd443eb78b45cf1e23d99caa371e9 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 1 Jan 2019 01:38:20 -0500 Subject: [PATCH 210/549] Fix #191, Add db-extra-search-path config --- install.rst | 48 +++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/install.rst b/install.rst index aab99d31a6..41ef578766 100644 --- a/install.rst +++ b/install.rst @@ -68,24 +68,25 @@ The user specified in the db-uri is also known as the authenticator role. For mo Here is the full list of configuration parameters. -================ ====== ========= ======== -Name Type Default Required -================ ====== ========= ======== -db-uri String Y -db-schema String Y -db-anon-role String Y -db-pool Int 10 -server-host String 127.0.0.1 -server-port Int 3000 -server-proxy-uri String -jwt-secret String -jwt-aud String -secret-is-base64 Bool False -max-rows Int ∞ -pre-request String -app.settings.* String -role-claim-key String .role -================ ====== ========= ======== +==================== ====== ========= ======== +Name Type Default Required +==================== ====== ========= ======== +db-uri String Y +db-schema String Y +db-anon-role String Y +db-pool Int 10 +db-extra-search-path String public +server-host String 127.0.0.1 +server-port Int 3000 +server-proxy-uri String +jwt-secret String +jwt-aud String +secret-is-base64 Bool False +max-rows Int ∞ +pre-request String +app.settings.* String +role-claim-key String .role +==================== ====== ========= ======== .. _db-uri: @@ -107,6 +108,8 @@ db-schema The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. + This schema gets added to the `search_path `_ of every request. + .. _db-anon-role: db-anon-role @@ -121,6 +124,13 @@ db-pool Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the :code:`max_connections` GUC in your database. +db-extra-search-path +-------------------- + + Extra schemas to add to the `search_path `_ of every request. + + Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``. + .. _server-host: server-host @@ -341,7 +351,7 @@ With this you can see the swagger-ui in your browser on port 8080. Deploying to Heroku =================== Assuming your making modifications locally and then pushing to GitHub, it's easy to deploy to Heroku. - + 1. Create a new app on Heroku 2. In Settings add the following buildpack :code:`https://github.com/PostgREST/postgrest-heroku` 3. Add the require Config Vars in Heroku (see https://github.com/PostgREST/postgrest/blob/master/app.json#L7-L57 for more details) From a163828c6ca2eaa53b09ad5e3b691daf7577d2af Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 1 Jan 2019 17:20:01 -0500 Subject: [PATCH 211/549] Fix #192, Add section for quoting filters. Also reorder the Unicode Support section. --- api.rst | 52 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/api.rst b/api.rst index 6d27c0b86d..566552f15c 100644 --- a/api.rst +++ b/api.rst @@ -232,6 +232,41 @@ As mentioned, computed columns do not appear in the output by default. However y Computed columns must be created under the :ref:`exposed schema ` to be used in this way. +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:: http + + GET /%D9%85%D9%88%D8%A7%D8%B1%D8%AF HTTP/1.1 + +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:: http + + GET /employees?name=in.(%22Hebdon,John%22,%22Williams,Mary%22) HTTP/1.1 + +Here ``information.cpe`` is a column name. + +.. code-block:: http + + GET /vulnerabilities?%22information.cpe%22=like.*MS* HTTP/1.1 + + Ordering -------- @@ -419,23 +454,6 @@ If the stored procedure returns non-scalar values, you need to do a :code:`selec If more than one row would be returned the binary results will be concatenated with no delimiter. -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:: html - - http://localhost:3000/موارد - -Do this: - -.. code-block:: html - - http://localhost:3000/%D9%85%D9%88%D8%A7%D8%B1%D8%AF - .. _resource_embedding: Resource Embedding From ff74567473b723bae2bbdac26670c666b6dc4640 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 2 Jan 2019 13:17:31 -0500 Subject: [PATCH 212/549] Fix #145, add authenticator role to tutorials --- tutorials/tut0.rst | 15 +++++++++++---- tutorials/tut1.rst | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index dea41538c7..c42a396fd0 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -93,8 +93,8 @@ If everything is working correctly it will print out its version and information
Windows -

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 +

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)

setx /m PATH "%PATH%;C:\Program Files\PostgreSQL\10\bin"

@@ -144,13 +144,20 @@ Next make a role to use for anonymous web requests. When a request comes in, Pos .. code-block:: postgres create role web_anon nologin; - grant web_anon to postgres; 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 him 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 @@ -164,7 +171,7 @@ PostgREST uses a configuration file to tell it how to connect to the database. C .. code-block:: ini - db-uri = "postgres://postgres:mysecretpassword@localhost/postgres" + db-uri = "postgres://authenticator:mysecretpassword@localhost/postgres" db-schema = "api" db-anon-role = "web_anon" diff --git a/tutorials/tut1.rst b/tutorials/tut1.rst index 84e357a381..6abe846980 100644 --- a/tutorials/tut1.rst +++ b/tutorials/tut1.rst @@ -16,7 +16,7 @@ The previous tutorial created a :code:`web_anon` role in the database with which -- in the previous tutorial create role todo_user nologin; - grant todo_user to postgres; + grant todo_user to authenticator; grant usage on schema api to todo_user; grant all on api.todos to todo_user; From 14cb86d14798f33ac46ea96328c8202c7432181b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 2 Jan 2019 13:29:20 -0500 Subject: [PATCH 213/549] Fix #182, change default port of tutorials to 5433 --- tutorials/tut0.rst | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index c42a396fd0..e061b4761d 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -29,21 +29,11 @@ If Docker is not installed, you can get it `here Date: Thu, 3 Jan 2019 12:17:53 -0500 Subject: [PATCH 214/549] Fix #183, change immediate revocation request to PATCH --- tutorials/tut1.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tutorials/tut1.rst b/tutorials/tut1.rst index 6abe846980..bcef5e6329 100644 --- a/tutorials/tut1.rst +++ b/tutorials/tut1.rst @@ -230,13 +230,17 @@ Restart PostgREST for the change to take effect. Next try making a request with # this request still works - curl http://localhost:3000/todos \ - -H "Authorization: Bearer $TOKEN" + 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 \ - -H "Authorization: Bearer $WAYWARD_TOKEN" + 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: From 9e9f913c9eb22c4465d919cbd2bd77b5fd2e4b6e Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 3 Jan 2019 13:08:57 -0500 Subject: [PATCH 215/549] Fix #150, add example for url encoded payload --- api.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api.rst b/api.rst index 566552f15c..10c82e4dd1 100644 --- a/api.rst +++ b/api.rst @@ -888,6 +888,15 @@ The response will include a :code:`Location` header describing where to find the 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. +URL encoded payloads can be posted with ``Content-Type: application/x-www-form-urlencoded``. + +.. code-block:: http + + POST /people HTTP/1.1 + Content-Type: application/x-www-form-urlencoded + + name=John+Doe&age=50&weight=80 + .. note:: When inserting a row you must post a JSON object, not quoted JSON. From 474b8e6426c42952efebeceac44dbb72bee31184 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 3 Jan 2019 13:36:28 -0500 Subject: [PATCH 216/549] Add chinese translation link --- index.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/index.rst b/index.rst index 32e5a42248..9b4afc945f 100644 --- a/index.rst +++ b/index.rst @@ -69,6 +69,11 @@ You can help PostgREST ongoing maintenance and development by: Every donation will be spent on making PostgREST better for the whole community. +Translations +------------ + +* `Chinese `_ (latest version ``v0.4.2.0``) + .. toctree:: :caption: Tutorials :titlesonly: From 2dcb5ee746b9180ebde2e86fca957c233e8e6ad2 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 3 Jan 2019 17:22:23 -0500 Subject: [PATCH 217/549] Fix #180, note about volatility marker --- api.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 10c82e4dd1..7d58bea723 100644 --- a/api.rst +++ b/api.rst @@ -673,7 +673,11 @@ Immutable and stable functions Procedures in PostgreSQL marked with :code:`stable` or :code:`immutable` `volatility `_ can only read, not modify, the database and PostgREST executes them in a read-only transaction compatible for read-replicas. Stable and immutable functions can be called with the HTTP GET verb if desired. -Because ``add_them`` is declared IMMUTABLE, we can alternately call the function with a GET request: +.. 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/stable`` without failure. However the function will fail when called through PostgREST since it executes it in a read-only transaction. + +Because ``add_them`` was declared IMMUTABLE, we can alternately call the function with a GET request: .. code-block:: http From 4119538ab76b3c991efabb3a16d250e8430dc7bf Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sun, 6 Jan 2019 18:49:10 -0500 Subject: [PATCH 218/549] Add section for table/columns with spaces --- api.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/api.rst b/api.rst index 7d58bea723..722c790007 100644 --- a/api.rst +++ b/api.rst @@ -249,6 +249,19 @@ Do this: GET /%D9%85%D9%88%D8%A7%D8%B1%D8%AF HTTP/1.1 +.. _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:: http + + GET /Order%20Items?Unit%20Price=lt.200 HTTP/1.1 + +.. _reserved-chars: + Reserved characters ~~~~~~~~~~~~~~~~~~~ From 367b259bd46b6fd9978a2c1c7b3308c2eab86889 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sun, 6 Jan 2019 20:02:32 -0500 Subject: [PATCH 219/549] Further clarify db-extra-search-path --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 41ef578766..32a7af31ff 100644 --- a/install.rst +++ b/install.rst @@ -127,7 +127,7 @@ db-pool db-extra-search-path -------------------- - Extra schemas to add to the `search_path `_ of every request. + Extra schemas to add to the `search_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 exposed in your :ref:`db-schema`. Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``. From a106edc5d0396ebffe292aae5f1117fe07380073 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 4 Jan 2019 00:37:31 -0500 Subject: [PATCH 220/549] Add release notes --- index.rst | 10 +++++++++- install.rst | 2 ++ release_notes.rst | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 release_notes.rst diff --git a/index.rst b/index.rst index 9b4afc945f..febf872232 100644 --- a/index.rst +++ b/index.rst @@ -58,6 +58,8 @@ Getting Support The project has a friendly and growing community. Join our `chat room `_ for discussion and help. You can also report or search for bugs/features on the Github `issues `_ page. +.. _supporting-dev: + Supporting development ---------------------- @@ -70,10 +72,16 @@ You can help PostgREST ongoing maintenance and development by: Every donation will be spent on making PostgREST better for the whole community. Translations ------------- +~~~~~~~~~~~~ * `Chinese `_ (latest version ``v0.4.2.0``) +.. toctree:: + :caption: Release Notes + :titlesonly: + + release_notes.rst + .. toctree:: :caption: Tutorials :titlesonly: diff --git a/install.rst b/install.rst index 32a7af31ff..de4bf4187f 100644 --- a/install.rst +++ b/install.rst @@ -124,6 +124,8 @@ db-pool Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the :code:`max_connections` GUC in your database. +.. _db-extra-search-path: + db-extra-search-path -------------------- diff --git a/release_notes.rst b/release_notes.rst new file mode 100644 index 0000000000..c4aa0b7c29 --- /dev/null +++ b/release_notes.rst @@ -0,0 +1,32 @@ +Release Notes +============= + +Here we'll include the most relevant changes so you can migrate to newer versions easily. +You can see the full changelog of each release in the `PostgREST repository `_. + +v5.2.0 +====== + +* `Explicit qualification `_ introduced in ``v5.0`` is no longer necessary, this section will not be included from this version onwards. A :ref:`db-extra-search-path` configuration parameter was introduced to avoid the need to explictly qualify database objects. If you install PostgreSQL extensions on the ``public`` schema, they'll work normally from now on. + +* Now you can filter :ref:`tabs-cols-w-spaces`. + +* Included the ability to quote columns that have :ref:`reserved-chars`. + +* Thanks to `Zhou Feng `_, now is possible to reference an external file in :ref:`db-uri`. + +* Thanks to `Russell Davies `_, Json Web Key Sets are now accepted by :ref:`jwt-secret`. + +Thanks +------ + +This release was made possible thanks to: + +* `Daniel Babiak `_ +* `Michel Pelletier `_ +* Tsingson Qin +* Jay Hannah +* Victor Adossi +* Petr Beles + +If you like to join them please consider :ref:`supporting PostgREST development `. From 44b825b93900fd8714572d0f11a7e2f0c83fbb1d Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 24 Jan 2019 19:07:54 -0500 Subject: [PATCH 221/549] Add Simply Connected Systems --- index.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.rst b/index.rst index febf872232..faec2ae8fe 100644 --- a/index.rst +++ b/index.rst @@ -189,7 +189,6 @@ Commercial In Production ------------- -* `triggerFS - A realtime messaging and distributed trigger system `_ * `Moat `_ * `Catarse `_ * `Redsmin `_ @@ -200,6 +199,8 @@ In Production * `Convene `_ by Thomson-Reuters * `eGull `_ * `Elyios `_ +* `Simply Connected Systems `_ +* `triggerFS - A realtime messaging and distributed trigger system `_ Testimonials ------------ From 1c357179fd8639cd2cd1658205963a7a87106be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20Ch=C3=A1vez?= Date: Fri, 1 Mar 2019 16:18:20 -0500 Subject: [PATCH 222/549] Add TimescaleDB tutorial (#201) * Add TimescaleDB sponsorship --- _static/timescaledb.png | Bin 0 -> 91295 bytes api.rst | 9 + index.rst | 16 ++ integrations/timescaledb.rst | 327 +++++++++++++++++++++++++++++++++++ 4 files changed, 352 insertions(+) create mode 100644 _static/timescaledb.png create mode 100644 integrations/timescaledb.rst diff --git a/_static/timescaledb.png b/_static/timescaledb.png new file mode 100644 index 0000000000000000000000000000000000000000..d6403efa4c91d87c9e6a3236438748662ea7a135 GIT binary patch literal 91295 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 literal 0 HcmV?d00001 diff --git a/api.rst b/api.rst index 722c790007..398c37b961 100644 --- a/api.rst +++ b/api.rst @@ -366,6 +366,8 @@ Note that the larger the table the slower this query runs in the database. The s Range-Unit: items Content-Range: 0-24/3573458 +.. _res_format: + Response Format --------------- @@ -577,6 +579,9 @@ Embedded resources can be aliased and filters can be applied on these aliases: GET /films?select=*,90_comps:competitions(name),91_comps:competitions(name)&90_comps.year=eq.1990&91_comps.year=eq.1991 HTTP/1.1 + +.. _custom_queries: + Custom Queries ============== @@ -587,6 +592,8 @@ The PostgREST URL grammar limits the kinds of queries clients can perform. It pr * Geo-spatial queries that require an argument, like "points near (lat,lon)" * More sophisticated full-text search than a simple use of the :sql:`fts` filter +.. _s_procs: + Stored Procedures ================= @@ -942,6 +949,8 @@ Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`. +.. _bulk_insert: + Bulk Insert ----------- diff --git a/index.rst b/index.rst index faec2ae8fe..ee17eb075b 100644 --- a/index.rst +++ b/index.rst @@ -23,6 +23,16 @@ | 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 +-------- + +.. image:: _static/timescaledb.png + :target: https://www.timescale.com?utm_campaign=postgrest&utm_source=sponsor&utm_medium=referral&utm_content=docs + :width: 222px + :align: center + +`TimescaleDB `_ is an scalable time-series database packaged as a PostgreSQL extension. See our tutorial for using `TimescaleDB with PostgREST `_. + Motivation ---------- @@ -89,6 +99,12 @@ Translations tutorials/tut0.rst tutorials/tut1.rst +.. toctree:: + :caption: Integrations + :titlesonly: + + integrations/timescaledb.rst + .. toctree:: :caption: Installation :titlesonly: diff --git a/integrations/timescaledb.rst b/integrations/timescaledb.rst new file mode 100644 index 0000000000..ea372a507b --- /dev/null +++ b/integrations/timescaledb.rst @@ -0,0 +1,327 @@ +TimescaleDB for Time-Series Data +================================ + +`TimescaleDB `_ is an open-source database designed to make SQL scalable for time-series data. It is engineered up from PostgreSQL, providing automatic partitioning across time and space, while retaining the standard PostgreSQL interface. + +PostgREST turns your PostgreSQL database directly into a RESTful API, since TimescaleDB is packaged as a PostgreSQL extension it works with PostgREST as well. + +In this tutorial we'll explore some of TimescaleDB features through PostgREST. + +Install Docker +-------------- + +For an easier setup we're going to use `Docker `_, make sure you have it installed. + +Run TimescaleDB +--------------- + +First, let’s pull and start the `TimescaleDB container image `_: + +.. code-block:: bash + + docker run --name tsdb_tut \ + -e POSTGRES_PASSWORD=mysecretpassword \ + -p 5433:5432 \ + -d timescale/timescaledb:latest-pg11 + +This will run the container as a daemon and expose port ``5433`` to the host system so that it doesn't conflict with another PostgreSQL installation. + +Set up TimescaleDB +------------------ + +Now, we'll create the ``timescaledb`` extension in our database. + +Run ``psql`` in the container we created in the previous step. + +.. code-block:: bash + + docker exec -it tsdb_tut psql -U postgres + ## Run all the following commands inside psql + +And create the extension: + +.. code-block:: postgres + + create extension if not exists timescaledb cascade; + +Create an Hypertable +-------------------- + +`Hypertables `_ are the core abstraction TimescaleDB offers for dealing with time-series data. + +To create an ``hypertable``, first we need to create standard PostgreSQL tables: + +.. code-block:: postgres + + create table if not exists locations( + device_id text primary key + , location text + , environment text + ); + + create table if not exists conditions( + time timestamp with time zone not null + , device_id text references locations(device_id) + , temperature numeric + , humidity numeric + ); + +Now, we'll convert ``conditions`` into an hypertable with `create_hypertable `_: + +.. code-block:: postgres + + SELECT create_hypertable('conditions', 'time', chunk_time_interval => interval '1 day'); + -- This also implicitly creates an index: CREATE INDEX ON "conditions"(time DESC); + + -- Exit psql + exit + + +Load sample data +---------------- + +To have some data to play with, we'll download the ``weather_small`` data set from `TimescaleDB's sample datasets `_. + +.. code-block:: bash + + ## Run bash inside the database container + docker exec -it tsdb_tut bash + + ## Download and uncompress the data + wget -qO- https://timescaledata.blob.core.windows.net/datasets/weather_small.tar.gz | tar xvz + + ## Copy data into the database + psql -U postgres <`_: + +.. code-block:: bash + + docker run --rm -p 3000:3000 \ + --name tsdb_pgrst \ + --link tsdb_tut \ + -e PGRST_DB_URI="postgres://postgres:mysecretpassword@tsdb_tut/postgres" \ + -e PGRST_DB_ANON_ROLE="postgres" \ + -d postgrest/postgrest:latest + +PostgREST on Hypertables +------------------------ + +We'll now see how to read data from hypertables through PostgREST. + +Since hypertables can be queried using standard `SELECT statements `_, we can query them through PostgREST normally. + +Suppose we want to run this query on ``conditions``: + +.. code-block:: postgres + + select + time, + device_id, + humidity + from conditions + where + humidity > 90 and + time < '2016-11-16' + order by time desc + limit 10; + +Using PostgREST :ref:`horizontal `/:ref:`vertical ` filtering, this query can be expressed as: + +.. code-block:: bash + + curl -G "localhost:3000/conditions" \ + -d select=time,device_id,humidity \ + -d humidity=gt.90 \ + -d time=lt.2016-11-16 \ + -d order=time.desc \ + -d limit=10 + ## This command is equivalent to: + ## curl "localhost:3000/conditions?select=time,device_id,humidity&humidity=gt.90&time=lt.2016-11-16&order=time.desc&limit=10" + ## Here we used -G and -d to make the command more readable + +The response will be: + +.. code-block:: json + + [{"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000982","humidity":90.90000000000006}, + {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000968","humidity":92.3}, + {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000963","humidity":96.29999999999993}, + {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000951","humidity":94.39999999999998}, + {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000950","humidity":93.69999999999982}, + {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000915","humidity":94.69999999999997}, + {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000911","humidity":93.2000000000001}, + {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000910","humidity":91.30000000000017}, + {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000901","humidity":92.30000000000005}, + {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000895","humidity":91.00000000000014}] + +JOINs with relational tables +---------------------------- + +Hypertables support all standard `PostgreSQL constraints `_ . We can make use of the foreign key defined on ``locations`` to make a JOIN through PostgREST. A query such as: + +.. code-block:: postgres + + select + c.time, + c.temperature, + l.location, + l.environment + from conditions c + left join locations l on + c.device_id = l.device_id + order by time desc + limit 10; + +Can be expressed in PostgREST by using :ref:`resource_embedding`. + +.. code-block:: bash + + curl -G localhost:3000/conditions \ + -d select="time,temperature,device:locations(location,environment)" \ + -d order=time.desc \ + -d limit=10 + +.. code-block:: json + + [{"time":"2016-11-16T21:18:00+00:00","temperature":69.49999999999991,"device":{"location":"office-000202","environment":"inside"}}, + {"time":"2016-11-16T21:18:00+00:00","temperature":90,"device":{"location":"field-000205","environment":"outside"}}, + {"time":"2016-11-16T21:18:00+00:00","temperature":60.499999999999986,"device":{"location":"door-00085","environment":"doorway"}}, + {"time":"2016-11-16T21:18:00+00:00","temperature":91,"device":{"location":"swamp-000188","environment":"outside"}}, + {"time":"2016-11-16T21:18:00+00:00","temperature":42,"device":{"location":"arctic-000219","environment":"outside"}}, + {"time":"2016-11-16T21:18:00+00:00","temperature":70.80000000000003,"device":{"location":"office-000201","environment":"inside"}}, + {"time":"2016-11-16T21:18:00+00:00","temperature":62.699999999999974,"device":{"location":"door-00084","environment":"doorway"}}, + {"time":"2016-11-16T21:18:00+00:00","temperature":85.49999999999918,"device":{"location":"field-000204","environment":"outside"}}, + {"time":"2016-11-16T21:18:00+00:00","temperature":42,"device":{"location":"arctic-000218","environment":"outside"}}, + {"time":"2016-11-16T21:18:00+00:00","temperature":42,"device":{"location":"arctic-000217","environment":"outside"}}] + +Time-Oriented Analytics +----------------------- + +TimescaleDB includes new aggregate functions for time-oriented `analytics `_. + +For using aggregate queries with PostgREST you must create VIEWs or :ref:`s_procs`. Here's an example for using `time_bucket `_: + +.. code-block:: postgres + + -- Run psql in the database container + docker exec -it tsdb_tut psql -U postgres + + -- Create the function + create or replace function temperature_summaries(gap interval default '1 hour', prefix text default 'field') + returns table(hour text, avg_temp numeric, min_temp numeric, max_temp numeric) as $$ + select + time_bucket(gap, time)::text as hour, + trunc(avg(temperature), 2), + trunc(min(temperature), 2), + trunc(max(temperature), 2) + from conditions c + where c.device_id in ( + select device_id from locations + where location like prefix || '-%') + group by hour + $$ language sql stable; + + -- Exit psql + exit + +Every time the schema is changed you must reload PostgREST :ref:`schema cache ` so it can pick up the function parameters correctly. To reload, run: + +.. code-block:: bash + + docker kill --signal=USR1 tsdb_pgrst + + +Now, since the function is ``stable``, we can call it with ``GET`` as: + +.. code-block:: bash + + curl -G "localhost:3000/rpc/temperature_summaries" \ + -d gap=2minutes \ + -d order=hour.asc \ + -d limit=10 \ + -H "Accept: text/csv" + ## time_bucket accepts an interval type as it's argument + ## so you can pass gap=5minutes or gap=5hours + +.. code-block:: sql + + hour,avg_temp,min_temp,max_temp + "2016-11-15 12:00:00+00",72.97,68.00,78.00 + "2016-11-15 12:02:00+00",73.01,68.00,78.00 + "2016-11-15 12:04:00+00",73.05,68.00,78.10 + "2016-11-15 12:06:00+00",73.07,68.00,78.10 + "2016-11-15 12:08:00+00",73.11,68.00,78.10 + "2016-11-15 12:10:00+00",73.14,68.00,78.10 + "2016-11-15 12:12:00+00",73.17,68.00,78.19 + "2016-11-15 12:14:00+00",73.21,68.10,78.19 + "2016-11-15 12:16:00+00",73.24,68.10,78.29 + "2016-11-15 12:18:00+00",73.27,68.10,78.39 + +Note you can use PostgREST standard filtering on function results. Here we also changed the :ref:`res_format` to CSV. + +Fast Ingestion with Bulk Insert +------------------------------- + +You can use PostgREST :ref:`bulk_insert` to leverage TimescaleDB `fast ingestion `_. + +Let's do an insert of three rows: + +.. code-block:: bash + + curl "localhost:3000/conditions" \ + -H "Content-Type: application/json" \ + -H "Prefer: return=representation" \ + -d @- << EOF + [ + {"time": "2019-02-21 01:00:01-05", "device_id": "weather-pro-000000", "temperature": 40.0, "humidity": 59.9}, + {"time": "2019-02-21 01:00:02-05", "device_id": "weather-pro-000000", "temperature": 42.0, "humidity": 69.9}, + {"time": "2019-02-21 01:00:03-05", "device_id": "weather-pro-000000", "temperature": 44.0, "humidity": 79.9} + ] + EOF + +By using the ``Prefer: return=representation`` header we can see the successfully inserted rows: + +.. code-block:: json + + [{"time":"2019-02-21T06:00:01+00:00","device_id":"weather-pro-000000","temperature":40.0,"humidity":59.9}, + {"time":"2019-02-21T06:00:02+00:00","device_id":"weather-pro-000000","temperature":42.0,"humidity":69.9}, + {"time":"2019-02-21T06:00:03+00:00","device_id":"weather-pro-000000","temperature":44.0,"humidity":79.9}] + +Let's now insert a thousand rows, we'll use `jq `_ for constructing the array. + +.. code-block:: bash + + yes "{\"time\": \"$(date +'%F %T')\", \"device_id\": \"weather-pro-000001\", \"temperature\": 50, \"humidity\": 60}" | \ + head -n 1000 | jq -s '.' | \ + curl -i -d @- "http://localhost:3000/conditions" \ + -H "Content-Type: application/json" \ + -H "Prefer: count=exact" + +With ``Prefer: count=exact`` we can know how many rows were inserted. Check out the response: + +.. code-block:: haskell + + HTTP/1.1 201 Created + Transfer-Encoding: chunked + Date: Fri, 22 Feb 2019 16:47:05 GMT + Server: postgrest/5.2.0 (9969262) + Content-Range: */1000 + +You can see in ``Content-Range`` that the total number of inserted rows is ``1000``. + +Summing it up +------------- + +There you have it, with PostgREST you can get an instant and performant RESTful API for a TimescaleDB database. + +For a more in depth exploration of TimescaleDB capabilities, check their `docs `_. From a8d209cd1c22f7c91480076d2cb473204c3e6f0b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 5 Mar 2019 10:33:05 -0500 Subject: [PATCH 223/549] Add translations section --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 5e705d82bf..f82cffcf54 100644 --- a/README.md +++ b/README.md @@ -16,3 +16,14 @@ Or if you use [nix](https://nixos.org/nix/), you can just run: ``` Both of these options will build the docs and start a livereload server on `http://localhost:5500`. + +## Translations + +Translations are maintained in separate repositories forked from this one. Once you finish translating in your fork you can upload the project +to https://readthedocs.org and we'll link to it in the official documentation site https://postgrest.org. + +See more details in the chinese translation [PR](https://github.com/PostgREST/postgrest-docs/issues/66#issuecomment-297431688). + +### Available translations + +- Chinese - https://github.com/Lellansin/postgrest-docs (latest version `v0.4.2.0`) From c0fe0a5ce10c77534aaf2bd2b6896140fb1af7bf Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 5 Mar 2019 11:35:19 -0500 Subject: [PATCH 224/549] Add repo in default.nix --- default.nix | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/default.nix b/default.nix index c70878de80..9bc5b1c42e 100644 --- a/default.nix +++ b/default.nix @@ -1,4 +1,8 @@ -with import {}; +with import (builtins.fetchGit { + url = https://github.com/NixOS/nixpkgs-channels; + ref = "nixos-18.09-small"; + rev = "95fed28ac372c61eb83c87ad97c24b0f957827bf"; +}) {}; stdenv.mkDerivation { name = "postgrest-docs"; From f32c9c7967babffc89466cf4ec1495bf172bc19d Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 6 Mar 2019 10:30:41 -0500 Subject: [PATCH 225/549] Remove Explicit Qualification section --- api.rst | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/api.rst b/api.rst index 398c37b961..32e5d1a444 100644 --- a/api.rst +++ b/api.rst @@ -786,35 +786,6 @@ You can call overloaded functions with different number of arguments. GET /rpc/rental_duration?customer_id=232&from_date=2018-07-01 HTTP/1.1 -Explicit Qualification ----------------------- - -As of ``v5.0``, PostgREST executes a ``SET SCHEMA `` on each request, since this overrides the `search_path `_, function bodies need qualified schema names for any database object that is not in your exposed schema. - -.. code-block:: plpgsql - - -- Assuming that: - -- exposed schema is "api" - -- ST_AsGeoJSON is in the "public" schema - -- streets is in the "api" schema - CREATE FUNCTION api.sample() RETURNS json AS $$ - SELECT public.ST_AsGeoJSON(geom)::json FROM streets LIMIT 1; - -- Notice streets doesn't need the schema prefix while ST_AsGeoJSON does - $$ LANGUAGE sql; - -To avoid having to qualify many database objects, you can add a ``search_path`` to the function: - -.. code-block:: plpgsql - - CREATE FUNCTION api.sample_distance() RETURNS float8 AS $$ - SELECT ST_MakePoint(1, 1) <-> ST_MakePoint(10, 10); - -- If the search_path is not specified, this would have to be: - -- SELECT public.ST_MakePoint(1, 1) operator(public.<->) public.ST_MakePoint(10, 10); - $$ LANGUAGE sql SET search_path = public, api; - - -- existing functions can be altered to add a search_path - ALTER FUNCTION api.make_point(float8, float8) SET search_path = public, api; - Accessing Request Headers, Cookies and JWT claims ------------------------------------------------- From 3d734c44cf2ef4437a3596fab891dc0db31d94ca Mon Sep 17 00:00:00 2001 From: Paulo Vieira Date: Sat, 9 Mar 2019 22:25:05 +0000 Subject: [PATCH 226/549] clarify the usage of upsert when the table uses a surrogate key --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 32e5d1a444..41b7f75e28 100644 --- a/api.rst +++ b/api.rst @@ -968,7 +968,7 @@ You can make an UPSERT with :code:`POST` and the :code:`Prefer: resolution=merge { "id": 3, "name": "New employee 3", "salary": 50000 } ] -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`. +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`. UPSERT 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 `_. A single row UPSERT can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: From b77149d48e164ed19980272ed79530d30c34b53a Mon Sep 17 00:00:00 2001 From: Hao Wu Date: Fri, 15 Mar 2019 21:52:38 +0800 Subject: [PATCH 227/549] update doc based on postgrest/issues/1253 (#206) --- api.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api.rst b/api.rst index 41b7f75e28..20c4113650 100644 --- a/api.rst +++ b/api.rst @@ -970,6 +970,10 @@ You can make an UPSERT with :code:`POST` and the :code:`Prefer: resolution=merge 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`. UPSERT 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 `_. +.. 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`. + + A single row UPSERT can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: .. code-block:: http From 3b8ec49fba9700d0718a8addf8091bac5b7472c6 Mon Sep 17 00:00:00 2001 From: kraserge Date: Sun, 14 Apr 2019 19:08:48 +0300 Subject: [PATCH 228/549] SQL inside pgjwt (#207) Correct url https://github.com/michelp/pgjwt/blob/master/pgjwt--0.1.0.sql --- auth.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth.rst b/auth.rst index a0ddc2ee58..0800a08106 100644 --- a/auth.rst +++ b/auth.rst @@ -155,7 +155,7 @@ You can create a valid JWT either from inside your database or via an external s JWT from SQL ~~~~~~~~~~~~ -You can create JWT tokens in SQL using the `pgjwt extension `_. 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 `_ (you'll need to replace ``@extschema@`` with another schema or just delete it) which creates the functions you will need. +You can create JWT tokens in SQL using the `pgjwt extension `_. 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 `_ (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. From 7d8bceea1003676c1a68dd6e33d38c5365da8124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20Ch=C3=A1vez?= Date: Thu, 23 May 2019 17:55:07 -0500 Subject: [PATCH 229/549] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f82cffcf54..f60577cdc7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PostgREST documentation +# PostgREST documentation http://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. From 3dde972d31519d3afc319015bce1dc0f73adeb8e Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 27 May 2019 11:28:19 -0500 Subject: [PATCH 230/549] Add Nimbus - In Production --- index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/index.rst b/index.rst index ee17eb075b..07bd59d3b7 100644 --- a/index.rst +++ b/index.rst @@ -216,6 +216,10 @@ In Production * `eGull `_ * `Elyios `_ * `Simply Connected Systems `_ +* `Nimbus `_ + + - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. + * `triggerFS - A realtime messaging and distributed trigger system `_ Testimonials From 93e457cd89a1cf2b7394b59dea32aab4ceab9f27 Mon Sep 17 00:00:00 2001 From: Anupam Garg <1021183+angarg@users.noreply.github.com> Date: Thu, 30 May 2019 11:57:34 -1000 Subject: [PATCH 231/549] add Datrium client library and testimonial we've been using PostgREST in internal production infrastructure at Datrium for a few years now. so, we're giving thanks in the form of a testimonial, and providing a link to a python-based postgrest client we developed and use. --- index.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/index.rst b/index.rst index 07bd59d3b7..3d72f2994e 100644 --- a/index.rst +++ b/index.rst @@ -167,6 +167,7 @@ Client-Side Libraries * `lewisjared/postgrest-request `_ - JS, SuperAgent * `JarvusInnovations/jarvus-postgrest-apikit `_ - JS, Sencha framework * `davidthewatson/postgrest_python_requests_client `_ - Python +* `datrium/postgrest-pyclient `_ - Python * `calebmer/postgrest-client `_ - JS * `clesiemo3/postgrestR `_ - R * `PierreRochard/postgrest-angular `_ - TypeScript, generate UI from API description @@ -221,6 +222,8 @@ In Production - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. * `triggerFS - A realtime messaging and distributed trigger system `_ +* `Datrium `_ + Testimonials ------------ @@ -253,3 +256,11 @@ Testimonials 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. From 2cc48151dd9d7fc6c539110ffbfabe86c16feabe Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 15 Jun 2019 18:10:37 -0500 Subject: [PATCH 232/549] Add 2ndQuadrant as Sponsor --- _static/2ndquadrant.png | Bin 0 -> 90540 bytes _static/css/custom.css | 8 ++++++++ _static/logo.png | Bin 8149 -> 191528 bytes _static/retool.png | Bin 0 -> 63627 bytes index.rst | 12 +++++++----- 5 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 _static/2ndquadrant.png create mode 100644 _static/retool.png diff --git a/_static/2ndquadrant.png b/_static/2ndquadrant.png new file mode 100644 index 0000000000000000000000000000000000000000..3b6a75589173d2fe9056ee5c3ec9a758b19c7529 GIT binary patch literal 90540 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 literal 0 HcmV?d00001 diff --git a/_static/css/custom.css b/_static/css/custom.css index 588d99d4cf..d4c0ceef8b 100644 --- a/_static/css/custom.css +++ b/_static/css/custom.css @@ -9,3 +9,11 @@ div.highlight { div.line-block { margin-bottom: 0px !important; } + +#sponsors{ + text-align: center; +} + +#sponsors h1{ + text-align: left; +} diff --git a/_static/logo.png b/_static/logo.png index 030673cfbaa40c2bc994436ff1aa67cab3a01d0e..7d23fffc4aad5ee9722dbb2e21818babaa22a4fc 100644 GIT binary patch literal 191528 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 UW`QN$zZL`JrIn<9Ng4$HAD&l41^@s6 diff --git a/_static/retool.png b/_static/retool.png new file mode 100644 index 0000000000000000000000000000000000000000..abf26a1eff1573bbb66e102e3441e87d4df877d3 GIT binary patch literal 63627 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
  • RZ^uC~9UW1}Ooq-w*yRWdeEzX$W4r~- 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

    ZOj+d|b3Nr(3%<)l|G0p1kY+1wl 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�Wog)oJ^|t6GOdI_*XvblBzaC~k3g z?}uzpoKT|OA1o1JA1E^0Goo|)kwj>er^F}2P`PAh@pbJT#>q~wi-N>Gwq7r)6q8qs z&>_{=J3vR!Xeu>R-?Q$QBUA4Wy23wR`<<2e^C25h>;zjb=nw7Os;i%9U0fdKP&UhY z?EYzM{2B;V86*SJBs&`?6D8fX^XlO|3C{@fryI?Sk<{U9EPcS_^{59PvY)f2k7F2@BvR#U-t~hFo3K z0VXuMsmpzm$jMEKamtz#4{f?3BfLclo*&%tfb$0~Q9lqJvVVFP-~9{21r9usSTL|+ zJ`Uo*66y)Zr84rG5o-WVrc5+>7hkD zaFM&}nAI}FJ+1JS+|y$h_6ZR7wEotiSv@uK#9})O5{xB!KqxE=MDEs4K(pqz8YYeh ziY+M;Cu#w{h-rlVfzv|sys;{=2~K&%#0It^!XyOi$Zc0{i>H<4HXK)nH@bG(wcNP) zO7H&MGixIW0PLTPm7c%jwM$qbQuu7mrRvPWZd-`S2gP~93)PuOO7$Wi?l#LVoy&YF zAYAPOtj?bAahjTc9)_F(4aE=@SRj98@}8&q4z`U-fWb-~8)DBmEhU5`9nMB9c|Rl~ zs%?1KDd87I9n6xfPN|>kZ*f9EhZ4e<$`Kb`1O^XK!Djm&Fn>h^Vj?XeT^#%&^v3Rh zy{a&XH8jgxoG#4-4943Q0-eN1NWRQ$1W{uLUkrKPhw;5g(T;Y9T)Fi(^jqZ!A7u$Q z$aFmZDHzRSp_dWjis>GJuR~HkC%UVbH9-Mm7;KzL^ zJ22<>m2cvs6*&#Zl>jGQIjY~*T72CR_ZM5VW4`9Bbp#;#{FT391z0Uey>io@+H5dV z0+e;v&W!^xlcXxfPg$b=h5ZEfP*Pkt2}YP|UGYwHzzrwo;SV^+Y&@aeN)%hOA4XkG zTOH3A6|sB)_;5{%lS1KDAShIo=(%xjBZw8XO!BAf*(~(^v)94G(bZNAIN0M zz{?xsF>z+FlIZu}Kd};OBJY6zwuHO5P^@}7WO;0n*@^GDBzczzA5wkY29V>h{7amL zkiyT^Wc2dP3L(ej+Y+M-?A?Nw0fx5NCHxvqYRE)$zSO`0$**Sl``=LDcltLV!7VDC z*&L%+z*NWdd~PaTFK;3Ju4c{Fm85lvcu>)e;?zOlRW z@XA$S|B5*~=D>QgT7cqeGivSaoLuu_sJ#)ZEcukc;01y`3!5*c)ZjV>W}|pJ3vkLW zNK{`G>bvlvGtPP5=W}v-R5CrkipF^eusiD=UT4n9NOxubsZ(q}YsoRz`@!+a?l=(B z7w1jyG_Rs`@lA0w`-82Pz_Q{H;*mbhSL}a#Z@?w~y$GfB;0tVUq74c{@dal#Zz1~~ z#P59((N~Dy*EqMYXg?-1cKuZKO*=WpGSRmGcS}KVO8@dxa7$p;{=ki)O((n zR5H&RQZ*&&fEpiJ{=<34Y89(9+Od+81b)&J?f;qOw66PkB6|vAF21_E58iq8Ye;?0 z%T_18xbk(t>C9&OYcSSWxq(OiI&NiK8@hVs?`{2d9m+59H*6u;(LGm#Yds}x8T*Bl z+M;vpU(s=oOv157^o8mtd7k{wz_a?Q8WLQ;|{qU=}B^_2S6ckg-v zm1=nZCw?k!J9dxg?ms_0ZVg^>14XV^_zp}ymzS`HQJrnytsOXB!E8qn=SlUn4bk{( z40knA(8-iFpg^2?TW^VtR_Gi6p=H7}SUY)Ru%UKvpqWV#pwIz9TT_uPm;Vb8%le`d z=J6a~im;T0(G9*_7~*4l4xmd_gYadKlS|~FAJaHPLf*W+>#tY5HDq-|(LD3kRAdto z-ii|qB-OWVs3c%qZ8#?y&;=#KE)zS;!`{wk7Mf#BR2_<8xT>QX}aQ_Aw?h} z4e&%^;t8Jh79)RLbSfh4gBepDLB_H)ocq*Vh4jcyL`c&YS0@({-Om0jdJbNt=4Cz} zgl%s+r+L^}Nj03C-Nl4UhT3dz>|d~E@t0&w zLg04sy%;`(Rf99vl_r&GV#V%#_Idf8zHc9t2tX{FT9A|bL?u)0tH{N*1f@@DH_u-i zFng)Y>|3L-OQN#$h@x}=C~F8+dMuGTQ30Eo^MWu*KD=_A-CRBMcLc=arr7`}%WVh! z!ubL9c%YOWgb!sMc6=ImMNm@PkKS-@`m7(RzzEqHp47 zAso=zUY#lZomO%Ptd(e3(vf$TP4`F7EEPCW;Z^xKW$VADfzU}^D}$3d+x4pIaf+PY zuPVs#Q-Bz`{_0E0M90KW+Upo^8f%%C1Zjk*p7XGTI zByinD0VN!$A!BhOeaNqgP>y|((P>0~vp-vt_&J=HL&icLl*2s_lSLw}>wcpmu?cQ@ zvf^S80nA2=!BN8OS!W+cpEV)Ti&`Y2 zh2uaz*~c&kzF=}(*tNODjOk{+-*FHK@#d?3+i3As=eW?xFKz6vDYcd(W+G~{g5X+& z^;4Woz)WJbAgM`nx|=SL#>p|GrMO|Bj#iDcIS}iuPrlQGCwJ=LsWlD%=X9K1D)x z|I_=)@+%+gG%mx(pJX;1Nt0rCF|q;t_Tc{?J$e`J+;%@k<92Zx+*&1niN)V{=iy7M zJ^wfaU)Zf4==R&iSE)H4%uF7FSlJ6!=SAy^ZL7mNOPxx75Dzx1DbDUrAznp=Px7qj zcHL&2NaTg(XHLCXSwyjo_(H5HLJzbKAZHKI+Z1(fL6OwIvPHK=Doj0r;HqXd@@(fp zpq20aTqSFZG;upp0gx^U{FHg|(3{OvFj@yRxOj7{*a`whLJ_EZMwUMMkVQM7We3+) zXV=VmK}Z2?TI%Z=N=pMFG5ZpVDu@Zxqy#H5R#o0csAoLi^Tf{-Bwq;laiu#Gk{x+# zCGygbYQT+9by}NLocgZNWFagwL5R7&)S?Dn*F4aR|J(E%^2+FTdSKVoaiX|Vu#(zK zu3njQNY)5p^?ZZmI1)cU@dn*nLzMO{A13*14wBzpxt6Rue_o0Q?jhi9>XXs-2RPFl zWc~g3fBlX_?oYz&St}4{6u2q+5ZG*BiiXXft%X4-%EdpLdc!9mA;trtW63u!Y8^k1aoS&vUE#@z)2`F|VT)%WH7wM@Ojhve0CkDln|KpD36 z2orFawS>`=$^J-#HYmnr{fodLlymCV5M`)irYKs(e-vr3E$iDHj6Jrqdz_E~BRm^1&| zFukN0VIjP{WrHn3j)fOw4TTXkYj#^lSRVMlR+}D?R~vJZcp!?0)zha~d?N0lrH>5xji{t9mtug$jRaBchFEsxE{?p3VsfSTkWG^K@e zwJR5D_VjycVgYm>AdYC75t6KjZZ*7Lw+nvd1a7mz}W-o3*+U{AU*;i0B z#QVv)jp&&8$#v6Rz;S$^ zt<<4gdK&;!m%q-Tc||N`KNr=HLs0`uECiNZh^Z!GfjL*eg8ZC0HOK%kKphohVjLSz3)F;03%P_Q?I&m1!^hU9PyjhX=MR%?S2xWymx-H97B=R1s= z{eg$rwtCh5a0cT2;{1U&13`+fGs4~%hkH1XekjCqY%EOd?rZvL6A<5<26MW!5SU

    J=>cK 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 Date: Fri, 12 Mar 2021 13:02:42 +0200 Subject: [PATCH 382/549] Update api.rst Changed SET LOCAL to SELECT set_config( ... ) so that the value of the header can be dynamically calculated. Supported in versions 9.6 and above. --- api.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api.rst b/api.rst index 548c7094d1..4c72eb03ae 100644 --- a/api.rst +++ b/api.rst @@ -1530,9 +1530,9 @@ PostgREST reads the ``response.headers`` SQL variable to add extra headers to th -- tell client to cache response for two days - SET LOCAL "response.headers" = - '[{"Cache-Control": "public"}, {"Cache-Control": "max-age=259200"}]'; - + SELECT set_config('response.headers', + '[{"Cache-Control": "public"}, {"Cache-Control": "max-age=259200"}]', true); + Notice that the variable 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 to be repeated when setting multiple values and an object would not allow the repeated key. .. note:: From 9f50e1d2d701a2805e74490f59b340268ba5610f Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 25 Mar 2021 20:15:08 -0500 Subject: [PATCH 383/549] Add Supabase as a sponsor --- _static/supabase.png | Bin 0 -> 4488 bytes index.rst | 6 ++++++ 2 files changed, 6 insertions(+) create mode 100644 _static/supabase.png diff --git a/_static/supabase.png b/_static/supabase.png new file mode 100644 index 0000000000000000000000000000000000000000..9c3686bd041532138671ea54604bc77885f7cad1 GIT binary patch literal 4488 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 af&T->Exd?b>wZE20000 Date: Mon, 5 Apr 2021 21:52:31 +0200 Subject: [PATCH 384/549] remove translation section from readme TODO: implement a proper translation workflow --- README.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/README.md b/README.md index 0bf6955132..9f964a975d 100644 --- a/README.md +++ b/README.md @@ -18,14 +18,3 @@ Once in the nix-shell you have the following commands available: 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. - -## Translations - -Translations are maintained in separate repositories forked from this one. Once you finish translating in your fork you can upload the project -to https://readthedocs.org and we'll link to it in the official documentation site https://postgrest.org. - -See more details in the chinese translation [PR](https://github.com/PostgREST/postgrest-docs/issues/66#issuecomment-297431688). - -### Available translations - -- Chinese - https://github.com/Lellansin/postgrest-docs (latest version `v0.4.2.0`) From a1178a80164d0f5fe92368dd87993abb08c874a3 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Tue, 1 Dec 2020 00:30:05 +0100 Subject: [PATCH 385/549] move "Build from Source" to installation chapter and remove "Running the Test Suite" --- development.rst | 156 ------------------------------------------ index.rst | 14 ++-- install.rst | 61 +++++++++++++---- releases/upcoming.rst | 2 +- 4 files changed, 55 insertions(+), 178 deletions(-) delete mode 100644 development.rst diff --git a/development.rst b/development.rst deleted file mode 100644 index ff9c987a9a..0000000000 --- a/development.rst +++ /dev/null @@ -1,156 +0,0 @@ -.. _build_source: - -Build from Source -================= - -.. note:: - - We discourage building and using PostgREST on **Alpine Linux** because of a reported GHC memory leak on that platform. - -To help with development, you'll need to build from source. `Stack `_ makes it easy. 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 postgresql95-client - OS X 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`. - -Running the Test Suite -====================== - -To properly run the test suite, you need a PostgreSQL database that the tests can run against. There are several ways to set up this database. - -Testing with a temporary database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you have PostgreSQL installed locally (:code:`initdb`, :code:`pg_ctl` and :code:`psql` should be on your PATH, no server needs to be running), you can run the test suite against a temporary database: - -.. code:: bash - - test/with_tmp_db stack test - -The :code:`with_tmp_db` script will set up a new PostgreSQL cluster in a temporary directory, set the required environment variables and run the command that you passed it as an argument, :code:`stack test` in the example above. When the command is done, the temporary database is torn down and deleted again. - -Manually creating the Test Database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To manually create a database for testing, use the test creation script :code:`create_test_database` in the :code:`test/` folder. - -The script expects the following parameters: - -.. code:: bash - - test/create_test_db connection_uri database_name [test_db_user] [test_db_user_password] - -Use the `connection URI `_ to specify the user, password, host, and port. Do not provide the database in the connection URI. The PostgreSQL role you are using to connect must be capable of creating new databases. - -The :code:`database_name` is the name of the database that :code:`stack test` will connect to. If the database of the same name already exists on the server, the script will first drop it and then re-create it. - -Optionally, specify the database user :code:`stack test` will use. The user will be given necessary permissions to reset the database after every test run. - -If the user is not specified, the script will generate the role name :code:`postgrest_test_` suffixed by the chosen database name, and will generate a random password for it. - -Optionally, if specifying an existing user to be used for the test connection, one can specify the password the user has. - -The script will return the db uri to use in the tests--this uri corresponds to the :code:`db-uri` parameter in the configuration file that one would use in production. - -Generating the user and the password allows one to create the database and run the tests against any PostgreSQL server without any modifications to the server. (Such as allowing accounts without a password or setting up trust authentication, or requiring the server to be on the same localhost the tests are run from). - -Running the Tests with the manually created database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To run the tests, one must supply the database uri in the environment variable :code:`POSTGREST_TEST_CONNECTION`. - -Typically, one would create the database and run the test in the same command line, using the ``postgres`` superuser: - -.. code:: bash - - POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@database-host" test_db) stack test - -For repeated runs on the same database, one should export the connection variable: - -.. code:: bash - - export POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@database-host" test_db) - stack test - stack test - ... - -If the environment variable is empty or not specified, then the test runner will default to connection uri - -.. code:: bash - - postgres://postgrest_test@localhost/postgrest_test - -This connection assumes the test server on the :code:`localhost:code:` with the user `postgrest_test` without the password and the database of the same name. - -Destroying the Database -~~~~~~~~~~~~~~~~~~~~~~~ - -The test database will remain after the test, together with four new roles created on the PostgreSQL server. To permanently erase the created database and the roles, run the script :code:`test/delete_test_database`, using the same superuser role used for creating the database: - -.. code:: bash - - test/destroy_test_db connection_uri database_name - -Testing with Docker -~~~~~~~~~~~~~~~~~~~ - -The ability to connect to non-local PostgreSQL simplifies the test setup. One elegant way of testing is to use a disposable PostgreSQL in docker. - -For example, if local development is on a mac with Docker for Mac installed: - -.. code:: bash - - $ docker run --name db-scripting-test -e POSTGRES_PASSWORD=pwd -p 5434:5432 -d postgres - $ POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@localhost:5434" test_db) stack test - -Additionally, if one creates a docker container to run stack test (this is necessary on Mac OS Sierra with GHC below 8.0.1, where :code:`stack test` fails), one can run PostgreSQL in a separate linked container, or use the locally installed PostgreSQL app. - -Build the test container with :code:`test/Dockerfile.test`: - -.. code:: bash - - $ docker build -t pgst-test - < test/Dockerfile.test - $ mkdir .stack-work-docker ~/.stack-linux - -The first run of the test container will take a long time while the dependencies get cached. Creating the :code:`~/.stack-linux` folder and mapping it as a volume into the container ensures that we can run the container in disposable mode and not worry about subsequent runs being slow. :code:`.stack-work-docker` is also mapped into the container and must be specified when using stack from Linux, not to interfere with the :code:`.stack-work` for local development. (On Sierra, :code:`stack build` works, while :code:`stack test` fails with GHC 8.0.1). - -Linked containers: - -.. code:: bash - - $ docker run --name pg -e POSTGRES_PASSWORD=pwd -d postgres - $ docker run --rm -it -v `pwd`:`pwd` -v ~/.stack-linux:/root/.stack --link pg:pg -w="`pwd`" -v `pwd`/.stack-work-docker:`pwd`/.stack-work pgst-test bash -c "POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@pg" test_db) stack test" - -Stack test in Docker for Mac, PostgreSQL app on mac: - -.. code:: bash - - $ host_ip=$(ifconfig en0 | grep 'inet ' | cut -f 2 -d' ') - $ export POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres@$HOST" test_db) - $ docker run --rm -it -v `pwd`:`pwd` -v ~/.stack-linux:/root/.stack -v `pwd`/.stack-work-docker:`pwd`/.stack-work -e "HOST=$host_ip" -e "POSTGREST_TEST_CONNECTION=$POSTGREST_TEST_CONNECTION" -w="`pwd`" pgst-test bash -c "stack test" - $ test/destroy_test_db "postgres://postgres@localhost" test_db diff --git a/index.rst b/index.rst index f0fb38f8f3..c071b055bc 100644 --- a/index.rst +++ b/index.rst @@ -188,15 +188,6 @@ PostgREST has a growing ecosystem of examples, libraries, and experiments. Here * :ref:`eco_extensions` * :ref:`clientside_libraries` -For helping with development, see the following page. - -* :doc:`Development ` - -.. toctree:: - :caption: Development - :hidden: - - development.rst Release Notes ------------- @@ -277,3 +268,8 @@ Translations ------------ * `Chinese `_ (latest version ``v0.4.2.0``) + +Contributing +------------ + +Please see the `Contributing guidelines `_ in the main PostgREST repository. diff --git a/install.rst b/install.rst index 88595fe822..87d650c9de 100644 --- a/install.rst +++ b/install.rst @@ -30,17 +30,15 @@ If you use **Nix**, then you can install PostgREST from nixpkgs. nix-env -i haskellPackages.postgrest -If you use Windows, you can install PostgREST using `Chocolatey `_ or `Scoop `_. +If you use Windows, you can install PostgREST using `Chocolatey `_ or `Scoop `_. .. code:: bash choco install postgrest scoop install postgrest -When a pre-built binary does not exist for your system you can :ref:`build the project from source `. - -Running -------- +Running PostgREST +================= If you downloaded PostgREST from the release page, first extract the compressed file to obtain the executable. @@ -88,14 +86,9 @@ For a complete reference of the configuration file, see :ref:`configuration`. .. _pg-dependency: PostgreSQL dependency -===================== - -To use PostgREST you will need an underlying database. We require PostgreSQL 9.4 or greater, but recommend at least 9.5 for row-level security features. -You can use something like Amazon `RDS `_ but installing your own locally is cheaper and more convenient for development. +--------------------- -* `Instructions for OS X `_ -* `Instructions for Ubuntu 14.04 `_ -* `Installer for Windows `_ +To use PostgREST you will need an underlying database. We require PostgreSQL 9.5 or greater. You can use something like `Amazon RDS `_ but installing your own locally is cheaper and more convenient for development. You can also run PostgreSQL in a :ref:`docker container`. Docker ====== @@ -157,6 +150,8 @@ The database connection string above is just an example. Adjust the role and pas host all all 10.0.0.10/32 trust +.. _pg-in-docker: + Containerized PostgREST *and* db with docker-compose ---------------------------------------------------- @@ -208,6 +203,48 @@ If you want to have a visual overview of your API in your browser you can add sw 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 postgresql95-client + OS X 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`. + Deploying to Heroku =================== diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 4ef4ea9af6..c7fb082c90 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -5,7 +5,7 @@ Upcoming ======== -These are changes yet unreleased. If you'd like to try them out before a new official release, you can :ref:`build_source`. +These are changes yet unreleased. If you'd like to try them out before a new official release, you can use a `nightly release `_. Added ----- From 92395eae8aa2185bc2e9d0f689e647434fb26f31 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Mon, 7 Dec 2020 20:08:42 +0100 Subject: [PATCH 386/549] added postgrest-docs-linkcheck; clean up links * removed outdated links * updated permanent redirections * updated postgres docs links to current version * sort links in ecosystems by repo-name * change http:// to https:// --- admin.rst | 6 +- api.rst | 26 ++--- auth.rst | 8 +- configuration.rst | 12 +-- default.nix | 14 ++- ecosystem.rst | 121 ++++++++++++------------ how-tos/casting-type-to-custom-json.rst | 4 +- how-tos/providing-images-for-img.rst | 2 +- index.rst | 43 ++++----- install.rst | 6 +- releases/upcoming.rst | 2 +- releases/v5.2.0.rst | 4 +- releases/v6.0.2.rst | 6 +- releases/v7.0.0.rst | 4 +- releases/v7.0.1.rst | 4 +- schema_structure.rst | 12 +-- shell.nix | 1 + tutorials/tut0.rst | 6 +- 18 files changed, 141 insertions(+), 140 deletions(-) diff --git a/admin.rst b/admin.rst index d131708e45..2a8d03d4ff 100644 --- a/admin.rst +++ b/admin.rst @@ -1,3 +1,5 @@ +.. _admin: + Hardening PostgREST =================== @@ -51,7 +53,7 @@ However it's very easy to delete the **entire table** by omitting the query para DELETE /logs HTTP/1.1 -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 `_ PostgreSQL extension. It raises an error if UPDATE or DELETE are executed without specifying conditions. To install it you can use the `PGXN `_ network: +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 `_ PostgreSQL extension. It raises an error if UPDATE or DELETE are executed without specifying conditions. To install it you can use the `PGXN `_ network: .. code-block:: bash @@ -60,7 +62,7 @@ This can happen accidentally such as by switching a request from a GET to a DELE # 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 `_ if finer access control is required. +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 `_ if finer access control is required. Count-Header DoS ---------------- diff --git a/api.rst b/api.rst index 4c72eb03ae..d5951818af 100644 --- a/api.rst +++ b/api.rst @@ -108,7 +108,7 @@ The view will provide a new endpoint: 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 `_. The following examples illustrate the possibilities. +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 `_. The following examples illustrate the possibilities. .. code-block:: http @@ -183,7 +183,7 @@ Casting the columns is possible by suffixing them with the double colon ``::`` p JSON Columns ~~~~~~~~~~~~ -You can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `_. +You can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `_. .. code-block:: http @@ -427,7 +427,7 @@ To do this, specify the ``Prefer: count=planned`` header. 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 `_ for more details. +See `ANALYZE `_ for more details. .. _estimated_count: @@ -676,14 +676,14 @@ Since it contains ``competition_id`` and ``film_id`` — and each one has a **fo GET /nominations_view?select=rank,competitions(name,year),films(title)&rank=eq.5 HTTP/1.1 -It's also possible to embed `Materialized Views `_. +It's also possible to embed `Materialized Views `_. .. warning:: It's not guaranteed that all kinds of views will be embeddable. In particular, views that contain UNIONs will not be made embeddable. - Why? PostgREST detects source table foreign keys in the view by querying and parsing `pg_rewrite `_. + Why? PostgREST detects source table foreign keys in the view by querying and parsing `pg_rewrite `_. This may fail depending on the complexity of the view. `Report an issue `_ if your view is not made embeddable so we can @@ -879,7 +879,7 @@ Similarly to the **target**, the **hint** can be a **table name**, **foreign key Insertions / Updates ==================== -All tables and `auto-updatable views `_ can be modified through the API, subject to permissions of the requester's database role. +All tables and `auto-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. @@ -1044,7 +1044,7 @@ All the columns must be specified in the request body, including the primary key .. note:: - Upsert features are only available starting from PostgreSQL 9.5 since it uses the `ON CONFLICT clause `_. + Upsert features are only available starting from PostgreSQL 9.5 since it uses the `ON CONFLICT clause `_. .. _delete: @@ -1138,7 +1138,7 @@ Procedures must be declared with named parameters. Procedures declared like CREATE FUNCTION non_named_args(integer, text, integer) ... -cannot be called with PostgREST, since we use `named notation `_ internally. +cannot be called with PostgREST, since we use `named notation `_ internally. Note that PostgreSQL converts identifier names to lowercase unless you quote them like: @@ -1161,7 +1161,7 @@ Procedures that do not modify the database can be called with the HTTP GET verb .. 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. However, because of the read-only transaction this would still fail with PostgREST. + 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. However, because of the read-only transaction this would still fail with PostgREST. Because ``add_them`` is ``IMMUTABLE``, we can alternately call the function with a GET request: @@ -1215,7 +1215,7 @@ You can call a function that takes an array parameter: [2,3,4,5] -For calling the function with GET, you can pass the array as an `array literal `_, +For calling the function with GET, you can pass the array as an `array literal `_, as in ``{1,2,3,4}``. Note that the curly brackets have to be urlencoded(``{`` is ``%7B`` and ``}`` is ``%7D``). .. code-block:: http @@ -1420,7 +1420,7 @@ This follows the same rules as :ref:`binary_output`. OpenAPI Support =============== -Every API hosted by PostgREST automatically serves a full `OpenAPI `_ 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. For extra customization, the OpenAPI output contains a "description" field for every `SQL comment `_ on any database object. For instance, +Every API hosted by PostgREST automatically serves a full `OpenAPI `_ 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. For extra customization, the OpenAPI output contains a "description" field for every `SQL comment `_ on any database object. For instance, .. code-block:: sql @@ -1446,7 +1446,7 @@ Also if you wish to generate a ``summary`` field you can do it by having a multi spans multiple lines$$; -You can use a tool like `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. +You can use a tool like `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:: @@ -1664,7 +1664,7 @@ Returns: HTTP Status Codes ----------------- -PostgREST translates `PostgreSQL error codes `_ into HTTP status as follows: +PostgREST translates `PostgreSQL error codes `_ into HTTP status as follows: +--------------------------+-------------------------+---------------------------------+ | PostgreSQL error code(s) | HTTP status | Error description | diff --git a/auth.rst b/auth.rst index 99a29c1f30..7451060437 100644 --- a/auth.rst +++ b/auth.rst @@ -16,7 +16,7 @@ The authenticator should be created :code:`NOINHERIT` and configured in the data .. image:: _static/security-anon-choice.png -Here are the technical details. We use `JSON Web Tokens `_ to authenticate API requests. 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. +Here are the technical details. We use `JSON Web Tokens `_ to authenticate API requests. 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 @@ -48,7 +48,7 @@ Roles for Each Web User PostgREST can accommodate either viewpoint. If you treat a role as a single user then the JWT-based role switching described above does most of what you need. When an authenticated user makes a request PostgREST will switch into the 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. +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 @@ -208,14 +208,14 @@ To use Auth0, create `an application `_ for .. note:: - Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata `_. Then, you will need to write `a rule `_ that will extract the role from the user's app_metadata and set it as a `custom claim `_ in the access token. Note that, you may use Auth0's `core authorization feature `_ for more complex use cases. Metadata solution is mentioned here for simplicity. + Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata `_. Then, you will need to write `a rule `_ that will extract the role from the user's app_metadata and set it as a `custom claim `_ in the access token. Note that, you may use Auth0's `core authorization feature `_ for more complex use cases. Metadata solution is mentioned here for simplicity. .. code:: javascript function (user, context, callback) { // Follow the documentations at - // http://postgrest.org/en/latest/configuration.html#role-claim-key + // https://postgrest.org/en/latest/configuration.html#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'; diff --git a/configuration.rst b/configuration.rst index 8654866b1c..dddc9d0d59 100644 --- a/configuration.rst +++ b/configuration.rst @@ -16,7 +16,7 @@ The configuration file must contain a set of key value pairs. At minimum you mus # postgrest.conf # The standard connection URI format, documented at - # https://www.postgresql.org/docs/current/static/libpq-connect.html#AEN45347 + # https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING db-uri = "postgres://user:pass@host:5432/dbname" # The name of which database schema to expose to REST clients @@ -59,9 +59,9 @@ raw-media-types String db-uri ------ - The standard connection PostgreSQL `URI 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 `_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``. + The standard connection PostgreSQL `URI 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 `_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``. - When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ and the `Peer Authentication method `_ 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 `_ documentation for more details. + When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ and the `Peer Authentication method `_ 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 `_ documentation for more details. On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. @@ -79,7 +79,7 @@ db-schema db-schema = "api" - This schema gets added to the `search_path `_ of every request. + This schema gets added to the `search_path `_ of every request. List of schemas ~~~~~~~~~~~~~~~ @@ -92,7 +92,7 @@ List of schemas If you don't :ref:`Switch Schemas `, the first schema in the list(``tenant1`` in this case) is chosen as the default schema. - *Only the chosen schema* gets added to the `search_path `_ of every request. + *Only the chosen schema* gets added to the `search_path `_ of every request. .. warning:: @@ -125,7 +125,7 @@ db-pool-timeout db-extra-search-path -------------------- - Extra schemas to add to the `search_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-schema`. + Extra schemas to add to the `search_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-schema`. This parameter was meant to make it easier to use **PostgreSQL extensions** (like PostGIS) that are outside of the :ref:`db-schema`. diff --git a/default.nix b/default.nix index b422be002b..a571acffb4 100644 --- a/default.nix +++ b/default.nix @@ -1,9 +1,9 @@ let # Commit of the Nixpkgs repository that we want to use. nixpkgsVersion = { - date = "2020-10-27"; - rev = "cd63096d6d887d689543a0b97743d28995bc9bc3"; - tarballHash = "1wg61h4gndm3vcprdcg7rc4s1v3jkm5xd7lw8r2f67w502y94gcy"; + date = "2021-04-04"; + rev = "c0e881852006b132236cbf0301bd1939bb50867e"; + tarballHash = "0fy7z7yxk5n7yslsvx5cyc6h21qwi4bhxf3awhirniszlbvaazy2"; }; # Nix files that describe the Nixpkgs repository. We evaluate the expression @@ -72,4 +72,12 @@ in | xargs -0 -n 1 -i \ sh -c "grep \"{}\" $FILES > /dev/null || echo \"{}\"" ''; + + linkcheck = + pkgs.writeShellScriptBin "postgrest-docs-linkcheck" + '' + set -euo pipefail + + ${python}/bin/sphinx-build -b linkcheck . _build + ''; } diff --git a/ecosystem.rst b/ecosystem.rst index 53c0f399b4..9344ba0f13 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -19,27 +19,26 @@ Community Tutorials Example Apps ------------ -* `monacoremo/postgrest-sessions-example `_ - example for cookie-based sessions -* `tatut/postgrest-ui `_ - ClojureScript UI components for PostgREST -* `priyank-purohit/PostGUI `_ - React Material UI admin panel -* `Qu4tro/pgrst-dev-setup `_ - docker-compose and tmuxp setup for experimentation. -* `subzerocloud/postgrest-starter-kit `_ - boilerplate for new project -* `NikolayS/postgrest-google-translate `_ - calling to external translation service -* `CodeforAustralia/heritage-near-me `_ - Elm and PostgREST with PostGIS -* `timwis/handsontable-postgrest `_ - an excel-like database table editor -* `Recmo/PostgrestSkeleton `_ - Docker Compose, PostgREST, Nginx and Auth0 -* `benoror/ember-postgrest-dynamic-ui `_ - generating Ember forms to edit data -* `ruslantalpa/blogdemo `_ - blog api demo in a vagrant image -* `timwis/ext-postgrest-crud `_ - browser-based spreadsheet -* `srid/chronicle `_ - tracking a tree of personal memories -* `diogob/elm-workshop `_ - building a simple database query UI -* `myfreeweb/moneylog `_ - accounting web app in Polymer + PostgREST -* `tyrchen/goodfilm `_ - example film api -* `begriffs/postgrest-example `_ - sqitch versioning for API -* `SMRxT/postgrest-demo `_ - multi-tenant logging system -* `PierreRochard/postgrest-boilerplate `_ - example auth back-end -* `marmelab/ng-admin-postgrest `_ - automatic database admin panel -* `seveibar/postgrest-vercel `_ - run PostgREST on Vercel (Serverless/AWS Lambda) +* `blogdemo `_ - blog api demo in a vagrant image +* `chronicle `_ - tracking a tree of personal memories +* `elm-workshop `_ - building a simple database query UI +* `ember-postgrest-dynamic-ui `_ - generating Ember forms to edit data +* `ext-postgrest-crud `_ - browser-based spreadsheet +* `general `_ - example auth back-end +* `goodfilm `_ - example film api +* `handsontable-postgrest `_ - an excel-like database table editor +* `heritage-near-me `_ - Elm and PostgREST with PostGIS +* `ng-admin-postgrest `_ - automatic database admin panel +* `pgrst-dev-setup `_ - docker-compose and tmuxp setup for experimentation. +* `postgrest-demo `_ - multi-tenant logging system +* `postgrest-example `_ - sqitch versioning for API +* `postgrest-sessions-example `_ - example for cookie-based sessions +* `postgrest-starter-kit `_ - boilerplate for new project +* `postgrest-translation-proxy `_ - calling to external translation service +* `postgrest-ui `_ - ClojureScript UI components for PostgREST +* `postgrest-vercel `_ - run PostgREST on Vercel (Serverless/AWS Lambda) +* `PostgrestSkeleton `_ - Docker Compose, PostgREST, Nginx and Auth0 +* `PostGUI `_ - React Material UI admin panel .. _eco_external_notification: @@ -48,14 +47,14 @@ 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. -* `vbalasu/pg-notify-webhook `_ - trigger webhooks from PostgreSQL's LISTEN/NOTIFY -* `diogob/postgres-websockets `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY -* `frafra/postgresql2websocket `_ - Websockets -* `matthewmueller/pg-bridge `_ - Amazon SNS -* `aweber/pgsql-listen-exchange `_ - RabbitMQ -* `SpiderOak/skeeter `_ - ZeroMQ -* `FGRibreau/postgresql-to-amqp `_ - AMQP -* `daurnimator/pg-kinesis-bridge `_ - Amazon Kinesis +* `pg-bridge `_ - Amazon SNS +* `pg-kinesis-bridge `_ - Amazon Kinesis +* `pg-notify-webhook `_ - trigger webhooks from PostgreSQL's LISTEN/NOTIFY +* `pgsql-listen-exchange `_ - RabbitMQ +* `postgres-websockets `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY +* `postgresql-to-amqp `_ - AMQP +* `postgresql2websocket `_ - Websockets +* `skeeter `_ - ZeroMQ .. _eco_extensions: @@ -63,45 +62,45 @@ These are PostgreSQL bridges that propagate LISTEN/NOTIFY to external queues for Extensions ---------- +* `aiodata `_ - Python, event-based proxy and caching client. * `pg-safeupdate `_ - prevent full-table updates or deletes -* `srid/spas `_ - allow file uploads and basic auth -* `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server -* `wildsurfer/postgrest-oauth-server `_ - OAuth2 server -* `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware -* `criles25/postgrest-auth `_ - email based auth/signup -* `ppKrauss/PostgREST-writeAPI `_ - generate nginx rewrite rules to fit an OpenAPI spec -* `seveibar/postgrest-node `_ - Run a PostgREST server in Node.js via npm module -* `Exahilosys/aiodata `_ - Python, event-based proxy and caching client. +* `postgrest-auth (criles25) `_ - email based auth/signup +* `postgrest-auth (svmotn) `_ - OAuth2-inspired external auth server +* `postgrest-node `_ - Run a PostgREST server in Node.js via npm module +* `postgrest-oauth `_ - OAuth2 WAI middleware +* `postgrest-oauth/api `_ - OAuth2 server +* `PostgREST-writeAPI `_ - generate nginx rewrite rules to fit an OpenAPI spec +* `spas `_ - allow file uploads and basic auth .. _clientside_libraries: Client-Side Libraries --------------------- -* `supabase/postgrest-js `_ - TypeScript/JavaScript -* `supabase/postgrest-rs `_ - Rust -* `supabase/postgrest-dart `_ - Dart -* `supabase/postgrest-py `_ - Python -* `supabase/postgrest-csharp `_ - C# -* `supabase/postgrest-kt `_ - Kotlin -* `supabase/postgrest-swift `_ - Swift -* `technowledgy/vue-postgrest `_ - Vue.js -* `SocialGouv/postgrester `_ - JS + Typescript -* `Kong/py-postgrest `_ - Python -* `datrium/postgrest-pyclient `_ - Python -* `tomberek/aor-postgrest-client `_ - JS, admin-on-rest -* `hugomrdias/postgrest-url `_ - JS, just for generating query URLs -* `john-kelly/elm-postgrest `_ - Elm -* `mithril.postgrest `_ - JS, Mithril -* `lewisjared/postgrest-request `_ - JS, SuperAgent -* `JarvusInnovations/jarvus-postgrest-apikit `_ - JS, Sencha framework -* `davidthewatson/postgrest_python_requests_client `_ - Python -* `calebmer/postgrest-client `_ - JS -* `clesiemo3/postgrestR `_ - R -* `PierreRochard/postgrest-angular `_ - TypeScript, generate UI from API description -* `thejettdurham/postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp -* `team142/ng-postgrest `_ - Angular app for browsing, editing data exposed over PostgREST. -* `andytango/redux-postgrest `_ - TypeScript/JS, client integrated with (React) Redux. +* `aor-postgrest-client `_ - JS, admin-on-rest +* `elm-postgrest `_ - Elm +* `general-angular `_ - TypeScript, generate UI from API description +* `jarvus-postgrest-apikit `_ - JS, Sencha framework +* `mithril-postgrest `_ - JS, Mithril +* `ng-postgrest `_ - Angular app for browsing, editing data exposed over PostgREST. +* `postgrest-client `_ - JS +* `postgrest-csharp `_ - C# +* `postgrest-dart `_ - Dart +* `postgrest-js `_ - TypeScript/JavaScript +* `postgrest-kt `_ - Kotlin +* `postgrest-py `_ - Python +* `postgrest-pyclient `_ - Python +* `postgrest-request `_ - JS, SuperAgent +* `postgrest-rs `_ - Rust +* `postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp +* `postgrest-swift `_ - Swift +* `postgrest-url `_ - JS, just for generating query URLs +* `postgrestR `_ - R +* `postgrest_python_requests_client `_ - Python +* `postgrester `_ - JS + Typescript +* `py-postgrest `_ - Python +* `redux-postgrest `_ - TypeScript/JS, client integrated with (React) Redux. +* `vue-postgrest `_ - Vue.js .. _eco_commercial: diff --git a/how-tos/casting-type-to-custom-json.rst b/how-tos/casting-type-to-custom-json.rst index 1cc6061279..b4687bad2f 100644 --- a/how-tos/casting-type-to-custom-json.rst +++ b/how-tos/casting-type-to-custom-json.rst @@ -4,11 +4,11 @@ Casting a type to a custom JSON object :author: `steve-chavez `_ While using PostgREST you might have noticed that certain PostgreSQL types translate to JSON strings when you would -have expected a JSON object or array. For example, let's see the case of `range types `_. +have expected a JSON object or array. For example, let's see the case of `range types `_. .. code-block:: postgres - -- example taken from https://www.postgresql.org/docs/11/rangetypes.html#RANGETYPES-EXAMPLES + -- example taken from https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-EXAMPLES create table reservations ( room int , during tsrange diff --git a/how-tos/providing-images-for-img.rst b/how-tos/providing-images-for-img.rst index a90c7939ee..5a1463d5ec 100644 --- a/how-tos/providing-images-for-img.rst +++ b/how-tos/providing-images-for-img.rst @@ -38,7 +38,7 @@ We can retrieve this image in binary format from our PostgREST API by requesting Unfortunately, putting the URL into the :code:`src` of an :code:`` tag will not work. That's because browsers do not send the required header. -Luckily, we can configure our `Nginx reverse proxy <../admin.html>`_ to fix this problem for us. +Luckily, we can configure our :ref:`Nginx reverse proxy ` to fix this problem for us. We assume that PostgREST is running on port 3000. We provide a new location :code:`/files/` that redirects requests to our endpoint with the :code:`Accept` header set to :code:`application/octet-stream`. diff --git a/index.rst b/index.rst index c071b055bc..5614f859fa 100644 --- a/index.rst +++ b/index.rst @@ -8,7 +8,7 @@ PostgREST Documentation .. image:: https://img.shields.io/github/stars/postgrest/postgrest.svg?style=social :target: https://github.com/PostgREST/postgrest -.. image:: https://img.shields.io/github/release/PostgREST/postgrest.svg +.. 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 @@ -39,11 +39,11 @@ Sponsors :width: 13em .. image:: _static/retool.png - :target: https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest + :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest :width: 13em .. image:: _static/supabase.png - :target: https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage + :target: https://supabase.io/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage :width: 13em | @@ -71,7 +71,7 @@ PostgREST has a focused scope. It works well with other tools like Nginx. This f Getting Support ---------------- -The project has a friendly and growing community. Join our `chat room `_ for discussion and help. You can also report or search for bugs/features on the Github `issues `_ page. +The project has a friendly and growing community. Join our `chat room `_ for discussion and help. You can also report or search for bugs/features on the Github `issues `_ page. .. toctree:: :glob: @@ -204,25 +204,21 @@ In Production Here are some companies that use PostgREST in production. -* `Sompani `_ +* `Catarse `_ * `Datrium `_ -* `Supabase `_ -* `Nimbus `_ - - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. -* `Catarse `_ -* `Moat `_ -* `Netwo `_ -* `Redsmin `_ -* `Image-charts `_ -* `MotionDynamic - Fast highly dynamic video generation at scale `_ -* `Drip Depot `_ -* `Convene `_ by Thomson-Reuters -* `eGull `_ +* `Drip Depot `_ +* `eGull `_ * `Elyios `_ -* `Simply Connected Systems `_ - -.. * `OpenBooking `_ -.. * `triggerFS - A realtime messaging and distributed trigger system `_ +* `Image-charts `_ +* `Moat `_ +* `MotionDynamic - Fast highly dynamic video generation at scale `_ +* `Netwo `_ +* `Nimbus `_ + - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. +* `OpenBooking `_ +* `Redsmin `_ +* `Sompani `_ +* `Supabase `_ Testimonials ------------ @@ -264,11 +260,6 @@ Testimonials -- Anupam Garg, Datrium, Inc. -Translations ------------- - -* `Chinese `_ (latest version ``v0.4.2.0``) - Contributing ------------ diff --git a/install.rst b/install.rst index 87d650c9de..8704afd40f 100644 --- a/install.rst +++ b/install.rst @@ -18,7 +18,7 @@ If you use **FreeBSD**, then you can install PostgREST from the `official ports pkg install hs-postgrest -If you use **Arch Linux**, then you can install PostgREST from the `community repo `_. +If you use **Arch Linux**, then you can install PostgREST from the `community repo `_. .. code:: bash @@ -30,7 +30,7 @@ If you use **Nix**, then you can install PostgREST from nixpkgs. nix-env -i haskellPackages.postgrest -If you use Windows, you can install PostgREST using `Chocolatey `_ or `Scoop `_. +If you use Windows, you can install PostgREST using `Chocolatey `_ or `Scoop `_. .. code:: bash @@ -252,7 +252,7 @@ Assuming you're making modifications locally and then pushing to GitHub, it's ea 1. Create a new app on Heroku 2. In Settings add the following buildpack :code:`https://github.com/PostgREST/postgrest-heroku` -3. Add the require Config Vars in Heroku (see https://github.com/PostgREST/postgrest/blob/master/app.json#L7-L57 for more details) +3. Add the require Config Vars in Heroku (see https://github.com/PostgREST/postgrest/blob/main/app.json for more details) 4. Modify your ``postgrest.conf`` file as required to match your Config Vars in Heroku 5. Create your :code:`Procfile` and add :code:`./env-to-config ./postgrest postgrest.conf` 6. Push your changes to GitHub diff --git a/releases/upcoming.rst b/releases/upcoming.rst index c7fb082c90..b7fa223234 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -25,5 +25,5 @@ Changed ------- * Docker images are now optimized to be built from the scratch image. This reduces the compressed image size from over 30 MB to about 4 MB. - For more details, see `Docker image built with Nix `_. + For more details, see `Docker image built with Nix `_. |br| -- `@monacoremo `_ diff --git a/releases/v5.2.0.rst b/releases/v5.2.0.rst index d49067b394..7cc90611b0 100644 --- a/releases/v5.2.0.rst +++ b/releases/v5.2.0.rst @@ -1,7 +1,7 @@ v5.2.0 ====== -* `Explicit qualification `_ introduced in ``v5.0`` is no longer necessary, this section will not be included from this version onwards. A :ref:`db-extra-search-path` configuration parameter was introduced to avoid the need to explictly qualify database objects. If you install PostgreSQL extensions on the ``public`` schema, they'll work normally from now on. +* `Explicit qualification `_ introduced in ``v5.0`` is no longer necessary, this section will not be included from this version onwards. A :ref:`db-extra-search-path` configuration parameter was introduced to avoid the need to explictly qualify database objects. If you install PostgreSQL extensions on the ``public`` schema, they'll work normally from now on. * Now you can filter :ref:`tabs-cols-w-spaces`. @@ -23,4 +23,4 @@ This release was made possible thanks to: * Victor Adossi * Petr Beles -If you like to join them please consider `supporting PostgREST development `_. +If you like to join them please consider `supporting PostgREST development `_. diff --git a/releases/v6.0.2.rst b/releases/v6.0.2.rst index 90cdfb514a..88a73ee5c8 100644 --- a/releases/v6.0.2.rst +++ b/releases/v6.0.2.rst @@ -13,7 +13,7 @@ Added * Ignoring payload keys for insert/update can be now done with the ``?columns`` query parameter. See :ref:`specify_columns`. |br| -- `@steve-chavez `_ -* `websearch_to_tsquery `_ can now be used +* `websearch_to_tsquery `_ can now be used through the ``wfts`` operator. See :ref:`fts`. |br| -- `@herulume `_ @@ -62,7 +62,7 @@ This release is sponsored by: :width: 13em .. image:: ../_static/retool.png - :target: https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest + :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest :width: 13em * `Daniel Babiak `_ @@ -76,4 +76,4 @@ This release is sponsored by: * Christopher Reid * Nathan Bouscal -If you like to join them please consider `supporting PostgREST development `_. +If you like to join them please consider `supporting PostgREST development `_. diff --git a/releases/v7.0.0.rst b/releases/v7.0.0.rst index d725790863..34e5d46db6 100644 --- a/releases/v7.0.0.rst +++ b/releases/v7.0.0.rst @@ -85,7 +85,7 @@ This release was made possible thanks to: :width: 13em .. image:: ../_static/retool.png - :target: https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest + :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest :width: 13em * `Daniel Babiak `_ @@ -103,4 +103,4 @@ This release was made possible thanks to: * David Fenko -If you like to join them please consider `supporting PostgREST development `_. +If you like to join them please consider `supporting PostgREST development `_. diff --git a/releases/v7.0.1.rst b/releases/v7.0.1.rst index 6adc232358..5186216e5d 100644 --- a/releases/v7.0.1.rst +++ b/releases/v7.0.1.rst @@ -48,7 +48,7 @@ This release was made possible thanks to: :width: 13em .. image:: ../_static/retool.png - :target: https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest + :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest :width: 13em * `Daniel Babiak `_ @@ -66,4 +66,4 @@ This release was made possible thanks to: * David Fenko -If you'd like to join them, consider `supporting PostgREST development `_. +If you'd like to join them, consider `supporting PostgREST development `_. diff --git a/schema_structure.rst b/schema_structure.rst index c45f30f32b..d21b9332ef 100644 --- a/schema_structure.rst +++ b/schema_structure.rst @@ -8,7 +8,7 @@ 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. +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. @@ -20,7 +20,7 @@ This allows you to change the internals of your schema and maintain backwards co 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: +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 @@ -36,7 +36,7 @@ This will change the privileges for all functions created in the future in all s 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. + 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: @@ -73,12 +73,12 @@ Another option is to define the function with the :code:`SECURITY DEFINER` optio end; $$ language plpgsql security definer; -Note the ``SECURITY DEFINER`` keywords at the end of the function. See `PostgreSQL documentation `_ for more details. +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 `_ will be bypassed unless a different, non-SUPERUSER owner is specified. +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 `_ will be bypassed unless a different, non-SUPERUSER owner is specified. For changing this, we can create a non-SUPERUSER role and make this role the view's owner. @@ -90,7 +90,7 @@ For changing this, we can create a non-SUPERUSER role and make this role the vie Rules ----- -Insertion on views with complex `rules `_ might not work out of the box with PostgREST. +Insertion on views with complex `rules `_ might not work out of the box with PostgREST. It's recommended that you `use triggers instead of rules `_. 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 `_. diff --git a/shell.nix b/shell.nix index 81649475d8..4800d62ec1 100644 --- a/shell.nix +++ b/shell.nix @@ -13,5 +13,6 @@ pkgs.mkShell { docs.serve docs.spellcheck docs.dictcheck + docs.linkcheck ]; } diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index a0c49bac2d..4d39ca5e71 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -23,9 +23,9 @@ As you begin the tutorial, pop open the project `chat room `_. Next, let's pull and start the database image: +If Docker is not installed, you can get it `here `_. Next, let's pull and start the database image: .. code-block:: bash @@ -109,7 +109,7 @@ You should see the psql command prompt: postgres=# -The first thing we'll do is create a `named schema `_ 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. +The first thing we'll do is create a `named schema `_ 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 From aa38221fc16d04eb5e8e82e3659e62b6d7d1340d Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 4 Apr 2021 16:06:45 +0200 Subject: [PATCH 387/549] Add linkcheck to CI --- .circleci/config.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2d7162fb6b..0cf4e87839 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,6 +25,18 @@ jobs: name: Run spellcheck command: postgrest-docs-spellcheck + linkcheck: + docker: + - image: nixos/nix:2.3 + steps: + - checkout + - run: + name: Install linkcheck script + command: nix-env -f default.nix -iA linkcheck + - run: + name: Run linkcheck + command: postgrest-docs-linkcheck + workflows: check: jobs: From d5b9f6cc8877e31e516ec18dac3127777cff3f2d Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Tue, 6 Apr 2021 11:09:33 +0200 Subject: [PATCH 388/549] fix postgrest repo link in CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 20fdb43dbe..be92f4d7a9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,3 @@ This repository follows the same contribution guidelines as the main PostgREST repository contribution guidelines: -https://github.com/PostgREST/postgrest/blob/master/.github/CONTRIBUTING.md +https://github.com/PostgREST/postgrest/blob/main/.github/CONTRIBUTING.md From 8273e270f2c594e609a4bbc371945f98bad9d095 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Tue, 6 Apr 2021 15:43:32 +0200 Subject: [PATCH 389/549] Change http to https in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9f964a975d..29050fb9a6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# PostgREST documentation http://postgrest.org/ +# 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. From 7e4810f0a6a2ccf72248762a51c2ebe31c242fc1 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Tue, 6 Apr 2021 15:51:32 +0200 Subject: [PATCH 390/549] change Master to Main in heroku deploy steps --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 8704afd40f..6a682d8fd5 100644 --- a/install.rst +++ b/install.rst @@ -256,5 +256,5 @@ Assuming you're making modifications locally and then pushing to GitHub, it's ea 4. Modify your ``postgrest.conf`` file as required to match your Config Vars in Heroku 5. Create your :code:`Procfile` and add :code:`./env-to-config ./postgrest postgrest.conf` 6. Push your changes to GitHub -7. Set Heroku to automatically deploy from Master and then manually deploy the branch for the first build +7. Set Heroku to automatically deploy from Main and then manually deploy the branch for the first build From 535ebba219c0a564e68accf4e8eece99ecda92fd Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 9 Apr 2021 23:33:50 -0500 Subject: [PATCH 391/549] Remove subzero link --- ecosystem.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ecosystem.rst b/ecosystem.rst index 9344ba0f13..7761678340 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -101,10 +101,3 @@ Client-Side Libraries * `py-postgrest `_ - Python * `redux-postgrest `_ - TypeScript/JS, client integrated with (React) Redux. * `vue-postgrest `_ - Vue.js - -.. _eco_commercial: - -Commercial ---------------- - -* `subZero `_ - Automated GraphQL & REST API with built-in caching (powered in part by PostgREST) From 038fcc02b2eb96036ea85ad21ad517bb0b0acfff Mon Sep 17 00:00:00 2001 From: Amanda Date: Mon, 19 Apr 2021 01:04:34 -0700 Subject: [PATCH 392/549] Adding a hint to Hello World This note helps people who are hitting this error `{"hint":null,"details":null,"code":"42P01","message":"relation \"api.todos\" does not exist"}` https://github.com/PostgREST/postgrest/issues/1009 --- tutorials/tut0.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index 4d39ca5e71..04e6420a30 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -165,7 +165,10 @@ PostgREST uses a configuration file to tell it how to connect to the database. C db-schema = "api" db-anon-role = "web_anon" -The configuration file has other :ref:`options `, but this is all we need. Now run the server: +The configuration file has other :ref:`options `, 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 From 27680197106e48686ef4d3716b30391f38413b09 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 21 Apr 2021 17:00:44 -0500 Subject: [PATCH 393/549] Add GNUHost as a sponsor --- _static/empty.png | Bin 0 -> 468 bytes _static/gnuhost.png | Bin 0 -> 9360 bytes index.rst | 11 +++++++++++ 3 files changed, 11 insertions(+) create mode 100644 _static/empty.png create mode 100644 _static/gnuhost.png diff --git a/_static/empty.png b/_static/empty.png new file mode 100644 index 0000000000000000000000000000000000000000..99fabe47fdacf17f9341a7150d38ddb3d609bc36 GIT binary patch literal 468 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 KMNUMnLSTZ?dYi)l literal 0 HcmV?d00001 diff --git a/index.rst b/index.rst index 5614f859fa..380746df81 100644 --- a/index.rst +++ b/index.rst @@ -42,10 +42,21 @@ Sponsors :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest :width: 13em +.. image:: _static/gnuhost.png + :target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + .. image:: _static/supabase.png :target: https://supabase.io/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage :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 + | Motivation From 92c5066a79f8a8298febece5da8d1d34d6b7b59b Mon Sep 17 00:00:00 2001 From: Lee Yi Jie Joel Date: Fri, 28 May 2021 21:45:50 +0800 Subject: [PATCH 394/549] docs: add elixir client library --- ecosystem.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ecosystem.rst b/ecosystem.rst index 7761678340..5cc044ec8d 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -101,3 +101,6 @@ Client-Side Libraries * `py-postgrest `_ - Python * `redux-postgrest `_ - TypeScript/JS, client integrated with (React) Redux. * `vue-postgrest `_ - Vue.js +* `postgrest-ex `_ - Elixir + + From 07cc6547eefb3532ce18a91530cb5ccf3f702830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Murat=20Mirg=C3=BCn=20ERCAN?= <45563173+muratmirgun@users.noreply.github.com> Date: Fri, 28 May 2021 22:21:04 +0300 Subject: [PATCH 395/549] Add Supabase-go Library Ecosystem --- ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem.rst b/ecosystem.rst index 5cc044ec8d..92507af6c1 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -86,6 +86,7 @@ Client-Side Libraries * `postgrest-client `_ - JS * `postgrest-csharp `_ - C# * `postgrest-dart `_ - Dart +* `postgrest-go `_ - Go * `postgrest-js `_ - TypeScript/JavaScript * `postgrest-kt `_ - Kotlin * `postgrest-py `_ - Python From 574e57d01af583e937fe5b70c240dc5feb4360b8 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Tue, 6 Apr 2021 10:25:19 +0200 Subject: [PATCH 396/549] Upgrade nixpkgs to enable linkcheck in CI --- .circleci/config.yml | 2 +- default.nix | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0cf4e87839..36c6ba737f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -42,4 +42,4 @@ workflows: jobs: - build - spellcheck - + - linkcheck diff --git a/default.nix b/default.nix index a571acffb4..039ae825bf 100644 --- a/default.nix +++ b/default.nix @@ -1,9 +1,9 @@ let # Commit of the Nixpkgs repository that we want to use. nixpkgsVersion = { - date = "2021-04-04"; - rev = "c0e881852006b132236cbf0301bd1939bb50867e"; - tarballHash = "0fy7z7yxk5n7yslsvx5cyc6h21qwi4bhxf3awhirniszlbvaazy2"; + date = "2021-06-02"; + rev = "84aa23742f6c72501f9cc209f29c438766f5352d"; + tarballHash = "0h7xl6q0yjrbl9vm3h6lkxw692nm8bg3wy65gm95a2mivhrdjpxp"; }; # Nix files that describe the Nixpkgs repository. We evaluate the expression From 7447b03295e83ea922b171a2c0b5b14bfae0c22b Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Wed, 2 Jun 2021 09:10:24 +0200 Subject: [PATCH 397/549] Fix broken links --- api.rst | 2 +- auth.rst | 4 ++-- configuration.rst | 4 ++-- index.rst | 3 +-- releases/upcoming.rst | 2 +- 5 files changed, 7 insertions(+), 8 deletions(-) diff --git a/api.rst b/api.rst index d5951818af..7e2c8d9af8 100644 --- a/api.rst +++ b/api.rst @@ -472,7 +472,7 @@ If we make a similar request on ``bigtable``, which has 3573458 rows, we would g Response Format --------------- -PostgREST uses proper HTTP content negotiation (`RFC7231 `_) to deliver the desired representation of a resource. That is to say the same API endpoint can respond in different formats like JSON or CSV depending on the client request. +PostgREST uses proper HTTP content negotiation (`RFC7231 `_) to deliver the desired representation of a resource. That is to say the same API endpoint can respond in different formats like JSON or CSV depending on the client request. Use the Accept request header to specify the acceptable format (or formats) for the response: diff --git a/auth.rst b/auth.rst index 7451060437..3d0c915bf3 100644 --- a/auth.rst +++ b/auth.rst @@ -208,7 +208,7 @@ To use Auth0, create `an application `_ for .. note:: - Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata `_. Then, you will need to write `a rule `_ that will extract the role from the user's app_metadata and set it as a `custom claim `_ in the access token. Note that, you may use Auth0's `core authorization feature `_ for more complex use cases. Metadata solution is mentioned here for simplicity. + Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata `_. Then, you will need to write `a rule `_ that will extract the role from the user's app_metadata and set it as a `custom claim `_ in the access token. Note that, you may use Auth0's `core authorization feature `_ for more complex use cases. Metadata solution is mentioned here for simplicity. .. code:: javascript @@ -272,7 +272,7 @@ 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 `_ is voiced in detail `elsewhere on the web `_. 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. +The critique against the `JWT standard `_ is voiced in detail `elsewhere on the web `_. 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 `_. Recommendations about high quality libraries for usage in API clients can be found on `jwt.io `_. diff --git a/configuration.rst b/configuration.rst index dddc9d0d59..b7f4e475d6 100644 --- a/configuration.rst +++ b/configuration.rst @@ -203,14 +203,14 @@ openapi-server-proxy-uri jwt-secret ---------- - The secret or `JSON Web Key (JWK) (or set) `_ 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`. + The secret or `JSON Web Key (JWK) (or set) `_ 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`. .. _jwt-aud: jwt-aud ------- - Specifies the `JWT audience claim `_. 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. + Specifies the `JWT audience claim `_. 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. .. _secret-is-base64: diff --git a/index.rst b/index.rst index 380746df81..b375f8b670 100644 --- a/index.rst +++ b/index.rst @@ -21,7 +21,7 @@ PostgREST Documentation :target: https://www.patreon.com/postgrest .. image:: https://img.shields.io/badge/Donate-PayPal-green.svg - :target: https://www.paypal.me/postgrest + :target: https://www.paypal.com/paypalme/postgrest | @@ -219,7 +219,6 @@ Here are some companies that use PostgREST in production. * `Datrium `_ * `Drip Depot `_ * `eGull `_ -* `Elyios `_ * `Image-charts `_ * `Moat `_ * `MotionDynamic - Fast highly dynamic video generation at scale `_ diff --git a/releases/upcoming.rst b/releases/upcoming.rst index b7fa223234..698e4f4ed7 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -25,5 +25,5 @@ Changed ------- * Docker images are now optimized to be built from the scratch image. This reduces the compressed image size from over 30 MB to about 4 MB. - For more details, see `Docker image built with Nix `_. + For more details, see `Docker image built with Nix `_. |br| -- `@monacoremo `_ From 8e0b67ed09fde863c9412bc5353f171145e00107 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Wed, 2 Jun 2021 09:18:49 +0200 Subject: [PATCH 398/549] Sort ecosystem --- ecosystem.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ecosystem.rst b/ecosystem.rst index 92507af6c1..c89d12820e 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -86,6 +86,7 @@ Client-Side Libraries * `postgrest-client `_ - JS * `postgrest-csharp `_ - C# * `postgrest-dart `_ - Dart +* `postgrest-ex `_ - Elixir * `postgrest-go `_ - Go * `postgrest-js `_ - TypeScript/JavaScript * `postgrest-kt `_ - Kotlin @@ -96,12 +97,10 @@ Client-Side Libraries * `postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp * `postgrest-swift `_ - Swift * `postgrest-url `_ - JS, just for generating query URLs -* `postgrestR `_ - R * `postgrest_python_requests_client `_ - Python * `postgrester `_ - JS + Typescript +* `postgrestR `_ - R * `py-postgrest `_ - Python * `redux-postgrest `_ - TypeScript/JS, client integrated with (React) Redux. * `vue-postgrest `_ - Vue.js -* `postgrest-ex `_ - Elixir - From a9dbd9e14f42493442a09e33cc5145b7a641fcf3 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Thu, 3 Jun 2021 16:51:45 +0200 Subject: [PATCH 399/549] Add user-agent to sphinx config to fix some linkchecks --- conf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conf.py b/conf.py index 29f4382cb4..71122e56a5 100644 --- a/conf.py +++ b/conf.py @@ -289,3 +289,7 @@ 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' + From 326019cca2ba7fca378c632608bf15a61002424a Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Mon, 14 Jun 2021 19:30:22 -0500 Subject: [PATCH 400/549] Add reference for OPTIONS on the API page (#407) * Add CORS documentation * Add options requests to upcoming page --- api.rst | 51 +++++++++++++++++++++++++++++++++++++++++++ postgrest.dict | 2 ++ releases/upcoming.rst | 5 +++++ 3 files changed, 58 insertions(+) diff --git a/api.rst b/api.rst index 7e2c8d9af8..7617b0841d 100644 --- a/api.rst +++ b/api.rst @@ -1452,6 +1452,57 @@ You can use a tool like `Swagger UI `_ to The OpenAPI information can go out of date as the schema changes under a running server. To learn how to refresh the cache see :ref:`schema_reloading`. +.. _options_requests: + +OPTIONS +======= + +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 example, the OPTIONS request and response for a table named ``people`` are: + +.. code-block:: http + + OPTIONS /people HTTP/1.1 + +.. 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 `_ | + +--------------------+-------------------------------------------------------------------------------------------------+ + +For database function endpoints, OPTIONS requests are not supported. + +.. 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`. + +CORS +---- + +PostgREST sets highly permissive cross origin resource sharing, that is why it accepts Ajax requests from any domain. + .. _multiple-schemas: Switching Schemas diff --git a/postgrest.dict b/postgrest.dict index 0d043e8058..6530644f4b 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -17,6 +17,7 @@ centric changelog ClojureScript config +CORS cryptographically CSV Daemonizing @@ -146,6 +147,7 @@ UI ui unicode unix +updatable UPSERT uri url diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 698e4f4ed7..1034cfbf54 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -12,9 +12,14 @@ Added * Allow HTTP status override through the :ref:`response.status ` GUC. |br| -- `@steve-chavez `_ + * Allow :ref:`s_procs_variadic`. |br| -- `@wolfgangwalther `_ +* Documentation improvements + + + Added the :ref:`OPTIONS requests ` section. + Fixed ----- From f7f3aadab86b4fb9c3498f349266ad560beb6049 Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Fri, 18 Jun 2021 12:10:31 -0500 Subject: [PATCH 401/549] Reorganize the Schema Cache information into a separate reference page (#404) --- admin.rst | 50 +--------------- api.rst | 14 +---- index.rst | 7 +++ releases/upcoming.rst | 2 + schema_cache.rst | 131 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 143 insertions(+), 61 deletions(-) create mode 100644 schema_cache.rst diff --git a/admin.rst b/admin.rst index 2a8d03d4ff..03b7579c1a 100644 --- a/admin.rst +++ b/admin.rst @@ -196,58 +196,10 @@ Restart the database and watch the log file in real-time to understand how HTTP docker run -v "$(pwd)/init.sh":"/docker-entrypoint-initdb.d/init.sh" -d postgres docker logs -f -.. _schema_reloading: - Schema Reloading ---------------- -Users are often confused by PostgREST's database schema cache. It is present because detecting foreign key relationships between tables (including how those relationships pass through views) is necessary, but costly. API requests consult the schema cache as part of :ref:`resource_embedding`. However if the schema changes while the server is running it results in a stale cache and leads to errors claiming that no relations are detected between tables. - -.. important:: - - Since v5.0, PostgREST also makes use of the schema cache for stored functions metadata: parameters, return type, volatility. - It also uses the schema cache for resolving overloaded functions. You should refresh the cache if a change in any of the prior is done. - -To refresh the cache without restarting the PostgREST server, send the server process a SIGUSR1 signal: - -.. code:: bash - - killall -SIGUSR1 postgrest - -.. note:: - - To refresh the cache in docker: - - .. code:: bash - - docker kill -s SIGUSR1 - - # or in docker-compose - docker-compose kill -s SIGUSR1 - -The above is the manual way to do it. To automate the schema reloads, use a database trigger like this: - -.. code-block:: postgresql - - CREATE OR REPLACE FUNCTION public.notify_ddl_postgrest() - RETURNS event_trigger - LANGUAGE plpgsql - AS $$ - BEGIN - NOTIFY ddl_command_end; - END; - $$; - - CREATE EVENT TRIGGER ddl_postgrest ON ddl_command_end - EXECUTE PROCEDURE public.notify_ddl_postgrest(); - -Then run the `pg_listen `_ utility to monitor for that event and send a SIGUSR1 when it occurs: - -.. code-block:: bash - - pg_listen ddl_command_end $(which killall) -SIGUSR1 postgrest - -Now, whenever the structure of the database schema changes, PostgreSQL will notify the ``ddl_command_end`` channel, which will cause ``pg_listen`` to send PostgREST the signal to reload its cache. Note that pg_listen requires full path to the executable in the example above. +Changing the schema while the server is running can lead to errors due to a stale schema cache. To learn how to refresh the cache see :ref:`schema_reloading`. Daemonizing =========== diff --git a/api.rst b/api.rst index 7617b0841d..ae9006734d 100644 --- a/api.rst +++ b/api.rst @@ -990,6 +990,8 @@ In this case, only **source**, **publication_date** and **figure** will be inser 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. +.. _upsert: + UPSERT ------ @@ -1109,18 +1111,6 @@ For instance, assume we have created this function in the database. Whenever you create or change a function you must refresh PostgREST's schema cache. See the section :ref:`schema_reloading`. - If the schema cache is not refreshed, PostgREST will assume :code:`text` as the default type for function arguments. This could - lead to getting error responses like: - - .. code-block:: json - - { - "hint":"No function matches the given name and argument types. You might need to add explicit type casts.", - "details":null, - "code":"42883", - "message":"function test.add_them(a => text, b => text) does not exist" - } - The client can call it by posting an object like .. code-block:: http diff --git a/index.rst b/index.rst index b375f8b670..8b8e69e9ed 100644 --- a/index.rst +++ b/index.rst @@ -127,8 +127,15 @@ Technical references for PostgREST's functionality. configuration.rst +.. toctree:: + :caption: Schema Cache + :hidden: + + schema_cache.rst + - :doc:`API ` - :doc:`configuration` +- :doc:`Schema Cache ` Topic guides ------------ diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 1034cfbf54..a3e10dc9fd 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -19,6 +19,8 @@ Added * Documentation improvements + Added the :ref:`OPTIONS requests ` section. + + Added the :ref:`schema_cache` section. + + Moved the :ref:`schema_reloading` reference from :ref:`admin` to :ref:`schema_cache` Fixed ----- diff --git a/schema_cache.rst b/schema_cache.rst new file mode 100644 index 0000000000..a57cf83f33 --- /dev/null +++ b/schema_cache.rst @@ -0,0 +1,131 @@ +.. _schema_cache: + +Schema Cache +============ + +PostgREST caches metadata from the database schema to avoid repeating expensive queries. This metadata is not required by all of the PostgREST features, only the following: + ++--------------------------------------------+-------------------------------------------------------------------------------+ +| Feature | Required Metadata | ++============================================+===============================================================================+ +| :ref:`resource_embedding` | Foreign key constraints | ++--------------------------------------------+-------------------------------------------------------------------------------+ +| :ref:`Stored Functions ` | Function signature (parameters, return type, volatility and | +| | `overloading `_) | ++--------------------------------------------+-------------------------------------------------------------------------------+ +| :ref:`Upserts ` | Primary keys | ++--------------------------------------------+-------------------------------------------------------------------------------+ +| :ref:`Insertions ` | Primary keys (optional: only if the Location header is requested) | ++--------------------------------------------+-------------------------------------------------------------------------------+ +| :ref:`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 | ++--------------------------------------------+-------------------------------------------------------------------------------+ + +The Stale Schema Cache +---------------------- + +When you make changes on the metadata mentioned above, the schema cache will turn stale on a running PostgREST. Future requests that use the above features will need the :ref:`schema cache to be reloaded `; otherwise, you'll get an error instead of the expected result. + +For instance, let's see what would happen if you have a stale schema for foreign key relationships and function signature: + +Stale Foreign Key Relationships +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Suppose you add a ``cities`` table to your database. This table has a foreign key referencing an existing ``countries`` table. Then, you make a request to get the ``cities`` and their belonging ``countries``: + +.. code-block:: http + + GET /cities?select=name,country:countries(id,name) HTTP/1.1 + +But instead, you get an error message that looks like this: + +.. code-block:: json + + { + "hint": "If a new foreign key between these entities was created in the database, try reloading the schema cache.", + "message": "Could not find a relationship between cities and countries in the schema cache" + } + +As you can see, PostgREST couldn't find the newly created foreign key in the schema cache. See the section :ref:`schema_reloading` to solve this issue. + +Stale Function Signature +~~~~~~~~~~~~~~~~~~~~~~~~ + +Suppose you create the following function while PostgREST is running: + +.. code-block:: plpgsql + + CREATE FUNCTION plus_one(num integer) + RETURNS integer AS $$ + SELECT num + 1; + $$ LANGUAGE SQL IMMUTABLE; + +Then, you make this request: + +.. code-block:: http + + GET /rpc/plus_one?num=1 HTTP/1.1 + +On a stale schema, PostgREST will assume :code:`text` as the default type for the function argument ``num``. Thus, the response you get is: + +.. code-block:: json + + { + "hint":"No function matches the given name and argument types. You might need to add explicit type casts.", + "details":null, + "code":"42883", + "message":"function test.plus_one(num => text) does not exist" + } + +See the section :ref:`schema_reloading` to solve this issue. + +.. _schema_reloading: + +Schema Cache Reloading +---------------------- + +To refresh the cache without restarting the PostgREST server, send the server process a SIGUSR1 signal: + +.. code:: bash + + killall -SIGUSR1 postgrest + +.. note:: + + To refresh the cache in docker: + + .. code:: bash + + docker kill -s SIGUSR1 + + # or in docker-compose + docker-compose kill -s SIGUSR1 + +The above is the manual way to do it. To automate cache reloads, use a database trigger like this: + +.. code-block:: postgresql + + CREATE OR REPLACE FUNCTION public.notify_ddl_postgrest() + RETURNS event_trigger + LANGUAGE plpgsql + AS $$ + BEGIN + NOTIFY ddl_command_end; + END; + $$; + + CREATE EVENT TRIGGER ddl_postgrest ON ddl_command_end + EXECUTE PROCEDURE public.notify_ddl_postgrest(); + +Then run the `pg_listen `_ utility to monitor for that event and send a SIGUSR1 when it occurs: + +.. code-block:: bash + + pg_listen ddl_command_end $(which killall) -SIGUSR1 postgrest + +Now, whenever the structure of the database changes, PostgreSQL will notify the ``ddl_command_end`` channel, which will cause ``pg_listen`` to send PostgREST the signal to reload its cache. Note that pg_listen requires full path to the executable in the example above. From 3f2a58ab7657c82ddb71a095c7a71fd0f4d2fc7b Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 12 Jul 2021 17:04:55 -0500 Subject: [PATCH 402/549] Add Oblivious as a sponsor --- _static/oblivious.jpg | Bin 0 -> 27612 bytes index.rst | 10 +++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 _static/oblivious.jpg diff --git a/_static/oblivious.jpg b/_static/oblivious.jpg new file mode 100644 index 0000000000000000000000000000000000000000..955e1a57d324c5a4423db4a66d8096ed4cf4a7ea GIT binary patch literal 27612 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�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 literal 0 HcmV?d00001 diff --git a/index.rst b/index.rst index 8b8e69e9ed..795d1a4796 100644 --- a/index.rst +++ b/index.rst @@ -50,12 +50,16 @@ Sponsors :target: https://supabase.io/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage :width: 13em +.. image:: _static/oblivious.jpg + :target: https://oblivious.ai/?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 +.. .. image:: _static/empty.png + :target: #sponsors + :width: 13em | From 8562f48a37d3cf9834d67da7ff05ec096dc4d1b8 Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Tue, 20 Jul 2021 16:15:17 -0500 Subject: [PATCH 403/549] Add openapi-mode configuration option --- api.rst | 8 +++++++- configuration.rst | 36 +++++++++++++++++++++++++++++------- releases/upcoming.rst | 3 +++ 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/api.rst b/api.rst index ae9006734d..735aa29402 100644 --- a/api.rst +++ b/api.rst @@ -1410,7 +1410,13 @@ This follows the same rules as :ref:`binary_output`. OpenAPI Support =============== -Every API hosted by PostgREST automatically serves a full `OpenAPI `_ 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. For extra customization, the OpenAPI output contains a "description" field for every `SQL comment `_ on any database object. For instance, +Every API hosted by PostgREST automatically serves a full `OpenAPI `_ 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 `_ on any database object. For instance, .. code-block:: sql diff --git a/configuration.rst b/configuration.rst index b7f4e475d6..960002b674 100644 --- a/configuration.rst +++ b/configuration.rst @@ -30,12 +30,12 @@ The user specified in the db-uri is also known as the authenticator role. For mo Here is the full list of configuration parameters. -======================== ======= ========= ======== -Name Type Default Required -======================== ======= ========= ======== -db-uri String Y -db-schema String Y -db-anon-role String Y +======================== ======= ================= ======== +Name Type Default Required +======================== ======= ================= ======== +db-uri String Y +db-schema String Y +db-anon-role String Y db-pool Int 10 db-pool-timeout Int 10 db-extra-search-path String public @@ -43,6 +43,7 @@ server-host String !4 server-port Int 3000 server-unix-socket String server-unix-socket-mode String 660 +openapi-mode String follow-privileges openapi-server-proxy-uri String jwt-secret String jwt-aud String @@ -52,7 +53,7 @@ pre-request String app.settings.* String role-claim-key String .role raw-media-types String -======================== ======= ========= ======== +======================== ======= ================= ======== .. _db-uri: @@ -175,6 +176,27 @@ server-unix-socket-mode server-unix-socket-mode = "660" +.. _openapi-mode: + +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-server-proxy-uri: openapi-server-proxy-uri diff --git a/releases/upcoming.rst b/releases/upcoming.rst index a3e10dc9fd..398370f05c 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -16,6 +16,9 @@ Added * Allow :ref:`s_procs_variadic`. |br| -- `@wolfgangwalther `_ +* Config options for showing a full OpenAPI output regardless of the JWT role privileges and for disabling it altogether. See :ref:`openapi-mode`. + |br| -- `@steve-chavez `_ + * Documentation improvements + Added the :ref:`OPTIONS requests ` section. From 204b0ed25950162df36b130b1d668409b096f691 Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Tue, 20 Jul 2021 16:40:45 -0500 Subject: [PATCH 404/549] Add improved error messages for RPC on a stale schema Error message for a not found RPC. Unsupported overloaded RPC with the same argument names but different types. --- api.rst | 4 ++++ releases/upcoming.rst | 4 ++++ schema_cache.rst | 14 +++++++------- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/api.rst b/api.rst index 735aa29402..2dea57d3fe 100644 --- a/api.rst +++ b/api.rst @@ -1346,6 +1346,10 @@ You can call overloaded functions with different number of arguments. GET /rpc/rental_duration?customer_id=232&from_date=2018-07-01 HTTP/1.1 +.. important:: + + Overloaded functions with the same argument names but different types are not supported. + .. _binary_output: Binary Output diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 398370f05c..cfebed6d28 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -37,3 +37,7 @@ Changed * Docker images are now optimized to be built from the scratch image. This reduces the compressed image size from over 30 MB to about 4 MB. For more details, see `Docker image built with Nix `_. |br| -- `@monacoremo `_ + +* Improved error message for a not found RPC on a stale schema (see :ref:`stale_function_signature`) and for the unsupported case of + overloaded functions with the same argument names but different types. + |br| -- `@laurenceisla `_ diff --git a/schema_cache.rst b/schema_cache.rst index a57cf83f33..9e994c75eb 100644 --- a/schema_cache.rst +++ b/schema_cache.rst @@ -53,6 +53,8 @@ But instead, you get an error message that looks like this: As you can see, PostgREST couldn't find the newly created foreign key in the schema cache. See the section :ref:`schema_reloading` to solve this issue. +.. _stale_function_signature: + Stale Function Signature ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -71,16 +73,14 @@ Then, you make this request: GET /rpc/plus_one?num=1 HTTP/1.1 -On a stale schema, PostgREST will assume :code:`text` as the default type for the function argument ``num``. Thus, the response you get is: +Next, PostgREST tries to find the function on the stale schema to no avail: .. code-block:: json - { - "hint":"No function matches the given name and argument types. You might need to add explicit type casts.", - "details":null, - "code":"42883", - "message":"function test.plus_one(num => text) does not exist" - } + { + "hint": "If a new function was created in the database with this name and arguments, try reloading the schema cache.", + "message": "Could not find the api.plus_one(num) function in the schema cache" + } See the section :ref:`schema_reloading` to solve this issue. From a7cddd8ecebcb74e8c3a2026eda4fa121f68c830 Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Wed, 21 Jul 2021 14:40:34 -0500 Subject: [PATCH 405/549] Add log-level config option --- configuration.rst | 24 +++++++++++++++++++++++- releases/upcoming.rst | 6 ++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/configuration.rst b/configuration.rst index 960002b674..577d14ae9f 100644 --- a/configuration.rst +++ b/configuration.rst @@ -43,6 +43,7 @@ server-host String !4 server-port Int 3000 server-unix-socket String server-unix-socket-mode String 660 +log-level String error openapi-mode String follow-privileges openapi-server-proxy-uri String jwt-secret String @@ -176,12 +177,33 @@ server-unix-socket-mode server-unix-socket-mode = "660" +.. _log-level: + +log-level +--------- + + 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 = "warning" + + # All the "warning" level events plus all requests (every status code) are logged + log-level "info" + .. _openapi-mode: openapi-mode ------------ - Specifies how the OpenAPI output should be displayed: + Specifies how the OpenAPI output should be displayed. .. code:: bash diff --git a/releases/upcoming.rst b/releases/upcoming.rst index cfebed6d28..8de3d97b1d 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -19,6 +19,9 @@ Added * Config options for showing a full OpenAPI output regardless of the JWT role privileges and for disabling it altogether. See :ref:`openapi-mode`. |br| -- `@steve-chavez `_ +* Config option for logging level. See :ref:`log-level`. + |br| -- `@steve-chavez `_ + * Documentation improvements + Added the :ref:`OPTIONS requests ` section. @@ -41,3 +44,6 @@ Changed * Improved error message for a not found RPC on a stale schema (see :ref:`stale_function_signature`) and for the unsupported case of overloaded functions with the same argument names but different types. |br| -- `@laurenceisla `_ + +* Modified the default logging level from ``info`` to ``error``. See :ref:`log-level`. + |br| -- `@steve-chavez `_ \ No newline at end of file From f5c5094d7dbc8ae6efa787211f3cbb03a90d51a4 Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Wed, 21 Jul 2021 19:49:41 -0500 Subject: [PATCH 406/549] Add db-tx-end config option --- configuration.rst | 23 ++++++++++++++++++++++- postgrest.dict | 1 + releases/upcoming.rst | 3 +++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/configuration.rst b/configuration.rst index 577d14ae9f..b6b9db3d64 100644 --- a/configuration.rst +++ b/configuration.rst @@ -39,6 +39,7 @@ db-anon-role String Y db-pool Int 10 db-pool-timeout Int 10 db-extra-search-path String public +db-tx-end String commit server-host String !4 server-port Int 3000 server-unix-socket String @@ -133,6 +134,27 @@ db-extra-search-path Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``. +.. _db-tx-end: + +db-tx-end +--------- + + 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" + .. _server-host: server-host @@ -317,4 +339,3 @@ raw-media-types raw-media-types="image/png, text/xml" - diff --git a/postgrest.dict b/postgrest.dict index 6530644f4b..6d86f45b20 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -142,6 +142,7 @@ todo todos Tsingson tsquery +tx TypeScript UI ui diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 8de3d97b1d..4a8f356880 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -22,6 +22,9 @@ Added * Config option for logging level. See :ref:`log-level`. |br| -- `@steve-chavez `_ +* Config option for specifying how to terminate the transactions (allowing rollbacks, useful for testing). See :ref:`db-tx-end`. + |br| -- `@wolfgangwalther `_ + * Documentation improvements + Added the :ref:`OPTIONS requests ` section. From 41325fd9ba6856fb45e1232be6e2dbe6a5189c37 Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Fri, 23 Jul 2021 11:41:47 -0500 Subject: [PATCH 407/549] Add db-prepared-statements config option --- admin.rst | 13 +++++++++++++ configuration.rst | 12 ++++++++++++ postgrest.dict | 2 ++ releases/upcoming.rst | 6 ++++++ 4 files changed, 33 insertions(+) diff --git a/admin.rst b/admin.rst index 03b7579c1a..4a1c7ab263 100644 --- a/admin.rst +++ b/admin.rst @@ -120,6 +120,19 @@ The burst argument tells Nginx to start dropping requests if more than five queu 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. +.. _connection_poolers: + +Using Connection Poolers +------------------------ + +In order to increase performance, PostgREST uses prepared statements by default. However, this setting is incompatible with connection poolers such as PgBouncer working in transaction pooling mode. In this case, you need to set the :ref:`db-prepared-statements` config option to ``false``. On the other hand, session pooling is fully compatible with PostgREST, while statement pooling is not compatible at all. + +.. note:: + + If prepared statements are enabled, PostgREST will quit after detecting that transaction or statement pooling is being used. + +You should also set the ``db-channel-enabled`` config option to ``false``, due to the ``LISTEN`` command not being compatible with transaction pooling, although it should not give any errors if it's left enabled by default. + Debugging ========= diff --git a/configuration.rst b/configuration.rst index b6b9db3d64..066b117be4 100644 --- a/configuration.rst +++ b/configuration.rst @@ -39,6 +39,7 @@ db-anon-role String Y db-pool Int 10 db-pool-timeout Int 10 db-extra-search-path String public +db-prepared-statements Boolean True db-tx-end String commit server-host String !4 server-port Int 3000 @@ -134,6 +135,17 @@ db-extra-search-path Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``. +.. _db-prepared-statements: + +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 a connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section ` for more information. + .. _db-tx-end: db-tx-end diff --git a/postgrest.dict b/postgrest.dict index 6d86f45b20..7c36449399 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -88,6 +88,7 @@ ov passphrase Pelletier Petr +PgBouncer pgcrypto pgjwt pgSQL @@ -95,6 +96,7 @@ phfts phraseto plainto plfts +poolers PostGIS PostgreSQL PostgreSQL's diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 4a8f356880..d35310ceb6 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -16,12 +16,18 @@ Added * Allow :ref:`s_procs_variadic`. |br| -- `@wolfgangwalther `_ +* Allow :ref:`connection_poolers` such as PgBouncer in transaction pooling mode. + |br| -- `@laurenceisla `_ + * Config options for showing a full OpenAPI output regardless of the JWT role privileges and for disabling it altogether. See :ref:`openapi-mode`. |br| -- `@steve-chavez `_ * Config option for logging level. See :ref:`log-level`. |br| -- `@steve-chavez `_ +* Config option for enabling or disabling prepared statements. See :ref:`db-prepared-statements`. + |br| -- `@steve-chavez `_ + * Config option for specifying how to terminate the transactions (allowing rollbacks, useful for testing). See :ref:`db-tx-end`. |br| -- `@wolfgangwalther `_ From 77faf9e9bc6088be3916eac0e1a1e2e1897eb62c Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Fri, 23 Jul 2021 17:15:30 -0500 Subject: [PATCH 408/549] Add explicit headers-only POST request using Prefer header and default the request to minimal --- api.rst | 2 +- releases/upcoming.rst | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 2dea57d3fe..3ec32d019b 100644 --- a/api.rst +++ b/api.rst @@ -889,7 +889,7 @@ To create a row in a database table post a JSON object whose keys are the names { "col1": "value1", "col2": "value2" } -If the table has a primary key, the response will include a :code:`Location` header describing where to find the new object. If the table is write-only then constructing the Location header will cause a permissions error. To successfully insert an item to a write-only table you will need to suppress the Location response header by including the request header :code:`Prefer: return=minimal`. +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. 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. diff --git a/releases/upcoming.rst b/releases/upcoming.rst index d35310ceb6..81c3a8a575 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -16,6 +16,9 @@ Added * Allow :ref:`s_procs_variadic`. |br| -- `@wolfgangwalther `_ +* Allow sending the header ``Prefer: headers-only`` to get a response with a ``Location`` header. See :ref:`insert_update`. + |br| -- `@laurenceisla `_ + * Allow :ref:`connection_poolers` such as PgBouncer in transaction pooling mode. |br| -- `@laurenceisla `_ @@ -55,4 +58,8 @@ Changed |br| -- `@laurenceisla `_ * Modified the default logging level from ``info`` to ``error``. See :ref:`log-level`. - |br| -- `@steve-chavez `_ \ No newline at end of file + |br| -- `@steve-chavez `_ + +* POST requests for insertions no longer include a ``Location`` header in the response by default and behave the same way as having a + ``Prefer: return=minimal`` header in the request. This prevents permissions errors when having a write-only table. See :ref:`insert_update`. + |br| -- `@laurenceisla `_ From 5cbabe4a2129a48dc3bfc8c8d93e1f09f395da11 Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Sat, 24 Jul 2021 16:27:02 -0500 Subject: [PATCH 409/549] Add database notification functionality and configuration variables for schema reloading --- admin.rst | 2 +- configuration.rst | 18 ++++++++++++++++++ postgrest.dict | 1 + releases/upcoming.rst | 7 +++++++ schema_cache.rst | 42 ++++++++++++++++++++++++++++++------------ 5 files changed, 57 insertions(+), 13 deletions(-) diff --git a/admin.rst b/admin.rst index 4a1c7ab263..7b5e5e95f4 100644 --- a/admin.rst +++ b/admin.rst @@ -131,7 +131,7 @@ In order to increase performance, PostgREST uses prepared statements by default. If prepared statements are enabled, PostgREST will quit after detecting that transaction or statement pooling is being used. -You should also set the ``db-channel-enabled`` config option to ``false``, due to the ``LISTEN`` command not being compatible with transaction pooling, although it should not give any errors if it's left enabled by default. +You should also set the :ref:`db-channel-enabled` config option to ``false``, due to the ``LISTEN`` command not being compatible with transaction pooling, although it should not give any errors if it's left enabled by default. Debugging ========= diff --git a/configuration.rst b/configuration.rst index 066b117be4..bed5a750db 100644 --- a/configuration.rst +++ b/configuration.rst @@ -39,6 +39,8 @@ db-anon-role String Y db-pool Int 10 db-pool-timeout Int 10 db-extra-search-path String public +db-channel String pgrst +db-channel-enabled Boolean True db-prepared-statements Boolean True db-tx-end String commit server-host String !4 @@ -135,6 +137,22 @@ db-extra-search-path Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``. +.. _db-channel: + +db-channel +---------- + + The name of the notification channel that PostgREST uses for :ref:`schema_reloading` and configuration reloading. + +.. _db-channel-enabled: + +db-channel-enabled +------------------ + + 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 a connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section ` for more information. + .. _db-prepared-statements: db-prepared-statements diff --git a/postgrest.dict b/postgrest.dict index 7c36449399..bc12f84384 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -91,6 +91,7 @@ Petr PgBouncer pgcrypto pgjwt +pgrst pgSQL phfts phraseto diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 81c3a8a575..cda81ff1bc 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -16,6 +16,9 @@ Added * Allow :ref:`s_procs_variadic`. |br| -- `@wolfgangwalther `_ +* Allow schema cache reloading using PostgreSQL :ref:`NOTIFY ` command. + |br| -- `@steve-chavez `_ + * Allow sending the header ``Prefer: headers-only`` to get a response with a ``Location`` header. See :ref:`insert_update`. |br| -- `@laurenceisla `_ @@ -53,6 +56,10 @@ Changed For more details, see `Docker image built with Nix `_. |br| -- `@monacoremo `_ +* The ``pg_listen`` `utility `_ is no longer needed to automatically reload the schema cache + and it's replaced entirely by database notifications. See :ref:`schema_reloading_notify`. + |br| -- `@steve-chavez `_ + * Improved error message for a not found RPC on a stale schema (see :ref:`stale_function_signature`) and for the unsupported case of overloaded functions with the same argument names but different types. |br| -- `@laurenceisla `_ diff --git a/schema_cache.rst b/schema_cache.rst index 9e994c75eb..fe684b7081 100644 --- a/schema_cache.rst +++ b/schema_cache.rst @@ -89,7 +89,7 @@ See the section :ref:`schema_reloading` to solve this issue. Schema Cache Reloading ---------------------- -To refresh the cache without restarting the PostgREST server, send the server process a SIGUSR1 signal: +To refresh the cache without restarting the PostgREST server, send a SIGUSR1 signal to the server process. .. code:: bash @@ -106,26 +106,44 @@ To refresh the cache without restarting the PostgREST server, send the server pr # or in docker-compose docker-compose kill -s SIGUSR1 -The above is the manual way to do it. To automate cache reloads, use a database trigger like this: +.. _schema_reloading_notify: + +Reloading with NOTIFY +~~~~~~~~~~~~~~~~~~~~~ + +There are environments where you can't send the SIGUSR1 Unix Signal (like on managed containers in cloud services or on Windows systems). For this reason, PostgREST also allows you to reload its schema cache through PostgreSQL `NOTIFY `_ as follows: .. code-block:: postgresql - CREATE OR REPLACE FUNCTION public.notify_ddl_postgrest() - RETURNS event_trigger - LANGUAGE plpgsql + NOTIFY pgrst, 'reload schema' + +The ``"pgrst"`` notification channel is enabled by default. For configuring the channel, see :ref:`db-channel` and :ref:`db-channel-enabled`. + +Automatic schema cache reloading +******************************** + +You can do automatic schema cache reloading in a pure SQL way with an `event trigger `_ and ``NOTIFY``. + +.. code-block:: postgresql + + -- Create an event trigger function + CREATE OR REPLACE FUNCTION public.pgrst_watch() RETURNS event_trigger + LANGUAGE plpgsql AS $$ BEGIN - NOTIFY ddl_command_end; + NOTIFY pgrst; END; $$; - CREATE EVENT TRIGGER ddl_postgrest ON ddl_command_end - EXECUTE PROCEDURE public.notify_ddl_postgrest(); + -- This event trigger will fire after every ddl_command_end event + CREATE EVENT TRIGGER pgrst_watch + ON ddl_command_end + EXECUTE PROCEDURE public.pgrst_watch(); -Then run the `pg_listen `_ utility to monitor for that event and send a SIGUSR1 when it occurs: +Now, whenever the ``pgrst_watch`` trigger is fired in the database, PostgREST will automatically reload the schema cache. -.. code-block:: bash +To disable auto reloading, drop the trigger: - pg_listen ddl_command_end $(which killall) -SIGUSR1 postgrest +.. code-block:: postgresql -Now, whenever the structure of the database changes, PostgreSQL will notify the ``ddl_command_end`` channel, which will cause ``pg_listen`` to send PostgREST the signal to reload its cache. Note that pg_listen requires full path to the executable in the example above. + DROP EVENT TRIGGER pgrst_watch \ No newline at end of file From 2e50a22e641e077444a0619bf836df2baa8676e7 Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Sat, 24 Jul 2021 18:52:52 -0500 Subject: [PATCH 410/549] Add config reloading with SIGUSR2 --- configuration.rst | 34 ++++++++++++++++++++++++++++++++++ releases/upcoming.rst | 3 +++ 2 files changed, 37 insertions(+) diff --git a/configuration.rst b/configuration.rst index bed5a750db..56aee47132 100644 --- a/configuration.rst +++ b/configuration.rst @@ -60,6 +60,40 @@ role-claim-key String .role raw-media-types String ======================== ======= ================= ======== +.. _config_reloading: + +Configuration Reloading +----------------------- + +To reload the configuration without restarting the PostgREST server send a SIGUSR2 signal to the server process. + +.. code:: bash + + killall -SIGUSR2 postgrest + +.. note:: + + To refresh the cache in docker: + + .. code:: bash + + docker kill -s SIGUSR2 + + # or in docker-compose + docker-compose kill -s SIGUSR2 + +.. important:: + + The following settings will not be reread when reloading the configuration. You will need to restart PostgREST in that case. + + * ``db-uri`` + * ``db-pool`` + * ``db-pool-timeout`` + * ``server-host`` + * ``server-port`` + * ``server-unix-socket`` + * ``server-unix-socket-mode`` + .. _db-uri: db-uri diff --git a/releases/upcoming.rst b/releases/upcoming.rst index cda81ff1bc..b8cf803e32 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -25,6 +25,9 @@ Added * Allow :ref:`connection_poolers` such as PgBouncer in transaction pooling mode. |br| -- `@laurenceisla `_ +* Allow :ref:`config_reloading` by sending a SIGUSR2 signal. + |br| -- `@steve-chavez `_ + * Config options for showing a full OpenAPI output regardless of the JWT role privileges and for disabling it altogether. See :ref:`openapi-mode`. |br| -- `@steve-chavez `_ From 6909e3435e3d0f3599ba345b03d3dbb4c3fbc02f Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Sat, 24 Jul 2021 19:42:47 -0500 Subject: [PATCH 411/549] Add changelog for embedding views recursively --- api.rst | 4 ++-- releases/upcoming.rst | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 3ec32d019b..246d17a20f 100644 --- a/api.rst +++ b/api.rst @@ -695,8 +695,6 @@ It's also possible to embed `Materialized Views `_ +* Allow :ref:`embedding_view_chains` recursively to any depth. + |br| -- `@wolfgangwalther `_ + * Allow schema cache reloading using PostgreSQL :ref:`NOTIFY ` command. |br| -- `@steve-chavez `_ From c4387ec7dd2964c02fc3f79788e3b9b0fd41d120 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Mon, 2 Aug 2021 18:45:12 +0200 Subject: [PATCH 412/549] Mention environment variables in the configuration section --- configuration.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/configuration.rst b/configuration.rst index 56aee47132..e1138ac181 100644 --- a/configuration.rst +++ b/configuration.rst @@ -60,6 +60,8 @@ role-claim-key String .role raw-media-types String ======================== ======= ================= ======== +You can also set these configuration parameters using environment variables. They are capitalized, have a ``PGRST_`` prefix, and use underscores. For example: ``PGRST_DB_URI`` corresponds to ``db-uri``. + .. _config_reloading: Configuration Reloading From 20bee2ec34c641f45bfb389580b869953b2bb380 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 2 Aug 2021 18:33:50 -0500 Subject: [PATCH 413/549] Fix log-level value for status 4xx errors --- configuration.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration.rst b/configuration.rst index e1138ac181..9a3711ef2e 100644 --- a/configuration.rst +++ b/configuration.rst @@ -281,9 +281,9 @@ log-level log-level = "error" # All the "error" level events plus request errors (status 4xx) are logged - log-level = "warning" + log-level = "warn" - # All the "warning" level events plus all requests (every status code) are logged + # All the "warn" level events plus all requests (every status code) are logged log-level "info" .. _openapi-mode: From 33f0da0509b2c2785a2a30f2189da54def8c062c Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sun, 25 Jul 2021 23:59:38 -0500 Subject: [PATCH 414/549] Add no downtime schema cache reload --- releases/upcoming.rst | 3 +++ schema_cache.rst | 15 +++++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/releases/upcoming.rst b/releases/upcoming.rst index e64afadbb5..3ab9d21c68 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -19,6 +19,9 @@ Added * Allow :ref:`embedding_view_chains` recursively to any depth. |br| -- `@wolfgangwalther `_ +* No downtime when reloading the schema cache. See the note in :ref:`schema_reloading`. + |br| -- `@steve-chavez `_ + * Allow schema cache reloading using PostgreSQL :ref:`NOTIFY ` command. |br| -- `@steve-chavez `_ diff --git a/schema_cache.rst b/schema_cache.rst index fe684b7081..df8804d21d 100644 --- a/schema_cache.rst +++ b/schema_cache.rst @@ -95,16 +95,19 @@ To refresh the cache without restarting the PostgREST server, send a SIGUSR1 sig killall -SIGUSR1 postgrest -.. note:: - To refresh the cache in docker: +For docker you can do: + +.. code:: bash - .. code:: bash + docker kill -s SIGUSR1 - docker kill -s SIGUSR1 + # or in docker-compose + docker-compose kill -s SIGUSR1 + +.. note:: - # or in docker-compose - docker-compose kill -s SIGUSR1 + There's no downtime when reloading the schema cache. The reloading will happen on a background thread while requests keep being served. .. _schema_reloading_notify: From 9543e746ec977001b716bb45ef147958e594a154 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 26 Jul 2021 01:11:43 -0500 Subject: [PATCH 415/549] Mention log-level=crit/error increase throughput --- configuration.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/configuration.rst b/configuration.rst index 9a3711ef2e..5f074ac87f 100644 --- a/configuration.rst +++ b/configuration.rst @@ -286,6 +286,9 @@ log-level # 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 From 57d725bab6fc935126d43f22be91a71af57e0e76 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 26 Jul 2021 01:44:34 -0500 Subject: [PATCH 416/549] Add Bearer without capitalization --- auth.rst | 4 ++++ releases/upcoming.rst | 3 +++ 2 files changed, 7 insertions(+) diff --git a/auth.rst b/auth.rst index 3d0c915bf3..d00f673682 100644 --- a/auth.rst +++ b/auth.rst @@ -143,6 +143,8 @@ In the function you can run arbitrary code to check the request and raise an exc END $$ LANGUAGE plpgsql; +.. _client_auth: + Client Auth =========== @@ -153,6 +155,8 @@ To make an authenticated request the client must include an :code:`Authorization GET /foo HTTP/1.1 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZSIsImV4cCI6MTQ3NTUxNjI1MH0.GYDZV3yM0gqvuEtJmfpplLBXSGYnke_Pvnl0tbKAjB4 +The ``Bearer`` header value can be used with or without capitalization(``bearer``). + JWT Generation -------------- diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 3ab9d21c68..0c1ee72a68 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -34,6 +34,9 @@ Added * Allow :ref:`config_reloading` by sending a SIGUSR2 signal. |br| -- `@steve-chavez `_ +* Allow ``Bearer`` with and without capitalization as authentication schema. See :ref:`client_auth`. + |br| -- `@wolfgangwalther `_ + * Config options for showing a full OpenAPI output regardless of the JWT role privileges and for disabling it altogether. See :ref:`openapi-mode`. |br| -- `@steve-chavez `_ From 52cdd505617e260056240e4cd38bd1ef4acb9576 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 26 Jul 2021 02:14:37 -0500 Subject: [PATCH 417/549] Show timestamps for server diagnostic information --- admin.rst | 21 ++++++++++++++++++++- releases/upcoming.rst | 3 +++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/admin.rst b/admin.rst index 7b5e5e95f4..7ab4f1277d 100644 --- a/admin.rst +++ b/admin.rst @@ -141,10 +141,27 @@ Server Version When debugging a problem it's important to verify the PostgREST version. At any time you can make a request to the running server and determine exactly which version is deployed. Look for the :code:`Server` HTTP response header, which contains the version number. +.. _pgrst_logging: + Logging ------- -The PostgREST server logs basic request information to stdout, including the requesting IP address and user agent, the URL requested, and HTTP response status. However this provides limited information for debugging server errors. It's helpful to get full information about both client requests and the corresponding SQL commands executed against the underlying database. +PostgREST logs basic request information to ``stdout``, including the requesting IP address and user agent, the URL requested, and HTTP response status. + +.. code-block:: + + 127.0.0.1 - - [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 - "" "curl/7.64.0" + 127.0.0.1 - - [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-block:: + + 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:: @@ -157,6 +174,8 @@ The PostgREST server logs basic request information to stdout, including the req # another option is to pipe the output into "logger -t postgrest" +PostgREST logging provides limited information for debugging server errors. It's helpful to get full information about both client requests and the corresponding SQL commands executed against the underlying database. + HTTP Requests ------------- diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 0c1ee72a68..7c86d907e9 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -37,6 +37,9 @@ Added * Allow ``Bearer`` with and without capitalization as authentication schema. See :ref:`client_auth`. |br| -- `@wolfgangwalther `_ +* Show timestamps for server diagnostic information. See :ref:`pgrst_logging`. + |br| -- `@steve-chavez `_ + * Config options for showing a full OpenAPI output regardless of the JWT role privileges and for disabling it altogether. See :ref:`openapi-mode`. |br| -- `@steve-chavez `_ From 8d4bc76aab358eb50d6ef61f2d9bceffa6f1e0ae Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 26 Jul 2021 02:46:44 -0500 Subject: [PATCH 418/549] Add entry for OPTIONS on views * Also clarify the schema cache page --- releases/upcoming.rst | 6 ++++-- schema_cache.rst | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 7c86d907e9..fde6b391dd 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -37,6 +37,9 @@ Added * Allow ``Bearer`` with and without capitalization as authentication schema. See :ref:`client_auth`. |br| -- `@wolfgangwalther `_ +* Allow OPTIONS to generate HTTP methods based on views triggers. See :ref:`OPTIONS requests `. + |br| -- `@laurenceisla `_ + * Show timestamps for server diagnostic information. See :ref:`pgrst_logging`. |br| -- `@steve-chavez `_ @@ -54,8 +57,7 @@ Added * Documentation improvements - + Added the :ref:`OPTIONS requests ` section. - + Added the :ref:`schema_cache` section. + + Added the :ref:`schema_cache` page. + Moved the :ref:`schema_reloading` reference from :ref:`admin` to :ref:`schema_cache` Fixed diff --git a/schema_cache.rst b/schema_cache.rst index df8804d21d..e03359a8c1 100644 --- a/schema_cache.rst +++ b/schema_cache.rst @@ -3,7 +3,10 @@ Schema Cache ============ -PostgREST caches metadata from the database schema to avoid repeating expensive queries. This metadata is not required by all of the PostgREST features, only the following: +Certain PostgREST features require metadata from the database schema. Getting this metadata requires executing expensive queries, so +in order to avoid repeating this work, PostgREST uses a schema cache. + +The following features are the ones that require metadata from the schema cache. +--------------------------------------------+-------------------------------------------------------------------------------+ | Feature | Required Metadata | From 698db875908fdcdeaf407e44d21041d005ecf713 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 26 Jul 2021 03:01:00 -0500 Subject: [PATCH 419/549] Reorder configuration reloading --- configuration.rst | 70 ++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/configuration.rst b/configuration.rst index 5f074ac87f..c7355f0fb1 100644 --- a/configuration.rst +++ b/configuration.rst @@ -9,6 +9,10 @@ PostgREST reads a configuration file to determine information about the database ./postgrest /path/to/postgrest.conf +.. note:: + + Configuration can be reloaded without restarting the server. See :ref:`config_reloading`. + The configuration file must contain a set of key value pairs. At minimum you must include these keys: .. code:: @@ -62,40 +66,6 @@ raw-media-types String You can also set these configuration parameters using environment variables. They are capitalized, have a ``PGRST_`` prefix, and use underscores. For example: ``PGRST_DB_URI`` corresponds to ``db-uri``. -.. _config_reloading: - -Configuration Reloading ------------------------ - -To reload the configuration without restarting the PostgREST server send a SIGUSR2 signal to the server process. - -.. code:: bash - - killall -SIGUSR2 postgrest - -.. note:: - - To refresh the cache in docker: - - .. code:: bash - - docker kill -s SIGUSR2 - - # or in docker-compose - docker-compose kill -s SIGUSR2 - -.. important:: - - The following settings will not be reread when reloading the configuration. You will need to restart PostgREST in that case. - - * ``db-uri`` - * ``db-pool`` - * ``db-pool-timeout`` - * ``server-host`` - * ``server-port`` - * ``server-unix-socket`` - * ``server-unix-socket-mode`` - .. _db-uri: db-uri @@ -408,3 +378,35 @@ raw-media-types raw-media-types="image/png, text/xml" +.. _config_reloading: + +Configuration Reloading +======================= + +To reload the configuration without restarting the PostgREST server send a SIGUSR2 signal to the server process. + +.. code:: bash + + killall -SIGUSR2 postgrest + +To refresh the cache in docker: + +.. code:: bash + + docker kill -s SIGUSR2 + + # or in docker-compose + docker-compose kill -s SIGUSR2 + +.. important:: + + The following settings will not be reread when reloading the configuration. You will need to restart PostgREST in that case. + + * ``db-uri`` + * ``db-pool`` + * ``db-pool-timeout`` + * ``server-host`` + * ``server-port`` + * ``server-unix-socket`` + * ``server-unix-socket-mode`` + From 254a2f7f36efd687561593773d06b00c34d14e2e Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 26 Jul 2021 04:39:02 -0500 Subject: [PATCH 420/549] Add in-db config plus reloading --- configuration.rst | 63 ++++++++++++++++++++++++++++++++++++++----- releases/upcoming.rst | 3 +++ schema_cache.rst | 2 +- 3 files changed, 60 insertions(+), 8 deletions(-) diff --git a/configuration.rst b/configuration.rst index c7355f0fb1..d70869d5a5 100644 --- a/configuration.rst +++ b/configuration.rst @@ -47,6 +47,7 @@ db-channel String pgrst db-channel-enabled Boolean True db-prepared-statements Boolean True db-tx-end String commit +db-config Boolean True server-host String !4 server-port Int 3000 server-unix-socket String @@ -191,6 +192,13 @@ db-tx-end # The transaction is rolled back unless a "Prefer: tx=commit" header is sent db-tx-end = "rollback-allow-override" +.. _db-config: + +db-config +--------- + + Enables the in-database configuration. + .. _server-host: server-host @@ -402,11 +410,52 @@ To refresh the cache in docker: The following settings will not be reread when reloading the configuration. You will need to restart PostgREST in that case. - * ``db-uri`` - * ``db-pool`` - * ``db-pool-timeout`` - * ``server-host`` - * ``server-port`` - * ``server-unix-socket`` - * ``server-unix-socket-mode`` + * :ref:`db-uri` + * :ref:`db-pool` + * :ref:`db-pool-timeout` + * :ref:`server-host` + * :ref:`server-port` + * :ref:`server-unix-socket` + * :ref:`server-unix-socket-mode` + +.. _in_db_config: + +In-Database Configuration +========================= + +By adding settings to the **authenticator** role (see :ref:`roles`), you can make the database the single source of truth for PostgREST's configuration. +This is enabled by :ref:`db-config`. + +For example, you can configure :ref:`db-schema` and :ref:`jwt-secret` like this: + +.. code:: postgresql + + ALTER ROLE authenticator SET pgrst.db_schema = "tenant1, tenant2, tenant3" + ALTER ROLE authenticator SET pgrst.jwt_secret = "REALLYREALLYREALLYREALLYVERYSAFE" + +.. important:: + + For altering a role in this way, you need a SUPERUSER. You might not be able to use this configuration mode on cloud-hosted databases. + +Note that underscores(``_``) need to be used instead of dashes(``-``) for the options when the configuration is inside the database. + +When using both the configuration file and the in-database configuration, the latter takes precedence. + +.. danger:: + + If direct connections to the database are allowed, then it's not safe to use the in-db configuration for storing the :ref:`jwt-secret`. + The settings of every role are PUBLIC - they can be viewed by any user that queries the ``pg_catalog.pg_db_role_setting`` table. + In this case you should keep the :ref:`jwt-secret` in the configuration file or as environment variables. + +.. _in_db_config_reloading: + +In-database configuration reloading +----------------------------------- + +To reload the in-database 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. For configuring the channel, see :ref:`db-channel` and :ref:`db-channel-enabled`. diff --git a/releases/upcoming.rst b/releases/upcoming.rst index fde6b391dd..81f8bbe88f 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -37,6 +37,9 @@ Added * Allow ``Bearer`` with and without capitalization as authentication schema. See :ref:`client_auth`. |br| -- `@wolfgangwalther `_ +* :ref:`in_db_config` that can be :ref:`reloaded with NOTIFY `. + |br| -- `@steve-chavez `_ + * Allow OPTIONS to generate HTTP methods based on views triggers. See :ref:`OPTIONS requests `. |br| -- `@laurenceisla `_ diff --git a/schema_cache.rst b/schema_cache.rst index e03359a8c1..04c8c84bfa 100644 --- a/schema_cache.rst +++ b/schema_cache.rst @@ -92,7 +92,7 @@ See the section :ref:`schema_reloading` to solve this issue. Schema Cache Reloading ---------------------- -To refresh the cache without restarting the PostgREST server, send a SIGUSR1 signal to the server process. +To reload the cache without restarting the PostgREST server, send a SIGUSR1 signal to the server process. .. code:: bash From 12f8852183f2cb0120d2abd992d98a6bed1d2926 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 26 Jul 2021 20:32:03 -0500 Subject: [PATCH 421/549] Add fixes and sponsors to upcoming page --- postgrest.dict | 9 ++++ releases/upcoming.rst | 107 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/postgrest.dict b/postgrest.dict index bc12f84384..a5c4b6d0c4 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -46,6 +46,7 @@ HMAC Homebrew HTTPS HV +Ibarluzea ilike io IP @@ -55,6 +56,7 @@ JSON JWK JWT jwt +JWTs Kinesis Kofi localhost @@ -86,6 +88,7 @@ openapi ORM ov passphrase +Pawel Pelletier Petr PgBouncer @@ -111,18 +114,22 @@ RabbitMQ Rafaj RDS reallyreallyreallyreallyverysafe +Rechkemmer Redux refactor +Remo requester's RESTful RestSharp RLS RPC RSA +Saleeba savepoint schemas Sencha Serverless +Severin SHA signup SIGUSR @@ -136,6 +143,7 @@ SSL stateful stdout Stolarz +subselect SuperAgent syslog systemd @@ -146,6 +154,7 @@ todos Tsingson tsquery tx +Tyll TypeScript UI ui diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 81f8bbe88f..21c84dc300 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -2,10 +2,10 @@
    -Upcoming -======== +v8.0.0 +====== -These are changes yet unreleased. If you'd like to try them out before a new official release, you can use a `nightly release `_. +You can download this release at the `PostgREST v8.0.0 release page `_. Added ----- @@ -69,6 +69,60 @@ Fixed * Fix showing UNKNOWN on ``postgrest --help`` invocation. |br| -- `@monacoremo `_ +* Removed single column restriction to allow composite foreign keys in join tables. + |br| -- `@goteguru `_ + +* Fix how the PostgREST version is shown in the help text when the .git directory is not available. + |br| -- `@monacoremo `_ + +* Fix expired JWTs starting an empty transaction on the db. + |br| -- `@steve-chavez `_ + +* Fix location header for POST request with ``select=`` without PK. + |br| -- `@wolfgangwalther `_ + +* Fix error messages on connection failure for localized PostgreSQL on Windows. + |br| -- `@wolfgangwalther `_ + +* Fix ``application/octet-stream`` appending ``charset=utf-8``. + |br| -- `@steve-chavez `_ + +* Fix overloading of functions with unnamed arguments. + |br| -- `@wolfgangwalther `_ + +* Return ``405 Method not Allowed`` for GET of volatile RPC instead of 500. + |br| -- `@wolfgangwalther `_ + +* Fix RPC return type handling and embedding for domains with composite base type. + |br| -- `@wolfgangwalther `_ + +* Fix embedding through views that have COALESCE with subselect. + |br| -- `@wolfgangwalther `_ + +* Fix parsing of boolean config values for Docker environment variables, now it accepts double quoted truth values ``("true", "false")`` and numbers ``("1", "0")``. + |br| -- `@wolfgangwalther `_ + +* Fix using ``app.settings.xxx`` config options in Docker, now they can be used as ``PGRST_APP_SETTINGS_xxx``. + |br| -- `@wolfgangwalther `_ + +* Fix panic when attempting to run with unix socket on non-unix host and properly close unix domain socket on exit. + |br| -- `@monacoremo `_ + +* Disregard internal junction (in non-exposed schema) when embedding. + |br| -- `@steve-chavez `_ + +* Fix requests for overloaded functions from HTML forms to no longer hang. + |br| -- `@laurenceisla `_ + +* Add a hint and clarification to the no relationship found error. + |br| -- `@laurenceisla `_ + +* Show comprehensive error when an RPC is not found in a stale schema cache. + |br| -- `@laurenceisla `_ + +* Fix Location headers in headers only representation for null PK inserts on views. + |br| -- `@laurenceisla `_ + Changed ------- @@ -90,3 +144,50 @@ Changed * POST requests for insertions no longer include a ``Location`` header in the response by default and behave the same way as having a ``Prefer: return=minimal`` header in the request. This prevents permissions errors when having a write-only table. See :ref:`insert_update`. |br| -- `@laurenceisla `_ + +Thanks +------ + +.. 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/2ndquadrant.png + :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo + :width: 13em + +.. image:: ../_static/retool.png + :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + +.. image:: ../_static/gnuhost.png + :target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + +.. image:: ../_static/supabase.png + :target: https://supabase.io/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage + :width: 13em + +.. image:: ../_static/oblivious.jpg + :target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + +* `Daniel Babiak `_ +* Evans Fernandes +* `Jan Sommer `_ +* `Franz Gusenbauer `_ +* Tsingson Qin +* Michel Pelletier +* Jay Hannah +* Robert Stolarz +* Nicholas DiBiase +* Christopher Reid +* Nathan Bouscal +* Daniel Rafaj +* David Fenko +* Remo Rechkemmer +* Severin Ibarluzea +* Tom Saleeba +* Pawel Tyll + +If you like to join them please consider `supporting PostgREST development `_. From 21e54aacda4ed3c77cde9f1f4d07e4d2a6e1eab2 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 26 Jul 2021 20:44:47 -0500 Subject: [PATCH 422/549] Rename upcoming to v8.0.0 --- index.rst | 1 + releases/{upcoming.rst => v8.0.0.rst} | 0 2 files changed, 1 insertion(+) rename releases/{upcoming.rst => v8.0.0.rst} (100%) diff --git a/index.rst b/index.rst index 795d1a4796..eb23a7c92a 100644 --- a/index.rst +++ b/index.rst @@ -217,6 +217,7 @@ Release Notes Here we'll include the most relevant changes so you can migrate to newer versions easily. You can see the full changelog of each release in the `PostgREST repository `_. +- :doc:`releases/v8.0.0` - :doc:`releases/v7.0.0` - :doc:`releases/v6.0.2` - :doc:`releases/v5.2.0` diff --git a/releases/upcoming.rst b/releases/v8.0.0.rst similarity index 100% rename from releases/upcoming.rst rename to releases/v8.0.0.rst From 37c2a8aa7b5c60a2fd1b54ca47ab7c7c4581e065 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Tue, 27 Jul 2021 14:04:30 -0500 Subject: [PATCH 423/549] Make the landing page wider * Add max-width for logos only * Fix margin on sponsor images and add reference to the PostgREST team * Fix information on v8 changelog --- _static/css/custom.css | 12 ++++++---- index.rst | 52 +++++++++++++++++++++++------------------- releases/v8.0.0.rst | 45 ++++++++++++++++++------------------ 3 files changed, 59 insertions(+), 50 deletions(-) diff --git a/_static/css/custom.css b/_static/css/custom.css index cd52bc8483..6015b6d486 100644 --- a/_static/css/custom.css +++ b/_static/css/custom.css @@ -2,10 +2,6 @@ max-width: initial; } -#postgrest-documentation { - max-width: 800px; -} - #postgrest-documentation > h1 { display: none; } @@ -53,3 +49,11 @@ div.line-block { #thanks ul{ text-align: left; } + +.image-container { + max-width: 800px; + display: block; + margin-left: auto; + margin-right: auto; + margin-bottom: 24px; +} diff --git a/index.rst b/index.rst index eb23a7c92a..af28331db9 100644 --- a/index.rst +++ b/index.rst @@ -3,7 +3,9 @@ PostgREST Documentation ======================= -.. figure:: _static/logo.png +.. 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 @@ -30,36 +32,38 @@ PostgREST is a standalone web server that turns your PostgreSQL database directl Sponsors -------- -.. image:: _static/cybertec-new.png - :target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest - :width: 13em +.. 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/2ndquadrant.png - :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo - :width: 13em + .. image:: _static/2ndquadrant.png + :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo + :width: 13em -.. image:: _static/retool.png - :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest - :width: 13em + .. image:: _static/retool.png + :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em -.. image:: _static/gnuhost.png - :target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest - :width: 13em + .. image:: _static/gnuhost.png + :target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em -.. image:: _static/supabase.png - :target: https://supabase.io/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage - :width: 13em + .. image:: _static/supabase.png + :target: https://supabase.io/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage + :width: 13em -.. image:: _static/oblivious.jpg - :target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest - :width: 13em + .. image:: _static/oblivious.jpg + :target: https://oblivious.ai/?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. + .. 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 + .. .. image:: _static/empty.png + :target: #sponsors + :width: 13em | diff --git a/releases/v8.0.0.rst b/releases/v8.0.0.rst index 21c84dc300..55c5ad3731 100644 --- a/releases/v8.0.0.rst +++ b/releases/v8.0.0.rst @@ -72,9 +72,6 @@ Fixed * Removed single column restriction to allow composite foreign keys in join tables. |br| -- `@goteguru `_ -* Fix how the PostgREST version is shown in the help text when the .git directory is not available. - |br| -- `@monacoremo `_ - * Fix expired JWTs starting an empty transaction on the db. |br| -- `@steve-chavez `_ @@ -148,34 +145,38 @@ Changed Thanks ------ -.. image:: ../_static/cybertec-new.png - :target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest - :width: 13em +Big thanks from the `PostgREST team `_ to our sponsors! -.. image:: ../_static/2ndquadrant.png - :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo - :width: 13em +.. container:: image-container -.. image:: ../_static/retool.png - :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest - :width: 13em + .. 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/2ndquadrant.png + :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo + :width: 13em -.. image:: ../_static/supabase.png - :target: https://supabase.io/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage - :width: 13em + .. image:: ../_static/retool.png + :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em -.. image:: ../_static/oblivious.jpg - :target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest - :width: 13em + .. image:: ../_static/gnuhost.png + :target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + + .. image:: ../_static/supabase.png + :target: https://supabase.io/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage + :width: 13em + + .. image:: ../_static/oblivious.jpg + :target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em -* `Daniel Babiak `_ * Evans Fernandes * `Jan Sommer `_ * `Franz Gusenbauer `_ +* `Daniel Babiak `_ * Tsingson Qin * Michel Pelletier * Jay Hannah From 42656edfd9af94a9f2391dba61a743fe8e36c4cf Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 9 Aug 2021 14:04:47 -0500 Subject: [PATCH 424/549] Improve wording in some sections --- api.rst | 9 +++++---- configuration.rst | 4 ++-- index.rst | 2 +- releases/v8.0.0.rst | 2 +- schema_cache.rst | 28 ++++++++++++---------------- 5 files changed, 21 insertions(+), 24 deletions(-) diff --git a/api.rst b/api.rst index 246d17a20f..ecc34487bd 100644 --- a/api.rst +++ b/api.rst @@ -1459,7 +1459,7 @@ OPTIONS 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 example, the OPTIONS request and response for a table named ``people`` are: +For a table named ``people``, OPTIONS would show: .. code-block:: http @@ -1470,7 +1470,7 @@ For example, the OPTIONS request and response for a table named ``people`` are: 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: +For a view, the methods are determined by the presence of INSTEAD OF TRIGGERS. .. table:: :widths: auto @@ -1493,9 +1493,10 @@ For a view, the methods are determined by the presence of INSTEAD OF TRIGGERS: | `auto-updatable views `_ | +--------------------+-------------------------------------------------------------------------------------------------+ -For database function endpoints, OPTIONS requests are not supported. +For functions, OPTIONS requests are not supported. .. 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`. CORS @@ -1583,7 +1584,7 @@ PostgREST reads the ``response.headers`` SQL variable to add extra headers to th SELECT set_config('response.headers', '[{"Cache-Control": "public"}, {"Cache-Control": "max-age=259200"}]', true); - + Notice that the variable 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 to be repeated when setting multiple values and an object would not allow the repeated key. .. note:: diff --git a/configuration.rst b/configuration.rst index d70869d5a5..b758b1265f 100644 --- a/configuration.rst +++ b/configuration.rst @@ -391,13 +391,13 @@ raw-media-types Configuration Reloading ======================= -To reload the configuration without restarting the PostgREST server send a SIGUSR2 signal to the server process. +To reload the configuration without restarting the PostgREST server, send a SIGUSR2 signal to the server process. .. code:: bash killall -SIGUSR2 postgrest -To refresh the cache in docker: +To reload the config in docker: .. code:: bash diff --git a/index.rst b/index.rst index af28331db9..8b2033ea10 100644 --- a/index.rst +++ b/index.rst @@ -234,7 +234,7 @@ Here are some companies that use PostgREST in production. * `Catarse `_ * `Datrium `_ * `Drip Depot `_ -* `eGull `_ +* `eGull `_ * `Image-charts `_ * `Moat `_ * `MotionDynamic - Fast highly dynamic video generation at scale `_ diff --git a/releases/v8.0.0.rst b/releases/v8.0.0.rst index 55c5ad3731..b79098df12 100644 --- a/releases/v8.0.0.rst +++ b/releases/v8.0.0.rst @@ -22,7 +22,7 @@ Added * No downtime when reloading the schema cache. See the note in :ref:`schema_reloading`. |br| -- `@steve-chavez `_ -* Allow schema cache reloading using PostgreSQL :ref:`NOTIFY ` command. +* Allow schema cache reloading using PostgreSQL :ref:`NOTIFY ` command. This enables :ref:`auto_schema_reloading`. |br| -- `@steve-chavez `_ * Allow sending the header ``Prefer: headers-only`` to get a response with a ``Location`` header. See :ref:`insert_update`. diff --git a/schema_cache.rst b/schema_cache.rst index 04c8c84bfa..e4c265703d 100644 --- a/schema_cache.rst +++ b/schema_cache.rst @@ -6,8 +6,6 @@ Schema Cache Certain PostgREST features require metadata from the database schema. Getting this metadata requires executing expensive queries, so in order to avoid repeating this work, PostgREST uses a schema cache. -The following features are the ones that require metadata from the schema cache. - +--------------------------------------------+-------------------------------------------------------------------------------+ | Feature | Required Metadata | +============================================+===============================================================================+ @@ -34,18 +32,18 @@ The Stale Schema Cache When you make changes on the metadata mentioned above, the schema cache will turn stale on a running PostgREST. Future requests that use the above features will need the :ref:`schema cache to be reloaded `; otherwise, you'll get an error instead of the expected result. -For instance, let's see what would happen if you have a stale schema for foreign key relationships and function signature: +For instance, let's see what would happen if you have a stale schema cache for foreign key relationships and function signatures. Stale Foreign Key Relationships ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Suppose you add a ``cities`` table to your database. This table has a foreign key referencing an existing ``countries`` table. Then, you make a request to get the ``cities`` and their belonging ``countries``: +Suppose you add a ``cities`` table to your database and define a foreign key that references an existing ``countries`` table. Then, you make a request to get the ``cities`` and their belonging ``countries``. .. code-block:: http GET /cities?select=name,country:countries(id,name) HTTP/1.1 -But instead, you get an error message that looks like this: +The result will be an error: .. code-block:: json @@ -54,14 +52,14 @@ But instead, you get an error message that looks like this: "message": "Could not find a relationship between cities and countries in the schema cache" } -As you can see, PostgREST couldn't find the newly created foreign key in the schema cache. See the section :ref:`schema_reloading` to solve this issue. +As you can see, PostgREST couldn't find the newly created foreign key in the schema cache. See :ref:`schema_reloading` and :ref:`auto_schema_reloading` to solve this issue. .. _stale_function_signature: Stale Function Signature ~~~~~~~~~~~~~~~~~~~~~~~~ -Suppose you create the following function while PostgREST is running: +The same issue will occur on newly created functions on a running PostgREST. .. code-block:: plpgsql @@ -70,14 +68,10 @@ Suppose you create the following function while PostgREST is running: SELECT num + 1; $$ LANGUAGE SQL IMMUTABLE; -Then, you make this request: - .. code-block:: http GET /rpc/plus_one?num=1 HTTP/1.1 -Next, PostgREST tries to find the function on the stale schema to no avail: - .. code-block:: json { @@ -85,7 +79,7 @@ Next, PostgREST tries to find the function on the stale schema to no avail: "message": "Could not find the api.plus_one(num) function in the schema cache" } -See the section :ref:`schema_reloading` to solve this issue. +Here, PostgREST tries to find the function on the stale schema to no avail. See :ref:`schema_reloading` and :ref:`auto_schema_reloading` to solve this issue. .. _schema_reloading: @@ -125,10 +119,12 @@ There are environments where you can't send the SIGUSR1 Unix Signal (like on man The ``"pgrst"`` notification channel is enabled by default. For configuring the channel, see :ref:`db-channel` and :ref:`db-channel-enabled`. -Automatic schema cache reloading -******************************** +.. _auto_schema_reloading: + +Automatic Schema Cache Reloading +-------------------------------- -You can do automatic schema cache reloading in a pure SQL way with an `event trigger `_ and ``NOTIFY``. +You can do automatic schema cache reloading in a pure SQL way and forget about stale schema cache errors with an `event trigger `_ and ``NOTIFY``. .. code-block:: postgresql @@ -137,7 +133,7 @@ You can do automatic schema cache reloading in a pure SQL way with an `event tri LANGUAGE plpgsql AS $$ BEGIN - NOTIFY pgrst; + NOTIFY pgrst, 'reload schema'; END; $$; From 98350e294148a2a066e47fbd12522f4ded7e9f4b Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Mon, 9 Aug 2021 18:06:45 -0500 Subject: [PATCH 425/549] Update docker configuration --- configuration.rst | 20 ++++++++++---------- install.rst | 15 ++------------- releases/v8.0.0.rst | 3 +++ 3 files changed, 15 insertions(+), 23 deletions(-) diff --git a/configuration.rst b/configuration.rst index b758b1265f..5b6a28e375 100644 --- a/configuration.rst +++ b/configuration.rst @@ -32,6 +32,8 @@ The configuration file must contain a set of key value pairs. At minimum you mus The user specified in the db-uri is also known as the authenticator role. For more information about the anonymous vs authenticator roles see the :ref:`roles`. +.. _config_full_list: + Here is the full list of configuration parameters. ======================== ======= ================= ======== @@ -65,8 +67,6 @@ role-claim-key String .role raw-media-types String ======================== ======= ================= ======== -You can also set these configuration parameters using environment variables. They are capitalized, have a ``PGRST_`` prefix, and use underscores. For example: ``PGRST_DB_URI`` corresponds to ``db-uri``. - .. _db-uri: db-uri @@ -386,6 +386,13 @@ raw-media-types raw-media-types="image/png, text/xml" +.. _env_variables_config: + +Environment Variables +===================== + +You can also set these :ref:`configuration parameters ` using environment variables. They 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.*``. + .. _config_reloading: Configuration Reloading @@ -397,14 +404,7 @@ To reload the configuration without restarting the PostgREST server, send a SIGU killall -SIGUSR2 postgrest -To reload the config in docker: - -.. code:: bash - - docker kill -s SIGUSR2 - - # or in docker-compose - docker-compose kill -s SIGUSR2 +This method does not reload :ref:`env_variables_config` and it will not work for reloading a Docker container configuration. In these cases, you need to restart the PostgREST server or use :ref:`in_db_config` as an alternative. .. important:: diff --git a/install.rst b/install.rst index 6a682d8fd5..afe50b6e86 100644 --- a/install.rst +++ b/install.rst @@ -99,19 +99,7 @@ You can get the `official PostgREST Docker image `_. |br| -- `@monacoremo `_ +* The Docker image no longer has an internal ``/etc/postgrest.conf`` file, you must use :ref:`env_variables_config` to configure it. + |br| -- `@wolfgangwalther `_ + * The ``pg_listen`` `utility `_ is no longer needed to automatically reload the schema cache and it's replaced entirely by database notifications. See :ref:`schema_reloading_notify`. |br| -- `@steve-chavez `_ From ff30d4d417547e4542aadcefac4e0d3d0c3bdb22 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 9 Aug 2021 18:29:37 -0500 Subject: [PATCH 426/549] Update config.py version/release --- conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf.py b/conf.py index 71122e56a5..6e017e016d 100644 --- a/conf.py +++ b/conf.py @@ -54,9 +54,9 @@ # built documents. # # The short X.Y version. -version = u'7.0' +version = u'8.0' # The full version, including alpha/beta/rc tags. -release = u'7.0.1' +release = u'8.0.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From fcabf4012b0c650a563b826ba8260322b51c2daa Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 10 Aug 2021 00:26:24 -0500 Subject: [PATCH 427/549] Reorder release notes for v8.0 --- admin.rst | 4 +-- configuration.rst | 6 ++--- releases/v8.0.0.rst | 64 ++++++++++++++++++++------------------------- schema_cache.rst | 6 ++--- 4 files changed, 37 insertions(+), 43 deletions(-) diff --git a/admin.rst b/admin.rst index 7ab4f1277d..dca785cbf2 100644 --- a/admin.rst +++ b/admin.rst @@ -148,14 +148,14 @@ Logging PostgREST logs basic request information to ``stdout``, including the requesting IP address and user agent, the URL requested, and HTTP response status. -.. code-block:: +.. code-block:: none 127.0.0.1 - - [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 - "" "curl/7.64.0" 127.0.0.1 - - [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-block:: +.. code-block:: none 12/Jun/2021:17:47:39 -0500: Attempting to connect to the database... 12/Jun/2021:17:47:39 -0500: Listening on port 3000 diff --git a/configuration.rst b/configuration.rst index 5b6a28e375..04dd236db9 100644 --- a/configuration.rst +++ b/configuration.rst @@ -404,7 +404,7 @@ To reload the configuration without restarting the PostgREST server, send a SIGU killall -SIGUSR2 postgrest -This method does not reload :ref:`env_variables_config` and it will not work for reloading a Docker container configuration. In these cases, you need to restart the PostgREST server or use :ref:`in_db_config` as an alternative. +This method does not reload :ref:`env_variables_config` and it will not work for reloading a Docker container configuration. In these cases, you need to restart the PostgREST server or use the :ref:`in_db_config` as an alternative. .. important:: @@ -433,12 +433,12 @@ For example, you can configure :ref:`db-schema` and :ref:`jwt-secret` like this: ALTER ROLE authenticator SET pgrst.db_schema = "tenant1, tenant2, tenant3" ALTER ROLE authenticator SET pgrst.jwt_secret = "REALLYREALLYREALLYREALLYVERYSAFE" +Note that underscores(``_``) need to be used instead of dashes(``-``) for the in-database config options. + .. important:: For altering a role in this way, you need a SUPERUSER. You might not be able to use this configuration mode on cloud-hosted databases. -Note that underscores(``_``) need to be used instead of dashes(``-``) for the options when the configuration is inside the database. - When using both the configuration file and the in-database configuration, the latter takes precedence. .. danger:: diff --git a/releases/v8.0.0.rst b/releases/v8.0.0.rst index a47dfa5033..6b55c98e1d 100644 --- a/releases/v8.0.0.rst +++ b/releases/v8.0.0.rst @@ -19,7 +19,7 @@ Added * Allow :ref:`embedding_view_chains` recursively to any depth. |br| -- `@wolfgangwalther `_ -* No downtime when reloading the schema cache. See the note in :ref:`schema_reloading`. +* No downtime when reloading the schema cache. See :ref:`schema_reloading`. |br| -- `@steve-chavez `_ * Allow schema cache reloading using PostgreSQL :ref:`NOTIFY ` command. This enables :ref:`auto_schema_reloading`. @@ -63,6 +63,34 @@ Added + Added the :ref:`schema_cache` page. + Moved the :ref:`schema_reloading` reference from :ref:`admin` to :ref:`schema_cache` +Changed +------- + +* Docker images are now optimized to be built from the scratch image. This reduces the compressed image size from over 30 MB to about 4 MB. + For more details, see `Docker image built with Nix `_. + |br| -- `@monacoremo `_ + +* The Docker image no longer has an internal ``/etc/postgrest.conf`` file, you must use :ref:`env_variables_config` to configure it. + |br| -- `@wolfgangwalther `_ + +* The ``pg_listen`` `utility `_ is no longer needed to automatically reload the schema cache + and it's replaced entirely by database notifications. See :ref:`auto_schema_reloading`. + |br| -- `@steve-chavez `_ + +* POST requests for insertions no longer include a ``Location`` header in the response by default and behave the same way as having a + ``Prefer: return=minimal`` header in the request. This prevents permissions errors when having a write-only table. See :ref:`insert_update`. + |br| -- `@laurenceisla `_ + +* Modified the default logging level from ``info`` to ``error``. See :ref:`log-level`. + |br| -- `@steve-chavez `_ + +* Changed the error message for a not found RPC on a stale schema (see :ref:`stale_function_signature`) and for the unsupported case of + overloaded functions with the same argument names but different types. + |br| -- `@laurenceisla `_ + +* Changed the error message for the no relationship found error. See :ref:`stale_fk_relationships`. + |br| -- `@laurenceisla `_ + Fixed ----- @@ -111,40 +139,6 @@ Fixed * Fix requests for overloaded functions from HTML forms to no longer hang. |br| -- `@laurenceisla `_ -* Add a hint and clarification to the no relationship found error. - |br| -- `@laurenceisla `_ - -* Show comprehensive error when an RPC is not found in a stale schema cache. - |br| -- `@laurenceisla `_ - -* Fix Location headers in headers only representation for null PK inserts on views. - |br| -- `@laurenceisla `_ - -Changed -------- - -* Docker images are now optimized to be built from the scratch image. This reduces the compressed image size from over 30 MB to about 4 MB. - For more details, see `Docker image built with Nix `_. - |br| -- `@monacoremo `_ - -* The Docker image no longer has an internal ``/etc/postgrest.conf`` file, you must use :ref:`env_variables_config` to configure it. - |br| -- `@wolfgangwalther `_ - -* The ``pg_listen`` `utility `_ is no longer needed to automatically reload the schema cache - and it's replaced entirely by database notifications. See :ref:`schema_reloading_notify`. - |br| -- `@steve-chavez `_ - -* Improved error message for a not found RPC on a stale schema (see :ref:`stale_function_signature`) and for the unsupported case of - overloaded functions with the same argument names but different types. - |br| -- `@laurenceisla `_ - -* Modified the default logging level from ``info`` to ``error``. See :ref:`log-level`. - |br| -- `@steve-chavez `_ - -* POST requests for insertions no longer include a ``Location`` header in the response by default and behave the same way as having a - ``Prefer: return=minimal`` header in the request. This prevents permissions errors when having a write-only table. See :ref:`insert_update`. - |br| -- `@laurenceisla `_ - Thanks ------ diff --git a/schema_cache.rst b/schema_cache.rst index e4c265703d..f6fd237499 100644 --- a/schema_cache.rst +++ b/schema_cache.rst @@ -34,6 +34,8 @@ When you make changes on the metadata mentioned above, the schema cache will tur For instance, let's see what would happen if you have a stale schema cache for foreign key relationships and function signatures. +.. _stale_fk_relationships: + Stale Foreign Key Relationships ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -102,9 +104,7 @@ For docker you can do: # or in docker-compose docker-compose kill -s SIGUSR1 -.. note:: - - There's no downtime when reloading the schema cache. The reloading will happen on a background thread while requests keep being served. +There's no downtime when reloading the schema cache. The reloading will happen on a background thread while requests keep being served. .. _schema_reloading_notify: From 0d23be7731ca7e601f9540e937871591c56fd04f Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 10 Aug 2021 00:37:25 -0500 Subject: [PATCH 428/549] Fix logging blocks not showing on RTD --- admin.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin.rst b/admin.rst index dca785cbf2..908514a9b4 100644 --- a/admin.rst +++ b/admin.rst @@ -148,14 +148,14 @@ Logging PostgREST logs basic request information to ``stdout``, including the requesting IP address and user agent, the URL requested, and HTTP response status. -.. code-block:: none +.. code:: 127.0.0.1 - - [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 - "" "curl/7.64.0" 127.0.0.1 - - [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-block:: none +.. code:: 12/Jun/2021:17:47:39 -0500: Attempting to connect to the database... 12/Jun/2021:17:47:39 -0500: Listening on port 3000 From f7108e8612808afb372827bd0d43c4e7d778c470 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 11 Aug 2021 20:13:04 -0500 Subject: [PATCH 429/549] Add example apps to ecosystem Add postgres-postgrest-cloudflared-example and svelte-postgrest-template --- ecosystem.rst | 2 ++ postgrest.dict | 3 +++ 2 files changed, 5 insertions(+) diff --git a/ecosystem.rst b/ecosystem.rst index c89d12820e..2d96671d10 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -32,6 +32,7 @@ Example Apps * `pgrst-dev-setup `_ - docker-compose and tmuxp setup for experimentation. * `postgrest-demo `_ - multi-tenant logging system * `postgrest-example `_ - sqitch versioning for API +* `postgres-postgrest-cloudflared-example `_ - docker-compose setup exposing PostgREST using cloudfared * `postgrest-sessions-example `_ - example for cookie-based sessions * `postgrest-starter-kit `_ - boilerplate for new project * `postgrest-translation-proxy `_ - calling to external translation service @@ -39,6 +40,7 @@ Example Apps * `postgrest-vercel `_ - run PostgREST on Vercel (Serverless/AWS Lambda) * `PostgrestSkeleton `_ - Docker Compose, PostgREST, Nginx and Auth0 * `PostGUI `_ - React Material UI admin panel +* `svelte-postgrest-template `_ - Svelte/SvelteKit, PostgREST, EveryLayout and social auth .. _eco_external_notification: diff --git a/postgrest.dict b/postgrest.dict index a5c4b6d0c4..202a9505ff 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -16,6 +16,7 @@ cd centric changelog ClojureScript +cloudfared config CORS cryptographically @@ -27,6 +28,7 @@ disjoined dockerize DoS eq +EveryLayout Fenko Fernandes filename @@ -145,6 +147,7 @@ stdout Stolarz subselect SuperAgent +SvelteKit syslog systemd Tcl From 91152ebad0625abc1acf7a0b65937e936b0c2f86 Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Fri, 13 Aug 2021 17:31:01 -0500 Subject: [PATCH 430/549] Add partitioned tables in the schema cache --- api.rst | 43 +++++++++++++++++++++++++++++++++++++++++++ index.rst | 4 +++- releases/upcoming.rst | 12 ++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 releases/upcoming.rst diff --git a/api.rst b/api.rst index ecc34487bd..ea7b7bd63a 100644 --- a/api.rst +++ b/api.rst @@ -651,6 +651,49 @@ Embedded resources can be aliased and filters can be applied on these aliases: GET /films?select=*,90_comps:competitions(name),91_comps:competitions(name)&90_comps.year=eq.1990&91_comps.year=eq.1991 HTTP/1.1 +.. _embedding_partitioned_tables: + +Embedding Partitioned Tables +---------------------------- + +Embedding can also be done between `partitioned tables `_ and other tables. + +For example, let's create the ``box_office`` partitioned table that has the gross daily revenue of a film: + +.. code-block:: postgres + + CREATE TABLE box_office ( + bo_date DATE NOT NULL, + film_id INT REFERENCES test.films NOT NULL, + gross_revenue DECIMAL(12,2) NOT NULL, + PRIMARY KEY (bo_date, film_id) + ) PARTITION BY RANGE (bo_date); + + -- Let's also create partitions for each month of 2021 + + CREATE TABLE box_office_2021_01 PARTITION OF test.box_office + FOR VALUES FROM ('2021-01-01') TO ('2021-01-31'); + + CREATE TABLE box_office_2021_02 PARTITION OF test.box_office + FOR VALUES FROM ('2021-02-01') TO ('2021-02-28'); + + -- and so until december 2021 + +Since it contains the ``films_id`` foreign key, it is possible to embed ``box_office`` and ``films``: + +.. code-block:: http + + GET /box_office?select=bo_date,gross_revenue,films(title)&gross_revenue=gte.1000000 HTTP/1.1 + +Embedding is also possible between ``box_office`` partitions and the ``films`` table: + +.. code-block:: http + + GET /films?select=title,box_office_2021_02(bo_date,gross_revenue)&rating=gt.8 HTTP/1.1 + +.. note:: + Partitioned tables can reference other tables since PostgreSQL 11 but can only be referenced from any other table since PostgreSQL 12. + .. _embedding_views: Embedding Views diff --git a/index.rst b/index.rst index 8b2033ea10..65522fad5f 100644 --- a/index.rst +++ b/index.rst @@ -234,7 +234,6 @@ Here are some companies that use PostgREST in production. * `Catarse `_ * `Datrium `_ * `Drip Depot `_ -* `eGull `_ * `Image-charts `_ * `Moat `_ * `MotionDynamic - Fast highly dynamic video generation at scale `_ @@ -246,6 +245,9 @@ Here are some companies that use PostgREST in production. * `Sompani `_ * `Supabase `_ +.. Certs are failing + * `eGull `_ + Testimonials ------------ diff --git a/releases/upcoming.rst b/releases/upcoming.rst new file mode 100644 index 0000000000..5d9e0e4372 --- /dev/null +++ b/releases/upcoming.rst @@ -0,0 +1,12 @@ +.. |br| raw:: html + +
    + +Upcoming +======== + +Added +----- + +* Allow :ref:`embedding `, UPSERT, INSERT with Location response, OPTIONS request and OpenAPI support for partitioned tables. + |br| -- `@laurenceisla `_ From 34958e723a57a0ea323a3a4f0501640263bf530d Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Fri, 3 Sep 2021 13:04:21 -0500 Subject: [PATCH 431/549] Add link and explanation to download the latest unreleased builds --- ecosystem.rst | 1 - releases/upcoming.rst | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ecosystem.rst b/ecosystem.rst index 2d96671d10..1104ac7a5c 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -93,7 +93,6 @@ Client-Side Libraries * `postgrest-js `_ - TypeScript/JavaScript * `postgrest-kt `_ - Kotlin * `postgrest-py `_ - Python -* `postgrest-pyclient `_ - Python * `postgrest-request `_ - JS, SuperAgent * `postgrest-rs `_ - Rust * `postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 5d9e0e4372..120b69eaa6 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -5,6 +5,9 @@ Upcoming ======== +These are changes yet unreleased. If you'd like to try them out before a new official release, access `the list of CI runs `_ +and select the newest commit, then download the build from the Artifacts section at the bottom of the page (you'll need a GitHub account to download it). + Added ----- From aea3b24b31f0cb8ed78a767a4a5e9140377a74cd Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 3 Sep 2021 15:05:15 -0500 Subject: [PATCH 432/549] Update example apps --- ecosystem.rst | 15 ++++++++++----- postgrest.dict | 3 +++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/ecosystem.rst b/ecosystem.rst index 1104ac7a5c..dd096a5341 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -19,27 +19,32 @@ Community Tutorials Example Apps ------------ -* `blogdemo `_ - blog api demo in a vagrant image * `chronicle `_ - tracking a tree of personal memories +* `cloudgov-demo-postgrest `_ - demo for a federally-compliant REST API on cloud.gov +* `code-du-travail-backoffice `_ - data administration portal for the official French Labor Code and Agreements +* `compose-postgrest `_ - docker-compose setup with Nginx and HTML example +* `delibrium-postgrest `_ - example school API and front-end in Vue.js * `elm-workshop `_ - building a simple database query UI * `ember-postgrest-dynamic-ui `_ - generating Ember forms to edit data +* `ETH-transactions-storage `_ - indexer for Ethereum to get transaction list by ETH address * `ext-postgrest-crud `_ - browser-based spreadsheet * `general `_ - example auth back-end -* `goodfilm `_ - example film api +* `goodfilm `_ - example film API +* `guild-operators `_ - example queries and functions that the Cardano Community uses for their Guild Operators' Repository * `handsontable-postgrest `_ - an excel-like database table editor * `heritage-near-me `_ - Elm and PostgREST with PostGIS * `ng-admin-postgrest `_ - automatic database admin panel * `pgrst-dev-setup `_ - docker-compose and tmuxp setup for experimentation. +* `postgres-postgrest-cloudflared-example `_ - docker-compose setup exposing PostgREST using cloudfared * `postgrest-demo `_ - multi-tenant logging system * `postgrest-example `_ - sqitch versioning for API -* `postgres-postgrest-cloudflared-example `_ - docker-compose setup exposing PostgREST using cloudfared * `postgrest-sessions-example `_ - example for cookie-based sessions -* `postgrest-starter-kit `_ - boilerplate for new project * `postgrest-translation-proxy `_ - calling to external translation service * `postgrest-ui `_ - ClojureScript UI components for PostgREST * `postgrest-vercel `_ - run PostgREST on Vercel (Serverless/AWS Lambda) * `PostgrestSkeleton `_ - Docker Compose, PostgREST, Nginx and Auth0 * `PostGUI `_ - React Material UI admin panel +* `prospector `_ - data warehouse and visualization platform * `svelte-postgrest-template `_ - Svelte/SvelteKit, PostgREST, EveryLayout and social auth .. _eco_external_notification: @@ -71,7 +76,7 @@ Extensions * `postgrest-node `_ - Run a PostgREST server in Node.js via npm module * `postgrest-oauth `_ - OAuth2 WAI middleware * `postgrest-oauth/api `_ - OAuth2 server -* `PostgREST-writeAPI `_ - generate nginx rewrite rules to fit an OpenAPI spec +* `PostgREST-writeAPI `_ - generate Nginx rewrite rules to fit an OpenAPI spec * `spas `_ - allow file uploads and basic auth .. _clientside_libraries: diff --git a/postgrest.dict b/postgrest.dict index 202a9505ff..227ccefcb4 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -12,6 +12,7 @@ balancer Beles Bouscal buildpack +Cardano cd centric changelog @@ -28,6 +29,8 @@ disjoined dockerize DoS eq +ETH +Ethereum EveryLayout Fenko Fernandes From 5fb11e37a3bf5116a183a88fe335d6da49a49ab7 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 15 Sep 2021 19:45:47 -0500 Subject: [PATCH 433/549] Add devops links to helm charts --- ecosystem.rst | 8 ++++++++ postgrest.dict | 2 ++ 2 files changed, 10 insertions(+) diff --git a/ecosystem.rst b/ecosystem.rst index dd096a5341..68d6f5bd98 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -47,6 +47,14 @@ Example Apps * `prospector `_ - data warehouse and visualization platform * `svelte-postgrest-template `_ - Svelte/SvelteKit, PostgREST, EveryLayout and social auth +.. _dev_ops: + +DevOps +------ + +* `jbkarle/postgrest `_ - helm chart with a demo database for development and test purposes +* `cloudstark/helm-charts `_ - helm chart to deploy PostgREST to a Kubernetes cluster via a Deployment and Service + .. _eco_external_notification: External Notification diff --git a/postgrest.dict b/postgrest.dict index 227ccefcb4..cfe0e3bead 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -24,6 +24,7 @@ cryptographically CSV Daemonizing DDL +DevOps DiBiase disjoined dockerize @@ -64,6 +65,7 @@ jwt JWTs Kinesis Kofi +Kubernetes localhost login Logins From 09ca21d032047086bd6c81545d0d4fabd3450b74 Mon Sep 17 00:00:00 2001 From: Boris Korzun Date: Thu, 23 Sep 2021 03:52:37 +1000 Subject: [PATCH 434/549] Updated FreeBSD dependencies Update FreeBSD dependencies as in bsd.default-versions.mk --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index afe50b6e86..c7592f5e11 100644 --- a/install.rst +++ b/install.rst @@ -213,7 +213,7 @@ You can build PostgREST from source with `Stack Date: Thu, 30 Sep 2021 17:18:58 -0500 Subject: [PATCH 435/549] Specify external connection poolers --- admin.rst | 8 ++++---- configuration.rst | 4 ++-- releases/v8.0.0.rst | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/admin.rst b/admin.rst index 908514a9b4..68df23052f 100644 --- a/admin.rst +++ b/admin.rst @@ -120,12 +120,12 @@ The burst argument tells Nginx to start dropping requests if more than five queu 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. -.. _connection_poolers: +.. _external_connection_poolers: -Using Connection Poolers ------------------------- +Using External Connection Poolers +--------------------------------- -In order to increase performance, PostgREST uses prepared statements by default. However, this setting is incompatible with connection poolers such as PgBouncer working in transaction pooling mode. In this case, you need to set the :ref:`db-prepared-statements` config option to ``false``. On the other hand, session pooling is fully compatible with PostgREST, while statement pooling is not compatible at all. +PostgREST manages its :ref:`own pool of connections ` and uses prepared statements by default in order to increase performance. However, this setting is incompatible with external connection poolers such as PgBouncer working in transaction pooling mode. In this case, you need to set the :ref:`db-prepared-statements` config option to ``false``. On the other hand, session pooling is fully compatible with PostgREST, while statement pooling is not compatible at all. .. note:: diff --git a/configuration.rst b/configuration.rst index 04dd236db9..2da88d208d 100644 --- a/configuration.rst +++ b/configuration.rst @@ -158,7 +158,7 @@ db-channel-enabled 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 a connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section ` for more information. + 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 ` for more information. .. _db-prepared-statements: @@ -169,7 +169,7 @@ db-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 a connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section ` for more information. + 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 ` for more information. .. _db-tx-end: diff --git a/releases/v8.0.0.rst b/releases/v8.0.0.rst index 6b55c98e1d..a02a20f697 100644 --- a/releases/v8.0.0.rst +++ b/releases/v8.0.0.rst @@ -28,7 +28,7 @@ Added * Allow sending the header ``Prefer: headers-only`` to get a response with a ``Location`` header. See :ref:`insert_update`. |br| -- `@laurenceisla `_ -* Allow :ref:`connection_poolers` such as PgBouncer in transaction pooling mode. +* Allow :ref:`external_connection_poolers` such as PgBouncer in transaction pooling mode. |br| -- `@laurenceisla `_ * Allow :ref:`config_reloading` by sending a SIGUSR2 signal. From ad13e00f6456c75cbe69a0f8921114806d1ef2af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Szostek?= Date: Sun, 3 Oct 2021 00:40:42 +0100 Subject: [PATCH 436/549] Typo in api.rst, correct `ignore_privileges` The correct setting name is `ignore-privileges`, with dash in between the words. --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index ea7b7bd63a..6ac6001ed9 100644 --- a/api.rst +++ b/api.rst @@ -1461,7 +1461,7 @@ Every API hosted by PostgREST automatically serves a full `OpenAPI `_ on any database object. For instance, From ec7ace2e73509e955b01e65b653baecd663b84ad Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Wed, 13 Oct 2021 14:31:49 -0500 Subject: [PATCH 437/549] Add compatibility with PostgreSQL v14 --- api.rst | 24 ++++++++++++++++++------ auth.rst | 2 +- configuration.rst | 10 ++++++++++ postgrest.dict | 1 + releases/upcoming.rst | 8 ++++++++ 5 files changed, 38 insertions(+), 7 deletions(-) diff --git a/api.rst b/api.rst index 6ac6001ed9..2a56d9aadc 100644 --- a/api.rst +++ b/api.rst @@ -1584,20 +1584,32 @@ HTTP Logic Accessing Request Headers, Cookies and JWT claims ------------------------------------------------- -You can access request headers, cookies and JWT claims by reading GUC variables set by PostgREST per request. They are named :code:`request.header.XYZ`, :code:`request.cookie.XYZ` and :code:`request.jwt.claim.XYZ`. +You can access request headers, cookies and JWT claims by reading GUC variables set by PostgREST per request. They are named :code:`request.headers`, :code:`request.cookies` and :code:`request.jwt.claims`. .. code-block:: postgresql - -- To read the value of the Origin request header: - SELECT current_setting('request.header.origin', true); + -- To read the value of the User-Agent request header: + SELECT current_setting('request.headers', true)::json->>'user-agent'; + -- To read the value of sessionId in a cookie: - SELECT current_setting('request.cookie.sessionId', true); + SELECT current_setting('request.cookies', true)::json->>'sessionId'; + -- To read the value of the email claim in a jwt: - SELECT current_setting('request.jwt.claim.email', true); + SELECT current_setting('request.jwt.claims', true)::json->>'email'; + + -- To get all the headers sent in the request + SELECT current_setting('request.headers', true)::json; .. note:: - ``request.jwt.claim.role`` defaults to the value of :ref:`db-anon-role`. + The ``role`` in ``request.jwt.claims`` defaults to the value of :ref:`db-anon-role`. + +.. _guc_legacy_names: + +Legacy GUC variable names +~~~~~~~~~~~~~~~~~~~~~~~~~ + +For PostgreSQL versions below 14, PostgREST will take into consideration the :ref:`db-use-legacy-gucs` config, which is set to true by default. This means that the interface for accessing these GUCs is `the same as in older versions `_. You can opt in to use the JSON GUCs mentioned above by setting the ``db-use-legacy-gucs`` to false. .. _guc_req_path_method: diff --git a/auth.rst b/auth.rst index d00f673682..748a9fe291 100644 --- a/auth.rst +++ b/auth.rst @@ -165,7 +165,7 @@ You can create a valid JWT either from inside your database or via an external s JWT from SQL ~~~~~~~~~~~~ -You can create JWT tokens in SQL using the `pgjwt extension `_. 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 `_ (you'll need to replace ``@extschema@`` with another schema or just delete it) which creates the functions you will need. +You can create JWT tokens in SQL using the `pgjwt extension `_. 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 `_ (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. diff --git a/configuration.rst b/configuration.rst index 2da88d208d..2826e2d5d9 100644 --- a/configuration.rst +++ b/configuration.rst @@ -50,6 +50,7 @@ db-channel-enabled Boolean True db-prepared-statements Boolean True db-tx-end String commit db-config Boolean True +db-use-legacy-gucs Boolean True server-host String !4 server-port Int 3000 server-unix-socket String @@ -199,6 +200,15 @@ db-config Enables the in-database configuration. +.. _db-use-legacy-gucs: + +db-use-legacy-gucs +------------------ + + Determine if GUC request settings for headers, cookies and jwt claims use the `legacy names `_ (string with dashes, invalid starting from PostgreSQL v14) with text values instead of the :ref:`new names ` (string without dashes, valid on all PostgreSQL versions) with json values. + + On PostgreSQL versions 14 and above, this parameter is ignored. + .. _server-host: server-host diff --git a/postgrest.dict b/postgrest.dict index cfe0e3bead..396e67b5ee 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -45,6 +45,7 @@ grantor GraphQL gte GUC +gucs Gumbs Haskell Heroku diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 120b69eaa6..060d528b09 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -13,3 +13,11 @@ Added * Allow :ref:`embedding `, UPSERT, INSERT with Location response, OPTIONS request and OpenAPI support for partitioned tables. |br| -- `@laurenceisla `_ + +* Make GUC names for headers, cookies and jwt claims compatible with PostgreSQL v14. + + + The GUC names on PostgreSQL 14 are changed to the ones :ref:`mentioned in this section `, while older versions still use the :ref:`guc_legacy_names`. + + PostgreSQL versions below 14 can opt in to the new JSON GUCs by setting the :ref:`db-use-legacy-gucs` config option to false (true by default). + + Managed to avoid a breaking change thanks to `@robertsosinski `_ who reported the bug that only one ``.`` character was allowed in GUC keys to the PostgreSQL team. See the `full discussion `_. + + -- `@laurenceisla `_ From ee1ec780d2dfe1e96257e67f406bb7c6586a0ee3 Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Mon, 18 Oct 2021 17:34:49 -0500 Subject: [PATCH 438/549] Add nested embedding examples --- api.rst | 19 +++++++++++++++++++ releases/upcoming.rst | 4 ++++ 2 files changed, 23 insertions(+) diff --git a/api.rst b/api.rst index 2a56d9aadc..e4255eddad 100644 --- a/api.rst +++ b/api.rst @@ -616,6 +616,17 @@ PostgREST can also detect relationships going through join tables. Thus you can GET /actors?select=films(title,year) HTTP/1.1 +.. _nested_embedding: + +Nested Embedding +---------------- + +If you want to embed through join tables but need more control on the intermediate resources, you can do nested embedding. For instance, you can request the Actors, their Roles and the Films for those Roles: + +.. code-block:: http + + GET /actors?select=roles(character,films(title,year)) HTTP/1.1 + Embedded Filters ---------------- @@ -651,6 +662,14 @@ Embedded resources can be aliased and filters can be applied on these aliases: GET /films?select=*,90_comps:competitions(name),91_comps:competitions(name)&90_comps.year=eq.1990&91_comps.year=eq.1991 HTTP/1.1 +Filters can also be applied on nested embedded resources: + +.. code-block:: http + + GET /films?select=*,roles(*,actors(*))&roles.actors.order=last_name&roles.actors.first_name=like.*Tom* HTTP/1.1 + +The result will show the nested actors named Tom and order them by last name. Aliases can also be used instead of the resource names to filter the nested tables. + .. _embedding_partitioned_tables: Embedding Partitioned Tables diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 060d528b09..37bc92dc02 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -21,3 +21,7 @@ Added + Managed to avoid a breaking change thanks to `@robertsosinski `_ who reported the bug that only one ``.`` character was allowed in GUC keys to the PostgreSQL team. See the `full discussion `_. -- `@laurenceisla `_ + +* Documentation improvements + + + Added :ref:`nested_embedding` to the :ref:`resource_embedding` section. From c6ef4305b13d2b6af1e4820dc71fbb0e100ff500 Mon Sep 17 00:00:00 2001 From: laurenceisla Date: Tue, 19 Oct 2021 11:34:18 -0500 Subject: [PATCH 439/549] Allow top-level resource with embed filter --- api.rst | 65 +++++++++++++++++++++++++++++++++++++++++++ configuration.rst | 16 +++++++++++ releases/upcoming.rst | 7 +++++ 3 files changed, 88 insertions(+) diff --git a/api.rst b/api.rst index e4255eddad..5826f7f45c 100644 --- a/api.rst +++ b/api.rst @@ -670,6 +670,65 @@ Filters can also be applied on nested embedded resources: The result will show the nested actors named Tom and order them by last name. Aliases can also be used instead of the resource names to filter the nested tables. +.. _embedding_top_level_filter: + +Top Level Filtering +~~~~~~~~~~~~~~~~~~~ + +By default, embedded filters don't change the top level resource rows at all: + +.. code-block:: http + + GET /films?select=title,actors(first_name,last_name)&actors.first_name=eq.Jehanne HTTP/1.1 + +.. code-block:: json + + [ + { + "title": "Workers Leaving The Lumière Factory In Lyon", + "actors": [] + }, + { + "title": "The Dickson Experimental Sound Film", + "actors": [] + }, + { + "title": "The Haunted Castle", + "actors": [ + { + "first_name": "Jehanne", + "last_name": "d'Alcy" + } + ] + } + ] + +In order to filter the top level rows you need to add ``!inner`` to the embedded resource. For instance, to get **only** the films that have an actor named ``Jehanne``: + +.. code-block:: http + + GET /films?select=title,actors!inner(first_name,last_name)&actors.first_name=eq.Jehanne HTTP/1.1 + +.. code-block:: json + + [ + { + "title": "The Haunted Castle", + "actors": [ + { + "first_name": "Jehanne", + "last_name": "d'Alcy" + } + ] + } + ] + +If you prefer to work with top level filtering as a default embedding behavior for PostgREST, set the :ref:`db-embed-default-join` configuration parameter to ``"inner"``. This way, you don't need to specify ``!inner`` on every request and, if you need the previous behavior, add ``!left`` to the embedding resource. For instance, this will not filter the films in any way: + +.. code-block:: http + + GET /films?select=title,actors!left(first_name,last_name)&actors.first_name=eq.Jehanne HTTP/1.1 + .. _embedding_partitioned_tables: Embedding Partitioned Tables @@ -936,6 +995,12 @@ Here we specify ``central_addresses`` as the **target** and the ``billing_addres Similarly to the **target**, the **hint** can be a **table name**, **foreign key constraint name** or **column name**. +Hints also work alongside ``!inner`` if a top level filtering is needed. From the above example: + +.. code-block:: http + + GET /orders?select=*,central_addresses!billing_address!inner(*)¢ral_addresses.code="AB1000" HTTP/1.1 + .. _insert_update: Insertions / Updates diff --git a/configuration.rst b/configuration.rst index 2826e2d5d9..c3a38301d1 100644 --- a/configuration.rst +++ b/configuration.rst @@ -50,6 +50,7 @@ db-channel-enabled Boolean True db-prepared-statements Boolean True db-tx-end String commit db-config Boolean True +db-embed-default-join String left db-use-legacy-gucs Boolean True server-host String !4 server-port Int 3000 @@ -200,6 +201,21 @@ db-config Enables the in-database configuration. +.. _db-embed-default-join: + +db-embed-default-join +--------------------- + + Determines the default embedding type between tables or views when none is specified in the request. For more info, see :ref:`embedding_top_level_filter`. + + .. code:: bash + + # Embeds using LEFT JOIN + db-embed-default-join = "left" + + # Embeds using INNER JOIN + db-embed-default-join = "inner" + .. _db-use-legacy-gucs: db-use-legacy-gucs diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 37bc92dc02..a7ac4b1168 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -14,6 +14,13 @@ Added * Allow :ref:`embedding `, UPSERT, INSERT with Location response, OPTIONS request and OpenAPI support for partitioned tables. |br| -- `@laurenceisla `_ +* Allow filtering top-level resource based on embedded resources filters + + + This is enabled by adding ``!inner`` to the embedded resource. See :ref:`embedding_top_level_filter`. + + This behavior can be enabled by default by setting the :ref:`db-embed-default-join` to ``"inner"``. + + -- `@steve-chavez `_ + * Make GUC names for headers, cookies and jwt claims compatible with PostgreSQL v14. + The GUC names on PostgreSQL 14 are changed to the ones :ref:`mentioned in this section `, while older versions still use the :ref:`guc_legacy_names`. From 7e7d4df93b85bddd13957b2b131335bade205a4a Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Wed, 20 Oct 2021 13:14:32 +0200 Subject: [PATCH 440/549] docker-compose: remove optional quotes around environment variable Resolves #446 --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index c7592f5e11..2c03932e18 100644 --- a/install.rst +++ b/install.rst @@ -160,7 +160,7 @@ To avoid having to install the database at all, you can run both it and the serv PGRST_DB_URI: postgres://app_user:password@db:5432/app_db PGRST_DB_SCHEMA: public PGRST_DB_ANON_ROLE: app_user #In production this role should not be the same as the one used for the connection - PGRST_OPENAPI_SERVER_PROXY_URI: "http://127.0.0.1:3000" + PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000 depends_on: - db db: From 8e11b6f5dfd1b07feb27a4fa947c531179809193 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 20 Oct 2021 16:13:49 -0500 Subject: [PATCH 441/549] Allow escaping inside double quotes with a backslash (#447) --- api.rst | 8 ++++++++ releases/upcoming.rst | 9 +++++++++ 2 files changed, 17 insertions(+) diff --git a/api.rst b/api.rst index 5826f7f45c..e85864d60e 100644 --- a/api.rst +++ b/api.rst @@ -312,6 +312,14 @@ Here ``information.cpe`` is a column name. GET /vulnerabilities?%22information.cpe%22=like.*MS* HTTP/1.1 +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:: http + + GET /marks?name=in.(%22Quote:%5C%22%22,%22Backslash:%5C%5C%22) HTTP/1.1 + .. note:: Some HTTP libraries might encode URLs automatically(e.g. :code:`axios`). In these cases you should use double quotes diff --git a/releases/upcoming.rst b/releases/upcoming.rst index a7ac4b1168..9610ba27e1 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -29,6 +29,15 @@ Added -- `@laurenceisla `_ +* Allow escaping inside double quotes with a backslash, e.g. ``?col=in.("Double\"Quote")``, ``?col=in.("Back\\slash")``. See :ref:`reserved-chars`. + |br| -- `@steve-chavez `_ + * Documentation improvements + Added :ref:`nested_embedding` to the :ref:`resource_embedding` section. + +Fixed +----- + +* Fix using single double quotes (``"``) and backslashes (``/``) as values on the "in" operator + |br| -- `@steve-chavez `_ From b15dd8783e77ab7932c11b7c9bc2b7093015f886 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 21 Oct 2021 21:08:29 -0500 Subject: [PATCH 442/549] Add Retry-After header when recovering the connection --- admin.rst | 11 +++++++++++ postgrest.dict | 2 ++ releases/upcoming.rst | 3 +++ 3 files changed, 16 insertions(+) diff --git a/admin.rst b/admin.rst index 68df23052f..df318e5162 100644 --- a/admin.rst +++ b/admin.rst @@ -188,6 +188,17 @@ A great way to inspect incoming HTTP requests including headers and query parame The options to ngrep vary depending on the address and host on which you've bound the server. The binding is described in the :ref:`configuration` section. The ngrep output isn't particularly pretty, but it's legible. +.. _automatic_recovery: + +Automatic Connection Recovery +----------------------------- + +When PostgREST loses the connection to the database, it retries the connection using capped exponential backoff, with 32 seconds being the maximum backoff time. + +This retry behavior is triggered immediately after the connection is lost if :ref:`db-channel-enabled` is set to true (this is the default behavior), otherwise it will be activated once a request is made. + +To notify the client when the next reconnection attempt will be, PostgREST responds with ``503 Service Unavailable`` and the ``Retry-After: x`` header, where ``x`` is the number of seconds programmed for the next retry. + Database Logs ------------- diff --git a/postgrest.dict b/postgrest.dict index 396e67b5ee..5a26892395 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -8,6 +8,7 @@ aud Auth auth authenticator +backoff balancer Beles Bouscal @@ -123,6 +124,7 @@ Rafaj RDS reallyreallyreallyreallyverysafe Rechkemmer +reconnection Redux refactor Remo diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 9610ba27e1..e5dc3541b2 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -32,6 +32,9 @@ Added * Allow escaping inside double quotes with a backslash, e.g. ``?col=in.("Double\"Quote")``, ``?col=in.("Back\\slash")``. See :ref:`reserved-chars`. |br| -- `@steve-chavez `_ +* Add ``Retry-After`` header when recovering the connection. See :ref:`automatic_recovery`. + |br| -- `@gautam1168 `_ + * Documentation improvements + Added :ref:`nested_embedding` to the :ref:`resource_embedding` section. From 23ad30a20ab712d1ffc16689f5eef97d41fb63eb Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 28 Oct 2021 17:47:21 -0500 Subject: [PATCH 443/549] Add templates section to the ecosystem --- ecosystem.rst | 10 ++++++++-- index.rst | 3 +++ releases/upcoming.rst | 1 + requirements.txt | 1 + 4 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 requirements.txt diff --git a/ecosystem.rst b/ecosystem.rst index 68d6f5bd98..787a8b75d1 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -14,6 +14,14 @@ Community Tutorials * `"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). +.. _templates: + +Templates +--------- + +* `compose-postgrest `_ - docker-compose setup with Nginx and HTML example +* `svelte-postgrest-template `_ - Svelte/SvelteKit, PostgREST, EveryLayout and social auth - `blog post `_ + .. _eco_example_apps: Example Apps @@ -22,7 +30,6 @@ Example Apps * `chronicle `_ - tracking a tree of personal memories * `cloudgov-demo-postgrest `_ - demo for a federally-compliant REST API on cloud.gov * `code-du-travail-backoffice `_ - data administration portal for the official French Labor Code and Agreements -* `compose-postgrest `_ - docker-compose setup with Nginx and HTML example * `delibrium-postgrest `_ - example school API and front-end in Vue.js * `elm-workshop `_ - building a simple database query UI * `ember-postgrest-dynamic-ui `_ - generating Ember forms to edit data @@ -45,7 +52,6 @@ Example Apps * `PostgrestSkeleton `_ - Docker Compose, PostgREST, Nginx and Auth0 * `PostGUI `_ - React Material UI admin panel * `prospector `_ - data warehouse and visualization platform -* `svelte-postgrest-template `_ - Svelte/SvelteKit, PostgREST, EveryLayout and social auth .. _dev_ops: diff --git a/index.rst b/index.rst index 65522fad5f..eb891698bb 100644 --- a/index.rst +++ b/index.rst @@ -209,7 +209,10 @@ PostgREST has a growing ecosystem of examples, libraries, and experiments. Here ecosystem.rst +* :ref:`community_tutorials` +* :ref:`templates` * :ref:`eco_example_apps` +* :ref:`dev_ops` * :ref:`eco_external_notification` * :ref:`eco_extensions` * :ref:`clientside_libraries` diff --git a/releases/upcoming.rst b/releases/upcoming.rst index e5dc3541b2..36fd101b18 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -38,6 +38,7 @@ Added * Documentation improvements + Added :ref:`nested_embedding` to the :ref:`resource_embedding` section. + + Added the :ref:`templates` section to the :doc:`Ecosystem `. Fixed ----- diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..163f583b56 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +docutils==0.17.1 \ No newline at end of file From a0bdcb706c01540173355bce05362c63453132f8 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 28 Oct 2021 18:06:52 -0500 Subject: [PATCH 444/549] Add curl examples alongside http snippets --- admin.rst | 33 +- api.rst | 1213 +++++++++++++++++++++++------- auth.rst | 27 +- conf.py | 7 +- default.nix | 5 +- extensions/sphinx-copybutton.nix | 33 + extensions/sphinx-tabs.nix | 29 + requirements.txt | 4 +- schema_cache.rst | 20 +- 9 files changed, 1087 insertions(+), 284 deletions(-) create mode 100644 extensions/sphinx-copybutton.nix create mode 100644 extensions/sphinx-tabs.nix diff --git a/admin.rst b/admin.rst index df318e5162..d1352228a6 100644 --- a/admin.rst +++ b/admin.rst @@ -43,15 +43,27 @@ Block Full-Table Operations Each table in the admin-selected schema gets exposed as a top level route. Client requests are executed by certain database roles depending on their authentication. All HTTP verbs are supported that correspond to actions permitted to the role. For instance if the active role can drop rows of the table then the DELETE verb is allowed for clients. Here's an API request to delete old rows from a hypothetical logs table: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + DELETE /logs?time=lt.1991-08-06 HTTP/1.1 - DELETE /logs?time=lt.1991-08-06 HTTP/1.1 + .. code-tab:: bash Curl + + curl "http://localhost:3000/logs?time=lt.1991-08-06" -X DELETE However it's very easy to delete the **entire table** by omitting the query parameter! -.. code-block:: http +.. tabs:: + + .. code-tab:: http - DELETE /logs HTTP/1.1 + DELETE /logs HTTP/1.1 + + .. code-tab:: bash Curl + + 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 `_ PostgreSQL extension. It raises an error if UPDATE or DELETE are executed without specifying conditions. To install it you can use the `PGXN `_ network: @@ -293,10 +305,17 @@ 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:: http +.. tabs:: + + .. code-tab:: http + + GET /people?id=eq.1 HTTP/1.1 + Accept: application/vnd.pgrst.object+json + + .. code-tab:: bash Curl - GET /people?id=eq.1 HTTP/1.1 - Accept: application/vnd.pgrst.object+json + 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. diff --git a/api.rst b/api.rst index e85864d60e..5e3fbe1ecf 100644 --- a/api.rst +++ b/api.rst @@ -6,9 +6,15 @@ Tables and Views All views and tables in the exposed schema and accessible by the active database role for a request are available for querying. They are exposed in one-level deep routes. For instance the full contents of a table `people` is returned at -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /people HTTP/1.1 + + .. code-tab:: bash Curl - GET /people HTTP/1.1 + 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. @@ -23,27 +29,51 @@ Horizontal Filtering (Rows) You can filter result rows by adding conditions on columns, each condition a query string parameter. For instance, to return people aged under 13 years old: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /people?age=lt.13 HTTP/1.1 - GET /people?age=lt.13 HTTP/1.1 + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?age=lt.13" Multiple parameters can be logically conjoined by: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /people?age=gte.18&student=is.true HTTP/1.1 - GET /people?age=gte.18&student=is.true HTTP/1.1 + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?age=gte.18&student=is.true" Multiple parameters can be logically disjoined by: -.. code-block:: http +.. tabs:: + + .. code-tab:: http - GET /people?or=(age.gte.14,age.lte.18) HTTP/1.1 + GET /people?or=(age.gte.14,age.lte.18) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?or=(age.gte.14,age.lte.18)" Complex logic can also be applied: -.. code-block:: http +.. tabs:: + + .. code-tab:: http - GET /people?and=(grade.gte.90,student.is.true,or(age.gte.14,age.is.null)) HTTP/1.1 + GET /people?and=(grade.gte.90,student.is.true,or(age.gte.14,age.is.null)) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?and=(grade.gte.90,student.is.true,or(age.gte.14,age.is.null))" .. _operators: @@ -99,9 +129,15 @@ For more complicated filters you will have to create a new view in the database, The view will provide a new endpoint: -.. code-block:: http +.. tabs:: - GET /fresh_stories HTTP/1.1 + .. code-tab:: http + + GET /fresh_stories HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/fresh_stories" .. _fts: @@ -110,21 +146,45 @@ 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 `_. The following examples illustrate the possibilities. -.. code-block:: http +.. tabs:: - GET /tsearch?my_tsv=fts(french).amusant HTTP/1.1 + .. code-tab:: http -.. code-block:: http + GET /tsearch?my_tsv=fts(french).amusant HTTP/1.1 - GET /tsearch?my_tsv=plfts.The%20Fat%20Cats HTTP/1.1 + .. code-tab:: bash Curl -.. code-block:: http + curl "http://localhost:3000/tsearch?my_tsv=fts(french).amusant" - GET /tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats HTTP/1.1 +.. tabs:: -.. code-block:: http + .. code-tab:: http + + GET /tsearch?my_tsv=plfts.The%20Fat%20Cats HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/tsearch?my_tsv=plfts.The%20Fat%20Cats" + +.. tabs:: + + .. code-tab:: http + + GET /tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats" + +.. tabs:: + + .. code-tab:: http + + GET /tsearch?my_tsv=not.wfts(french).amusant HTTP/1.1 + + .. code-tab:: bash Curl - GET /tsearch?my_tsv=not.wfts(french).amusant HTTP/1.1 + curl "http://localhost:3000/tsearch?my_tsv=not.wfts(french).amusant" Using phrase search mode requires PostgreSQL of version at least 9.6 and will raise an error in earlier versions of the database. @@ -137,9 +197,17 @@ Vertical Filtering (Columns) 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 :sql:`select` parameter. -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /people?select=first_name,age HTTP/1.1 + + .. code-tab:: bash Curl - GET /people?select=first_name,age HTTP/1.1 + curl "http://localhost:3000/people?select=first_name,age" + +.. code-block:: json [ {"first_name": "John", "age": 30}, @@ -153,9 +221,17 @@ Renaming Columns You can rename the columns by prefixing them with an alias followed by the colon ``:`` operator. -.. code-block:: http +.. tabs:: - GET /people?select=fullName:full_name,birthDate:birth_date HTTP/1.1 + .. code-tab:: http + + GET /people?select=fullName:full_name,birthDate:birth_date HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?select=fullName:full_name,birthDate:birth_date" + +.. code-block:: json [ {"fullName": "John Doe", "birthDate": "04/25/1988"}, @@ -169,9 +245,17 @@ Casting Columns Casting the columns is possible by suffixing them with the double colon ``::`` plus the desired type. -.. code-block:: http +.. tabs:: - GET /people?select=full_name,salary::text HTTP/1.1 + .. code-tab:: http + + GET /people?select=full_name,salary::text HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?select=full_name,salary::text" + +.. code-block:: json [ {"full_name": "John Doe", "salary": "90000.00"}, @@ -185,18 +269,34 @@ JSON Columns You can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `_. -.. code-block:: http +.. tabs:: + + .. code-tab:: http - GET /people?select=id,json_data->>blood_type,json_data->phones HTTP/1.1 + GET /people?select=id,json_data->>blood_type,json_data->phones HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: http +.. tabs:: + + .. code-tab:: http + + GET /people?select=id,json_data->phones->0->>number HTTP/1.1 - GET /people?select=id,json_data->phones->0->>number HTTP/1.1 + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?select=id,json_data->phones->0->>number" + +.. code-block:: json [ { "id": 1, "number": "917-929-5745"}, @@ -205,9 +305,17 @@ You can specify a path for a ``json`` or ``jsonb`` column using the arrow operat This also works with filters: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /people?select=id,json_data->blood_type&json_data->>blood_type=eq.A- HTTP/1.1 + + .. code-tab:: bash Curl - GET /people?select=id,json_data->blood_type&json_data->>blood_type=eq.A- HTTP/1.1 + 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-" }, @@ -217,9 +325,17 @@ This also works with filters: Note that ``->>`` is used to compare ``blood_type`` as ``text``. To compare with an integer value use ``->``: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /people?select=id,json_data->age&json_data->age=gt.20 HTTP/1.1 + + .. code-tab:: bash Curl - GET /people?select=id,json_data->age&json_data->age=gt.20 HTTP/1.1 + curl "http://localhost:3000/people?select=id,json_data->age&json_data->age=gt.20" + +.. code-block:: json [ { "id": 11, "age": 25 }, @@ -251,15 +367,27 @@ Filters may be applied to computed columns(**a.k.a. virtual columns**) as well a A full-text search on the computed column: -.. code-block:: http +.. tabs:: + + .. code-tab:: http - GET /people?full_name=fts.Beckett HTTP/1.1 + GET /people?full_name=fts.Beckett HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?full_name=fts.Beckett" As mentioned, computed columns do not appear in the output by default. However you can include them by listing them in the vertical filtering :code:`select` parameter: -.. code-block:: HTTP +.. tabs:: - GET /people?select=*,full_name HTTP/1.1 + .. code-tab:: http + + GET /people?select=*,full_name HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?select=*,full_name" .. important:: @@ -278,9 +406,15 @@ To request this: Do this: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /%D9%85%D9%88%D8%A7%D8%B1%D8%AF HTTP/1.1 - GET /%D9%85%D9%88%D8%A7%D8%B1%D8%AF HTTP/1.1 + .. code-tab:: bash Curl + + curl "http://localhost:3000/%D9%85%D9%88%D8%A7%D8%B1%D8%AF" .. _tabs-cols-w-spaces: @@ -289,9 +423,15 @@ Table / Columns with spaces You can request table/columns with spaces in them by percent encoding the spaces with ``%20``: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /Order%20Items?Unit%20Price=lt.200 HTTP/1.1 - GET /Order%20Items?Unit%20Price=lt.200 HTTP/1.1 + .. code-tab:: bash Curl + + curl "http://localhost:3000/Order%20Items?Unit%20Price=lt.200" .. _reserved-chars: @@ -302,15 +442,27 @@ If filters include PostgREST reserved characters(``,``, ``.``, ``:``, ``()``) yo Here ``Hebdon,John`` and ``Williams,Mary`` are values. -.. code-block:: http +.. tabs:: + + .. code-tab:: http - GET /employees?name=in.(%22Hebdon,John%22,%22Williams,Mary%22) HTTP/1.1 + GET /employees?name=in.(%22Hebdon,John%22,%22Williams,Mary%22) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/employees?name=in.(%22Hebdon,John%22,%22Williams,Mary%22)" Here ``information.cpe`` is a column name. -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /vulnerabilities?%22information.cpe%22=like.*MS* HTTP/1.1 + + .. code-tab:: bash Curl - GET /vulnerabilities?%22information.cpe%22=like.*MS* HTTP/1.1 + 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 ``"\\"``. @@ -330,25 +482,49 @@ Ordering The reserved word :sql:`order` reorders the response rows. It uses a comma-separated list of columns and directions: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /people?order=age.desc,height.asc HTTP/1.1 + + .. code-tab:: bash Curl - GET /people?order=age.desc,height.asc HTTP/1.1 + curl "http://localhost:3000/people?order=age.desc,height.asc" If no direction is specified it defaults to ascending order: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /people?order=age HTTP/1.1 - GET /people?order=age HTTP/1.1 + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?order=age" If you care where nulls are sorted, add ``nullsfirst`` or ``nullslast``: -.. code-block:: http +.. tabs:: - GET /people?order=age.nullsfirst HTTP/1.1 + .. code-tab:: http -.. code-block:: http + GET /people?order=age.nullsfirst HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?order=age.nullsfirst" + +.. tabs:: + + .. code-tab:: http - GET /people?order=age.desc.nullslast HTTP/1.1 + GET /people?order=age.desc.nullslast HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?order=age.desc.nullslast" You can also use :ref:`computed_cols` to order the results, even though the computed columns will not appear in the output. @@ -369,11 +545,19 @@ Here items zero through fourteen are returned. This information is available in There are two ways to apply a limit and offset rows: through request headers or query parameters. When using headers you specify the range of rows desired. This request gets the first twenty people. -.. code-block:: http +.. tabs:: - GET /people HTTP/1.1 - Range-Unit: items - Range: 0-19 + .. code-tab:: http + + GET /people HTTP/1.1 + Range-Unit: items + Range: 0-19 + + .. code-tab:: bash Curl + + 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: @@ -387,9 +571,15 @@ You may also request open-ended ranges for an offset with no limit, e.g. :code:` The other way to request a limit or offset is with query parameters. For example -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /people?limit=15&offset=30 HTTP/1.1 - GET /people?limit=15&offset=30 HTTP/1.1 + .. code-tab:: bash Curl + + 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. @@ -400,12 +590,21 @@ Exact Count In order to obtain the total size of the table or view (such as when rendering the last page link in a pagination control), specify ``Prefer: count=exact`` as a request header: -.. code-block:: http +.. tabs:: - HEAD /bigtable HTTP/1.1 - Range-Unit: items - Range: 0-24 - Prefer: count=exact + .. code-tab:: http + + HEAD /bigtable HTTP/1.1 + Range-Unit: items + Range: 0-24 + Prefer: count=exact + + .. code-tab:: bash Curl + + 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 @@ -423,10 +622,17 @@ Planned Count To avoid the shortcomings of :ref:`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:: http +.. tabs:: + + .. code-tab:: http + + HEAD /bigtable?limit=25 HTTP/1.1 + Prefer: count=planned + + .. code-tab:: bash Curl - HEAD /bigtable?limit=25 HTTP/1.1 - Prefer: count=planned + curl "http://localhost:3000/bigtable?limit=25" -I \ + -H "Prefer: count=planned" .. code-block:: http @@ -453,10 +659,17 @@ defined by :ref:`max-rows`. Here's an example. Suppose we set ``max-rows=1000`` and ``smalltable`` has 321 rows, then we'll get the exact count: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + HEAD /smalltable?limit=25 HTTP/1.1 + Prefer: count=estimated - HEAD /smalltable?limit=25 HTTP/1.1 - Prefer: count=estimated + .. code-tab:: bash Curl + + curl "http://localhost:3000/smalltable?limit=25" -I \ + -H "Prefer: count=estimated" .. code-block:: http @@ -465,10 +678,17 @@ Here's an example. Suppose we set ``max-rows=1000`` and ``smalltable`` has 321 r If we make a similar request on ``bigtable``, which has 3573458 rows, we would get the planned count: -.. code-block:: http +.. tabs:: + + .. code-tab:: http - HEAD /bigtable?limit=25 HTTP/1.1 - Prefer: count=estimated + HEAD /bigtable?limit=25 HTTP/1.1 + Prefer: count=estimated + + .. code-tab:: bash Curl + + curl "http://localhost:3000/bigtable?limit=25" -I \ + -H "Prefer: count=estimated" .. code-block:: http @@ -484,10 +704,17 @@ PostgREST uses proper HTTP content negotiation (`RFC7231 `_. @@ -848,9 +1166,15 @@ Here's a sample function (notice the ``RETURNS SETOF films``). A request with ``directors`` embedded: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers* HTTP/1.1 + + .. code-tab:: bash Curl - GET /rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers* HTTP/1.1 + curl "http://localhost:3000/rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers*" .. code-block:: json @@ -872,19 +1196,36 @@ You can embed related resources after doing :ref:`insert_update` or :ref:`delete Say you want to insert a **film** and then get some of its attributes plus embed its **director**. -.. code-block:: http +.. tabs:: - POST /films?select=title,year,director:directors(first_name,last_name) HTTP/1.1 - Prefer: return=representation + .. code-tab:: http - { - "id": 100, - "director_id": 40, - "title": "127 hours", - "year": 2010, - "rating": 7.6, - "language": "english" - } + POST /films?select=title,year,director:directors(first_name,last_name) HTTP/1.1 + Prefer: return=representation + + { + "id": 100, + "director_id": 40, + "title": "127 hours", + "year": 2010, + "rating": 7.6, + "language": "english" + } + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=title,year,director:directors(first_name,last_name)" \ + -H "Prefer: return=representation" \ + -d @- << EOF + { + "id": 100, + "director_id": 40, + "title": "127 hours", + "year": 2010, + "rating": 7.6, + "language": "english" + } + EOF Response: @@ -917,9 +1258,15 @@ For example, suppose you have the following ``orders`` and ``addresses`` tables: And you try to embed ``orders`` with ``addresses`` (this is the **target**): -.. code-block:: http +.. tabs:: - GET /orders?select=*,addresses(*) HTTP/1.1 + .. code-tab:: http + + GET /orders?select=*,addresses(*) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/orders?select=*,addresses(*)" -i Since the ``orders`` table has two foreign keys to the ``addresses`` table — an order has a billing address and a shipping address — the request is ambiguous and PostgREST will respond with an error: @@ -946,9 +1293,17 @@ Let's try first with the **foreign key constraint name**. To make it clearer we Now we can unambiguously embed the billing address by specifying the ``billing_address`` foreign key constraint as the **target**. -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /orders?select=name,billing_address(name) HTTP/1.1 + + .. code-tab:: bash Curl - GET /orders?select=name,billing_address(name) HTTP/1.1 + curl "http://localhost:3000/orders?select=name,billing_address(name)" + +.. code-block:: json [ { @@ -962,9 +1317,17 @@ Now we can unambiguously embed the billing address by specifying the ``billing_a Alternatively, you can specify the **column name** of the foreign key constraint as the **target**. This can be aliased to make the result more clear. -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /orders?select=name,billing_address:billing_address_id(name) HTTP/1.1 + + .. code-tab:: bash Curl - GET /orders?select=name,billing_address:billing_address_id(name) HTTP/1.1 + curl "http://localhost:3000/orders?select=name,billing_address:billing_address_id(name)" + +.. code-block:: json [ { @@ -984,18 +1347,34 @@ two views of ``addresses``: ``central_addresses`` and ``eastern_addresses``. Since PostgREST supports :ref:`embedding_views` by detecting **source foreign keys** in the views, embedding with the foreign key as the **target** will not be enough for an unambiguous embed: -.. code-block:: http +.. tabs:: + + .. code-tab:: http - GET /orders?select=*,billing_address(*) HTTP/1.1 + GET /orders?select=*,billing_address(*) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/orders?select=*,billing_address(*)" -i + +.. code-block:: http HTTP/1.1 300 Multiple Choices For solving this case, in addition to the **target**, we can add a **hint**. Here we specify ``central_addresses`` as the **target** and the ``billing_address`` foreign key as the **hint**: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /orders?select=*,central_addresses!billing_address(*) HTTP/1.1 + + .. code-tab:: bash Curl + + curl 'http://localhost:3000/orders?select=*,central_addresses!billing_address(*)' -i - GET /orders?select=*,central_addresses!billing_address(*) HTTP/1.1 +.. code-block:: http HTTP/1.1 200 OK @@ -1018,11 +1397,19 @@ All tables and `auto-updatable views `_. @@ -1155,27 +1616,50 @@ 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:: http +.. tabs:: - POST /employees?on_conflict=name HTTP/1.1 - Prefer: resolution=merge-duplicates + .. code-tab:: http - [ - { "name": "Old employee 1", "salary": 40000 }, - { "name": "Old employee 2", "salary": 52000 }, - { "name": "New employee 3", "salary": 60000 } - ] + POST /employees?on_conflict=name HTTP/1.1 + Prefer: resolution=merge-duplicates + + [ + { "name": "Old employee 1", "salary": 40000 }, + { "name": "Old employee 2", "salary": 52000 }, + { "name": "New employee 3", "salary": 60000 } + ] + + .. code-tab:: bash Curl + + 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 PUT ~~~ A single row UPSERT can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: -.. code-block:: http +.. tabs:: + + .. code-tab:: http - PUT /employees?id=eq.4 HTTP/1.1 + PUT /employees?id=eq.4 HTTP/1.1 - { "id": 4, "name": "Sara B.", "salary": 60000 } + { "id": 4, "name": "Sara B.", "salary": 60000 } + + .. code-tab:: bash Curl + + 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. @@ -1190,9 +1674,15 @@ Deletions To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instance deleting inactive users: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + DELETE /user?active=is.false HTTP/1.1 + + .. code-tab:: bash Curl - DELETE /user?active=is.false HTTP/1.1 + curl "http://localhost:3000/user?active=is.false" -X DELETE Deletions also support :code:`Prefer: return=representation` plus :ref:`v_filter`. @@ -1225,9 +1715,15 @@ Stored Procedures Every stored procedure in the API-exposed database schema is accessible under the :code:`/rpc` prefix. The API endpoint supports POST (and in some cases GET) to execute the function. -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + POST /rpc/function_name HTTP/1.1 + + .. code-tab:: bash Curl - POST /rpc/function_name HTTP/1.1 + curl "http://localhost:3000/rpc/function_name" -X POST Such functions can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). @@ -1248,11 +1744,21 @@ For instance, assume we have created this function in the database. The client can call it by posting an object like -.. code-block:: http +.. tabs:: + + .. code-tab:: http - POST /rpc/add_them HTTP/1.1 + POST /rpc/add_them HTTP/1.1 - { "a": 1, "b": 2 } + { "a": 1, "b": 2 } + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/add_them" \ + -X POST -H "Content-Type: application/json" \ + -d '{ "a": 1, "b": 2 }' + +.. code-block:: json 3 @@ -1290,9 +1796,15 @@ Procedures that do not modify the database can be called with the HTTP GET verb Because ``add_them`` is ``IMMUTABLE``, we can alternately call the function with a GET request: -.. code-block:: http +.. tabs:: + + .. code-tab:: http - GET /rpc/add_them?a=1&b=2 HTTP/1.1 + GET /rpc/add_them?a=1&b=2 HTTP/1.1 + + .. code-tab:: bash Curl + + 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``. @@ -1307,12 +1819,23 @@ You can also call a function that takes a single parameter of type JSON by sendi SELECT (param->>'x')::int * (param->>'y')::int $$ LANGUAGE SQL; -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + POST /rpc/mult_them HTTP/1.1 + Prefer: params=single-object + + { "x": 4, "y": 2 } + + .. code-tab:: bash Curl - POST /rpc/mult_them HTTP/1.1 - Prefer: params=single-object + curl "http://localhost:3000/rpc/mult_them" \ + -X POST -H "Content-Type: application/json" \ + -H "Prefer: params=single-object" \ + -d '{ "x": 4, "y": 2 }' - { "x": 4, "y": 2 } +.. code-block:: json 8 @@ -1329,12 +1852,20 @@ You can call a function that takes an array parameter: SELECT array_agg(n + 1) FROM unnest($1) AS n; $$ language sql; -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + POST /rpc/plus_one HTTP/1.1 + Content-Type: application/json - POST /rpc/plus_one HTTP/1.1 - Content-Type: application/json + {"arr": [1,2,3,4]} - {"arr": [1,2,3,4]} + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/plus_one" \ + -X POST -H "Content-Type: application/json" \ + -d '{"arr": [1,2,3,4]}' .. code-block:: json @@ -1343,19 +1874,33 @@ You can call a function that takes an array parameter: For calling the function with GET, you can pass the array as an `array literal `_, as in ``{1,2,3,4}``. Note that the curly brackets have to be urlencoded(``{`` is ``%7B`` and ``}`` is ``%7D``). -.. code-block:: http +.. tabs:: + + .. code-tab:: http - GET /rpc/plus_one?arr=%7B1,2,3,4%7D' HTTP/1.1 + GET /rpc/plus_one?arr=%7B1,2,3,4%7D' HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: http + .. tabs:: - POST /rpc/plus_one HTTP/1.1 + .. code-tab:: http + + POST /rpc/plus_one HTTP/1.1 - { "arr": "{1,2,3,4}" } + { "arr": "{1,2,3,4}" } + + .. code-tab:: bash Curl + + 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. @@ -1372,12 +1917,20 @@ You can call a variadic function by passing a JSON array in a POST request: SELECT array_agg(n + 1) FROM unnest($1) AS n; $$ language sql; -.. code-block:: http +.. tabs:: - POST /rpc/plus_one HTTP/1.1 - Content-Type: application/json + .. code-tab:: http - {"v": [1,2,3,4]} + POST /rpc/plus_one HTTP/1.1 + Content-Type: application/json + + {"v": [1,2,3,4]} + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/plus_one" \ + -X POST -H "Content-Type: application/json" \ + -d '{"v": [1,2,3,4]}' .. code-block:: json @@ -1385,33 +1938,63 @@ You can call a variadic function by passing a JSON array in a POST request: In a GET request, you can repeat the same parameter name: -.. code-block:: http +.. tabs:: + + .. code-tab:: http - GET /rpc/plus_one?v=1&v=2&v=3&v=4 HTTP/1.1 + GET /rpc/plus_one?v=1&v=2&v=3&v=4 HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: http +.. tabs:: + + .. code-tab:: http + + POST /rpc/plus_one HTTP/1.1 + Content-Type: application/x-www-form-urlencoded + + v=1&v=2&v=3&v=4 - POST /rpc/plus_one HTTP/1.1 - Content-Type: application/x-www-form-urlencoded + .. code-tab:: bash Curl - v=1&v=2&v=3&v=4 + 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' Scalar functions ---------------- PostgREST will detect if the function is scalar or table-valued and will shape the response format accordingly: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /rpc/add_them?a=1&b=2 HTTP/1.1 - GET /rpc/add_them?a=1&b=2 HTTP/1.1 + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/add_them?a=1&b=2" + +.. code-block:: json 3 -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /rpc/best_films_2017 HTTP/1.1 + + .. code-tab:: bash Curl - GET /rpc/best_films_2017 HTTP/1.1 + curl "http://localhost:3000/rpc/best_films_2017" + +.. code-block:: json [ { "title": "Okja", "rating": 7.4}, @@ -1427,15 +2010,28 @@ Bulk Call It's possible to call a function in a bulk way, analogously to :ref:`bulk_insert`. To do this, you need to add the ``Prefer: params=multiple-objects`` header to your request. -.. code-block:: http +.. tabs:: - POST /rpc/add_them HTTP/1.1 - Content-Type: text/csv - Prefer: params=multiple-objects + .. code-tab:: http - a,b - 1,2 - 3,4 + POST /rpc/add_them HTTP/1.1 + Content-Type: text/csv + Prefer: params=multiple-objects + + a,b + 1,2 + 3,4 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/add_them" \ + -X POST -H "Content-Type: text/csv" \ + -H "Prefer: params=multiple-objects" \ + --data-binary @- << EOF + a,b + 1,2 + 3,4 + EOF .. code-block:: json @@ -1454,13 +2050,25 @@ A function that returns a table type response can be shaped using the same filte CREATE FUNCTION best_films_2017() RETURNS SETOF films .. -.. code-block:: http +.. tabs:: - GET /rpc/best_films_2017?select=title,director:directors(*) HTTP/1.1 + .. code-tab:: http -.. code-block:: http + GET /rpc/best_films_2017?select=title,director:directors(*) HTTP/1.1 + + .. code-tab:: bash Curl - GET /rpc/best_films_2017?rating=gt.8&order=title.desc HTTP/1.1 + curl "http://localhost:3000/rpc/best_films_2017?select=title,director:directors(*)" + +.. tabs:: + + .. code-tab:: http + + GET /rpc/best_films_2017?rating=gt.8&order=title.desc HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/best_films_2017?rating=gt.8&order=title.desc" Overloaded functions -------------------- @@ -1473,13 +2081,25 @@ You can call overloaded functions with different number of arguments. CREATE FUNCTION rental_duration(customer_id integer, from_date date) .. -.. code-block:: http +.. tabs:: - GET /rpc/rental_duration?customer_id=232 HTTP/1.1 + .. code-tab:: http -.. code-block:: http + GET /rpc/rental_duration?customer_id=232 HTTP/1.1 - GET /rpc/rental_duration?customer_id=232&from_date=2018-07-01 HTTP/1.1 + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/rental_duration?customer_id=232" + +.. tabs:: + + .. code-tab:: http + + GET /rpc/rental_duration?customer_id=232&from_date=2018-07-01 HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/rental_duration?customer_id=232&from_date=2018-07-01" .. important:: @@ -1493,10 +2113,17 @@ Binary Output If you want to return raw binary data from a :code:`bytea` column, you must specify :code:`application/octet-stream` as part of the :code:`Accept` header and select a single column :code:`?select=bin_data`. -.. code-block:: http +.. tabs:: - GET /items?select=bin_data&id=eq.1 HTTP/1.1 - Accept: application/octet-stream + .. code-tab:: http + + GET /items?select=bin_data&id=eq.1 HTTP/1.1 + Accept: application/octet-stream + + .. code-tab:: bash Curl + + curl "http://localhost:3000/items?select=bin_data&id=eq.1" \ + -H "Accept: application/octet-stream" You can also request binary output when calling `Stored Procedures`_ and since they can return a scalar value you are not forced to use :code:`select` for this case. @@ -1505,10 +2132,17 @@ for this case. CREATE FUNCTION closest_point(..) RETURNS bytea .. -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + POST /rpc/closest_point HTTP/1.1 + Accept: application/octet-stream + + .. code-tab:: bash Curl - POST /rpc/closest_point HTTP/1.1 - Accept: application/octet-stream + curl "http://localhost:3000/rpc/closest_point" \ + -X POST -H "Accept: application/octet-stream" If the stored procedure returns non-scalar values, you need to do a :code:`select` in the same way as for GET binary output. @@ -1516,10 +2150,17 @@ If the stored procedure returns non-scalar values, you need to do a :code:`selec CREATE FUNCTION overlapping_regions(..) RETURNS SETOF TABLE(geom_twkb bytea, ..) .. -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + POST /rpc/overlapping_regions?select=geom_twkb HTTP/1.1 + Accept: application/octet-stream - POST /rpc/overlapping_regions?select=geom_twkb HTTP/1.1 - Accept: application/octet-stream + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/overlapping_regions?select=geom_twkb" \ + -X POST -H "Accept: application/octet-stream" .. note:: @@ -1532,10 +2173,19 @@ Plain Text Output You can get raw output from a ``text`` column by using ``Accept: text/plain``. -.. code-block:: http +.. tabs:: + + .. code-tab:: http - GET /workers?select=custom_psv_format HTTP/1.1 - Accept: text/plain + GET /workers?select=custom_psv_format HTTP/1.1 + Accept: text/plain + + .. code-tab:: bash Curl + + curl "http://localhost:3000/workers?select=custom_psv_format" \ + -H "Accept: text/plain" + +.. code-block:: text 09310817|JOHN|DOE|15/04/88| 42152780|FRED|BLOGGS|20/02/85| @@ -1596,9 +2246,15 @@ You can verify which HTTP methods are allowed on endpoints for tables and views For a table named ``people``, OPTIONS would show: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + OPTIONS /people HTTP/1.1 + + .. code-tab:: bash Curl - OPTIONS /people HTTP/1.1 + curl "http://localhost:3000/people" -X OPTIONS -i .. code-block:: http @@ -1648,19 +2304,35 @@ You can switch schemas at runtime with the ``Accept-Profile`` and ``Content-Prof For GET or HEAD, the schema to be used can be selected through the ``Accept-Profile`` header: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /items HTTP/1.1 + Accept-Profile: tenant2 - GET /items HTTP/1.1 - Accept-Profile: tenant2 + .. code-tab:: bash Curl + + curl "http://localhost:3000/items" \ + -H "Accept-Profile: tenant2" For POST, PATCH, PUT and DELETE, you can use the ``Content-Profile`` header for selecting the schema: -.. code-block:: http +.. tabs:: + + .. code-tab:: http - POST /items HTTP/1.1 - Content-Profile: tenant2 + POST /items HTTP/1.1 + Content-Profile: tenant2 - {...} + {...} + + .. code-tab:: bash Curl + + 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`. @@ -1763,17 +2435,24 @@ As an example, let's add some cache headers for all requests that come from an I Now when you make a GET request to a table or view, you'll get the cache headers. -.. code-block:: http +.. tabs:: + + .. code-tab:: http - GET /people HTTP/1.1 - User-Agent: Mozilla/4.01 (compatible; MSIE 6.0; Windows NT 5.1) + GET /people HTTP/1.1 + User-Agent: Mozilla/4.01 (compatible; MSIE 6.0; Windows NT 5.1) + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people" -i \ + -H "User-Agent: Mozilla/4.01 (compatible; MSIE 6.0; Windows NT 5.1)" + +.. code-block:: http HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Cache-Control: no-cache, no-store, must-revalidate - ... - .. _guc_resp_status: Setting Response Status Code @@ -1791,9 +2470,15 @@ You can set the ``response.status`` GUC to override the default status code Post end; $$ language plpgsql; -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /rpc/teapot HTTP/1.1 + + .. code-tab:: bash Curl - GET /rpc/teapot HTTP/1.1 + curl "http://localhost:3000/rpc/teapot" -i .. code-block:: http diff --git a/auth.rst b/auth.rst index 748a9fe291..8e3383cb0e 100644 --- a/auth.rst +++ b/auth.rst @@ -150,10 +150,17 @@ Client Auth To make an authenticated request the client must include an :code:`Authorization` HTTP header with the value :code:`Bearer `. For instance: -.. code:: http +.. tabs:: - GET /foo HTTP/1.1 - Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZSIsImV4cCI6MTQ3NTUxNjI1MH0.GYDZV3yM0gqvuEtJmfpplLBXSGYnke_Pvnl0tbKAjB4 + .. code-tab:: http + + GET /foo HTTP/1.1 + Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZSIsImV4cCI6MTQ3NTUxNjI1MH0.GYDZV3yM0gqvuEtJmfpplLBXSGYnke_Pvnl0tbKAjB4 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/foo" \ + -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZSIsImV4cCI6MTQ3NTUxNjI1MH0.GYDZV3yM0gqvuEtJmfpplLBXSGYnke_Pvnl0tbKAjB4" The ``Bearer`` header value can be used with or without capitalization(``bearer``). @@ -422,11 +429,19 @@ As described in `JWT from SQL`_, we'll create a JWT inside our login function. N An API request to call this function would look like: -.. code:: http +.. tabs:: + + .. code-tab:: http + + POST /rpc/login HTTP/1.1 + + { "email": "foo@bar.com", "pass": "foobar" } - POST /rpc/login HTTP/1.1 + .. code-tab:: bash Curl - { "email": "foo@bar.com", "pass": "foobar" } + 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 `_. (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!) diff --git a/conf.py b/conf.py index 6e017e016d..be38720aa5 100644 --- a/conf.py +++ b/conf.py @@ -28,7 +28,10 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [] +extensions = [ + 'sphinx_tabs.tabs', + 'sphinx_copybutton' +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -293,3 +296,5 @@ def setup(app): # 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 diff --git a/default.nix b/default.nix index 039ae825bf..40db658258 100644 --- a/default.nix +++ b/default.nix @@ -15,7 +15,10 @@ let }) { }; - python = pkgs.python3.withPackages (ps: [ ps.sphinx ps.sphinx_rtd_theme ps.livereload ]); + sphinxTabsPkg = ps: ps.callPackage ./extensions/sphinx-tabs.nix {}; + sphinxCopybuttonPkg = ps: ps.callPackage ./extensions/sphinx-copybutton.nix {}; + + python = pkgs.python3.withPackages (ps: [ ps.sphinx ps.sphinx_rtd_theme ps.livereload (sphinxTabsPkg ps) (sphinxCopybuttonPkg ps) ]); in { inherit pkgs; diff --git a/extensions/sphinx-copybutton.nix b/extensions/sphinx-copybutton.nix new file mode 100644 index 0000000000..8d408d300c --- /dev/null +++ b/extensions/sphinx-copybutton.nix @@ -0,0 +1,33 @@ +{ lib +, buildPythonPackage +, fetchFromGitHub +, sphinx +}: + +buildPythonPackage rec { + pname = "sphinx-copybutton"; + version = "0.4.0"; + + src = fetchFromGitHub { + owner = "executablebooks"; + repo = "sphinx-copybutton"; + rev = "v${version}"; + sha256 = "sha256-vrEIvQeP7AMXSme1PBp0ox5k8Q1rz+1cbHIO+o17Jqc="; + fetchSubmodules = true; + }; + + propagatedBuildInputs = [ + sphinx + ]; + + doCheck = false; # no tests + + pythonImportsCheck = [ "sphinx_copybutton" ]; + + meta = with lib; { + description = "A small sphinx extension to add a \"copy\" button to code blocks"; + homepage = "https://github.com/executablebooks/sphinx-copybutton"; + license = licenses.mit; + maintainers = with maintainers; [ Luflosi ]; + }; +} diff --git a/extensions/sphinx-tabs.nix b/extensions/sphinx-tabs.nix new file mode 100644 index 0000000000..f90b1c18d8 --- /dev/null +++ b/extensions/sphinx-tabs.nix @@ -0,0 +1,29 @@ +{ lib +, buildPythonPackage +, fetchPypi +, sphinx +}: + +buildPythonPackage rec { + pname = "sphinx-tabs"; + version = "3.2.0"; + + src = fetchPypi { + inherit pname version; + sha256 = "sha256:1970aahi6sa7c37cpz8nwgdb2xzf21rk6ykdd1m6w9wvxla7j4rk"; + }; + + propagatedBuildInputs = [ + sphinx + ]; + + doCheck = false; + + pythonImportsCheck = [ "sphinx_tabs" ]; + + meta = with lib; { + description = "Create tabbed content in Sphinx documentation when building HTML"; + homepage = "https://sphinx-tabs.readthedocs.io"; + license = licenses.mit; + }; +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 163f583b56..7c17253241 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,3 @@ -docutils==0.17.1 \ No newline at end of file +docutils==0.17.1 +sphinx-tabs +sphinx-copybutton \ No newline at end of file diff --git a/schema_cache.rst b/schema_cache.rst index f6fd237499..1e708876df 100644 --- a/schema_cache.rst +++ b/schema_cache.rst @@ -41,9 +41,15 @@ Stale Foreign Key Relationships Suppose you add a ``cities`` table to your database and define a foreign key that references an existing ``countries`` table. Then, you make a request to get the ``cities`` and their belonging ``countries``. -.. code-block:: http +.. tabs:: - GET /cities?select=name,country:countries(id,name) HTTP/1.1 + .. code-tab:: http + + GET /cities?select=name,country:countries(id,name) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/cities?select=name,country:countries(id,name)" The result will be an error: @@ -70,9 +76,15 @@ The same issue will occur on newly created functions on a running PostgREST. SELECT num + 1; $$ LANGUAGE SQL IMMUTABLE; -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /rpc/plus_one?num=1 HTTP/1.1 + + .. code-tab:: bash Curl - GET /rpc/plus_one?num=1 HTTP/1.1 + curl "http://localhost:3000/rpc/plus_one?num=1" .. code-block:: json From 7cc3d319bc3971c8d897b817bc9f072630712a39 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 29 Oct 2021 15:11:39 -0500 Subject: [PATCH 445/549] Fix broken styles --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7c17253241..5a5a51b65a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -docutils==0.17.1 +docutils<0.18 sphinx-tabs sphinx-copybutton \ No newline at end of file From 8b74e14c4f9e9ca3762795a19411bdb68d0d6bcb Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 8 Nov 2021 12:27:22 -0500 Subject: [PATCH 446/549] Use tabs to show different installation methods --- install.rst | 50 ++++++++++++++++++++++++++++++++++---------------- postgrest.dict | 1 - 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/install.rst b/install.rst index 2c03932e18..2d789ad083 100644 --- a/install.rst +++ b/install.rst @@ -6,36 +6,54 @@ Installation The release page has `pre-compiled binaries for Mac OS X, Windows, Linux and FreeBSD `_ . The Linux binary is a static executable that can be run on any Linux distribution. -If you use **macOS Homebrew**, then you can install PostgREST from the `official repo `_. +You can also use your OS package manager. -.. code:: bash +.. tabs:: - brew install postgrest + .. group-tab:: Mac OSX -If you use **FreeBSD**, then you can install PostgREST from the `official ports `_. + You can install PostgREST from the `Homebrew official repo `_. -.. code:: bash + .. code:: bash - pkg install hs-postgrest + brew install postgrest -If you use **Arch Linux**, then you can install PostgREST from the `community repo `_. + .. group-tab:: FreeBSD -.. code:: bash + You can install PostgREST from the `official ports `_. - pacman -S postgrest + .. code:: bash -If you use **Nix**, then you can install PostgREST from nixpkgs. + pkg install hs-postgrest -.. code:: bash + .. group-tab:: Linux - nix-env -i haskellPackages.postgrest + .. tabs:: -If you use Windows, you can install PostgREST using `Chocolatey `_ or `Scoop `_. + .. tab:: Arch Linux -.. code:: bash + You can install PostgREST from the `community repo `_. + + .. 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 `_ or `Scoop `_. + + .. code:: bash - choco install postgrest - scoop install postgrest + choco install postgrest + scoop install postgrest Running PostgREST ================= diff --git a/postgrest.dict b/postgrest.dict index 5a26892395..684faa9340 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -75,7 +75,6 @@ logins lon lt lte -macOS middleware misprediction Mithril From 4e4458eb95eb8ae57031b305b5fec2893eb2f2f8 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 10 Nov 2021 19:35:44 -0500 Subject: [PATCH 447/549] Allow POST rpc with a single unnamed parameter --- api.rst | 42 ++++++++++++++++++++++++++++++++++++++++++ releases/upcoming.rst | 3 +++ 2 files changed, 45 insertions(+) diff --git a/api.rst b/api.rst index 5e3fbe1ecf..6917c937ff 100644 --- a/api.rst +++ b/api.rst @@ -1808,6 +1808,8 @@ Because ``add_them`` is ``IMMUTABLE``, we can alternately call the function with 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: + Calling functions with a single JSON parameter ---------------------------------------------- @@ -1839,6 +1841,46 @@ You can also call a function that takes a single parameter of type JSON by sendi 8 +.. _s_proc_single_unnamed: + +Calling functions with a single unnamed parameter +------------------------------------------------- + +You can make a POST request to a function with a single unnamed parameter to send raw ``json/jsonb``, ``bytea`` or ``text`` data. + +To send raw JSON, you can avoid using the ``Prefer: params=single-object`` header if the function has a single unnamed ``json`` or ``jsonb`` parameter and the header ``Content-Type: application/json`` is included in the request. + +.. code-block:: plpgsql + + CREATE FUNCTION mult_them(json) RETURNS int AS $$ + SELECT ($1->>'x')::int * ($1->>'y')::int + $$ LANGUAGE SQL; + +.. tabs:: + + .. code-tab:: http + + POST /rpc/mult_them HTTP/1.1 + Content-Type: application/json + + { "x": 4, "y": 2 } + + .. code-tab:: bash Curl + + 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. + +To send raw binary, the parameter type must be ``bytea`` and the header ``Content-Type: application/octet-stream`` must be included in the request. Similarly, 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: Calling functions with array parameters diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 36fd101b18..48446f113d 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -35,6 +35,9 @@ Added * Add ``Retry-After`` header when recovering the connection. See :ref:`automatic_recovery`. |br| -- `@gautam1168 `_ +* Allow calling a function with a :ref:`single unnamed parameter ` to POST raw ``json/jsonb``, ``bytea`` or ``text``. + |br| -- `@steve-chavez `_ + * Documentation improvements + Added :ref:`nested_embedding` to the :ref:`resource_embedding` section. From 4720cfc1d36081e3bf3de88f441b6972bfad38f4 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 11 Nov 2021 17:02:17 -0500 Subject: [PATCH 448/549] Add example to rpc unnamed single bytea parameter --- api.rst | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 6917c937ff..661fba0913 100644 --- a/api.rst +++ b/api.rst @@ -1848,7 +1848,7 @@ Calling functions with a single unnamed parameter You can make a POST request to a function with a single unnamed parameter to send raw ``json/jsonb``, ``bytea`` or ``text`` data. -To send raw JSON, you can avoid using the ``Prefer: params=single-object`` header if the function has a single unnamed ``json`` or ``jsonb`` parameter and the header ``Content-Type: application/json`` is included in the request. +To send raw JSON, the function must have a single unnamed ``json`` or ``jsonb`` parameter and the header ``Content-Type: application/json`` must be included in the request. .. code-block:: plpgsql @@ -1879,7 +1879,38 @@ To send raw JSON, you can avoid using the ``Prefer: params=single-object`` heade 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. -To send raw binary, the parameter type must be ``bytea`` and the header ``Content-Type: application/octet-stream`` must be included in the request. Similarly, to send raw text, the parameter type must be ``text`` and the header ``Content-Type: text/plain`` 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; + +.. tabs:: + + .. code-tab:: http + + POST /rpc/upload_binary HTTP/1.1 + Content-Type: application/octet-stream + + file_name.ext + + .. code-tab:: bash Curl + + 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: From 06c0180e4f297a071c7ff3d166defa3e9bd74a44 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 15 Nov 2021 17:36:29 -0500 Subject: [PATCH 449/549] Add the Logical Operators section to Horizontal Filtering --- api.rst | 65 ++++++++++++++++++++++++------------------- postgrest.dict | 1 - releases/upcoming.rst | 1 + 3 files changed, 37 insertions(+), 30 deletions(-) diff --git a/api.rst b/api.rst index 661fba0913..077a6665d1 100644 --- a/api.rst +++ b/api.rst @@ -27,7 +27,7 @@ There are no deeply/nested/routes. Each route provides OPTIONS, GET, HEAD, POST, Horizontal Filtering (Rows) --------------------------- -You can filter result rows by adding conditions on columns, each condition a query string parameter. For instance, to return people aged under 13 years old: +You can filter result rows by adding conditions on columns. For instance, to return people aged under 13 years old: .. tabs:: @@ -39,7 +39,7 @@ You can filter result rows by adding conditions on columns, each condition a que curl "http://localhost:3000/people?age=lt.13" -Multiple parameters can be logically conjoined by: +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: .. tabs:: @@ -51,30 +51,6 @@ Multiple parameters can be logically conjoined by: curl "http://localhost:3000/people?age=gte.18&student=is.true" -Multiple parameters can be logically disjoined by: - -.. tabs:: - - .. code-tab:: http - - GET /people?or=(age.gte.14,age.lte.18) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people?or=(age.gte.14,age.lte.18)" - -Complex logic can also be applied: - -.. tabs:: - - .. code-tab:: http - - GET /people?and=(grade.gte.90,student.is.true,or(age.gte.14,age.is.null)) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people?and=(grade.gte.90,student.is.true,or(age.gte.14,age.is.null))" - .. _operators: Operators @@ -111,11 +87,11 @@ 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 below +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` ============ ======================== ================================================================================== -To negate any operator, prefix it with :code:`not` like :code:`?a=not.eq.2` or :code:`?not.and=(a.gte.0,a.lte.100)` . - 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 @@ -139,6 +115,37 @@ The view will provide a new endpoint: 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: + +.. tabs:: + + .. code-tab:: http + + GET /people?or=(age.lt.18,age.gt.21) HTTP/1.1 + + .. code-tab:: bash Curl + + 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: + +.. tabs:: + + .. code-tab:: http + + GET /people?grade=gte.90&student=is.true&or=(age.eq.14,not.and(age.gte.11,age.lte.17)) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?grade=gte.90&student=is.true&or=(age.eq.14,not.and(age.gte.11,age.lte.17))" + .. _fts: Full-Text Search diff --git a/postgrest.dict b/postgrest.dict index 684faa9340..fba460fd99 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -27,7 +27,6 @@ Daemonizing DDL DevOps DiBiase -disjoined dockerize DoS eq diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 48446f113d..2a958c7e29 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -42,6 +42,7 @@ Added + Added :ref:`nested_embedding` to the :ref:`resource_embedding` section. + Added the :ref:`templates` section to the :doc:`Ecosystem `. + + Added the :ref:`logical_operators` section Fixed ----- From d12974339acec70abd0000100b5db23da856f660 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 18 Nov 2021 17:34:51 -0500 Subject: [PATCH 450/549] Allow unknown for is operator --- api.rst | 2 +- releases/upcoming.rst | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 077a6665d1..1feb1a60c0 100644 --- a/api.rst +++ b/api.rst @@ -72,7 +72,7 @@ ilike :code:`ILIKE` ILIKE operator (use * in place of %) 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) +is :code:`IS` checking for exact equality (null,true,false,unknown) fts :code:`@@` :ref:`fts` using to_tsquery plfts :code:`@@` :ref:`fts` using plainto_tsquery phfts :code:`@@` :ref:`fts` using phraseto_tsquery diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 2a958c7e29..9cbba9fe33 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -38,6 +38,9 @@ Added * Allow calling a function with a :ref:`single unnamed parameter ` to POST raw ``json/jsonb``, ``bytea`` or ``text``. |br| -- `@steve-chavez `_ +* Allow specifying ``unknown`` for the ``is`` :ref:`operator `. + |br| -- `@steve-chavez `_ + * Documentation improvements + Added :ref:`nested_embedding` to the :ref:`resource_embedding` section. From c946f02607a080c88779d2c6fb3a59eec79b9255 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 22 Nov 2021 14:43:24 -0500 Subject: [PATCH 451/549] Remove partitions from the schema cache --- api.rst | 27 ++++++++------------------- releases/upcoming.rst | 6 ++++++ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/api.rst b/api.rst index 1feb1a60c0..a58d4356a7 100644 --- a/api.rst +++ b/api.rst @@ -720,7 +720,7 @@ Use the Accept request header to specify the acceptable format (or formats) for .. code-tab:: bash Curl - curl "http://localhost:3000/people" + curl "http://localhost:3000/people" \ -H "Accept: application/json" The current possibilities are: @@ -757,7 +757,7 @@ This can be inconvenient for client code. To return the first result as an objec .. code-tab:: bash Curl - curl "http://localhost:3000/items?id=eq.1" + curl "http://localhost:3000/items?id=eq.1" \ -H "Accept: application/vnd.pgrst.object+json" This returns @@ -1084,20 +1084,9 @@ Since it contains the ``films_id`` foreign key, it is possible to embed ``box_of curl "http://localhost:3000/box_office?select=bo_date,gross_revenue,films(title)&gross_revenue=gte.1000000" -Embedding is also possible between ``box_office`` partitions and the ``films`` table: - -.. tabs:: - - .. code-tab:: http - - GET /films?select=title,box_office_2021_02(bo_date,gross_revenue)&rating=gt.8 HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/films?select=title,box_office_2021_02(bo_date,gross_revenue)&rating=gt.8" - .. note:: - Partitioned tables can reference other tables since PostgreSQL 11 but can only be referenced from any other table since PostgreSQL 12. + * Partitioned tables can reference other tables since PostgreSQL 11 but can only be referenced from any other table since PostgreSQL 12. + * Embedding on partitions is not allowed because it leads to ambiguity errors (see :ref:`embed_disamb`) between them and their parent partitioned table. :ref:`custom_queries` can be used if this is needed. .. _embedding_views: @@ -1600,8 +1589,8 @@ You can make an UPSERT with :code:`POST` and the :code:`Prefer: resolution=merge .. code-tab:: bash Curl - curl "http://localhost:3000OST /employees" \ - -X POST -H "Content-Type: application/json" + curl "http://localhost:3000/employees" \ + -X POST -H "Content-Type: application/json" \ -H "Prefer: resolution=merge-duplicates" \ -d @- << EOF [ @@ -1639,7 +1628,7 @@ By specifying the ``on_conflict`` query parameter, you can make UPSERT work on a .. code-tab:: bash Curl curl "http://localhost:3000/employees?on_conflict=name" \ - -X POST -H "Content-Type: application/json" + -X POST -H "Content-Type: application/json" \ -H "Prefer: resolution=merge-duplicates" \ -d @- << EOF [ @@ -2042,7 +2031,7 @@ Repeating also works in POST requests with ``Content-Type: application/x-www-for .. code-tab:: bash Curl curl "http://localhost:3000/rpc/plus_one" \ - -X POST -H "Content-Type: application/x-www-form-urlencoded" + -X POST -H "Content-Type: application/x-www-form-urlencoded" \ -d 'v=1&v=2&v=3&v=4' Scalar functions diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 9cbba9fe33..b62c2d63fa 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -52,3 +52,9 @@ Fixed * Fix using single double quotes (``"``) and backslashes (``/``) as values on the "in" operator |br| -- `@steve-chavez `_ + +Changed +------- + +* Partitions (created using ``PARTITION OF``) are no longer included in the :ref:`schema_cache`. + |br| -- `@laurenceisla `_ From 35d9b98bff9f177179c7d8911f1c6a80557192f0 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Tue, 23 Nov 2021 22:12:29 -0500 Subject: [PATCH 452/549] Remove the db-embed-default-join config --- api.rst | 61 +++++++++++++++++++++++++++++++------------ configuration.rst | 16 ------------ releases/upcoming.rst | 8 ++---- 3 files changed, 46 insertions(+), 39 deletions(-) diff --git a/api.rst b/api.rst index a58d4356a7..3529b3e194 100644 --- a/api.rst +++ b/api.rst @@ -475,9 +475,15 @@ If the value filtered by the ``in`` operator has a double quote (``"``), you can Here ``Quote:"`` and ``Backslash:\`` are percent-encoded values. Note that ``%5C`` is the percent-encoded backslash. -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /marks?name=in.(%22Quote:%5C%22%22,%22Backslash:%5C%5C%22) HTTP/1.1 - GET /marks?name=in.(%22Quote:%5C%22%22,%22Backslash:%5C%5C%22) HTTP/1.1 + .. code-tab:: bash Curl + + curl "http://localhost:3000/marks?name=in.(%22Quote:%5C%22%22,%22Backslash:%5C%5C%22)" .. note:: @@ -992,9 +998,15 @@ Top Level Filtering By default, embedded filters don't change the top level resource rows at all: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /films?select=title,actors(first_name,last_name)&actors.first_name=eq.Jehanne HTTP/1.1 - GET /films?select=title,actors(first_name,last_name)&actors.first_name=eq.Jehanne HTTP/1.1 + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=title,actors(first_name,last_name)&actors.first_name=eq.Jehanne .. code-block:: json @@ -1020,9 +1032,15 @@ By default, embedded filters don't change the top level resource rows at all: In order to filter the top level rows you need to add ``!inner`` to the embedded resource. For instance, to get **only** the films that have an actor named ``Jehanne``: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /films?select=title,actors!inner(first_name,last_name)&actors.first_name=eq.Jehanne HTTP/1.1 + + .. code-tab:: bash Curl - GET /films?select=title,actors!inner(first_name,last_name)&actors.first_name=eq.Jehanne HTTP/1.1 + curl "http://localhost:3000/films?select=title,actors!inner(first_name,last_name)&actors.first_name=eq.Jehanne" .. code-block:: json @@ -1038,12 +1056,6 @@ In order to filter the top level rows you need to add ``!inner`` to the embedded } ] -If you prefer to work with top level filtering as a default embedding behavior for PostgREST, set the :ref:`db-embed-default-join` configuration parameter to ``"inner"``. This way, you don't need to specify ``!inner`` on every request and, if you need the previous behavior, add ``!left`` to the embedding resource. For instance, this will not filter the films in any way: - -.. code-block:: http - - GET /films?select=title,actors!left(first_name,last_name)&actors.first_name=eq.Jehanne HTTP/1.1 - .. _embedding_partitioned_tables: Embedding Partitioned Tables @@ -1380,9 +1392,15 @@ Similarly to the **target**, the **hint** can be a **table name**, **foreign key Hints also work alongside ``!inner`` if a top level filtering is needed. From the above example: -.. code-block:: http +.. tabs:: + + .. code-tab:: http + + GET /orders?select=*,central_addresses!billing_address!inner(*)¢ral_addresses.code=AB1000 HTTP/1.1 - GET /orders?select=*,central_addresses!billing_address!inner(*)¢ral_addresses.code="AB1000" HTTP/1.1 + .. code-tab:: bash Curl + + curl "http://localhost:3000/orders?select=*,central_addresses!billing_address!inner(*)¢ral_addresses.code=AB1000" .. _insert_update: @@ -1682,10 +1700,19 @@ To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instanc Deletions also support :code:`Prefer: return=representation` plus :ref:`v_filter`. -.. code-block:: HTTP +.. tabs:: + + .. code-tab:: http + + DELETE /user?id=eq.1 HTTP/1.1 + Prefer: return=representation + + .. code-tab:: bash Curl - DELETE /user?id=eq.1 HTTP/1.1 - Prefer: return=representation + curl "http://localhost:3000/user?id=eq.1" -X DELETE \ + -H "Prefer: return=representation" + +.. code-block:: json {"id": 1, "email": "johndoe@email.com"} diff --git a/configuration.rst b/configuration.rst index c3a38301d1..2826e2d5d9 100644 --- a/configuration.rst +++ b/configuration.rst @@ -50,7 +50,6 @@ db-channel-enabled Boolean True db-prepared-statements Boolean True db-tx-end String commit db-config Boolean True -db-embed-default-join String left db-use-legacy-gucs Boolean True server-host String !4 server-port Int 3000 @@ -201,21 +200,6 @@ db-config Enables the in-database configuration. -.. _db-embed-default-join: - -db-embed-default-join ---------------------- - - Determines the default embedding type between tables or views when none is specified in the request. For more info, see :ref:`embedding_top_level_filter`. - - .. code:: bash - - # Embeds using LEFT JOIN - db-embed-default-join = "left" - - # Embeds using INNER JOIN - db-embed-default-join = "inner" - .. _db-use-legacy-gucs: db-use-legacy-gucs diff --git a/releases/upcoming.rst b/releases/upcoming.rst index b62c2d63fa..3f79dba41c 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -14,12 +14,8 @@ Added * Allow :ref:`embedding `, UPSERT, INSERT with Location response, OPTIONS request and OpenAPI support for partitioned tables. |br| -- `@laurenceisla `_ -* Allow filtering top-level resource based on embedded resources filters - - + This is enabled by adding ``!inner`` to the embedded resource. See :ref:`embedding_top_level_filter`. - + This behavior can be enabled by default by setting the :ref:`db-embed-default-join` to ``"inner"``. - - -- `@steve-chavez `_ +* Allow filtering top-level resource based on embedded resources filters. This is enabled by adding ``!inner`` to the embedded resource. See :ref:`embedding_top_level_filter`. + |br| -- `@steve-chavez `_ * Make GUC names for headers, cookies and jwt claims compatible with PostgreSQL v14. From 9eac683e5146b9dbc2c694cbc5444356ec238369 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Wed, 24 Nov 2021 16:04:55 +0100 Subject: [PATCH 453/549] Drop support for PG 9.5. Related: https://github.com/PostgREST/postgrest/pull/2038 --- api.rst | 6 ------ auth.rst | 2 +- install.rst | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/api.rst b/api.rst index 3529b3e194..31fcbb906d 100644 --- a/api.rst +++ b/api.rst @@ -193,8 +193,6 @@ The :code:`fts` filter mentioned above has a number of options to support flexib curl "http://localhost:3000/tsearch?my_tsv=not.wfts(french).amusant" -Using phrase search mode requires PostgreSQL of version at least 9.6 and will raise an error in earlier versions of the database. - 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: @@ -1677,10 +1675,6 @@ A single row UPSERT can be done by using :code:`PUT` and filtering the primary k All the columns must be specified in the request body, including the primary key columns. -.. note:: - - Upsert features are only available starting from PostgreSQL 9.5 since it uses the `ON CONFLICT clause `_. - .. _delete: Deletions diff --git a/auth.rst b/auth.rst index 8e3383cb0e..0c8c507268 100644 --- a/auth.rst +++ b/auth.rst @@ -65,7 +65,7 @@ You can use row-level security to flexibly restrict visibility and access for th We want to enforce a policy that ensures a user can see only those messages sent by him or intended for him. Also we want to prevent a user from forging the message_from column with another person's name. -PostgreSQL (9.5 and later) allows us to set this policy with row-level security: +PostgreSQL allows us to set this policy with row-level security: .. code-block:: postgres diff --git a/install.rst b/install.rst index 2d789ad083..0a454835de 100644 --- a/install.rst +++ b/install.rst @@ -106,7 +106,7 @@ For a complete reference of the configuration file, see :ref:`configuration`. PostgreSQL dependency --------------------- -To use PostgREST you will need an underlying database. We require PostgreSQL 9.5 or greater. You can use something like `Amazon RDS `_ but installing your own locally is cheaper and more convenient for development. You can also run PostgreSQL in a :ref:`docker container`. +To use PostgREST you will need an underlying database. We require PostgreSQL 9.6 or greater. You can use something like `Amazon RDS `_ but installing your own locally is cheaper and more convenient for development. You can also run PostgreSQL in a :ref:`docker container`. Docker ====== From c46c3b7a860939e60cbdfaa8ef49fdeb490fc8e8 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 29 Nov 2021 03:27:38 -0500 Subject: [PATCH 454/549] Bump to PostgREST v9 (#468) * Add highlights and modify changelog structure * Rename upcoming page to v9.0.0 * Classify features, edit breaking changes and fixes * Highlight top-level filtering * Clarify PostgreSQL 14 breaking change * Add devops to doc improvements * Add curl snippets to doc improvements --- admin.rst | 2 +- api.rst | 23 ++++++-- ecosystem.rst | 6 +- index.rst | 11 +++- releases/upcoming.rst | 56 ------------------ releases/v9.0.0.rst | 130 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 159 insertions(+), 69 deletions(-) delete mode 100644 releases/upcoming.rst create mode 100644 releases/v9.0.0.rst diff --git a/admin.rst b/admin.rst index d1352228a6..054a31a393 100644 --- a/admin.rst +++ b/admin.rst @@ -207,7 +207,7 @@ Automatic Connection Recovery When PostgREST loses the connection to the database, it retries the connection using capped exponential backoff, with 32 seconds being the maximum backoff time. -This retry behavior is triggered immediately after the connection is lost if :ref:`db-channel-enabled` is set to true (this is the default behavior), otherwise it will be activated once a request is made. +This retry behavior is triggered immediately after the connection is lost if :ref:`db-channel-enabled` is set to true(the default), otherwise it will be activated once a request is made. To notify the client when the next reconnection attempt will be, PostgREST responds with ``503 Service Unavailable`` and the ``Retry-After: x`` header, where ``x`` is the number of seconds programmed for the next retry. diff --git a/api.rst b/api.rst index 31fcbb906d..eb79826ab0 100644 --- a/api.rst +++ b/api.rst @@ -910,6 +910,8 @@ If you want to embed through join tables but need more control on the intermedia curl "http://localhost:3000/actors?select=roles(character,films(title,year))" +.. _embed_filters: + Embedded Filters ---------------- @@ -991,10 +993,10 @@ The result will show the nested actors named Tom and order them by last name. Al .. _embedding_top_level_filter: -Top Level Filtering -~~~~~~~~~~~~~~~~~~~ +Embedding with Top-level Filtering +---------------------------------- -By default, embedded filters don't change the top level resource rows at all: +By default, :ref:`embed_filters` don't change the top-level resource(``films``) rows at all: .. tabs:: @@ -1095,8 +1097,9 @@ Since it contains the ``films_id`` foreign key, it is possible to embed ``box_of curl "http://localhost:3000/box_office?select=bo_date,gross_revenue,films(title)&gross_revenue=gte.1000000" .. note:: + * Embedding on partitions is not allowed because it leads to ambiguity errors (see :ref:`embed_disamb`) between them and their parent partitioned table(more details at `#1783(comment) `_). :ref:`custom_queries` can be used if this is needed. + * Partitioned tables can reference other tables since PostgreSQL 11 but can only be referenced from any other table since PostgreSQL 12. - * Embedding on partitions is not allowed because it leads to ambiguity errors (see :ref:`embed_disamb`) between them and their parent partitioned table. :ref:`custom_queries` can be used if this is needed. .. _embedding_views: @@ -1255,6 +1258,8 @@ For doing resource embedding, PostgREST infers the relationship between two tabl However, in cases where there's more than one foreign key between two tables, it's not possible to infer the relationship unambiguously by just specifying the tables names. +.. _target_disamb: + Target Disambiguation ~~~~~~~~~~~~~~~~~~~~~ @@ -1281,6 +1286,8 @@ the request is ambiguous and PostgREST will respond with an error: HTTP/1.1 300 Multiple Choices + {..} + If this happens, you need to disambiguate the request by adding precision to the **target**. Instead of the **table name**, you can specify the **foreign key constraint name** or the **column name** that is part of the foreign key. @@ -1344,6 +1351,8 @@ the result more clear. } ] +.. _hint_disamb: + Hint Disambiguation ~~~~~~~~~~~~~~~~~~~ @@ -2430,8 +2439,10 @@ You can also select the schema for :ref:`s_procs` and :ref:`open-api`. These headers are based on the nascent "Content Negotiation by Profile" spec: https://www.w3.org/TR/dx-prof-conneg -HTTP Logic -========== +.. _http_context: + +HTTP Context +============ .. _guc_req_headers_cookies_claims: diff --git a/ecosystem.rst b/ecosystem.rst index 787a8b75d1..6d4aa68c1b 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -28,7 +28,6 @@ Example Apps ------------ * `chronicle `_ - tracking a tree of personal memories -* `cloudgov-demo-postgrest `_ - demo for a federally-compliant REST API on cloud.gov * `code-du-travail-backoffice `_ - data administration portal for the official French Labor Code and Agreements * `delibrium-postgrest `_ - example school API and front-end in Vue.js * `elm-workshop `_ - building a simple database query UI @@ -53,13 +52,14 @@ Example Apps * `PostGUI `_ - React Material UI admin panel * `prospector `_ - data warehouse and visualization platform -.. _dev_ops: +.. _devops: DevOps ------ -* `jbkarle/postgrest `_ - helm chart with a demo database for development and test purposes +* `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 .. _eco_external_notification: diff --git a/index.rst b/index.rst index eb891698bb..aa48aa9c99 100644 --- a/index.rst +++ b/index.rst @@ -94,12 +94,16 @@ The project has a friendly and growing community. Join our `chat room + releases/v8.0.0 + releases/v7.0.1 + releases/v7.0.0 + releases/v6.0.2 + releases/v5.2.0 Tutorials --------- @@ -212,7 +216,7 @@ PostgREST has a growing ecosystem of examples, libraries, and experiments. Here * :ref:`community_tutorials` * :ref:`templates` * :ref:`eco_example_apps` -* :ref:`dev_ops` +* :ref:`devops` * :ref:`eco_external_notification` * :ref:`eco_extensions` * :ref:`clientside_libraries` @@ -224,6 +228,7 @@ Release Notes Here we'll include the most relevant changes so you can migrate to newer versions easily. You can see the full changelog of each release in the `PostgREST repository `_. +- :doc:`releases/v9.0.0` - :doc:`releases/v8.0.0` - :doc:`releases/v7.0.0` - :doc:`releases/v6.0.2` diff --git a/releases/upcoming.rst b/releases/upcoming.rst deleted file mode 100644 index 3f79dba41c..0000000000 --- a/releases/upcoming.rst +++ /dev/null @@ -1,56 +0,0 @@ -.. |br| raw:: html - -
    - -Upcoming -======== - -These are changes yet unreleased. If you'd like to try them out before a new official release, access `the list of CI runs `_ -and select the newest commit, then download the build from the Artifacts section at the bottom of the page (you'll need a GitHub account to download it). - -Added ------ - -* Allow :ref:`embedding `, UPSERT, INSERT with Location response, OPTIONS request and OpenAPI support for partitioned tables. - |br| -- `@laurenceisla `_ - -* Allow filtering top-level resource based on embedded resources filters. This is enabled by adding ``!inner`` to the embedded resource. See :ref:`embedding_top_level_filter`. - |br| -- `@steve-chavez `_ - -* Make GUC names for headers, cookies and jwt claims compatible with PostgreSQL v14. - - + The GUC names on PostgreSQL 14 are changed to the ones :ref:`mentioned in this section `, while older versions still use the :ref:`guc_legacy_names`. - + PostgreSQL versions below 14 can opt in to the new JSON GUCs by setting the :ref:`db-use-legacy-gucs` config option to false (true by default). - + Managed to avoid a breaking change thanks to `@robertsosinski `_ who reported the bug that only one ``.`` character was allowed in GUC keys to the PostgreSQL team. See the `full discussion `_. - - -- `@laurenceisla `_ - -* Allow escaping inside double quotes with a backslash, e.g. ``?col=in.("Double\"Quote")``, ``?col=in.("Back\\slash")``. See :ref:`reserved-chars`. - |br| -- `@steve-chavez `_ - -* Add ``Retry-After`` header when recovering the connection. See :ref:`automatic_recovery`. - |br| -- `@gautam1168 `_ - -* Allow calling a function with a :ref:`single unnamed parameter ` to POST raw ``json/jsonb``, ``bytea`` or ``text``. - |br| -- `@steve-chavez `_ - -* Allow specifying ``unknown`` for the ``is`` :ref:`operator `. - |br| -- `@steve-chavez `_ - -* Documentation improvements - - + Added :ref:`nested_embedding` to the :ref:`resource_embedding` section. - + Added the :ref:`templates` section to the :doc:`Ecosystem `. - + Added the :ref:`logical_operators` section - -Fixed ------ - -* Fix using single double quotes (``"``) and backslashes (``/``) as values on the "in" operator - |br| -- `@steve-chavez `_ - -Changed -------- - -* Partitions (created using ``PARTITION OF``) are no longer included in the :ref:`schema_cache`. - |br| -- `@laurenceisla `_ diff --git a/releases/v9.0.0.rst b/releases/v9.0.0.rst new file mode 100644 index 0000000000..09187b46fe --- /dev/null +++ b/releases/v9.0.0.rst @@ -0,0 +1,130 @@ + +PostgREST 9.0.0 +=============== + +This major version is released with PostgreSQL 14 compatibility and is accompanied with new features and bug fixes. You can look at the detailed changelog and download the pre-compiled binaries on the `GitHub release page `_. + +Features +-------- + +PostgreSQL 14 compatibility +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +PostgreSQL 14 Beta 1 tightened its GUC naming scheme making it impossible to use multiple dots (``.``) and dashes (``-``) on custom GUC parameters, this caused our `old HTTP Context `_ to fail across all requests. Thankfully, `@robertsosinski `_ got the PostgreSQL team to reconsider allowing multiple dots in the GUC name, allowing us to avoid a major breaking change. You can see the full discussion `here `_. + +Still, dashes cannot be used on PostgreSQL 14 custom GUC parameters, so we changed our HTTP Context :ref:`to namespace using a mix of dots and JSON `. On older PostgreSQL versions we still use the :ref:`guc_legacy_names`. If you wish to use the new JSON GUCs on these versions, set the :ref:`db-use-legacy-gucs` config option to false. + +Resource Embedding with Top-level Filtering +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Historically, Resource Embedding was always done with a query that included the equivalent of a ``LEFT JOIN``, which meant you could not +exclude any of the top-level resource rows. You can now use :ref:`embedding_top_level_filter` to do the equivalent of an ``INNER JOIN``, thus you can filter the top-level resource rows with any of the available operators. + +Partitioned Tables +~~~~~~~~~~~~~~~~~~ + +Partitioned tables now integrate with all the feature set. You can :ref:`embed partitioned tables `, UPSERT, INSERT(with a correctly generated Location header) and make OPTIONS requests on them. They're also included in the generated OpenAPI. + +Functions(RPC) +~~~~~~~~~~~~~~ + +* Functions with a :ref:`single unnamed parameter ` can now be used to POST raw ``bytea``, ``text`` or ``json/jsonb``. + +Horizontal Filtering +~~~~~~~~~~~~~~~~~~~~ + +* The ``unknown`` value for three-valued logic can now be used on the ``is`` :ref:`operator `. + +* Escaping double quotes(``"``) in double-quoted surrounded strings is now possible by using backslashes, e.g. ``?col=in.("Double\"Quote")``. Backslashes can be escaped with a preceding backslash, e.g. ``?col=in.("Back\\slash")``. See :ref:`reserved-chars`. + +Administration +~~~~~~~~~~~~~~ + +* A ``Retry-After`` header is now added when PostgREST is doing :ref:`automatic_recovery`. + +Error messages +~~~~~~~~~~~~~~ + +* :ref:`embed_disamb` now shows an improved error message that includes relevant hints for clearing out the ambiguous embedding. + +Documentation improvements +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Added ``curl`` snippets to the :doc:`API <../api>` page. + +* Added the :ref:`automatic_recovery` section. + +* Added the :ref:`nested_embedding` section. + +* Added the :ref:`logical_operators` section. + +* Added the :ref:`templates` and :ref:`devops` sections to the :doc:`Ecosystem `. + +Bug fixes +--------- + +* Correct RPC return type handling for RETURNS TABLE with a single column (`#1930 `_). + +* Schema Cache query failing with ``standard_conforming_strings = off`` (`#1992 `_). + +* OpenAPI missing default values for String types (`#1871 `_). + +Breaking changes +---------------- + +* Dropped support for PostgreSQL 9.5 as it already reached its end-of-life according to `PostgreSQL versioning policy `_. + +* Partitions of a `partitioned table `_ are no longer included in the :ref:`schema_cache`. This is so errors are not generated when doing resource embedding on partitioned tables. + +* Dropped support for doing :ref:`hint_disamb` using dots instead of exclamation marks, e.g. doing ``select=*,projects.client_id(*)`` instead of ``select=*,projects!client_id(*)``). Using dots was undocumented and deprecated back in `v6.0.2 `_. + +Thanks +------ + +Big thanks from the `PostgREST team `_ to our 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/2ndquadrant.png + :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo + :width: 13em + + .. image:: ../_static/retool.png + :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + + .. image:: ../_static/gnuhost.png + :target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + + .. image:: ../_static/supabase.png + :target: https://supabase.io/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage + :width: 13em + + .. image:: ../_static/oblivious.jpg + :target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + +* Evans Fernandes +* `Jan Sommer `_ +* `Franz Gusenbauer `_ +* `Daniel Babiak `_ +* Tsingson Qin +* Michel Pelletier +* Jay Hannah +* Robert Stolarz +* Nicholas DiBiase +* Christopher Reid +* Nathan Bouscal +* Daniel Rafaj +* David Fenko +* Remo Rechkemmer +* Severin Ibarluzea +* Tom Saleeba +* Pawel Tyll + +If you like to join them please consider `supporting PostgREST development `_. From d5fb73bf9bfce9707befd5895d08f8c10eb5bc4a Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Tue, 30 Nov 2021 16:09:21 -0500 Subject: [PATCH 455/549] Fix GUC names using the old syntax --- api.rst | 2 +- auth.rst | 2 +- tutorials/tut1.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api.rst b/api.rst index eb79826ab0..f5d5f796d6 100644 --- a/api.rst +++ b/api.rst @@ -2523,7 +2523,7 @@ As an example, let's add some cache headers for all requests that come from an I create or replace function custom_headers() returns void as $$ declare - user_agent text := current_setting('request.header.user-agent', true); + 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', diff --git a/auth.rst b/auth.rst index 0c8c507268..cb2928e8c4 100644 --- a/auth.rst +++ b/auth.rst @@ -95,7 +95,7 @@ SQL code can access claims through GUC variables set by PostgREST per request. F .. code:: sql - current_setting('request.jwt.claim.email', true) + current_setting('request.jwt.claims', true)::json->>'email'; 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. diff --git a/tutorials/tut1.rst b/tutorials/tut1.rst index fa89591f34..ec288fc972 100644 --- a/tutorials/tut1.rst +++ b/tutorials/tut1.rst @@ -212,7 +212,7 @@ First make a new schema and add the function: language plpgsql as $$ begin - if current_setting('request.jwt.claim.email', true) = + if current_setting('request.jwt.claims', true)::json->>'email' = 'disgruntled@mycompany.com' then raise insufficient_privilege using hint = 'Nope, we are on to you'; From 1b24eb34ee6aaef5151c2dc8a84bc4d4a7bd2e93 Mon Sep 17 00:00:00 2001 From: Devin Stein Date: Fri, 10 Dec 2021 15:08:51 -0800 Subject: [PATCH 456/549] Add missing = in log-level docs --- configuration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration.rst b/configuration.rst index 2826e2d5d9..3b76ea7655 100644 --- a/configuration.rst +++ b/configuration.rst @@ -272,7 +272,7 @@ log-level log-level = "warn" # All the "warn" level events plus all requests (every status code) are logged - log-level "info" + log-level = "info" Because currently there's no buffering for logging, the levels with minimal logging(``crit/error``) will increase throughput. From 66e49f0ea20d645829fb4f7d4ac31fd625910a75 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 23 Dec 2021 16:54:08 -0500 Subject: [PATCH 457/549] Update config.py version/release --- conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf.py b/conf.py index be38720aa5..f0390613d5 100644 --- a/conf.py +++ b/conf.py @@ -57,9 +57,9 @@ # built documents. # # The short X.Y version. -version = u'8.0' +version = u'9.0' # The full version, including alpha/beta/rc tags. -release = u'8.0.0' +release = u'9.0.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 04e7020ae3c4be128a89f605d116978c09110836 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Fri, 24 Dec 2021 12:06:34 +0100 Subject: [PATCH 458/549] fix database-configuration ALTER ROLE example, resolves #481 --- configuration.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration.rst b/configuration.rst index 3b76ea7655..4bccf0d8c0 100644 --- a/configuration.rst +++ b/configuration.rst @@ -440,8 +440,8 @@ For example, you can configure :ref:`db-schema` and :ref:`jwt-secret` like this: .. code:: postgresql - ALTER ROLE authenticator SET pgrst.db_schema = "tenant1, tenant2, tenant3" - ALTER ROLE authenticator SET pgrst.jwt_secret = "REALLYREALLYREALLYREALLYVERYSAFE" + ALTER ROLE authenticator IN DATABASE SET pgrst.db_schema = "tenant1, tenant2, tenant3" + ALTER ROLE authenticator IN DATABASE SET pgrst.jwt_secret = "REALLYREALLYREALLYREALLYVERYSAFE" Note that underscores(``_``) need to be used instead of dashes(``-``) for the in-database config options. From cfde95b0aad909d03183749d80e3a78206412dd6 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sat, 1 Jan 2022 15:13:57 +0100 Subject: [PATCH 459/549] remove reference to heroku app.json in main repo --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 0a454835de..6c410455e4 100644 --- a/install.rst +++ b/install.rst @@ -259,7 +259,7 @@ Assuming you're making modifications locally and then pushing to GitHub, it's ea 1. Create a new app on Heroku 2. In Settings add the following buildpack :code:`https://github.com/PostgREST/postgrest-heroku` -3. Add the require Config Vars in Heroku (see https://github.com/PostgREST/postgrest/blob/main/app.json for more details) +3. Add the require Config Vars in Heroku 4. Modify your ``postgrest.conf`` file as required to match your Config Vars in Heroku 5. Create your :code:`Procfile` and add :code:`./env-to-config ./postgrest postgrest.conf` 6. Push your changes to GitHub From 01874108822aff4501f8f233d00fbc59e6d9f338 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Fri, 7 Jan 2022 20:30:25 +0100 Subject: [PATCH 460/549] remove dead link to v5 docs --- releases/v5.2.0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releases/v5.2.0.rst b/releases/v5.2.0.rst index 7cc90611b0..d32044526e 100644 --- a/releases/v5.2.0.rst +++ b/releases/v5.2.0.rst @@ -1,7 +1,7 @@ v5.2.0 ====== -* `Explicit qualification `_ introduced in ``v5.0`` is no longer necessary, this section will not be included from this version onwards. A :ref:`db-extra-search-path` configuration parameter was introduced to avoid the need to explictly qualify database objects. If you install PostgreSQL extensions on the ``public`` schema, they'll work normally from now on. +* Explicit qualification introduced in ``v5.0`` is no longer necessary, this section will not be included from this version onwards. A :ref:`db-extra-search-path` configuration parameter was introduced to avoid the need to explictly qualify database objects. If you install PostgreSQL extensions on the ``public`` schema, they'll work normally from now on. * Now you can filter :ref:`tabs-cols-w-spaces`. From 6c382c598511ba444506d4033ec03193898d6524 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Fri, 7 Jan 2022 21:40:32 +0100 Subject: [PATCH 461/549] replace permanently redirected URIs --- auth.rst | 4 ++-- ecosystem.rst | 16 ++++++++-------- index.rst | 6 +++--- releases/v8.0.0.rst | 2 +- releases/v9.0.0.rst | 4 ++-- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/auth.rst b/auth.rst index cb2928e8c4..9df91c78fa 100644 --- a/auth.rst +++ b/auth.rst @@ -215,11 +215,11 @@ JWT from Auth0 An external service like `Auth0 `_ 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 `_ for your app and `an API `_ 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 `_ by setting it with the `audience parameter `_ 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. +To use Auth0, create `an application `_ for your app and `an API `_ 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 `_ by setting it with the `audience parameter `_ 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 `_. Then, you will need to write `a rule `_ that will extract the role from the user's app_metadata and set it as a `custom claim `_ in the access token. Note that, you may use Auth0's `core authorization feature `_ for more complex use cases. Metadata solution is mentioned here for simplicity. + Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata `_. Then, you will need to write `a rule `_ that will extract the role from the user's app_metadata and set it as a `custom claim `_ in the access token. Note that, you may use Auth0's `core authorization feature `_ for more complex use cases. Metadata solution is mentioned here for simplicity. .. code:: javascript diff --git a/ecosystem.rst b/ecosystem.rst index 6d4aa68c1b..883db8bc04 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -105,20 +105,20 @@ Client-Side Libraries * `mithril-postgrest `_ - JS, Mithril * `ng-postgrest `_ - Angular app for browsing, editing data exposed over PostgREST. * `postgrest-client `_ - JS -* `postgrest-csharp `_ - C# -* `postgrest-dart `_ - Dart +* `postgrest-csharp `_ - C# +* `postgrest-dart `_ - Dart * `postgrest-ex `_ - Elixir -* `postgrest-go `_ - Go +* `postgrest-go `_ - Go * `postgrest-js `_ - TypeScript/JavaScript -* `postgrest-kt `_ - Kotlin -* `postgrest-py `_ - Python +* `postgrest-kt `_ - Kotlin +* `postgrest-py `_ - Python * `postgrest-request `_ - JS, SuperAgent -* `postgrest-rs `_ - Rust +* `postgrest-rs `_ - Rust * `postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp -* `postgrest-swift `_ - Swift +* `postgrest-swift `_ - Swift * `postgrest-url `_ - JS, just for generating query URLs * `postgrest_python_requests_client `_ - Python -* `postgrester `_ - JS + Typescript +* `postgrester `_ - JS + Typescript * `postgrestR `_ - R * `py-postgrest `_ - Python * `redux-postgrest `_ - TypeScript/JS, client integrated with (React) Redux. diff --git a/index.rst b/index.rst index aa48aa9c99..3d522585d7 100644 --- a/index.rst +++ b/index.rst @@ -51,7 +51,7 @@ Sponsors :width: 13em .. image:: _static/supabase.png - :target: https://supabase.io/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage + :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/oblivious.jpg @@ -243,7 +243,7 @@ Here are some companies that use PostgREST in production. * `Datrium `_ * `Drip Depot `_ * `Image-charts `_ -* `Moat `_ +* `Moat `_ * `MotionDynamic - Fast highly dynamic video generation at scale `_ * `Netwo `_ * `Nimbus `_ @@ -251,7 +251,7 @@ Here are some companies that use PostgREST in production. * `OpenBooking `_ * `Redsmin `_ * `Sompani `_ -* `Supabase `_ +* `Supabase `_ .. Certs are failing * `eGull `_ diff --git a/releases/v8.0.0.rst b/releases/v8.0.0.rst index a02a20f697..7cdc36c58b 100644 --- a/releases/v8.0.0.rst +++ b/releases/v8.0.0.rst @@ -163,7 +163,7 @@ Big thanks from the `PostgREST team `_ :width: 13em .. image:: ../_static/supabase.png - :target: https://supabase.io/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage + :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/oblivious.jpg diff --git a/releases/v9.0.0.rst b/releases/v9.0.0.rst index 09187b46fe..7843f24030 100644 --- a/releases/v9.0.0.rst +++ b/releases/v9.0.0.rst @@ -63,7 +63,7 @@ Documentation improvements Bug fixes --------- -* Correct RPC return type handling for RETURNS TABLE with a single column (`#1930 `_). +* Correct RPC return type handling for RETURNS TABLE with a single column (`#1930 `_). * Schema Cache query failing with ``standard_conforming_strings = off`` (`#1992 `_). @@ -102,7 +102,7 @@ Big thanks from the `PostgREST team `_ :width: 13em .. image:: ../_static/supabase.png - :target: https://supabase.io/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage + :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/oblivious.jpg From 62726ddcfdd4835745d798ffcf082e2a3dca4364 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Fri, 7 Jan 2022 21:45:48 +0100 Subject: [PATCH 462/549] force colored output in build and linkcheck tools --- default.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/default.nix b/default.nix index 40db658258..651046e369 100644 --- a/default.nix +++ b/default.nix @@ -31,7 +31,7 @@ in # clean previous build, otherwise some errors might be supressed rm -rf _build - ${python}/bin/sphinx-build -W -b html -a -n . _build + ${python}/bin/sphinx-build --color -W -b html -a -n . _build ''; serve = @@ -81,6 +81,6 @@ in '' set -euo pipefail - ${python}/bin/sphinx-build -b linkcheck . _build + ${python}/bin/sphinx-build --color -b linkcheck . _build ''; } From 540ef4ad9f5f5242c86ddafa4b5f7e837636a9be Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Fri, 7 Jan 2022 20:17:18 +0100 Subject: [PATCH 463/549] Update configuration page --- admin.rst | 2 +- api.rst | 14 +- auth.rst | 6 +- configuration.rst | 579 +++++++++++------- .../embedding-table-from-another-schema.rst | 2 +- postgrest.dict | 1 + releases/v7.0.0.rst | 2 +- tutorials/tut0.rst | 2 +- tutorials/tut1.rst | 2 +- 9 files changed, 377 insertions(+), 233 deletions(-) diff --git a/admin.rst b/admin.rst index 054a31a393..6f906363f0 100644 --- a/admin.rst +++ b/admin.rst @@ -266,7 +266,7 @@ First, create postgrest configuration in ``/etc/postgrest/config`` .. code-block:: ini db-uri = "postgres://:@localhost:5432/" - db-schema = "" + db-schemas = "" db-anon-role = "" db-pool = 10 diff --git a/api.rst b/api.rst index f5d5f796d6..2e39e01786 100644 --- a/api.rst +++ b/api.rst @@ -396,7 +396,7 @@ As mentioned, computed columns do not appear in the output by default. However y .. important:: - Computed columns must be created under the :ref:`exposed schema ` to be used in this way. + Computed columns must be created under the :ref:`exposed schema ` to be used in this way. Unicode support --------------- @@ -666,9 +666,9 @@ In general, when having smaller row-counts, the estimated count should be as clo 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:`max-rows`. +defined by :ref:`db-max-rows`. -Here's an example. Suppose we set ``max-rows=1000`` and ``smalltable`` has 321 rows, then we'll get the exact count: +Here's an example. Suppose we set ``db-max-rows=1000`` and ``smalltable`` has 321 rows, then we'll get the exact count: .. tabs:: @@ -1156,7 +1156,7 @@ It's also possible to embed `Materialized Views ` using environment variables. They 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.*``. + +.. _config_reloading: + +Configuration Reloading +======================= + +To reload the configuration without restarting the PostgREST server, send a SIGUSR2 signal to the server process. + +.. code:: bash + + killall -SIGUSR2 postgrest + +This method does not reload :ref:`env_variables_config` and it will not work for reloading a Docker container configuration. In these cases, you need to restart the PostgREST server or use the :ref:`in_db_config` as an alternative. + +.. important:: + + The following settings will not be reread when reloading the configuration. You will need to restart PostgREST in that case. + + * :ref:`db-uri` + * :ref:`db-pool` + * :ref:`db-pool-timeout` + * :ref:`server-host` + * :ref:`server-port` + * :ref:`server-unix-socket` + * :ref:`server-unix-socket-mode` + +.. _in_db_config: + +In-Database Configuration +========================= + +By adding settings to the **authenticator** role (see :ref:`roles`), you can make the database the single source of truth for PostgREST's configuration. +This is enabled by :ref:`db-config`. + +For example, you can configure :ref:`db-schemas` and :ref:`jwt-secret` like this: + +.. code:: postgresql + + ALTER ROLE authenticator IN DATABASE SET pgrst.db_schemas = "tenant1, tenant2, tenant3" + ALTER ROLE authenticator IN DATABASE SET pgrst.jwt_secret = "REALLYREALLYREALLYREALLYVERYSAFE" + +Note that underscores(``_``) need to be used instead of dashes(``-``) for the in-database config parameters. + +.. important:: + + For altering a role in this way, you need a SUPERUSER. You might not be able to use this configuration mode on cloud-hosted databases. + +When using both the configuration file and the in-database configuration, the latter takes precedence. + +.. danger:: + + If direct connections to the database are allowed, then it's not safe to use the in-db configuration for storing the :ref:`jwt-secret`. + The settings of every role are PUBLIC - they can be viewed by any user that queries the ``pg_catalog.pg_db_role_setting`` table. + In this case you should keep the :ref:`jwt-secret` in the configuration file or as environment variables. + +.. _in_db_config_reloading: + +In-database configuration reloading +----------------------------------- + +To reload the in-database 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. For configuring the channel, see :ref:`db-channel` and :ref:`db-channel-enabled`. + +.. _config_full_list: + +List of parameters +================== + +======================== ======= ================= ======== ========== +Name Type Default Required Reloadable +======================== ======= ================= ======== ========== +app.settings.* String Y +db-anon-role String Y Y +db-channel String pgrst Y +db-channel-enabled Boolean True Y +db-config Boolean True Y +db-extra-search-path String public Y +db-max-rows Int ∞ Y db-pool Int 10 db-pool-timeout Int 10 -db-extra-search-path String public -db-channel String pgrst -db-channel-enabled Boolean True -db-prepared-statements Boolean True -db-tx-end String commit -db-config Boolean True -db-use-legacy-gucs Boolean True +db-pre-request String Y +db-prepared-statements Boolean True Y +db-schemas String Y Y +db-tx-end String commit Y +db-uri String Y +db-use-legacy-gucs Boolean True Y +jwt-aud String Y +jwt-role-claim-key String .role Y +jwt-secret String Y +jwt-secret-is-base64 Boolean False Y +log-level String error Y +openapi-mode String follow-privileges Y +openapi-server-proxy-uri String Y +raw-media-types String Y server-host String !4 server-port Int 3000 server-unix-socket String server-unix-socket-mode String 660 -log-level String error -openapi-mode String follow-privileges -openapi-server-proxy-uri String -jwt-secret String -jwt-aud String -secret-is-base64 Boolean False -max-rows Int ∞ -pre-request String -app.settings.* String -role-claim-key String .role -raw-media-types String -======================== ======= ================= ======== +======================== ======= ================= ======== ========== -.. _db-uri: +.. _app.settings.*: -db-uri ------- +app.settings.* +-------------- - The standard connection PostgreSQL `URI 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 `_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``. + =============== ==================== + **Environment** PGRST_APP_SETTINGS_* + **In-Database** pgrst.app_settings_* + =============== ==================== - When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ and the `Peer Authentication method `_ 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 `_ documentation for more details. + 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')`. - On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. +.. _db-anon-role: - 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. +db-anon-role +------------ + =============== ================== + **Environment** PGRST_DB_ANON_ROLE + **In-Database** `n/a` + =============== ================== -.. _db-schema: + The database role to use when executing commands on behalf of unauthenticated clients. For more information, see :ref:`roles`. -db-schema ---------- +.. _db-channel: - The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. +db-channel +---------- - .. code:: bash + =============== ================ + **Environment** PGRST_DB_CHANNEL + **In-Database** `n/a` + =============== ================ - db-schema = "api" + The name of the notification channel that PostgREST uses for :ref:`schema_reloading` and configuration reloading. - This schema gets added to the `search_path `_ of every request. +.. _db-channel-enabled: -List of schemas -~~~~~~~~~~~~~~~ +db-channel-enabled +------------------ - You can also specify a list of schemas that can be used for **schema-based multitenancy** and **api versioning** by :ref:`multiple-schemas`. Example: + =============== ======================== + **Environment** PGRST_DB_CHANNEL_ENABLED + **In-Database** `n/a` + =============== ======================== - .. code:: bash + When this is set to :code:`true`, the notification channel specified in :ref:`db-channel` is enabled. - db-schema = "tenant1, tenant2" + 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 ` for more information. - If you don't :ref:`Switch Schemas `, the first schema in the list(``tenant1`` in this case) is chosen as the default schema. +.. _db-config: - *Only the chosen schema* gets added to the `search_path `_ of every request. +db-config +--------- - .. warning:: + =============== =============== + **Environment** PGRST_DB_CONFIG + **In-Database** `n/a` + =============== =============== - Never expose private schemas in this way. See :ref:`schema_isolation`. + Enables the in-database configuration. -.. _db-anon-role: +.. _db-extra-search-path: -db-anon-role ------------- +db-extra-search-path +-------------------- - The database role to use when executing commands on behalf of unauthenticated clients. For more information, see :ref:`roles`. + =============== ========================== + **Environment** PGRST_DB_EXTRA_SEARCH_PATH + **In-Database** pgrst.db_extra_search_path + =============== ========================== + + Extra schemas to add to the `search_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 +----------- + + *For backwards compatibility, this config parameter is also available without prefix as "max-rows".* + + =============== ================= + **Environment** PGRST_DB_MAX_ROWS + **In-Database** pgrst.db_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-pool: db-pool ------- + =============== ================= + **Environment** PGRST_DB_POOL + **In-Database** `n/a` + =============== ================= + Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the :code:`max_connections` GUC in your database. .. _db-pool-timeout: @@ -131,52 +252,91 @@ db-pool db-pool-timeout --------------- + =============== ================= + **Environment** PGRST_DB_POOL_TIMEOUT + **In-Database** `n/a` + =============== ================= + Time to live, in seconds, for an idle database pool connection. If the timeout is reached the connection will be closed. Once a new request arrives a new connection will be started. -.. _db-extra-search-path: +.. _db-pre-request: -db-extra-search-path --------------------- +db-pre-request +-------------- - Extra schemas to add to the `search_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-schema`. + *For backwards compatibility, this config parameter is also available without prefix as "pre-request".* - This parameter was meant to make it easier to use **PostgreSQL extensions** (like PostGIS) that are outside of the :ref:`db-schema`. + =============== ================= + **Environment** PGRST_DB_PRE_REQUEST + **In-Database** pgrst.db_pre_request + =============== ================= - Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``. + A schema-qualified stored procedure name to call right after switching roles for a client request. This provides an opportunity to modify SQL variables or raise an exception to prevent the request from completing. -.. _db-channel: +.. _db-prepared-statements: -db-channel +db-prepared-statements +---------------------- + + =============== ================= + **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 ` for more information. + +.. _db-schemas: + +db-schemas ---------- - The name of the notification channel that PostgREST uses for :ref:`schema_reloading` and configuration reloading. + *For backwards compatibility, this config parameter is also available in singular as "db-schema".* -.. _db-channel-enabled: + =============== ================= + **Environment** PGRST_DB_SCHEMAS + **In-Database** pgrst.db_schemas + =============== ================= -db-channel-enabled ------------------- + The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. - When this is set to :code:`true`, the notification channel specified in :ref:`db-channel` is enabled. + .. code:: bash - 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 ` for more information. + db-schemas = "api" -.. _db-prepared-statements: + This schema gets added to the `search_path `_ of every request. -db-prepared-statements ----------------------- +List of schemas +~~~~~~~~~~~~~~~ - Enables or disables prepared statements. + You can also specify a list of schemas that can be used for **schema-based multitenancy** and **api versioning** by :ref:`multiple-schemas`. Example: - 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. + .. code:: bash - 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 ` for more information. + db-schemas = "tenant1, tenant2" + + If you don't :ref:`Switch Schemas `, the first schema in the list(``tenant1`` in this case) is chosen as the default schema. + + *Only the chosen schema* gets added to the `search_path `_ of every request. + + .. warning:: + + Never expose private schemas in this way. See :ref:`schema_isolation`. .. _db-tx-end: db-tx-end --------- + =============== ================= + **Environment** PGRST_DB_TX_END + **In-Database** pgrst.db_tx_end + =============== ================= + Specifies how to terminate the database transactions. .. code:: bash @@ -193,71 +353,108 @@ db-tx-end # The transaction is rolled back unless a "Prefer: tx=commit" header is sent db-tx-end = "rollback-allow-override" -.. _db-config: +.. _db-uri: -db-config ---------- +db-uri +------ - Enables the in-database configuration. + =============== ================= + **Environment** PGRST_DB_URI + **In-Database** `n/a` + =============== ================= + + The standard connection PostgreSQL `URI 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 `_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``. + + When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ and the `Peer Authentication method `_ 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 `_ documentation for more details. + + On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. + + 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. .. _db-use-legacy-gucs: db-use-legacy-gucs ------------------ + =============== ================= + **Environment** PGRST_DB_USE_LEGACY_GUCS + **In-Database** pgrst.db_use_legacy_gucs + =============== ================= + Determine if GUC request settings for headers, cookies and jwt claims use the `legacy names `_ (string with dashes, invalid starting from PostgreSQL v14) with text values instead of the :ref:`new names ` (string without dashes, valid on all PostgreSQL versions) with json values. On PostgreSQL versions 14 and above, this parameter is ignored. -.. _server-host: - -server-host ------------ +.. _jwt-aud: - Where to bind the PostgREST web server. In addition to the usual address options, PostgREST interprets these reserved addresses with special meanings: +jwt-aud +------- - * :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 + =============== ================= + **Environment** PGRST_JWT_AUD + **In-Database** pgrst.jwt_aud + =============== ================= -.. _server-port: + Specifies the `JWT audience claim `_. 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. -server-port ------------ +.. _jwt-role-claim-key: - The TCP port to bind the web server. +jwt-role-claim-key +------------------ -.. _server-unix-socket: + *For backwards compatibility, this config parameter is also available without prefix as "role-claim-key".* -server-unix-socket ------------------- + =============== ================= + **Environment** PGRST_JWT_ROLE_CLAIM_KEY + **In-Database** pgrst.jwt_role_claim_key + =============== ================= - `Unix domain socket `_ where to bind the PostgREST web server. - If specified, this takes precedence over :ref:`server-port`. Example: + 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 - server-unix-socket = "/tmp/pgrst.sock" + # {"postgrest":{"roles": ["other", "author"]}} + # the DSL accepts characters that are alphanumerical or one of "_$@" as keys + jwt-role-claim-key = ".postgrest.roles[1]" -.. _server-unix-socket-mode: + # {"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" -server-unix-socket-mode ------------------------ +.. _jwt-secret: - `Unix file mode `_ to be set for the socket specified in :ref:`server-unix-socket` - Needs to be a valid octal between 600 and 777. +jwt-secret +---------- - .. code:: bash + =============== ================= + **Environment** PGRST_JWT_SECRET + **In-Database** pgrst.jwt_secret + =============== ================= - server-unix-socket-mode = "660" + The secret or `JSON Web Key (JWK) (or set) `_ 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`. + +.. _jwt-secret-is-base64: + +jwt-secret-is-base64 +-------------------- + + =============== ================= + **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. .. _log-level: log-level --------- + =============== ================= + **Environment** PGRST_LOG_LEVEL + **In-Database** `n/a` + =============== ================= + Specifies the level of information to be logged while running PostgREST. .. code:: bash @@ -282,6 +479,11 @@ log-level openapi-mode ------------ + =============== ================= + **Environment** PGRST_OPENAPI_MODE + **In-Database** pgrst.openapi_mode + =============== ================= + Specifies how the OpenAPI output should be displayed. .. code:: bash @@ -303,6 +505,11 @@ openapi-mode openapi-server-proxy-uri ------------------------ + =============== ================= + **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 @@ -321,70 +528,16 @@ openapi-server-proxy-uri ] } -.. _jwt-secret: - -jwt-secret ----------- - - The secret or `JSON Web Key (JWK) (or set) `_ 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`. - -.. _jwt-aud: - -jwt-aud -------- - - Specifies the `JWT audience claim `_. 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. - -.. _secret-is-base64: - -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. - -.. _max-rows: - -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. - -.. _pre-request: - -pre-request ------------ - - A schema-qualified stored procedure name to call right after switching roles for a client request. This provides an opportunity to modify SQL variables or raise an exception to prevent the request from completing. - -.. _app.settings.*: - -app.settings.* --------------- - - 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')`. - -.. _role-claim-key: - -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 - 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) - role-claim-key = ".\"https://www.example.com/role\".key" - .. _raw-media-types: raw-media-types --------------- + =============== ================= + **Environment** PGRST_RAW_MEDIA_TYPES + **In-Database** pgrst.raw_media_types + =============== ================= + This serves to extend the `Media Types `_ that PostgREST currently accepts through an ``Accept`` header. These media types can be requested by following the same rules as the ones defined in :ref:`binary_output`. @@ -396,76 +549,66 @@ raw-media-types raw-media-types="image/png, text/xml" -.. _env_variables_config: - -Environment Variables -===================== - -You can also set these :ref:`configuration parameters ` using environment variables. They 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.*``. - -.. _config_reloading: - -Configuration Reloading -======================= - -To reload the configuration without restarting the PostgREST server, send a SIGUSR2 signal to the server process. - -.. code:: bash - - killall -SIGUSR2 postgrest - -This method does not reload :ref:`env_variables_config` and it will not work for reloading a Docker container configuration. In these cases, you need to restart the PostgREST server or use the :ref:`in_db_config` as an alternative. +.. _server-host: -.. important:: +server-host +----------- - The following settings will not be reread when reloading the configuration. You will need to restart PostgREST in that case. + =============== ================= + **Environment** PGRST_SERVER_HOST + **In-Database** pgrst.server_host + =============== ================= - * :ref:`db-uri` - * :ref:`db-pool` - * :ref:`db-pool-timeout` - * :ref:`server-host` - * :ref:`server-port` - * :ref:`server-unix-socket` - * :ref:`server-unix-socket-mode` - -.. _in_db_config: + Where to bind the PostgREST web server. In addition to the usual address options, PostgREST interprets these reserved addresses with special meanings: -In-Database Configuration -========================= + * :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 -By adding settings to the **authenticator** role (see :ref:`roles`), you can make the database the single source of truth for PostgREST's configuration. -This is enabled by :ref:`db-config`. +.. _server-port: -For example, you can configure :ref:`db-schema` and :ref:`jwt-secret` like this: +server-port +----------- -.. code:: postgresql + =============== ================= + **Environment** PGRST_SERVER_PORT + **In-Database** pgrst.server_port + =============== ================= - ALTER ROLE authenticator IN DATABASE SET pgrst.db_schema = "tenant1, tenant2, tenant3" - ALTER ROLE authenticator IN DATABASE SET pgrst.jwt_secret = "REALLYREALLYREALLYREALLYVERYSAFE" + The TCP port to bind the web server. -Note that underscores(``_``) need to be used instead of dashes(``-``) for the in-database config options. +.. _server-unix-socket: -.. important:: +server-unix-socket +------------------ - For altering a role in this way, you need a SUPERUSER. You might not be able to use this configuration mode on cloud-hosted databases. + =============== ================= + **Environment** PGRST_SERVER_UNIX_SOCKET + **In-Database** pgrst.server_unix_socket + =============== ================= -When using both the configuration file and the in-database configuration, the latter takes precedence. + `Unix domain socket `_ where to bind the PostgREST web server. + If specified, this takes precedence over :ref:`server-port`. Example: -.. danger:: + .. code:: bash - If direct connections to the database are allowed, then it's not safe to use the in-db configuration for storing the :ref:`jwt-secret`. - The settings of every role are PUBLIC - they can be viewed by any user that queries the ``pg_catalog.pg_db_role_setting`` table. - In this case you should keep the :ref:`jwt-secret` in the configuration file or as environment variables. + server-unix-socket = "/tmp/pgrst.sock" -.. _in_db_config_reloading: +.. _server-unix-socket-mode: -In-database configuration reloading ------------------------------------ +server-unix-socket-mode +----------------------- -To reload the in-database configuration from within the database, you can use a NOTIFY command. + =============== ================= + **Environment** PGRST_SERVER_UNIX_SOCKET_MODE + **In-Database** pgrst.server_unix_socket_mode + =============== ================= -.. code:: postgresql + `Unix file mode `_ to be set for the socket specified in :ref:`server-unix-socket` + Needs to be a valid octal between 600 and 777. - NOTIFY pgrst, 'reload config' + .. code:: bash -The ``"pgrst"`` notification channel is enabled by default. For configuring the channel, see :ref:`db-channel` and :ref:`db-channel-enabled`. + server-unix-socket-mode = "660" diff --git a/how-tos/embedding-table-from-another-schema.rst b/how-tos/embedding-table-from-another-schema.rst index 118d630d17..6631982de6 100644 --- a/how-tos/embedding-table-from-another-schema.rst +++ b/how-tos/embedding-table-from-another-schema.rst @@ -3,7 +3,7 @@ Embedding a table from another schema :author: `steve-chavez `_ -Suppose you have a **people** table in the ``public`` schema and this schema is exposed through PostgREST's :ref:`db-schema`. +Suppose you have a **people** table in the ``public`` schema and this schema is exposed through PostgREST's :ref:`db-schemas`. .. code-block:: postgres diff --git a/postgrest.dict b/postgrest.dict index fba460fd99..16a91dd584 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -125,6 +125,7 @@ Rechkemmer reconnection Redux refactor +Reloadable Remo requester's RESTful diff --git a/releases/v7.0.0.rst b/releases/v7.0.0.rst index 34e5d46db6..3c2c0ef58a 100644 --- a/releases/v7.0.0.rst +++ b/releases/v7.0.0.rst @@ -10,7 +10,7 @@ You can download this release at the `PostgREST v7.0.0 release page ` defined in :ref:`db-schema`. +* Support for :ref:`Switching to a schema ` defined in :ref:`db-schemas`. |br| -- `@steve-chavez `_, `@mahmoudkassem `_ * Support for :ref:`planned_count` and :ref:`estimated_count`. diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index 04e6420a30..27e9cb44df 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -162,7 +162,7 @@ PostgREST uses a configuration file to tell it how to connect to the database. C .. code-block:: ini db-uri = "postgres://authenticator:mysecretpassword@localhost:5433/postgres" - db-schema = "api" + db-schemas = "api" db-anon-role = "web_anon" The configuration file has other :ref:`options `, but this is all we need. diff --git a/tutorials/tut1.rst b/tutorials/tut1.rst index ec288fc972..cb307dc4b1 100644 --- a/tutorials/tut1.rst +++ b/tutorials/tut1.rst @@ -226,7 +226,7 @@ Next update :code:`tutorial.conf` and specify the new function: # add this line to tutorial.conf - pre-request = "auth.check_token" + 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. From 4bd090ba13ade70b9f87714dad3d2031f72082ff Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Fri, 7 Jan 2022 18:53:25 +0100 Subject: [PATCH 464/549] add user to logging example --- admin.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/admin.rst b/admin.rst index 6f906363f0..20404d21d4 100644 --- a/admin.rst +++ b/admin.rst @@ -158,12 +158,12 @@ When debugging a problem it's important to verify the PostgREST version. At any Logging ------- -PostgREST logs basic request information to ``stdout``, including the requesting IP address and user agent, the URL requested, and HTTP response status. +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 - - [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 - "" "curl/7.64.0" - 127.0.0.1 - - [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 - "" "curl/7.64.0" + 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``. From 737ad76dfdc1188bd463bbcefb280318f2d4bcf2 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sat, 8 Jan 2022 11:35:51 +0100 Subject: [PATCH 465/549] Clarify in-database configuration example regarding IN DATABASE --- configuration.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/configuration.rst b/configuration.rst index 1215d9a3d8..50f8ce7ee5 100644 --- a/configuration.rst +++ b/configuration.rst @@ -76,9 +76,11 @@ For example, you can configure :ref:`db-schemas` and :ref:`jwt-secret` like this .. code:: postgresql - ALTER ROLE authenticator IN DATABASE SET pgrst.db_schemas = "tenant1, tenant2, tenant3" + ALTER ROLE authenticator SET pgrst.db_schemas = "tenant1, tenant2, tenant3" ALTER ROLE authenticator IN DATABASE SET pgrst.jwt_secret = "REALLYREALLYREALLYREALLYVERYSAFE" +You can use both database-specific settings with `IN DATABASE` and cluster-wide settings without it. Database-specific settings will override cluster-wide settings if both are used for the same parameter. + Note that underscores(``_``) need to be used instead of dashes(``-``) for the in-database config parameters. .. important:: From 9d23cfde9dceb906e736c55bc77a1f4f9b2eb7e1 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Wed, 12 Jan 2022 12:52:34 -0500 Subject: [PATCH 466/549] Add finer-grained event trigger (#489) --- schema_cache.rst | 70 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/schema_cache.rst b/schema_cache.rst index 1e708876df..11d0f95105 100644 --- a/schema_cache.rst +++ b/schema_cache.rst @@ -160,4 +160,72 @@ To disable auto reloading, drop the trigger: .. code-block:: postgresql - DROP EVENT TRIGGER pgrst_watch \ No newline at end of file + DROP EVENT TRIGGER pgrst_watch + +Finer-Grained Event Trigger +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can refine the previous event trigger and only react to the events relevant to the schema cache. This also prevents unnecessary +reloading when creating temporary tables(``CREATE TEMP TABLE``) 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' + , '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(); From 5187eadd0bcbbee91d054fa7e2f29794a0521099 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 23 Jan 2022 11:04:35 +0100 Subject: [PATCH 467/549] Restructure configuration reloading section --- configuration.rst | 65 +++++++++++++++++++++++++-------------------- releases/v8.0.0.rst | 2 +- 2 files changed, 37 insertions(+), 30 deletions(-) diff --git a/configuration.rst b/configuration.rst index 50f8ce7ee5..ff6784c265 100644 --- a/configuration.rst +++ b/configuration.rst @@ -39,31 +39,6 @@ Environment Variables You can also set these :ref:`configuration parameters ` using environment variables. They 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.*``. -.. _config_reloading: - -Configuration Reloading -======================= - -To reload the configuration without restarting the PostgREST server, send a SIGUSR2 signal to the server process. - -.. code:: bash - - killall -SIGUSR2 postgrest - -This method does not reload :ref:`env_variables_config` and it will not work for reloading a Docker container configuration. In these cases, you need to restart the PostgREST server or use the :ref:`in_db_config` as an alternative. - -.. important:: - - The following settings will not be reread when reloading the configuration. You will need to restart PostgREST in that case. - - * :ref:`db-uri` - * :ref:`db-pool` - * :ref:`db-pool-timeout` - * :ref:`server-host` - * :ref:`server-port` - * :ref:`server-unix-socket` - * :ref:`server-unix-socket-mode` - .. _in_db_config: In-Database Configuration @@ -95,12 +70,44 @@ When using both the configuration file and the in-database configuration, the la The settings of every role are PUBLIC - they can be viewed by any user that queries the ``pg_catalog.pg_db_role_setting`` table. In this case you should keep the :ref:`jwt-secret` in the configuration file or as environment variables. -.. _in_db_config_reloading: +.. _config_reloading: -In-database configuration reloading ------------------------------------ +Configuration Reloading +======================= + +It's possible to reload PostgREST's configuration without restarting the server. You can do this :ref:`via signal ` or :ref:`via notification `. + +It's not possible to change :ref:`env_variables_config` for a running process and reloading a Docker container configuration will not work. In these cases, you need to restart the PostgREST server or use :ref:`in_db_config` as an alternative. + +.. important:: + + The following settings will not be reloaded. You will need to restart PostgREST to change those. + + * :ref:`db-uri` + * :ref:`db-pool` + * :ref:`db-pool-timeout` + * :ref:`server-host` + * :ref:`server-port` + * :ref:`server-unix-socket` + * :ref:`server-unix-socket-mode` + +.. _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 in-database configuration from within the database, you can use a NOTIFY command. +To reload the configuration from within the database, you can use a NOTIFY command. .. code:: postgresql diff --git a/releases/v8.0.0.rst b/releases/v8.0.0.rst index 7cdc36c58b..9df17821ef 100644 --- a/releases/v8.0.0.rst +++ b/releases/v8.0.0.rst @@ -37,7 +37,7 @@ Added * Allow ``Bearer`` with and without capitalization as authentication schema. See :ref:`client_auth`. |br| -- `@wolfgangwalther `_ -* :ref:`in_db_config` that can be :ref:`reloaded with NOTIFY `. +* :ref:`in_db_config` that can be :ref:`reloaded with NOTIFY `. |br| -- `@steve-chavez `_ * Allow OPTIONS to generate HTTP methods based on views triggers. See :ref:`OPTIONS requests `. From 5f938dbe1a05df6ee978e236bc9e63a043ea6d1f Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sat, 22 Jan 2022 15:54:13 +0100 Subject: [PATCH 468/549] Run PostgREST with zero config. Related to https://github.com/PostgREST/postgrest/pull/2112 --- admin.rst | 5 --- auth.rst | 2 +- configuration.rst | 92 +++++++++++++++++++++++++++------------------- tutorials/tut0.rst | 4 +- 4 files changed, 58 insertions(+), 45 deletions(-) diff --git a/admin.rst b/admin.rst index 20404d21d4..c319c29c47 100644 --- a/admin.rst +++ b/admin.rst @@ -268,11 +268,6 @@ First, create postgrest configuration in ``/etc/postgrest/config`` db-uri = "postgres://:@localhost:5432/" db-schemas = "" db-anon-role = "" - db-pool = 10 - - server-host = "127.0.0.1" - server-port = 3000 - jwt-secret = "" Then create the systemd service file in ``/etc/systemd/system/postgrest.service`` diff --git a/auth.rst b/auth.rst index 892f73f774..867339cbc9 100644 --- a/auth.rst +++ b/auth.rst @@ -63,7 +63,7 @@ You can use row-level security to flexibly restrict visibility and access for th ALTER TABLE chat ENABLE ROW LEVEL SECURITY; -We want to enforce a policy that ensures a user can see only those messages sent by him or intended for him. Also we want to prevent a user from forging the message_from column with another person's name. +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: diff --git a/configuration.rst b/configuration.rst index ff6784c265..59501140ce 100644 --- a/configuration.rst +++ b/configuration.rst @@ -3,7 +3,18 @@ Configuration ============= -PostgREST reads a configuration file to determine information about the database and how to serve client requests. There is no predefined location for this file, you must specify the file path as the one and only argument to the server: +Without configuration, PostgREST won't be able to serve requests. At the minimum it needs either :ref:`a role to serve anonymous requests with ` - or :ref:`a secret to use for JWT authentication `. Config parameters can be provided via :ref:`file_config`, via :ref:`env_variables_config` or through :ref:`in_db_config`. + +To connect to a database it uses a `libpq connection string `_. The connection string can be set in the configuration file or via environment variable or can be read from an external file. See :ref:`db-uri` for details. Any parameter that is not set in the connection string is read from `libpq environment variables `_. The default connection string is ``postgresql://``, which reads **all** parameters from the environment. + +The user with whom PostgREST connects to the database is also known as the authenticator role. For more information about the anonymous vs authenticator roles see :ref:`roles`. + +.. _file_config: + +Config File +----------- + +PostgREST can read a config file. There is no predefined location for this file, you must specify the file path as the one and only argument to the server: .. code:: bash @@ -13,7 +24,7 @@ PostgREST reads a configuration file to determine information about the database Configuration can be reloaded without restarting the server. See :ref:`config_reloading`. -The configuration file must contain a set of key value pairs. At minimum you must include these keys: +The configuration file must contain a set of key value pairs: .. code:: @@ -23,26 +34,31 @@ The configuration file must contain a set of key value pairs. At minimum you mus # https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING db-uri = "postgres://user:pass@host:5432/dbname" - # The name of which database schema to expose to REST clients - db-schemas = "api" - # The database role to use when no client authentication is provided. - # Can (and should) differ from user in db-uri + # Should differ from authenticator db-anon-role = "anon" -The user specified in the db-uri is also known as the authenticator role. For more information about the anonymous vs authenticator roles see the :ref:`roles`. + # 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 = 80 + +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 -===================== +--------------------- You can also set these :ref:`configuration parameters ` using environment variables. They 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.*``. .. _in_db_config: In-Database Configuration -========================= +------------------------- By adding settings to the **authenticator** role (see :ref:`roles`), you can make the database the single source of truth for PostgREST's configuration. This is enabled by :ref:`db-config`. @@ -120,37 +136,37 @@ The ``"pgrst"`` notification channel is enabled by default. For configuring the List of parameters ================== -======================== ======= ================= ======== ========== -Name Type Default Required Reloadable -======================== ======= ================= ======== ========== -app.settings.* String Y -db-anon-role String Y Y -db-channel String pgrst Y -db-channel-enabled Boolean True Y -db-config Boolean True Y -db-extra-search-path String public Y -db-max-rows Int ∞ Y +======================== ======= ================= ========== +Name Type Default Reloadable +======================== ======= ================= ========== +app.settings.* String Y +db-anon-role String Y +db-channel String pgrst Y +db-channel-enabled Boolean True Y +db-config Boolean True Y +db-extra-search-path String public Y +db-max-rows Int ∞ Y db-pool Int 10 db-pool-timeout Int 10 -db-pre-request String Y -db-prepared-statements Boolean True Y -db-schemas String Y Y -db-tx-end String commit Y -db-uri String Y -db-use-legacy-gucs Boolean True Y -jwt-aud String Y -jwt-role-claim-key String .role Y -jwt-secret String Y -jwt-secret-is-base64 Boolean False Y -log-level String error Y -openapi-mode String follow-privileges Y -openapi-server-proxy-uri String Y -raw-media-types String Y +db-pre-request String Y +db-prepared-statements Boolean True Y +db-schemas String public Y +db-tx-end String commit +db-uri String postgresql:// +db-use-legacy-gucs Boolean True Y +jwt-aud String Y +jwt-role-claim-key String .role Y +jwt-secret String Y +jwt-secret-is-base64 Boolean False Y +log-level String error Y +openapi-mode String follow-privileges Y +openapi-server-proxy-uri String Y +raw-media-types String Y server-host String !4 server-port Int 3000 server-unix-socket String server-unix-socket-mode String 660 -======================== ======= ================= ======== ========== +======================== ======= ================= ========== .. _app.settings.*: @@ -176,6 +192,8 @@ 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 @@ -376,9 +394,7 @@ db-uri When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ and the `Peer Authentication method `_ 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 `_ documentation for more details. - On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. - - 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. + 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. .. _db-use-legacy-gucs: @@ -442,6 +458,8 @@ jwt-secret The secret or `JSON Web Key (JWK) (or set) `_ 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. + .. _jwt-secret-is-base64: jwt-secret-is-base64 diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index 27e9cb44df..d17b32b7c5 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -140,7 +140,7 @@ Next make a role to use for anonymous web requests. When a request comes in, Pos 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 him the ability to switch to the ``web_anon`` role : +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 @@ -157,7 +157,7 @@ Now quit out of psql; it's time to start the API! Step 5. Run PostgREST --------------------- -PostgREST uses a configuration file to tell it how to connect to the database. Create a file :code:`tutorial.conf` with this inside: +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 From 0824139069043ad963d33f2fb901696d7ace24ee Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 23 Jan 2022 12:12:32 +0100 Subject: [PATCH 469/549] Add order of precedence for config parameters --- configuration.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/configuration.rst b/configuration.rst index 59501140ce..422093d36c 100644 --- a/configuration.rst +++ b/configuration.rst @@ -9,6 +9,12 @@ To connect to a database it uses a `libpq connection string Date: Fri, 28 Jan 2022 00:27:19 +0000 Subject: [PATCH 470/549] Improving docs for setting response headers like content-type (#494) --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 2e39e01786..6367d4a3b5 100644 --- a/api.rst +++ b/api.rst @@ -2509,7 +2509,7 @@ Notice that the variable should be set to an *array* of single-key objects rathe .. note:: - PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overriden this way. + 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 also set :ref:`raw-media-types` to something like ``text/html``. .. _pre_req_headers: From 0ae86b7e7490ce986ab47f84c98c5278e09ca83e Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 30 Jan 2022 13:24:29 +0100 Subject: [PATCH 471/549] Fix references to full documents instead of first section Resolves #492 Signed-off-by: Wolfgang Walther --- how-tos/providing-images-for-img.rst | 2 +- releases/v7.0.0.rst | 2 +- releases/v8.0.0.rst | 4 ++-- releases/v9.0.0.rst | 2 +- tutorials/tut0.rst | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/how-tos/providing-images-for-img.rst b/how-tos/providing-images-for-img.rst index 5a1463d5ec..989e197494 100644 --- a/how-tos/providing-images-for-img.rst +++ b/how-tos/providing-images-for-img.rst @@ -38,7 +38,7 @@ We can retrieve this image in binary format from our PostgREST API by requesting Unfortunately, putting the URL into the :code:`src` of an :code:`` tag will not work. That's because browsers do not send the required header. -Luckily, we can configure our :ref:`Nginx reverse proxy ` to fix this problem for us. +Luckily, we can configure our :doc:`Nginx reverse proxy <../admin>` to fix this problem for us. We assume that PostgREST is running on port 3000. We provide a new location :code:`/files/` that redirects requests to our endpoint with the :code:`Accept` header set to :code:`application/octet-stream`. diff --git a/releases/v7.0.0.rst b/releases/v7.0.0.rst index 3c2c0ef58a..241f0e9010 100644 --- a/releases/v7.0.0.rst +++ b/releases/v7.0.0.rst @@ -35,7 +35,7 @@ Added * Documentation improvements - + Explanation for :ref:`Schema Structure `. + + Explanation for :doc:`Schema Structure <../schema_structure>`. + Reference for :ref:`s_proc_embed`. + Reference for :ref:`mutation_embed`. + Reference for filters on :ref:`json_columns`. diff --git a/releases/v8.0.0.rst b/releases/v8.0.0.rst index 9df17821ef..b99bd695f3 100644 --- a/releases/v8.0.0.rst +++ b/releases/v8.0.0.rst @@ -60,8 +60,8 @@ Added * Documentation improvements - + Added the :ref:`schema_cache` page. - + Moved the :ref:`schema_reloading` reference from :ref:`admin` to :ref:`schema_cache` + + Added the :doc:`../schema_cache` page. + + Moved the :ref:`schema_reloading` reference from :doc:`../admin` to :doc:`../schema_cache` Changed ------- diff --git a/releases/v9.0.0.rst b/releases/v9.0.0.rst index 7843f24030..d451bba7eb 100644 --- a/releases/v9.0.0.rst +++ b/releases/v9.0.0.rst @@ -74,7 +74,7 @@ Breaking changes * Dropped support for PostgreSQL 9.5 as it already reached its end-of-life according to `PostgreSQL versioning policy `_. -* Partitions of a `partitioned table `_ are no longer included in the :ref:`schema_cache`. This is so errors are not generated when doing resource embedding on partitioned tables. +* Partitions of a `partitioned table `_ are no longer included in the :doc:`../schema_cache`. This is so errors are not generated when doing resource embedding on partitioned tables. * Dropped support for doing :ref:`hint_disamb` using dots instead of exclamation marks, e.g. doing ``select=*,projects.client_id(*)`` instead of ``select=*,projects!client_id(*)``). Using dots was undocumented and deprecated back in `v6.0.2 `_. diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index d17b32b7c5..a0ffb936c3 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -165,7 +165,7 @@ PostgREST can use a configuration file to tell it how to connect to the database db-schemas = "api" db-anon-role = "web_anon" -The configuration file has other :ref:`options `, but this is all we need. +The configuration file has other :doc:`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: From a85dfe955832ca306df369765694e85c00273faf Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 30 Jan 2022 10:54:36 +0100 Subject: [PATCH 472/549] chore: Move *.rst files to docs/ folder Signed-off-by: Wolfgang Walther --- .readthedocs.yaml | 6 ++++++ default.nix | 8 ++++---- diagrams/README.md | 4 ++-- {_static => docs/_static}/2ndquadrant.png | Bin {_static => docs/_static}/css/custom.css | 0 {_static => docs/_static}/cybertec-new.png | Bin {_static => docs/_static}/cybertec.png | Bin {_static => docs/_static}/db.png | Bin {_static => docs/_static}/empty.png | Bin {_static => docs/_static}/favicon.ico | Bin {_static => docs/_static}/film.png | Bin {_static => docs/_static}/gnuhost.png | Bin {_static => docs/_static}/logo.png | Bin {_static => docs/_static}/oblivious.jpg | Bin {_static => docs/_static}/orders.png | Bin {_static => docs/_static}/retool.png | Bin {_static => docs/_static}/security-anon-choice.png | Bin {_static => docs/_static}/security-roles.png | Bin {_static => docs/_static}/supabase.png | Bin {_static => docs/_static}/timescaledb.png | Bin .../_static}/tuts/tut0-request-flow.png | Bin {_static => docs/_static}/tuts/tut1-jwt-io.png | Bin {_static => docs/_static}/win-err-dialog.png | Bin admin.rst => docs/admin.rst | 0 api.rst => docs/api.rst | 0 auth.rst => docs/auth.rst | 0 conf.py => docs/conf.py | 0 configuration.rst => docs/configuration.rst | 0 ecosystem.rst => docs/ecosystem.rst | 0 .../how-tos}/casting-type-to-custom-json.rst | 0 .../embedding-table-from-another-schema.rst | 0 .../how-tos}/providing-images-for-img.rst | 0 index.rst => docs/index.rst | 0 install.rst => docs/install.rst | 0 {releases => docs/releases}/v5.2.0.rst | 0 {releases => docs/releases}/v6.0.2.rst | 0 {releases => docs/releases}/v7.0.0.rst | 0 {releases => docs/releases}/v7.0.1.rst | 0 {releases => docs/releases}/v8.0.0.rst | 0 {releases => docs/releases}/v9.0.0.rst | 0 schema_cache.rst => docs/schema_cache.rst | 0 schema_structure.rst => docs/schema_structure.rst | 0 {tutorials => docs/tutorials}/tut0.rst | 0 {tutorials => docs/tutorials}/tut1.rst | 0 livereload_docs.py | 10 +++++----- 45 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 .readthedocs.yaml rename {_static => docs/_static}/2ndquadrant.png (100%) rename {_static => docs/_static}/css/custom.css (100%) rename {_static => docs/_static}/cybertec-new.png (100%) rename {_static => docs/_static}/cybertec.png (100%) rename {_static => docs/_static}/db.png (100%) rename {_static => docs/_static}/empty.png (100%) rename {_static => docs/_static}/favicon.ico (100%) rename {_static => docs/_static}/film.png (100%) rename {_static => docs/_static}/gnuhost.png (100%) rename {_static => docs/_static}/logo.png (100%) rename {_static => docs/_static}/oblivious.jpg (100%) rename {_static => docs/_static}/orders.png (100%) rename {_static => docs/_static}/retool.png (100%) rename {_static => docs/_static}/security-anon-choice.png (100%) rename {_static => docs/_static}/security-roles.png (100%) rename {_static => docs/_static}/supabase.png (100%) rename {_static => docs/_static}/timescaledb.png (100%) rename {_static => docs/_static}/tuts/tut0-request-flow.png (100%) rename {_static => docs/_static}/tuts/tut1-jwt-io.png (100%) rename {_static => docs/_static}/win-err-dialog.png (100%) rename admin.rst => docs/admin.rst (100%) rename api.rst => docs/api.rst (100%) rename auth.rst => docs/auth.rst (100%) rename conf.py => docs/conf.py (100%) rename configuration.rst => docs/configuration.rst (100%) rename ecosystem.rst => docs/ecosystem.rst (100%) rename {how-tos => docs/how-tos}/casting-type-to-custom-json.rst (100%) rename {how-tos => docs/how-tos}/embedding-table-from-another-schema.rst (100%) rename {how-tos => docs/how-tos}/providing-images-for-img.rst (100%) rename index.rst => docs/index.rst (100%) rename install.rst => docs/install.rst (100%) rename {releases => docs/releases}/v5.2.0.rst (100%) rename {releases => docs/releases}/v6.0.2.rst (100%) rename {releases => docs/releases}/v7.0.0.rst (100%) rename {releases => docs/releases}/v7.0.1.rst (100%) rename {releases => docs/releases}/v8.0.0.rst (100%) rename {releases => docs/releases}/v9.0.0.rst (100%) rename schema_cache.rst => docs/schema_cache.rst (100%) rename schema_structure.rst => docs/schema_structure.rst (100%) rename {tutorials => docs/tutorials}/tut0.rst (100%) rename {tutorials => docs/tutorials}/tut1.rst (100%) diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000000..3bce02afa5 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,6 @@ +version: 2 +sphinx: + configuration: docs/conf.py +python: + install: + - requirements: requirements.txt diff --git a/default.nix b/default.nix index 651046e369..5dffe887ac 100644 --- a/default.nix +++ b/default.nix @@ -31,7 +31,7 @@ in # clean previous build, otherwise some errors might be supressed rm -rf _build - ${python}/bin/sphinx-build --color -W -b html -a -n . _build + ${python}/bin/sphinx-build --color -W -b html -a -n docs _build ''; serve = @@ -50,7 +50,7 @@ in '' set -euo pipefail - FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ') + FILES=$(find docs -type f -iname '*.rst' | tr '\n' ' ') cat $FILES \ | grep -v '^\(\.\.\| \)' \ @@ -67,7 +67,7 @@ in '' set -euo pipefail - FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ') + FILES=$(find docs -type f -iname '*.rst' | tr '\n' ' ') cat postgrest.dict \ | tail -n+2 \ @@ -81,6 +81,6 @@ in '' set -euo pipefail - ${python}/bin/sphinx-build --color -b linkcheck . _build + ${python}/bin/sphinx-build --color -b linkcheck docs _build ''; } diff --git a/diagrams/README.md b/diagrams/README.md index 36fd9911d0..67009dc31c 100644 --- a/diagrams/README.md +++ b/diagrams/README.md @@ -5,7 +5,7 @@ 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 diagrams/film.er -o _static/film.png +./erd_static-x86-64 -i diagrams/film.er -o docs/_static/film.png ``` ## LaTeX @@ -18,7 +18,7 @@ Then use this command to generate the png file. 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/ +mv db.png ../docs/_static/ ``` LaTeX is used because it's a tweakable plain text format. diff --git a/_static/2ndquadrant.png b/docs/_static/2ndquadrant.png similarity index 100% rename from _static/2ndquadrant.png rename to docs/_static/2ndquadrant.png diff --git a/_static/css/custom.css b/docs/_static/css/custom.css similarity index 100% rename from _static/css/custom.css rename to docs/_static/css/custom.css diff --git a/_static/cybertec-new.png b/docs/_static/cybertec-new.png similarity index 100% rename from _static/cybertec-new.png rename to docs/_static/cybertec-new.png diff --git a/_static/cybertec.png b/docs/_static/cybertec.png similarity index 100% rename from _static/cybertec.png rename to docs/_static/cybertec.png diff --git a/_static/db.png b/docs/_static/db.png similarity index 100% rename from _static/db.png rename to docs/_static/db.png diff --git a/_static/empty.png b/docs/_static/empty.png similarity index 100% rename from _static/empty.png rename to docs/_static/empty.png diff --git a/_static/favicon.ico b/docs/_static/favicon.ico similarity index 100% rename from _static/favicon.ico rename to docs/_static/favicon.ico diff --git a/_static/film.png b/docs/_static/film.png similarity index 100% rename from _static/film.png rename to docs/_static/film.png diff --git a/_static/gnuhost.png b/docs/_static/gnuhost.png similarity index 100% rename from _static/gnuhost.png rename to docs/_static/gnuhost.png diff --git a/_static/logo.png b/docs/_static/logo.png similarity index 100% rename from _static/logo.png rename to docs/_static/logo.png diff --git a/_static/oblivious.jpg b/docs/_static/oblivious.jpg similarity index 100% rename from _static/oblivious.jpg rename to docs/_static/oblivious.jpg diff --git a/_static/orders.png b/docs/_static/orders.png similarity index 100% rename from _static/orders.png rename to docs/_static/orders.png diff --git a/_static/retool.png b/docs/_static/retool.png similarity index 100% rename from _static/retool.png rename to docs/_static/retool.png diff --git a/_static/security-anon-choice.png b/docs/_static/security-anon-choice.png similarity index 100% rename from _static/security-anon-choice.png rename to docs/_static/security-anon-choice.png diff --git a/_static/security-roles.png b/docs/_static/security-roles.png similarity index 100% rename from _static/security-roles.png rename to docs/_static/security-roles.png diff --git a/_static/supabase.png b/docs/_static/supabase.png similarity index 100% rename from _static/supabase.png rename to docs/_static/supabase.png diff --git a/_static/timescaledb.png b/docs/_static/timescaledb.png similarity index 100% rename from _static/timescaledb.png rename to docs/_static/timescaledb.png diff --git a/_static/tuts/tut0-request-flow.png b/docs/_static/tuts/tut0-request-flow.png similarity index 100% rename from _static/tuts/tut0-request-flow.png rename to docs/_static/tuts/tut0-request-flow.png diff --git a/_static/tuts/tut1-jwt-io.png b/docs/_static/tuts/tut1-jwt-io.png similarity index 100% rename from _static/tuts/tut1-jwt-io.png rename to docs/_static/tuts/tut1-jwt-io.png diff --git a/_static/win-err-dialog.png b/docs/_static/win-err-dialog.png similarity index 100% rename from _static/win-err-dialog.png rename to docs/_static/win-err-dialog.png diff --git a/admin.rst b/docs/admin.rst similarity index 100% rename from admin.rst rename to docs/admin.rst diff --git a/api.rst b/docs/api.rst similarity index 100% rename from api.rst rename to docs/api.rst diff --git a/auth.rst b/docs/auth.rst similarity index 100% rename from auth.rst rename to docs/auth.rst diff --git a/conf.py b/docs/conf.py similarity index 100% rename from conf.py rename to docs/conf.py diff --git a/configuration.rst b/docs/configuration.rst similarity index 100% rename from configuration.rst rename to docs/configuration.rst diff --git a/ecosystem.rst b/docs/ecosystem.rst similarity index 100% rename from ecosystem.rst rename to docs/ecosystem.rst diff --git a/how-tos/casting-type-to-custom-json.rst b/docs/how-tos/casting-type-to-custom-json.rst similarity index 100% rename from how-tos/casting-type-to-custom-json.rst rename to docs/how-tos/casting-type-to-custom-json.rst diff --git a/how-tos/embedding-table-from-another-schema.rst b/docs/how-tos/embedding-table-from-another-schema.rst similarity index 100% rename from how-tos/embedding-table-from-another-schema.rst rename to docs/how-tos/embedding-table-from-another-schema.rst diff --git a/how-tos/providing-images-for-img.rst b/docs/how-tos/providing-images-for-img.rst similarity index 100% rename from how-tos/providing-images-for-img.rst rename to docs/how-tos/providing-images-for-img.rst diff --git a/index.rst b/docs/index.rst similarity index 100% rename from index.rst rename to docs/index.rst diff --git a/install.rst b/docs/install.rst similarity index 100% rename from install.rst rename to docs/install.rst diff --git a/releases/v5.2.0.rst b/docs/releases/v5.2.0.rst similarity index 100% rename from releases/v5.2.0.rst rename to docs/releases/v5.2.0.rst diff --git a/releases/v6.0.2.rst b/docs/releases/v6.0.2.rst similarity index 100% rename from releases/v6.0.2.rst rename to docs/releases/v6.0.2.rst diff --git a/releases/v7.0.0.rst b/docs/releases/v7.0.0.rst similarity index 100% rename from releases/v7.0.0.rst rename to docs/releases/v7.0.0.rst diff --git a/releases/v7.0.1.rst b/docs/releases/v7.0.1.rst similarity index 100% rename from releases/v7.0.1.rst rename to docs/releases/v7.0.1.rst diff --git a/releases/v8.0.0.rst b/docs/releases/v8.0.0.rst similarity index 100% rename from releases/v8.0.0.rst rename to docs/releases/v8.0.0.rst diff --git a/releases/v9.0.0.rst b/docs/releases/v9.0.0.rst similarity index 100% rename from releases/v9.0.0.rst rename to docs/releases/v9.0.0.rst diff --git a/schema_cache.rst b/docs/schema_cache.rst similarity index 100% rename from schema_cache.rst rename to docs/schema_cache.rst diff --git a/schema_structure.rst b/docs/schema_structure.rst similarity index 100% rename from schema_structure.rst rename to docs/schema_structure.rst diff --git a/tutorials/tut0.rst b/docs/tutorials/tut0.rst similarity index 100% rename from tutorials/tut0.rst rename to docs/tutorials/tut0.rst diff --git a/tutorials/tut1.rst b/docs/tutorials/tut1.rst similarity index 100% rename from tutorials/tut1.rst rename to docs/tutorials/tut1.rst diff --git a/livereload_docs.py b/livereload_docs.py index 7674d11d38..5d0f5db2eb 100755 --- a/livereload_docs.py +++ b/livereload_docs.py @@ -2,12 +2,12 @@ from livereload import Server, shell from subprocess import call ## Build docs at startup -call(['sphinx-build', '-b', 'html', '-a', '-n', '.', '_build']) +call(['sphinx-build', '-b', 'html', '-a', '-n', 'docs', '_build']) server = Server() -server.watch('*.rst', shell('sphinx-build -b html -a -n . _build')) -server.watch('tutorials/*.rst', shell('sphinx-build -b html -a -n . _build')) -server.watch('how-tos/*.rst', shell('sphinx-build -b html -a -n . _build')) -server.watch('releases/*.rst', shell('sphinx-build -b html -a -n . _build')) +server.watch('*.rst', shell('sphinx-build -b html -a -n docs _build')) +server.watch('tutorials/*.rst', shell('sphinx-build -b html -a -n docs _build')) +server.watch('how-tos/*.rst', shell('sphinx-build -b html -a -n docs _build')) +server.watch('releases/*.rst', shell('sphinx-build -b html -a -n docs _build')) # For custom port and host # server.serve(root='_build/', host='192.168.1.2') server.serve(root='_build/') From a4db05aebab3cca4f2d7264c9ec505cabdc70fa9 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 30 Jan 2022 14:19:07 +0100 Subject: [PATCH 473/549] Improve docker install examples Removes the PGRST_DB_SCHEMA variable which is public by default now. Removes the PGRST_DB_ANON_ROLE variable, because using the authenticator role or even a superuser is very bad practice. Replaces the postgres superuser with app_user, because using a superuser to connect to postgres is very bad practice. Signed-off-by: Wolfgang Walther --- docs/install.rst | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index 6c410455e4..290596b0f2 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -130,9 +130,7 @@ The first way to run PostgREST in Docker is to connect it to an existing native # Run the server docker run --rm --net=host -p 3000:3000 \ - -e PGRST_DB_URI="postgres://postgres@localhost/postgres" \ - -e PGRST_DB_SCHEMA="public" \ - -e PGRST_DB_ANON_ROLE="postgres" \ + -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. @@ -176,8 +174,6 @@ To avoid having to install the database at all, you can run both it and the serv - "3000:3000" environment: PGRST_DB_URI: postgres://app_user:password@db:5432/app_db - PGRST_DB_SCHEMA: public - PGRST_DB_ANON_ROLE: app_user #In production this role should not be the same as the one used for the connection PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000 depends_on: - db From 91f1580f954e99f3cdecfe611c676b035de4e683 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 30 Jan 2022 14:19:58 +0100 Subject: [PATCH 474/549] Fix warning when using docker host network Resolves #491 Signed-off-by: Wolfgang Walther --- docs/install.rst | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/install.rst b/docs/install.rst index 290596b0f2..eebeeb33a3 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -129,7 +129,7 @@ The first way to run PostgREST in Docker is to connect it to an existing native .. code-block:: bash # Run the server - docker run --rm --net=host -p 3000:3000 \ + docker run --rm --net=host \ -e PGRST_DB_URI="postgres://app_user:password@localhost/postgres" \ postgrest/postgrest @@ -155,6 +155,15 @@ The database connection string above is just an example. Adjust the role and pas 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 From e7e102bc4281fc654f98f384c7d0b374b6ba22b2 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 30 Jan 2022 11:09:41 +0100 Subject: [PATCH 475/549] ci: switch to GitHub Actions Signed-off-by: Wolfgang Walther --- .circleci/config.yml | 45 --------------------------------------- .github/dependabot.yml | 6 ++++++ .github/workflows/ci.yaml | 40 ++++++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 45 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yaml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 36c6ba737f..0000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,45 +0,0 @@ -version: 2.1 - -jobs: - build: - docker: - - image: nixos/nix:2.3 - steps: - - checkout - - run: - name: Install build script - command: nix-env -f default.nix -iA build - - run: - name: Build docs - command: postgrest-docs-build - - spellcheck: - docker: - - image: nixos/nix:2.3 - steps: - - checkout - - run: - name: Install spellcheck script - command: nix-env -f default.nix -iA spellcheck - - run: - name: Run spellcheck - command: postgrest-docs-spellcheck - - linkcheck: - docker: - - image: nixos/nix:2.3 - steps: - - checkout - - run: - name: Install linkcheck script - command: nix-env -f default.nix -iA linkcheck - - run: - name: Run linkcheck - command: postgrest-docs-linkcheck - -workflows: - check: - jobs: - - build - - spellcheck - - linkcheck diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..e2347a8c76 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: +- package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000000..4f4c763493 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: + - main + - v* + pull_request: + branches: + - main + - v* + +jobs: + build: + name: Build docs + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2.4.0 + - uses: cachix/install-nix-action@v16 + - run: nix-env -f default.nix -iA build + - run: postgrest-docs-build + + spellcheck: + name: Run spellcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2.4.0 + - uses: cachix/install-nix-action@v16 + - run: nix-env -f default.nix -iA spellcheck + - run: postgrest-docs-spellcheck + + linkcheck: + name: Run linkcheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2.4.0 + - uses: cachix/install-nix-action@v16 + - run: nix-env -f default.nix -iA linkcheck + - run: postgrest-docs-linkcheck + From 43a92d4e6b78f18650fbaf111f9b2c28472b4ae2 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 30 Jan 2022 13:57:32 +0100 Subject: [PATCH 476/549] ci: Only run linkcheck in pull requests to main branch Avoids running linkcheck on back branches, where links are outdated anyway. Signed-off-by: Wolfgang Walther --- .github/workflows/ci.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 4f4c763493..b03dadad10 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,6 +31,7 @@ jobs: linkcheck: name: Run linkcheck + if: github.base_ref == 'main' runs-on: ubuntu-latest steps: - uses: actions/checkout@v2.4.0 From 71dd891d2f8309c6c58a1f6ed88d1f32bac8f203 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Thu, 3 Feb 2022 07:49:49 +0100 Subject: [PATCH 477/549] fix postgrest-serve after move to docs/ folder Signed-off-by: Wolfgang Walther --- livereload_docs.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/livereload_docs.py b/livereload_docs.py index 5d0f5db2eb..8dae165774 100755 --- a/livereload_docs.py +++ b/livereload_docs.py @@ -4,10 +4,7 @@ ## Build docs at startup call(['sphinx-build', '-b', 'html', '-a', '-n', 'docs', '_build']) server = Server() -server.watch('*.rst', shell('sphinx-build -b html -a -n docs _build')) -server.watch('tutorials/*.rst', shell('sphinx-build -b html -a -n docs _build')) -server.watch('how-tos/*.rst', shell('sphinx-build -b html -a -n docs _build')) -server.watch('releases/*.rst', shell('sphinx-build -b html -a -n docs _build')) +server.watch('docs/**/*.rst', shell('sphinx-build -b html -a -n docs _build')) # For custom port and host # server.serve(root='_build/', host='192.168.1.2') server.serve(root='_build/') From 9c0db849ce6f46cd618555c5bd7b2375c724745c Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Thu, 3 Feb 2022 07:50:55 +0100 Subject: [PATCH 478/549] Move JSON Columns and Computed Columns sections one level up Both of those are about select and filters - so do not belong into "Vertical Filtering" only. Signed-off-by: Wolfgang Walther --- docs/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 6367d4a3b5..92dd04fbe6 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -270,7 +270,7 @@ Casting the columns is possible by suffixing them with the double colon ``::`` p .. _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 `_. @@ -351,7 +351,7 @@ Note that ``->>`` is used to compare ``blood_type`` as ``text``. To compare with .. _computed_cols: Computed Columns -~~~~~~~~~~~~~~~~ +---------------- Filters may be applied to computed columns(**a.k.a. virtual columns**) as well as actual table/view columns, even though the computed columns will not appear in the output. For example, to search first and last names at once we can create a computed column that will not appear in the output but can be used in a filter: From 4d6909a08f654628458855776e17b5f1c28037fa Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Thu, 3 Feb 2022 08:05:01 +0100 Subject: [PATCH 479/549] Mention computed columns can be on the extra search path. Signed-off-by: Wolfgang Walther --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 92dd04fbe6..a95f1118c8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -396,7 +396,7 @@ As mentioned, computed columns do not appear in the output by default. However y .. important:: - Computed columns must be created under the :ref:`exposed schema ` to be used in this way. + Computed columns must be created in the :ref:`exposed schema ` or in a schema in the :ref:`extra search path ` to be used in this way. Unicode support --------------- From 6aeb5065f8f901888e325400fca93a31e70fa023 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Thu, 3 Feb 2022 08:06:01 +0100 Subject: [PATCH 480/549] Add docs about accessing array items and fields of composite types with JSON operators. Added in https://github.com/PostgREST/postgrest/pull/2145 Signed-off-by: Wolfgang Walther --- docs/api.rst | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index a95f1118c8..2cac9c2a22 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -269,10 +269,10 @@ Casting the columns is possible by suffixing them with the double colon ``::`` p .. _json_columns: -JSON Columns ------------- +Array / Composite / JSON Columns +-------------------------------- -You can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `_. +You can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `_. This also works for array items and fields of composite types. .. tabs:: @@ -348,10 +348,19 @@ Note that ``->>`` is used to compare ``blood_type`` as ``text``. To compare with { "id": 15, "age": 35 } ] + +.. important:: + + When using the ``->`` and ``->>`` operators, PostgREST uses a query like ``to_jsonb()->'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')); + .. _computed_cols: -Computed Columns ----------------- +Computed / Virtual Columns +-------------------------- Filters may be applied to computed columns(**a.k.a. virtual columns**) as well as actual table/view columns, even though the computed columns will not appear in the output. For example, to search first and last names at once we can create a computed column that will not appear in the output but can be used in a filter: @@ -537,7 +546,7 @@ If you care where nulls are sorted, add ``nullsfirst`` or ``nullslast``: curl "http://localhost:3000/people?order=age.desc.nullslast" -You can also use :ref:`computed_cols` to order the results, even though the computed columns will not appear in the output. +You can also use :ref:`computed_cols` to order the results, even though the computed columns will not appear in the output. You can sort by nested fields of :ref:`json_columns` with the JSON operators. .. _limits: From 9049ed1f7139e231142aae48eeb14ceec34b4373 Mon Sep 17 00:00:00 2001 From: Electronoob Date: Tue, 8 Feb 2022 01:28:16 +0000 Subject: [PATCH 481/549] updated the example config switch replacing postgrest 2 with postgrest -e --- docs/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/install.rst b/docs/install.rst index eebeeb33a3..5ab43ba111 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -86,7 +86,7 @@ The PostgREST server reads a configuration file as its only argument: postgrest /path/to/postgrest.conf # You can also generate a sample config file with - # postgrest 2> postgrest.conf + # 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`. From b209731ff326b050e4491ab571bce8dff01bfc7c Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Wed, 9 Feb 2022 14:17:22 -0500 Subject: [PATCH 482/549] Add admin section on file descriptors (#504) --- docs/admin.rst | 12 ++++++++++++ postgrest.dict | 1 + 2 files changed, 13 insertions(+) diff --git a/docs/admin.rst b/docs/admin.rst index c319c29c47..d487062ed5 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -256,6 +256,7 @@ Schema Reloading Changing the schema while the server is running can lead to errors due to a stale schema cache. To learn how to refresh the cache see :ref:`schema_reloading`. + Daemonizing =========== @@ -295,6 +296,17 @@ After that, you can enable the service at boot time and start it with: ## For reloading the service ## systemctl restart postgrest +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 + Alternate URL Structure ======================= diff --git a/postgrest.dict b/postgrest.dict index 16a91dd584..60c7055c13 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -51,6 +51,7 @@ Haskell Heroku HMAC Homebrew +HTTP HTTPS HV Ibarluzea From abe0daa3dcd9398da6a3a7cd2e25b94766dc78c3 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Tue, 15 Feb 2022 14:20:12 -0500 Subject: [PATCH 483/549] Add example for array and composite type columns --- docs/api.rst | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 2cac9c2a22..6856e6e811 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -272,7 +272,14 @@ Casting the columns is possible by suffixing them with the double colon ``::`` p Array / Composite / JSON Columns -------------------------------- -You can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `_. This also works for array items and fields of composite types. +You can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `_. + +.. code-block:: postgres + + CREATE TABLE people ( + id int, + json_data json + ); .. tabs:: @@ -348,6 +355,41 @@ Note that ``->>`` is used to compare ``blood_type`` as ``text``. To compare with { "id": 15, "age": 35 } ] +The arrow operators are also used for array and composite type columns. + +.. code-block:: postgres + + CREATE TYPE coordinates ( + lat decimal(8,6), + long decimal(9,6) + ); + + CREATE TABLE countries ( + id int, + location coordinates, + languages text[] + ); + +.. tabs:: + + .. code-tab:: http + + GET /countries?select=id,location->>lat,location->>long,primary_language:languages->0&location->lat=gte.19 HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: From fbe317b2444ef0ae0a0b550906b27537bc2394c1 Mon Sep 17 00:00:00 2001 From: Adam Kliment <79609+netmilk@users.noreply.github.com> Date: Tue, 22 Feb 2022 17:48:22 +0100 Subject: [PATCH 484/549] Update auth.rst (#507) --- docs/auth.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/auth.rst b/docs/auth.rst index 867339cbc9..94e449db58 100644 --- a/docs/auth.rst +++ b/docs/auth.rst @@ -93,9 +93,18 @@ Alternately database roles can represent groups instead of (or in addition to) i SQL code can access claims through GUC variables set by PostgREST per request. For instance to get the email claim, call this function: +For PostgreSQL server version >= 14 + .. code:: sql current_setting('request.jwt.claims', true)::json->>'email'; + + +For PostgreSQL server version < 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. From 4aff0caa59b88b3c828fb12bcdfc13e0f797f532 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 23 Feb 2022 17:25:07 -0500 Subject: [PATCH 485/549] Add documentation on the minimal health check --- docs/admin.rst | 29 +++++++++++++++++++++++++++++ docs/configuration.rst | 22 ++++++++++++++++++---- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/docs/admin.rst b/docs/admin.rst index d487062ed5..f2f0ad8f80 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -256,6 +256,35 @@ Schema Reloading Changing the schema while the server is running can lead to errors due to a stale schema cache. To learn how to refresh the cache see :ref:`schema_reloading`. +.. _health_check: + +Health Check +------------ + +You can enable a minimal health check to verify if PostgREST is available for client requests and 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``: + +.. tabs:: + + .. code-tab:: http + + GET localhost:3001/live HTTP/1.1 + + .. code-tab:: bash Curl + + curl -I "http://localhost:3001/live" + +.. code-block:: http + + HTTP/1.1 200 OK + Daemonizing =========== diff --git a/docs/configuration.rst b/docs/configuration.rst index 422093d36c..57c22199b5 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -105,6 +105,7 @@ It's not possible to change :ref:`env_variables_config` for a running process an The following settings will not be reloaded. You will need to restart PostgREST to change those. + * :ref:`admin-server-port` * :ref:`db-uri` * :ref:`db-pool` * :ref:`db-pool-timeout` @@ -145,6 +146,7 @@ List of parameters ======================== ======= ================= ========== Name Type Default Reloadable ======================== ======= ================= ========== +admin-server-port Int app.settings.* String Y db-anon-role String Y db-channel String pgrst Y @@ -174,6 +176,18 @@ server-unix-socket String server-unix-socket-mode String 660 ======================== ======= ================= ========== +.. _admin-server-port: + +admin-server-port +----------------- + + =============== ======================= + **Environment** PGRST_ADMIN_SERVER_PORT + **In-Database** `n/a` + =============== ======================= + +Specifies the port for the :ref:`health_check` endpoints. + .. _app.settings.*: app.settings.* @@ -589,7 +603,7 @@ server-host =============== ================= **Environment** PGRST_SERVER_HOST - **In-Database** 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: @@ -607,7 +621,7 @@ server-port =============== ================= **Environment** PGRST_SERVER_PORT - **In-Database** pgrst.server_port + **In-Database** `n/a` =============== ================= The TCP port to bind the web server. @@ -619,7 +633,7 @@ server-unix-socket =============== ================= **Environment** PGRST_SERVER_UNIX_SOCKET - **In-Database** pgrst.server_unix_socket + **In-Database** `n/a` =============== ================= `Unix domain socket `_ where to bind the PostgREST web server. @@ -636,7 +650,7 @@ server-unix-socket-mode =============== ================= **Environment** PGRST_SERVER_UNIX_SOCKET_MODE - **In-Database** pgrst.server_unix_socket_mode + **In-Database** `n/a` =============== ================= `Unix file mode `_ to be set for the socket specified in :ref:`server-unix-socket` From e9ef876433348a76e32320857ad4edf0de207041 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Tue, 8 Feb 2022 09:07:02 +0100 Subject: [PATCH 486/549] Add hint on how to prevent exposing computed columns as RPCs Signed-off-by: Wolfgang Walther --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 6856e6e811..ab4f997fbe 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -447,7 +447,7 @@ As mentioned, computed columns do not appear in the output by default. However y .. important:: - Computed columns must be created in the :ref:`exposed schema ` or in a schema in the :ref:`extra search path ` to be used in this way. + Computed columns must be created in the :ref:`exposed schema ` or in a schema in the :ref:`extra search path ` to be used in this way. When placing the computed column in the :ref:`exposed schema ` you can use an **unnamed** argument, as in the example above, to prevent it from being exposed as an :ref:`RPC ` under ``/rpc``. Unicode support --------------- From 98976e4d57b862e4b177438763aae351c02ecdab Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 4 Mar 2022 20:04:49 -0500 Subject: [PATCH 487/549] Add how-to for working with PostgreSQL data types --- .../working-with-postgresql-data-types.rst | 80 +++++++++++++++++++ docs/index.rst | 1 + postgrest.dict | 1 + 3 files changed, 82 insertions(+) create mode 100644 docs/how-tos/working-with-postgresql-data-types.rst diff --git a/docs/how-tos/working-with-postgresql-data-types.rst b/docs/how-tos/working-with-postgresql-data-types.rst new file mode 100644 index 0000000000..7cba0a979d --- /dev/null +++ b/docs/how-tos/working-with-postgresql-data-types.rst @@ -0,0 +1,80 @@ +.. _working_with_types: + +Working with PostgreSQL data types +================================== + +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. + +Timestamps +---------- + +You can use the **time zone** to filter or send data if needed. Let's use this table as an example: + +.. 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: + +.. tabs:: + + .. code-tab:: http + + POST /reports HTTP/1.1 + Content-Type: application/json + + [{ "id": 1, "due_date": "2022-02-24 11:10:15 Australia/Sydney" }, + { "id": 2, "due_date": "2022-02-27 22:00:00 Australia/Sydney" }] + + .. code-tab:: bash Curl + + 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: + +.. tabs:: + + .. code-tab:: http + + GET /reports?due_date=eq.2022-02-24+02:10:15+Africa/Cairo HTTP/1.1 + + .. code-tab:: bash Curl + + 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``. + +You can use other comparative filters and also `PostgreSQL special date/time input values `_. For instance, to get the reports that are due after today you would do: + +.. tabs:: + + .. code-tab:: http + + GET /reports?due_date=gt.today HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/reports?due_date=gt.today" + +.. code-block:: json + + [ + { + "id": 2, + "due_date": "2022-02-27T06:00:00-05:00" + } + ] diff --git a/docs/index.rst b/docs/index.rst index 3d522585d7..8fa7b04f76 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -201,6 +201,7 @@ These are recipes that'll help you address specific use-cases. - :doc:`how-tos/embedding-table-from-another-schema` - :doc:`how-tos/providing-images-for-img` - `How PostgreSQL triggers work when called with a PostgREST PATCH HTTP request `_ +- :doc:`how-tos/working-with-postgresql-data-types` Ecosystem --------- diff --git a/postgrest.dict b/postgrest.dict index 60c7055c13..1ac2dd9766 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -11,6 +11,7 @@ authenticator backoff balancer Beles +booleans Bouscal buildpack Cardano From ea58095e92bac0ad5f5af826aec2145d67fd82db Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 8 Mar 2022 01:56:21 +0100 Subject: [PATCH 488/549] Bump actions/checkout from 2.4.0 to 3 (#512) --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b03dadad10..fb815e2ccf 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -15,7 +15,7 @@ jobs: name: Build docs runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 - uses: cachix/install-nix-action@v16 - run: nix-env -f default.nix -iA build - run: postgrest-docs-build @@ -24,7 +24,7 @@ jobs: name: Run spellcheck runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 - uses: cachix/install-nix-action@v16 - run: nix-env -f default.nix -iA spellcheck - run: postgrest-docs-spellcheck @@ -34,7 +34,7 @@ jobs: if: github.base_ref == 'main' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2.4.0 + - uses: actions/checkout@v3 - uses: cachix/install-nix-action@v16 - run: nix-env -f default.nix -iA linkcheck - run: postgrest-docs-linkcheck From 1489fc84b8a28d61936b2d2c8ed0ca7f68147159 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 14 Mar 2022 14:56:34 -0500 Subject: [PATCH 489/549] Recommend specifying host names for health check (#514) In case of multiple network interfaces --- docs/admin.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/admin.rst b/docs/admin.rst index f2f0ad8f80..acdfc29a1b 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -285,6 +285,7 @@ For instance, to verify if PostgREST is running at ``localhost:3000`` while the 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 ` 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. Daemonizing =========== From 8f989480783ab05067e66fe623be7974b83950d1 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 14 Mar 2022 15:03:01 -0500 Subject: [PATCH 490/549] Add Errors reference (#430) * Adds PostgREST' error codes * Move error information to a dedicated reference page * Organize errors into tables --- docs/_static/css/custom.css | 8 + docs/admin.rst | 5 + docs/api.rst | 89 ++--------- docs/errors.rst | 301 ++++++++++++++++++++++++++++++++++++ docs/index.rst | 7 + docs/schema_cache.rst | 12 +- docs/tutorials/tut0.rst | 2 +- docs/tutorials/tut1.rst | 7 +- postgrest.dict | 2 + 9 files changed, 356 insertions(+), 77 deletions(-) create mode 100644 docs/errors.rst diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 6015b6d486..fc7f2edb68 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -57,3 +57,11 @@ div.line-block { margin-right: auto; margin-bottom: 24px; } + +.wy-table-responsive table td { + white-space: normal !important; +} + +.wy-table-responsive { + overflow: visible !important; +} diff --git a/docs/admin.rst b/docs/admin.rst index acdfc29a1b..e7a7448f98 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -153,6 +153,11 @@ Server Version When debugging a problem it's important to verify the PostgREST version. At any time you can make a request to the running server and determine exactly which version is deployed. Look for the :code:`Server` HTTP response header, which contains the version number. +Errors +------ + +See the :doc:`Errors ` reference page for detailed information on the errors that PostgREST returns. + .. _pgrst_logging: Logging diff --git a/docs/api.rst b/docs/api.rst index ab4f997fbe..7acaafa366 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -539,6 +539,8 @@ Here ``Quote:"`` and ``Backslash:\`` are percent-encoded values. Note that ``%5C 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`. +.. _ordering: + Ordering -------- @@ -827,7 +829,9 @@ When a singular response is requested but no entries are found, the server respo { "message": "JSON object requested, multiple (or no) rows returned", - "details": "Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row" + "details": "Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row", + "hint": null, + "code": "PGRST505" } .. note:: @@ -1714,6 +1718,8 @@ By specifying the ``on_conflict`` query parameter, you can make UPSERT work on a ] EOF +.. _upsert_put: + PUT ~~~ @@ -2636,8 +2642,10 @@ You can set the ``response.status`` GUC to override the default status code Post 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."} + { + "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). @@ -2693,72 +2701,9 @@ Returns: HTTP/1.1 402 Payment Required Content-Type: application/json; charset=utf-8 - {"hint":"Upgrade your plan","details":"Quota exceeded"} - -.. _status_codes: - -HTTP Status Codes ------------------ - -PostgREST translates `PostgreSQL error codes `_ 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 | | -+--------------------------+-------------------------+---------------------------------+ + { + "message": "Payment Required", + "details": "Quota exceeded", + "hint": "Upgrade your plan", + "code": "PT402" + } diff --git a/docs/errors.rst b/docs/errors.rst new file mode 100644 index 0000000000..39fa807a43 --- /dev/null +++ b/docs/errors.rst @@ -0,0 +1,301 @@ +.. _error_source: + +Error Source +============ + +For the most part, error messages will come directly from the database with the same `format that PostgreSQL uses `_, in other words, PostgREST will convert the ``MESSAGE``, ``DETAIL``, ``HINT`` and ``ERRCODE`` from the PostgreSQL error to JSON format and add an HTTP status code to the response (see :ref:`status_codes`). For instance, this is the error you will get when querying a nonexistent table: + +.. code-block:: http + + GET /nonexistent_table?id=eq.1 HTTP/1.1 + +.. code-block:: json + + { + "hint": null, + "details": null, + "code": "42P01", + "message": "relation \"api.nonexistent_table\" does not exist" + } + +However, some errors do come from PostgREST itself (such as those related to the :ref:`schema_cache`). These have the same structure as the PostgreSQL errors (message, details, hint and code) but are differentiated by the ``PGRST`` prefix in the ``code`` field (see :ref:`pgrst_errors`). For instance, when querying a function that does not exist, the error will be: + +.. code-block:: http + + POST /rpc/nonexistent_function HTTP/1.1 + +.. code-block:: json + + { + "hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.", + "details": null + "code": "PGRST202", + "message": "Could not find the api.nonexistent_function() function in the schema cache" + } + +.. _status_codes: + +HTTP Status Codes +================= + +PostgREST translates `PostgreSQL error codes `_ 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 | | ++--------------------------+-------------------------+---------------------------------+ + +.. _pgrst_errors: + +PostgREST Error Codes +===================== + +PostgREST error codes have the form ``PGRSTgxx``, where ``PGRST`` is the prefix that differentiates the error from a PostgreSQL error, ``g`` is the group where the error belongs and ``xx`` is the number that identifies the error in the group. + +.. _pgrst0**: + +Group 0 - Connection +-------------------- + +Related to the connection with the database. + ++---------------+-------------------------------------------------------------+ +| Code | Description | ++===============+=============================================================+ +| .. _pgrst000: | Could not connect with the database due to an incorrect | +| | :ref:`db-uri` or due to the PostgreSQL service not running. | +| PGRST000 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst001: | Could not connect with the database due to an internal | +| | error. | +| PGRST001 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst002: | Could not connect with the database when building the | +| | :ref:`schema_cache` due to the PostgreSQL service not | +| PGRST002 | running. | ++---------------+-------------------------------------------------------------+ + +.. _pgrst1**: + +Group 1 - Api Request +--------------------- + +Related to the HTTP request elements. + ++---------------+-------------------------------------------------------------+ +| Code | Description | ++===============+=============================================================+ +| .. _pgrst100: | Parsing error in the query string parameter. | +| | See :ref:`h_filter`, :ref:`operators` and :ref:`ordering`. | +| PGRST100 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst101: | For :ref:`functions `, only ``GET`` and ``POST`` | +| | verbs are allowed. Any other verb will throw this error. | +| PGRST101 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst102: | Related to the request body structure. | +| | See :ref:`insert_update`. | +| PGRST102 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst103: | Related to :ref:`limits`. | +| | | +| PGRST103 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst104: | Either the :ref:`filter operator ` is missing | +| | or it doesn't exist. | +| PGRST104 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst105: | Related to an :ref:`UPSERT using PUT `. | +| | | +| PGRST105 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst106: | The schema specified when | +| | :ref:`switching schemas ` is not present | +| PGRST106 | in the :ref:`db-schemas` configuration variable. | ++---------------+-------------------------------------------------------------+ +| .. _pgrst107: | The ``Content-Type`` sent in the request is invalid. | +| | | +| PGRST107 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst108: | 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`. | ++---------------+-------------------------------------------------------------+ + +.. _pgrst2**: + +Group 2 - Schema Cache +---------------------- + +Related to a :ref:`stale schema cache `. Most of the time, these errors are solved by :ref:`reloading the schema cache `. + ++---------------+-------------------------------------------------------------+ +| Code | Description | ++===============+=============================================================+ +| .. _pgrst200: | Caused by :ref:`stale_fk_relationships`, otherwise any of | +| | the embedding resources or the relationship itself may not | +| PGRST200 | exist in the database. | ++---------------+-------------------------------------------------------------+ +| .. _pgrst201: | Related to :ref:`embed_disamb`. | +| | | +| PGRST201 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst202: | Caused by a :ref:`stale_function_signature`, otherwise | +| | the function may not exist in the database. | +| PGRST202 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst203: | 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. | ++---------------+-------------------------------------------------------------+ + +.. _pgrst3**: + +Group 3 - JWT errors +-------------------- + +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 ` for more information on this process. + ++---------------+-------------------------------------------------------------+ +| Code | Description | ++===============+=============================================================+ +| .. _pgrst300: | A :ref:`JWT secret ` is missing from the | +| | configuration. | +| PGRST300 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst301: | Any error related to the verification of the JWT, | +| | which means that the JWT provided is invalid in some way. | +| PGRST301 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst302: | Attempted to do a request without | +| | :ref:`authentication ` when the anonymous role | +| PGRST302 | is disabled by not setting it in :ref:`db-anon-role`. | ++---------------+-------------------------------------------------------------+ + +.. _pgrst4**: + +Group 4 - Hasql +--------------- + +Related to `the library `_ that PostgREST uses to connect to the database. If you encounter any of these errors, you may have stumbled on a PostgREST bug, please `open an issue `_ and we'll be glad to fix it. + ++---------------+-------------------------------------------------------------+ +| Code | Description | ++===============+=============================================================+ +| .. _pgrst400: | Internal error: Unexpected Result. | +| | | +| PGRST400 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst401: | Internal error: Attempted to parse more columns than | +| | there are in the result. | +| PGRST401 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst402: | Internal error: Attempted to parse a NULL as some value. | +| | | +| PGRST402 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst403: | Internal error: Wrong value parser used. | +| | | +| PGRST403 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst404: | Internal error: Unexpected amount of rows. | +| | | +| PGRST404 | | ++---------------+-------------------------------------------------------------+ + +.. _pgrst5**: + +Group 5 - General +----------------- + +These are uncategorized errors. + ++---------------+-------------------------------------------------------------+ +| Code | Description | ++===============+=============================================================+ +| .. _pgrst500: | Related to :ref:`guc_resp_hdrs`. | +| | | +| PGRST500 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst501: | The status code must be a positive integer. | +| | See :ref:`guc_resp_status`. | +| PGRST501 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst502: | Related to :ref:`binary_output`. See :ref:`providing_img` | +| | for an example on requesting images. | +| PGRST502 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst503: | For an :ref:`UPSERT using PUT `, when | +| | :ref:`limits and offsets ` are used. | +| PGRST503 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst504: | For an :ref:`UPSERT using PUT `, when the | +| | primary key in the query string and the body are different. | +| PGRST504 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst505: | More than 1 or no items where returned when requesting | +| | a singular response. See :ref:`singular_plural`. | +| PGRST505 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst506: | The HTTP verb used in the request in not supported. | +| | | +| PGRST506 | | ++---------------+-------------------------------------------------------------+ diff --git a/docs/index.rst b/docs/index.rst index 8fa7b04f76..ea08aa3ade 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -145,9 +145,16 @@ Technical references for PostgREST's functionality. schema_cache.rst +.. toctree:: + :caption: Errors + :hidden: + + errors.rst + - :doc:`API ` - :doc:`configuration` - :doc:`Schema Cache ` +- :doc:`Errors ` Topic guides ------------ diff --git a/docs/schema_cache.rst b/docs/schema_cache.rst index 11d0f95105..4b59c79887 100644 --- a/docs/schema_cache.rst +++ b/docs/schema_cache.rst @@ -27,6 +27,8 @@ in order to avoid repeating this work, PostgREST uses a schema cache. | | Function signature | +--------------------------------------------+-------------------------------------------------------------------------------+ +.. _stale_schema: + The Stale Schema Cache ---------------------- @@ -56,8 +58,10 @@ The result will be an error: .. code-block:: json { - "hint": "If a new foreign key between these entities was created in the database, try reloading the schema cache.", - "message": "Could not find a relationship between cities and countries in the schema cache" + "hint": "Verify that 'cities' and 'countries' exist in the schema 'api' and that there is a foreign key relationship between them. If a new relationship was created, try reloading the schema cache.", + "details": null, + "code": "PGRST200", + "message": "Could not find a relationship between 'cities' and 'countries' in the schema cache" } As you can see, PostgREST couldn't find the newly created foreign key in the schema cache. See :ref:`schema_reloading` and :ref:`auto_schema_reloading` to solve this issue. @@ -89,7 +93,9 @@ The same issue will occur on newly created functions on a running PostgREST. .. code-block:: json { - "hint": "If a new function was created in the database with this name and arguments, try reloading the schema cache.", + "hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.", + "details": null, + "code": "PGRST202", "message": "Could not find the api.plus_one(num) function in the schema cache" } diff --git a/docs/tutorials/tut0.rst b/docs/tutorials/tut0.rst index a0ffb936c3..5a2ed16047 100644 --- a/docs/tutorials/tut0.rst +++ b/docs/tutorials/tut0.rst @@ -223,7 +223,7 @@ Response is 401 Unauthorized: "hint": null, "details": null, "code": "42501", - "message": "permission denied for relation todos" + "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. diff --git a/docs/tutorials/tut1.rst b/docs/tutorials/tut1.rst index cb307dc4b1..33ef764e73 100644 --- a/docs/tutorials/tut1.rst +++ b/docs/tutorials/tut1.rst @@ -175,7 +175,12 @@ After expiration, the API returns HTTP 401 Unauthorized: .. code-block:: json - {"message":"JWT expired"} + { + "hint": null, + "details": null, + "code": "PGRST301", + "message": "JWT expired" + } Bonus Topic: Immediate Revocation --------------------------------- diff --git a/postgrest.dict b/postgrest.dict index 1ac2dd9766..2b15bd67b0 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -49,6 +49,7 @@ GUC gucs Gumbs Haskell +Hasql Heroku HMAC Homebrew @@ -170,6 +171,7 @@ Tyll TypeScript UI ui +uncategorized unicode unix updatable From f56373313ee076c5df652cd9afef584f1d51b91d Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Tue, 22 Mar 2022 10:57:04 +0100 Subject: [PATCH 491/549] Fix not reloading on ALTER TYPE (#517) --- docs/schema_cache.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/schema_cache.rst b/docs/schema_cache.rst index 4b59c79887..9aa1f3a13a 100644 --- a/docs/schema_cache.rst +++ b/docs/schema_cache.rst @@ -191,7 +191,7 @@ reloading when creating temporary tables(``CREATE TEMP TABLE``) inside functions , 'CREATE MATERIALIZED VIEW', 'ALTER MATERIALIZED VIEW' , 'CREATE FUNCTION', 'ALTER FUNCTION' , 'CREATE TRIGGER' - , 'CREATE TYPE' + , 'CREATE TYPE', 'ALTER TYPE' , 'CREATE RULE' , 'COMMENT' ) From a8034df2bf7a5d07b57e760b664ec2efc1b77f24 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Tue, 22 Mar 2022 05:30:45 -0500 Subject: [PATCH 492/549] Add how to on PostGIS and hstore data types (#516) --- .../working-with-postgresql-data-types.rst | 214 ++++++++++++++++++ postgrest.dict | 1 + 2 files changed, 215 insertions(+) diff --git a/docs/how-tos/working-with-postgresql-data-types.rst b/docs/how-tos/working-with-postgresql-data-types.rst index 7cba0a979d..921be2ba53 100644 --- a/docs/how-tos/working-with-postgresql-data-types.rst +++ b/docs/how-tos/working-with-postgresql-data-types.rst @@ -78,3 +78,217 @@ You can use other comparative filters and also `PostgreSQL special date/time inp "due_date": "2022-02-27T06:00:00-05:00" } ] + +hstore +------ + +You can work with data types belonging to additional supplied modules such as `hstore `_. Let's use the following table: + +.. 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, for instance: + +.. tabs:: + + .. code-tab:: http + + POST /countries HTTP/1.1 + Content-Type: application/json + + [ + { "id": 1, "name": "common => Egypt, official => \"Arab Republic of Egypt\", native => مصر" }, + { "id": 2, "name": "common => Germany, official => \"Federal Republic of Germany\", native => Deutschland" } + ] + + .. code-tab:: bash Curl + + 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`. For example, if you want to get the native name of Egypt, the query would be: + +.. tabs:: + + .. code-tab:: http + + GET /countries?select=name->>native&name->>common=like.Egypt HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/countries?select=name->>native&name->>common=like.Egypt" + +.. code-block:: json + + [{ "native": "مصر" }] + +PostGIS +------------------ + +You can use the string representation for `PostGIS `_ data types such as ``geometry`` or ``geography``. As an example, let's create a table using the ``geometry`` type (you need to `install PostGIS `_ 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 + ); + +Say you want to add areas in polygon format. The request using string representation would look like: + +.. tabs:: + + .. code-tab:: http + + POST /coverage HTTP/1.1 + Content-Type: application/json + + [ + { "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))" } + ] + + .. code-tab:: bash Curl + + 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 to ``JSON`` format. Although this output is useful, you will want to use the PostGIS functions to have more control on filters or casts. For these cases, creating a ``view`` is your best option. For example, let's use some of the functions to get the data in `GeoJSON format `_ and to calculate the area in square units: + +.. code-block:: postgres + + create or replace view coverage_geo as + select name, + -- Get the Geometry Object + st_AsGeoJSON(c.area)::json as geo_geometry, + -- Get the Feature Object + st_AsGeoJSON(c.*)::json as geo_feature, + -- Calculate the area in square units + st_area(c.area) as square_units + from coverage c; + + -- Create another view for the FeatureCollection Object + -- for the sake of making the examples clearer + create or replace view coverage_geo_collection as + select + json_build_object( + 'type', 'FeatureCollection', + 'features', json_agg(st_AsGeoJSON(c.*)::json) + ) + as geo_feature_collection + from coverage c; + +Now the query will return the information as you expected: + +.. tabs:: + + .. code-tab:: http + + GET /coverage_geo?name=eq.big HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/coverage_geo?name=eq.big" + +.. code-block:: json + + [ + { + "name": "big", + "geo_geometry": { + "type": "Polygon", + "coordinates": [ + [[0,0],[10,0],[10,10],[0,10],[0,0]] + ] + }, + "geo_feature": { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [[0,0],[10,0],[10,10],[0,10],[0,0]] + ] + }, + "properties": { + "id": 2, + "name": "big" + } + }, + "square_units": 100 + } + ] + +And for the Feature Collection format: + +.. tabs:: + + .. code-tab:: http + + GET /coverage_geo_collection HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/coverage_geo_collection" + +.. code-block:: json + + [ + { + "geo_feature_collection": { + "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" + } + } + ] + } + } + ] diff --git a/postgrest.dict b/postgrest.dict index 2b15bd67b0..a20158ba4d 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -53,6 +53,7 @@ Hasql Heroku HMAC Homebrew +hstore HTTP HTTPS HV From 5688bc1521f992b00dd37662d3510d08d3ecec44 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 24 Mar 2022 17:48:43 -0500 Subject: [PATCH 493/549] Add JSON, Composite and Ranges types to the working with types how-to --- docs/ecosystem.rst | 2 + docs/how-tos/casting-type-to-custom-json.rst | 97 ------- .../embedding-table-from-another-schema.rst | 81 ------ .../working-with-postgresql-data-types.rst | 253 ++++++++++++++++++ docs/index.rst | 3 - 5 files changed, 255 insertions(+), 181 deletions(-) delete mode 100644 docs/how-tos/casting-type-to-custom-json.rst delete mode 100644 docs/how-tos/embedding-table-from-another-schema.rst diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index 883db8bc04..10923ab158 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -14,6 +14,8 @@ Community Tutorials * `"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. + .. _templates: Templates diff --git a/docs/how-tos/casting-type-to-custom-json.rst b/docs/how-tos/casting-type-to-custom-json.rst deleted file mode 100644 index b4687bad2f..0000000000 --- a/docs/how-tos/casting-type-to-custom-json.rst +++ /dev/null @@ -1,97 +0,0 @@ -Casting a type to a custom JSON object -====================================== - -:author: `steve-chavez `_ - -While using PostgREST you might have noticed that certain PostgreSQL types translate to JSON strings when you would -have expected a JSON object or array. For example, let's see the case of `range types `_. - -.. code-block:: postgres - - -- example taken from https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-EXAMPLES - create table reservations ( - room int - , during tsrange - ); - - insert into - reservations - values - (1108, tsrange('2010-01-01 14:30', '2010-01-01 15:30')); - -Here we have a column named **during** as a ``tsrange`` type, we would like to get it as JSON through PostgREST. - -.. code-block:: bash - - curl "http://localhost:3000/reservations" - -Result: - -.. code-block:: json - - [ - { - "room":1108, - "during":"[\"2010-01-01 14:30:00\",\"2010-01-01 15:30:00\")" - } - ] - -The **during** value is probably not the in the format you want. We get a JSON string because by default PostgreSQL casts -the type to JSON by using its ``text`` representation. We can change this representation to a custom JSON object by `creating a CAST `_ . - -To do this, first we'll define the function that will do the conversion from ``tsrange`` to ``json``. - -.. 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; - -Using this function we'll create the CAST. - -.. code-block:: postgres - - create cast (tsrange as json) with function tsrange_to_json(tsrange) as assignment; - -And we'll do the request and :ref:`cast the column `. - -.. code-block:: bash - - curl "http://localhost:3000/reservations?select=room,during::json" - -The result now is: - -.. code-block:: json - - [ - { - "room":1108, - "during":{ - "lower" : "2010-01-01T14:30:00", - "upper" : "2010-01-01T15:30:00", - "lower_inc" : true, - "upper_inc" : false - } - } - ] - -You can use the same idea for creating custom casts for different types. - -.. note:: - - If you don't want to modify casts for built-in types, an option would be to `create a custom type `_ - 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 analoguously to the above example - -- ... - - create cast (mytsrange as json) with function mytsrange_to_json(mytsrange) as assignment; diff --git a/docs/how-tos/embedding-table-from-another-schema.rst b/docs/how-tos/embedding-table-from-another-schema.rst deleted file mode 100644 index 6631982de6..0000000000 --- a/docs/how-tos/embedding-table-from-another-schema.rst +++ /dev/null @@ -1,81 +0,0 @@ -Embedding a table from another schema -===================================== - -:author: `steve-chavez `_ - -Suppose you have a **people** table in the ``public`` schema and this schema is exposed through PostgREST's :ref:`db-schemas`. - -.. code-block:: postgres - - create table public.people( - id int primary key - , full_name text - ); - -And you want to :ref:`embed ` the **people** table with a **details** table that's in another schema named ``private``. - -.. code-block:: postgres - - create schema if not exists private; - - -- For simplicity's sake the table is devoid of constraints/domains on email, phone, etc. - create table private.details( - id int primary key references public.people - , email text - , phone text - , birthday date - , occupation text - , company text - ); - - -- other database objects in this schema - -- ... - -- ... - -To solve this, you can create a view of **details** in the ``public`` schema. We'll call it **public_details**. - -.. code-block:: postgres - - create view public.public_details as - select - id - , occupation - , company - from - private.details; - -Since PostgREST supports :ref:`embedding_views`, you can embed **people** with **public_details**. - -Let's insert some data to test this: - -.. code-block:: postgres - - insert into - public.people - values - (1, 'John Doe'), (2, 'Jane Doe'); - - insert into - private.details - values - (1, 'jhon@fake.com', '772-323-5433', '1990-02-01', 'Transportation attendant', 'Body Fate'), - (2, 'jane@fake.com', '480-474-6571', '1980-04-21', 'Geotechnical engineer', 'Earthworks Garden Kare'); - -.. important:: - - Make sure PostgREST's schema cache is up-to-date. See :ref:`schema_reloading`. - -Now, make the following request: - -.. code-block:: bash - - curl "http://localhost:3000/people?select=full_name,public_details(occupation,company)" - -The result should be: - -.. code-block:: json - - [ - {"full_name":"John Doe","public_details":[{"occupation":"Transportation attendant","company":"Body Fate"}]}, - {"full_name":"Jane Doe","public_details":[{"occupation":"Geotechnical engineer","company":"Earthworks Garden Kare"}]} - ] diff --git a/docs/how-tos/working-with-postgresql-data-types.rst b/docs/how-tos/working-with-postgresql-data-types.rst index 921be2ba53..06276f6c79 100644 --- a/docs/how-tos/working-with-postgresql-data-types.rst +++ b/docs/how-tos/working-with-postgresql-data-types.rst @@ -79,6 +79,259 @@ You can use other comparative filters and also `PostgreSQL special date/time inp } ] +JSON +---- + +To work with a ``json`` type column, you can handle the value as a JSON object. For instance, let's use this table: + +.. code-block:: postgres + + create table products ( + id int primary key, + name text unique, + extra_info json + ); + +Now, you can insert a new product using a JSON object for the ``extra_info`` column: + +.. tabs:: + + .. code-tab:: http + + POST /products HTTP/1.1 + Content-Type: application/json + + { + "id": 1, + "name": "Canned fish", + "extra_info": { + "expiry_date": "2025-12-31", + "exportable": true + } + } + + .. code-tab:: bash Curl + + 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. + +Composite Types +--------------- + +With PostgREST, you have two options to handle `composite type columns `_. On one hand you can use string representation and on the other you can handle it as you would a JSON column. Let's create a type and a table for this example: + +.. 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")'); + +Now, you could insert values using string representation as seen in the example above. + +.. tabs:: + + .. code-tab:: http + + POST /products HTTP/1.1 + Content-Type: application/json + + { "id": 2, "size": "(0.7,0.5,1.8,\"m\")" } + + .. code-tab:: bash Curl + + 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 data in JSON format. The following request is equivalent to the previous one: + +.. tabs:: + + .. code-tab:: http + + POST /products HTTP/1.1 + Content-Type: application/json + + { + "id": 2, + "size": { + "length": 0.7, + "width": 0.5, + "height": 1.8, + "unit": "m" + } + } + + .. code-tab:: bash Curl + + 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 data using the arrow operators as you would for :ref:`JSON columns `. + +Ranges +------ + +To illustrate how to work with `ranges `_, let's use the following table as an example: + +.. code-block:: postgres + + create table events ( + id int primary key, + name text unique, + duration tsrange + ); + +Now, to insert a new event, specify the ``duration`` value as a string representation of the ``tsrange`` type, for example: + +.. tabs:: + + .. code-tab:: http + + POST /events HTTP/1.1 + Content-Type: application/json + + { + "id": 1, + "name": "New Year's Party", + "duration": "['2022-12-31 11:00','2023-01-01 06:00']" + } + + .. code-tab:: bash Curl + + 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 ` to filter the data. But what if you need get the events for the New Year 2023? Doing this filter ``events?duration=cs.2023-01-01`` will return an error because PostgreSQL needs an explicit cast to timestamp of the string value. A workaround would be to use a range starting and ending in the same date, like this: + +.. tabs:: + + .. code-tab:: http + + GET /events?duration=cs.[2023-01-01,2023-01-01] HTTP/1.1 + + .. code-tab:: bash Curl + + 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 `: + +.. tabs:: + + .. code-tab:: http + + GET /events?select=id,name,duration::json HTTP/1.1 + + .. code-tab:: bash Curl + + 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 `_ + 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; + hstore ------ diff --git a/docs/index.rst b/docs/index.rst index ea08aa3ade..9448cd0890 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -204,10 +204,7 @@ These are recipes that'll help you address specific use-cases. how-tos/* -- :doc:`how-tos/casting-type-to-custom-json` -- :doc:`how-tos/embedding-table-from-another-schema` - :doc:`how-tos/providing-images-for-img` -- `How PostgreSQL triggers work when called with a PostgREST PATCH HTTP request `_ - :doc:`how-tos/working-with-postgresql-data-types` Ecosystem From eb4c428da618263367bb14c4fc5bfd39bb0f7fdc Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 25 Mar 2022 00:14:18 -0500 Subject: [PATCH 494/549] Fix unordered list styles --- requirements.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5a5a51b65a..57ed0b59fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ -docutils<0.18 -sphinx-tabs -sphinx-copybutton \ No newline at end of file +docutils==0.16 +sphinx>=4.3.0 +sphinx-copybutton +sphinx-rtd-theme>=0.5.1 +sphinx-tabs \ No newline at end of file From e1c987dc5fe5e0993b250158bf690b0380149134 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Sun, 27 Mar 2022 10:54:48 -0500 Subject: [PATCH 495/549] Add latest page (#521) * better highlight for composite/array reference --- docs/admin.rst | 2 + docs/api.rst | 12 ++- docs/errors.rst | 2 +- docs/index.rst | 7 +- docs/releases/latest.rst | 158 +++++++++++++++++++++++++++++++++++++++ postgrest.dict | 1 + 6 files changed, 172 insertions(+), 10 deletions(-) create mode 100644 docs/releases/latest.rst diff --git a/docs/admin.rst b/docs/admin.rst index e7a7448f98..36b9e5e6f2 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -331,6 +331,8 @@ After that, you can enable the service at boot time and start it with: ## For reloading the service ## systemctl restart postgrest +.. _file_descriptors: + File Descriptors ---------------- diff --git a/docs/api.rst b/docs/api.rst index 7acaafa366..d1503fd424 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -269,8 +269,8 @@ Casting the columns is possible by suffixing them with the double colon ``::`` p .. _json_columns: -Array / Composite / 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 `_. @@ -354,8 +354,12 @@ Note that ``->>`` is used to compare ``blood_type`` as ``text``. To compare with { "id": 12, "age": 30 }, { "id": 15, "age": 35 } ] +.. _composite_array_columns: -The arrow operators are also used for array and composite type columns. +Composite / Array Columns +------------------------- + +The arrow operators(``->``, ``->>``) can also be used for accessing composite fields and array elements. .. code-block:: postgres @@ -396,7 +400,7 @@ The arrow operators are also used for array and composite type columns. When using the ``->`` and ``->>`` operators, PostgREST uses a query like ``to_jsonb()->'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')); .. _computed_cols: diff --git a/docs/errors.rst b/docs/errors.rst index 39fa807a43..8024c1109e 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -3,7 +3,7 @@ Error Source ============ -For the most part, error messages will come directly from the database with the same `format that PostgreSQL uses `_, in other words, PostgREST will convert the ``MESSAGE``, ``DETAIL``, ``HINT`` and ``ERRCODE`` from the PostgreSQL error to JSON format and add an HTTP status code to the response (see :ref:`status_codes`). For instance, this is the error you will get when querying a nonexistent table: +For the most part, error messages will come directly from the database with the same `structure that PostgreSQL uses `_, PostgREST will convert the ``MESSAGE``, ``DETAIL``, ``HINT`` and ``ERRCODE`` from the PostgreSQL error to JSON format and add an HTTP status code to the response (see :ref:`status_codes`). For instance, this is the error you will get when querying a nonexistent table: .. code-block:: http diff --git a/docs/index.rst b/docs/index.rst index 9448cd0890..e2caa80962 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -98,6 +98,7 @@ The project has a friendly and growing community. Join our `chat room v9.0.0 releases/v8.0.0 releases/v7.0.1 @@ -230,14 +231,10 @@ PostgREST has a growing ecosystem of examples, libraries, and experiments. Here Release Notes ------------- -Here we'll include the most relevant changes so you can migrate to newer versions easily. -You can see the full changelog of each release in the `PostgREST repository `_. +Changes among versions. - :doc:`releases/v9.0.0` - :doc:`releases/v8.0.0` -- :doc:`releases/v7.0.0` -- :doc:`releases/v6.0.2` -- :doc:`releases/v5.2.0` In Production ------------- diff --git a/docs/releases/latest.rst b/docs/releases/latest.rst new file mode 100644 index 0000000000..101d8fd773 --- /dev/null +++ b/docs/releases/latest.rst @@ -0,0 +1,158 @@ + +Latest +====== + +These are features/bugfixes not yet on a stable version. You can try them by downloading the latest pre-releases `on the GitHub release page `_. + +Features +-------- + +API +~~~ + +Access Composite Type fields and Array elements +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can now :ref:`access fields of a Composite type or elements of an Array type ` with the arrow operators(``->``, ``->>``) in the same way you would access the JSON type fields. + +Improved Error Messages +^^^^^^^^^^^^^^^^^^^^^^^ + +To increase consistency, all the errors messages are now normalized. The ``hint``, ``details``, ``code`` and ``message`` fields will always be present in the body, each one defaulting to a +``null`` value. In the same way, the :ref:`errors that were raised ` with ``SQLSTATE`` now include the ``message`` and ``code`` in the body. + +In addition to these changes and to further clarify the source of an error, PostgREST now adds a ``PGRST`` prefix to the error code of all the errors that are PostgREST-specific and don't come from the database. These errors have a unique code that identifies them and are documented in the :ref:`pgrst_errors` section. + +Alongside these changes, there is now a dedicated reference page for :doc:`Error documentation `. + +Administration +~~~~~~~~~~~~~~ + +Health checks +^^^^^^^^^^^^^ + +Admins can now benefit from two :ref:`health check endpoints ` exposed in a different port than the main app. When activated, the ``live`` and ``ready`` endpoints are available to verify if PostgREST is alive and running or if the database connection and the :ref:`schema cache ` are ready for querying. + +Logging users +^^^^^^^^^^^^^ + +You can now verify the current authenticated database user in the :ref:`request log ` on stdout. + +Run without configuration +^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is now possible to execute PostgREST without specifying any configuration variable, even without the three that were mandatory + + - If :ref:`db-uri` is not set, PostgREST will use the `libpq environment variables `_ for the database connection. + - If :ref:`db-schemas` is not set, it will use the database ``public`` schema. + - If :ref:`db-anon-role` is not set, it will not allow anonymous requests. + +Documentation improvements +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Added a :doc:`/how-tos/working-with-postgresql-data-types` how-to, which contains explanations and examples on how to work with different PostgreSQL data types such as timestamps, ranges or PostGIS types, among others. + +* Added in-database and environment variable settings for each :ref:`configuration variable `. + +* Added the :ref:`file_descriptors` subsection. + +* Moved the :ref:`error_source` and the :ref:`status_codes` sections to the :doc:`errors reference page `. + +* Moved the *Casting type to custom JSON* how-to to the :ref:`casting_range_to_json` subsection. + +* Removed direct links for PostgREST versions older than 8.0 from the versions menu. + +* Removed the deprecated *Embedding table from another schema* how-to. + +Bug fixes +--------- + +* Execute deferred constraint triggers when using ``Prefer: tx=rollback`` (`#2020 `_) + +* Return ``204 No Content`` without ``Content-Type`` for ``PUT`` (`#2058 `_) + +* Fix ``is`` not working with upper or mixed case values like ``NULL, TrUe, FaLsE`` (`#2077 `_) + +* Fix schema cache loading when views with ``XMLTABLE`` and ``DEFAULT`` are present (`#2024 `_) + +* Fix wrong CORS header Authentication -> Authorization (`#1724 `_) + +* Clarify error for failed schema cache load. (`#2107 `_) + + - From ``Database connection lost. Retrying the connection`` to ``Could not query the database for the schema cache. Retrying.`` + +* Fix reading database configuration properly when ``=`` is present in its value (`#2120 `_) + +* Fix silently ignoring filter on a non-existent embedded resource (`#1771 `_) + +* Remove trigger functions from schema cache and OpenAPI output, because they can't be called directly anyway. (`#2135 `_) + +* Remove aggregates, procedures and window functions from the schema cache and OpenAPI output. (`#2101 `_) + +* Remove functions, which are not callable due to unnamed arguments, from schema cache and OpenAPI output. (`#2152 `_) + +* Fix accessing JSON array fields with ``->`` and ``->>`` in ``?select=`` and ``?order=``. (`#2145 `_) + +* Fix ``--dump-schema`` running with a wrong PG version. (`#2153 `_) + +* Keep working when ``EMFILE (Too many open files)`` is reached. (`#2042 `_) + +* Ignore ``Content-Type`` headers for ``GET`` requests when calling RPCs. Previously, ``GET`` without parameters, but with ``Content-Type: text/plain`` or ``Content-Type: application/octet-stream`` would fail with ``404 Not Found``, even if a function without arguments was available. (`#2147 `_) + +Breaking changes +---------------- + +* Return ``204 No Content`` without ``Content-Type`` for RPCs returning ``VOID`` (`#2001 `_) + + - Previously, those RPCs would return ``null`` as a body with ``Content-Type: application/json``. + +Thanks +------ + +Big thanks from the `PostgREST team `_ to our 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/2ndquadrant.png + :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo + :width: 13em + + .. image:: ../_static/retool.png + :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + + .. image:: ../_static/gnuhost.png + :target: https://gnuhost.eu/?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/oblivious.jpg + :target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + +* Evans Fernandes +* `Jan Sommer `_ +* `Franz Gusenbauer `_ +* `Daniel Babiak `_ +* Tsingson Qin +* Michel Pelletier +* Jay Hannah +* Robert Stolarz +* Nicholas DiBiase +* Christopher Reid +* Nathan Bouscal +* Daniel Rafaj +* David Fenko +* Remo Rechkemmer +* Severin Ibarluzea +* Tom Saleeba +* Pawel Tyll + +If you like to join them please consider `supporting PostgREST development `_. diff --git a/postgrest.dict b/postgrest.dict index a20158ba4d..f9cc140fc9 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -14,6 +14,7 @@ Beles booleans Bouscal buildpack +bugfixes Cardano cd centric From 06156eb79796249e5f425cd9ff055d2a71fece64 Mon Sep 17 00:00:00 2001 From: Christopher Ahlers <11285058+FooBar1969@users.noreply.github.com> Date: Mon, 4 Apr 2022 14:48:29 -0700 Subject: [PATCH 496/549] Update ecosystem.rst Fixed incorrect link for the swift library in Client-Side Libraries section. It now links properly to: https://github.com/supabase-community/postgrest-swift --- docs/ecosystem.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index 10923ab158..6ae4e1c9f4 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -117,7 +117,7 @@ Client-Side Libraries * `postgrest-request `_ - JS, SuperAgent * `postgrest-rs `_ - Rust * `postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp -* `postgrest-swift `_ - Swift +* `postgrest-swift `_ - Swift * `postgrest-url `_ - JS, just for generating query URLs * `postgrest_python_requests_client `_ - Python * `postgrester `_ - JS + Typescript From 6f79b41bc12814f75ebc19956eb930fbc50e88b5 Mon Sep 17 00:00:00 2001 From: Michael Kane Juncker Date: Tue, 5 Apr 2022 19:03:35 +0200 Subject: [PATCH 497/549] feat(api): add match/imatch operators for regular expression support - refs PostgREST/postgrest#2236 --- docs/api.rst | 13 ++++++++++++- postgrest.dict | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index d1503fd424..7ae36a41a3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -69,6 +69,8 @@ lte :code:`<=` less than or equal neq :code:`<>` or :code:`!=` not equal like :code:`LIKE` LIKE operator (use * in place of %) ilike :code:`ILIKE` ILIKE operator (use * in place of %) +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")` @@ -146,6 +148,15 @@ You can also apply complex logic to the conditions: curl "http://localhost:3000/people?grade=gte.90&student=is.true&or=(age.eq.14,not.and(age.gte.11,age.lte.17))" +.. _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 `__. + +To ensure best performance on larger data sets, an `appropriate 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 @@ -272,7 +283,7 @@ Casting the columns is possible by suffixing them with the double colon ``::`` p JSON Columns ------------ -You can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `_. +You can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `__. .. code-block:: postgres diff --git a/postgrest.dict b/postgrest.dict index f9cc140fc9..f9861cce23 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -60,6 +60,7 @@ HTTPS HV Ibarluzea ilike +imatch io IP JS From f6c68ddafb83ccd4ff33b47973081f6de647be43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Apr 2022 12:34:22 +0000 Subject: [PATCH 498/549] Bump cachix/install-nix-action from 16 to 17 Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 16 to 17. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/v16...v17) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index fb815e2ccf..865683ba16 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v16 + - uses: cachix/install-nix-action@v17 - run: nix-env -f default.nix -iA build - run: postgrest-docs-build @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v16 + - uses: cachix/install-nix-action@v17 - run: nix-env -f default.nix -iA spellcheck - run: postgrest-docs-spellcheck @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v16 + - uses: cachix/install-nix-action@v17 - run: nix-env -f default.nix -iA linkcheck - run: postgrest-docs-linkcheck From 72bffddbf18eac309216323a524f8d56ef458d04 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 24 Mar 2022 18:21:13 +0100 Subject: [PATCH 499/549] Add limited updates/deletions Also move updates to its own section --- docs/api.rst | 101 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 71 insertions(+), 30 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 7ae36a41a3..1b886af09d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1479,10 +1479,10 @@ Hints also work alongside ``!inner`` if a top level filtering is needed. From th curl "http://localhost:3000/orders?select=*,central_addresses!billing_address!inner(*)¢ral_addresses.code=AB1000" -.. _insert_update: +.. _insert: -Insertions / Updates -==================== +Insertions +========== All tables and `auto-updatable views `_ can be modified through the API, subject to permissions of the requester's database role. @@ -1537,28 +1537,6 @@ URL encoded payloads can be posted with ``Content-Type: application/x-www-form-u 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. -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. - -.. tabs:: - - .. code-tab:: http - - PATCH /people?age=lt.13 HTTP/1.1 - - { "category": "child" } - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people?age=lt.13" \ - -X PATCH -H "Content-Type: application/json" \ - -d '{ "category": "child" }' - -Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. - -.. warning:: - - Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`. - .. _bulk_insert: Bulk Insert @@ -1621,8 +1599,7 @@ To bulk insert JSON post an array of objects having all-matching keys Specifying Columns ------------------ -By using the :code:`columns` query parameter it's possible to specify the payload keys that will be inserted/updated -and ignore the rest of the payload. +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. .. tabs:: @@ -1662,10 +1639,37 @@ In this case, only **source**, **publication_date** and **figure** will be inser 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: + +Updates +======= + +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. + +.. tabs:: + + .. code-tab:: http + + PATCH /people?age=lt.13 HTTP/1.1 + + { "category": "child" } + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?age=lt.13" \ + -X PATCH -H "Content-Type: application/json" \ + -d '{ "category": "child" }' + +Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. + +.. warning:: + + Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`. + .. _upsert: UPSERT ------- +====== You can make an UPSERT with :code:`POST` and the :code:`Prefer: resolution=merge-duplicates` header: @@ -1703,7 +1707,7 @@ By default, UPSERT operates based on the primary key columns, you must specify a .. _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. @@ -1736,7 +1740,7 @@ By specifying the ``on_conflict`` query parameter, you can make UPSERT work on a .. _upsert_put: PUT -~~~ +--- A single row UPSERT can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: @@ -1795,6 +1799,43 @@ Deletions also support :code:`Prefer: return=representation` plus :ref:`v_filter Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`. +Limited Updates/Deletions +========================= + +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). + +.. tabs:: + + .. code-tab:: http + + PATCH /users?limit=10&order=id&last_login=lt.2017-01-01 HTTP/1.1 + + { "status": "inactive" } + + .. code-tab:: bash Curl + + curl -X PATCH "/users?limit=10&order=id&last_login=lt.2020-01-01" \ + -H "Content-Type: application/json" \ + -d '{ "status": "inactive" }' + +.. tabs:: + + .. code-tab:: http + + DELETE /users?limit=10&order=id&status=eq.inactive HTTP/1.1 + + .. code-tab:: bash Curl + + 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 `_ 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 `_. + .. _custom_queries: Custom Queries From 5f3284eed0682116d3c8e4e06ff115585902cccd Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 24 Mar 2022 18:41:11 +0100 Subject: [PATCH 500/549] fix broken links --- docs/api.rst | 2 +- docs/ecosystem.rst | 2 +- docs/errors.rst | 2 +- docs/install.rst | 2 +- docs/releases/v8.0.0.rst | 4 ++-- docs/schema_cache.rst | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 1b886af09d..727a2d315f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1271,7 +1271,7 @@ A request with ``directors`` embedded: Embedding after Insertions/Updates/Deletions -------------------------------------------- -You can embed related resources after doing :ref:`insert_update` or :ref:`delete`. +You can embed related resources after doing :ref:`insert`, :ref:`update` or :ref:`delete`. Say you want to insert a **film** and then get some of its attributes plus embed its **director**. diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index 6ae4e1c9f4..8283d6c698 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -22,7 +22,7 @@ Templates --------- * `compose-postgrest `_ - docker-compose setup with Nginx and HTML example -* `svelte-postgrest-template `_ - Svelte/SvelteKit, PostgREST, EveryLayout and social auth - `blog post `_ +* `svelte-postgrest-template `_ - Svelte/SvelteKit, PostgREST, EveryLayout and social auth .. _eco_example_apps: diff --git a/docs/errors.rst b/docs/errors.rst index 8024c1109e..950c9a9fa2 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -150,7 +150,7 @@ Related to the HTTP request elements. | PGRST101 | | +---------------+-------------------------------------------------------------+ | .. _pgrst102: | Related to the request body structure. | -| | See :ref:`insert_update`. | +| | See :ref:`insert` and :ref:`update`. | | PGRST102 | | +---------------+-------------------------------------------------------------+ | .. _pgrst103: | Related to :ref:`limits`. | diff --git a/docs/install.rst b/docs/install.rst index 5ab43ba111..c68ae15863 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -48,7 +48,7 @@ You can also use your OS package manager. .. group-tab:: Windows - You can install PostgREST using `Chocolatey `_ or `Scoop `_. + You can install PostgREST using `Chocolatey `_ or `Scoop `_. .. code:: bash diff --git a/docs/releases/v8.0.0.rst b/docs/releases/v8.0.0.rst index b99bd695f3..bdaea49162 100644 --- a/docs/releases/v8.0.0.rst +++ b/docs/releases/v8.0.0.rst @@ -25,7 +25,7 @@ Added * Allow schema cache reloading using PostgreSQL :ref:`NOTIFY ` command. This enables :ref:`auto_schema_reloading`. |br| -- `@steve-chavez `_ -* Allow sending the header ``Prefer: headers-only`` to get a response with a ``Location`` header. See :ref:`insert_update`. +* Allow sending the header ``Prefer: headers-only`` to get a response with a ``Location`` header. See :ref:`insert`. |br| -- `@laurenceisla `_ * Allow :ref:`external_connection_poolers` such as PgBouncer in transaction pooling mode. @@ -78,7 +78,7 @@ Changed |br| -- `@steve-chavez `_ * POST requests for insertions no longer include a ``Location`` header in the response by default and behave the same way as having a - ``Prefer: return=minimal`` header in the request. This prevents permissions errors when having a write-only table. See :ref:`insert_update`. + ``Prefer: return=minimal`` header in the request. This prevents permissions errors when having a write-only table. See :ref:`insert`. |br| -- `@laurenceisla `_ * Modified the default logging level from ``info`` to ``error``. See :ref:`log-level`. diff --git a/docs/schema_cache.rst b/docs/schema_cache.rst index 9aa1f3a13a..5e5fce3a6a 100644 --- a/docs/schema_cache.rst +++ b/docs/schema_cache.rst @@ -16,7 +16,7 @@ in order to avoid repeating this work, PostgREST uses a schema cache. +--------------------------------------------+-------------------------------------------------------------------------------+ | :ref:`Upserts ` | Primary keys | +--------------------------------------------+-------------------------------------------------------------------------------+ -| :ref:`Insertions ` | Primary keys (optional: only if the Location header is requested) | +| :ref:`Insertions ` | Primary keys (optional: only if the Location header is requested) | +--------------------------------------------+-------------------------------------------------------------------------------+ | :ref:`OPTIONS requests ` | View INSTEAD OF TRIGGERS and primary keys | +--------------------------------------------+-------------------------------------------------------------------------------+ From 9c23ac2f39b3d4f0699f09fdbe51500c892a8ebb Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 2 May 2022 23:14:24 -0500 Subject: [PATCH 501/549] Reorganize error groups --- docs/api.rst | 2 + docs/errors.rst | 109 ++++++++++++++++++++---------------------------- postgrest.dict | 4 +- 3 files changed, 50 insertions(+), 65 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 727a2d315f..2eb7e7cd97 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1799,6 +1799,8 @@ Deletions also support :code:`Prefer: return=representation` plus :ref:`v_filter Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`. +.. _limited_update_delete: + Limited Updates/Deletions ========================= diff --git a/docs/errors.rst b/docs/errors.rst index 950c9a9fa2..248051f98c 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -177,6 +177,42 @@ Related to the HTTP request elements. | | specified in the ``select`` part of the query string. | | PGRST108 | See :ref:`embed_filters`. | +---------------+-------------------------------------------------------------+ +| .. _pgrst109: | Restricting a Deletion or an Update using limits must | +| | include the ordering of a unique column. | +| PGRST109 | See :ref:`limited_update_delete`. | ++---------------+-------------------------------------------------------------+ +| .. _pgrst110: | 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: | Related to :ref:`guc_resp_hdrs`. | +| | | +| PGRST111 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst112: | The status code must be a positive integer. | +| | See :ref:`guc_resp_status`. | +| PGRST112 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst113: | Related to :ref:`binary_output`. See :ref:`providing_img` | +| | for an example on requesting images. | +| PGRST113 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst114: | For an :ref:`UPSERT using PUT `, when | +| | :ref:`limits and offsets ` are used. | +| PGRST114 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst115: | For an :ref:`UPSERT using PUT `, when the | +| | primary key in the query string and the body are different. | +| PGRST115 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst116: | More than 1 or no items where returned when requesting | +| | a singular response. See :ref:`singular_plural`. | +| PGRST116 | | ++---------------+-------------------------------------------------------------+ +| .. _pgrst117: | The HTTP verb used in the request in not supported. | +| | | +| PGRST117 | | ++---------------+-------------------------------------------------------------+ .. _pgrst2**: @@ -209,8 +245,8 @@ Related to a :ref:`stale schema cache `. Most of the time, these e .. _pgrst3**: -Group 3 - JWT errors --------------------- +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 ` for more information on this process. @@ -230,72 +266,19 @@ Related to the authentication process using JWT. You can follow the :ref:`tut1` | PGRST302 | is disabled by not setting it in :ref:`db-anon-role`. | +---------------+-------------------------------------------------------------+ -.. _pgrst4**: +.. The Internal Errors Group X** is always at the end -Group 4 - Hasql ---------------- +.. _pgrst_X**: -Related to `the library `_ that PostgREST uses to connect to the database. If you encounter any of these errors, you may have stumbled on a PostgREST bug, please `open an issue `_ and we'll be glad to fix it. +Group X - Internal +------------------ -+---------------+-------------------------------------------------------------+ -| Code | Description | -+===============+=============================================================+ -| .. _pgrst400: | Internal error: Unexpected Result. | -| | | -| PGRST400 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst401: | Internal error: Attempted to parse more columns than | -| | there are in the result. | -| PGRST401 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst402: | Internal error: Attempted to parse a NULL as some value. | -| | | -| PGRST402 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst403: | Internal error: Wrong value parser used. | -| | | -| PGRST403 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst404: | Internal error: Unexpected amount of rows. | -| | | -| PGRST404 | | -+---------------+-------------------------------------------------------------+ - -.. _pgrst5**: - -Group 5 - General ------------------ - -These are uncategorized errors. +Internal errors mostly related to `the library `_ that PostgREST uses to connect to the database. If you encounter any of these errors, you may have stumbled on a PostgREST bug, please `open an issue `_ and we'll be glad to fix it. +---------------+-------------------------------------------------------------+ | Code | Description | +===============+=============================================================+ -| .. _pgrst500: | Related to :ref:`guc_resp_hdrs`. | -| | | -| PGRST500 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst501: | The status code must be a positive integer. | -| | See :ref:`guc_resp_status`. | -| PGRST501 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst502: | Related to :ref:`binary_output`. See :ref:`providing_img` | -| | for an example on requesting images. | -| PGRST502 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst503: | For an :ref:`UPSERT using PUT `, when | -| | :ref:`limits and offsets ` are used. | -| PGRST503 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst504: | For an :ref:`UPSERT using PUT `, when the | -| | primary key in the query string and the body are different. | -| PGRST504 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst505: | More than 1 or no items where returned when requesting | -| | a singular response. See :ref:`singular_plural`. | -| PGRST505 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst506: | The HTTP verb used in the request in not supported. | -| | | -| PGRST506 | | +| .. _pgrstX00: | Internal errors related to the library that connects to the | +| | database. | +| PGRSTX00 | | +---------------+-------------------------------------------------------------+ diff --git a/postgrest.dict b/postgrest.dict index f9861cce23..5a3f172710 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -50,7 +50,6 @@ GUC gucs Gumbs Haskell -Hasql Heroku HMAC Homebrew @@ -108,6 +107,8 @@ PgBouncer pgcrypto pgjwt pgrst +pgrstX +PGRSTX pgSQL phfts phraseto @@ -174,7 +175,6 @@ Tyll TypeScript UI ui -uncategorized unicode unix updatable From 6e24e23b4b99ed1232aa82869933e0211428db20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz-Josef=20F=C3=A4rber?= Date: Thu, 28 Apr 2022 10:31:32 +0200 Subject: [PATCH 502/549] Allow returning XML --- docs/api.rst | 84 +++++++++++++++++++--------------------- docs/configuration.rst | 6 +-- docs/errors.rst | 5 ++- docs/releases/v6.0.2.rst | 2 +- 4 files changed, 46 insertions(+), 51 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 2eb7e7cd97..3f95f1dcad 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -801,7 +801,13 @@ The current possibilities are: * ``text/csv`` * ``application/json`` * ``application/openapi+json`` + +and in the special case of a single-column select the following additional three formats; +also see the section :ref:`scalar_return_formats`: + * ``application/octet-stream`` +* ``text/plain`` +* ``text/xml`` The server will default to JSON for API endpoints and OpenAPI on the root. @@ -2216,6 +2222,9 @@ PostgREST will detect if the function is scalar or table-valued and will shape t { "title": "Blade Runner 2049", "rating": 8.1} ] +To manually choose a return format such as binary, plain text or XML, see the section :ref:`scalar_return_formats`. + + .. _bulk_call: Bulk Call @@ -2319,12 +2328,25 @@ You can call overloaded functions with different number of arguments. Overloaded functions with the same argument names but different types are not supported. -.. _binary_output: +.. _scalar_return_formats: + +Response Formats For Scalar Responses +===================================== + +For scalar return values such as + +* single-column selects on tables or +* scalar functions, -Binary Output -============= +you can set the additional content types -If you want to return raw binary data from a :code:`bytea` column, you must specify :code:`application/octet-stream` as part of the :code:`Accept` header +* ``application/octet-stream`` +* ``text/plain`` +* ``text/xml`` + +as part of the :code:`Accept` header. + +Example 1: If you want to return raw binary data from a :code:`bytea` column, you must specify :code:`application/octet-stream` as part of the :code:`Accept` header and select a single column :code:`?select=bin_data`. .. tabs:: @@ -2339,74 +2361,46 @@ and select a single column :code:`?select=bin_data`. curl "http://localhost:3000/items?select=bin_data&id=eq.1" \ -H "Accept: application/octet-stream" -You can also request binary output when calling `Stored Procedures`_ and since they can return a scalar value you are not forced to use :code:`select` -for this case. +Example 2: You can request XML output when calling `Stored Procedures`_ that return a scalar value of type ``text/xml``. You are not forced to use select for this case. .. code-block:: postgres - CREATE FUNCTION closest_point(..) RETURNS bytea .. + CREATE FUNCTION generate_xml_content(..) RETURNS xml .. .. tabs:: .. code-tab:: http - POST /rpc/closest_point HTTP/1.1 - Accept: application/octet-stream + POST /rpc/generate_xml_content HTTP/1.1 + Accept: text/xml .. code-tab:: bash Curl - curl "http://localhost:3000/rpc/closest_point" \ - -X POST -H "Accept: application/octet-stream" + curl "http://localhost:3000/rpc/generate_xml_content" \ + -X POST -H "Accept: text/xml" -If the stored procedure returns non-scalar values, you need to do a :code:`select` in the same way as for GET binary output. +Example 3: If the stored procedure returns non-scalar values, you need to do a :code:`select` in the same way as for GET binary output. .. code-block:: sql - CREATE FUNCTION overlapping_regions(..) RETURNS SETOF TABLE(geom_twkb bytea, ..) .. + CREATE FUNCTION get_descriptions(..) RETURNS SETOF TABLE(id int, description text) .. .. tabs:: .. code-tab:: http - POST /rpc/overlapping_regions?select=geom_twkb HTTP/1.1 - Accept: application/octet-stream - - .. code-tab:: bash Curl - - curl "http://localhost:3000/rpc/overlapping_regions?select=geom_twkb" \ - -X POST -H "Accept: application/octet-stream" - -.. note:: - - If more than one row would be returned the binary results will be concatenated with no delimiter. - -.. _plain_text_output: - -Plain Text Output ------------------ - -You can get raw output from a ``text`` column by using ``Accept: text/plain``. - -.. tabs:: - - .. code-tab:: http - - GET /workers?select=custom_psv_format HTTP/1.1 + POST /rpc/get_descriptions?select=description HTTP/1.1 Accept: text/plain .. code-tab:: bash Curl - curl "http://localhost:3000/workers?select=custom_psv_format" \ - -H "Accept: text/plain" + curl "http://localhost:3000/rpc/get_descriptions?select=description" \ + -X POST -H "Accept: text/plain" -.. code-block:: text +.. note:: - 09310817|JOHN|DOE|15/04/88| - 42152780|FRED|BLOGGS|20/02/85| - 43006541|OTTO|NORMALVERBRAUCHER|01/07/90| - 02452492|ERIKA|MUSTERMANN|11/01/80| + If more than one row would be returned the binary/plain-text/xml results will be concatenated with no delimiter. -This follows the same rules as :ref:`binary_output`. .. _open-api: diff --git a/docs/configuration.rst b/docs/configuration.rst index 57c22199b5..9d2a336197 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -587,14 +587,14 @@ raw-media-types This serves to extend the `Media Types `_ that PostgREST currently accepts through an ``Accept`` header. - These media types can be requested by following the same rules as the ones defined in :ref:`binary_output`. + These media types can be requested by following the same rules as the ones defined in :ref:`scalar_return_formats`. As an example, the below config would allow you to request an **image** and a **XML** file by doing a request with ``Accept: image/png`` - or ``Accept: text/xml``, respectively. + or ``Accept: font/woff2``, respectively. .. code:: bash - raw-media-types="image/png, text/xml" + raw-media-types="image/png, font/woff2" .. _server-host: diff --git a/docs/errors.rst b/docs/errors.rst index 248051f98c..08782f4ecc 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -193,8 +193,9 @@ Related to the HTTP request elements. | | See :ref:`guc_resp_status`. | | PGRST112 | | +---------------+-------------------------------------------------------------+ -| .. _pgrst113: | Related to :ref:`binary_output`. See :ref:`providing_img` | -| | for an example on requesting images. | +| .. _pgrst113: | Related to :ref:`scalar_return_formats`. | +| | See :ref:`providing_img` for an example on requesting | +| | images. | | PGRST113 | | +---------------+-------------------------------------------------------------+ | .. _pgrst114: | For an :ref:`UPSERT using PUT `, when | diff --git a/docs/releases/v6.0.2.rst b/docs/releases/v6.0.2.rst index 88a73ee5c8..f96550515c 100644 --- a/docs/releases/v6.0.2.rst +++ b/docs/releases/v6.0.2.rst @@ -23,7 +23,7 @@ Added * Bulk calling an RPC is now allowed. See :ref:`bulk_call`. |br| -- `@steve-chavez `_ -* It's now possible to request a ``text/plain`` output. See :ref:`plain_text_output`. +* It's now possible to request a ``text/plain`` output. See :ref:`scalar_return_formats`. |br| -- `@steve-chavez `_ * Config option for specifying PostgREST database pool timeout. See :ref:`db-pool-timeout`. From 496a1734eca3163f20ccdfa4336462c51ccbda62 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 18 May 2022 20:50:06 -0500 Subject: [PATCH 503/549] Add documentation on embedding views containing joins --- docs/api.rst | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 3f95f1dcad..cedbd4aa27 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1227,6 +1227,58 @@ It's also possible to embed `Materialized Views Date: Thu, 19 May 2022 16:44:31 -0500 Subject: [PATCH 504/549] Fix main explanation on views with joins (#537) * Fix indentation and example view * Change films.id to just id --- docs/api.rst | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index cedbd4aa27..5b30e6bd11 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1230,54 +1230,52 @@ It's also possible to embed `Materialized Views Date: Thu, 19 May 2022 20:37:27 -0500 Subject: [PATCH 505/549] Add how to work with array columns (#538) --- .../working-with-postgresql-data-types.rst | 133 +++++++++++++++++- 1 file changed, 131 insertions(+), 2 deletions(-) diff --git a/docs/how-tos/working-with-postgresql-data-types.rst b/docs/how-tos/working-with-postgresql-data-types.rst index 06276f6c79..c5095bd9f6 100644 --- a/docs/how-tos/working-with-postgresql-data-types.rst +++ b/docs/how-tos/working-with-postgresql-data-types.rst @@ -127,6 +127,135 @@ Now, you can insert a new product using a JSON object for the ``extra_info`` col To query and filter the data see :ref:`json_columns` for a complete reference. +Arrays +------ + +To handle `array types `_ you can use string representation or JSON array format. For instance, let's create the following table: + +.. code-block:: postgres + + create table movies ( + id int primary key, + title text not null, + tags text[], + performance_times time[] + ); + +To insert a new value you can use string representation. + +.. tabs:: + + .. code-tab:: http + + POST /movies HTTP/1.1 + Content-Type: application/json + + { + "id": 1, + "title": "Paddington", + "tags": "{family,comedy,not streamable}", + "performance_times": "{12:40,15:00,20:00}" + } + + .. code-tab:: bash Curl + + 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 data using a JSON array format. The following request sends the same data as the example above: + +.. tabs:: + + .. code-tab:: http + + POST /movies HTTP/1.1 + Content-Type: application/json + + { + "id": 1, + "title": "Paddington", + "tags": ["family", "comedy", "not streamable"], + "performance_times": ["12:40", "15:00", "20:00"] + } + + .. code-tab:: bash Curl + + 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 the arrow operators. See :ref:`composite_array_columns`. + +Multidimensional Arrays +~~~~~~~~~~~~~~~~~~~~~~~ + +Handling multidimensional arrays is no different than handling one-dimensional ones: both the string representation and the JSON array format are allowed. For example, let's add a new column to the table: + +.. code-block:: postgres + + -- The column stores the cinema, floor and auditorium numbers in that order + alter table movies + add column cinema_floor_auditorium int[][][]; + +Now, let's update the row we inserted before using JSON array format: + +.. tabs:: + + .. code-tab:: http + + PATCH /movies?id=eq.1 HTTP/1.1 + Content-Type: application/json + + { + "cinema_floor_auditorium": [ [ [1,2], [6,7] ], [ [3,5], [8,9] ] ] + } + + .. code-tab:: bash Curl + + 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 + +Now, 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: + +.. tabs:: + + .. code-tab:: http + + GET /movies?select=title,auditorium:cinema_floor_auditorium->0->1&id=eq.1 HTTP/1.1 + + .. code-tab:: bash Curl + + 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 --------------- @@ -203,7 +332,7 @@ Or, you could insert the data in JSON format. The following request is equivalen } EOF -You can also query data using the arrow operators as you would for :ref:`JSON columns `. +You can also query data using the arrow operators. See :ref:`composite_array_columns`. Ranges ------ @@ -391,7 +520,7 @@ You can also query and filter the value of a ``hstore`` column using the arrow o [{ "native": "مصر" }] PostGIS ------------------- +------- You can use the string representation for `PostGIS `_ data types such as ``geometry`` or ``geography``. As an example, let's create a table using the ``geometry`` type (you need to `install PostGIS `_ first). From 06956c1994264d7c1b7c23d0d55ca04d95d612c5 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Tue, 24 May 2022 20:36:17 -0500 Subject: [PATCH 506/549] Add explicit use of special timestamp values --- docs/how-tos/working-with-postgresql-data-types.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/how-tos/working-with-postgresql-data-types.rst b/docs/how-tos/working-with-postgresql-data-types.rst index c5095bd9f6..c439301d04 100644 --- a/docs/how-tos/working-with-postgresql-data-types.rst +++ b/docs/how-tos/working-with-postgresql-data-types.rst @@ -58,17 +58,17 @@ Someone located in Cairo can retrieve the data using their local time, too: The response has the date in the time zone configured by the server: ``UTC -05:00``. -You can use other comparative filters and also `PostgreSQL special date/time input values `_. For instance, to get the reports that are due after today you would do: +You can use other comparative filters and also all the `PostgreSQL special date/time input values `_ as illustrated in this example: .. tabs:: .. code-tab:: http - GET /reports?due_date=gt.today HTTP/1.1 + GET /reports?or=(and(due_date.gte.today,due_date.lte.tomorrow),and(due_date.gt.-infinity,due_date.lte.epoch)) HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/reports?due_date=gt.today" + 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 From 209b70cbfca8b23f7cf0f0b8b9d5822079e6aeff Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 25 May 2022 17:29:52 -0500 Subject: [PATCH 507/549] Add disclaimer on $ usage in jwt-secret values (#543) --- docs/configuration.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/configuration.rst b/docs/configuration.rst index 9d2a336197..66061904b4 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -480,6 +480,10 @@ jwt-secret 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 From 96c169e593b5ef75a461da5a15a0e6e3fac68602 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 27 May 2022 17:12:47 -0500 Subject: [PATCH 508/549] Add bytea to working with types and improve img example (#542) --- docs/how-tos/providing-images-for-img.rst | 82 ++++++------------- .../working-with-postgresql-data-types.rst | 78 ++++++++++++++++++ docs/index.rst | 3 +- postgrest.dict | 1 + 4 files changed, 106 insertions(+), 58 deletions(-) diff --git a/docs/how-tos/providing-images-for-img.rst b/docs/how-tos/providing-images-for-img.rst index 989e197494..f1d892e0d6 100644 --- a/docs/how-tos/providing-images-for-img.rst +++ b/docs/how-tos/providing-images-for-img.rst @@ -5,25 +5,18 @@ Providing images for ```` :author: `pkel `_ -In this how-to, you will learn how to create an endpoint for providing images to HTML :code:`` tags without client side JavaScript. -The resulting HTML might look like this: +In this how-to, you will learn how to create an endpoint for providing images to HTML :code:`` tags without client side JavaScript. In fact, the presented technique is suitable for providing not only images, but arbitrary files. -.. code-block:: html - - Cute Kittens +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. -In fact, the presented technique is suitable for providing not only images, but arbitrary files. +.. warning:: -We will start with a minimal example that highlights the general concept. -Afterwards we present are more detailed solution that fixes a few shortcomings of the first approach. + 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 `_. Minimal Example --------------- -PostgREST returns binary data on requests that set the :code:`Accept: application/octet-stream` header. -The general idea is to configure the reverse proxy in front of the API to set this header for all requests to :code:`/files/`. -We will show how to achieve this using Nginx. - First, we need a public table for storing the files. .. code-block:: postgres @@ -36,64 +29,40 @@ First, we need a public table for storing the files. 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 requesting :code:`/files?select=blob&id=eq.42` with the :code:`Accept: application/octet-stream` header. Unfortunately, putting the URL into the :code:`src` of an :code:`` tag will not work. -That's because browsers do not send the required header. - -Luckily, we can configure our :doc:`Nginx reverse proxy <../admin>` to fix this problem for us. -We assume that PostgREST is running on port 3000. -We provide a new location :code:`/files/` that redirects requests to our endpoint with the :code:`Accept` header set to :code:`application/octet-stream`. - -.. code-block:: nginx - - server { - # rest of reverse proxy and web server configuration - ... - - location /files/ { - # /files//* ---> /files?select=blob&id=eq. - rewrite /files/([^/]+).* /files?select=blob&id=eq.$1 break; - # if id is missing - return 404; - # request binary output - proxy_set_header Accept application/octet-stream; - # usual proxy setup - 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://localhost:3000/; - } - -With this setup, we can request the cat image at :code:`localhost/files/42/cats.jpeg` without setting any headers. -In fact, you can replace :code:`cats.jpeg` with any other filename or simply omit it. -Putting the URL into the :code:`src` of an :code:`` tag should now work as expected. +That's because browsers do not send the required :code:`Accept: application/octet-stream` header. + +Luckily we can specify the accepted media types in the :ref:`raw-media-types` configuration variable. +In this case, the :code:`Accept: image/webp` header is sent by many web browsers by default, so let's add it to the configuration variable, like this: :code:`raw-media-types="image/webp"`. +Now, the image will be displayed in the HTML page: + +.. code-block:: html + + Cute Kittens Improved Version ---------------- The basic solution has some shortcomings: -1. The response :code:`Content-Type` header is set to :code:`application/octet-stream`. - This might confuse clients and users. -2. Download requests (e.g. Right Click -> Save Image As) to :code:`files/42` will propose :code:`42` as filename. +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, we store the media types and names of our files in the database. +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 - create table files( - id int primary key - , type text - , name text - , blob bytea - ); + alter table files + add column type text, + add column name text; Next, we set up an RPC endpoint that sets 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 reverse proxy. +For production, you probably want to configure additional caches, e.g. on the :ref:`reverse proxy `. .. code-block:: postgres @@ -120,9 +89,8 @@ For production, you probably want to configure additional caches, e.g. on the re end $$ language plpgsql; -With this, we can obtain the cat image from :code:`/rpc/file?id=42`. -Consequently, we have to replace our previous rewrite rule in the Nginx recipe with the following. +With this, we can obtain the cat image from :code:`/rpc/file?id=42`. Thus, the resulting HTML will be: -.. code-block:: nginx +.. code-block:: html - rewrite /files/([^/]+).* /rpc/file?id=$1 break; + Cute Kittens diff --git a/docs/how-tos/working-with-postgresql-data-types.rst b/docs/how-tos/working-with-postgresql-data-types.rst index c439301d04..9cfcf52794 100644 --- a/docs/how-tos/working-with-postgresql-data-types.rst +++ b/docs/how-tos/working-with-postgresql-data-types.rst @@ -5,6 +5,10 @@ Working with PostgreSQL data types 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 ---------- @@ -461,6 +465,80 @@ Finally, do the request :ref:`casting the range column `: 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 `_. For example, let's create a table that will save some files and a function that inserts data to that table: + +.. 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; + +Next, let's use 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: + +.. tabs:: + + .. code-tab:: http + + POST /rpc/upload_binary HTTP/1.1 + Content-Type: application/octet-stream + + postgrest-logo.png + + .. code-tab:: bash Curl + + 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, you will need to set the ``Accept: application/octet-stream`` header in the request and select only the +``bytea`` column. + +.. tabs:: + + .. code-tab:: http + + GET /files?select=file&id=eq.1 HTTP/1.1 + Accept: application/octet-stream + + .. code-tab:: bash Curl + + curl "http://localhost:3000/files?select=file&id=eq.1" \ + -H "Accept: application/octet-stream" + +You can also use more accurate headers depending on the type of the files by using the :ref:`raw-media-types` configuration. For example, adding the ``raw-media-types="image/png"`` setting to the configuration file will allow you to use the ``Accept: image/png`` header: + +.. tabs:: + + .. code-tab:: http + + GET /files?select=file&id=eq.1 HTTP/1.1 + Accept: image/png + + .. code-tab:: bash Curl + + curl "http://localhost:3000/files?select=file&id=eq.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 `_. + hstore ------ diff --git a/docs/index.rst b/docs/index.rst index e2caa80962..c26e2477af 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -203,7 +203,8 @@ These are recipes that'll help you address specific use-cases. :caption: How-to guides :hidden: - how-tos/* + how-tos/working-with-postgresql-data-types + how-tos/providing-images-for-img - :doc:`how-tos/providing-images-for-img` - :doc:`how-tos/working-with-postgresql-data-types` diff --git a/postgrest.dict b/postgrest.dict index 5a3f172710..fe4bf205a8 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -15,6 +15,7 @@ booleans Bouscal buildpack bugfixes +Bytea Cardano cd centric From 48783087de6307bbeae39abd3c938cb41b1beca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz-Josef=20F=C3=A4rber?= Date: Thu, 2 Jun 2022 12:07:35 +0200 Subject: [PATCH 509/549] RPC POST for function w/single unnamed XML param, postgrest-PR #2300 --- docs/api.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 5b30e6bd11..886b465785 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2043,7 +2043,7 @@ You can also call a function that takes a single parameter of type JSON by sendi Calling functions with a single unnamed parameter ------------------------------------------------- -You can make a POST request to a function with a single unnamed parameter to send raw ``json/jsonb``, ``bytea`` or ``text`` data. +You can make a POST request to a function with a single unnamed parameter to send raw ``json/jsonb``, ``bytea``, ``text`` or ``xml`` data. To send raw JSON, the function must have a single unnamed ``json`` or ``jsonb`` parameter and the header ``Content-Type: application/json`` must be included in the request. @@ -2076,6 +2076,8 @@ To send raw JSON, the function must have a single unnamed ``json`` or ``jsonb`` 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. +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 From 580184532b8d39d077fd71ddad060c46e84a8e02 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Tue, 14 Jun 2022 10:40:10 -0500 Subject: [PATCH 510/549] Add 9.0.1 changelog --- docs/index.rst | 1 + docs/releases/latest.rst | 20 --------- docs/releases/v9.0.1.rst | 89 ++++++++++++++++++++++++++++++++++++++++ postgrest.dict | 2 + 4 files changed, 92 insertions(+), 20 deletions(-) create mode 100644 docs/releases/v9.0.1.rst diff --git a/docs/index.rst b/docs/index.rst index c26e2477af..db3a181d50 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -99,6 +99,7 @@ The project has a friendly and growing community. Join our `chat room + v9.0.1 v9.0.0 releases/v8.0.0 releases/v7.0.1 diff --git a/docs/releases/latest.rst b/docs/releases/latest.rst index 101d8fd773..e09ccb247b 100644 --- a/docs/releases/latest.rst +++ b/docs/releases/latest.rst @@ -67,38 +67,18 @@ Documentation improvements Bug fixes --------- -* Execute deferred constraint triggers when using ``Prefer: tx=rollback`` (`#2020 `_) - * Return ``204 No Content`` without ``Content-Type`` for ``PUT`` (`#2058 `_) -* Fix ``is`` not working with upper or mixed case values like ``NULL, TrUe, FaLsE`` (`#2077 `_) - -* Fix schema cache loading when views with ``XMLTABLE`` and ``DEFAULT`` are present (`#2024 `_) - -* Fix wrong CORS header Authentication -> Authorization (`#1724 `_) - * Clarify error for failed schema cache load. (`#2107 `_) - From ``Database connection lost. Retrying the connection`` to ``Could not query the database for the schema cache. Retrying.`` -* Fix reading database configuration properly when ``=`` is present in its value (`#2120 `_) - * Fix silently ignoring filter on a non-existent embedded resource (`#1771 `_) -* Remove trigger functions from schema cache and OpenAPI output, because they can't be called directly anyway. (`#2135 `_) - -* Remove aggregates, procedures and window functions from the schema cache and OpenAPI output. (`#2101 `_) - * Remove functions, which are not callable due to unnamed arguments, from schema cache and OpenAPI output. (`#2152 `_) * Fix accessing JSON array fields with ``->`` and ``->>`` in ``?select=`` and ``?order=``. (`#2145 `_) -* Fix ``--dump-schema`` running with a wrong PG version. (`#2153 `_) - -* Keep working when ``EMFILE (Too many open files)`` is reached. (`#2042 `_) - -* Ignore ``Content-Type`` headers for ``GET`` requests when calling RPCs. Previously, ``GET`` without parameters, but with ``Content-Type: text/plain`` or ``Content-Type: application/octet-stream`` would fail with ``404 Not Found``, even if a function without arguments was available. (`#2147 `_) - Breaking changes ---------------- diff --git a/docs/releases/v9.0.1.rst b/docs/releases/v9.0.1.rst new file mode 100644 index 0000000000..ffec1ee2ad --- /dev/null +++ b/docs/releases/v9.0.1.rst @@ -0,0 +1,89 @@ + +PostgREST 9.0.1 +=============== + +This version includes important fixes for production environments and other miscellaneous fixes. You can download the pre-compiled binaries on the `GitHub release page `_. + +Bug Fixes +--------- + +* Keep working when ``EMFILE (Too many open files)`` is reached. (`#2042 `_) + +* Disable parallel GC for better performance on higher core CPUs (`#2294 `_). Thanks to `NoRedInk for their blog post `_ that lead us to this fix. + +* Fix using CPU while idle. (`#1076 `_) + +* Fix reading database configuration properly when ``=`` is present in the value. (`#2120 `_) + +* Fix ``is`` not working with upper or mixed case values like ``NULL``, ``TrUe``, ``FaLsE``. (`#2077 `_) + +* Execute deferred constraint triggers when using ``Prefer: tx=rollback``. (`#2020 `_) + +* Ignore ``Content-Type`` headers for ``GET`` requests when calling RPCs. (`#2147 `_) + + * Previously, ``GET`` without parameters, but with ``Content-Type: text/plain`` or ``Content-Type: application/octet-stream`` would fail with ``404 Not Found``, even if a function without arguments was available. + +* Fix wrong CORS header from ``Authentication`` to ``Authorization``. (`#1724 `_) + +* Fix ``json`` and ``jsonb`` columns showing a type in OpenAPI spec. (`#2165 `_) + +* Remove trigger functions from the schema cache and OpenAPI output, because they can't be called directly anyway. (`#2135 `_) + +* Remove aggregates, procedures and window functions from the schema cache and OpenAPI output. (`#2101 `_) + +* Fix schema cache loading when views with ``XMLTABLE`` and ``DEFAULT`` are present. (`#2024 `_) + +* Fix ``--dump-schema`` running with a wrong PG version. (`#2153 `_) + +* Fix misleading disambiguation error where the content of the ``relationship`` key looks like valid syntax. (`#2239 `_) + +Thanks +------ + +Big thanks from the `PostgREST team `_ to our 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/2ndquadrant.png + :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo + :width: 13em + + .. image:: ../_static/retool.png + :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + + .. image:: ../_static/gnuhost.png + :target: https://gnuhost.eu/?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/oblivious.jpg + :target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + +* Evans Fernandes +* `Jan Sommer `_ +* `Franz Gusenbauer `_ +* `Daniel Babiak `_ +* Tsingson Qin +* Michel Pelletier +* Jay Hannah +* Robert Stolarz +* Nicholas DiBiase +* Christopher Reid +* Nathan Bouscal +* Daniel Rafaj +* David Fenko +* Remo Rechkemmer +* Severin Ibarluzea +* Tom Saleeba +* Pawel Tyll + +If you like to join them please consider `supporting PostgREST development `_. diff --git a/postgrest.dict b/postgrest.dict index fe4bf205a8..2262fe9bc9 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -24,6 +24,7 @@ ClojureScript cloudfared config CORS +CPUs cryptographically CSV Daemonizing @@ -41,6 +42,7 @@ Fernandes filename FreeBSD fts +GC GHC Github Google From a9baa5bc9f73009d19b87fb24b252eaea3206ef1 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 15 Jun 2022 23:11:42 -0500 Subject: [PATCH 511/549] lowercase upsert --- docs/api.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 886b465785..e41e7513ab 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1724,10 +1724,10 @@ Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. .. _upsert: -UPSERT +Upsert ====== -You can make an UPSERT with :code:`POST` and the :code:`Prefer: resolution=merge-duplicates` header: +You can make an upsert with :code:`POST` and the :code:`Prefer: resolution=merge-duplicates` header: .. tabs:: @@ -1755,17 +1755,17 @@ You can make an UPSERT with :code:`POST` and the :code:`Prefer: resolution=merge ] 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 `_. +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 `_. .. 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`. + 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. +By specifying the ``on_conflict`` query parameter, you can make upsert work on a column(s) that has a UNIQUE constraint. .. tabs:: @@ -1798,7 +1798,7 @@ By specifying the ``on_conflict`` query parameter, you can make UPSERT work on a PUT --- -A single row UPSERT can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: +A single row upsert can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: .. tabs:: @@ -2385,7 +2385,7 @@ You can call overloaded functions with different number of arguments. Response Formats For Scalar Responses ===================================== -For scalar return values such as +For scalar return values such as * single-column selects on tables or * scalar functions, From 5305998a952b0a84c76ed7d6a3935ca7a9d3e2d8 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 15 Jun 2022 23:32:53 -0500 Subject: [PATCH 512/549] disallowed full table update --- docs/api.rst | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index e41e7513ab..54426024e2 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1716,11 +1716,9 @@ To update a row or rows in a table, use the PATCH verb. Use :ref:`h_filter` to s -X PATCH -H "Content-Type: application/json" \ -d '{ "category": "child" }' -Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. - -.. warning:: +Doing a full table update without filters is not allowed and will result in 0 updated rows. To make a an update without filters, you must limit the rows affected. See :ref:`limited_update_delete`. - Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`. +Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. .. _upsert: From 13ce0f33548c741fabd1800f2ffa3c313a00b4f7 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 15 Jun 2022 23:51:04 -0500 Subject: [PATCH 513/549] add bulk update --- docs/api.rst | 33 +++++++++++++++++++++++++++++++++ docs/releases/latest.rst | 5 +++++ postgrest.dict | 2 ++ 3 files changed, 40 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 54426024e2..5d34ecf776 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1720,6 +1720,39 @@ Doing a full table update without filters is not allowed and will result in 0 up Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. +.. _bulk_update: + +Bulk Update +----------- + +You can update rows with different data by providing a JSON array of objects having uniform keys, the rows will be chosen based on the primary key column(s) values. + +.. tabs:: + + .. code-tab:: http + + PATCH /employees HTTP/1.1 + + [ + { "id": 1, "name": "Renamed employee 1", "salary": 40000 }, + { "id": 2, "name": "Renamed employee 2", "salary": 52000 }, + { "id": 3, "name": "Renamed employee 3", "salary": 60000 } + ] + + .. code-tab:: bash Curl + + curl "http://localhost:3000/employees" \ + -X PATCH -H "Content-Type: application/json" \ + -d @- << EOF + [ + { "id": 1, "name": "Renamed employee 1", "salary": 40000 }, + { "id": 2, "name": "Renamed employee 2", "salary": 52000 }, + { "id": 3, "name": "Renamed employee 3", "salary": 60000 } + ] + EOF + +You must not include any filters for this to work. If you provide filters, only the values of the first object in the array will be used for the update. + .. _upsert: Upsert diff --git a/docs/releases/latest.rst b/docs/releases/latest.rst index e09ccb247b..3b7eaaf97a 100644 --- a/docs/releases/latest.rst +++ b/docs/releases/latest.rst @@ -10,6 +10,11 @@ Features API ~~~ +Bulk Update +^^^^^^^^^^^ + +See :ref:`bulk_update`. + Access Composite Type fields and Array elements ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/postgrest.dict b/postgrest.dict index 2262fe9bc9..d1430e1f6d 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -182,6 +182,8 @@ unicode unix updatable UPSERT +Upsert +upsert uri url urls From 360cfa3fbd0e6a6f4b3f39a488f56e838c3fc8b2 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Mon, 20 Jun 2022 13:50:47 -0500 Subject: [PATCH 514/549] clarify errors (#550) --- docs/errors.rst | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index 08782f4ecc..e3473e4878 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -3,12 +3,17 @@ Error Source ============ -For the most part, error messages will come directly from the database with the same `structure that PostgreSQL uses `_, PostgREST will convert the ``MESSAGE``, ``DETAIL``, ``HINT`` and ``ERRCODE`` from the PostgreSQL error to JSON format and add an HTTP status code to the response (see :ref:`status_codes`). For instance, this is the error you will get when querying a nonexistent table: +For the most part, error messages will come directly from the database with the same `structure that PostgreSQL uses `_. PostgREST will convert the ``MESSAGE``, ``DETAIL``, ``HINT`` and ``ERRCODE`` from the PostgreSQL error to JSON format and add an HTTP status code to the response (see :ref:`status_codes`). For instance, this is the error you will get when querying a nonexistent table: .. code-block:: http GET /nonexistent_table?id=eq.1 HTTP/1.1 +.. code-block:: http + + HTTP/1.1 404 Not Found + Content-Type: application/json; charset=utf-8 + .. code-block:: json { @@ -18,12 +23,17 @@ For the most part, error messages will come directly from the database with the "message": "relation \"api.nonexistent_table\" does not exist" } -However, some errors do come from PostgREST itself (such as those related to the :ref:`schema_cache`). These have the same structure as the PostgreSQL errors (message, details, hint and code) but are differentiated by the ``PGRST`` prefix in the ``code`` field (see :ref:`pgrst_errors`). For instance, when querying a function that does not exist, the error will be: +However, some errors do come from PostgREST itself (such as those related to the :ref:`schema_cache`). These have the same structure as the PostgreSQL errors but are differentiated by the ``PGRST`` prefix in the ``code`` field (see :ref:`pgrst_errors`). For instance, when querying a function that does not exist, the error will be: .. 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 { @@ -149,11 +159,11 @@ Related to the HTTP request elements. | | verbs are allowed. Any other verb will throw this error. | | PGRST101 | | +---------------+-------------------------------------------------------------+ -| .. _pgrst102: | Related to the request body structure. | -| | See :ref:`insert` and :ref:`update`. | +| .. _pgrst102: | An invalid request body was sent(e.g. an empty body or | +| | malformed JSON). | | PGRST102 | | +---------------+-------------------------------------------------------------+ -| .. _pgrst103: | Related to :ref:`limits`. | +| .. _pgrst103: | An invalid range was specified for :ref:`limits`. | | | | | PGRST103 | | +---------------+-------------------------------------------------------------+ @@ -161,7 +171,7 @@ Related to the HTTP request elements. | | or it doesn't exist. | | PGRST104 | | +---------------+-------------------------------------------------------------+ -| .. _pgrst105: | Related to an :ref:`UPSERT using PUT `. | +| .. _pgrst105: | An invalid :ref:`PUT ` request was done | | | | | PGRST105 | | +---------------+-------------------------------------------------------------+ @@ -185,17 +195,17 @@ Related to the HTTP request elements. | | modifies more rows than the maximum specified in the limit. | | PGRST110 | See :ref:`limited_update_delete`. | +---------------+-------------------------------------------------------------+ -| .. _pgrst111: | Related to :ref:`guc_resp_hdrs`. | -| | | +| .. _pgrst111: | An invalid ``response.headers`` was set. | +| | See :ref:`guc_resp_hdrs`. | | PGRST111 | | +---------------+-------------------------------------------------------------+ | .. _pgrst112: | The status code must be a positive integer. | | | See :ref:`guc_resp_status`. | | PGRST112 | | +---------------+-------------------------------------------------------------+ -| .. _pgrst113: | Related to :ref:`scalar_return_formats`. | -| | See :ref:`providing_img` for an example on requesting | -| | images. | +| .. _pgrst113: | More than one column was returned for a scalar result. | +| | See :ref:`scalar_return_formats`. | +| | | | PGRST113 | | +---------------+-------------------------------------------------------------+ | .. _pgrst114: | For an :ref:`UPSERT using PUT `, when | @@ -229,8 +239,8 @@ Related to a :ref:`stale schema cache `. Most of the time, these e | | the embedding resources or the relationship itself may not | | PGRST200 | exist in the database. | +---------------+-------------------------------------------------------------+ -| .. _pgrst201: | Related to :ref:`embed_disamb`. | -| | | +| .. _pgrst201: | An ambiguous embedding request was made. | +| | See :ref:`embed_disamb`. | | PGRST201 | | +---------------+-------------------------------------------------------------+ | .. _pgrst202: | Caused by a :ref:`stale_function_signature`, otherwise | @@ -274,12 +284,12 @@ Related to the authentication process using JWT. You can follow the :ref:`tut1` Group X - Internal ------------------ -Internal errors mostly related to `the library `_ that PostgREST uses to connect to the database. If you encounter any of these errors, you may have stumbled on a PostgREST bug, please `open an issue `_ and we'll be glad to fix it. +Internal errors. If you encounter any of these, you may have stumbled on a PostgREST bug, please `open an issue `_ and we'll be glad to fix it. +---------------+-------------------------------------------------------------+ | Code | Description | +===============+=============================================================+ -| .. _pgrstX00: | Internal errors related to the library that connects to the | -| | database. | +| .. _pgrstX00: | Internal errors related to the library used for connecting | +| | to the database. | | PGRSTX00 | | +---------------+-------------------------------------------------------------+ From 65c2754719c38d26f7ab10e97f648193e59e5ce1 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Tue, 21 Jun 2022 17:38:29 -0500 Subject: [PATCH 515/549] Modify GeoJSON examples from views to functions (#551) --- .../working-with-postgresql-data-types.rst | 140 +++++++++--------- 1 file changed, 71 insertions(+), 69 deletions(-) diff --git a/docs/how-tos/working-with-postgresql-data-types.rst b/docs/how-tos/working-with-postgresql-data-types.rst index 9cfcf52794..9ad0c934da 100644 --- a/docs/how-tos/working-with-postgresql-data-types.rst +++ b/docs/how-tos/working-with-postgresql-data-types.rst @@ -638,23 +638,28 @@ Say you want to add areas in polygon format. The request using string representa ] EOF -Now, when you request the information, PostgREST will automatically cast the ``area`` column to ``JSON`` format. Although this output is useful, you will want to use the PostGIS functions to have more control on filters or casts. For these cases, creating a ``view`` is your best option. For example, let's use some of the functions to get the data in `GeoJSON format `_ and to calculate the area in square units: +Now, when you request the information, PostgREST will automatically cast the ``area`` column to ``JSON`` format. Although this output is useful, you will want to use the PostGIS functions to have more control on filters or casts. For these cases, creating a ``function`` is your best option. For example, let's use some of the functions to get the data in `GeoJSON format `_ and to calculate the area in square units: .. code-block:: postgres - create or replace view coverage_geo as - select name, - -- Get the Geometry Object - st_AsGeoJSON(c.area)::json as geo_geometry, - -- Get the Feature Object - st_AsGeoJSON(c.*)::json as geo_feature, - -- Calculate the area in square units - st_area(c.area) as square_units - from coverage c; - - -- Create another view for the FeatureCollection Object + create or replace function coverage_geo(filter text) returns json as $$ + select + json_build_object( + 'name', c.name, + -- Get the Geometry Object + 'geo_geometry', st_AsGeoJSON(c.area)::json, + -- Get the Feature Object + 'geo_feature', st_AsGeoJSON(c.*)::json, + -- Calculate the area in square units + 'square_units', st_area(c.area) + ) + from coverage c + where c.name = filter; + $$ language sql; + + -- Create another function for the FeatureCollection Object -- for the sake of making the examples clearer - create or replace view coverage_geo_collection as + create or replace function coverage_geo_collection() returns json as $$ select json_build_object( 'type', 'FeatureCollection', @@ -662,6 +667,7 @@ Now, when you request the information, PostgREST will automatically cast the ``a ) as geo_feature_collection from coverage c; + $$ language sql; Now the query will return the information as you expected: @@ -669,39 +675,37 @@ Now the query will return the information as you expected: .. code-tab:: http - GET /coverage_geo?name=eq.big HTTP/1.1 + GET /rpc/coverage_geo?filter=big HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/coverage_geo?name=eq.big" + curl "http://localhost:3000/rpc/coverage_geo?filter=big" .. code-block:: json - [ - { - "name": "big", - "geo_geometry": { + { + "name": "big", + "geo_geometry": { + "type": "Polygon", + "coordinates": [ + [[0,0],[10,0],[10,10],[0,10],[0,0]] + ] + }, + "geo_feature": { + "type": "Feature", + "geometry": { "type": "Polygon", "coordinates": [ [[0,0],[10,0],[10,10],[0,10],[0,0]] ] }, - "geo_feature": { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [[0,0],[10,0],[10,10],[0,10],[0,0]] - ] - }, - "properties": { - "id": 2, - "name": "big" - } - }, - "square_units": 100 - } - ] + "properties": { + "id": 2, + "name": "big" + } + }, + "square_units": 100 + } And for the Feature Collection format: @@ -709,46 +713,44 @@ And for the Feature Collection format: .. code-tab:: http - GET /coverage_geo_collection HTTP/1.1 + GET /rpc/coverage_geo_collection HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/coverage_geo_collection" + curl "http://localhost:3000/rpc/coverage_geo_collection" .. code-block:: json - [ - { - "geo_feature_collection": { - "type": "FeatureCollection", - "features": [ - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [[0,0],[1,0],[1,1],[0,1],[0,0]] - ] - }, - "properties": { - "id": 1, - "name": "small" - } + { + "geo_feature_collection": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [[0,0],[1,0],[1,1],[0,1],[0,0]] + ] }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [[0,0],[10,0],[10,10],[0,10],[0,0]] - ] - }, - "properties": { - "id": 2, - "name": "big" - } + "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" + } + } + ] } - ] + } From 06ece64cb0b2b490d986e1b1033fea9c42174e3a Mon Sep 17 00:00:00 2001 From: fjf2002 Date: Wed, 29 Jun 2022 17:11:15 +0200 Subject: [PATCH 516/549] HOW-TO for SOAP (#554) --- docs/how-tos/create-soap-endpoint.rst | 222 ++++++++++++++++++++++++++ docs/index.rst | 2 + 2 files changed, 224 insertions(+) create mode 100644 docs/how-tos/create-soap-endpoint.rst diff --git a/docs/how-tos/create-soap-endpoint.rst b/docs/how-tos/create-soap-endpoint.rst new file mode 100644 index 0000000000..62f0d0412b --- /dev/null +++ b/docs/how-tos/create-soap-endpoint.rst @@ -0,0 +1,222 @@ +.. _create_soap_endpoint: + +Create a SOAP endpoint +====================== + +:author: `fjf2002 `_ + +PostgREST now has XML support. With a bit of work, SOAP endpoints become possible. + +Please note that PostgREST supports just ``text/xml`` MIME type in request/response headers ``Content-Type`` and ``Accept``. +If you have to use other MIME types such as ``application/soap+xml``, you could manipulate the headers in your reverse proxy. + + + +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 OR REPLACE FUNCTION my_soap_endpoint(xml) RETURNS 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 + + + + + + +Unfortunately the ``Accept: text/xml`` header is currently mandatory concerning PostgREST, otherwise it will respond +with a ``Content-Type: application/json`` header and enclose the response with quotes. +(You can check the returned headers by adding ``-v`` to the curl call.) + +If your SOAP clients do not send the ``Accept: text/xml`` header, you can fix that in your nginx reverse proxy +by adding something like ... + +.. code-block:: nginx + + set $accept $http_accept; + if ($contentType ~ "^text/xml($|;)") { + set $accept "text/xml"; + } + proxy_set_header Accept $accept; + +to your ``location`` nginx configuration. +(The given example sets the ``Accept`` header for each request of Content-Type ``text/xml``.) + + +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 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:`scalar_return_formats` +- :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/index.rst b/docs/index.rst index db3a181d50..989c55f169 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -206,9 +206,11 @@ These are recipes that'll help you address specific use-cases. how-tos/working-with-postgresql-data-types how-tos/providing-images-for-img + how-tos/create-soap-endpoint - :doc:`how-tos/providing-images-for-img` - :doc:`how-tos/working-with-postgresql-data-types` +- :doc:`how-tos/create-soap-endpoint` Ecosystem --------- From b37bacb19a5da5b9834ba09a7865840119d5851e Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 8 Jul 2022 12:27:14 -0500 Subject: [PATCH 517/549] Add Limezest/postgrest-cloud-run to ecosystem --- docs/ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index 8283d6c698..3d177848c7 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -62,6 +62,7 @@ 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 .. _eco_external_notification: From bba960d4c631a79837215785911b22cea9de8b3a Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 11 Jul 2022 23:55:56 -0500 Subject: [PATCH 518/549] new m2m relationship detection --- docs/api.rst | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 5d34ecf776..d28ca24758 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -957,7 +957,29 @@ this: Embedding through join tables ----------------------------- -PostgREST can also detect relationships going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). +PostgREST can also detect many-to-many relationships going through join tables. For this, the join table must contain foreign keys to the tables in +the many-to-many relationship and its primary key must include these foreign key columns. + +.. code-block:: postgresql + + create table "Roles"( + film_id int references "Films"(id) + , actor_id int references "Actors"(id) + , primary key(film_id, actor_id) + ) + + -- the many-to-many relationship can also be detected if the join table has a surrogate key, + -- as long as the foreign key columns are also part of the primary key + + create table "Roles"( + id int generated always as identity, + , film_id int references "Films"(id) + , actor_id int references "Actors"(id) + , primary key(id, film_id, actor_id) + ) + + +Then you can request the Actors for Films (which in this case finds the information through Roles). .. tabs:: From 2592639d1b37078fd3df7537b60a5e46e4df116d Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Tue, 19 Jul 2022 10:51:36 -0500 Subject: [PATCH 519/549] Reword and clarify embedding on views --- docs/api.rst | 75 ++++++++++------------------------------------------ 1 file changed, 14 insertions(+), 61 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index d28ca24758..4e7c4813f8 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1204,31 +1204,34 @@ Since it contains the ``films_id`` foreign key, it is possible to embed ``box_of Embedding Views --------------- -Embedding a view is possible if the view contains columns that have **foreign keys** defined in their source tables. +PostgREST will infer the relationships of a view based on its source tables. Source tables are the ones referenced in the ``FROM`` and ``JOIN`` clauses of the view definition. The foreign keys of the relationships must be present in the top ``SELECT`` clause of the view for this to work. -As an example, let's create a view called ``nominations_view`` based on the *nominations* table. +For instance, the following view has ``nominations``, ``films`` and ``competitions`` as source tables: .. code-block:: postgres CREATE VIEW nominations_view AS - SELECT - rank - , competition_id - , film_id - FROM - nominations; + SELECT + films.title as film_title + , competitions.name as competition_name + , nominations.rank + , nominations.film_id as nominations_film_id + , films.id as film_id + FROM nominations + JOIN films ON films.id = nominations.film_id + JOIN competitions ON competitions.id = nominations.competition_id; -Since it contains ``competition_id`` and ``film_id`` — and each one has a **foreign key** defined in its source table — we can embed *competitions* and *films*: +Since this view contains ``nominations.film_id``, which has a **foreign key** relationship to ``films``, then we can embed the ``films`` table. Similarly, because the view contains ``films.id``, then we can also embed the ``roles`` and the ``actors`` tables (the last one in a many-to-many relationship): .. tabs:: .. code-tab:: http - GET /nominations_view?select=rank,competitions(name,year),films(title)&rank=eq.5 HTTP/1.1 + GET /nominations_view?select=film_title,films(language),roles(character),actors(last_name,first_name)&rank=eq.5 HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/nominations_view?select=rank,competitions(name,year),films(title)&rank=eq.5" + curl "http://localhost:3000/nominations_view?select=film_title,films(language),roles(character),actors(last_name,first_name)&rank=eq.5" It's also possible to embed `Materialized Views `_. @@ -1249,56 +1252,6 @@ It's also possible to embed `Materialized Views Date: Fri, 5 Aug 2022 18:36:25 -0500 Subject: [PATCH 520/549] clarify * as an alias of % for LIKE --- docs/api.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 4e7c4813f8..31f627bb90 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -67,8 +67,8 @@ 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 (use * in place of %) -ilike :code:`ILIKE` ILIKE operator (use * in place of %) +like :code:`LIKE` LIKE operator (to avoid `URL encoding `_ you can use ``*`` as an alias of the percent sign ``%`` for the pattern) +ilike :code:`ILIKE` ILIKE operator (to avoid `URL 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)` From a0c463b99824eb8e7cfc68a3bc2bfb36919a4bbe Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Tue, 23 Aug 2022 21:59:48 -0500 Subject: [PATCH 521/549] Add missing changes/fixes/features for v10 --- docs/api.rst | 286 +++++++++++---- docs/configuration.rst | 28 +- .../working-with-postgresql-data-types.rst | 171 ++++----- docs/index.rst | 2 +- docs/install.rst | 2 +- docs/releases/latest.rst | 143 -------- docs/releases/v10.0.0.rst | 337 ++++++++++++++++++ postgrest.dict | 5 + 8 files changed, 674 insertions(+), 300 deletions(-) delete mode 100644 docs/releases/latest.rst create mode 100644 docs/releases/v10.0.0.rst diff --git a/docs/api.rst b/docs/api.rst index 31f627bb90..50d78aec41 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -801,6 +801,7 @@ The current possibilities are: * ``text/csv`` * ``application/json`` * ``application/openapi+json`` +* ``application/geo+json`` and in the special case of a single-column select the following additional three formats; also see the section :ref:`scalar_return_formats`: @@ -870,35 +871,56 @@ In addition to providing RESTful routes for each table and view, PostgREST allow API call. This reduces the need for multiple API requests. The server uses **foreign keys** to determine which tables and views can be returned together. For example, consider a database of films and their awards: +.. image:: _static/film.png + .. important:: - PostgREST needs `FOREIGN KEY constraints `_ to be able to do Resource Embedding. + * PostgREST needs `FOREIGN KEY constraints `_ to be able to do Resource Embedding. + * Whenever FOREIGN KEY constraints change in the database schema you must refresh PostgREST's schema cache for Resource Embedding to work properly. See the section :ref:`schema_reloading`. -.. image:: _static/film.png +.. _one-to-many: + +One-to-many relationships +------------------------- -As seen above in :ref:`v_filter` we can request the titles of all films like this: +When a one-to-many relationship is detected, the embedded resource is returned as a JSON array. For example, we can request the Directors and the Films they directed because there is a foreign key constraint between them, like this: .. tabs:: .. code-tab:: http - GET /films?select=title HTTP/1.1 + GET /directors?select=last_name,films(title) HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/films?select=title" - -This might return something like + curl "http://localhost:3000/directors?select=last_name,films(title)" .. code-block:: json [ - { "title": "Workers Leaving The Lumière Factory In Lyon" }, - { "title": "The Dickson Experimental Sound Film" }, - { "title": "The Haunted Castle" } + { "last_name": "Lumière", + "films": [ + {"title": "Workers Leaving The Lumière Factory In Lyon"} + ] + }, + { "last_name": "Dickson", + "films": [ + {"title": "The Dickson Experimental Sound Film"} + ] + }, + { "last_name": "Méliès", + "films": [ + {"title": "The Haunted Castle"} + ] + } ] -However because a foreign key constraint exists between Films and Directors, we can request this information be included: +.. _many-to-one: + +Many-to-one relationships +------------------------- + +When a many-to-one relationship is detected, the embedded resource is returned as a JSON object. For example, we can request all the Films and the Director for each film like this: .. tabs:: @@ -910,8 +932,6 @@ However because a foreign key constraint exists between Films and Directors, we curl "http://localhost:3000/films?select=title,directors(id,last_name)" -Which would return - .. code-block:: json [ @@ -935,10 +955,7 @@ Which would return } ] -In this example, since the relationship is a forward relationship, there is -only one director associated with a film. As the table name is plural it might -be preferable for it to be singular instead. An table name alias can accomplish -this: +However, the table name is in plural, which is not accurate since a Film is directed by only one Director. Using a table name alias can solve this: .. tabs:: @@ -950,34 +967,31 @@ this: curl "http://localhost:3000/films?select=title,director:directors(id,last_name)" -.. important:: - - Whenever FOREIGN KEY constraints change in the database schema you must refresh PostgREST's schema cache for Resource Embedding to work properly. See the section :ref:`schema_reloading`. +.. _many-to-many: -Embedding through join tables ------------------------------ +Many-to-many relationships +-------------------------- PostgREST can also detect many-to-many relationships going through join tables. For this, the join table must contain foreign keys to the tables in -the many-to-many relationship and its primary key must include these foreign key columns. +the many-to-many relationship and its composite primary key must include these foreign key columns. .. code-block:: postgresql - create table "Roles"( - film_id int references "Films"(id) - , actor_id int references "Actors"(id) + create table roles( + film_id int references films(id) + , actor_id int references actors(id) , primary key(film_id, actor_id) - ) + ); -- the many-to-many relationship can also be detected if the join table has a surrogate key, -- as long as the foreign key columns are also part of the primary key - create table "Roles"( + create table roles( id int generated always as identity, - , film_id int references "Films"(id) - , actor_id int references "Actors"(id) + , film_id int references films(id) + , actor_id int references actors(id) , primary key(id, film_id, actor_id) - ) - + ); Then you can request the Actors for Films (which in this case finds the information through Roles). @@ -991,6 +1005,84 @@ Then you can request the Actors for Films (which in this case finds the informat curl "http://localhost:3000/actors?select=films(title,year)" +.. _one-to-one: + +One-to-one relationships +------------------------ + +PostgREST detects one-to-one relationships when a foreign key is also the primary key of the table or when the foreign key has a ``UNIQUE`` constraint. + +.. code-block:: postgresql + + -- references Films using the primary key as a foreign key + CREATE TABLE technical_specs( + film_id INT PRIMARY KEY REFERENCES films, + runtime TIME, + camera TEXT, + sound TEXT + ); + + -- references Films using a foreign key with unique constraint + CREATE TABLE technical_specs( + film_id INT REFERENCES films UNIQUE, + runtime TIME, + camera TEXT, + sound TEXT + ); + +Now, the embedding between Films and Technical_Specs is returned as a JSON object no matter the order. + +.. tabs:: + + .. code-tab:: http + + GET /films?select=title,technical_specs(*) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=title,technical_specs(*)" + +.. _computed_relationships: + +Computed Relationships +---------------------- + +You can customize how PostgREST detects relationships between two tables. To do this, you need to create a function that has one of the tables as a single parameter and the other as its return type. For instance: + +.. code-block:: postgres + + CREATE FUNCTION director_competition(directors) RETURNS SETOF competitions AS $$ + SELECT c.* + FROM competitions c + JOIN nominations n ON c.id = n.competition_id + JOIN films f ON n.film_id = f.id + WHERE f.director_id = $1.id + $$ STABLE LANGUAGE sql; + +The above function allows a direct relationship between ``directors`` and ``competitions``: + +.. tabs:: + + .. code-tab:: http + + GET /directors?select=*,competitions:director_competition(name) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/directors?select=*,competitions:director_competition(name)" + +Take into consideration that the opposite relationship will not be detected, so you need to create another function for that. + +Computed relationships also allow you to override the ones that are detected by default. For example, this function can change the ``/films?select=directors(*)`` embedding: + +.. code-block:: postgres + + CREATE FUNCTION directors(films) RETURNS SETOF directors ROW 1 AS $$ + -- Override the relationship here + $$ STABLE LANGUAGE sql; + +Note that if ``ROW 1`` is added, PostgREST will detect a :ref:`many-to-one relationship ` and return a JSON object instead of an array embedding. + .. _nested_embedding: Nested Embedding @@ -1460,25 +1552,24 @@ Hint Disambiguation If specifying the **target** is not enough for unambiguous embedding, you can add a **hint**. For example, let's assume we create two views of ``addresses``: ``central_addresses`` and ``eastern_addresses``. -Since PostgREST supports :ref:`embedding_views` by detecting **source foreign keys** in the views, embedding with the foreign key -as the **target** will not be enough for an unambiguous embed: +PostgREST cannot detect a view as an embedded resource by using a column name or foreign key name as targets, that is why we need to use the view name ``central_addresses`` instead. But, still, this is not enough for an unambiguous embed. .. tabs:: .. code-tab:: http - GET /orders?select=*,billing_address(*) HTTP/1.1 + GET /orders?select=*,central_addresses(*) HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/orders?select=*,billing_address(*)" -i + curl "http://localhost:3000/orders?select=*,central_addresses(*)" -i .. code-block:: http HTTP/1.1 300 Multiple Choices For solving this case, in addition to the **target**, we can add a **hint**. -Here we specify ``central_addresses`` as the **target** and the ``billing_address`` foreign key as the **hint**: +Here, we still specify ``central_addresses`` as the **target** and use the ``billing_address`` foreign key as the **hint**: .. tabs:: @@ -1510,6 +1601,10 @@ Hints also work alongside ``!inner`` if a top level filtering is needed. From th curl "http://localhost:3000/orders?select=*,central_addresses!billing_address!inner(*)¢ral_addresses.code=AB1000" +.. note:: + + If the relationship is so complex that hint disambiguation does not solve it, then using :ref:`computed_relationships` is the best alternative. + .. _insert: Insertions @@ -1695,39 +1790,6 @@ Doing a full table update without filters is not allowed and will result in 0 up Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. -.. _bulk_update: - -Bulk Update ------------ - -You can update rows with different data by providing a JSON array of objects having uniform keys, the rows will be chosen based on the primary key column(s) values. - -.. tabs:: - - .. code-tab:: http - - PATCH /employees HTTP/1.1 - - [ - { "id": 1, "name": "Renamed employee 1", "salary": 40000 }, - { "id": 2, "name": "Renamed employee 2", "salary": 52000 }, - { "id": 3, "name": "Renamed employee 3", "salary": 60000 } - ] - - .. code-tab:: bash Curl - - curl "http://localhost:3000/employees" \ - -X PATCH -H "Content-Type: application/json" \ - -d @- << EOF - [ - { "id": 1, "name": "Renamed employee 1", "salary": 40000 }, - { "id": 2, "name": "Renamed employee 2", "salary": 52000 }, - { "id": 3, "name": "Renamed employee 3", "salary": 60000 } - ] - EOF - -You must not include any filters for this to work. If you provide filters, only the values of the first object in the array will be used for the update. - .. _upsert: Upsert @@ -2497,6 +2559,8 @@ Also if you wish to generate a ``summary`` field you can do it by having a multi spans multiple lines$$; +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 `_ 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:: @@ -2550,7 +2614,7 @@ For a view, the methods are determined by the presence of INSTEAD OF TRIGGERS. | `auto-updatable views `_ | +--------------------+-------------------------------------------------------------------------------------------------+ -For functions, OPTIONS requests are not supported. +For functions, the methods depend on their volatility. ``VOLATILE`` functions allow only ``OPTIONS,POST``, whereas the rest also permit ``GET,HEAD``. .. important:: @@ -2817,3 +2881,83 @@ Returns: "hint": "Upgrade your plan", "code": "PT402" } + +.. _explain_plan: + +Execution plan +-------------- + +You can get the execution plan of a request by adding the ``Accept: application/vnd.pgrst.plan`` header after setting the :ref:`db-plan-enabled` configuration to ``true``. It is useful to verify why a certain operation might be expensive as a result of using `EXPLAIN `_ on the generated query for the request. + +The output of the plan is generated in ``text`` format by default: + +.. tabs:: + + .. code-tab:: http + + GET /users?select=name&order=id HTTP/1.1 + Accept: application/vnd.pgrst.plan + + .. code-tab:: bash Curl + + 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 same execution can be returned in ``json`` format by using the ``Accept: application/vnd.pgrst.plan+json`` header instead: + +.. tabs:: + + .. code-tab:: http + + GET /users?select=name&order=id HTTP/1.1 + Accept: application/vnd.pgrst.plan+json + + .. code-tab:: bash Curl + + 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 + } + ] + } + } + ] + +You can also get the result plan of the different media types that PostgREST supports by adding them to the header using ``for``. For instance, to obtain the plan for a :ref:`text/xml ` media type in json format, you need to add the ``Accept: application/vnd.pgrst.plan; for=text/xml`` header. + +Additionally, the deactivated parameters of the ``EXPLAIN`` command can be enabled by adding them to the header using ``options``. The available parameters are ``analyze``, ``verbose``, ``settings``, ``buffers`` and ``wal``, while the remaining ones are active by default. For example, to add the ``analyze`` and ``wal`` parameters, add the ``Accept: application/vnd.pgrst.plan; options=analyze|wal`` header. + +Note that any changes done will be committed when activating the ``analyze`` option. To avoid this, set the :ref:`db-tx-end` configuration in a way that allows to rollback the changes according to your preference. diff --git a/docs/configuration.rst b/docs/configuration.rst index 66061904b4..1e45e10651 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -154,8 +154,9 @@ db-channel-enabled Boolean True Y db-config Boolean True Y db-extra-search-path String public Y db-max-rows Int ∞ Y +db-plan-enabled Boolean False Y db-pool Int 10 -db-pool-timeout Int 10 +db-pool-timeout Int 3600 db-pre-request String Y db-prepared-statements Boolean True Y db-schemas String public Y @@ -168,6 +169,7 @@ jwt-secret String Y jwt-secret-is-base64 Boolean False Y log-level String error Y openapi-mode String follow-privileges Y +openapi-security-active Boolean False Y openapi-server-proxy-uri String Y raw-media-types String Y server-host String !4 @@ -282,6 +284,18 @@ db-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 +--------------- + + =============== ===================== + **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 @@ -551,6 +565,18 @@ openapi-mode # Throws a `404 Not Found` error when accessing the API root path openapi-mode = "disabled" +.. _openapi-security-active: + +openapi-security-active +----------------------- + + =============== ============================= + **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 `. + .. _openapi-server-proxy-uri: openapi-server-proxy-uri diff --git a/docs/how-tos/working-with-postgresql-data-types.rst b/docs/how-tos/working-with-postgresql-data-types.rst index 9ad0c934da..fb317c97e9 100644 --- a/docs/how-tos/working-with-postgresql-data-types.rst +++ b/docs/how-tos/working-with-postgresql-data-types.rst @@ -597,6 +597,8 @@ You can also query and filter the value of a ``hstore`` column using the arrow o [{ "native": "مصر" }] +.. _ww_postgis: + PostGIS ------- @@ -638,76 +640,81 @@ Say you want to add areas in polygon format. The request using string representa ] EOF -Now, when you request the information, PostgREST will automatically cast the ``area`` column to ``JSON`` format. Although this output is useful, you will want to use the PostGIS functions to have more control on filters or casts. For these cases, creating a ``function`` is your best option. For example, let's use some of the functions to get the data in `GeoJSON format `_ and to calculate the area in square units: - -.. code-block:: postgres - - create or replace function coverage_geo(filter text) returns json as $$ - select - json_build_object( - 'name', c.name, - -- Get the Geometry Object - 'geo_geometry', st_AsGeoJSON(c.area)::json, - -- Get the Feature Object - 'geo_feature', st_AsGeoJSON(c.*)::json, - -- Calculate the area in square units - 'square_units', st_area(c.area) - ) - from coverage c - where c.name = filter; - $$ language sql; - - -- Create another function for the FeatureCollection Object - -- for the sake of making the examples clearer - create or replace function coverage_geo_collection() returns json as $$ - select - json_build_object( - 'type', 'FeatureCollection', - 'features', json_agg(st_AsGeoJSON(c.*)::json) - ) - as geo_feature_collection - from coverage c; - $$ language sql; - -Now the query will return the information as you expected: +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 `_ 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 `_: .. tabs:: .. code-tab:: http - GET /rpc/coverage_geo?filter=big HTTP/1.1 + GET /coverage HTTP/1.1 + Accept: application/geo+json .. code-tab:: bash Curl - curl "http://localhost:3000/rpc/coverage_geo?filter=big" + curl "http://localhost:3000/coverage" \ + -H "Accept: application/geo+json" .. code-block:: json { - "name": "big", - "geo_geometry": { - "type": "Polygon", - "coordinates": [ - [[0,0],[10,0],[10,10],[0,10],[0,0]] - ] - }, - "geo_feature": { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [[0,0],[10,0],[10,10],[0,10],[0,0]] - ] + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [[0,0],[1,0],[1,1],[0,1],[0,0]] + ] + }, + "properties": { + "id": 1, + "name": "small" + } }, - "properties": { - "id": 2, - "name": "big" + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [[0,0],[10,0],[10,10],[0,10],[0,0]] + ] + }, + "properties": { + "id": 2, + "name": "big" + } } - }, - "square_units": 100 + ] } -And for the Feature Collection format: +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. For example: + +.. 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: .. tabs:: @@ -722,35 +729,33 @@ And for the Feature Collection format: .. code-block:: json { - "geo_feature_collection": { - "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": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [[0,0],[1,0],[1,1],[0,1],[0,0]] + ] }, - { - "type": "Feature", - "geometry": { - "type": "Polygon", - "coordinates": [ - [[0,0],[10,0],[10,10],[0,10],[0,0]] - ] - }, - "properties": { - "id": 2, - "name": "big" - } + "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" + } + } + ] } diff --git a/docs/index.rst b/docs/index.rst index 989c55f169..dcb1089fd9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -98,7 +98,7 @@ The project has a friendly and growing community. Join our `chat room + v10.0.0 v9.0.1 v9.0.0 releases/v8.0.0 diff --git a/docs/install.rst b/docs/install.rst index c68ae15863..ec3c97721b 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -228,7 +228,7 @@ When a pre-built binary does not exist for your system you can build the project You can build PostgREST from source with `Stack `_. It will install any necessary Haskell dependencies on your system. -* `Install Stack `_ for your platform +* `Install Stack `_ for your platform * Install Library Dependencies ===================== ======================================= diff --git a/docs/releases/latest.rst b/docs/releases/latest.rst deleted file mode 100644 index 3b7eaaf97a..0000000000 --- a/docs/releases/latest.rst +++ /dev/null @@ -1,143 +0,0 @@ - -Latest -====== - -These are features/bugfixes not yet on a stable version. You can try them by downloading the latest pre-releases `on the GitHub release page `_. - -Features --------- - -API -~~~ - -Bulk Update -^^^^^^^^^^^ - -See :ref:`bulk_update`. - -Access Composite Type fields and Array elements -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -You can now :ref:`access fields of a Composite type or elements of an Array type ` with the arrow operators(``->``, ``->>``) in the same way you would access the JSON type fields. - -Improved Error Messages -^^^^^^^^^^^^^^^^^^^^^^^ - -To increase consistency, all the errors messages are now normalized. The ``hint``, ``details``, ``code`` and ``message`` fields will always be present in the body, each one defaulting to a -``null`` value. In the same way, the :ref:`errors that were raised ` with ``SQLSTATE`` now include the ``message`` and ``code`` in the body. - -In addition to these changes and to further clarify the source of an error, PostgREST now adds a ``PGRST`` prefix to the error code of all the errors that are PostgREST-specific and don't come from the database. These errors have a unique code that identifies them and are documented in the :ref:`pgrst_errors` section. - -Alongside these changes, there is now a dedicated reference page for :doc:`Error documentation `. - -Administration -~~~~~~~~~~~~~~ - -Health checks -^^^^^^^^^^^^^ - -Admins can now benefit from two :ref:`health check endpoints ` exposed in a different port than the main app. When activated, the ``live`` and ``ready`` endpoints are available to verify if PostgREST is alive and running or if the database connection and the :ref:`schema cache ` are ready for querying. - -Logging users -^^^^^^^^^^^^^ - -You can now verify the current authenticated database user in the :ref:`request log ` on stdout. - -Run without configuration -^^^^^^^^^^^^^^^^^^^^^^^^^ - -It is now possible to execute PostgREST without specifying any configuration variable, even without the three that were mandatory - - - If :ref:`db-uri` is not set, PostgREST will use the `libpq environment variables `_ for the database connection. - - If :ref:`db-schemas` is not set, it will use the database ``public`` schema. - - If :ref:`db-anon-role` is not set, it will not allow anonymous requests. - -Documentation improvements -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -* Added a :doc:`/how-tos/working-with-postgresql-data-types` how-to, which contains explanations and examples on how to work with different PostgreSQL data types such as timestamps, ranges or PostGIS types, among others. - -* Added in-database and environment variable settings for each :ref:`configuration variable `. - -* Added the :ref:`file_descriptors` subsection. - -* Moved the :ref:`error_source` and the :ref:`status_codes` sections to the :doc:`errors reference page `. - -* Moved the *Casting type to custom JSON* how-to to the :ref:`casting_range_to_json` subsection. - -* Removed direct links for PostgREST versions older than 8.0 from the versions menu. - -* Removed the deprecated *Embedding table from another schema* how-to. - -Bug fixes ---------- - -* Return ``204 No Content`` without ``Content-Type`` for ``PUT`` (`#2058 `_) - -* Clarify error for failed schema cache load. (`#2107 `_) - - - From ``Database connection lost. Retrying the connection`` to ``Could not query the database for the schema cache. Retrying.`` - -* Fix silently ignoring filter on a non-existent embedded resource (`#1771 `_) - -* Remove functions, which are not callable due to unnamed arguments, from schema cache and OpenAPI output. (`#2152 `_) - -* Fix accessing JSON array fields with ``->`` and ``->>`` in ``?select=`` and ``?order=``. (`#2145 `_) - -Breaking changes ----------------- - -* Return ``204 No Content`` without ``Content-Type`` for RPCs returning ``VOID`` (`#2001 `_) - - - Previously, those RPCs would return ``null`` as a body with ``Content-Type: application/json``. - -Thanks ------- - -Big thanks from the `PostgREST team `_ to our 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/2ndquadrant.png - :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo - :width: 13em - - .. image:: ../_static/retool.png - :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest - :width: 13em - - .. image:: ../_static/gnuhost.png - :target: https://gnuhost.eu/?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/oblivious.jpg - :target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest - :width: 13em - -* Evans Fernandes -* `Jan Sommer `_ -* `Franz Gusenbauer `_ -* `Daniel Babiak `_ -* Tsingson Qin -* Michel Pelletier -* Jay Hannah -* Robert Stolarz -* Nicholas DiBiase -* Christopher Reid -* Nathan Bouscal -* Daniel Rafaj -* David Fenko -* Remo Rechkemmer -* Severin Ibarluzea -* Tom Saleeba -* Pawel Tyll - -If you like to join them please consider `supporting PostgREST development `_. diff --git a/docs/releases/v10.0.0.rst b/docs/releases/v10.0.0.rst new file mode 100644 index 0000000000..4d2a6aea89 --- /dev/null +++ b/docs/releases/v10.0.0.rst @@ -0,0 +1,337 @@ + +PostgREST 10.0.0 +================ + +Features +-------- + +API +~~~ + +XML/SOAP support for RPC +^^^^^^^^^^^^^^^^^^^^^^^^ + +RPC now understands the ``text/xml`` media type, allowing SQL functions to send XML output(``Accept: text/xml``) and receive XML input(``Content-Type: text/xml``). This makes SOAP endpoints possible, check the :ref:`create_soap_endpoint` how-to for more details. + +GeoJSON support +^^^^^^^^^^^^^^^ + +GeoJSON is supported across the board(reads, writes, RPC) with the ``Accept: application/geo+json`` header, this depends on PostGIS from the versions 3.0.0 and up. The :ref:`working with PostGIS section ` has an example to get you started. + +One-to-one relationships +^^^^^^^^^^^^^^^^^^^^^^^^ + +A :ref:`one-to-one relationship ` is now detected when a table's foreign key is also its primary key or when the foreign key has a ``UNIQUE`` constraint. + +Customizable Relationships for Resource Embedding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Using :ref:`computed_relationships`, you can add custom relationships or override automatically detected ones. This makes :ref:`resource_embedding` possible on Foreign Data Wrappers and complex SQL views. + +EXPLAIN Execution Plan +^^^^^^^^^^^^^^^^^^^^^^ + +The :ref:`EXPLAIN execution plan of a request ` is now obtainable with the ``Accept: application/vnd.pgrst.plan`` header. The result can be in ``text`` or ``json`` formats and is compatible with EXPLAIN vizualizers like `explain.depesz.com `_ or `explain.dalibo.com `_. + +POSIX Regular Expressions +^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can now use two :ref:`pattern matching ` operators for `POSIX regular expressions `_: ``match`` and ``imatch``, equivalent in PostgreSQL to ``~`` and ``~*`` respectively. + +Access composite type fields and array elements +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:ref:`Accessing fields of a Composite type or elements of an Array type ` is now possible with the arrow operators(``->``, ``->>``) in the same way you would access a JSON type fields. + +Authorize button for SwaggerUI +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +You can now activate the "Authorize" button in SwaggerUI by enabling the :ref:`openapi-security-active` configuration. Add your JWT token prepending :code:`Bearer` to it and you'll be able to request protected resources. + +Improved error messages +^^^^^^^^^^^^^^^^^^^^^^^ + +To increase consistency, all the errors messages are now normalized. The ``hint``, ``details``, ``code`` and ``message`` fields will always be present in the body, each one defaulting to a +``null`` value. In the same way, the :ref:`errors that were raised ` with ``SQLSTATE`` now include the ``message`` and ``code`` in the body. + +To further clarify the source of an error, we now add a ``PGRST`` prefix to the error code of all the errors that are PostgREST-specific and don't come from the database. These errors have unique codes that identifies them and are documented in the :ref:`pgrst_errors` section. + +Administration +~~~~~~~~~~~~~~ + +Health checks +^^^^^^^^^^^^^ + +Admins can now benefit from two :ref:`health check endpoints ` exposed in a different port than the main app. When activated, the ``live`` and ``ready`` endpoints are available to verify if PostgREST is alive and running or if the database connection and the :ref:`schema cache ` are ready for querying. + +Logging users +^^^^^^^^^^^^^ + +You can now see the :ref:`request database user in the logs `. + +Run without configuration +^^^^^^^^^^^^^^^^^^^^^^^^^ + +It is now possible to execute PostgREST without specifying any configuration variable. The three that were mandatory on the previous versions, are no longer so. + + - If :ref:`db-uri` is not set, PostgREST will use the `libpq environment variables `_ for the database connection. + - If :ref:`db-schemas` is not set, it will use the database ``public`` schema. + - If :ref:`db-anon-role` is not set, it will not allow anonymous requests. + +Documentation improvements +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Added a :doc:`/how-tos/working-with-postgresql-data-types` how-to, which contains explanations and examples on how to work with different PostgreSQL data types such as timestamps, ranges or PostGIS types, among others. + +* Added in-database and environment variable settings for each :ref:`configuration variable `. + +* Added the :ref:`file_descriptors` subsection. + +* Added a reference page for :doc:`Error documentation `. + +* Moved the :ref:`error_source` and the :ref:`status_codes` sections to the :doc:`errors reference page `. + +* Moved the *Casting type to custom JSON* how-to to the :ref:`casting_range_to_json` subsection. + +* Removed direct links for PostgREST versions older than 8.0 from the versions menu. + +* Removed the deprecated *Embedding table from another schema* how-to. + +* Restructured the :ref:`resource_embedding` section: + + - Added a :ref:`one-to-many` and :ref:`many-to-one` subsections. + + - Renamed the *Embedding through join tables* subsection to :ref:`many-to-many`. + +Bug fixes +--------- + +* Return ``204 No Content`` without ``Content-Type`` for ``PUT`` (`#2058 `_) + +* Clarify error for failed schema cache load. (`#2107 `_) + + - From ``Database connection lost. Retrying the connection`` to ``Could not query the database for the schema cache. Retrying.`` + +* Fix silently ignoring filter on a non-existent embedded resource (`#1771 `_) + +* Remove functions, which are not callable due to unnamed arguments, from schema cache and OpenAPI output. (`#2152 `_) + +* Fix accessing JSON array fields with ``->`` and ``->>`` in ``?select=`` and ``?order=``. (`#2145 `_) + +* Ignore ``max-rows`` on ``POST``, ``PATCH``, ``PUT`` and ``DELETE`` (`#2155 `_) + +* Fix inferring a foreign key column as a primary key column on views (`#2254 `_) + +* Restrict generated many-to-many relationships (`#2070 `_) + + - Only adds many-to-many relationships when a table has foreign keys to two other tables and these foreign key columns are part of the table's primary key columns. + +* Allow casting to types with underscores and numbers (e.g. ``select=oid_array::_int4``) (`#2278 `_) + +* Prevent views from breaking one-to-many/many-to-one embeds when using column or foreign key as target (`#2277 `_, `#2238 `_, `#1643 `_) + + - When using a column or foreign key as target for embedding (``/tbl?select=*,col-or-fk(*)``), only tables are now detected and views are not. + + - You can still use a column or an inferred foreign key on a view to embed a table (``/view?select=*,col-or-fk(*)``) + +* Increase the ``db-pool-timeout`` to 1 hour to prevent frequent high connection latency (`#2317 `_) + +* The search path now correctly identifies schemas with uppercase and special characters in their names (regression) (`#2341 `_) + +* "404 Not Found" on nested routes and "405 Method Not Allowed" errors no longer start an empty database transaction (`#2364 `_) + +* Fix inaccurate result count when an inner embed was selected after a normal embed in the query string (`#2342 `_) + +* ``OPTIONS`` requests no longer start an empty database transaction (`#2376 `_) + +* Allow using columns with dollar sign ($) without double quoting in filters and ``select`` (`#2395 `_) + +* Fix loop crash error on startup in PostgreSQL 15 beta 3. ``Log: "UNION types \"char\" and text cannot be matched."`` (`#2410 `_) + +* Fix race conditions managing database connection helper (`#2397 `_) + +* Allow ``limit=0`` in the request query to return an empty array (`#2269 `_) + +Breaking changes +---------------- + +* Return ``204 No Content`` without ``Content-Type`` for RPCs returning ``VOID`` (`#2001 `_) + + - Previously, those RPCs would return ``null`` as a body with ``Content-Type: application/json``. + +* ``limit/offset`` now limits the affected rows on ``UPDATE``/``DELETE`` (`#2156 `_) + + - Previously, ``limit``/``offset`` only limited the returned rows but not the actual updated rows + +* ``max-rows`` is no longer applied on ``POST``, ``PATCH``, ``PUT`` and ``DELETE`` returned rows (`#2155 `_) + + - This was misleading because the affected rows were not really affected by ``max-rows``, only the returned rows were limited + +* Restrict generated many-to-many relationships (`#2070 `_) + + - A primary key that contains the foreign key columns is now needed for generating many-to-many relationships. + +* Views now are not detected when embedding using the column or foreign key as target (``/view?select=*,column(*)``) (`#2277 `_) + + - This embedding form was easily made ambiguous whenever a new view was added. + + - For migrating, clients must be updated to the embedding form of ``/view?select=*,other_view!column(*)``. + +* Using ``Prefer: return=representation`` no longer returns a ``Location`` header (`#2312 `_) + +Migration Guide +~~~~~~~~~~~~~~~ + +Many-to-may relationships +^^^^^^^^^^^^^^^^^^^^^^^^^ + +The way PostgREST infers many-to-many relationships is now restricted. Before this change, a table could work as an intermediate join between two tables just by having foreign keys referencing each one of them. Consider the following: + +.. code-block:: postgresql + + CREATE TABLE users ( + id INT PRIMARY KEY, + name TEXT + ); + + CREATE TABLE permissions ( + id INT PRIMARY KEY, + name TEXT + ); + + CREATE TABLE permission_user ( + id INT PRIMARY KEY, + user_id INT REFERENCES users(id), + permission_id INT REFERENCES permissions(id) + ); + +Before, PostgREST could infer a relationship between ``users`` and ``permissions`` through ``permission_user``. + +.. tabs:: + + .. code-tab:: http + + GET /users?select=permissions(*) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/users?select=permissions(*)" + +But now this is not allowed. In order for it to work, the intermediate table must also have the foreign keys included in its primary key. So, in this case we need to do the following: + +.. code-block:: postgresql + + -- This table has a pk defined already so we drop it first + alter table permission_user + drop constraint permission_user_pkey; + + -- Then we add all the foreign keys to the primary key + alter table permission_user + add primary key (id, user_id, permission_id); + +With this, PostgREST 10 will infer successfully a relationship between ``users`` and ``permissions``. + +If you want an alternative to the previous method or need a more customized relationship, you could use :ref:`computed_relationships` to get a similar result. + +Embedding views +^^^^^^^^^^^^^^^ + +Using column names or foreign key constraint names as :ref:`embedding targets ` will not detect views anymore. Consider this as an example: + +.. code-block:: postgresql + + CREATE TABLE users ( + id INT PRIMARY KEY, + name TEXT, + is_active BOOL + ); + + CREATE TABLE messages ( + id INT PRIMARY KEY, + body TEXT, + user_id INT REFERENCES users(id) + ); + + CREATE VIEW active_users AS + SELECT * + FROM users + WHERE is_active; + +Previously, the following request returned a ``300 Multiple Choices`` error, because the ``active_users`` view was also detected: + +.. tabs:: + + .. code-tab:: http + + GET /messages?select=body,user_id(name) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/messages?select=body,user_id(name)" + +But in this version, this will not fail and will embed the table ``users`` instead. You need to use the view name as target in order to embed it, like this: + +.. tabs:: + + .. code-tab:: http + + GET /messages?select=body,active_users(name) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000messages?select=body,active_users(name)" + +For other cases, adding a column or foreign key as :ref:`hint ` may be needed. + +You could also use :ref:`computed_relationships` to get a similar result or if you want a more customized relationship. + +Thanks +------ + +Big thanks from the `PostgREST team `_ to our 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/2ndquadrant.png + :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo + :width: 13em + + .. image:: ../_static/retool.png + :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + + .. image:: ../_static/gnuhost.png + :target: https://gnuhost.eu/?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/oblivious.jpg + :target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + +* Evans Fernandes +* `Jan Sommer `_ +* `Franz Gusenbauer `_ +* `Daniel Babiak `_ +* Tsingson Qin +* Michel Pelletier +* Jay Hannah +* Robert Stolarz +* Nicholas DiBiase +* Christopher Reid +* Nathan Bouscal +* Daniel Rafaj +* David Fenko +* Remo Rechkemmer +* Severin Ibarluzea +* Tom Saleeba +* Pawel Tyll + +If you like to join them please consider `supporting PostgREST development `_. diff --git a/postgrest.dict b/postgrest.dict index d1430e1f6d..2105b58390 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -43,6 +43,7 @@ filename FreeBSD fts GC +GeoJSON GHC Github Google @@ -118,6 +119,7 @@ phraseto plainto plfts poolers +POSIX PostGIS PostgreSQL PostgreSQL's @@ -165,6 +167,7 @@ Stolarz subselect SuperAgent SvelteKit +SwaggerUI syslog systemd Tcl @@ -200,3 +203,5 @@ Websockets webuser wfts ZeroMQ +Customizable +customizable From 9128c8602ae5b2ff3ecea50eee1f3ecaedce0d85 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Fri, 26 Aug 2022 16:08:08 -0500 Subject: [PATCH 522/549] Refine docs for v10 * shorten release page * shorten explain docs * add limited update/delete to release page * refine relationships * refine disambiguation * refine release page * remove migration guide * add author to WWT how-to --- docs/api.rst | 272 +++++++++++------- .../working-with-postgresql-data-types.rst | 2 + docs/releases/v10.0.0.rst | 244 +++++----------- 3 files changed, 244 insertions(+), 274 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 50d78aec41..bc95ea3372 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -875,52 +875,15 @@ returned together. For example, consider a database of films and their awards: .. important:: - * PostgREST needs `FOREIGN KEY constraints `_ to be able to do Resource Embedding. - * Whenever FOREIGN KEY constraints change in the database schema you must refresh PostgREST's schema cache for Resource Embedding to work properly. See the section :ref:`schema_reloading`. - -.. _one-to-many: - -One-to-many relationships -------------------------- - -When a one-to-many relationship is detected, the embedded resource is returned as a JSON array. For example, we can request the Directors and the Films they directed because there is a foreign key constraint between them, like this: - -.. tabs:: - - .. code-tab:: http - - GET /directors?select=last_name,films(title) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/directors?select=last_name,films(title)" - -.. code-block:: json - - [ - { "last_name": "Lumière", - "films": [ - {"title": "Workers Leaving The Lumière Factory In Lyon"} - ] - }, - { "last_name": "Dickson", - "films": [ - {"title": "The Dickson Experimental Sound Film"} - ] - }, - { "last_name": "Méliès", - "films": [ - {"title": "The Haunted Castle"} - ] - } - ] + Whenever FOREIGN KEY constraints change in the database schema you must refresh PostgREST's schema cache for Resource Embedding to work properly. See the section :ref:`schema_reloading`. .. _many-to-one: Many-to-one relationships ------------------------- -When a many-to-one relationship is detected, the embedded resource is returned as a JSON object. For example, we can request all the Films and the Director for each film like this: +Since ``films`` has a **foreign key** referencing ``directors``, this establishes a many-to-one relationship between them. Because of this, we're able +to request all the films and the director for each film. .. tabs:: @@ -955,7 +918,9 @@ When a many-to-one relationship is detected, the embedded resource is returned a } ] -However, the table name is in plural, which is not accurate since a Film is directed by only one Director. Using a table name alias can solve this: +Note that the embedded ``directors`` is returned as a JSON object because of the "to-one" end. + +Since the table name is plural, we can be more accurate by making it singular with an alias. .. tabs:: @@ -967,13 +932,64 @@ However, the table name is in plural, which is not accurate since a Film is dire curl "http://localhost:3000/films?select=title,director:directors(id,last_name)" +.. code-block:: json + + [ + { "title": "Workers Leaving The Lumière Factory In Lyon", + "director": { + "id": 2, + "last_name": "Lumière" + } + }, + ".." + ] + +.. _one-to-many: + +One-to-many relationships +------------------------- + +The inverse one-to-many relationship between ``directors`` and ``films`` is detected based on the **foreign key** reference. In this case, the embedded ``films`` are returned as a JSON array because of the "to-many" end. + +.. tabs:: + + .. code-tab:: http + + GET /directors?select=last_name,films(title) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/directors?select=last_name,films(title)" + +.. code-block:: json + + [ + { "last_name": "Lumière", + "films": [ + {"title": "Workers Leaving The Lumière Factory In Lyon"} + ] + }, + { "last_name": "Dickson", + "films": [ + {"title": "The Dickson Experimental Sound Film"} + ] + }, + { "last_name": "Méliès", + "films": [ + {"title": "The Haunted Castle"} + ] + } + ] + .. _many-to-many: Many-to-many relationships -------------------------- -PostgREST can also detect many-to-many relationships going through join tables. For this, the join table must contain foreign keys to the tables in -the many-to-many relationship and its composite primary key must include these foreign key columns. +Many-to-many relationships are detected based on the join table. The join table must contain foreign keys to other two tables +and they must be part of its composite key. + +For the many-to-many relationship between ``films`` and ``actors``, the join table ``roles`` would be: .. code-block:: postgresql @@ -983,8 +999,7 @@ the many-to-many relationship and its composite primary key must include these f , primary key(film_id, actor_id) ); - -- the many-to-many relationship can also be detected if the join table has a surrogate key, - -- as long as the foreign key columns are also part of the primary key + -- the join table can also be detected if the composite key has additional columns create table roles( id int generated always as identity, @@ -993,95 +1008,167 @@ the many-to-many relationship and its composite primary key must include these f , primary key(id, film_id, actor_id) ); -Then you can request the Actors for Films (which in this case finds the information through Roles). - .. tabs:: .. code-tab:: http - GET /actors?select=films(title,year) HTTP/1.1 + GET /actors?select=first_name,last_name,films(title) HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/actors?select=films(title,year)" + curl "http://localhost:3000/actors?select=first_name,last_name,films(title)" + +.. code-block:: json + + [ + { "first_name": "Willem", + "last_name": "Dafoe", + "films": [ + {"title": "The Lighthouse"} + ] + }, + ".." + ] .. _one-to-one: One-to-one relationships ------------------------ -PostgREST detects one-to-one relationships when a foreign key is also the primary key of the table or when the foreign key has a ``UNIQUE`` constraint. +one-to-one relationships are detected if there's an unique constraint on a foreign key. .. code-block:: postgresql - -- references Films using the primary key as a foreign key CREATE TABLE technical_specs( - film_id INT PRIMARY KEY REFERENCES films, + film_id INT REFERENCES films UNIQUE, runtime TIME, camera TEXT, sound TEXT ); - -- references Films using a foreign key with unique constraint +Or if the foreign key is also a primary key. + +.. code-block:: postgresql + + -- references Films using the primary key as a foreign key CREATE TABLE technical_specs( - film_id INT REFERENCES films UNIQUE, + film_id INT PRIMARY KEY REFERENCES films, runtime TIME, camera TEXT, sound TEXT ); -Now, the embedding between Films and Technical_Specs is returned as a JSON object no matter the order. - .. tabs:: .. code-tab:: http - GET /films?select=title,technical_specs(*) HTTP/1.1 + GET /films?select=title,technical_specs(runtime) HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/films?select=title,technical_specs(*)" + curl "http://localhost:3000/films?select=title,technical_specs(runtime)" + +.. code-block:: json + + [ + { + "title": "Pulp Fiction", + "technical_specs": {"camera": "Arriflex 35-III"} + }, + ".." + ] .. _computed_relationships: -Computed Relationships +Computed relationships ---------------------- -You can customize how PostgREST detects relationships between two tables. To do this, you need to create a function that has one of the tables as a single parameter and the other as its return type. For instance: +You can manually define relationships between resources. This is useful for database objects that can't define foreign keys, like `Foreign Data Wrappers `_. +To do this, you can create functions similar to :ref:`computed_cols`. + +Assuming there's a foreign table ``premieres`` that we want to relate to ``films``. .. code-block:: postgres - CREATE FUNCTION director_competition(directors) RETURNS SETOF competitions AS $$ - SELECT c.* - FROM competitions c - JOIN nominations n ON c.id = n.competition_id - JOIN films f ON n.film_id = f.id - WHERE f.director_id = $1.id - $$ STABLE LANGUAGE sql; + create foreign table premieres ( + id integer, + location text, + "date" date, + film_id integer + ) server import_csv options ( filename '/tmp/directors.csv', format 'csv'); -The above function allows a direct relationship between ``directors`` and ``competitions``: + create function film(premieres) returns setof films rows 1 as $$ + select * from films where id = $1.film_id + $$ stable language sql; + +The above function defines a relationship between ``premieres`` (the parameter) and ``films`` (the return type) and since there's a ``rows 1``, this defines a many-to-one relationship. +The name of the function ``film`` is arbitrary and can be used to do the embedding: + +.. tabs:: + + .. code-tab:: http + + GET /premieres?select=location,film(name) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/premieres?select=location,film(name)" + +.. code-block:: json + + [ + { + "location": "Cannes Film Festival", + "film": {"name": "Pulp Fiction"} + }, + ".." + ] + +Now let's define the opposite one-to-many relationship with another function. + +.. code-block:: postgres + + create function premieres(films) returns setof premieres as $$ + select * from premieres where film_id = $1.director_id + $$ stable language sql; + +Similarly, this function defines a relationship between the parameter ``films`` and the return type ``premieres``. +In this case there's an implicit ``ROWS 1000`` defined by PostgreSQL(`search "result_rows" on this PostgreSQL doc `_), +we consider any value greater than 1 as "many" so this defines a one-to-many relationship. .. tabs:: .. code-tab:: http - GET /directors?select=*,competitions:director_competition(name) HTTP/1.1 + GET /films?select=name,premieres(name) HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/directors?select=*,competitions:director_competition(name)" + curl "http://localhost:3000/films?select=name,premieres(name)" + +.. code-block:: json + + [ + { + "name": "Pulp Ficiton", + "premieres": [{"location": "Cannes Festival"}] + }, + ".." + ] -Take into consideration that the opposite relationship will not be detected, so you need to create another function for that. +Computed relationships also allow you to override the ones that are automatically detected by PostgREST. -Computed relationships also allow you to override the ones that are detected by default. For example, this function can change the ``/films?select=directors(*)`` embedding: +For example, to override the :ref:`many-to-one relationship ` between ``films`` and ``directors``. .. code-block:: postgres - CREATE FUNCTION directors(films) RETURNS SETOF directors ROW 1 AS $$ - -- Override the relationship here - $$ STABLE LANGUAGE sql; + create function directors(films) returns setof directors rows 1 as $$ + select * from directors where id = $1.director_id + $$ stable language sql; + +Taking advantage of overloaded functions, you can use the same function name for different parameters and thus define relationships from other tables/views to ``directors``. -Note that if ``ROW 1`` is added, PostgREST will detect a :ref:`many-to-one relationship ` and return a JSON object instead of an array embedding. +Computed relationships have good performance as they follow the `Inlining conditions for table functions `_. .. _nested_embedding: @@ -1327,22 +1414,15 @@ Since this view contains ``nominations.film_id``, which has a **foreign key** re It's also possible to embed `Materialized Views `_. -.. warning:: - - It's not guaranteed that all kinds of views will be embeddable. In particular, views that contain - UNIONs will not be made embeddable. - - Why? PostgREST detects source table foreign keys in the view by querying and parsing `pg_rewrite `_. - This may fail depending on the complexity of the view. - - `Report an issue `_ if your view is not made embeddable so we can - keep continue improving foreign key detection. +.. important:: - In the future we'll include a way to manually specify views source foreign keys to address this limitation. + - It's not guaranteed that all kinds of views will be embeddable. In particular, views that contain UNIONs will not be made embeddable. -.. important:: + + Why? PostgREST detects source table foreign keys in the view by querying and parsing `pg_rewrite `_. + This may fail depending on the complexity of the view. + + As a workaround, you can use :ref:`computed_relationships` to define manual relationships for views. - If view definitions change you must refresh PostgREST's schema cache for this to work properly. See the section :ref:`schema_reloading`. + - If view definitions change you must refresh PostgREST's schema cache for this to work properly. See the section :ref:`schema_reloading`. .. _embedding_view_chains: @@ -1603,7 +1683,7 @@ Hints also work alongside ``!inner`` if a top level filtering is needed. From th .. note:: - If the relationship is so complex that hint disambiguation does not solve it, then using :ref:`computed_relationships` is the best alternative. + If the relationship is so complex that hint disambiguation does not solve it, you can use :ref:`computed_relationships`. .. _insert: @@ -2887,9 +2967,7 @@ Returns: Execution plan -------------- -You can get the execution plan of a request by adding the ``Accept: application/vnd.pgrst.plan`` header after setting the :ref:`db-plan-enabled` configuration to ``true``. It is useful to verify why a certain operation might be expensive as a result of using `EXPLAIN `_ on the generated query for the request. - -The output of the plan is generated in ``text`` format by default: +You can get the `EXPLAIN execution plan `_ of a request by adding the ``Accept: application/vnd.pgrst.plan`` header when :ref:`db-plan-enabled` is set to ``true``. .. tabs:: @@ -2908,7 +2986,7 @@ The output of the plan is generated in ``text`` format by default: 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 same execution can be returned in ``json`` format by using the ``Accept: application/vnd.pgrst.plan+json`` header instead: +The output of the plan is generated in ``text`` format by default but you can change it to JSON by using the ``+json`` suffix. .. tabs:: @@ -2956,8 +3034,8 @@ The same execution can be returned in ``json`` format by using the ``Accept: app } ] -You can also get the result plan of the different media types that PostgREST supports by adding them to the header using ``for``. For instance, to obtain the plan for a :ref:`text/xml ` media type in json format, you need to add the ``Accept: application/vnd.pgrst.plan; for=text/xml`` header. +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 ` 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``. -Additionally, the deactivated parameters of the ``EXPLAIN`` command can be enabled by adding them to the header using ``options``. The available parameters are ``analyze``, ``verbose``, ``settings``, ``buffers`` and ``wal``, while the remaining ones are active by default. For example, to add the ``analyze`` and ``wal`` parameters, add the ``Accept: application/vnd.pgrst.plan; options=analyze|wal`` header. +The other available parameters are ``analyze``, ``verbose``, ``settings``, ``buffers`` and ``wal``, which correspond to the `EXPLAIN command options `_. To use the ``analyze`` and ``wal`` parameters for example, you would add them like ``Accept: application/vnd.pgrst.plan; options=analyze|wal``. -Note that any changes done will be committed when activating the ``analyze`` option. To avoid this, set the :ref:`db-tx-end` configuration in a way that allows to rollback the changes according to your preference. +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. diff --git a/docs/how-tos/working-with-postgresql-data-types.rst b/docs/how-tos/working-with-postgresql-data-types.rst index fb317c97e9..68a50fa24d 100644 --- a/docs/how-tos/working-with-postgresql-data-types.rst +++ b/docs/how-tos/working-with-postgresql-data-types.rst @@ -3,6 +3,8 @@ Working with PostgreSQL data types ================================== +:author: `Laurence Isla `_ + 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:: diff --git a/docs/releases/v10.0.0.rst b/docs/releases/v10.0.0.rst index 4d2a6aea89..7dc46fedae 100644 --- a/docs/releases/v10.0.0.rst +++ b/docs/releases/v10.0.0.rst @@ -5,79 +5,65 @@ PostgREST 10.0.0 Features -------- -API -~~~ - XML/SOAP support for RPC -^^^^^^^^^^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~~~~~~~~~~ -RPC now understands the ``text/xml`` media type, allowing SQL functions to send XML output(``Accept: text/xml``) and receive XML input(``Content-Type: text/xml``). This makes SOAP endpoints possible, check the :ref:`create_soap_endpoint` how-to for more details. +RPC now understands the ``text/xml`` media type, allowing SQL functions to send XML output(``Accept: text/xml``) and receive XML input(``Content-Type: text/xml``). This makes SOAP endpoints possible, check the :ref:`create_soap_endpoint` how-to and the :ref:`scalar_return_formats` reference for more details. GeoJSON support -^^^^^^^^^^^^^^^ +~~~~~~~~~~~~~~~ GeoJSON is supported across the board(reads, writes, RPC) with the ``Accept: application/geo+json`` header, this depends on PostGIS from the versions 3.0.0 and up. The :ref:`working with PostGIS section ` has an example to get you started. -One-to-one relationships -^^^^^^^^^^^^^^^^^^^^^^^^ +Execution Plan +~~~~~~~~~~~~~~ -A :ref:`one-to-one relationship ` is now detected when a table's foreign key is also its primary key or when the foreign key has a ``UNIQUE`` constraint. +The :ref:`execution plan ` of a request is now obtainable with the ``Accept: application/vnd.pgrst.plan`` header. The result can be in ``text`` or ``json`` formats and is compatible with EXPLAIN vizualizers like `explain.depesz.com `_ or `explain.dalibo.com `_. -Customizable Relationships for Resource Embedding -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Resource Embedding +~~~~~~~~~~~~~~~~~~ -Using :ref:`computed_relationships`, you can add custom relationships or override automatically detected ones. This makes :ref:`resource_embedding` possible on Foreign Data Wrappers and complex SQL views. +- A :ref:`one-to-one relationship ` is now detected when a foreign key is unique. -EXPLAIN Execution Plan -^^^^^^^^^^^^^^^^^^^^^^ +- Using :ref:`computed_relationships`, you can add custom relationships or override automatically detected ones. This makes :ref:`resource_embedding` possible on Foreign Data Wrappers and complex SQL views. -The :ref:`EXPLAIN execution plan of a request ` is now obtainable with the ``Accept: application/vnd.pgrst.plan`` header. The result can be in ``text`` or ``json`` formats and is compatible with EXPLAIN vizualizers like `explain.depesz.com `_ or `explain.dalibo.com `_. +Horizontal/Vertical Filtering +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -POSIX Regular Expressions -^^^^^^^^^^^^^^^^^^^^^^^^^ +- :ref:`Accessing fields of a Composite type or elements of an Array type ` is now possible with the arrow operators(``->``, ``->>``) in the same way you would access a JSON type fields. -You can now use two :ref:`pattern matching ` operators for `POSIX regular expressions `_: ``match`` and ``imatch``, equivalent in PostgreSQL to ``~`` and ``~*`` respectively. +- :ref:`pattern_matching` operators for `POSIX regular expressions `_ are now available: ``match`` and ``imatch``, equivalent in PostgreSQL to ``~`` and ``~*`` respectively. -Access composite type fields and array elements -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Insertions/Updates +~~~~~~~~~~~~~~~~~~ -:ref:`Accessing fields of a Composite type or elements of an Array type ` is now possible with the arrow operators(``->``, ``->>``) in the same way you would access a JSON type fields. +- ``limit`` can now affect the number of updated/deleted rows. See :ref:`limited_update_delete`. -Authorize button for SwaggerUI -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +OpenAPI +~~~~~~~ You can now activate the "Authorize" button in SwaggerUI by enabling the :ref:`openapi-security-active` configuration. Add your JWT token prepending :code:`Bearer` to it and you'll be able to request protected resources. -Improved error messages -^^^^^^^^^^^^^^^^^^^^^^^ - -To increase consistency, all the errors messages are now normalized. The ``hint``, ``details``, ``code`` and ``message`` fields will always be present in the body, each one defaulting to a -``null`` value. In the same way, the :ref:`errors that were raised ` with ``SQLSTATE`` now include the ``message`` and ``code`` in the body. - -To further clarify the source of an error, we now add a ``PGRST`` prefix to the error code of all the errors that are PostgREST-specific and don't come from the database. These errors have unique codes that identifies them and are documented in the :ref:`pgrst_errors` section. - Administration ~~~~~~~~~~~~~~ -Health checks -^^^^^^^^^^^^^ +- Two :ref:`health check endpoints ` are now exposed in a secondary port. -Admins can now benefit from two :ref:`health check endpoints ` exposed in a different port than the main app. When activated, the ``live`` and ``ready`` endpoints are available to verify if PostgREST is alive and running or if the database connection and the :ref:`schema cache ` are ready for querying. +- :ref:`pgrst_logging` now shows the database user. -Logging users -^^^^^^^^^^^^^ - -You can now see the :ref:`request database user in the logs `. - -Run without configuration -^^^^^^^^^^^^^^^^^^^^^^^^^ - -It is now possible to execute PostgREST without specifying any configuration variable. The three that were mandatory on the previous versions, are no longer so. +- It is now possible to execute PostgREST without specifying any configuration variable. The three that were mandatory on the previous versions, are no longer so. - If :ref:`db-uri` is not set, PostgREST will use the `libpq environment variables `_ for the database connection. - If :ref:`db-schemas` is not set, it will use the database ``public`` schema. - If :ref:`db-anon-role` is not set, it will not allow anonymous requests. +Error messages +~~~~~~~~~~~~~~ + +- To increase consistency, all the errors messages are now normalized. The ``hint``, ``details``, ``code`` and ``message`` fields will always be present in the body, each one defaulting to a ``null`` value. In the same way, the :ref:`errors that were raised ` with ``SQLSTATE`` now include the ``message`` and ``code`` in the body. + +- To further clarify the source of an error, we now add a ``PGRST`` prefix to the error code of all the errors that are PostgREST-specific and don't come from the database. These errors have unique codes that identify them and are documented in the :ref:`pgrst_errors` section. + Documentation improvements ~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -95,7 +81,7 @@ Documentation improvements * Removed direct links for PostgREST versions older than 8.0 from the versions menu. -* Removed the deprecated *Embedding table from another schema* how-to. +* Removed the *Embedding table from another schema* how-to. * Restructured the :ref:`resource_embedding` section: @@ -103,6 +89,43 @@ Documentation improvements - Renamed the *Embedding through join tables* subsection to :ref:`many-to-many`. +* Split up the *Insertions/Updates* section into :ref:`insert` and :ref:`update`. + +Breaking changes +---------------- + +* Many-to-many relationships now require that foreign key columns be part of the join table composite key + + - This was needed to reduce :ref:`embed_disamb` errors in complex schemas(`#2070 `_). + + - For migrating to this version, the less invasive method is to use :ref:`computed_relationships` to replace the previous many-to-many relationships. + + - Otherwise you can change your join table primary key. For example with ``alter table permission_user drop constraint permission_user_pkey, add primary key (id, user_id, permission_id);`` + +* Views now are not detected when embedding using :ref:`target_disamb`. + + - This embedding form was easily made ambiguous whenever a new view was added(`#2277 `_). + + - For migrating to this version, you can use :ref:`computed_relationships` to replace the previous view relationships. + + - :ref:`hint_disamb` works as usual on views. + +* ``limit/offset`` now limits the affected rows on ``UPDATE``/``DELETE`` + + - Previously, ``limit``/``offset`` only limited the returned rows but not the actual updated rows(`#2156 `_) + +* ``max-rows`` is no longer applied on ``POST``, ``PATCH``, ``PUT`` and ``DELETE`` returned rows + + - This was misleading because the affected rows were not really affected by ``max-rows``, only the returned rows were limited(`#2155 `_) + +* Return ``204 No Content`` without ``Content-Type`` for RPCs returning ``VOID`` + + - Previously, those RPCs would return ``null`` as a body with ``Content-Type: application/json`` (`#2001 `_). + +* Using ``Prefer: return=representation`` no longer returns a ``Location`` header + + - This reduces unnecessary computing for all insertions (`#2312 `_) + Bug fixes --------- @@ -152,139 +175,6 @@ Bug fixes * Allow ``limit=0`` in the request query to return an empty array (`#2269 `_) -Breaking changes ----------------- - -* Return ``204 No Content`` without ``Content-Type`` for RPCs returning ``VOID`` (`#2001 `_) - - - Previously, those RPCs would return ``null`` as a body with ``Content-Type: application/json``. - -* ``limit/offset`` now limits the affected rows on ``UPDATE``/``DELETE`` (`#2156 `_) - - - Previously, ``limit``/``offset`` only limited the returned rows but not the actual updated rows - -* ``max-rows`` is no longer applied on ``POST``, ``PATCH``, ``PUT`` and ``DELETE`` returned rows (`#2155 `_) - - - This was misleading because the affected rows were not really affected by ``max-rows``, only the returned rows were limited - -* Restrict generated many-to-many relationships (`#2070 `_) - - - A primary key that contains the foreign key columns is now needed for generating many-to-many relationships. - -* Views now are not detected when embedding using the column or foreign key as target (``/view?select=*,column(*)``) (`#2277 `_) - - - This embedding form was easily made ambiguous whenever a new view was added. - - - For migrating, clients must be updated to the embedding form of ``/view?select=*,other_view!column(*)``. - -* Using ``Prefer: return=representation`` no longer returns a ``Location`` header (`#2312 `_) - -Migration Guide -~~~~~~~~~~~~~~~ - -Many-to-may relationships -^^^^^^^^^^^^^^^^^^^^^^^^^ - -The way PostgREST infers many-to-many relationships is now restricted. Before this change, a table could work as an intermediate join between two tables just by having foreign keys referencing each one of them. Consider the following: - -.. code-block:: postgresql - - CREATE TABLE users ( - id INT PRIMARY KEY, - name TEXT - ); - - CREATE TABLE permissions ( - id INT PRIMARY KEY, - name TEXT - ); - - CREATE TABLE permission_user ( - id INT PRIMARY KEY, - user_id INT REFERENCES users(id), - permission_id INT REFERENCES permissions(id) - ); - -Before, PostgREST could infer a relationship between ``users`` and ``permissions`` through ``permission_user``. - -.. tabs:: - - .. code-tab:: http - - GET /users?select=permissions(*) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/users?select=permissions(*)" - -But now this is not allowed. In order for it to work, the intermediate table must also have the foreign keys included in its primary key. So, in this case we need to do the following: - -.. code-block:: postgresql - - -- This table has a pk defined already so we drop it first - alter table permission_user - drop constraint permission_user_pkey; - - -- Then we add all the foreign keys to the primary key - alter table permission_user - add primary key (id, user_id, permission_id); - -With this, PostgREST 10 will infer successfully a relationship between ``users`` and ``permissions``. - -If you want an alternative to the previous method or need a more customized relationship, you could use :ref:`computed_relationships` to get a similar result. - -Embedding views -^^^^^^^^^^^^^^^ - -Using column names or foreign key constraint names as :ref:`embedding targets ` will not detect views anymore. Consider this as an example: - -.. code-block:: postgresql - - CREATE TABLE users ( - id INT PRIMARY KEY, - name TEXT, - is_active BOOL - ); - - CREATE TABLE messages ( - id INT PRIMARY KEY, - body TEXT, - user_id INT REFERENCES users(id) - ); - - CREATE VIEW active_users AS - SELECT * - FROM users - WHERE is_active; - -Previously, the following request returned a ``300 Multiple Choices`` error, because the ``active_users`` view was also detected: - -.. tabs:: - - .. code-tab:: http - - GET /messages?select=body,user_id(name) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/messages?select=body,user_id(name)" - -But in this version, this will not fail and will embed the table ``users`` instead. You need to use the view name as target in order to embed it, like this: - -.. tabs:: - - .. code-tab:: http - - GET /messages?select=body,active_users(name) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000messages?select=body,active_users(name)" - -For other cases, adding a column or foreign key as :ref:`hint ` may be needed. - -You could also use :ref:`computed_relationships` to get a similar result or if you want a more customized relationship. - Thanks ------ From e7413f3aeae53b2f2034f13106e7941157a86e43 Mon Sep 17 00:00:00 2001 From: Andrea Bernicchia <51401007+abernicchia-heroku@users.noreply.github.com> Date: Thu, 27 Oct 2022 17:58:17 +0200 Subject: [PATCH 523/549] Update heroku installation docs * Heroku installation docs aligned with https://github.com/PostgREST/postgrest-heroku/pull/40 * Update install.rst --- docs/install.rst | 123 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 115 insertions(+), 8 deletions(-) diff --git a/docs/install.rst b/docs/install.rst index ec3c97721b..39de4f0630 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -260,13 +260,120 @@ You can build PostgREST from source with `Stack `_: -1. Create a new app on Heroku -2. In Settings add the following buildpack :code:`https://github.com/PostgREST/postgrest-heroku` -3. Add the require Config Vars in Heroku -4. Modify your ``postgrest.conf`` file as required to match your Config Vars in Heroku -5. Create your :code:`Procfile` and add :code:`./env-to-config ./postgrest postgrest.conf` -6. Push your changes to GitHub -7. Set Heroku to automatically deploy from Main and then manually deploy the branch for the first build + .. code-block:: bash + + # If you have multiple Heroku accounts, use flag '--interactive' to switch between them + heroku login --interactive + + +2. Create a new Heroku app using the PostgREST buildpack: + + .. code-block:: bash + + mkdir ${YOUR_APP_NAME} + cd ${YOUR_APP_NAME} + git init . + + heroku apps:create ${YOUR_APP_NAME} --buildpack https://github.com/PostgREST/postgrest-heroku.git + heroku git:remote -a ${YOUR_APP_NAME} + +3. Create a new Heroku PostgreSQL add-on attached to the app and keep notes of the assigned add-on name (e.g. :code:`postgresql-curly-58902`) referred later as ${HEROKU_PG_DB_NAME} + + .. code-block:: bash + + heroku addons:create heroku-postgresql:standard-0 -a ${YOUR_APP_NAME} + # wait until the add-on is available + heroku pg:wait -a ${YOUR_APP_NAME} + +4. Create the necessary user roles according to the + `PostgREST documentation `_: + + .. code-block:: bash + + heroku pg:credentials:create --name api_user -a ${YOUR_APP_NAME} + # use the following command to ensure the new credential state is active before attaching it + heroku pg:credentials -a ${YOUR_APP_NAME} + + heroku addons:attach ${HEROKU_PG_DB_NAME} --credential api_user -a ${YOUR_APP_NAME} + +5. Connect to the PostgreSQL database and create some sample data: + + .. code-block:: bash + + heroku psql -a ${YOUR_APP_NAME} + + .. code-block:: postgres + + # from the psql command prompt execute the following commands: + create schema api; + + 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'); + + grant usage on schema api to api_user; + grant select on api.todos to api_user; + +6. Create the :code:`Procfile`: + + .. code-block:: bash + + web: PGRST_SERVER_HOST=0.0.0.0 PGRST_SERVER_PORT=${PORT} PGRST_DB_URI=${PGRST_DB_URI:-${DATABASE_URL}} ./postgrest-${POSTGREST_VER} + .. + + Set the following environment variables on Heroku: + + .. code-block:: bash + + heroku config:set POSTGREST_VER=10.0.0 + heroku config:set PGRST_DB_SCHEMA=api + heroku config:set PGRST_DB_ANON_ROLE=api_user + .. + + PGRST_DB_URI can be set if an external database is used or if it's different from the default Heroku DATABASE_URL. This latter is used if nothing is provided. + POSTGREST_VER is mandatory to select and build the required PostgREST release. + + See https://postgrest.org/en/stable/configuration.html#environment-variables for the full list of environment variables. + +7. Build and deploy your app: + + .. code-block:: bash + + git add Procfile + git commit -m "PostgREST on Heroku" + git push heroku master + .. + + Your Heroku app should be live at :code:`${YOUR_APP_NAME}.herokuapp.com` + +8. Test your app + + From a terminal display the application logs: + + .. code-block:: bash + + heroku logs -t + .. + + From a different terminal retrieve with curl the records previously created: + + .. code-block:: bash + + curl https://${YOUR_APP_NAME}.herokuapp.com/todos + .. + + and test that any attempt to modify the table via a read-only user is not allowed: + + .. code-block:: bash + curl https://${YOUR_APP_NAME}.herokuapp.com/todos -X POST \ + -H "Content-Type: application/json" \ + -d '{"task": "do bad thing"}' From a65d387a3129f04615625f7bb080f4b89fe5674e Mon Sep 17 00:00:00 2001 From: mdr1384 <32360633+mdr1384@users.noreply.github.com> Date: Thu, 15 Sep 2022 09:32:24 -0400 Subject: [PATCH 524/549] Fix example code in computed relationships Looks like a copy-paste error - the `films.id` column should be associated with the `premieres.film_id` column. --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index bc95ea3372..89a48524ca 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1129,7 +1129,7 @@ Now let's define the opposite one-to-many relationship with another function. .. code-block:: postgres create function premieres(films) returns setof premieres as $$ - select * from premieres where film_id = $1.director_id + select * from premieres where film_id = $1.id $$ stable language sql; Similarly, this function defines a relationship between the parameter ``films`` and the return type ``premieres``. From 45ba35657b633cb186c2963df13f0e56a5913bc8 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 19 Sep 2022 21:09:32 -0500 Subject: [PATCH 525/549] Shorten explanations for the working with types section --- .../working-with-postgresql-data-types.rst | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/docs/how-tos/working-with-postgresql-data-types.rst b/docs/how-tos/working-with-postgresql-data-types.rst index 68a50fa24d..04e6562de1 100644 --- a/docs/how-tos/working-with-postgresql-data-types.rst +++ b/docs/how-tos/working-with-postgresql-data-types.rst @@ -14,7 +14,7 @@ PostgREST makes use of PostgreSQL string representations to work with data types Timestamps ---------- -You can use the **time zone** to filter or send data if needed. Let's use this table as an example: +You can use the **time zone** to filter or send data if needed. .. code-block:: postgres @@ -88,7 +88,7 @@ You can use other comparative filters and also all the `PostgreSQL special date/ JSON ---- -To work with a ``json`` type column, you can handle the value as a JSON object. For instance, let's use this table: +To work with a ``json`` type column, you can handle the value as a JSON object. .. code-block:: postgres @@ -98,7 +98,7 @@ To work with a ``json`` type column, you can handle the value as a JSON object. extra_info json ); -Now, you can insert a new product using a JSON object for the ``extra_info`` column: +You can insert a new product using a JSON object for the ``extra_info`` column: .. tabs:: @@ -136,7 +136,7 @@ To query and filter the data see :ref:`json_columns` for a complete reference. Arrays ------ -To handle `array types `_ you can use string representation or JSON array format. For instance, let's create the following table: +To handle `array types `_ you can use string representation or JSON array format. .. code-block:: postgres @@ -147,7 +147,7 @@ To handle `array types `_ y performance_times time[] ); -To insert a new value you can use string representation. +You can insert a new value using string representation. .. tabs:: @@ -176,7 +176,7 @@ To insert a new value you can use string representation. } EOF -Or you could send the data using a JSON array format. The following request sends the same data as the example above: +Or you could send the same data using JSON array format: .. tabs:: @@ -205,20 +205,20 @@ Or you could send the data using a JSON array format. The following request send } EOF -To query the data you can use the arrow operators. See :ref:`composite_array_columns`. +To query the data you can use arrow operators. See :ref:`composite_array_columns`. Multidimensional Arrays ~~~~~~~~~~~~~~~~~~~~~~~ -Handling multidimensional arrays is no different than handling one-dimensional ones: both the string representation and the JSON array format are allowed. For example, let's add a new column to the table: +Similarly to one-dimensional arrays, both the string representation and JSON array format are allowed. .. code-block:: postgres - -- The column stores the cinema, floor and auditorium numbers in that order + -- This new column stores the cinema, floor and auditorium numbers in that order alter table movies add column cinema_floor_auditorium int[][][]; -Now, let's update the row we inserted before using JSON array format: +You can now update the item using JSON array format: .. tabs:: @@ -241,7 +241,7 @@ Now, let's update the row we inserted before using JSON array format: } EOF -Now, 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: +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: .. tabs:: @@ -265,7 +265,7 @@ Now, for example, to query the auditoriums that are located in the first cinema Composite Types --------------- -With PostgREST, you have two options to handle `composite type columns `_. On one hand you can use string representation and on the other you can handle it as you would a JSON column. Let's create a type and a table for this example: +With PostgREST, you have two options to handle `composite type columns `_. .. code-block:: postgres @@ -284,7 +284,7 @@ With PostgREST, you have two options to handle `composite type columns `_, let's use the following table as an example: +PostgREST allows you to handle `ranges `_. .. code-block:: postgres @@ -353,7 +353,7 @@ To illustrate how to work with `ranges ` to filter the data. But what if you need get the events for the New Year 2023? Doing this filter ``events?duration=cs.2023-01-01`` will return an error because PostgreSQL needs an explicit cast to timestamp of the string value. A workaround would be to use a range starting and ending in the same date, like this: +You can use range :ref:`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: .. tabs:: @@ -470,7 +470,7 @@ Finally, do the request :ref:`casting the range column `: Bytea ----- -To send raw binary to PostgREST you need a function with a single unnamed parameter of `bytea type `_. For example, let's create a table that will save some files and a function that inserts data to that table: +To send raw binary to PostgREST you need a function with a single unnamed parameter of `bytea type `_. .. code-block:: postgres @@ -483,7 +483,7 @@ To send raw binary to PostgREST you need a function with a single unnamed parame insert into files (file) values ($1); $$ language sql; -Next, let's use the PostgREST logo for our test. +Let's download the PostgREST logo for our test. .. code-block:: bash @@ -506,8 +506,8 @@ Now, to send the file ``postgrest-logo.png`` we need to set the ``Content-Type: -X POST -H "Content-Type: application/octet-stream" \ --data-binary "@postgrest-logo.png" -To get the image from the database, you will need to set the ``Accept: application/octet-stream`` header in the request and select only the -``bytea`` column. +To get the image from the database, set the ``Accept: application/octet-stream`` header and select only the +``bytea`` type column. .. tabs:: @@ -521,7 +521,7 @@ To get the image from the database, you will need to set the ``Accept: applicati curl "http://localhost:3000/files?select=file&id=eq.1" \ -H "Accept: application/octet-stream" -You can also use more accurate headers depending on the type of the files by using the :ref:`raw-media-types` configuration. For example, adding the ``raw-media-types="image/png"`` setting to the configuration file will allow you to use the ``Accept: image/png`` header: +Use more accurate headers according to the type of the files by using the :ref:`raw-media-types` configuration. For example, adding the ``raw-media-types="image/png"`` setting to the configuration file will allow you to use the ``Accept: image/png`` header: .. tabs:: @@ -544,7 +544,7 @@ See :ref:`providing_img` for a step-by-step example on how to handle images in H hstore ------ -You can work with data types belonging to additional supplied modules such as `hstore `_. Let's use the following table: +You can work with data types belonging to additional supplied modules such as `hstore `_. .. code-block:: postgres @@ -556,7 +556,7 @@ You can work with data types belonging to additional supplied modules such as `h 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, for instance: +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: .. tabs:: @@ -583,7 +583,7 @@ The ``name`` column will have the name of the country in different formats. You 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`. For example, if you want to get the native name of Egypt, the query would be: +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`. For example, if you want to get the native name of Egypt: .. tabs:: @@ -604,7 +604,7 @@ You can also query and filter the value of a ``hstore`` column using the arrow o PostGIS ------- -You can use the string representation for `PostGIS `_ data types such as ``geometry`` or ``geography``. As an example, let's create a table using the ``geometry`` type (you need to `install PostGIS `_ first). +You can use the string representation for `PostGIS `_ data types such as ``geometry`` or ``geography`` (you need to `install PostGIS `_ first). .. code-block:: postgres @@ -617,7 +617,7 @@ You can use the string representation for `PostGIS `_ data area geometry ); -Say you want to add areas in polygon format. The request using string representation would look like: +To add areas in polygon format, you can use string representation: .. tabs:: @@ -697,7 +697,7 @@ If you need to add an extra property, like the area in square units by using ``s 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. For example: +In the case that you are using older PostGIS versions, then creating a function is your best option: .. code-block:: postgres From 0a5ae531a4059a5be4a4b87387fb87d1387a3334 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 21 Sep 2022 19:09:50 -0500 Subject: [PATCH 526/549] Add information on preflight requests in CORS subsection --- docs/api.rst | 30 ++++++++++++++++++++++++++++++ postgrest.dict | 1 + 2 files changed, 31 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 89a48524ca..8638445b7c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -2705,6 +2705,36 @@ CORS PostgREST sets highly permissive cross origin resource sharing, that is why it accepts Ajax requests from any domain. +It also handles `preflight requests `_ 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: + +.. tabs:: + + .. code-tab:: http + + OPTIONS /items HTTP/1.1 + Origin: http://example.com + Access-Control-Allow-Method: POST + Access-Control-Allow-Headers: Content-Type + + .. code-tab:: bash Curl + + 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 + .. _multiple-schemas: Switching Schemas diff --git a/postgrest.dict b/postgrest.dict index 2105b58390..39588be676 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -127,6 +127,7 @@ PostgREST postgrest PostgREST's pre +preflight psql Qin RabbitMQ From aecb7910ded6a73c9fa84d52793e1a6895ae0a75 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 14 Nov 2022 19:11:43 -0500 Subject: [PATCH 527/549] Fix step 3 of the tutorial 0 --- docs/tutorials/tut0.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorials/tut0.rst b/docs/tutorials/tut0.rst index 5a2ed16047..7b2ce647c3 100644 --- a/docs/tutorials/tut0.rst +++ b/docs/tutorials/tut0.rst @@ -52,9 +52,9 @@ The result will be a file named simply :code:`postgrest` (or :code:`postgrest.ex .. code-block:: bash - ./postgrest + ./postgrest -h -If everything is working correctly it will print out its version and information about configuration. 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. +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:: From 08228fa2fc1b728c0f255c77a15d7f063b34afc9 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 14 Nov 2022 19:16:42 -0500 Subject: [PATCH 528/549] Fix auth page due to anonymous requests not allowed when db-anon-role is not set --- docs/auth.rst | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/docs/auth.rst b/docs/auth.rst index 94e449db58..5a3d129d37 100644 --- a/docs/auth.rst +++ b/docs/auth.rst @@ -12,7 +12,7 @@ There are three types of roles used by PostgREST, the **authenticator**, **anony .. image:: _static/security-roles.png -The authenticator should be created :code:`NOINHERIT` and configured in the database to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests. 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. +The authenticator should be created :code:`NOINHERIT` and configured in the database to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests. 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 @@ -399,6 +399,27 @@ 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 `Overview of Role System`_ 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 + + -- the names "anon" and "authenticator" are configurable and not + -- sacred, we simply choose them for clarity + 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" + Logins ~~~~~~ @@ -436,6 +457,12 @@ As described in `JWT from SQL`_, we'll create a JWT inside our login function. N 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 `_, +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: .. tabs:: @@ -459,24 +486,3 @@ The response would look like the snippet below. Try decoding the token at `jwt.i { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicGFzcyI6ImZvb2JhciJ9.37066TTRlh-1hXhnA9oO9Pj6lgL6zFuJU0iCHhuCFno" } - -Permissions -~~~~~~~~~~~ - -Your database roles need access to the schema, tables, views and functions in order to service HTTP requests. -Recall from the `Overview of Role System`_ 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 - - -- the names "anon" and "authenticator" are configurable and not - -- sacred, we simply choose them for clarity - create role anon noinherit; - create role authenticator noinherit; - grant anon to authenticator; - - grant execute on function login(text,text) to anon; - -Since the above :code:`login` function is defined as `security definer `_, -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. From 1e09b9f06d979a68727b5bc48853b255c0ac3771 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 14 Nov 2022 19:33:25 -0500 Subject: [PATCH 529/549] Fix broken links and dictcheck --- docs/api.rst | 2 +- docs/ecosystem.rst | 2 +- postgrest.dict | 3 --- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 8638445b7c..c40b747dd1 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -155,7 +155,7 @@ 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 `__. -To ensure best performance on larger data sets, an `appropriate 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. +To ensure best performance on larger data sets, an `appropriate 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: diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index 3d177848c7..69f9eb350a 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -38,7 +38,7 @@ Example Apps * `ext-postgrest-crud `_ - browser-based spreadsheet * `general `_ - example auth back-end * `goodfilm `_ - example film API -* `guild-operators `_ - example queries and functions that the Cardano Community uses for their Guild Operators' Repository +* `guild-operators `_ - example queries and functions that the Cardano Community uses for their Guild Operators' Repository * `handsontable-postgrest `_ - an excel-like database table editor * `heritage-near-me `_ - Elm and PostgREST with PostGIS * `ng-admin-postgrest `_ - automatic database admin panel diff --git a/postgrest.dict b/postgrest.dict index 39588be676..96eade670d 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -14,7 +14,6 @@ Beles booleans Bouscal buildpack -bugfixes Bytea Cardano cd @@ -204,5 +203,3 @@ Websockets webuser wfts ZeroMQ -Customizable -customizable From df2cde81cac642c92fa7f2c2398bb75cccbc56f6 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Sat, 28 Jan 2023 20:20:48 -0500 Subject: [PATCH 530/549] Add db-plan-enabled recommendation (#588) * fix broken links --- docs/auth.rst | 4 ++-- docs/configuration.rst | 26 +++++++++++++++++++++++++- docs/ecosystem.rst | 1 - 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/docs/auth.rst b/docs/auth.rst index 5a3d129d37..c895eb6b14 100644 --- a/docs/auth.rst +++ b/docs/auth.rst @@ -98,7 +98,7 @@ For PostgreSQL server version >= 14 .. code:: sql current_setting('request.jwt.claims', true)::json->>'email'; - + For PostgreSQL server version < 14 @@ -292,7 +292,7 @@ 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 `_ is voiced in detail `elsewhere on the web `_. 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. +The critique against the `JWT standard `_ is voiced in detail `elsewhere on the web `_. 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 `_. Recommendations about high quality libraries for usage in API clients can be found on `jwt.io `_. diff --git a/docs/configuration.rst b/docs/configuration.rst index 1e45e10651..988eae8c9a 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -160,7 +160,7 @@ db-pool-timeout Int 3600 db-pre-request String Y db-prepared-statements Boolean True Y db-schemas String public Y -db-tx-end String commit +db-tx-end String commit db-uri String postgresql:// db-use-legacy-gucs Boolean True Y jwt-aud String Y @@ -296,6 +296,30 @@ 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`. + It's recommended to use this in testing environments only 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 + .. _db-pool: db-pool diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index 69f9eb350a..2fe80a94e7 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -89,7 +89,6 @@ Extensions * `aiodata `_ - Python, event-based proxy and caching client. * `pg-safeupdate `_ - prevent full-table updates or deletes * `postgrest-auth (criles25) `_ - email based auth/signup -* `postgrest-auth (svmotn) `_ - OAuth2-inspired external auth server * `postgrest-node `_ - Run a PostgREST server in Node.js via npm module * `postgrest-oauth `_ - OAuth2 WAI middleware * `postgrest-oauth/api `_ - OAuth2 server From 9c075e5e5204912d7155fbe5605fa874d2ca9114 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 13 Apr 2023 18:38:07 -0500 Subject: [PATCH 531/549] Fix info on updates without filters --- docs/api.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index c40b747dd1..a973575293 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1866,10 +1866,12 @@ To update a row or rows in a table, use the PATCH verb. Use :ref:`h_filter` to s -X PATCH -H "Content-Type: application/json" \ -d '{ "category": "child" }' -Doing a full table update without filters is not allowed and will result in 0 updated rows. To make a an update without filters, you must limit the rows affected. See :ref:`limited_update_delete`. - Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. +.. warning:: + + Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`. + .. _upsert: Upsert From 4d256ce5db7dcaea3771df9432a9b7c750fcd8d5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 12:23:52 +0000 Subject: [PATCH 532/549] Bump cachix/install-nix-action from 17 to 18 Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 17 to 18. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/v17...v18) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 865683ba16..18290bff2f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v17 + - uses: cachix/install-nix-action@v18 - run: nix-env -f default.nix -iA build - run: postgrest-docs-build @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v17 + - uses: cachix/install-nix-action@v18 - run: nix-env -f default.nix -iA spellcheck - run: postgrest-docs-spellcheck @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v17 + - uses: cachix/install-nix-action@v18 - run: nix-env -f default.nix -iA linkcheck - run: postgrest-docs-linkcheck From 165e0acf2471a9249ff325fd3a1b8c6e60f8f40e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Feb 2023 12:34:33 -0500 Subject: [PATCH 533/549] Bump cachix/install-nix-action from 18 to 19 (#593) Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 18 to 19. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/v18...v19) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 18290bff2f..ac19ad0812 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v18 + - uses: cachix/install-nix-action@v19 - run: nix-env -f default.nix -iA build - run: postgrest-docs-build @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v18 + - uses: cachix/install-nix-action@v19 - run: nix-env -f default.nix -iA spellcheck - run: postgrest-docs-spellcheck @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v18 + - uses: cachix/install-nix-action@v19 - run: nix-env -f default.nix -iA linkcheck - run: postgrest-docs-linkcheck From 865dd12836ab69432ec439409977b215516e2a65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Mar 2023 11:17:46 -0500 Subject: [PATCH 534/549] Bump cachix/install-nix-action from 19 to 20 (#597) Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 19 to 20. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/v19...v20) --- updated-dependencies: - dependency-name: cachix/install-nix-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ac19ad0812..c3bc418294 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v19 + - uses: cachix/install-nix-action@v20 - run: nix-env -f default.nix -iA build - run: postgrest-docs-build @@ -25,7 +25,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v19 + - uses: cachix/install-nix-action@v20 - run: nix-env -f default.nix -iA spellcheck - run: postgrest-docs-spellcheck @@ -35,7 +35,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v19 + - uses: cachix/install-nix-action@v20 - run: nix-env -f default.nix -iA linkcheck - run: postgrest-docs-linkcheck From 5fb9e819695cdf3b1f82df5645b1d3cfa664283e Mon Sep 17 00:00:00 2001 From: Andrea Bernicchia <51401007+abernicchia-heroku@users.noreply.github.com> Date: Wed, 2 Nov 2022 18:23:35 +0100 Subject: [PATCH 535/549] LICENSE file added (#579) --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..4d0857d540 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +Copyright (c) 2014 Joe Nelson +Copyright (c) 2019 Steve Chavez + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. From 1a03ece44167c948ad6cdcbc085e6b6ed96a5313 Mon Sep 17 00:00:00 2001 From: fjf2002 Date: Wed, 16 Nov 2022 23:10:18 +0100 Subject: [PATCH 536/549] =?UTF-8?q?how-to:=20SQL=20User=20Management=20usi?= =?UTF-8?q?ng=20postgres=E2=80=99=20users=20and=20passwords=20(#581)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/auth.rst | 8 + ...ent-using-postgres-users-and-passwords.rst | 342 ++++++++++++++++++ docs/index.rst | 2 + postgrest.dict | 4 + 4 files changed, 356 insertions(+) create mode 100644 docs/how-tos/sql-user-management-using-postgres-users-and-passwords.rst diff --git a/docs/auth.rst b/docs/auth.rst index c895eb6b14..eeb55c0b05 100644 --- a/docs/auth.rst +++ b/docs/auth.rst @@ -305,6 +305,8 @@ Schema Isolation You can isolate your api schema from internal implementation details, as explained in :ref:`schema_isolation`. For an example of wrapping a private table with a public view see the :ref:`public_ui` section below. +.. _sql_user_management: + SQL User Management =================== @@ -486,3 +488,9 @@ The response would look like the snippet below. Try decoding the token at `jwt.i { "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`. diff --git a/docs/how-tos/sql-user-management-using-postgres-users-and-passwords.rst b/docs/how-tos/sql-user-management-using-postgres-users-and-passwords.rst new file mode 100644 index 0000000000..ea53a5d32d --- /dev/null +++ b/docs/how-tos/sql-user-management-using-postgres-users-and-passwords.rst @@ -0,0 +1,342 @@ +.. _sql-user-management-using-postgres-users-and-passwords: + +SQL User Management using postgres' users and passwords +======================================================= + +:author: `fjf2002 `_ + + +This is an alternative to :ref:`sql_user_management`, solely using the built-in table `pg_catalog.pg_authid `_ for user management. This means + +- no dedicated user table (aside from :code:`pg_authid`) is required + +- postgres' users and passwords (i. e. the stuff in :code:`pg_authid`) are also used at the postgrest level. + +.. note:: + Only postgres 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 `_, 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 `_: + +.. 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 `_: + +.. 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 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 + + -- the names "anon" and "authenticator" are configurable and not + -- sacred, we simply choose them for clarity + 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 `_, +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: + +.. tabs:: + + .. code-tab:: http + + POST /rpc/login HTTP/1.1 + + { "username": "foo", "password": "bar" } + + .. code-tab:: bash Curl + + 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 `_. (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 us configure the :code:`foo` user correctly and add a table for him/her: + +.. 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: + +.. tabs:: + + .. code-tab:: http + + GET /foobar HTTP/1.1 + + .. code-tab:: bash Curl + + 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. + +.. tabs:: + + .. code-tab:: http + + GET /foobar HTTP/1.1 + Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiZm9vIiwiZXhwIjoxNjY4MTkyMjAyfQ.zzdHCBjfkqDQLQ8D7CHO3cIALF6KBCsfPTWgwhCiHCY + + .. code-tab:: bash Curl + + 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. diff --git a/docs/index.rst b/docs/index.rst index dcb1089fd9..d5311d4acb 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -207,10 +207,12 @@ These are recipes that'll help you address specific use-cases. how-tos/working-with-postgresql-data-types how-tos/providing-images-for-img how-tos/create-soap-endpoint + how-tos/sql-user-management-using-postgres-users-and-passwords - :doc:`how-tos/providing-images-for-img` - :doc:`how-tos/working-with-postgresql-data-types` - :doc:`how-tos/create-soap-endpoint` +- :doc:`how-tos/sql-user-management-using-postgres-users-and-passwords` Ecosystem --------- diff --git a/postgrest.dict b/postgrest.dict index 96eade670d..671c299117 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -104,6 +104,7 @@ ORM ov passphrase Pawel +PBKDF Pelletier Petr PgBouncer @@ -113,6 +114,7 @@ pgrst pgrstX PGRSTX pgSQL +authid phfts phraseto plainto @@ -123,6 +125,8 @@ PostGIS PostgreSQL PostgreSQL's PostgREST +postgres +postgres's postgrest PostgREST's pre From 1e353dc4d20414b96458e2a27e67aa404c4f9b5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franz-Josef=20F=C3=A4rber?= Date: Tue, 3 Jan 2023 16:03:06 +0100 Subject: [PATCH 537/549] Minor improvements to sql-user-management-using-postgres-users-and-passwords.rst --- ...ement-using-postgres-users-and-passwords.rst | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/docs/how-tos/sql-user-management-using-postgres-users-and-passwords.rst b/docs/how-tos/sql-user-management-using-postgres-users-and-passwords.rst index ea53a5d32d..4dec8ed1fa 100644 --- a/docs/how-tos/sql-user-management-using-postgres-users-and-passwords.rst +++ b/docs/how-tos/sql-user-management-using-postgres-users-and-passwords.rst @@ -6,14 +6,14 @@ SQL User Management using postgres' users and passwords :author: `fjf2002 `_ -This is an alternative to :ref:`sql_user_management`, solely using the built-in table `pg_catalog.pg_authid `_ for user management. This means +This is an alternative to chapter :ref:`sql_user_management`, solely using the PostgreSQL built-in table `pg_catalog.pg_authid `_ for user management. This means - no dedicated user table (aside from :code:`pg_authid`) is required -- postgres' users and passwords (i. e. the stuff in :code:`pg_authid`) are also used at the postgrest level. +- PostgreSQL's users and passwords (i. e. the stuff in :code:`pg_authid`) are also used at the PostgREST level. .. note:: - Only postgres users with SCRAM-SHA-256 password hashes (the default since PostgreSQL v14) are supported. + Only PostgreSQL users with SCRAM-SHA-256 password hashes (the default since PostgreSQL v14) are supported. .. warning:: @@ -98,9 +98,7 @@ In order to be able to work with postgres' SCRAM-SHA-256 password hashes, we als 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; @@ -140,7 +138,7 @@ But contrary to :ref:`sql_user_management`, this function does not use a dedicat 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: + -- verify password: AND check_password_part.stored_key = stored_password_part.stored_key AND check_password_part.server_key = stored_password_part.server_key; $$; @@ -152,7 +150,7 @@ But contrary to :ref:`sql_user_management`, this function does not use a dedicat Public User Interface --------------------- -Analogous to :ref:`sql_user_management`, we create a login function which takes a username and password and returns JWT if the credentials match a user in the internal table. +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. @@ -168,7 +166,7 @@ As described in :ref:`client_auth`, we'll create a JWT token inside our login fu token text ); - -- if you are not using psql, you need to replace :dbname with the current database's name. + -- 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'; @@ -289,7 +287,8 @@ The response would look like the snippet below. Try decoding the token at `jwt.i A more sophisticated test at the REST level ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Let us configure the :code:`foo` user correctly and add a table for him/her: +Let's add a table, intended for the :code:`foo` user: + .. code-block:: postgres From bd11334df76b619539676c25f4f1f4e2ddb1dff7 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 9 Feb 2023 14:56:38 -0500 Subject: [PATCH 538/549] Add db-pool-acquisition-timeout configuration parameter --- docs/configuration.rst | 81 ++++++++++++++++++++-------------------- docs/releases/v6.0.2.rst | 2 +- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/docs/configuration.rst b/docs/configuration.rst index 988eae8c9a..b479e5a56a 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -108,7 +108,7 @@ It's not possible to change :ref:`env_variables_config` for a running process an * :ref:`admin-server-port` * :ref:`db-uri` * :ref:`db-pool` - * :ref:`db-pool-timeout` + * :ref:`db-pool-acquisition-timeout` * :ref:`server-host` * :ref:`server-port` * :ref:`server-unix-socket` @@ -143,40 +143,40 @@ The ``"pgrst"`` notification channel is enabled by default. For configuring the List of parameters ================== -======================== ======= ================= ========== -Name Type Default Reloadable -======================== ======= ================= ========== -admin-server-port Int -app.settings.* String Y -db-anon-role String Y -db-channel String pgrst Y -db-channel-enabled Boolean True Y -db-config Boolean True Y -db-extra-search-path String public Y -db-max-rows Int ∞ Y -db-plan-enabled Boolean False Y -db-pool Int 10 -db-pool-timeout Int 3600 -db-pre-request String Y -db-prepared-statements Boolean True Y -db-schemas String public Y -db-tx-end String commit -db-uri String postgresql:// -db-use-legacy-gucs Boolean True Y -jwt-aud String Y -jwt-role-claim-key String .role Y -jwt-secret String Y -jwt-secret-is-base64 Boolean False Y -log-level String error Y -openapi-mode String follow-privileges Y -openapi-security-active Boolean False Y -openapi-server-proxy-uri String Y -raw-media-types String Y -server-host String !4 -server-port Int 3000 -server-unix-socket String -server-unix-socket-mode String 660 -======================== ======= ================= ========== +=========================== ======= ================= ========== +Name Type Default Reloadable +=========================== ======= ================= ========== +admin-server-port Int +app.settings.* String Y +db-anon-role String Y +db-channel String pgrst Y +db-channel-enabled Boolean True Y +db-config Boolean True Y +db-extra-search-path String public Y +db-max-rows Int ∞ Y +db-plan-enabled Boolean False Y +db-pool Int 10 +db-pool-acquisition-timeout Int ∞ +db-pre-request String Y +db-prepared-statements Boolean True Y +db-schemas String public Y +db-tx-end String commit +db-uri String postgresql:// +db-use-legacy-gucs Boolean True Y +jwt-aud String Y +jwt-role-claim-key String .role Y +jwt-secret String Y +jwt-secret-is-base64 Boolean False Y +log-level String error Y +openapi-mode String follow-privileges Y +openapi-security-active Boolean False Y +openapi-server-proxy-uri String Y +raw-media-types String Y +server-host String !4 +server-port Int 3000 +server-unix-socket String +server-unix-socket-mode String 660 +=========================== ======= ================= ========== .. _admin-server-port: @@ -332,18 +332,17 @@ db-pool Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the :code:`max_connections` GUC in your database. -.. _db-pool-timeout: +.. _db-pool-acquisition-timeout: -db-pool-timeout ---------------- +db-pool-acquisition-timeout +--------------------------- =============== ================= - **Environment** PGRST_DB_POOL_TIMEOUT + **Environment** PGRST_DB_POOL_ACQUISITION_TIMEOUT **In-Database** `n/a` =============== ================= - Time to live, in seconds, for an idle database pool connection. If the timeout is reached the connection will be closed. - Once a new request arrives a new connection will be started. + Specifies the maximum time in seconds that the request will wait for the pool to free up a connection slot to the database. If it times out without acquiring a connection, then the request is aborted and a ``504`` error is returned. .. _db-pre-request: diff --git a/docs/releases/v6.0.2.rst b/docs/releases/v6.0.2.rst index f96550515c..47f870e443 100644 --- a/docs/releases/v6.0.2.rst +++ b/docs/releases/v6.0.2.rst @@ -26,7 +26,7 @@ Added * It's now possible to request a ``text/plain`` output. See :ref:`scalar_return_formats`. |br| -- `@steve-chavez `_ -* Config option for specifying PostgREST database pool timeout. See :ref:`db-pool-timeout`. +* Config option for specifying PostgREST database pool timeout ``db-pool-timeout``. |br| -- `@Qu4tro `_ * Config option for binding the PostgREST web server to an unix socket. See :ref:`server-unix-socket`. From a74552a1c456d93fd185e04f5a701312693e6422 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 9 Feb 2023 16:48:17 -0500 Subject: [PATCH 539/549] Add HTTP status codes to PGRST errors (#590) --- docs/errors.rst | 271 ++++++++++++++++++++++++------------------------ postgrest.dict | 1 - 2 files changed, 137 insertions(+), 135 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index e3473e4878..d266179998 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -125,21 +125,25 @@ Group 0 - Connection Related to the connection with the database. -+---------------+-------------------------------------------------------------+ -| Code | Description | -+===============+=============================================================+ -| .. _pgrst000: | Could not connect with the database due to an incorrect | -| | :ref:`db-uri` or due to the PostgreSQL service not running. | -| PGRST000 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst001: | Could not connect with the database due to an internal | -| | error. | -| PGRST001 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst002: | Could not connect with the database when building the | -| | :ref:`schema_cache` due to the PostgreSQL service not | -| PGRST002 | running. | -+---------------+-------------------------------------------------------------+ ++---------------+-------------+-------------------------------------------------------------+ +| 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 | +| | | :ref:`schema_cache` due to the PostgreSQL service not | +| PGRST002 | | running. | ++---------------+-------------+-------------------------------------------------------------+ +| .. _pgrst003: | 503 | The request time exceeded the timeout specified in | +| | | :ref:`db-pool-acquisition-timeout`. | +| PGRST003 | | | ++---------------+-------------+-------------------------------------------------------------+ .. _pgrst1**: @@ -148,82 +152,77 @@ Group 1 - Api Request Related to the HTTP request elements. -+---------------+-------------------------------------------------------------+ -| Code | Description | -+===============+=============================================================+ -| .. _pgrst100: | Parsing error in the query string parameter. | -| | See :ref:`h_filter`, :ref:`operators` and :ref:`ordering`. | -| PGRST100 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst101: | For :ref:`functions `, only ``GET`` and ``POST`` | -| | verbs are allowed. Any other verb will throw this error. | -| PGRST101 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst102: | An invalid request body was sent(e.g. an empty body or | -| | malformed JSON). | -| PGRST102 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst103: | An invalid range was specified for :ref:`limits`. | -| | | -| PGRST103 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst104: | Either the :ref:`filter operator ` is missing | -| | or it doesn't exist. | -| PGRST104 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst105: | An invalid :ref:`PUT ` request was done | -| | | -| PGRST105 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst106: | The schema specified when | -| | :ref:`switching schemas ` is not present | -| PGRST106 | in the :ref:`db-schemas` configuration variable. | -+---------------+-------------------------------------------------------------+ -| .. _pgrst107: | The ``Content-Type`` sent in the request is invalid. | -| | | -| PGRST107 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst108: | 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: | Restricting a Deletion or an Update using limits must | -| | include the ordering of a unique column. | -| PGRST109 | See :ref:`limited_update_delete`. | -+---------------+-------------------------------------------------------------+ -| .. _pgrst110: | 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: | An invalid ``response.headers`` was set. | -| | See :ref:`guc_resp_hdrs`. | -| PGRST111 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst112: | The status code must be a positive integer. | -| | See :ref:`guc_resp_status`. | -| PGRST112 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst113: | More than one column was returned for a scalar result. | -| | See :ref:`scalar_return_formats`. | -| | | -| PGRST113 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst114: | For an :ref:`UPSERT using PUT `, when | -| | :ref:`limits and offsets ` are used. | -| PGRST114 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst115: | For an :ref:`UPSERT using PUT `, when the | -| | primary key in the query string and the body are different. | -| PGRST115 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst116: | More than 1 or no items where returned when requesting | -| | a singular response. See :ref:`singular_plural`. | -| PGRST116 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst117: | The HTTP verb used in the request in not supported. | -| | | -| PGRST117 | | -+---------------+-------------------------------------------------------------+ ++---------------+-------------+-------------------------------------------------------------+ +| 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 `, 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 ` request was done | +| | | | +| PGRST105 | | | ++---------------+-------------+-------------------------------------------------------------+ +| .. _pgrst106: | 406 | The schema specified when | +| | | :ref:`switching 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 | | | ++---------------+-------------+-------------------------------------------------------------+ +| .. _pgrst113: | 406 | More than one column was returned for a scalar result. | +| | | See :ref:`scalar_return_formats`. | +| PGRST113 | | | ++---------------+-------------+-------------------------------------------------------------+ +| .. _pgrst114: | 400 | For an :ref:`UPSERT using PUT `, when | +| | | :ref:`limits and offsets ` are used. | +| PGRST114 | | | ++---------------+-------------+-------------------------------------------------------------+ +| .. _pgrst115: | 400 | For an :ref:`UPSERT using 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 | | | ++---------------+-------------+-------------------------------------------------------------+ .. _pgrst2**: @@ -232,27 +231,31 @@ Group 2 - Schema Cache Related to a :ref:`stale schema cache `. Most of the time, these errors are solved by :ref:`reloading the schema cache `. -+---------------+-------------------------------------------------------------+ -| Code | Description | -+===============+=============================================================+ -| .. _pgrst200: | Caused by :ref:`stale_fk_relationships`, otherwise any of | -| | the embedding resources or the relationship itself may not | -| PGRST200 | exist in the database. | -+---------------+-------------------------------------------------------------+ -| .. _pgrst201: | An ambiguous embedding request was made. | -| | See :ref:`embed_disamb`. | -| PGRST201 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst202: | Caused by a :ref:`stale_function_signature`, otherwise | -| | the function may not exist in the database. | -| PGRST202 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst203: | 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. | -+---------------+-------------------------------------------------------------+ ++---------------+-------------+-------------------------------------------------------------+ +| Code | HTTP status | Description | ++===============+=============+=============================================================+ +| .. _pgrst200: | 400 | Caused by :ref:`stale_fk_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:`embed_disamb`. | +| PGRST201 | | | ++---------------+-------------+-------------------------------------------------------------+ +| .. _pgrst202: | 404 | Caused by a :ref:`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 ` | +| | | in the ``columns`` query parameter is not found. | +| PGRST204 | | | ++---------------+-------------+-------------------------------------------------------------+ .. _pgrst3**: @@ -261,21 +264,21 @@ 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 ` for more information on this process. -+---------------+-------------------------------------------------------------+ -| Code | Description | -+===============+=============================================================+ -| .. _pgrst300: | A :ref:`JWT secret ` is missing from the | -| | configuration. | -| PGRST300 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst301: | Any error related to the verification of the JWT, | -| | which means that the JWT provided is invalid in some way. | -| PGRST301 | | -+---------------+-------------------------------------------------------------+ -| .. _pgrst302: | Attempted to do a request without | -| | :ref:`authentication ` when the anonymous role | -| PGRST302 | is disabled by not setting it in :ref:`db-anon-role`. | -+---------------+-------------------------------------------------------------+ ++---------------+-------------+-------------------------------------------------------------+ +| Code | HTTP status | Description | ++===============+=============+=============================================================+ +| .. _pgrst300: | 500 | A :ref:`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 ` 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 @@ -286,10 +289,10 @@ Group X - Internal Internal errors. If you encounter any of these, you may have stumbled on a PostgREST bug, please `open an issue `_ and we'll be glad to fix it. -+---------------+-------------------------------------------------------------+ -| Code | Description | -+===============+=============================================================+ -| .. _pgrstX00: | Internal errors related to the library used for connecting | -| | to the database. | -| PGRSTX00 | | -+---------------+-------------------------------------------------------------+ ++---------------+-------------+-------------------------------------------------------------+ +| Code | HTTP status | Description | ++===============+=============+=============================================================+ +| .. _pgrstX00: | 500 | Internal errors related to the library used for connecting | +| | | to the database. | +| PGRSTX00 | | | ++---------------+-------------+-------------------------------------------------------------+ diff --git a/postgrest.dict b/postgrest.dict index 671c299117..789cdcdf4c 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -126,7 +126,6 @@ PostgreSQL PostgreSQL's PostgREST postgres -postgres's postgrest PostgREST's pre From 113903185868cf8a246860fdbca6fa13dcc50823 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Mon, 13 Feb 2023 11:06:22 -0500 Subject: [PATCH 540/549] fix: PGRST003 status code to 504 (#594) --- docs/errors.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index d266179998..f981d5e4fb 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -140,8 +140,8 @@ Related to the connection with the database. | | | :ref:`schema_cache` due to the PostgreSQL service not | | PGRST002 | | running. | +---------------+-------------+-------------------------------------------------------------+ -| .. _pgrst003: | 503 | The request time exceeded the timeout specified in | -| | | :ref:`db-pool-acquisition-timeout`. | +| .. _pgrst003: | 504 | The request timed out waiting for a pool connection | +| | | to be available. See :ref:`db-pool-acquisition-timeout`. | | PGRST003 | | | +---------------+-------------+-------------------------------------------------------------+ From 593b8bf3854189d3749b5017e558ea4816fbdf14 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Mon, 13 Feb 2023 22:29:38 -0500 Subject: [PATCH 541/549] remove wrong claim about CSV being faster (#595) --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index a973575293..caaefa5c96 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1748,7 +1748,7 @@ URL encoded payloads can be posted with ``Content-Type: application/x-www-form-u 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. Note that using CSV requires less parsing on the server and is much faster. +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 From 49cd1f3447a285a8ea97b7ff61a374b8c559e655 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Tue, 18 Apr 2023 12:43:15 -0500 Subject: [PATCH 542/549] Remove public from schema cache event trigger --- docs/schema_cache.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/schema_cache.rst b/docs/schema_cache.rst index 5e5fce3a6a..c966a39907 100644 --- a/docs/schema_cache.rst +++ b/docs/schema_cache.rst @@ -147,7 +147,7 @@ You can do automatic schema cache reloading in a pure SQL way and forget about s .. code-block:: postgresql -- Create an event trigger function - CREATE OR REPLACE FUNCTION public.pgrst_watch() RETURNS event_trigger + CREATE OR REPLACE FUNCTION pgrst_watch() RETURNS event_trigger LANGUAGE plpgsql AS $$ BEGIN @@ -158,7 +158,7 @@ You can do automatic schema cache reloading in a pure SQL way and forget about s -- This event trigger will fire after every ddl_command_end event CREATE EVENT TRIGGER pgrst_watch ON ddl_command_end - EXECUTE PROCEDURE public.pgrst_watch(); + EXECUTE PROCEDURE pgrst_watch(); Now, whenever the ``pgrst_watch`` trigger is fired in the database, PostgREST will automatically reload the schema cache. From 0a3a20347b8c3961d1aa68aa17c39b78bfb59f51 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Tue, 18 Apr 2023 17:13:40 -0500 Subject: [PATCH 543/549] snippet for computed rel overload --- docs/api.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index caaefa5c96..1e5e10c463 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1168,6 +1168,12 @@ For example, to override the :ref:`many-to-one relationship ` betwe Taking advantage of overloaded functions, you can use the same function name for different parameters and thus define relationships from other tables/views to ``directors``. +.. code-block:: postgres + + create function directors(film_schools) returns setof directors as $$ + select * from directors where film_school_id = $1.id + $$ stable language sql; + Computed relationships have good performance as they follow the `Inlining conditions for table functions `_. .. _nested_embedding: From 1b218a97f4bf0d3b6a44823f26f029990b10a96c Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 21 Apr 2023 17:04:31 -0500 Subject: [PATCH 544/549] Add a warning when working with computed relationships --- docs/api.rst | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 1e5e10c463..a9483635e7 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1174,7 +1174,13 @@ Taking advantage of overloaded functions, you can use the same function name for select * from directors where film_school_id = $1.id $$ stable language sql; -Computed relationships have good performance as they follow the `Inlining conditions for table functions `_. +Computed relationships have good performance as their intended design follow the `Inlining conditions for table functions `_. + +.. warning:: + + - Always use ``SETOF`` when creating computed relationships. Functions can return a table without using ``SETOF``, but bear in mind that they will not be inlined. + + - Make sure to correctly label the ``to-one`` part of the relationship. When using the ``ROWS 1`` estimation, PostgREST will expect a single row to be returned. If that is not the case, then it will unnest the embedding and return repeated values for the top level resource. .. _nested_embedding: From d5a9e5d41feddc4b73511ed1dfb77e65a392ec8e Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 21 Apr 2023 11:29:05 -0500 Subject: [PATCH 545/549] Add documentation for v10.2.0 Co-authored-by: Steve Chavez --- docs/configuration.rst | 16 +++- docs/index.rst | 1 + docs/install.rst | 2 + docs/releases/v10.2.0.rst | 153 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 docs/releases/v10.2.0.rst diff --git a/docs/configuration.rst b/docs/configuration.rst index b479e5a56a..a2b41a303d 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -109,6 +109,7 @@ It's not possible to change :ref:`env_variables_config` for a running process an * :ref:`db-uri` * :ref:`db-pool` * :ref:`db-pool-acquisition-timeout` + * :ref:`db-pool-max-lifetime` * :ref:`server-host` * :ref:`server-port` * :ref:`server-unix-socket` @@ -156,7 +157,8 @@ db-extra-search-path String public Y db-max-rows Int ∞ Y db-plan-enabled Boolean False Y db-pool Int 10 -db-pool-acquisition-timeout Int ∞ +db-pool-acquisition-timeout Int 10 +db-pool-max-lifetime Int 1800 db-pre-request String Y db-prepared-statements Boolean True Y db-schemas String public Y @@ -344,6 +346,18 @@ db-pool-acquisition-timeout Specifies the maximum time in seconds that the request will wait for the pool to free up a connection slot to the database. If it times out without acquiring a connection, then the request is aborted and a ``504`` error is returned. +.. _db-pool-max-lifetime: + +db-pool-max-lifetime +-------------------- + + =============== ================= + **Environment** PGRST_DB_POOL_MAX_LIFETIME + **In-Database** `n/a` + =============== ================= + + Specifies the maximum time in seconds of an existing connection in the pool. When this lifetime is reached, then the connection will be closed and returned to the pool. + .. _db-pre-request: db-pre-request diff --git a/docs/index.rst b/docs/index.rst index d5311d4acb..f138876d81 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -98,6 +98,7 @@ The project has a friendly and growing community. Join our `chat room v10.0.0 v9.0.1 v9.0.0 diff --git a/docs/install.rst b/docs/install.rst index 39de4f0630..8ba9f56d4d 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -257,6 +257,8 @@ You can build PostgREST from source with `Stack `_, `10.1.1 `_ and `10.1.2 `_. You can look at the detailed changelog and download the pre-compiled binaries on the `GitHub release page `_. + +Features +-------- + +Connection Lifetime +~~~~~~~~~~~~~~~~~~~ + +To prevent memory leaks caused by long-lived connections, PostgREST limits their lifetime in the pool through :ref:`db-pool-max-lifetime`. + +Connection Timeout +~~~~~~~~~~~~~~~~~~ + +There is now a time limit to wait for new connections in the pool, that is, if a new request cannot get a connection in the time specified in :ref:`db-pool-acquisition-timeout` then a response with ``504`` status is returned + +Documentation improvements +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* Added HTTP status codes to the :ref:`pgrst_errors`. + +* Added a how-to on :ref:`sql-user-management-using-postgres-users-and-passwords`. + +* Updated the :ref:`Heroku installation page `. + +Changes +------- + +* Removed ``db-pool-timeout`` option because it was removed in the ``hasql-pool`` library that PostgREST uses for SQL connections. (`#2444 `_) + +Deprecated +---------- + +* Deprecate bulk-calls when including the ``Prefer: params=multiple-objects`` in the request. It is preferable to use a function with an :ref:`array ` or JSON parameter for a better performance. (`#1385 `_) + +Bug fixes +--------- + +* Reduce allocations communication with PostgreSQL, particularly for request bodies. (`#2261 `_, `#2349 `_, `#2467 `_) + +* Fix ``SIGUSR1`` to fully flush the connection pool. (`#2401 `_, `#2444 `_) + +* Fix opening an empty transaction on failed resource embedding. (`#2428 `_) + +* Fix embedding the same table multiple times. (`#2455 `_) + +* Fix a regression when embedding views where base tables have a different column order for foreign key columns (`#2518 `_) + +* Fix a regression with the ``Location`` header when :ref:`inserting ` into views with primary keys from multiple tables (`#2458 `_) + +* Fix a regression in OpenAPI output with mode ``follow-privileges`` (`#2356 `_) + +* Fix infinite recursion when loading schema cache with self-referencing view (`#2283 `_) + +* Return status code ``200`` instead of ``404`` for ``PATCH`` requests which don't affect any rows (`#2343 `_) + +* Treat the :ref:`computed relationships ` that do not return ``SETOF`` as M2O/O2O relationship (`#2481 `_) + +* Fix embedding a computed relationship with a normal relationship (`#2534 `_) + +* Fix error message when ``[]`` is used inside ``select`` (`#2362 `_) + +* Disallow ``!inner`` on computed columns (`#2475 `_) + +* Ignore leading and trailing spaces in column names when parsing the query string (`#2285 `_) + +* Fix ``UPSERT`` with PostgreSQL 15 (`#2545 `_) + +* Fix embedding views with multiple references to the same base column (`#2459 `_) + +* Fix regression when embedding views with partial references to multi column foreign keys (`#2548 `_) + +* Fix regression when requesting ``limit=0`` and ``db-max-row`` is set (`#2558 `_) + +* Return a clear error without hitting the database when trying to update or insert an unknown column with ``?columns`` (`#2542 `_) + +* Fix bad M2M embedding on RPC (`#2565 `_) + +* Replace misleading error message when no function is found with a hint containing functions/parameters names suggestions (`#2575 `_) + +* Move explanation about "single parameters" from the ``message`` to the ``details`` in the error output (`#2582 `_) + +* Replace misleading error message when no relationship is found with a hint containing parent/child names suggestions (`#2569 `_) + +* Add the required OpenAPI items object when the parameter is an array (`#1405 `_) + +* Add upsert headers for ``POST`` requests to the OpenAPI output (`#2592 `_) + +* Fix foreign keys pointing to ``VIEW`` instead of ``TABLE`` in OpenAPI output (`#2623 `_) + +* Consider any PostgreSQL authentication failure as fatal and exit immediately (`#2622 `_) + +* Fix ``NOTIFY pgrst`` not reloading the db connections catalog cache (`#2620 `_) + +* Fix ``db-pool-acquisition-timeout`` not logging to stderr when the timeout is reached (`#2667 `_) + +* Fix PostgreSQL resource leak with long-lived connections through the :ref:`db-pool-max-lifetime` configuration (`#2638 `_) + +* There is now a stricter parsing of the query string. Instead of silently ignoring, the parser now returns a :ref:`PostgREST error ` on invalid syntax. (`#2537 `_) + +Thanks +------ + +Big thanks from the `PostgREST team `_ to our 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/2ndquadrant.png + :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo + :width: 13em + + .. image:: ../_static/retool.png + :target: https://retool.com/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + + .. image:: ../_static/gnuhost.png + :target: https://gnuhost.eu/?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/oblivious.jpg + :target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + +* Evans Fernandes +* `Jan Sommer `_ +* `Franz Gusenbauer `_ +* `Daniel Babiak `_ +* Tsingson Qin +* Michel Pelletier +* Jay Hannah +* Robert Stolarz +* Nicholas DiBiase +* Christopher Reid +* Nathan Bouscal +* Daniel Rafaj +* David Fenko +* Remo Rechkemmer +* Severin Ibarluzea +* Tom Saleeba +* Pawel Tyll + +If you like to join them please consider `supporting PostgREST development `_. From 291da1a41f817528af5da0976be13f110a06eb9f Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Fri, 21 Apr 2023 19:20:46 -0500 Subject: [PATCH 546/549] Clarify pool connection features --- docs/releases/v10.2.0.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/releases/v10.2.0.rst b/docs/releases/v10.2.0.rst index 5d1e1cebf9..3011e470e2 100644 --- a/docs/releases/v10.2.0.rst +++ b/docs/releases/v10.2.0.rst @@ -7,15 +7,15 @@ This minor version adds bug fixes and some features that provide stability to v1 Features -------- -Connection Lifetime -~~~~~~~~~~~~~~~~~~~ +Pool Connection Lifetime +~~~~~~~~~~~~~~~~~~~~~~~~ To prevent memory leaks caused by long-lived connections, PostgREST limits their lifetime in the pool through :ref:`db-pool-max-lifetime`. -Connection Timeout -~~~~~~~~~~~~~~~~~~ +Pool Connection Acquisition Timeout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -There is now a time limit to wait for new connections in the pool, that is, if a new request cannot get a connection in the time specified in :ref:`db-pool-acquisition-timeout` then a response with ``504`` status is returned +There is now a time limit to wait for pool connections to be acquired. If a new request cannot get a connection in the time specified in :ref:`db-pool-acquisition-timeout` then a response with a ``504`` status is returned. Documentation improvements ~~~~~~~~~~~~~~~~~~~~~~~~~~ From e2f3a862be8a40d0a75daab0f857dfcf5e4da443 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Tue, 25 Apr 2023 14:55:56 -0500 Subject: [PATCH 547/549] Fix version to v10.2 --- docs/conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index f0390613d5..699ab51320 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,9 +57,9 @@ # built documents. # # The short X.Y version. -version = u'9.0' +version = u'10.2' # The full version, including alpha/beta/rc tags. -release = u'9.0.0' +release = u'10.2.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 33c57506b4523c758994be799a721dfa34412c97 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sat, 17 Feb 2024 12:53:41 +0100 Subject: [PATCH 548/549] chore: Prepare merge of postgrest-docs into postgrest main repo This avoids some merge conflicts to allow git blame to detect renames properly. --- .github/workflows/ci.yaml | 41 --------------- .github/workflows/docs.yaml | 50 +++++++++++++++++++ .readthedocs.yaml | 2 +- .gitignore => docs/.gitignore | 2 +- README.md => docs/README.md | 0 {diagrams => docs/_diagrams}/README.md | 4 +- {diagrams => docs/_diagrams}/db.tex | 0 {diagrams => docs/_diagrams}/film.er | 0 {diagrams => docs/_diagrams}/orders.er | 0 default.nix => docs/default.nix | 13 +++-- .../extensions}/sphinx-copybutton.nix | 0 .../extensions}/sphinx-tabs.nix | 0 livereload_docs.py => docs/livereload_docs.py | 4 +- postgrest.dict => docs/postgrest.dict | 0 requirements.txt => docs/requirements.txt | 0 shell.nix => docs/shell.nix | 0 16 files changed, 65 insertions(+), 51 deletions(-) delete mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/docs.yaml rename .gitignore => docs/.gitignore (72%) rename README.md => docs/README.md (100%) rename {diagrams => docs/_diagrams}/README.md (90%) rename {diagrams => docs/_diagrams}/db.tex (100%) rename {diagrams => docs/_diagrams}/film.er (100%) rename {diagrams => docs/_diagrams}/orders.er (100%) rename default.nix => docs/default.nix (78%) rename {extensions => docs/extensions}/sphinx-copybutton.nix (100%) rename {extensions => docs/extensions}/sphinx-tabs.nix (100%) rename livereload_docs.py => docs/livereload_docs.py (61%) rename postgrest.dict => docs/postgrest.dict (100%) rename requirements.txt => docs/requirements.txt (100%) rename shell.nix => docs/shell.nix (100%) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index c3bc418294..0000000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,41 +0,0 @@ -name: CI - -on: - push: - branches: - - main - - v* - pull_request: - branches: - - main - - v* - -jobs: - build: - name: Build docs - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v20 - - run: nix-env -f default.nix -iA build - - run: postgrest-docs-build - - spellcheck: - name: Run spellcheck - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v20 - - run: nix-env -f default.nix -iA spellcheck - - run: postgrest-docs-spellcheck - - linkcheck: - name: Run linkcheck - if: github.base_ref == 'main' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v20 - - run: nix-env -f default.nix -iA linkcheck - - run: postgrest-docs-linkcheck - diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000000..f9bf2fd3a5 --- /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@v3 + - uses: cachix/install-nix-action@v22 + - 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@v3 + - uses: cachix/install-nix-action@v22 + - 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@v3 + - uses: cachix/install-nix-action@v22 + - 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@v3 + - uses: cachix/install-nix-action@v22 + - run: nix-env -f docs/default.nix -iA linkcheck + - run: postgrest-docs-linkcheck + diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 3bce02afa5..e7e973dcee 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,4 +3,4 @@ sphinx: configuration: docs/conf.py python: install: - - requirements: requirements.txt + - requirements: docs/requirements.txt diff --git a/.gitignore b/docs/.gitignore similarity index 72% rename from .gitignore rename to docs/.gitignore index 4c1f956008..24f3c27992 100644 --- a/.gitignore +++ b/docs/.gitignore @@ -2,5 +2,5 @@ _build Pipfile.lock *.aux *.log -diagrams/db.pdf +_diagrams/db.pdf misspellings diff --git a/README.md b/docs/README.md similarity index 100% rename from README.md rename to docs/README.md diff --git a/diagrams/README.md b/docs/_diagrams/README.md similarity index 90% rename from diagrams/README.md rename to docs/_diagrams/README.md index 67009dc31c..408e767f27 100644 --- a/diagrams/README.md +++ b/docs/_diagrams/README.md @@ -5,7 +5,7 @@ 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 diagrams/film.er -o docs/_static/film.png +./erd_static-x86-64 -i film.er -o ../_static/film.png ``` ## LaTeX @@ -18,7 +18,7 @@ Then use this command to generate the png file. 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 ../docs/_static/ +mv db.png ../_static/ ``` LaTeX is used because it's a tweakable plain text format. diff --git a/diagrams/db.tex b/docs/_diagrams/db.tex similarity index 100% rename from diagrams/db.tex rename to docs/_diagrams/db.tex diff --git a/diagrams/film.er b/docs/_diagrams/film.er similarity index 100% rename from diagrams/film.er rename to docs/_diagrams/film.er diff --git a/diagrams/orders.er b/docs/_diagrams/orders.er similarity index 100% rename from diagrams/orders.er rename to docs/_diagrams/orders.er diff --git a/default.nix b/docs/default.nix similarity index 78% rename from default.nix rename to docs/default.nix index 5dffe887ac..363f53d4cf 100644 --- a/default.nix +++ b/docs/default.nix @@ -27,17 +27,19 @@ in 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 docs _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 @@ -49,8 +51,9 @@ in pkgs.writeShellScriptBin "postgrest-docs-spellcheck" '' set -euo pipefail + cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/docs" - FILES=$(find docs -type f -iname '*.rst' | tr '\n' ' ') + FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ') cat $FILES \ | grep -v '^\(\.\.\| \)' \ @@ -66,8 +69,9 @@ in pkgs.writeShellScriptBin "postgrest-docs-dictcheck" '' set -euo pipefail + cd "$(${pkgs.git}/bin/git rev-parse --show-toplevel)/docs" - FILES=$(find docs -type f -iname '*.rst' | tr '\n' ' ') + FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ') cat postgrest.dict \ | tail -n+2 \ @@ -80,7 +84,8 @@ in 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 docs _build + ${python}/bin/sphinx-build --color -b linkcheck . _build ''; } diff --git a/extensions/sphinx-copybutton.nix b/docs/extensions/sphinx-copybutton.nix similarity index 100% rename from extensions/sphinx-copybutton.nix rename to docs/extensions/sphinx-copybutton.nix diff --git a/extensions/sphinx-tabs.nix b/docs/extensions/sphinx-tabs.nix similarity index 100% rename from extensions/sphinx-tabs.nix rename to docs/extensions/sphinx-tabs.nix diff --git a/livereload_docs.py b/docs/livereload_docs.py similarity index 61% rename from livereload_docs.py rename to docs/livereload_docs.py index 8dae165774..741633d43d 100755 --- a/livereload_docs.py +++ b/docs/livereload_docs.py @@ -2,9 +2,9 @@ from livereload import Server, shell from subprocess import call ## Build docs at startup -call(['sphinx-build', '-b', 'html', '-a', '-n', 'docs', '_build']) +call(['sphinx-build', '-b', 'html', '-a', '-n', '.', '_build']) server = Server() -server.watch('docs/**/*.rst', shell('sphinx-build -b html -a -n docs _build')) +server.watch('**/*.rst', shell('sphinx-build -b html -a -n . _build')) # For custom port and host # server.serve(root='_build/', host='192.168.1.2') server.serve(root='_build/') diff --git a/postgrest.dict b/docs/postgrest.dict similarity index 100% rename from postgrest.dict rename to docs/postgrest.dict diff --git a/requirements.txt b/docs/requirements.txt similarity index 100% rename from requirements.txt rename to docs/requirements.txt diff --git a/shell.nix b/docs/shell.nix similarity index 100% rename from shell.nix rename to docs/shell.nix From a14559647b821ed8f24ff5478a8ca91ae83a0ec7 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sat, 17 Feb 2024 12:57:18 +0100 Subject: [PATCH 549/549] chore: Run postgrest-style on docs files This prevents CI from failing after the merge. --- docs/conf.py | 164 ++++++++++++++++---------------- docs/default.nix | 4 +- docs/extensions/sphinx-tabs.nix | 2 +- docs/livereload_docs.py | 7 +- docs/shell.nix | 3 +- 5 files changed, 89 insertions(+), 91 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 699ab51320..39165ee23d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,48 +18,45 @@ # 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('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# 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' -] +extensions = ["sphinx_tabs.tabs", "sphinx_copybutton"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +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' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'PostgREST' -author = u'Joe Nelson, Steve Chavez' -copyright = u'2017, ' + author +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 = u'10.2' +version = "10.2" # The full version, including alpha/beta/rc tags. -release = u'10.2.0' +release = "10.2.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -70,38 +67,38 @@ # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# 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'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The reST default role (used for this markup: `text`) to use for all # documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# 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 +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False +# keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False @@ -111,158 +108,151 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'sphinx_rtd_theme' +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 = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. -#html_title = u'PostgREST v0.4.0.0' +# 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 +# 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 +# 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' +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'] +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 = [] +# 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 +# html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = 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 = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# 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' +# 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'} +# 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' +# html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. -htmlhelp_basename = 'PostgRESTdoc' +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', + # 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', u'PostgREST Documentation', - author, 'manual'), + (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 +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# 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', u'PostgREST Documentation', - [author], 1) -] +man_pages = [(master_doc, "postgrest", "PostgREST Documentation", [author], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------- @@ -271,30 +261,38 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - (master_doc, 'PostgREST', u'PostgREST Documentation', - author, 'PostgREST', 'REST API for any PostgreSQL database', - 'Web'), + ( + 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 = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False +# texinfo_no_detailmenu = False # -- Custom setup --------------------------------------------------------- + def setup(app): - app.add_css_file('css/custom.css') + 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' +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 diff --git a/docs/default.nix b/docs/default.nix index 363f53d4cf..b986ca6ccb 100644 --- a/docs/default.nix +++ b/docs/default.nix @@ -15,8 +15,8 @@ let }) { }; - sphinxTabsPkg = ps: ps.callPackage ./extensions/sphinx-tabs.nix {}; - sphinxCopybuttonPkg = ps: ps.callPackage ./extensions/sphinx-copybutton.nix {}; + sphinxTabsPkg = ps: ps.callPackage ./extensions/sphinx-tabs.nix { }; + sphinxCopybuttonPkg = ps: ps.callPackage ./extensions/sphinx-copybutton.nix { }; python = pkgs.python3.withPackages (ps: [ ps.sphinx ps.sphinx_rtd_theme ps.livereload (sphinxTabsPkg ps) (sphinxCopybuttonPkg ps) ]); in diff --git a/docs/extensions/sphinx-tabs.nix b/docs/extensions/sphinx-tabs.nix index f90b1c18d8..fbc164419b 100644 --- a/docs/extensions/sphinx-tabs.nix +++ b/docs/extensions/sphinx-tabs.nix @@ -26,4 +26,4 @@ buildPythonPackage rec { homepage = "https://sphinx-tabs.readthedocs.io"; license = licenses.mit; }; -} \ No newline at end of file +} diff --git a/docs/livereload_docs.py b/docs/livereload_docs.py index 741633d43d..67a2165d66 100755 --- a/docs/livereload_docs.py +++ b/docs/livereload_docs.py @@ -1,10 +1,11 @@ #!/usr/bin/env python from livereload import Server, shell from subprocess import call + ## Build docs at startup -call(['sphinx-build', '-b', 'html', '-a', '-n', '.', '_build']) +call(["sphinx-build", "-b", "html", "-a", "-n", ".", "_build"]) server = Server() -server.watch('**/*.rst', shell('sphinx-build -b html -a -n . _build')) +server.watch("**/*.rst", shell("sphinx-build -b html -a -n . _build")) # For custom port and host # server.serve(root='_build/', host='192.168.1.2') -server.serve(root='_build/') +server.serve(root="_build/") diff --git a/docs/shell.nix b/docs/shell.nix index 4800d62ec1..7c048643f6 100644 --- a/docs/shell.nix +++ b/docs/shell.nix @@ -2,8 +2,7 @@ let docs = import ./default.nix; - pkgs = - docs.pkgs; + inherit (docs) pkgs; in pkgs.mkShell { name = "postgrest-docs";

  • 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 PKXR{>UY0*M{`h|Y$-4j2 literal 0 HcmV?d00001 diff --git a/index.rst b/index.rst index 3d72f2994e..a16caa4dd6 100644 --- a/index.rst +++ b/index.rst @@ -26,12 +26,14 @@ PostgREST is a standalone web server that turns your PostgreSQL database directl Sponsors -------- -.. image:: _static/timescaledb.png - :target: https://www.timescale.com?utm_campaign=postgrest&utm_source=sponsor&utm_medium=referral&utm_content=docs - :width: 222px - :align: center -`TimescaleDB `_ is an scalable time-series database packaged as a PostgreSQL extension. See our tutorial for using `TimescaleDB with PostgREST `_. +.. image:: _static/2ndquadrant.png + :target: https://www.2ndquadrant.com/en/?utm_campaign=external%20websites&utm_source=postgrest&utm_medium=logo + :width: 13em + +.. image:: _static/retool.png + :target: https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em Motivation ---------- From c21e98c9ae4dcfa4ab7d1ed8aef89046ef01db21 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sun, 16 Jun 2019 15:09:49 -0500 Subject: [PATCH 233/549] Correct url --- index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.rst b/index.rst index a16caa4dd6..ee35708508 100644 --- a/index.rst +++ b/index.rst @@ -28,7 +28,7 @@ Sponsors .. image:: _static/2ndquadrant.png - :target: https://www.2ndquadrant.com/en/?utm_campaign=external%20websites&utm_source=postgrest&utm_medium=logo + :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo :width: 13em .. image:: _static/retool.png From ecb4f15de0792313666874455b82f0367b147059 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 19 Jun 2019 21:11:46 -0500 Subject: [PATCH 234/549] Add CYBERTEC as Sponsor --- _static/css/custom.css | 4 ++++ _static/cybertec.png | Bin 0 -> 15406 bytes index.rst | 3 +++ 3 files changed, 7 insertions(+) create mode 100644 _static/cybertec.png diff --git a/_static/css/custom.css b/_static/css/custom.css index d4c0ceef8b..555afaa1c8 100644 --- a/_static/css/custom.css +++ b/_static/css/custom.css @@ -17,3 +17,7 @@ div.line-block { #sponsors h1{ text-align: left; } + +#sponsors img{ + margin: 10px; +} diff --git a/_static/cybertec.png b/_static/cybertec.png new file mode 100644 index 0000000000000000000000000000000000000000..4bb395027fa0fe438446e46d76c32a8a3d46df0c GIT binary patch literal 15406 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 DEZ}=4 literal 0 HcmV?d00001 diff --git a/index.rst b/index.rst index ee35708508..0b45f87136 100644 --- a/index.rst +++ b/index.rst @@ -26,6 +26,9 @@ PostgREST is a standalone web server that turns your PostgreSQL database directl Sponsors -------- +.. image:: _static/cybertec.png + :target: https://www.cybertec-postgresql.com/en/ + :width: 13em .. image:: _static/2ndquadrant.png :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo From 59816e65765c241d4791d768bd132f15c4363e55 Mon Sep 17 00:00:00 2001 From: Eduardo Jorge Date: Thu, 4 Jul 2019 21:14:18 +0100 Subject: [PATCH 235/549] Add missing doc for the ov operator in array types (#225) --- api.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/api.rst b/api.rst index 20c4113650..085eec6ff5 100644 --- a/api.rst +++ b/api.rst @@ -70,6 +70,9 @@ cs contains e.g. :code:`?tags=cs.{example, new}` :code:`@>` cd contained in e.g. :code:`?values=cd.{1,2,3}` :code:`<@` ov overlap (have points in common), :code:`&&` 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 strictly left of, e.g. :code:`?range=sl.(1,10)` :code:`<<` sr strictly right of :code:`>>` nxr does not extend to the right of, :code:`&<` From 5ad1afbf73c8d252a0429a14aa5eccc70e877eb4 Mon Sep 17 00:00:00 2001 From: Eduardo Jorge Date: Wed, 24 Jul 2019 17:02:58 +0100 Subject: [PATCH 236/549] Add websearch_to_tsquery doc (#226) --- api.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api.rst b/api.rst index 085eec6ff5..ebe0bdab66 100644 --- a/api.rst +++ b/api.rst @@ -66,6 +66,7 @@ is checking for exact equality (null,true,false) :code:`IS` fts :ref:`fts` using to_tsquery :code:`@@` plfts :ref:`fts` using plainto_tsquery :code:`@@` phfts :ref:`fts` using phraseto_tsquery :code:`@@` +wfts :ref:`fts` using websearch_to_tsquery :code:`@@` cs contains e.g. :code:`?tags=cs.{example, new}` :code:`@>` cd contained in e.g. :code:`?values=cd.{1,2,3}` :code:`<@` ov overlap (have points in common), :code:`&&` @@ -132,8 +133,14 @@ The :code:`fts` filter mentioned above has a number of options to support flexib GET /tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats HTTP/1.1 +.. code-block:: http + + GET /tsearch?my_tsv=not.wfts(french).amusant HTTP/1.1 + Using phrase search mode requires PostgreSQL of version at least 9.6 and will raise an error in earlier versions of the database. +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 (Columns) From ca456ecc51765f8f622b2aca837bf8adab0a70da Mon Sep 17 00:00:00 2001 From: stefan8888 Date: Mon, 29 Jul 2019 20:15:00 +0200 Subject: [PATCH 237/549] Updated Docker section (#236) * Updated Docker section On macOS, it is also necessary to add the IP address in pg_hba.conf --- install.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/install.rst b/install.rst index de4bf4187f..ebc0ea9792 100644 --- a/install.rst +++ b/install.rst @@ -297,6 +297,11 @@ The database connection string above is just an example. Adjust the role and pas .. 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 Containerized PostgREST *and* db with docker-compose ---------------------------------------------------- From f759b8bfc6b7d15f4432bc7fd318d6fdaf2b376b Mon Sep 17 00:00:00 2001 From: Erwan Thomas Date: Sun, 18 Aug 2019 23:32:08 +0200 Subject: [PATCH 238/549] Fix miscellaneous Sphinx warnings (#244) * /index.rst:24: WARNING: Line block ends without a blank line. * /install.rst:300: WARNING: Explicit markup ends without a blank line; unexpected unindent. * /admin.rst:273: WARNING: Could not lex literal_block as "http". Highlighting skipped. * /tutorials/tut1.rst:150: WARNING: Could not lex literal_block as "json". Highlighting skipped. * Remove extra newline * Use integer value as epoch in JSON snippet Even though using a string value is convenient (due to the lack of JSON comments), it is confusing since the value should actually be an integer. A slightly more verbose documentation is therefore preferable. --- admin.rst | 2 +- index.rst | 1 + install.rst | 5 +++-- tutorials/tut1.rst | 4 +++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/admin.rst b/admin.rst index f8bfd1a958..12af107e4d 100644 --- a/admin.rst +++ b/admin.rst @@ -272,7 +272,7 @@ As discussed in :ref:`singular_plural`, there are no special URL forms for singu .. code:: http - GET /people?id=eq.1 + GET /people?id=eq.1 HTTP/1.1 Accept: application/vnd.pgrst.object+json This allows compound primary keys and makes the intent for singular response independent of a URL convention. diff --git a/index.rst b/index.rst index 0b45f87136..5c8a34dad5 100644 --- a/index.rst +++ b/index.rst @@ -21,6 +21,7 @@ :target: https://www.paypal.me/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 diff --git a/install.rst b/install.rst index ebc0ea9792..807b87c783 100644 --- a/install.rst +++ b/install.rst @@ -297,10 +297,11 @@ The database connection string above is just an example. Adjust the role and pas .. 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 Containerized PostgREST *and* db with docker-compose diff --git a/tutorials/tut1.rst b/tutorials/tut1.rst index bcef5e6329..71c288d11d 100644 --- a/tutorials/tut1.rst +++ b/tutorials/tut1.rst @@ -151,9 +151,11 @@ Go back to jwt.io and change the payload to { "role": "todo_user", - "exp": + "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 From 0b5e168c8d944a7cecee1200126521918a4b4240 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 1 Aug 2019 12:22:32 -0500 Subject: [PATCH 239/549] Fix #205, operators table with heading --- api.rst | 71 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/api.rst b/api.rst index ebe0bdab66..34c8d4abf9 100644 --- a/api.rst +++ b/api.rst @@ -45,43 +45,42 @@ Complex logic can also be applied: GET /people?and=(grade.gte.90,student.is.true,or(age.gte.14,age.is.null)) HTTP/1.1 +Operators +~~~~~~~~~ + These operators are available: -============ =============================================== ===================== -Abbreviation Meaning PostgreSQL Equivalent -============ =============================================== ===================== -eq equals :code:`=` -gt greater than :code:`>` -gte greater than or equal :code:`>=` -lt less than :code:`<` -lte less than or equal :code:`<=` -neq not equal :code:`<>` or :code:`!=` -like LIKE operator (use * in place of %) :code:`LIKE` -ilike ILIKE operator (use * in place of %) :code:`ILIKE` -in one of a list of values e.g. :code:`IN` - :code:`?a=in.(1,2,3)` – also supports commas - in quoted strings like - :code:`?a=in.("hi,there","yes,you")` -is checking for exact equality (null,true,false) :code:`IS` -fts :ref:`fts` using to_tsquery :code:`@@` -plfts :ref:`fts` using plainto_tsquery :code:`@@` -phfts :ref:`fts` using phraseto_tsquery :code:`@@` -wfts :ref:`fts` using websearch_to_tsquery :code:`@@` -cs contains e.g. :code:`?tags=cs.{example, new}` :code:`@>` -cd contained in e.g. :code:`?values=cd.{1,2,3}` :code:`<@` -ov overlap (have points in common), :code:`&&` - 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 strictly left of, e.g. :code:`?range=sl.(1,10)` :code:`<<` -sr strictly right of :code:`>>` -nxr does not extend to the right of, :code:`&<` - e.g. :code:`?range=nxr.(1,10)` -nxl does not extend to the left of :code:`&>` -adj is adjacent to, e.g. :code:`?range=adj.(1,10)` :code:`-|-` -not negates another operator, see below :code:`NOT` -============ =============================================== ===================== +============ ======================== ================================================================================== +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 (use * in place of %) +ilike :code:`ILIKE` ILIKE operator (use * in place of %) +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) +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 below +============ ======================== ================================================================================== To negate any operator, prefix it with :code:`not` like :code:`?a=not.eq.2` or :code:`?not.and=(a.gte.0,a.lte.100)` . @@ -982,7 +981,7 @@ UPSERT operates based on the primary key columns, you must specify all of them. .. 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`. - + A single row UPSERT can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: From f983e0af349a0594c55d5882a9e4e6d350333318 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 1 Aug 2019 12:35:58 -0500 Subject: [PATCH 240/549] Fix #196, add virtual columns a.k.a. --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 34c8d4abf9..cf529570fd 100644 --- a/api.rst +++ b/api.rst @@ -208,7 +208,7 @@ You can specify a path for a ``json`` or ``jsonb`` column using the arrow operat Computed Columns ~~~~~~~~~~~~~~~~ -Filters may be applied to computed columns as well as actual table/view columns, even though the computed columns will not appear in the output. For example, to search first and last names at once we can create a computed column that will not appear in the output but can be used in a filter: +Filters may be applied to computed columns(**a.k.a. virtual columns**) as well as actual table/view columns, even though the computed columns will not appear in the output. For example, to search first and last names at once we can create a computed column that will not appear in the output but can be used in a filter: .. code-block:: postgres From b07d07f17d5978de623b05a4cae4c20a8f0541fa Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 1 Aug 2019 13:06:52 -0500 Subject: [PATCH 241/549] Fix #231, add note about assuming text in rpc calls --- api.rst | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/api.rst b/api.rst index cf529570fd..728e485af7 100644 --- a/api.rst +++ b/api.rst @@ -625,10 +625,6 @@ For instance, assume we have created this function in the database. SELECT a + b; $$ LANGUAGE SQL IMMUTABLE STRICT; -.. important:: - - Whenever you create or change a function you must refresh PostgREST's schema. See the section :ref:`schema_reloading`. - The client can call it by posting an object like .. code-block:: http @@ -639,6 +635,22 @@ The client can call it by posting an object like 3 +.. important:: + + Whenever you create or change a function you must refresh PostgREST's schema cache. See the section :ref:`schema_reloading`. + + If the schema cache is not refreshed, PostgREST will assume :code:`text` as the default type for function arguments. This could + lead to getting error responses like: + + .. code-block:: json + + { + "hint":"No function matches the given name and argument types. You might need to add explicit type casts.", + "details":null, + "code":"42883", + "message":"function test.add_them(a => text, b => text) does not exist" + } + You can also call a function that takes a single parameter of type json by sending the header :code:`Prefer: params=single-object` with your request. That way the JSON request body will be used as the single argument. .. code-block:: plpgsql From 03905a584e6740d2880bdacc31a4c653a63ff4ba Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 1 Aug 2019 13:26:27 -0500 Subject: [PATCH 242/549] Fix #224, remove STRICT from example rpc --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 728e485af7..074135b58f 100644 --- a/api.rst +++ b/api.rst @@ -623,7 +623,7 @@ For instance, assume we have created this function in the database. CREATE FUNCTION add_them(a integer, b integer) RETURNS integer AS $$ SELECT a + b; - $$ LANGUAGE SQL IMMUTABLE STRICT; + $$ LANGUAGE SQL IMMUTABLE; The client can call it by posting an object like From 1fd06ff65d32728b585a70b3f7c79cb2117a0612 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 1 Aug 2019 14:10:02 -0500 Subject: [PATCH 243/549] Fix #199, db-extra-search-path clarification --- install.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 807b87c783..7f6e596761 100644 --- a/install.rst +++ b/install.rst @@ -129,7 +129,9 @@ db-pool db-extra-search-path -------------------- - Extra schemas to add to the `search_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 exposed in your :ref:`db-schema`. + Extra schemas to add to the `search_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-schema`. + + This parameter was meant to make it easier to use **PostgreSQL extensions** (like PostGIS) that are outside of the :ref:`db-schema`. Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``. From 3babff99b9b0c5d2f74dc90a292f84065856baf2 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 1 Aug 2019 14:30:13 -0500 Subject: [PATCH 244/549] Fix #198, add note about nginx config location --- admin.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/admin.rst b/admin.rst index 12af107e4d..26dd090f4c 100644 --- a/admin.rst +++ b/admin.rst @@ -30,6 +30,11 @@ The first step is to create an Nginx configuration file that proxies requests to } } +.. 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`. + .. _block_fulltable: Block Full-Table Operations From 7ccc8c4610edcda420cfeae27e962a8e3b31926a Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 2 Aug 2019 13:33:59 -0500 Subject: [PATCH 245/549] Fix #197, add note about axios url encoding --- api.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api.rst b/api.rst index 074135b58f..d1599e02d0 100644 --- a/api.rst +++ b/api.rst @@ -288,6 +288,10 @@ Here ``information.cpe`` is a column name. GET /vulnerabilities?%22information.cpe%22=like.*MS* HTTP/1.1 +.. 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`. Ordering -------- From 91e44f12ec843ef4ae5b1dcefe19dff865833c3b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 2 Aug 2019 13:50:11 -0500 Subject: [PATCH 246/549] Fix #216, remove HUP reference --- admin.rst | 4 ---- install.rst | 3 --- postgrest.dict | 1 - 3 files changed, 8 deletions(-) diff --git a/admin.rst b/admin.rst index 26dd090f4c..f7075f9a61 100644 --- a/admin.rst +++ b/admin.rst @@ -222,10 +222,6 @@ Then run the `pg_listen `_ utility to mon Now, whenever the structure of the database schema changes, PostgreSQL will notify the ``ddl_command_end`` channel, which will cause ``pg_listen`` to send PostgREST the signal to reload its cache. -.. important:: - - As of PostgREST v5.1 reloading with SIGHUP is deprecated, it's still supported but will be removed in v6.0. SIGUSR1 should be used instead. - Daemonizing =========== diff --git a/install.rst b/install.rst index 7f6e596761..98abbb3a6a 100644 --- a/install.rst +++ b/install.rst @@ -249,9 +249,6 @@ PostgREST outputs basic request logging to stdout. When running it in an SSH ses # another option is to pipe the output into "logger -t postgrest" -(Avoid :code:`nohup postgrest` because the HUP signal is used for manual :ref:`schema_reloading`.) - - Docker ====== diff --git a/postgrest.dict b/postgrest.dict index 563e11994a..0faacadb5f 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -45,7 +45,6 @@ RSA RabbitMQ RestSharp SHA -SIGHUP SIGUSR1 SNS SQL From e9712b0aff72e7bd782f29f90294288aba29321e Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 5 Aug 2019 10:22:29 -0500 Subject: [PATCH 247/549] Fix #200, anon permissions on auth section --- api.rst | 2 ++ auth.rst | 16 +++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/api.rst b/api.rst index d1599e02d0..34aa00188b 100644 --- a/api.rst +++ b/api.rst @@ -768,6 +768,8 @@ A function that returns a table type response can be shaped using the same filte GET /rpc/best_films_2017?rating=gt.8&order=title.desc HTTP/1.1 +.. _func_privs: + Function privileges ------------------- diff --git a/auth.rst b/auth.rst index 0800a08106..bf6fa09811 100644 --- a/auth.rst +++ b/auth.rst @@ -425,7 +425,7 @@ As described in `JWT from SQL`_, we'll create a JWT inside our login function. N into result; return result; end; - $$ language plpgsql; + $$ language plpgsql security definer; An API request to call this function would look like: @@ -446,18 +446,20 @@ The response would look like the snippet below. Try decoding the token at `jwt.i Permissions ~~~~~~~~~~~ -Your database roles need access to the schema, tables, views and functions in order to service HTTP requests. Recall from the `Overview of Role System`_ 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. +Your database roles need access to the schema, tables, views and functions in order to service HTTP requests. +Recall from the `Overview of Role System`_ 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:: sql +.. code-block:: postgres -- the names "anon" and "authenticator" are configurable and not -- sacred, we simply choose them for clarity - create role anon; + create role anon noinherit; create role authenticator noinherit; grant anon to authenticator; - grant usage on schema public, basic_auth to anon; - grant select on table pg_authid, basic_auth.users to anon; grant execute on function login(text,text) to anon; -You may be worried from the above that anonymous users can read everything from the :code:`basic_auth.users` table. However this table is not available for direct queries because it lives in a separate schema. The anonymous role needs access because the public :code:`users` view reads the underlying table with the permissions of the calling user. But we have made sure the view properly restricts access to sensitive information. +Since the above :code:`login` function is defined as `security definer `_, +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. From a1509dc7315b4ebc3d4e87f1b47255decb606c67 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 5 Aug 2019 11:07:44 -0500 Subject: [PATCH 248/549] Fix #170, remove no performance penalty claim --- auth.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/auth.rst b/auth.rst index bf6fa09811..4b770cb24e 100644 --- a/auth.rst +++ b/auth.rst @@ -50,7 +50,7 @@ PostgREST can accommodate either viewpoint. If you treat a role as a single user 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:: sql +.. code-block:: postgres CREATE TABLE chat ( message_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), @@ -65,7 +65,7 @@ We want to enforce a policy that ensures a user can see only those messages sent PostgreSQL (9.5 and later) allows us to set this policy with row-level security: -.. code:: sql +.. code-block:: postgres CREATE POLICY chat_policy ON chat USING ((message_to = current_user) OR (message_from = current_user)) @@ -73,6 +73,10 @@ PostgreSQL (9.5 and later) allows us to set this policy with row-level security: 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 ~~~~~~~~~~~~~~~~~~~~~~ @@ -96,9 +100,9 @@ This allows JWT generation services to include extra information and your databa Hybrid User-Group Roles ~~~~~~~~~~~~~~~~~~~~~~~ -There is no performance penalty for having many database roles, although roles are namespaced per-cluster rather than per-database so may be prone to collision within the database. You are free to assign a new role for every user in a web application if desired. 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: +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:: sql +.. code-block:: postgres CREATE ROLE webuser NOLOGIN; -- grant this role access to certain tables etc From 664604e6cafc6825c2b36df0e4e011366f844ea0 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 5 Aug 2019 13:12:26 -0500 Subject: [PATCH 249/549] Fix #212, note about view with complex RULEs --- api.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api.rst b/api.rst index 34aa00188b..c54b01937c 100644 --- a/api.rst +++ b/api.rst @@ -947,6 +947,12 @@ Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`. +.. warning:: + + Insertion on VIEWs with complex `RULEs `_ might not work out of the box with PostgREST. + It's recommended that you `use triggers instead of RULEs `_. + 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. + .. _bulk_insert: Bulk Insert From aba2f43c1019897d084e0bdd743f943ec09903ee Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 10 Aug 2019 13:40:42 -0500 Subject: [PATCH 250/549] Fix #230, make clear FKs are needed for embedding --- api.rst | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/api.rst b/api.rst index c54b01937c..8381f85642 100644 --- a/api.rst +++ b/api.rst @@ -487,7 +487,13 @@ If the stored procedure returns non-scalar values, you need to do a :code:`selec Resource Embedding ================== -In addition to providing RESTful routes for each table and view, PostgREST allows related resources to be included together in a single API call. This reduces the need for multiple API requests. The server uses foreign keys to determine which tables and views can be returned together. For example, consider a database of films and their awards: +In addition to providing RESTful routes for each table and view, PostgREST allows related resources to be included together in a single +API call. This reduces the need for multiple API requests. The server uses **foreign keys** to determine which tables and views can be +returned together. For example, consider a database of films and their awards: + +.. important:: + + PostgREST needs `FOREIGN KEY constraints `_ to be able to do Resource Embedding. .. image:: _static/film.png @@ -547,15 +553,18 @@ this: GET /films?select=title,director:directors(id,last_name) HTTP/1.1 -PostgREST can also detect relations going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). You can also reverse the direction of inclusion, asking for all Directors with each including the list of their Films: +.. important:: -.. code-block:: http + Whenever FOREIGN KEY constraints change in the database schema you must refresh PostgREST's schema cache for Resource Embedding to work properly. See the section :ref:`schema_reloading`. - GET /directors?select=films(title,year) HTTP/1.1 +Embeddeding through join tables +------------------------------- -.. important:: +PostgREST can also detect relationships going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). You can also reverse the direction of inclusion, asking for all Directors with each including the list of their Films: - Whenever foreign key relations change in the database schema you must refresh PostgREST's schema cache to allow resource embedding to work properly. See the section :ref:`schema_reloading`. +.. code-block:: http + + GET /directors?select=films(title,year) HTTP/1.1 Embedded Filters ---------------- From 5c22846e12d137b323755dc8e0f1f869ecddcc22 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 10 Aug 2019 18:39:55 -0500 Subject: [PATCH 251/549] Fix #172, add section on embedding views --- api.rst | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 8381f85642..7161eeb480 100644 --- a/api.rst +++ b/api.rst @@ -488,7 +488,7 @@ Resource Embedding ================== In addition to providing RESTful routes for each table and view, PostgREST allows related resources to be included together in a single -API call. This reduces the need for multiple API requests. The server uses **foreign keys** to determine which tables and views can be +API call. This reduces the need for multiple API requests. The server uses **foreign keys** to determine which tables and views can be returned together. For example, consider a database of films and their awards: .. important:: @@ -601,6 +601,41 @@ Embedded resources can be aliased and filters can be applied on these aliases: GET /films?select=*,90_comps:competitions(name),91_comps:competitions(name)&90_comps.year=eq.1990&91_comps.year=eq.1991 HTTP/1.1 +Embedding Views +--------------- + +Embedding a view is possible if the view contains columns that have **foreign keys** defined in their source tables. + +As an example, let's create a view called ``nominations_view`` based on the nominations table. + +.. code-block:: postgres + + CREATE VIEW nominations_view AS + SELECT + rank + , competition_id + , film_id + FROM + nominations; + +Since it contains ``competition_id`` and ``film_id``—and each one has a **foreign key** defined in its source table—we can embed competitions and films: + +.. code-block:: http + + GET /nominations_view?select=rank,competitions(name,year),films(title)&rank=eq.5 HTTP/1.1 + + +.. warning:: + + Is not guaranteed that all kinds of views will be embeddable. In particular, views that contain + UNIONs will not be made embeddable. + + Why? PostgREST detects source table foreign keys in the view by querying and parsing `pg_rewrite `_. + This may fail depending on the complexity of the view, it's a best-effort approach. + +.. important:: + + If view definitions change you must refresh PostgREST's schema cache for this to work properly. See the section :ref:`schema_reloading`. .. _custom_queries: From 396b0468ce9c0b2d9b0003050beeea99e473dd7a Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 12 Aug 2019 14:13:20 -0500 Subject: [PATCH 252/549] Refine embedding views section --- api.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/api.rst b/api.rst index 7161eeb480..efdff6b20c 100644 --- a/api.rst +++ b/api.rst @@ -606,7 +606,7 @@ Embedding Views Embedding a view is possible if the view contains columns that have **foreign keys** defined in their source tables. -As an example, let's create a view called ``nominations_view`` based on the nominations table. +As an example, let's create a view called ``nominations_view`` based on the *nominations* table. .. code-block:: postgres @@ -618,7 +618,7 @@ As an example, let's create a view called ``nominations_view`` based on the nomi FROM nominations; -Since it contains ``competition_id`` and ``film_id``—and each one has a **foreign key** defined in its source table—we can embed competitions and films: +Since it contains ``competition_id`` and ``film_id``—and each one has a **foreign key** defined in its source table—we can embed *competitions* and *films*: .. code-block:: http @@ -631,7 +631,12 @@ Since it contains ``competition_id`` and ``film_id``—and each one has a **fore UNIONs will not be made embeddable. Why? PostgREST detects source table foreign keys in the view by querying and parsing `pg_rewrite `_. - This may fail depending on the complexity of the view, it's a best-effort approach. + This may fail depending on the complexity of the view. + + `Report an issue `_ if your view is not made embeddable so we can + keep continue improving foreign key detection. + + In the future we'll include include a way to manually specify views source foreign keys to address this limitation. .. important:: From cb2bf809a7fd4b28ddc5c8097eab40dab135d4dd Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 13 Aug 2019 11:55:21 -0500 Subject: [PATCH 253/549] Fix #219, how-to for embedding table from other schema --- api.rst | 2 + .../embedding-table-from-another-schema.rst | 79 +++++++++++++++++++ index.rst | 6 ++ livereload_docs.py | 1 + 4 files changed, 88 insertions(+) create mode 100644 how-tos/embedding-table-from-another-schema.rst diff --git a/api.rst b/api.rst index efdff6b20c..7c9ed9b9ed 100644 --- a/api.rst +++ b/api.rst @@ -601,6 +601,8 @@ Embedded resources can be aliased and filters can be applied on these aliases: GET /films?select=*,90_comps:competitions(name),91_comps:competitions(name)&90_comps.year=eq.1990&91_comps.year=eq.1991 HTTP/1.1 +.. _embedding_views: + Embedding Views --------------- diff --git a/how-tos/embedding-table-from-another-schema.rst b/how-tos/embedding-table-from-another-schema.rst new file mode 100644 index 0000000000..7517064646 --- /dev/null +++ b/how-tos/embedding-table-from-another-schema.rst @@ -0,0 +1,79 @@ +Embedding a table from another schema +===================================== + +Suppose you have a **people** table in the ``public`` schema and this schema is exposed through PostgREST's :ref:`db-schema`. + +.. code-block:: postgres + + create table public.people( + id int primary key + , full_name text + ); + +And you want to :ref:`embed ` the **people** table with a **details** table that's in another schema named ``private``. + +.. code-block:: postgres + + create schema if not exists private; + + -- For simplicity's sake the table is devoid of constraints on email, phone, etc. + create table private.details( + id int primary key references public.people + , email text + , phone text + , birthday date + , occupation text + , company text + ); + + -- other database objects in this schema + -- ... + -- ... + +To solve this, you can create a view of **details** in the ``public`` schema. We'll call it **public_details**. + +.. code-block:: postgres + + create view public.public_details as + select + id + , occupation + , company + from + private.details; + +Since PostgREST supports :ref:`embedding_views`, you can embed **people** with **public_details**. + +Let's insert some data to test this: + +.. code-block:: postgres + + insert into + public.people + values + (1, 'John Doe'), (2, 'Jane Doe'); + + insert into + private.details + values + (1, 'jhon@fake.com', '772-323-5433', '1990-02-01', 'Transportation attendant', 'Body Fate'), + (2, 'jane@fake.com', '480-474-6571', '1980-04-21', 'Geotechnical engineer', 'Earthworks Garden Kare'); + +.. important:: + + Make sure PostgREST's schema cache is up-to-date. See :ref:`schema_reloading`. + +Now, make the following request: + +.. code-block:: bash + + curl "http://localhost:3000/people?select=full_name,public_details(occupation,company)" + +The result should be: + +.. code-block:: json + + [ + {"full_name":"John Doe","public_details":[{"occupation":"Transportation attendant","company":"Body Fate"}]}, + {"full_name":"Jane Doe","public_details":[{"occupation":"Geotechnical engineer","company":"Earthworks Garden Kare"}]} + ] diff --git a/index.rst b/index.rst index 5c8a34dad5..f8649000dd 100644 --- a/index.rst +++ b/index.rst @@ -105,6 +105,12 @@ Translations tutorials/tut0.rst tutorials/tut1.rst +.. toctree:: + :caption: How-to guides + :titlesonly: + + how-tos/embedding-table-from-another-schema.rst + .. toctree:: :caption: Integrations :titlesonly: diff --git a/livereload_docs.py b/livereload_docs.py index 9983589aa5..d4d0ba6e8e 100755 --- a/livereload_docs.py +++ b/livereload_docs.py @@ -6,4 +6,5 @@ server = Server() server.watch('*.rst', shell('sphinx-build -b html -a -n . _build')) server.watch('tutorials/*.rst', shell('sphinx-build -b html -a -n . _build')) +server.watch('how-tos/*.rst', shell('sphinx-build -b html -a -n . _build')) server.serve(root='_build/') From bed1013ecd5334b0bf8a6ee8abaa2b6c63aed551 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 14 Aug 2019 21:49:49 -0500 Subject: [PATCH 254/549] Fix #235, casting type to json object --- api.rst | 8 ++ how-tos/casting-type-to-custom-json.rst | 95 +++++++++++++++++++ .../embedding-table-from-another-schema.rst | 2 +- index.rst | 1 + 4 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 how-tos/casting-type-to-custom-json.rst diff --git a/api.rst b/api.rst index 7c9ed9b9ed..ca8212c3e1 100644 --- a/api.rst +++ b/api.rst @@ -158,6 +158,9 @@ When certain columns are wide (such as those holding binary data), it is more ef The default is :sql:`*`, meaning all columns. This value will become more important below in :ref:`resource_embedding`. +Renaming Columns +~~~~~~~~~~~~~~~~ + You can rename the columns by prefixing them with an alias followed by the colon ``:`` operator. .. code-block:: http @@ -169,6 +172,11 @@ You can rename the columns by prefixing them with an alias followed by the colon {"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:: http diff --git a/how-tos/casting-type-to-custom-json.rst b/how-tos/casting-type-to-custom-json.rst new file mode 100644 index 0000000000..f9d13afa79 --- /dev/null +++ b/how-tos/casting-type-to-custom-json.rst @@ -0,0 +1,95 @@ +Casting a type to a custom JSON object +====================================== + +While using PostgREST you might have noticed that certain PostgreSQL types translate to JSON strings when you would +have expected a JSON object or array. For example, let's see the case of `range types `_. + +.. code-block:: postgres + + -- example taken from https://www.postgresql.org/docs/11/rangetypes.html#RANGETYPES-EXAMPLES + create table reservations ( + room int + , during tsrange + ); + + insert into + reservations + values + (1108, tsrange('2010-01-01 14:30', '2010-01-01 15:30')); + +Here we have a column named **during** as a ``tsrange`` type, we would like to get it as JSON through PostgREST. + +.. code-block:: bash + + curl "http://localhost:3000/reservations" + +Result: + +.. code-block:: json + + [ + { + "room":1108, + "during":"[\"2010-01-01 14:30:00\",\"2010-01-01 15:30:00\")" + } + ] + +The **during** value is probably not the in the format you want. We get a JSON string because by default PostgreSQL casts +the type to JSON by using its ``text`` representation. We can change this representation to a custom JSON object by `creating a CAST `_ . + +To do this, first we'll define the function that will do the conversion from ``tsrange`` to ``json``. + +.. 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; + +Using this function we'll create the CAST. + +.. code-block:: postgres + + create cast (tsrange as json) with function tsrange_to_json(tsrange) as assignment; + +And we'll do the request and :ref:`cast the column `. + +.. code-block:: bash + + curl "http://localhost:3000/reservations?select=room,during::json" + +The result now is: + +.. code-block:: json + + [ + { + "room":1108, + "during":{ + "lower" : "2010-01-01T14:30:00", + "upper" : "2010-01-01T15:30:00", + "lower_inc" : true, + "upper_inc" : false + } + } + ] + +You can use the same idea for creating custom CASTs for different types. + +.. note:: + + If you don't want to modify CASTs for built-in types, an option would be to `create a custom type `_ + 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 analoguously to the above example + -- ... + + create cast (mytsrange as json) with function mytsrange_to_json(mytsrange) as assignment; diff --git a/how-tos/embedding-table-from-another-schema.rst b/how-tos/embedding-table-from-another-schema.rst index 7517064646..60265dd409 100644 --- a/how-tos/embedding-table-from-another-schema.rst +++ b/how-tos/embedding-table-from-another-schema.rst @@ -16,7 +16,7 @@ And you want to :ref:`embed ` the **people** table with a ** create schema if not exists private; - -- For simplicity's sake the table is devoid of constraints on email, phone, etc. + -- For simplicity's sake the table is devoid of constraints/domains on email, phone, etc. create table private.details( id int primary key references public.people , email text diff --git a/index.rst b/index.rst index f8649000dd..8f6bdf28ff 100644 --- a/index.rst +++ b/index.rst @@ -110,6 +110,7 @@ Translations :titlesonly: how-tos/embedding-table-from-another-schema.rst + how-tos/casting-type-to-custom-json.rst .. toctree:: :caption: Integrations From abdc2ac2a98ae8ae4277e5c9fd868e5e8893f5a6 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 15 Aug 2019 14:40:06 -0500 Subject: [PATCH 255/549] Remove integrations section --- index.rst | 6 - integrations/timescaledb.rst | 327 ----------------------------------- 2 files changed, 333 deletions(-) delete mode 100644 integrations/timescaledb.rst diff --git a/index.rst b/index.rst index 8f6bdf28ff..c126cddaf1 100644 --- a/index.rst +++ b/index.rst @@ -112,12 +112,6 @@ Translations how-tos/embedding-table-from-another-schema.rst how-tos/casting-type-to-custom-json.rst -.. toctree:: - :caption: Integrations - :titlesonly: - - integrations/timescaledb.rst - .. toctree:: :caption: Installation :titlesonly: diff --git a/integrations/timescaledb.rst b/integrations/timescaledb.rst deleted file mode 100644 index ea372a507b..0000000000 --- a/integrations/timescaledb.rst +++ /dev/null @@ -1,327 +0,0 @@ -TimescaleDB for Time-Series Data -================================ - -`TimescaleDB `_ is an open-source database designed to make SQL scalable for time-series data. It is engineered up from PostgreSQL, providing automatic partitioning across time and space, while retaining the standard PostgreSQL interface. - -PostgREST turns your PostgreSQL database directly into a RESTful API, since TimescaleDB is packaged as a PostgreSQL extension it works with PostgREST as well. - -In this tutorial we'll explore some of TimescaleDB features through PostgREST. - -Install Docker --------------- - -For an easier setup we're going to use `Docker `_, make sure you have it installed. - -Run TimescaleDB ---------------- - -First, let’s pull and start the `TimescaleDB container image `_: - -.. code-block:: bash - - docker run --name tsdb_tut \ - -e POSTGRES_PASSWORD=mysecretpassword \ - -p 5433:5432 \ - -d timescale/timescaledb:latest-pg11 - -This will run the container as a daemon and expose port ``5433`` to the host system so that it doesn't conflict with another PostgreSQL installation. - -Set up TimescaleDB ------------------- - -Now, we'll create the ``timescaledb`` extension in our database. - -Run ``psql`` in the container we created in the previous step. - -.. code-block:: bash - - docker exec -it tsdb_tut psql -U postgres - ## Run all the following commands inside psql - -And create the extension: - -.. code-block:: postgres - - create extension if not exists timescaledb cascade; - -Create an Hypertable --------------------- - -`Hypertables `_ are the core abstraction TimescaleDB offers for dealing with time-series data. - -To create an ``hypertable``, first we need to create standard PostgreSQL tables: - -.. code-block:: postgres - - create table if not exists locations( - device_id text primary key - , location text - , environment text - ); - - create table if not exists conditions( - time timestamp with time zone not null - , device_id text references locations(device_id) - , temperature numeric - , humidity numeric - ); - -Now, we'll convert ``conditions`` into an hypertable with `create_hypertable `_: - -.. code-block:: postgres - - SELECT create_hypertable('conditions', 'time', chunk_time_interval => interval '1 day'); - -- This also implicitly creates an index: CREATE INDEX ON "conditions"(time DESC); - - -- Exit psql - exit - - -Load sample data ----------------- - -To have some data to play with, we'll download the ``weather_small`` data set from `TimescaleDB's sample datasets `_. - -.. code-block:: bash - - ## Run bash inside the database container - docker exec -it tsdb_tut bash - - ## Download and uncompress the data - wget -qO- https://timescaledata.blob.core.windows.net/datasets/weather_small.tar.gz | tar xvz - - ## Copy data into the database - psql -U postgres <`_: - -.. code-block:: bash - - docker run --rm -p 3000:3000 \ - --name tsdb_pgrst \ - --link tsdb_tut \ - -e PGRST_DB_URI="postgres://postgres:mysecretpassword@tsdb_tut/postgres" \ - -e PGRST_DB_ANON_ROLE="postgres" \ - -d postgrest/postgrest:latest - -PostgREST on Hypertables ------------------------- - -We'll now see how to read data from hypertables through PostgREST. - -Since hypertables can be queried using standard `SELECT statements `_, we can query them through PostgREST normally. - -Suppose we want to run this query on ``conditions``: - -.. code-block:: postgres - - select - time, - device_id, - humidity - from conditions - where - humidity > 90 and - time < '2016-11-16' - order by time desc - limit 10; - -Using PostgREST :ref:`horizontal `/:ref:`vertical ` filtering, this query can be expressed as: - -.. code-block:: bash - - curl -G "localhost:3000/conditions" \ - -d select=time,device_id,humidity \ - -d humidity=gt.90 \ - -d time=lt.2016-11-16 \ - -d order=time.desc \ - -d limit=10 - ## This command is equivalent to: - ## curl "localhost:3000/conditions?select=time,device_id,humidity&humidity=gt.90&time=lt.2016-11-16&order=time.desc&limit=10" - ## Here we used -G and -d to make the command more readable - -The response will be: - -.. code-block:: json - - [{"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000982","humidity":90.90000000000006}, - {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000968","humidity":92.3}, - {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000963","humidity":96.29999999999993}, - {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000951","humidity":94.39999999999998}, - {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000950","humidity":93.69999999999982}, - {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000915","humidity":94.69999999999997}, - {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000911","humidity":93.2000000000001}, - {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000910","humidity":91.30000000000017}, - {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000901","humidity":92.30000000000005}, - {"time":"2016-11-15T23:58:00+00:00","device_id":"weather-pro-000895","humidity":91.00000000000014}] - -JOINs with relational tables ----------------------------- - -Hypertables support all standard `PostgreSQL constraints `_ . We can make use of the foreign key defined on ``locations`` to make a JOIN through PostgREST. A query such as: - -.. code-block:: postgres - - select - c.time, - c.temperature, - l.location, - l.environment - from conditions c - left join locations l on - c.device_id = l.device_id - order by time desc - limit 10; - -Can be expressed in PostgREST by using :ref:`resource_embedding`. - -.. code-block:: bash - - curl -G localhost:3000/conditions \ - -d select="time,temperature,device:locations(location,environment)" \ - -d order=time.desc \ - -d limit=10 - -.. code-block:: json - - [{"time":"2016-11-16T21:18:00+00:00","temperature":69.49999999999991,"device":{"location":"office-000202","environment":"inside"}}, - {"time":"2016-11-16T21:18:00+00:00","temperature":90,"device":{"location":"field-000205","environment":"outside"}}, - {"time":"2016-11-16T21:18:00+00:00","temperature":60.499999999999986,"device":{"location":"door-00085","environment":"doorway"}}, - {"time":"2016-11-16T21:18:00+00:00","temperature":91,"device":{"location":"swamp-000188","environment":"outside"}}, - {"time":"2016-11-16T21:18:00+00:00","temperature":42,"device":{"location":"arctic-000219","environment":"outside"}}, - {"time":"2016-11-16T21:18:00+00:00","temperature":70.80000000000003,"device":{"location":"office-000201","environment":"inside"}}, - {"time":"2016-11-16T21:18:00+00:00","temperature":62.699999999999974,"device":{"location":"door-00084","environment":"doorway"}}, - {"time":"2016-11-16T21:18:00+00:00","temperature":85.49999999999918,"device":{"location":"field-000204","environment":"outside"}}, - {"time":"2016-11-16T21:18:00+00:00","temperature":42,"device":{"location":"arctic-000218","environment":"outside"}}, - {"time":"2016-11-16T21:18:00+00:00","temperature":42,"device":{"location":"arctic-000217","environment":"outside"}}] - -Time-Oriented Analytics ------------------------ - -TimescaleDB includes new aggregate functions for time-oriented `analytics `_. - -For using aggregate queries with PostgREST you must create VIEWs or :ref:`s_procs`. Here's an example for using `time_bucket `_: - -.. code-block:: postgres - - -- Run psql in the database container - docker exec -it tsdb_tut psql -U postgres - - -- Create the function - create or replace function temperature_summaries(gap interval default '1 hour', prefix text default 'field') - returns table(hour text, avg_temp numeric, min_temp numeric, max_temp numeric) as $$ - select - time_bucket(gap, time)::text as hour, - trunc(avg(temperature), 2), - trunc(min(temperature), 2), - trunc(max(temperature), 2) - from conditions c - where c.device_id in ( - select device_id from locations - where location like prefix || '-%') - group by hour - $$ language sql stable; - - -- Exit psql - exit - -Every time the schema is changed you must reload PostgREST :ref:`schema cache ` so it can pick up the function parameters correctly. To reload, run: - -.. code-block:: bash - - docker kill --signal=USR1 tsdb_pgrst - - -Now, since the function is ``stable``, we can call it with ``GET`` as: - -.. code-block:: bash - - curl -G "localhost:3000/rpc/temperature_summaries" \ - -d gap=2minutes \ - -d order=hour.asc \ - -d limit=10 \ - -H "Accept: text/csv" - ## time_bucket accepts an interval type as it's argument - ## so you can pass gap=5minutes or gap=5hours - -.. code-block:: sql - - hour,avg_temp,min_temp,max_temp - "2016-11-15 12:00:00+00",72.97,68.00,78.00 - "2016-11-15 12:02:00+00",73.01,68.00,78.00 - "2016-11-15 12:04:00+00",73.05,68.00,78.10 - "2016-11-15 12:06:00+00",73.07,68.00,78.10 - "2016-11-15 12:08:00+00",73.11,68.00,78.10 - "2016-11-15 12:10:00+00",73.14,68.00,78.10 - "2016-11-15 12:12:00+00",73.17,68.00,78.19 - "2016-11-15 12:14:00+00",73.21,68.10,78.19 - "2016-11-15 12:16:00+00",73.24,68.10,78.29 - "2016-11-15 12:18:00+00",73.27,68.10,78.39 - -Note you can use PostgREST standard filtering on function results. Here we also changed the :ref:`res_format` to CSV. - -Fast Ingestion with Bulk Insert -------------------------------- - -You can use PostgREST :ref:`bulk_insert` to leverage TimescaleDB `fast ingestion `_. - -Let's do an insert of three rows: - -.. code-block:: bash - - curl "localhost:3000/conditions" \ - -H "Content-Type: application/json" \ - -H "Prefer: return=representation" \ - -d @- << EOF - [ - {"time": "2019-02-21 01:00:01-05", "device_id": "weather-pro-000000", "temperature": 40.0, "humidity": 59.9}, - {"time": "2019-02-21 01:00:02-05", "device_id": "weather-pro-000000", "temperature": 42.0, "humidity": 69.9}, - {"time": "2019-02-21 01:00:03-05", "device_id": "weather-pro-000000", "temperature": 44.0, "humidity": 79.9} - ] - EOF - -By using the ``Prefer: return=representation`` header we can see the successfully inserted rows: - -.. code-block:: json - - [{"time":"2019-02-21T06:00:01+00:00","device_id":"weather-pro-000000","temperature":40.0,"humidity":59.9}, - {"time":"2019-02-21T06:00:02+00:00","device_id":"weather-pro-000000","temperature":42.0,"humidity":69.9}, - {"time":"2019-02-21T06:00:03+00:00","device_id":"weather-pro-000000","temperature":44.0,"humidity":79.9}] - -Let's now insert a thousand rows, we'll use `jq `_ for constructing the array. - -.. code-block:: bash - - yes "{\"time\": \"$(date +'%F %T')\", \"device_id\": \"weather-pro-000001\", \"temperature\": 50, \"humidity\": 60}" | \ - head -n 1000 | jq -s '.' | \ - curl -i -d @- "http://localhost:3000/conditions" \ - -H "Content-Type: application/json" \ - -H "Prefer: count=exact" - -With ``Prefer: count=exact`` we can know how many rows were inserted. Check out the response: - -.. code-block:: haskell - - HTTP/1.1 201 Created - Transfer-Encoding: chunked - Date: Fri, 22 Feb 2019 16:47:05 GMT - Server: postgrest/5.2.0 (9969262) - Content-Range: */1000 - -You can see in ``Content-Range`` that the total number of inserted rows is ``1000``. - -Summing it up -------------- - -There you have it, with PostgREST you can get an instant and performant RESTful API for a TimescaleDB database. - -For a more in depth exploration of TimescaleDB capabilities, check their `docs `_. From f5868bc277a511654009830f95991192c16bfa2b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 16 Aug 2019 14:25:55 -0500 Subject: [PATCH 256/549] Add server-unix-socket config --- install.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 98abbb3a6a..72a0d9fadc 100644 --- a/install.rst +++ b/install.rst @@ -78,6 +78,7 @@ db-pool Int 10 db-extra-search-path String public server-host String 127.0.0.1 server-port Int 3000 +server-unix-socket String server-proxy-uri String jwt-secret String jwt-aud String @@ -153,7 +154,17 @@ server-host server-port ----------- - The port to bind the web server. + The TCP port to bind the web server. + +server-unix-socket +------------------ + + `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-proxy-uri: From eca325c11b7fbe9f638b2340cb7a0c243d909e79 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 16 Aug 2019 14:36:59 -0500 Subject: [PATCH 257/549] Add materialized views mention --- api.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index ca8212c3e1..c5904779e6 100644 --- a/api.rst +++ b/api.rst @@ -565,8 +565,8 @@ this: Whenever FOREIGN KEY constraints change in the database schema you must refresh PostgREST's schema cache for Resource Embedding to work properly. See the section :ref:`schema_reloading`. -Embeddeding through join tables -------------------------------- +Embedding through join tables +----------------------------- PostgREST can also detect relationships going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). You can also reverse the direction of inclusion, asking for all Directors with each including the list of their Films: @@ -634,6 +634,7 @@ Since it contains ``competition_id`` and ``film_id``—and each one has a **fore GET /nominations_view?select=rank,competitions(name,year),films(title)&rank=eq.5 HTTP/1.1 +It's also possible to embed `Materialized Views `_. .. warning:: From a8b2ed313e64bc7b8759a3cd1306f52fb94c7131 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 17 Aug 2019 13:11:15 -0500 Subject: [PATCH 258/549] Add db-pool-timeout config --- install.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install.rst b/install.rst index 72a0d9fadc..0e288d3eee 100644 --- a/install.rst +++ b/install.rst @@ -75,6 +75,7 @@ db-uri String Y db-schema String Y db-anon-role String Y db-pool Int 10 +db-pool-timeout Int 10 db-extra-search-path String public server-host String 127.0.0.1 server-port Int 3000 @@ -125,6 +126,11 @@ db-pool Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the :code:`max_connections` GUC in your database. +db-pool-timeout +--------------- + + Time to live for an idle database pool connection. + .. _db-extra-search-path: db-extra-search-path From 2414c7f7f0e58012fa238574aeab4f13d8f3b0af Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 17 Aug 2019 14:45:45 -0500 Subject: [PATCH 259/549] Add bulk call reference --- api.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/api.rst b/api.rst index c5904779e6..d0470ed160 100644 --- a/api.rst +++ b/api.rst @@ -811,6 +811,27 @@ PostgREST will detect if the function is scalar or table-valued and will shape t { "title": "Blade Runner 2049", "rating": 8.1} ] +Bulk Call +--------- + +It's possible to call a function in a bulk way, analoguosly to :ref:`bulk_insert`. + +.. code-block:: http + + POST /rpc/add_them HTTP/1.1 + Content-Type: application/json + + [ + {"a": 1, "b": 2}, + {"a": 3, "b": 4} + ] + +Result: + +.. code-block:: json + + [ 3, 7 ] + Function filters ---------------- From 51c0ad3e0c0467af53de208a69ed7ed0b511f543 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 17 Aug 2019 15:34:14 -0500 Subject: [PATCH 260/549] Add specify columns reference --- api.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/api.rst b/api.rst index d0470ed160..82948fe792 100644 --- a/api.rst +++ b/api.rst @@ -832,6 +832,8 @@ Result: [ 3, 7 ] +It's also possible to :ref:`Specify Columns ` on functions calls. + Function filters ---------------- @@ -1066,6 +1068,34 @@ To bulk insert JSON post an array of objects having all-matching keys { "name": "Janus", "age": 10, "height": 55 } ] +.. _specify_columns: + +Specifying Columns +------------------ + +By using the :code:`columns` query parameter it's possible to specify the payload keys that will be inserted/updated +and ignore the rest of the payload. + +.. code-block:: http + + POST /datasets?columns=source,publication_date,figure HTTP/1.1 + Content-Type: application/json + + { + "source": "Natural Disaster Prevention and Control", + "publication_date": "2015-09-11", + "figure": 1100, + "location": "...", + "comment": "...", + "extra": "...", + "stuff": "..." + } + +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. + Upsert ------ From afff7a00a48a173f36dacbb1b01bf558ff4ad8e3 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 19 Aug 2019 13:06:12 -0500 Subject: [PATCH 261/549] Put accessing and setting headers together --- api.rst | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/api.rst b/api.rst index 82948fe792..67a911b5bb 100644 --- a/api.rst +++ b/api.rst @@ -914,6 +914,20 @@ Stored procedures can access request headers, cookies and jwt claims by reading ``request.jwt.claim.role`` defaults to the value of :ref:`db-anon-role`. +Setting Response Headers +------------------------ + +PostgREST reads the ``response.headers`` SQL variable to add extra headers to the HTTP response. Stored procedures can modify this variable. For instance, this statement would add caching headers to the response: + +.. code-block:: sql + + -- tell client to cache response for two days + + SET LOCAL "response.headers" = + '[{"Cache-Control": "public"}, {"Cache-Control": "max-age=259200"}]'; + +Notice that the variable 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 to be repeated when setting multiple values and an object would not allow the repeated key. + Errors and HTTP Status Codes ---------------------------- @@ -962,20 +976,6 @@ Returns: {"hint":"Upgrade your plan","details":"Quota exceeded"} -Setting Response Headers ------------------------- - -PostgREST reads the ``response.headers`` SQL variable to add extra headers to the HTTP response. Stored procedures can modify this variable. For instance, this statement would add caching headers to the response: - -.. code-block:: sql - - -- tell client to cache response for two days - - SET LOCAL "response.headers" = - '[{"Cache-Control": "public"}, {"Cache-Control": "max-age=259200"}]'; - -Notice that the variable 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 to be repeated when setting multiple values and an object would not allow the repeated key. - Insertions / Updates ==================== From b38e29c3d1f258fcb9af5a021a62bf5ea1b2a1c3 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 19 Aug 2019 13:50:46 -0500 Subject: [PATCH 262/549] Add text/plain reference Also move binary output to a top heading. --- api.rst | 95 ++++++++++++++++++++++++++++++++++----------------------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/api.rst b/api.rst index 67a911b5bb..5e7271de50 100644 --- a/api.rst +++ b/api.rst @@ -452,44 +452,6 @@ When a singular response is requested but no entries are found, the server respo 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. -Binary output -------------- - -If you want to return raw binary data from a :code:`bytea` column, you must specify :code:`application/octet-stream` as part of the :code:`Accept` header -and select a single column :code:`?select=bin_data`. - -.. code-block:: http - - GET /items?select=bin_data&id=eq.1 HTTP/1.1 - Accept: application/octet-stream - -You can also request binary output when calling `Stored Procedures`_ and since they can return a scalar value you are not forced to use :code:`select` -for this case. - -.. code-block:: postgres - - CREATE FUNCTION closest_point(..) RETURNS bytea .. - -.. code-block:: http - - POST /rpc/closest_point HTTP/1.1 - Accept: application/octet-stream - -If the stored procedure returns non-scalar values, you need to do a :code:`select` in the same way as for GET binary output. - -.. code-block:: sql - - CREATE FUNCTION overlapping_regions(..) RETURNS SETOF TABLE(geom_twkb bytea, ..) .. - -.. code-block:: http - - POST /rpc/overlapping_regions?select=geom_twkb HTTP/1.1 - Accept: application/octet-stream - -.. note:: - - If more than one row would be returned the binary results will be concatenated with no delimiter. - .. _resource_embedding: Resource Embedding @@ -1145,6 +1107,63 @@ To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instanc Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`. +.. _binary_output: + +Binary Output +============= + +If you want to return raw binary data from a :code:`bytea` column, you must specify :code:`application/octet-stream` as part of the :code:`Accept` header +and select a single column :code:`?select=bin_data`. + +.. code-block:: http + + GET /items?select=bin_data&id=eq.1 HTTP/1.1 + Accept: application/octet-stream + +You can also request binary output when calling `Stored Procedures`_ and since they can return a scalar value you are not forced to use :code:`select` +for this case. + +.. code-block:: postgres + + CREATE FUNCTION closest_point(..) RETURNS bytea .. + +.. code-block:: http + + POST /rpc/closest_point HTTP/1.1 + Accept: application/octet-stream + +If the stored procedure returns non-scalar values, you need to do a :code:`select` in the same way as for GET binary output. + +.. code-block:: sql + + CREATE FUNCTION overlapping_regions(..) RETURNS SETOF TABLE(geom_twkb bytea, ..) .. + +.. code-block:: http + + POST /rpc/overlapping_regions?select=geom_twkb HTTP/1.1 + Accept: application/octet-stream + +.. note:: + + If more than one row would be returned the binary results will be concatenated with no delimiter. + +Plain Text Output +----------------- + +You can get raw output from a ``text`` column by using ``Accept: text/plain``. + +.. code-block:: http + + GET /workers?select=custom_psv_format HTTP/1.1 + Accept: text/plain + + 09310817|JOHN|DOE|15/04/88| + 42152780|FRED|BLOGGS|20/02/85| + 43006541|OTTO|NORMALVERBRAUCHER|01/07/90| + 02452492|ERIKA|MUSTERMANN|11/01/80| + +This follows the same rules as :ref:`binary_output`. + OpenAPI Support =============== From 2d6c16c2a6ef2a0ecd03909a6673e92a71189718 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 19 Aug 2019 15:33:55 -0500 Subject: [PATCH 263/549] Fix #234, reference for raw-media-types --- install.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/install.rst b/install.rst index 0e288d3eee..0c71f25edb 100644 --- a/install.rst +++ b/install.rst @@ -88,6 +88,7 @@ max-rows Int ∞ pre-request String app.settings.* String role-claim-key String .role +raw-media-types String ==================== ====== ========= ======== .. _db-uri: @@ -254,6 +255,22 @@ role-claim-key # non-alphanumerical characters can go inside quotes(escaped in the config value) role-claim-key = ".\"https://www.example.com/role\".key" +.. _raw-media-types: + +raw-media-types +--------------- + + This serves to extend the media types that PostgREST currently accepts through an ``Accept`` header. + + These media types can be requested by following the same rules as the ones defined in :ref:`binary_output`. + + As an example, the below config would allow you to request an **image** and an **xml** by doing a request with ``Accept: image/png`` + and a request with ``Accept: text/xml``, respectively. + + .. code:: bash + + raw-media-types="image/png, text/xml" + Running the Server ================== From 4fe34079d8da42a280e9d23ec87c1b44e6dc5039 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 20 Aug 2019 14:23:05 -0500 Subject: [PATCH 264/549] Reorganize ecosystem section --- admin.rst | 4 +- ecosystem.rst | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++ index.rst | 98 +++++------------------------------------------- 3 files changed, 114 insertions(+), 90 deletions(-) create mode 100644 ecosystem.rst diff --git a/admin.rst b/admin.rst index f7075f9a61..3638516567 100644 --- a/admin.rst +++ b/admin.rst @@ -84,9 +84,9 @@ However including the request header :code:`Prefer: count=exact` calculates and This is fine in small tables, but count performance degrades in big tables due to the MVCC architecture of PostgreSQL. For very large tables it can take a very long time to retrieve the results which allows a denial of service attack. The solution is to strip this header from all requests: -.. code:: +.. code-block:: postgres - Nginx stuff. Remove any prefer header which contains the word count + -- Pending nginx config: Remove any prefer header which contains the word count .. note:: diff --git a/ecosystem.rst b/ecosystem.rst new file mode 100644 index 0000000000..a64c88579e --- /dev/null +++ b/ecosystem.rst @@ -0,0 +1,102 @@ +.. _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. + +* `diogob/postgres-websockets `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY +* `frafra/postgresql2websocket `_ - Websockets +* `matthewmueller/pg-bridge `_ - Amazon SNS +* `aweber/pgsql-listen-exchange `_ - RabbitMQ +* `SpiderOak/skeeter `_ - ZeroMQ +* `FGRibreau/postgresql-to-amqp `_ - AMQP +* `daurnimator/pg-kinesis-bridge `_ - Amazon Kinesis + +.. _eco_example_apps: + +Example Apps +------------ + +* `tatut/postgrest-ui `_ - ClojureScript UI components for PostgREST +* `priyank-purohit/PostGUI `_ - React Material UI admin panel +* `Qu4tro/pgrst-dev-setup `_ - docker-compose and tmuxp setup for experimentation. +* `subzerocloud/postgrest-starter-kit `_ - Boilerplate for new project +* `NikolayS/postgrest-google-translate `_ - Calling to external translation service +* `CodeforAustralia/heritage-near-me `_ - Elm and PostgREST with PostGIS +* `timwis/handsontable-postgrest `_ - An excel-like database table editor +* `Recmo/PostgrestSkeleton `_ - Docker Compose, PostgREST, Nginx and Auth0 +* `benoror/ember-postgrest-dynamic-ui `_ - generating Ember forms to edit data +* `ruslantalpa/blogdemo `_ - blog api demo in a vagrant image +* `timwis/ext-postgrest-crud `_ - browser-based spreadsheet +* `srid/chronicle `_ - tracking a tree of personal memories +* `diogob/elm-workshop `_ - building a simple database query UI +* `myfreeweb/moneylog `_ - accounting web app in Polymer + PostgREST +* `tyrchen/goodfilm `_ - example film api +* `begriffs/postgrest-example `_ - sqitch versioning for API +* `SMRxT/postgrest-demo `_ - multi-tenant logging system +* `PierreRochard/postgrest-boilerplate `_ - example auth back-end +* `marmelab/ng-admin-postgrest `_ - automatic database admin panel + +.. _eco_extensions: + +Extensions +---------- + +* `pg-safeupdate `_ - Prevent full-table updates or deletes +* `srid/spas `_ - allow file uploads and basic auth +* `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server +* `wildsurfer/postgrest-oauth-server `_ - OAuth2 server +* `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware +* `criles25/postgrest-auth `_ - email based auth/signup +* `ppKrauss/PostgREST-writeAPI `_ - generate Nginx rewrite rules to fit an OpenAPI spec + +.. _clientside_libraries: + +Client-Side Libraries +--------------------- + +* `Kong/py-postgrest `_ - Python +* `datrium/postgrest-pyclient `_ - Python +* `tomberek/aor-postgrest-client `_ - JS, admin-on-rest +* `hugomrdias/postgrest-url `_ - JS, just for generating query URLs +* `john-kelly/elm-postgrest `_ - Elm +* `mithril.postgrest `_ - JS, Mithril +* `lewisjared/postgrest-request `_ - JS, SuperAgent +* `JarvusInnovations/jarvus-postgrest-apikit `_ - JS, Sencha framework +* `davidthewatson/postgrest_python_requests_client `_ - Python +* `calebmer/postgrest-client `_ - JS +* `clesiemo3/postgrestR `_ - R +* `PierreRochard/postgrest-angular `_ - TypeScript, generate UI from API description +* `thejettdurham/postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp +* `team142/ng-postgrest `_ - Angular app for browsing, editing data exposed over Postgrest. + +.. _eco_commercial: + +Commercial +--------------- + +* `subZero `_ - Automated GraphQL & REST API with built-in caching (powered in part by PostgREST) + +.. _eco_production: + +In Production +------------- + +* `Moat `_ +* `Catarse `_ +* `Redsmin `_ +* `Image-charts `_ +* `MotionDynamic - Fast highly dynamic video generation at scale `_ +* `Drip Depot `_ +* `Convene `_ by Thomson-Reuters +* `eGull `_ +* `Elyios `_ +* `Simply Connected Systems `_ +* `Nimbus `_ + + - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. +* `Datrium `_ + +.. * `OpenBooking `_ +.. * `triggerFS - A realtime messaging and distributed trigger system `_ diff --git a/index.rst b/index.rst index c126cddaf1..f60cccf674 100644 --- a/index.rst +++ b/index.rst @@ -141,96 +141,18 @@ Ecosystem PostgREST has a growing ecosystem of examples, and libraries, experiments, and users. Here is a selection. -Example Apps ------------- - -* `subzerocloud/postgrest-starter-kit `_ - Boilerplate for new project -* `NikolayS/postgrest-google-translate `_ - Calling to external translation service -* `CodeforAustralia/heritage-near-me `_ - Elm and PostgREST with PostGIS -* `timwis/handsontable-postgrest `_ - An excel-like database table editor -* `Recmo/PostgrestSkeleton `_ - Docker Compose, PostgREST, Nginx and Auth0 -* `benoror/ember-postgrest-dynamic-ui `_ - generating Ember forms to edit data -* `ruslantalpa/blogdemo `_ - blog api demo in a vagrant image -* `timwis/ext-postgrest-crud `_ - browser-based spreadsheet -* `srid/chronicle `_ - tracking a tree of personal memories -* `diogob/elm-workshop `_ - building a simple database query UI -* `marmelab/ng-admin-postgrest `_ - automatic database admin panel -* `myfreeweb/moneylog `_ - accounting web app in Polymer + PostgREST -* `tyrchen/goodfilm `_ - example film api -* `begriffs/postgrest-example `_ - sqitch versioning for API -* `SMRxT/postgrest-demo `_ - multi-tenant logging system -* `PierreRochard/postgrest-boilerplate `_ - example auth back-end - - -.. _clientside_libraries: - -Client-Side Libraries ---------------------- - -* `tomberek/aor-postgrest-client `_ - JS, admin-on-rest -* `hugomrdias/postgrest-url `_ - JS, just for generating query URLs -* `john-kelly/elm-postgrest `_ - Elm -* `mithril.postgrest `_ - JS, Mithril -* `lewisjared/postgrest-request `_ - JS, SuperAgent -* `JarvusInnovations/jarvus-postgrest-apikit `_ - JS, Sencha framework -* `davidthewatson/postgrest_python_requests_client `_ - Python -* `datrium/postgrest-pyclient `_ - Python -* `calebmer/postgrest-client `_ - JS -* `clesiemo3/postgrestR `_ - R -* `PierreRochard/postgrest-angular `_ - TypeScript, generate UI from API description -* `thejettdurham/postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp -* `team142/ng-postgrest `_ - Angular app for browsing, editing data exposed over Postgrest. - -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. - -* `diogob/postgres-websockets `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY -* `frafra/postgresql2websocket `_ - Websockets -* `matthewmueller/pg-bridge `_ - Amazon SNS -* `aweber/pgsql-listen-exchange `_ - RabbitMQ -* `SpiderOak/skeeter `_ - ZeroMQ -* `FGRibreau/postgresql-to-amqp `_ - AMQP -* `daurnimator/pg-kinesis-bridge `_ - Amazon Kinesis - -Extensions ----------- - -* `pg-safeupdate `_ - Prevent full-table updates or deletes -* `srid/spas `_ - allow file uploads and basic auth -* `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server -* `wildsurfer/postgrest-oauth-server `_ - OAuth2 server -* `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware -* `criles25/postgrest-auth `_ - email based auth/signup -* `ppKrauss/PostgREST-writeAPI `_ - generate Nginx rewrite rules to fit an OpenAPI spec - -Commercial ---------------- - -* `subZero `_ - Automated GraphQL & REST API with built-in caching (powered in part by PostgREST) - -In Production -------------- - -* `Moat `_ -* `Catarse `_ -* `Redsmin `_ -* `Image-charts `_ -* `MotionDynamic - Fast highly dynamic video generation at scale `_ -* `Drip Depot `_ -* `OpenBooking `_ -* `Convene `_ by Thomson-Reuters -* `eGull `_ -* `Elyios `_ -* `Simply Connected Systems `_ -* `Nimbus `_ - - - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. +.. toctree:: + :caption: Ecosystem + :hidden: -* `triggerFS - A realtime messaging and distributed trigger system `_ -* `Datrium `_ + ecosystem.rst +* :ref:`eco_external_notification` +* :ref:`eco_example_apps` +* :ref:`eco_extensions` +* :ref:`clientside_libraries` +* :ref:`eco_commercial` +* :ref:`eco_production` Testimonials ------------ From 8271885bb2385612e993edc94997955911c5a05e Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 20 Aug 2019 14:42:22 -0500 Subject: [PATCH 265/549] Remove some elements of the index page As mentioned in https://github.com/PostgREST/postgrest-docs/issues/249, the embracing the relational model sectin would better fit in a page about our REST style. The shared improvements section no longer seems relevant since we're already an established open source project. Release notes are not removed but hidden from the index page. --- index.rst | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/index.rst b/index.rst index f60cccf674..021c1bb5cd 100644 --- a/index.rst +++ b/index.rst @@ -54,21 +54,11 @@ 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. -Embracing the Relational Model ------------------------------- - -In 1970 E. F. Codd criticized the then-dominant hierarchical model of databases in his article A Relational Model of Data for Large Shared Data Banks. Reading the article reveals a striking similarity between hierarchical databases and nested http routes. With PostgREST we attempt to use flexible filtering and embedding rather than nested routes. - 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. -Shared Improvements -------------------- - -As with any open source project, we all gain from features and fixes in the tool. It's more beneficial than improvements locked inextricably within custom code-bases. - Getting Support ---------------- @@ -95,6 +85,7 @@ Translations .. toctree:: :caption: Release Notes :titlesonly: + :hidden: release_notes.rst @@ -107,6 +98,7 @@ Translations .. toctree:: :caption: How-to guides + :name: how-tos :titlesonly: how-tos/embedding-table-from-another-schema.rst From 7805b99d573f7ae8532f0a28fcdfb3769d73a595 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 22 Aug 2019 15:06:37 -0500 Subject: [PATCH 266/549] Fix #213, reorganize index --- configuration.rst | 210 +++++++++++++++++++++++++++++++++++++++++++++ index.rst | 88 ++++++++++++------- install.rst | 209 +------------------------------------------- livereload_docs.py | 2 + 4 files changed, 273 insertions(+), 236 deletions(-) create mode 100644 configuration.rst diff --git a/configuration.rst b/configuration.rst new file mode 100644 index 0000000000..1bc197385a --- /dev/null +++ b/configuration.rst @@ -0,0 +1,210 @@ +.. _configuration: + +Configuration +============= + +Here is the full list of configuration parameters. + +==================== ====== ========= ======== +Name Type Default Required +==================== ====== ========= ======== +db-uri String Y +db-schema String Y +db-anon-role String Y +db-pool Int 10 +db-pool-timeout Int 10 +db-extra-search-path String public +server-host String 127.0.0.1 +server-port Int 3000 +server-unix-socket String +server-proxy-uri String +jwt-secret String +jwt-aud String +secret-is-base64 Bool False +max-rows Int ∞ +pre-request String +app.settings.* String +role-claim-key String .role +raw-media-types String +==================== ====== ========= ======== + +.. _db-uri: + +db-uri +------ + + The standard connection PostgreSQL `URI 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 `_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``. + + When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ and the `Peer Authentication method `_ 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 `_ documentation for more details. + + On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. + + 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. + +.. _db-schema: + +db-schema +--------- + + The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. + + This schema gets added to the `search_path `_ of every request. + +.. _db-anon-role: + +db-anon-role +------------ + + The database role to use when executing commands on behalf of unauthenticated clients. + +.. _db-pool: + +db-pool +------- + + Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the :code:`max_connections` GUC in your database. + +db-pool-timeout +--------------- + + Time to live for an idle database pool connection. + +.. _db-extra-search-path: + +db-extra-search-path +-------------------- + + Extra schemas to add to the `search_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-schema`. + + This parameter was meant to make it easier to use **PostgreSQL extensions** (like PostGIS) that are outside of the :ref:`db-schema`. + + Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``. + +.. _server-host: + +server-host +----------- + + 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 +----------- + + The TCP port to bind the web server. + +server-unix-socket +------------------ + + `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-proxy-uri: + +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" + ] + } + +.. _jwt-secret: + +jwt-secret +---------- + + The secret or `JSON Web Key (JWK) (or set) `_ 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`. + +.. _jwt-aud: + +jwt-aud +------- + + Specifies the `JWT audience claim `_. 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. + +.. _secret-is-base64: + +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. + +.. _max-rows: + +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. + +.. _pre-request: + +pre-request +----------- + + A schema-qualified stored procedure name to call right after switching roles for a client request. This provides an opportunity to modify SQL variables or raise an exception to prevent the request from completing. + +.. _app.settings.*: + +app.settings.* +-------------- + + 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')`. + +.. _role-claim-key: + +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 + 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) + role-claim-key = ".\"https://www.example.com/role\".key" + +.. _raw-media-types: + +raw-media-types +--------------- + + This serves to extend the media types that PostgREST currently accepts through an ``Accept`` header. + + These media types can be requested by following the same rules as the ones defined in :ref:`binary_output`. + + As an example, the below config would allow you to request an **image** and an **xml** by doing a request with ``Accept: image/png`` + and a request with ``Accept: text/xml``, respectively. + + .. code:: bash + + raw-media-types="image/png, text/xml" + diff --git a/index.rst b/index.rst index 021c1bb5cd..3d43f32b33 100644 --- a/index.rst +++ b/index.rst @@ -64,24 +64,6 @@ Getting Support The project has a friendly and growing community. Join our `chat room `_ for discussion and help. You can also report or search for bugs/features on the Github `issues `_ page. -.. _supporting-dev: - -Supporting development ----------------------- - -You can help PostgREST ongoing maintenance and development by: - -- Making a regular donation through `Patreon `_ - -- Alternatively, you can make a one-time donation via `Paypal `_ - -Every donation will be spent on making PostgREST better for the whole community. - -Translations -~~~~~~~~~~~~ - -* `Chinese `_ (latest version ``v0.4.2.0``) - .. toctree:: :caption: Release Notes :titlesonly: @@ -89,26 +71,40 @@ Translations release_notes.rst +Tutorials +--------- + +Start here if you're new to PostgREST. + .. toctree:: + :glob: :caption: Tutorials - :titlesonly: + :hidden: + + tutorials/* + +- :doc:`tutorials/tut0` +- :doc:`tutorials/tut1` - tutorials/tut0.rst - tutorials/tut1.rst +How-to guides +------------- + +Goal-oriented guides that show how to solve a specific problem. .. toctree:: + :glob: :caption: How-to guides - :name: how-tos - :titlesonly: + :hidden: - how-tos/embedding-table-from-another-schema.rst - how-tos/casting-type-to-custom-json.rst + how-tos/* -.. toctree:: - :caption: Installation - :titlesonly: +- :doc:`how-tos/embedding-table-from-another-schema` +- :doc:`how-tos/casting-type-to-custom-json` - install.rst +Reference guides +---------------- + +Technical references for PostgREST's API and Configuration. .. toctree:: :caption: API @@ -116,12 +112,28 @@ Translations api.rst +.. toctree:: + :caption: Configuration + + configuration.rst + +Topic guides +------------ + +Explanations of some key concepts in PostgREST. + .. toctree:: :caption: Authentication :titlesonly: auth.rst +.. toctree:: + :caption: Installation + :titlesonly: + + install.rst + .. toctree:: :caption: Administration :titlesonly: @@ -146,6 +158,24 @@ PostgREST has a growing ecosystem of examples, and libraries, experiments, and u * :ref:`eco_commercial` * :ref:`eco_production` +.. _supporting-dev: + +Supporting development +---------------------- + +You can help PostgREST ongoing maintenance and development by: + +- Making a regular donation through `Patreon `_ + +- Alternatively, you can make a one-time donation via `Paypal `_ + +Every donation will be spent on making PostgREST better for the whole community. + +Translations +------------ + +* `Chinese `_ (latest version ``v0.4.2.0``) + Testimonials ------------ diff --git a/install.rst b/install.rst index 0c71f25edb..5c1a0777ab 100644 --- a/install.rst +++ b/install.rst @@ -36,8 +36,6 @@ To use PostgREST you will need an underlying database (PostgreSQL version 9.5 or On Windows, PostgREST will fail to run unless the PostgreSQL binaries are on the system path. To test whether this is the case, run ``pg_config`` from the command line. You should see it output a list of paths. -.. _configuration: - Configuration ============= @@ -47,7 +45,7 @@ The PostgREST server reads a configuration file to determine information about t ./postgrest /path/to/postgrest.conf -The file must contain a set of key value pairs. At minimum you must include these keys: +The configuration file must contain a set of key value pairs. At minimum you must include these keys: .. code:: @@ -66,210 +64,7 @@ The file must contain a set of key value pairs. At minimum you must include thes The user specified in the db-uri is also known as the authenticator role. For more information about the anonymous vs authenticator roles see the :ref:`roles`. -Here is the full list of configuration parameters. - -==================== ====== ========= ======== -Name Type Default Required -==================== ====== ========= ======== -db-uri String Y -db-schema String Y -db-anon-role String Y -db-pool Int 10 -db-pool-timeout Int 10 -db-extra-search-path String public -server-host String 127.0.0.1 -server-port Int 3000 -server-unix-socket String -server-proxy-uri String -jwt-secret String -jwt-aud String -secret-is-base64 Bool False -max-rows Int ∞ -pre-request String -app.settings.* String -role-claim-key String .role -raw-media-types String -==================== ====== ========= ======== - -.. _db-uri: - -db-uri ------- - - The standard connection PostgreSQL `URI 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 `_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``. - - When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ and the `Peer Authentication method `_ 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 `_ documentation for more details. - - On older systems like Centos 6, with older versions of libpq, a different db-uri syntax has to be used. In this case the URI is a string of space separated key-value pairs (key=value), so the example above would be :code:`"host=host user=user port=5432 dbname=dbname password=pass"`. - - 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. - -.. _db-schema: - -db-schema ---------- - - The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. - - This schema gets added to the `search_path `_ of every request. - -.. _db-anon-role: - -db-anon-role ------------- - - The database role to use when executing commands on behalf of unauthenticated clients. - -.. _db-pool: - -db-pool -------- - - Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the :code:`max_connections` GUC in your database. - -db-pool-timeout ---------------- - - Time to live for an idle database pool connection. - -.. _db-extra-search-path: - -db-extra-search-path --------------------- - - Extra schemas to add to the `search_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-schema`. - - This parameter was meant to make it easier to use **PostgreSQL extensions** (like PostGIS) that are outside of the :ref:`db-schema`. - - Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``. - -.. _server-host: - -server-host ------------ - - 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 ------------ - - The TCP port to bind the web server. - -server-unix-socket ------------------- - - `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-proxy-uri: - -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" - ] - } - -.. _jwt-secret: - -jwt-secret ----------- - - The secret or `JSON Web Key (JWK) (or set) `_ 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`. - -.. _jwt-aud: - -jwt-aud -------- - - Specifies the `JWT audience claim `_. 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. - -.. _secret-is-base64: - -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. - -.. _max-rows: - -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. - -.. _pre-request: - -pre-request ------------ - - A schema-qualified stored procedure name to call right after switching roles for a client request. This provides an opportunity to modify SQL variables or raise an exception to prevent the request from completing. - -.. _app.settings.*: - -app.settings.* --------------- - - 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')`. - -.. _role-claim-key: - -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 - 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) - role-claim-key = ".\"https://www.example.com/role\".key" - -.. _raw-media-types: - -raw-media-types ---------------- - - This serves to extend the media types that PostgREST currently accepts through an ``Accept`` header. - - These media types can be requested by following the same rules as the ones defined in :ref:`binary_output`. - - As an example, the below config would allow you to request an **image** and an **xml** by doing a request with ``Accept: image/png`` - and a request with ``Accept: text/xml``, respectively. - - .. code:: bash - - raw-media-types="image/png, text/xml" +For a complete reference of the configuration parameters, see :ref:`configuration`. Running the Server ================== diff --git a/livereload_docs.py b/livereload_docs.py index d4d0ba6e8e..befcaa35ef 100755 --- a/livereload_docs.py +++ b/livereload_docs.py @@ -7,4 +7,6 @@ server.watch('*.rst', shell('sphinx-build -b html -a -n . _build')) server.watch('tutorials/*.rst', shell('sphinx-build -b html -a -n . _build')) server.watch('how-tos/*.rst', shell('sphinx-build -b html -a -n . _build')) +# For custom port and host +# server.serve(port=8080, host='192.168.1.2') server.serve(root='_build/') From 5942db7fa75a367551aa423e5d9bcb8d169670d6 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 23 Aug 2019 13:30:26 -0500 Subject: [PATCH 267/549] Collapse index sections and improve the wording --- ecosystem.rst | 54 +++++++++++------------------------- index.rst | 70 +++++++++++++++++++++++++++++------------------ release_notes.rst | 32 ---------------------- 3 files changed, 60 insertions(+), 96 deletions(-) diff --git a/ecosystem.rst b/ecosystem.rst index a64c88579e..1ffc96e370 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -1,18 +1,3 @@ -.. _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. - -* `diogob/postgres-websockets `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY -* `frafra/postgresql2websocket `_ - Websockets -* `matthewmueller/pg-bridge `_ - Amazon SNS -* `aweber/pgsql-listen-exchange `_ - RabbitMQ -* `SpiderOak/skeeter `_ - ZeroMQ -* `FGRibreau/postgresql-to-amqp `_ - AMQP -* `daurnimator/pg-kinesis-bridge `_ - Amazon Kinesis - .. _eco_example_apps: Example Apps @@ -38,6 +23,22 @@ Example Apps * `PierreRochard/postgrest-boilerplate `_ - example auth back-end * `marmelab/ng-admin-postgrest `_ - automatic database admin panel +.. _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. + +* `diogob/postgres-websockets `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY +* `frafra/postgresql2websocket `_ - Websockets +* `matthewmueller/pg-bridge `_ - Amazon SNS +* `aweber/pgsql-listen-exchange `_ - RabbitMQ +* `SpiderOak/skeeter `_ - ZeroMQ +* `FGRibreau/postgresql-to-amqp `_ - AMQP +* `daurnimator/pg-kinesis-bridge `_ - Amazon Kinesis + + .. _eco_extensions: Extensions @@ -77,26 +78,3 @@ Commercial --------------- * `subZero `_ - Automated GraphQL & REST API with built-in caching (powered in part by PostgREST) - -.. _eco_production: - -In Production -------------- - -* `Moat `_ -* `Catarse `_ -* `Redsmin `_ -* `Image-charts `_ -* `MotionDynamic - Fast highly dynamic video generation at scale `_ -* `Drip Depot `_ -* `Convene `_ by Thomson-Reuters -* `eGull `_ -* `Elyios `_ -* `Simply Connected Systems `_ -* `Nimbus `_ - - - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. -* `Datrium `_ - -.. * `OpenBooking `_ -.. * `triggerFS - A realtime messaging and distributed trigger system `_ diff --git a/index.rst b/index.rst index 3d43f32b33..3b49b816f3 100644 --- a/index.rst +++ b/index.rst @@ -74,7 +74,7 @@ The project has a friendly and growing community. Join our `chat room `. + How-to guides ------------- -Goal-oriented guides that show how to solve a specific problem. +These are recipes that'll help you address specific use-cases. .. toctree:: :glob: @@ -104,19 +106,23 @@ Goal-oriented guides that show how to solve a specific problem. Reference guides ---------------- -Technical references for PostgREST's API and Configuration. +Technical references for PostgREST's functionality. .. toctree:: :caption: API - :titlesonly: + :hidden: api.rst .. toctree:: :caption: Configuration + :hidden: configuration.rst +- :doc:`API ` +- :doc:`configuration` + Topic guides ------------ @@ -124,26 +130,30 @@ Explanations of some key concepts in PostgREST. .. toctree:: :caption: Authentication - :titlesonly: + :hidden: auth.rst .. toctree:: :caption: Installation - :titlesonly: + :hidden: install.rst .. toctree:: :caption: Administration - :titlesonly: + :hidden: admin.rst +- :doc:`Authentication ` +- :doc:`Installation ` +- :doc:`Administration ` + Ecosystem --------- -PostgREST has a growing ecosystem of examples, and libraries, experiments, and users. Here is a selection. +PostgREST has a growing ecosystem of examples, libraries, and experiments. Here is a selection. .. toctree:: :caption: Ecosystem @@ -151,30 +161,33 @@ PostgREST has a growing ecosystem of examples, and libraries, experiments, and u ecosystem.rst -* :ref:`eco_external_notification` * :ref:`eco_example_apps` +* :ref:`eco_external_notification` * :ref:`eco_extensions` * :ref:`clientside_libraries` * :ref:`eco_commercial` -* :ref:`eco_production` - -.. _supporting-dev: -Supporting development ----------------------- - -You can help PostgREST ongoing maintenance and development by: - -- Making a regular donation through `Patreon `_ - -- Alternatively, you can make a one-time donation via `Paypal `_ - -Every donation will be spent on making PostgREST better for the whole community. - -Translations ------------- +In Production +------------- -* `Chinese `_ (latest version ``v0.4.2.0``) +Here are some companies that use PostgREST in production. + +* `Datrium `_ +* `Nimbus `_ + - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. +* `Catarse `_ +* `Moat `_ +* `Redsmin `_ +* `Image-charts `_ +* `MotionDynamic - Fast highly dynamic video generation at scale `_ +* `Drip Depot `_ +* `Convene `_ by Thomson-Reuters +* `eGull `_ +* `Elyios `_ +* `Simply Connected Systems `_ + +.. * `OpenBooking `_ +.. * `triggerFS - A realtime messaging and distributed trigger system `_ Testimonials ------------ @@ -215,3 +228,8 @@ Testimonials Couldn't be happier." -- Anupam Garg, Datrium, Inc. + +Translations +------------ + +* `Chinese `_ (latest version ``v0.4.2.0``) diff --git a/release_notes.rst b/release_notes.rst index c4aa0b7c29..e69de29bb2 100644 --- a/release_notes.rst +++ b/release_notes.rst @@ -1,32 +0,0 @@ -Release Notes -============= - -Here we'll include the most relevant changes so you can migrate to newer versions easily. -You can see the full changelog of each release in the `PostgREST repository `_. - -v5.2.0 -====== - -* `Explicit qualification `_ introduced in ``v5.0`` is no longer necessary, this section will not be included from this version onwards. A :ref:`db-extra-search-path` configuration parameter was introduced to avoid the need to explictly qualify database objects. If you install PostgreSQL extensions on the ``public`` schema, they'll work normally from now on. - -* Now you can filter :ref:`tabs-cols-w-spaces`. - -* Included the ability to quote columns that have :ref:`reserved-chars`. - -* Thanks to `Zhou Feng `_, now is possible to reference an external file in :ref:`db-uri`. - -* Thanks to `Russell Davies `_, Json Web Key Sets are now accepted by :ref:`jwt-secret`. - -Thanks ------- - -This release was made possible thanks to: - -* `Daniel Babiak `_ -* `Michel Pelletier `_ -* Tsingson Qin -* Jay Hannah -* Victor Adossi -* Petr Beles - -If you like to join them please consider :ref:`supporting PostgREST development `. From b9c9aee645c055c8fbbe6aeaa3ad1cb21cbbe1f6 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 23 Aug 2019 14:23:58 -0500 Subject: [PATCH 268/549] Reorder configuration section --- configuration.rst | 34 ++++++++++++++++++++++++++++++---- install.rst | 23 ++--------------------- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/configuration.rst b/configuration.rst index 1bc197385a..f44019b1e6 100644 --- a/configuration.rst +++ b/configuration.rst @@ -3,6 +3,31 @@ Configuration ============= +PostgREST reads a configuration file to determine information about the database and how to serve client requests. There is no predefined location for this 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. At minimum you must include these keys: + +.. code:: + + # postgrest.conf + + # The standard connection URI format, documented at + # https://www.postgresql.org/docs/current/static/libpq-connect.html#AEN45347 + db-uri = "postgres://user:pass@host:5432/dbname" + + # The name of which database schema to expose to REST clients + db-schema = "api" + + # The database role to use when no client authentication is provided. + # Can (and should) differ from user in db-uri + db-anon-role = "anon" + +The user specified in the db-uri is also known as the authenticator role. For more information about the anonymous vs authenticator roles see the :ref:`roles`. + Here is the full list of configuration parameters. ==================== ====== ========= ======== @@ -14,7 +39,7 @@ db-anon-role String Y db-pool Int 10 db-pool-timeout Int 10 db-extra-search-path String public -server-host String 127.0.0.1 +server-host String !4 server-port Int 3000 server-unix-socket String server-proxy-uri String @@ -41,6 +66,7 @@ db-uri 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. + .. _db-schema: db-schema @@ -55,7 +81,7 @@ db-schema db-anon-role ------------ - The database role to use when executing commands on behalf of unauthenticated clients. + The database role to use when executing commands on behalf of unauthenticated clients. For more information, see :ref:`roles`. .. _db-pool: @@ -197,11 +223,11 @@ role-claim-key raw-media-types --------------- - This serves to extend the media types that PostgREST currently accepts through an ``Accept`` header. + This serves to extend the `Media Types `_ that PostgREST currently accepts through an ``Accept`` header. These media types can be requested by following the same rules as the ones defined in :ref:`binary_output`. - As an example, the below config would allow you to request an **image** and an **xml** by doing a request with ``Accept: image/png`` + As an example, the below config would allow you to request an **image** and an **xml** by doing a request with ``Accept: image/png`` and a request with ``Accept: text/xml``, respectively. .. code:: bash diff --git a/install.rst b/install.rst index 5c1a0777ab..fc3f278cd9 100644 --- a/install.rst +++ b/install.rst @@ -39,32 +39,13 @@ On Windows, PostgREST will fail to run unless the PostgreSQL binaries are on the Configuration ============= -The PostgREST server reads a configuration file to determine information about the database and how to serve client requests. There is no predefined location for this file, you must specify the file path as the one and only argument to the server: +The PostgREST server reads a configuration file as its only argument: .. code:: bash ./postgrest /path/to/postgrest.conf -The configuration file must contain a set of key value pairs. At minimum you must include these keys: - -.. code:: - - # postgrest.conf - - # The standard connection URI format, documented at - # https://www.postgresql.org/docs/current/static/libpq-connect.html#AEN45347 - db-uri = "postgres://user:pass@host:5432/dbname" - - # The name of which database schema to expose to REST clients - db-schema = "api" - - # The database role to use when no client authentication is provided. - # Can (and probably should) differ from user in db-uri - db-anon-role = "anon" - -The user specified in the db-uri is also known as the authenticator role. For more information about the anonymous vs authenticator roles see the :ref:`roles`. - -For a complete reference of the configuration parameters, see :ref:`configuration`. +For a complete reference of the configuration file, see :ref:`configuration`. Running the Server ================== From 0d4b1d8f925d71fdf727374d9065ea3413a12cc1 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 26 Aug 2019 07:44:00 -0500 Subject: [PATCH 269/549] Add release notes for v6.0.2 Also add relase notes section at index. --- _static/css/custom.css | 20 +++++++++++ api.rst | 6 ++++ configuration.rst | 7 +++- index.rst | 45 +++++++++++++++--------- livereload_docs.py | 5 +-- release_notes.rst | 0 releases/v5.2.0.rst | 26 ++++++++++++++ releases/v6.0.2.rst | 79 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 169 insertions(+), 19 deletions(-) delete mode 100644 release_notes.rst create mode 100644 releases/v5.2.0.rst create mode 100644 releases/v6.0.2.rst diff --git a/_static/css/custom.css b/_static/css/custom.css index 555afaa1c8..50359ce055 100644 --- a/_static/css/custom.css +++ b/_static/css/custom.css @@ -21,3 +21,23 @@ div.line-block { #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; +} diff --git a/api.rst b/api.rst index 5e7271de50..97f3cf8b70 100644 --- a/api.rst +++ b/api.rst @@ -45,6 +45,8 @@ Complex logic can also be applied: GET /people?and=(grade.gte.90,student.is.true,or(age.gte.14,age.is.null)) HTTP/1.1 +.. _operators: + Operators ~~~~~~~~~ @@ -773,6 +775,8 @@ PostgREST will detect if the function is scalar or table-valued and will shape t { "title": "Blade Runner 2049", "rating": 8.1} ] +.. _bulk_call: + Bulk Call --------- @@ -1147,6 +1151,8 @@ If the stored procedure returns non-scalar values, you need to do a :code:`selec If more than one row would be returned the binary results will be concatenated with no delimiter. +.. _plain_text_output: + Plain Text Output ----------------- diff --git a/configuration.rst b/configuration.rst index f44019b1e6..d44096de43 100644 --- a/configuration.rst +++ b/configuration.rst @@ -90,10 +90,13 @@ db-pool Number of connections to keep open in PostgREST's database pool. Having enough here for the maximum expected simultaneous client connections can improve performance. Note it's pointless to set this higher than the :code:`max_connections` GUC in your database. +.. _db-pool-timeout: + db-pool-timeout --------------- - Time to live for an idle database pool connection. + Time to live for an idle database pool connection. If the timeout is reached the connection will be closed. + Once a new request arrives a new connection will be started. .. _db-extra-search-path: @@ -126,6 +129,8 @@ server-port The TCP port to bind the web server. +.. _server-unix-socket: + server-unix-socket ------------------ diff --git a/index.rst b/index.rst index 3b49b816f3..41b8be7aa3 100644 --- a/index.rst +++ b/index.rst @@ -65,11 +65,13 @@ Getting Support The project has a friendly and growing community. Join our `chat room `_ for discussion and help. You can also report or search for bugs/features on the Github `issues `_ page. .. toctree:: + :glob: + :reversed: :caption: Release Notes :titlesonly: :hidden: - release_notes.rst + releases/* Tutorials --------- @@ -88,21 +90,6 @@ Are you new to PostgREST? This is the place to start! Also have a look at :doc:`Installation `. -How-to guides -------------- - -These are recipes that'll help you address specific use-cases. - -.. toctree:: - :glob: - :caption: How-to guides - :hidden: - - how-tos/* - -- :doc:`how-tos/embedding-table-from-another-schema` -- :doc:`how-tos/casting-type-to-custom-json` - Reference guides ---------------- @@ -123,6 +110,23 @@ Technical references for PostgREST's functionality. - :doc:`API ` - :doc:`configuration` +.. _how_tos: + +How-to guides +------------- + +These are recipes that'll help you address specific use-cases. + +.. toctree:: + :glob: + :caption: How-to guides + :hidden: + + how-tos/* + +- :doc:`how-tos/embedding-table-from-another-schema` +- :doc:`how-tos/casting-type-to-custom-json` + Topic guides ------------ @@ -167,6 +171,15 @@ PostgREST has a growing ecosystem of examples, libraries, and experiments. Here * :ref:`clientside_libraries` * :ref:`eco_commercial` +Release Notes +------------- + +Here we'll include the most relevant changes so you can migrate to newer versions easily. +You can see the full changelog of each release in the `PostgREST repository `_. + +- :doc:`releases/v6.0.2` +- :doc:`releases/v5.2.0` + In Production ------------- diff --git a/livereload_docs.py b/livereload_docs.py index befcaa35ef..03910bd6a9 100755 --- a/livereload_docs.py +++ b/livereload_docs.py @@ -7,6 +7,7 @@ server.watch('*.rst', shell('sphinx-build -b html -a -n . _build')) server.watch('tutorials/*.rst', shell('sphinx-build -b html -a -n . _build')) server.watch('how-tos/*.rst', shell('sphinx-build -b html -a -n . _build')) +server.watch('releases/*.rst', shell('sphinx-build -b html -a -n . _build')) # For custom port and host -# server.serve(port=8080, host='192.168.1.2') -server.serve(root='_build/') +server.serve(root='_build/', host='192.168.1.2') +# server.serve(root='_build/') diff --git a/release_notes.rst b/release_notes.rst deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/releases/v5.2.0.rst b/releases/v5.2.0.rst new file mode 100644 index 0000000000..43bc188339 --- /dev/null +++ b/releases/v5.2.0.rst @@ -0,0 +1,26 @@ +v5.2.0 +====== + +* `Explicit qualification `_ introduced in ``v5.0`` is no longer necessary, this section will not be included from this version onwards. A :ref:`db-extra-search-path` configuration parameter was introduced to avoid the need to explictly qualify database objects. If you install PostgreSQL extensions on the ``public`` schema, they'll work normally from now on. + +* Now you can filter :ref:`tabs-cols-w-spaces`. + +* Included the ability to quote columns that have :ref:`reserved-chars`. + +* Thanks to `Zhou Feng `_, now is possible to reference an external file in :ref:`db-uri`. + +* Thanks to `Russell Davies `_, Json Web Key Sets are now accepted by :ref:`jwt-secret`. + +Thanks +------ + +This release was made possible thanks to: + +* `Daniel Babiak `_ +* `Michel Pelletier `_ +* Tsingson Qin +* Jay Hannah +* Victor Adossi +* Petr Beles + +If you like to join them please consider `supporting PostgREST development `_. diff --git a/releases/v6.0.2.rst b/releases/v6.0.2.rst new file mode 100644 index 0000000000..774ac61bb0 --- /dev/null +++ b/releases/v6.0.2.rst @@ -0,0 +1,79 @@ +.. |br| raw:: html + +
    + +v6.0.2 +====== + +Full changelog is available at `PostgREST releases page `_. + +Added +----- + +* Ignoring payload keys for insert/update can be now done with the ``?columns`` query parameter. See :ref:`specify_columns`. + |br| -- `@steve-chavez `_ + +* `websearch_to_tsquery `_ can now be used + through the ``wfts`` operator. See :ref:`fts`. + |br| -- `@herulume `_ + +* Resource Embedding on materialized views is now possible. See :ref:`embedding_views`. + |br| -- `@vitorbaptista `_ + +* Bulk calling an RPC is now allowed. See :ref:`bulk_call`. + |br| -- `@steve-chavez `_ + +* It's now possible to request a ``text/plain`` output. See :ref:`plain_text_output`. + |br| -- `@steve-chavez `_ + +* Config option for specifying PostgREST database pool timeout. See :ref:`db-pool-timeout`. + |br| -- `@Qu4tro `_ + +* Config option for binding the PostgREST web server to an unix socket. See :ref:`server-unix-socket`. + |br| -- `@Dansvidania `_ + +* Config option for extending the supported media types. See :ref:`raw-media-types`. + |br| -- `@Dansvidania `_ + +* We now offer an statically linked binary for Linux. Look for **postgrest--linux-x64-static.tar.xz** on the + `releases page `_. + |br| -- `@clojurians-org `_ + +* A :ref:`how_tos` section was added to the documentation. + +Changed +------- + +* ``SIGHUP`` support was removed. You should use ``SIGUSR1`` instead. See :ref:`schema_reloading`. + +* server-host default of ``127.0.0.1`` was changed to ``!4``. See :ref:`server-host`. + +Thanks +------ + +This release was sponsored by: + +.. image:: ../_static/cybertec.png + :target: https://www.cybertec-postgresql.com/en/ + :width: 13em + +.. image:: ../_static/2ndquadrant.png + :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo + :width: 13em + +.. image:: ../_static/retool.png + :target: https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + +* Daniel Babiak +* Evans Fernandes +* Tsingson Qin +* Michel Pelletier +* Jay Hannah +* Robert Stolarz +* Kofi Gumbs +* Nicholas DiBiase +* Christopher Reid +* Nathan Bouscal + +If you like to join them please consider `supporting PostgREST development `_. From 7dd37c0bc4a00c942190a9c5ddc29bc29567d6d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20Ch=C3=A1vez?= Date: Mon, 26 Aug 2019 12:02:42 -0500 Subject: [PATCH 270/549] Update index.rst --- index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.rst b/index.rst index 41b8be7aa3..5be91d9d44 100644 --- a/index.rst +++ b/index.rst @@ -124,8 +124,8 @@ These are recipes that'll help you address specific use-cases. how-tos/* -- :doc:`how-tos/embedding-table-from-another-schema` - :doc:`how-tos/casting-type-to-custom-json` +- :doc:`how-tos/embedding-table-from-another-schema` Topic guides ------------ From e81ab0d0bb62168d950695520e1894c6ea52ac6a Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 26 Aug 2019 13:34:58 -0500 Subject: [PATCH 271/549] Fix #246, add CONTRIBUTING.md Also add structure section on README.md --- CONTRIBUTING.md | 3 +++ README.md | 5 +++++ 2 files changed, 8 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..20fdb43dbe --- /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/master/.github/CONTRIBUTING.md diff --git a/README.md b/README.md index f60577cdc7..e8a06defb8 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,11 @@ Or if you use [nix](https://nixos.org/nix/), you can just run: Both of these options will build the docs and start a livereload server on `http://localhost:5500`. +## 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. + ## Translations Translations are maintained in separate repositories forked from this one. Once you finish translating in your fork you can upload the project From 126236c3d03d7c4916e44897e1cc346b2f9b57fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20Ch=C3=A1vez?= Date: Mon, 26 Aug 2019 13:52:26 -0500 Subject: [PATCH 272/549] Update v6.0.2.rst --- releases/v6.0.2.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releases/v6.0.2.rst b/releases/v6.0.2.rst index 774ac61bb0..19176d5b28 100644 --- a/releases/v6.0.2.rst +++ b/releases/v6.0.2.rst @@ -51,7 +51,7 @@ Changed Thanks ------ -This release was sponsored by: +This release is sponsored by: .. image:: ../_static/cybertec.png :target: https://www.cybertec-postgresql.com/en/ From bc6e181d03ce39bd5dfcd99b63781cfadba0d129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20Ch=C3=A1vez?= Date: Wed, 11 Sep 2019 12:05:51 -0500 Subject: [PATCH 273/549] Add params=multiple-objects to api reference (#253) --- api.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 97f3cf8b70..42fa40a7f3 100644 --- a/api.rst +++ b/api.rst @@ -602,7 +602,7 @@ It's also possible to embed `Materialized Views `_. @@ -780,12 +780,14 @@ PostgREST will detect if the function is scalar or table-valued and will shape t Bulk Call --------- -It's possible to call a function in a bulk way, analoguosly to :ref:`bulk_insert`. +It's possible to call a function in a bulk way, analoguosly to :ref:`bulk_insert`. To do this, you need to add the +``Prefer: params=multiple-objects`` header to your request. .. code-block:: http POST /rpc/add_them HTTP/1.1 Content-Type: application/json + Prefer: params=multiple-objects [ {"a": 1, "b": 2}, From 1a18ef5a2016a101c87143a88b0da6721750b783 Mon Sep 17 00:00:00 2001 From: Reuben Thomas-Davis Date: Sun, 15 Sep 2019 21:24:17 +0100 Subject: [PATCH 274/549] add server proxy uri env var for smoother swagger-ui use with docker-compose --- install.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/install.rst b/install.rst index fc3f278cd9..685d282354 100644 --- a/install.rst +++ b/install.rst @@ -134,6 +134,7 @@ To avoid having to install the database at all, you can run both it and the serv PGRST_DB_URI: postgres://app_user:password@db:5432/app_db PGRST_DB_SCHEMA: public PGRST_DB_ANON_ROLE: app_user #In production this role should not be the same as the one used for the connection + PGRST_SERVER_PROXY_URI: "http://127.0.0.1:3000" depends_on: - db db: From 2186a7e1a84525b118de956a1fc333fa5075d05a Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 17 Sep 2019 17:52:10 -0500 Subject: [PATCH 275/549] Add upcoming release page * Add HEAD support mention * Add change for bulk call --- api.rst | 2 +- releases/upcoming.rst | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 releases/upcoming.rst diff --git a/api.rst b/api.rst index 42fa40a7f3..1880dc9fc6 100644 --- a/api.rst +++ b/api.rst @@ -10,7 +10,7 @@ All views and tables in the exposed schema and accessible by the active database GET /people HTTP/1.1 -There are no deeply/nested/routes. Each route provides OPTIONS, GET, POST, PATCH, and DELETE verbs depending entirely on database permissions. +There are no deeply/nested/routes. Each route provides OPTIONS, GET, HEAD, POST, PATCH, and DELETE verbs depending entirely on database permissions. .. note:: diff --git a/releases/upcoming.rst b/releases/upcoming.rst new file mode 100644 index 0000000000..836a2c78c8 --- /dev/null +++ b/releases/upcoming.rst @@ -0,0 +1,19 @@ +.. |br| raw:: html + +
    + +Upcoming +======== + +These are changes yet unreleased. If you'd like to try them out before a new official release, you can :ref:`build_source`. + +Added +----- + +* Support for HTTP HEAD requests. + |br| -- `@steve-chavez `_ + +Changed +------- + +* :ref:`bulk_call` should now be done by specifying a ``Prefer: params=multiple-objects`` header. From 476af62326f1828a0f6e8084caf3dcc1adfcc166 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 18 Sep 2019 12:40:52 -0500 Subject: [PATCH 276/549] Add planned count reference --- admin.rst | 6 +----- api.rst | 30 ++++++++++++++++++++++++++++-- releases/upcoming.rst | 3 +++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/admin.rst b/admin.rst index 3638516567..9d4841c8d9 100644 --- a/admin.rst +++ b/admin.rst @@ -32,7 +32,7 @@ The first step is to create an Nginx configuration file that proxies requests to .. note:: - For ubuntu, if you already installed nginx through :code:`apt` you can add this to the config file in + 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`. .. _block_fulltable: @@ -88,10 +88,6 @@ This is fine in small tables, but count performance degrades in big tables due t -- Pending nginx config: Remove any prefer header which contains the word count -.. note:: - - In future versions we will support :code:`Prefer: count=estimated` to leverage the PostgreSQL statistics tables for a fast (and fairly accurate) result. - .. _hardening_https: HTTPS diff --git a/api.rst b/api.rst index 1880dc9fc6..9284d3eede 100644 --- a/api.rst +++ b/api.rst @@ -371,12 +371,16 @@ The other way to request a limit or offset is with query parameters. For example 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. -In order to obtain the total size of the table or view (such as when rendering the last page link in a pagination control), specify your preference in a request header: +.. _exact_count: +Exact Count +~~~~~~~~~~~ + +In order to obtain the total size of the table or view (such as when rendering the last page link in a pagination control), specify ``Prefer: count=exact`` as a request header: .. code-block:: http - GET /bigtable HTTP/1.1 + HEAD /bigtable HTTP/1.1 Range-Unit: items Range: 0-24 Prefer: count=exact @@ -389,6 +393,28 @@ Note that the larger the table the slower this query runs in the database. The s Range-Unit: items Content-Range: 0-24/3573458 +.. _planned_count: + +Planned Count +~~~~~~~~~~~~~ + +To avoid the shortcomings of :ref:`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:: http + + HEAD /bigtable?limit=25 HTTP/1.1 + 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 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 `_ for more details. + .. _res_format: Response Format diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 836a2c78c8..8096b97ea3 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -13,6 +13,9 @@ Added * Support for HTTP HEAD requests. |br| -- `@steve-chavez `_ +* Support for :ref:`planned_count`. + |br| -- `@steve-chavez `_ + Changed ------- From 5ef1db42efd962f674ebc68832b9f8f5256d3ddd Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 18 Sep 2019 14:15:38 -0500 Subject: [PATCH 277/549] Add estimated count reference --- api.rst | 40 +++++++++++++++++++++++++++++++++++++++- releases/upcoming.rst | 2 +- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 9284d3eede..7dba6729ef 100644 --- a/api.rst +++ b/api.rst @@ -411,10 +411,48 @@ To do this, specify the ``Prefer: count=planned`` header. HTTP/1.1 206 Partial Content Content-Range: 0-24/3572000 -Note that the accuracy of this count depends how up-to-date are the PostgreSQL statistics tables. +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 `_ for more details. +.. _estimated_count: + +Estimated Count +~~~~~~~~~~~~~~~ + +When you are interested in the count, the relative error is important. If you have an estimated count of 1000000 and the exact count is +1001000, the error is small enough to be ignored. But with an estimated 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 estimated count when +that threshold is surpassed. To use this behavior, you can specify the ``Prefer: count=estimated`` header. The **threshold** is +defined by :ref:`max-rows`. + +Here's an example. Suppose we set ``max-rows=1000`` and *smalltable* has 321 rows, then we'll get the exact count: + +.. code-block:: http + + HEAD /smalltable?limit=25 HTTP/1.1 + 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 estimated count: + +.. code-block:: http + + HEAD /bigtable?limit=25 HTTP/1.1 + Prefer: count=estimated + +.. code-block:: http + + HTTP/1.1 206 Partial Content + Content-Range: 0-24/3572000 + .. _res_format: Response Format diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 8096b97ea3..886c4449b8 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -13,7 +13,7 @@ Added * Support for HTTP HEAD requests. |br| -- `@steve-chavez `_ -* Support for :ref:`planned_count`. +* Support for :ref:`planned_count` and :ref:`estimated_count`. |br| -- `@steve-chavez `_ Changed From 4f1f80d4f2e94aca4db268146edc4e212c5fb641 Mon Sep 17 00:00:00 2001 From: Lorenz Henk Date: Thu, 19 Sep 2019 09:38:45 +0200 Subject: [PATCH 278/549] Replace estimated with planned The names changed according to https://github.com/PostgREST/postgrest/issues/1378#issuecomment-531506803 --- api.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api.rst b/api.rst index 7dba6729ef..19a7d85182 100644 --- a/api.rst +++ b/api.rst @@ -420,12 +420,12 @@ See `ANALYZE `_ for more de Estimated Count ~~~~~~~~~~~~~~~ -When you are interested in the count, the relative error is important. If you have an estimated count of 1000000 and the exact count is -1001000, the error is small enough to be ignored. But with an estimated count of 7, an exact count of 28 would be a huge misprediction. +When you are interested in the count, the relative error is important. If you have a :ref:`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 estimated count when +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:`max-rows`. @@ -441,7 +441,7 @@ Here's an example. Suppose we set ``max-rows=1000`` and *smalltable* has 321 row 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 estimated count: +If we make a similar request on *bigtable*, which has 3573458 rows, we would get the planned count: .. code-block:: http From 6afed0b30dcf7254cf3288cce41d96705bbecdc9 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 28 Sep 2019 15:40:31 -0500 Subject: [PATCH 279/549] Add reference for stored proc embedding --- api.rst | 32 ++++++++++++++++++++++++++++++++ releases/upcoming.rst | 2 ++ 2 files changed, 34 insertions(+) diff --git a/api.rst b/api.rst index 19a7d85182..71948cc59d 100644 --- a/api.rst +++ b/api.rst @@ -681,6 +681,38 @@ It's also possible to embed `Materialized Views ` that returns a table type, you can embed its related tables. + +Here's a sample function(notice the ``RETURNS SETOF films``). + +.. code-block:: plpgsql + + CREATE FUNCTION getallfilms() RETURNS SETOF films AS $$ + SELECT * FROM films; + $$ LANGUAGE SQL IMMUTABLE; + +A request with ``directors`` embedded: + +.. code-block:: http + + GET /rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers* HTTP/1.1 + +.. code-block:: json + + [ + { "title": "Workers Leaving The Lumière Factory In Lyon", + "directors": { + "id": 2, + "last_name": "Lumière" + } + } + ] + .. _custom_queries: Custom Queries diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 886c4449b8..3085f0a925 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -16,6 +16,8 @@ Added * Support for :ref:`planned_count` and :ref:`estimated_count`. |br| -- `@steve-chavez `_ +* Documentation reference for :ref:`s_proc_embed`. + Changed ------- From 3325a1bbc6375bf2c2a57fa88b6fcd0a23112c0c Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 28 Sep 2019 20:08:21 -0500 Subject: [PATCH 280/549] Add reference for mutation embed --- api.rst | 39 ++++++++++++++++++++++++++++++++++++++- releases/upcoming.rst | 4 +++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 71948cc59d..ca5187cb5f 100644 --- a/api.rst +++ b/api.rst @@ -686,7 +686,7 @@ It's also possible to embed `Materialized Views ` that returns a table type, you can embed its related tables. +If you have a :ref:`Stored Procedure ` that returns a table type, you can embed its related resources. Here's a sample function(notice the ``RETURNS SETOF films``). @@ -713,6 +713,39 @@ A request with ``directors`` embedded: } ] +.. _mutation_embed: + +Embedding after Insertions/Updates/Deletions +-------------------------------------------- + +You can embed related resources after doing :ref:`insert_update` or :ref:`delete`. + +Say you want to insert a **film** and then get some of its attributes plus embed its **director**. + +.. code-block:: http + + POST /films?select=title,year,director:directors(first_name,last_name) HTTP/1.1 + Prefer: return=representation + + { + "id": 100, "director_id": 40, + "title": "127 hours", "year": 2010, + "rating": 7.6, "language": "english" + } + +Response: + +.. code-block:: json + + { + "title": "127 hours", + "year": 2010, + "director": { + "first_name": "Danny", + "last_name": "Boyle" + } + } + .. _custom_queries: Custom Queries @@ -1040,6 +1073,8 @@ Returns: {"hint":"Upgrade your plan","details":"Quota exceeded"} +.. _insert_update: + Insertions / Updates ==================== @@ -1196,6 +1231,8 @@ All the columns must be specified in the request body, including the primary key This feature is only available starting from PostgreSQL 9.5 since it uses the `ON CONFLICT clause `_. +.. _delete: + Deletions ========= diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 3085f0a925..72293b8faa 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -16,7 +16,9 @@ Added * Support for :ref:`planned_count` and :ref:`estimated_count`. |br| -- `@steve-chavez `_ -* Documentation reference for :ref:`s_proc_embed`. +* Reference for :ref:`s_proc_embed`. + +* Reference for :ref:`mutation_embed`. Changed ------- From 22b6ff764dd985a02b031988832bbc6d6a9d2c57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steve=20Ch=C3=A1vez?= Date: Fri, 18 Oct 2019 11:03:32 -0500 Subject: [PATCH 281/549] Credit howtos/tuts authors (#264) --- how-tos/casting-type-to-custom-json.rst | 4 +++- how-tos/embedding-table-from-another-schema.rst | 2 ++ tutorials/tut0.rst | 2 ++ tutorials/tut1.rst | 2 ++ 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/how-tos/casting-type-to-custom-json.rst b/how-tos/casting-type-to-custom-json.rst index f9d13afa79..b2b25128dc 100644 --- a/how-tos/casting-type-to-custom-json.rst +++ b/how-tos/casting-type-to-custom-json.rst @@ -1,6 +1,8 @@ Casting a type to a custom JSON object ====================================== +:author: `steve-chavez `_ + While using PostgREST you might have noticed that certain PostgreSQL types translate to JSON strings when you would have expected a JSON object or array. For example, let's see the case of `range types `_. @@ -82,7 +84,7 @@ You can use the same idea for creating custom CASTs for different types. .. note:: - If you don't want to modify CASTs for built-in types, an option would be to `create a custom type `_ + If you don't want to modify CASTs for built-in types, an option would be to `create a custom type `_ for your own ``tsrange`` and add its own CAST. .. code-block:: postgres diff --git a/how-tos/embedding-table-from-another-schema.rst b/how-tos/embedding-table-from-another-schema.rst index 60265dd409..118d630d17 100644 --- a/how-tos/embedding-table-from-another-schema.rst +++ b/how-tos/embedding-table-from-another-schema.rst @@ -1,6 +1,8 @@ Embedding a table from another schema ===================================== +:author: `steve-chavez `_ + Suppose you have a **people** table in the ``public`` schema and this schema is exposed through PostgREST's :ref:`db-schema`. .. code-block:: postgres diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index e061b4761d..038b92f6fc 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -3,6 +3,8 @@ Tutorial 0 - Get it Running =========================== +:author: `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. diff --git a/tutorials/tut1.rst b/tutorials/tut1.rst index 71c288d11d..e9fe78f547 100644 --- a/tutorials/tut1.rst +++ b/tutorials/tut1.rst @@ -3,6 +3,8 @@ Tutorial 1 - The Golden Key =========================== +:author: `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 From 2fc0e2f8cbae385cb0148677665521ba28500bcf Mon Sep 17 00:00:00 2001 From: Fedor Ortyanov Date: Tue, 22 Oct 2019 11:13:51 +0300 Subject: [PATCH 282/549] fix api.rst other status from 500 to 400 --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index ca5187cb5f..871d1a6f48 100644 --- a/api.rst +++ b/api.rst @@ -1403,5 +1403,5 @@ PostgREST translates `PostgreSQL error codes Date: Thu, 14 Nov 2019 10:57:23 -0800 Subject: [PATCH 283/549] Specify units for the pool timeout setting --- configuration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/configuration.rst b/configuration.rst index d44096de43..267ed24a16 100644 --- a/configuration.rst +++ b/configuration.rst @@ -95,7 +95,7 @@ db-pool db-pool-timeout --------------- - Time to live for an idle database pool connection. If the timeout is reached the connection will be closed. + Time to live, in seconds, for an idle database pool connection. If the timeout is reached the connection will be closed. Once a new request arrives a new connection will be started. .. _db-extra-search-path: From a0179cfd4b7052633d4c9f322bef09c034203626 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 18 Nov 2019 15:47:11 -0500 Subject: [PATCH 284/549] Remove keepalive nginx recommendation --- admin.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/admin.rst b/admin.rst index 9d4841c8d9..8334fd02b6 100644 --- a/admin.rst +++ b/admin.rst @@ -12,7 +12,6 @@ The first step is to create an Nginx configuration file that proxies requests to # upstream configuration upstream postgrest { server localhost:3000; - keepalive 64; } # ... server { From e1c9b8fe768167c7fb281a095ff46240b0a657ce Mon Sep 17 00:00:00 2001 From: Danilo Amoroso Date: Mon, 25 Nov 2019 12:55:57 +0100 Subject: [PATCH 285/549] added documentation for server-unix-socket-mode config option --- configuration.rst | 58 +++++++++++++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/configuration.rst b/configuration.rst index 267ed24a16..c02d8a62c8 100644 --- a/configuration.rst +++ b/configuration.rst @@ -30,28 +30,29 @@ The user specified in the db-uri is also known as the authenticator role. For mo Here is the full list of configuration parameters. -==================== ====== ========= ======== -Name Type Default Required -==================== ====== ========= ======== -db-uri String Y -db-schema String Y -db-anon-role String Y -db-pool Int 10 -db-pool-timeout Int 10 -db-extra-search-path String public -server-host String !4 -server-port Int 3000 -server-unix-socket String -server-proxy-uri String -jwt-secret String -jwt-aud String -secret-is-base64 Bool False -max-rows Int ∞ -pre-request String -app.settings.* String -role-claim-key String .role -raw-media-types String -==================== ====== ========= ======== +======================= ====== ========= ======== +Name Type Default Required +======================= ====== ========= ======== +db-uri String Y +db-schema String Y +db-anon-role String Y +db-pool Int 10 +db-pool-timeout Int 10 +db-extra-search-path String public +server-host String !4 +server-port Int 3000 +server-unix-socket String +server-unix-socket-mode String 755 +server-proxy-uri String +jwt-secret String +jwt-aud String +secret-is-base64 Bool False +max-rows Int ∞ +pre-request String +app.settings.* String +role-claim-key String .role +raw-media-types String +======================= ====== ========= ======== .. _db-uri: @@ -141,6 +142,18 @@ server-unix-socket server-unix-socket = "/tmp/pgrst.sock" +.. _server-unix-socket-mode: + +server-unix-socket-mode +----------------------- + + `Unix file mode `_ 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 = "755" + .. _server-proxy-uri: server-proxy-uri @@ -239,3 +252,4 @@ raw-media-types raw-media-types="image/png, text/xml" + From a0091f61a183a23da67679716a54591c0b0d6822 Mon Sep 17 00:00:00 2001 From: Jean SIMARD Date: Tue, 26 Nov 2019 10:45:36 +0100 Subject: [PATCH 286/549] Typo (repeated word) --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 871d1a6f48..06082c2f79 100644 --- a/api.rst +++ b/api.rst @@ -675,7 +675,7 @@ It's also possible to embed `Materialized Views `_ if your view is not made embeddable so we can keep continue improving foreign key detection. - In the future we'll include include a way to manually specify views source foreign keys to address this limitation. + In the future we'll include a way to manually specify views source foreign keys to address this limitation. .. important:: From 30d241fcff4ab8aa2af094b4c3c361606ddcc20e Mon Sep 17 00:00:00 2001 From: ycheng2020 <12297766+ycheng-kf@users.noreply.github.com> Date: Sun, 8 Dec 2019 01:00:02 -0500 Subject: [PATCH 287/549] Fixed the pg_listen syntax (#285) * Fixed the pg_listen syntax Can't run killall -SIGUSR1 postgrest: No such file or directory Error happens because pg_listen needs the full path to killall. --- admin.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin.rst b/admin.rst index 8334fd02b6..a192b304b4 100644 --- a/admin.rst +++ b/admin.rst @@ -213,9 +213,9 @@ Then run the `pg_listen `_ utility to mon .. code-block:: bash - pg_listen ddl_command_end "killall -SIGUSR1 postgrest" + pg_listen ddl_command_end $(which killall) -SIGUSR1 postgrest -Now, whenever the structure of the database schema changes, PostgreSQL will notify the ``ddl_command_end`` channel, which will cause ``pg_listen`` to send PostgREST the signal to reload its cache. +Now, whenever the structure of the database schema changes, PostgreSQL will notify the ``ddl_command_end`` channel, which will cause ``pg_listen`` to send PostgREST the signal to reload its cache. Note that pg_listen requires full path to the executable in the example above. Daemonizing =========== From 458960f7c3f6475bb0a9748bddbb361d8d54c2c7 Mon Sep 17 00:00:00 2001 From: Dmitry Wagin Date: Mon, 16 Dec 2019 16:38:25 +0300 Subject: [PATCH 288/549] change server-unix-socket-mode 755 -> 660 (#286) --- configuration.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/configuration.rst b/configuration.rst index c02d8a62c8..6317aa4db5 100644 --- a/configuration.rst +++ b/configuration.rst @@ -42,7 +42,7 @@ db-extra-search-path String public server-host String !4 server-port Int 3000 server-unix-socket String -server-unix-socket-mode String 755 +server-unix-socket-mode String 660 server-proxy-uri String jwt-secret String jwt-aud String @@ -152,7 +152,7 @@ server-unix-socket-mode .. code:: bash - server-unix-socket-mode = "755" + server-unix-socket-mode = "660" .. _server-proxy-uri: From 1b166ce0483cfacb7ab3093b9adf15b7718d66bd Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Mon, 16 Dec 2019 12:39:57 -0500 Subject: [PATCH 289/549] Add Community Tutorials section (#287) Include DO video series --- ecosystem.rst | 8 ++++++++ index.rst | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ecosystem.rst b/ecosystem.rst index 1ffc96e370..ba01d1435d 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -1,3 +1,11 @@ +.. _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. + .. _eco_example_apps: Example Apps diff --git a/index.rst b/index.rst index 5be91d9d44..967a33ac9c 100644 --- a/index.rst +++ b/index.rst @@ -88,7 +88,7 @@ Are you new to PostgREST? This is the place to start! - :doc:`tutorials/tut0` - :doc:`tutorials/tut1` -Also have a look at :doc:`Installation `. +Also have a look at :doc:`Installation ` and :ref:`community_tutorials`. Reference guides ---------------- From 8dba0dcc11f50c9a8abc6387e27ad176eb0c727d Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Wed, 18 Dec 2019 01:28:44 +0800 Subject: [PATCH 290/549] Remove fragment from Nimbus blog post URL (#288) --- index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.rst b/index.rst index 967a33ac9c..eb7f581f91 100644 --- a/index.rst +++ b/index.rst @@ -187,7 +187,7 @@ Here are some companies that use PostgREST in production. * `Datrium `_ * `Nimbus `_ - - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. + - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. * `Catarse `_ * `Moat `_ * `Redsmin `_ From 4eefe14706e0059eaa588c89c898be919c287752 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Mon, 23 Dec 2019 13:46:22 -0500 Subject: [PATCH 291/549] Add GISOPS community tutorial (#291) --- ecosystem.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ecosystem.rst b/ecosystem.rst index ba01d1435d..6e0317bdf5 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -6,6 +6,9 @@ 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 + PostGIS API tutorial in 5 minutes `_ - + In this tutorial, GIS • OPS shows how to perform PostGIS calculations through PostgREST :ref:`s_procs` interface. + .. _eco_example_apps: Example Apps From e47d19d4dc8095be53830186205c2a976d8bbd15 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 28 Dec 2019 11:21:30 -0500 Subject: [PATCH 292/549] Fix livereload host --- livereload_docs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/livereload_docs.py b/livereload_docs.py index 03910bd6a9..7674d11d38 100755 --- a/livereload_docs.py +++ b/livereload_docs.py @@ -9,5 +9,5 @@ server.watch('how-tos/*.rst', shell('sphinx-build -b html -a -n . _build')) server.watch('releases/*.rst', shell('sphinx-build -b html -a -n . _build')) # For custom port and host -server.serve(root='_build/', host='192.168.1.2') -# server.serve(root='_build/') +# server.serve(root='_build/', host='192.168.1.2') +server.serve(root='_build/') From 4cd05681dd84abb6616f02920330ef6d9f0ca3e5 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Mon, 6 Jan 2020 09:43:54 -0500 Subject: [PATCH 293/549] Add embedding disambiguation section (#290) --- _static/orders.png | Bin 0 -> 17892 bytes api.rst | 104 +++++++++++++++++++++++++++++++++++++++++++++ erd/README.md | 7 +++ erd/orders.er | 15 +++++++ 4 files changed, 126 insertions(+) create mode 100644 _static/orders.png create mode 100644 erd/README.md create mode 100644 erd/orders.er diff --git a/_static/orders.png b/_static/orders.png new file mode 100644 index 0000000000000000000000000000000000000000..db709c873da885c3c73b42678bbcc279f582c836 GIT binary patch literal 17892 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 M>FVdQ&MBb@0J;5iXaE2J literal 0 HcmV?d00001 diff --git a/api.rst b/api.rst index 06082c2f79..2910f7e3aa 100644 --- a/api.rst +++ b/api.rst @@ -746,6 +746,110 @@ Response: } } +.. _embed_disamb: + +Embedding Disambiguation +------------------------ + +For doing resource embedding, PostgREST infers the relationship between two tables based on a foreign key between them. +However, in cases where there's more than one foreign key between two tables, it's not possible to infer the relationship unambiguosly +by just specifying the tables names. + +Target Disambiguation +~~~~~~~~~~~~~~~~~~~~~ + +For example, suppose you have the following ``orders`` and ``addresses`` tables: + +.. image:: _static/orders.png + +And you try to embed ``orders`` with ``addresses`` (this is the **target**): + +.. code-block:: http + + GET /orders?select=*,addresses(*) HTTP/1.1 + +Since the ``orders`` table has two foreign keys to the ``addresses`` table — an order has a billing address and a shipping address — +the request is ambiguous and PostgREST will respond with an error: + +.. code-block:: http + + HTTP/1.1 300 Multiple Choices + +If this happens, you need to disambiguate the request by adding precision to the **target**. +Instead of the **table name**, you can specify the **foreign key constraint name** or the **column name** that is part of the foreign key. + +Let's try first with the **foreign key constraint name**. To make it clearer we can name it: + +.. code-block:: postgresql + + ALTER TABLE orders + ADD CONSTRAINT billing_address foreign key (billing_address_id) references addresses(id), + ADD CONSTRAINT shipping_address foreign key (shipping_address_id) references addresses(id); + + -- Or if the constraints names were already generated by PostgreSQL we can rename them + -- ALTER TABLE orders + -- RENAME CONSTRAINT orders_billing_address_id_fkey TO billing_address, + -- RENAME CONSTRAINT orders_shipping_address_id_fkey TO shipping_address; + +Now we can unambiguously embed the billing address by specifying the ``billing_address`` foreign key constraint as the **target**. + +.. code-block:: http + + GET /orders?select=name,billing_address(name) HTTP/1.1 + + [ + { + "name": "Personal Water Filter", + "billing_address": { + "name": "32 Glenlake Dr.Dearborn, MI 48124" + } + } + ] + +Alternatively, you can specify the **column name** of the foreign key constraint as the **target**. This can be aliased to make +the result more clear. + +.. code-block:: http + + GET /orders?select=name,billing_address:billing_address_id(name) HTTP/1.1 + + [ + { + "name": "Personal Water Filter", + "billing_address": { + "name": "32 Glenlake Dr.Dearborn, MI 48124" + } + } + ] + +Hint Disambiguation +~~~~~~~~~~~~~~~~~~~ + +If specifying the **target** is not enough for unambiguous embedding, you can add a **hint**. For example, let's assume we create +two VIEWs of ``addresses``: ``central_addresses`` and ``eastern_addresses``. + +Since PostgREST supports :ref:`embedding_views` by detecting **source foreign keys** in the views, embedding with the foreign key +as the **target** will not be enough for an unambiguous embed: + +.. code-block:: http + + GET /orders?select=*,billing_address(*) HTTP/1.1 + + HTTP/1.1 300 Multiple Choices + +For solving this case, in addition to the **target**, we can add a **hint**. +Here we specify ``central_addresses`` as the **target** and the ``billing_address`` foreign key as the **hint**: + +.. code-block:: http + + GET /orders?select=*,central_addresses!billing_address(*) HTTP/1.1 + + HTTP/1.1 200 OK + + [ ... ] + +Similarly to the **target**, the **hint** can be a **table name**, **foreign key constraint name** or **column name**. + .. _custom_queries: Custom Queries diff --git a/erd/README.md b/erd/README.md new file mode 100644 index 0000000000..d71e8e263f --- /dev/null +++ b/erd/README.md @@ -0,0 +1,7 @@ +This files 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 erd/film.er -o _static/film.png +``` diff --git a/erd/orders.er b/erd/orders.er new file mode 100644 index 0000000000..bdd93de2ef --- /dev/null +++ b/erd/orders.er @@ -0,0 +1,15 @@ +[Addresses] +*id +name +city +state +postal_code + +[Orders] +*id +name ++billing_address_id ++shipping_address_id + +Orders *--1 Addresses +Orders *--1 Addresses From da1cf9f76d6c98d81d18372a7df1b1e27f81b450 Mon Sep 17 00:00:00 2001 From: Remo <59358383+monacoremo@users.noreply.github.com> Date: Sun, 19 Jan 2020 20:17:56 +0100 Subject: [PATCH 294/549] Add postgrest-sessions-example to Example Apps (#294) --- ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem.rst b/ecosystem.rst index 6e0317bdf5..1e41ecbd66 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -14,6 +14,7 @@ Community Tutorials Example Apps ------------ +* `monacoremo/postgrest-sessions-example `_ - example for cookie-based sessions * `tatut/postgrest-ui `_ - ClojureScript UI components for PostgREST * `priyank-purohit/PostGUI `_ - React Material UI admin panel * `Qu4tro/pgrst-dev-setup `_ - docker-compose and tmuxp setup for experimentation. From 1caf114e5aa960c2b7596e96df64fabf7c65fa8f Mon Sep 17 00:00:00 2001 From: Ivan Gabriele Date: Thu, 23 Jan 2020 10:07:08 +0100 Subject: [PATCH 295/549] Add postgrester client library --- ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem.rst b/ecosystem.rst index 1e41ecbd66..46cdca03ac 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -69,6 +69,7 @@ Extensions Client-Side Libraries --------------------- +* `SocialGouv/postgrester `_ - JS + Typescript * `Kong/py-postgrest `_ - Python * `datrium/postgrest-pyclient `_ - Python * `tomberek/aor-postgrest-client `_ - JS, admin-on-rest From 95fccbb00f89cd9987417e5a0854c58577906523 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Mon, 3 Feb 2020 11:33:39 +0800 Subject: [PATCH 296/549] Add supabase/postgrest-js client library --- ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem.rst b/ecosystem.rst index 46cdca03ac..f3e230f537 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -69,6 +69,7 @@ Extensions Client-Side Libraries --------------------- +* `supabase/postgrest-js `_ - Isomorphic JS client * `SocialGouv/postgrester `_ - JS + Typescript * `Kong/py-postgrest `_ - Python * `datrium/postgrest-pyclient `_ - Python From f6c67ffdc144ac654e5d62f897f594d86bb95fe0 Mon Sep 17 00:00:00 2001 From: Wouter Scherphof Date: Fri, 7 Feb 2020 23:27:41 +0100 Subject: [PATCH 297/549] correction url embedded resources through join tables --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 2910f7e3aa..01f35676c3 100644 --- a/api.rst +++ b/api.rst @@ -600,7 +600,7 @@ PostgREST can also detect relationships going through join tables. Thus you can .. code-block:: http - GET /directors?select=films(title,year) HTTP/1.1 + GET /actors?select=films(title,year) HTTP/1.1 Embedded Filters ---------------- From 878752faee2070914d36c56e49c140a4b17d313f Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 8 Feb 2020 14:52:28 -0500 Subject: [PATCH 298/549] Correct join table paragraph --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 01f35676c3..03d91ae305 100644 --- a/api.rst +++ b/api.rst @@ -596,7 +596,7 @@ this: Embedding through join tables ----------------------------- -PostgREST can also detect relationships going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). You can also reverse the direction of inclusion, asking for all Directors with each including the list of their Films: +PostgREST can also detect relationships going through join tables. Thus you can request the Actors for Films (which in this case finds the information through Roles). .. code-block:: http From c4005ad8f27ad4463fbb48bc0904f04d5fe57341 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 6 Feb 2020 14:21:43 -0500 Subject: [PATCH 299/549] Change server-proxy-uri 2 openapi-server-proxy-uri --- configuration.rst | 54 +++++++++++++++++++++---------------------- releases/upcoming.rst | 2 ++ 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/configuration.rst b/configuration.rst index 6317aa4db5..fe9b9f7f26 100644 --- a/configuration.rst +++ b/configuration.rst @@ -30,29 +30,29 @@ The user specified in the db-uri is also known as the authenticator role. For mo Here is the full list of configuration parameters. -======================= ====== ========= ======== -Name Type Default Required -======================= ====== ========= ======== -db-uri String Y -db-schema String Y -db-anon-role String Y -db-pool Int 10 -db-pool-timeout Int 10 -db-extra-search-path String public -server-host String !4 -server-port Int 3000 -server-unix-socket String -server-unix-socket-mode String 660 -server-proxy-uri String -jwt-secret String -jwt-aud String -secret-is-base64 Bool False -max-rows Int ∞ -pre-request String -app.settings.* String -role-claim-key String .role -raw-media-types String -======================= ====== ========= ======== +======================== ====== ========= ======== +Name Type Default Required +======================== ====== ========= ======== +db-uri String Y +db-schema String Y +db-anon-role String Y +db-pool Int 10 +db-pool-timeout Int 10 +db-extra-search-path String public +server-host String !4 +server-port Int 3000 +server-unix-socket String +server-unix-socket-mode String 660 +openapi-server-proxy-uri String +jwt-secret String +jwt-aud String +secret-is-base64 Bool False +max-rows Int ∞ +pre-request String +app.settings.* String +role-claim-key String .role +raw-media-types String +======================== ====== ========= ======== .. _db-uri: @@ -149,15 +149,15 @@ server-unix-socket-mode `Unix file mode `_ 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" -.. _server-proxy-uri: +.. _openapi-server-proxy-uri: -server-proxy-uri ----------------- +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` diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 72293b8faa..19572008f5 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -24,3 +24,5 @@ Changed ------- * :ref:`bulk_call` should now be done by specifying a ``Prefer: params=multiple-objects`` header. + +* ``server-proxy-uri`` config option has been renamed to :ref:`openapi-server-proxy-uri`. From 0ed1d31c7240e9b7dec9d43298609c2dffadc7de Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 7 Feb 2020 12:52:18 -0500 Subject: [PATCH 300/549] Add embedding disambiguation in upcoming --- releases/upcoming.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 19572008f5..8f00c63961 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -16,13 +16,18 @@ Added * Support for :ref:`planned_count` and :ref:`estimated_count`. |br| -- `@steve-chavez `_ -* Reference for :ref:`s_proc_embed`. +* Support for :ref:`Resource Embedding Disambiguation `. + |br| -- `@steve-chavez `_ + +* Documentation reference for :ref:`s_proc_embed`. -* Reference for :ref:`mutation_embed`. +* Documentation reference for :ref:`mutation_embed`. Changed ------- * :ref:`bulk_call` should now be done by specifying a ``Prefer: params=multiple-objects`` header. +* Resource Embedding now outputs an error when multiple relationships between two tables are found, see :ref:`embed_disamb`. + * ``server-proxy-uri`` config option has been renamed to :ref:`openapi-server-proxy-uri`. From 1f40492d64b8fea00c53cadf60d6c141e31732f7 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 7 Feb 2020 13:23:33 -0500 Subject: [PATCH 301/549] Add request path/method GUC --- api.rst | 183 +++++++++++++++++++++++------------------- releases/upcoming.rst | 3 + 2 files changed, 104 insertions(+), 82 deletions(-) diff --git a/api.rst b/api.rst index 03d91ae305..506cf35b98 100644 --- a/api.rst +++ b/api.rst @@ -1096,87 +1096,6 @@ You can call overloaded functions with different number of arguments. .. code-block:: http GET /rpc/rental_duration?customer_id=232&from_date=2018-07-01 HTTP/1.1 - -Accessing Request Headers, Cookies and JWT claims -------------------------------------------------- - -Stored procedures can access request headers, cookies and jwt claims by reading GUC variables set by PostgREST per request. They are named :code:`request.header.XYZ`, :code:`request.cookie.XYZ` and :code:`request.jwt.claim.XYZ`. - -.. code-block:: postgresql - - -- To read the value of the Origin request header: - SELECT current_setting('request.header.origin', true); - -- To read the value of sessionId in a cookie: - SELECT current_setting('request.cookie.sessionId', true); - -- To read the value of the email claim in a jwt: - SELECT current_setting('request.jwt.claim.email', true); - -.. note:: - - ``request.jwt.claim.role`` defaults to the value of :ref:`db-anon-role`. - -Setting Response Headers ------------------------- - -PostgREST reads the ``response.headers`` SQL variable to add extra headers to the HTTP response. Stored procedures can modify this variable. For instance, this statement would add caching headers to the response: - -.. code-block:: sql - - -- tell client to cache response for two days - - SET LOCAL "response.headers" = - '[{"Cache-Control": "public"}, {"Cache-Control": "max-age=259200"}]'; - -Notice that the variable 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 to be repeated when setting multiple values and an object would not allow the repeated key. - -Errors and HTTP Status Codes ----------------------------- - -Stored procedures can return non-200 HTTP status codes by raising SQL exceptions. 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 `. 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 - - {"hint":"Upgrade your plan","details":"Quota exceeded"} - .. _insert_update: Insertions / Updates @@ -1444,10 +1363,110 @@ You can use a tool like `Swagger UI `_ to create The OpenAPI information can go out of date as the schema changes under a running server. To learn how to refresh the cache see :ref:`schema_reloading`. +HTTP Logic +========== + +.. _guc_req_headers_cookies_claims: + +Accessing Request Headers, Cookies and JWT claims +------------------------------------------------- + +You can access request headers, cookies and jwt claims by reading GUC variables set by PostgREST per request. They are named :code:`request.header.XYZ`, :code:`request.cookie.XYZ` and :code:`request.jwt.claim.XYZ`. + +.. code-block:: postgresql + + -- To read the value of the Origin request header: + SELECT current_setting('request.header.origin', true); + -- To read the value of sessionId in a cookie: + SELECT current_setting('request.cookie.sessionId', true); + -- To read the value of the email claim in a jwt: + SELECT current_setting('request.jwt.claim.email', true); + +.. note:: + + ``request.jwt.claim.role`` defaults to the value of :ref:`db-anon-role`. + +.. _guc_req_path_method: + +Accessing Request Path and Method +--------------------------------- + +You can also access the request path and method with :code:`request.path` and :code:`request.method`. + +.. code-block:: postgresql + + -- You can get the path of the request with + SELECT current_setting('request.path', true); + + -- You can get the method of the request with + SELECT current_setting('request.method', true); + +Setting Response Headers +------------------------ + +PostgREST reads the ``response.headers`` SQL variable to add extra headers to the HTTP response. Stored procedures can modify this variable. For instance, this statement would add caching headers to the response: + +.. code-block:: sql + + -- tell client to cache response for two days + + SET LOCAL "response.headers" = + '[{"Cache-Control": "public"}, {"Cache-Control": "max-age=259200"}]'; + +Notice that the variable 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 to be repeated when setting multiple values and an object would not allow the repeated key. + +Errors and HTTP Status Codes +---------------------------- + +Stored procedures can return non-200 HTTP status codes by raising SQL exceptions. 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 `. 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 + + {"hint":"Upgrade your plan","details":"Quota exceeded"} + .. _status_codes: HTTP Status Codes -================= +----------------- PostgREST translates `PostgreSQL error codes `_ into HTTP status as follows: diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 8f00c63961..cf8bb706c8 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -13,6 +13,9 @@ Added * Support for HTTP HEAD requests. |br| -- `@steve-chavez `_ +* Add GUCs for :ref:`guc_req_path_method`. + |br| -- `@steve-chavez `_ + * Support for :ref:`planned_count` and :ref:`estimated_count`. |br| -- `@steve-chavez `_ From 5fd86e682bea3f6ff9256c8f67de21125805c0f6 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 10 Feb 2020 12:00:04 -0500 Subject: [PATCH 302/549] Add setting headers via pre-request --- api.rst | 37 +++++++++++++++++++++++++++++++++++++ releases/upcoming.rst | 12 ++++++------ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/api.rst b/api.rst index 506cf35b98..dd234b7693 100644 --- a/api.rst +++ b/api.rst @@ -1415,6 +1415,43 @@ PostgREST reads the ``response.headers`` SQL variable to add extra headers to th Notice that the variable 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 to be repeated when setting multiple values and an object would not allow the repeated key. +.. _pre_req_headers: + +Setting headers via pre-request +------------------------------- + +By using a :ref:`pre-request` function, you can add headers to GET/POST/PATCH/PUT/DELETE responses. +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.header.user-agent', true); + 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 + -- pre-request = custom_headers + +Now when you make a GET request to a table or view, you'll get the cache headers. + +.. code-block:: http + + GET /people HTTP/1.1 + User-Agent: Mozilla/4.01 (compatible; MSIE 6.0; Windows NT 5.1) + + HTTP/1.1 200 OK + Content-Type: application/json; charset=utf-8 + Cache-Control: no-cache, no-store, must-revalidate + + ... + + Errors and HTTP Status Codes ---------------------------- diff --git a/releases/upcoming.rst b/releases/upcoming.rst index cf8bb706c8..4da513f6ec 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -10,18 +10,18 @@ These are changes yet unreleased. If you'd like to try them out before a new off Added ----- -* Support for HTTP HEAD requests. - |br| -- `@steve-chavez `_ - -* Add GUCs for :ref:`guc_req_path_method`. - |br| -- `@steve-chavez `_ - * Support for :ref:`planned_count` and :ref:`estimated_count`. |br| -- `@steve-chavez `_ * Support for :ref:`Resource Embedding Disambiguation `. |br| -- `@steve-chavez `_ +* HTTP improvements -- `@steve-chavez `_ + + + Support for HTTP HEAD requests. + + GUCs for :ref:`guc_req_path_method`. + + Support for :ref:`pre_req_headers`. + * Documentation reference for :ref:`s_proc_embed`. * Documentation reference for :ref:`mutation_embed`. From 87528412c0e24111349b7a2b07841f496a96876f Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 11 Feb 2020 14:39:00 -0500 Subject: [PATCH 303/549] Add overriding provided headers note --- api.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api.rst b/api.rst index dd234b7693..3e8bbad058 100644 --- a/api.rst +++ b/api.rst @@ -1415,6 +1415,10 @@ PostgREST reads the ``response.headers`` SQL variable to add extra headers to th Notice that the variable 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 to be repeated when setting multiple values and an object would not allow the repeated key. +.. note:: + + PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overriden this way. + .. _pre_req_headers: Setting headers via pre-request From 2aae1a389432cb9d6fa9399ab62fbd869c075586 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 13 Feb 2020 11:24:28 -0500 Subject: [PATCH 304/549] Add unix socket changes --- releases/upcoming.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 4da513f6ec..ae5ad42254 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -16,6 +16,9 @@ Added * Support for :ref:`Resource Embedding Disambiguation `. |br| -- `@steve-chavez `_ +* Support for user defined socket permission via :ref:`server-unix-socket-mode` config option + |br| -- `@Dansvidania `_ + * HTTP improvements -- `@steve-chavez `_ + Support for HTTP HEAD requests. @@ -34,3 +37,5 @@ Changed * Resource Embedding now outputs an error when multiple relationships between two tables are found, see :ref:`embed_disamb`. * ``server-proxy-uri`` config option has been renamed to :ref:`openapi-server-proxy-uri`. + +* Default Unix Socket file mode from 755 to 660 From 1d54b8b07d74f48ddf64ff3c2ab0feb858a610f3 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 13 Feb 2020 11:42:15 -0500 Subject: [PATCH 305/549] Add fixes to upcoming --- releases/upcoming.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/releases/upcoming.rst b/releases/upcoming.rst index ae5ad42254..fc7b7b3ef5 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -29,6 +29,24 @@ Added * Documentation reference for :ref:`mutation_embed`. +Fixed +----- + +* Allow embedding a VIEW when its source table foreign key is UNIQUE + |br| -- `@bwbroersma `_ + +* ``Accept: application/vnd.pgrst.object+json`` behavior is now enforced for POST/PATCH/DELETE regardless of ``Prefer: return=minimal`` + |br| -- `@dwagin `_ + +* Fix self join resource embedding on PATCH + |br| -- `@herulume `_, `@steve-chavez `_ + +* Allow PATCH/DELETE without ``Prefer: return=minimal`` on tables with no SELECT privileges + |br| -- `@steve-chavez `_ + +* Fix many to many resource embedding for RPC/PATCH + |br| -- `@steve-chavez `_ + Changed ------- From f34a480a99b4e33fd15f5f6375a37911a05e6c1f Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 13 Feb 2020 13:32:31 -0500 Subject: [PATCH 306/549] Add misc header improvements to upcoming --- api.rst | 2 ++ releases/upcoming.rst | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 3e8bbad058..17960fa482 100644 --- a/api.rst +++ b/api.rst @@ -1401,6 +1401,8 @@ You can also access the request path and method with :code:`request.path` and :c -- You can get the method of the request with SELECT current_setting('request.method', true); +.. _guc_resp_hdrs: + Setting Response Headers ------------------------ diff --git a/releases/upcoming.rst b/releases/upcoming.rst index fc7b7b3ef5..de5e751cd5 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -19,11 +19,13 @@ Added * Support for user defined socket permission via :ref:`server-unix-socket-mode` config option |br| -- `@Dansvidania `_ -* HTTP improvements -- `@steve-chavez `_ +* HTTP logic improvements -- `@steve-chavez `_ + Support for HTTP HEAD requests. + GUCs for :ref:`guc_req_path_method`. + Support for :ref:`pre_req_headers`. + + Allow overriding provided headers(Content-Type, Location, etc) by :ref:`guc_resp_hdrs` + + Access to the ``Authorization`` header value through ``request.header.authorization`` * Documentation reference for :ref:`s_proc_embed`. From 53f35f638c5203d50bf63db0090fb224cb8fb2dc Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 14 Feb 2020 14:14:32 -0500 Subject: [PATCH 307/549] Add on_conflict query parameter --- api.rst | 26 +++++++++++++++++++++++--- releases/upcoming.rst | 3 +++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/api.rst b/api.rst index 17960fa482..fb478dc12c 100644 --- a/api.rst +++ b/api.rst @@ -1218,7 +1218,7 @@ In this case, only **source**, **publication_date** and **figure** will be inser 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. -Upsert +UPSERT ------ You can make an UPSERT with :code:`POST` and the :code:`Prefer: resolution=merge-duplicates` header: @@ -1234,11 +1234,31 @@ You can make an UPSERT with :code:`POST` and the :code:`Prefer: resolution=merge { "id": 3, "name": "New employee 3", "salary": 50000 } ] -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`. UPSERT 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 `_. +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 `_. .. 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:: http + + POST /employees?on_conflict=name HTTP/1.1 + Prefer: resolution=merge-duplicates + + [ + { "name": "Old employee 1", "salary": 40000 }, + { "name": "Old employee 2", "salary": 52000 }, + { "name": "New employee 3", "salary": 60000 } + ] + +PUT +~~~ A single row UPSERT can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: @@ -1252,7 +1272,7 @@ All the columns must be specified in the request body, including the primary key .. note:: - This feature is only available starting from PostgreSQL 9.5 since it uses the `ON CONFLICT clause `_. + Upsert features are only available starting from PostgreSQL 9.5 since it uses the `ON CONFLICT clause `_. .. _delete: diff --git a/releases/upcoming.rst b/releases/upcoming.rst index de5e751cd5..858861f46f 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -10,6 +10,9 @@ These are changes yet unreleased. If you'd like to try them out before a new off Added ----- +* Support for the :ref:`on_conflict ` query parameter to UPSERT based on a unique constraint. + |br| -- `@ykst `_ + * Support for :ref:`planned_count` and :ref:`estimated_count`. |br| -- `@steve-chavez `_ From 66e0e88539ffab842f4dce4111e40f835008f367 Mon Sep 17 00:00:00 2001 From: H20-17 <51759305+H20-17@users.noreply.github.com> Date: Tue, 18 Feb 2020 17:04:17 -0500 Subject: [PATCH 308/549] Clarify function privileges section (#303) * Add best_practices.rst file * Move function privileges to best_practices.rst --- api.rst | 27 +-------------------------- best_practices.rst | 22 ++++++++++++++++++++++ index.rst | 7 +++++++ 3 files changed, 30 insertions(+), 26 deletions(-) create mode 100644 best_practices.rst diff --git a/api.rst b/api.rst index fb478dc12c..0cb77ee176 100644 --- a/api.rst +++ b/api.rst @@ -1052,32 +1052,6 @@ A function that returns a table type response can be shaped using the same filte GET /rpc/best_films_2017?rating=gt.8&order=title.desc HTTP/1.1 -.. _func_privs: - -Function privileges -------------------- - -By default, 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. - -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. See `PostgreSQL documentation `_ for more details. - -.. warning:: - - Unlike tables/views, functions privileges work as a blacklist, so they're executable for all the roles by default. You can workaround this by revoking the PUBLIC privileges of the function and then granting privileges to specific roles: - - .. code-block:: postgres - - REVOKE ALL PRIVILEGES ON FUNCTION private_func() FROM PUBLIC; - GRANT EXECUTE ON FUNCTION private_func() TO a_role; - - Also to avoid doing ``REVOKE`` on every function you can enable this behavior by default with: - - .. code-block:: postgres - - ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; - - See `PostgreSQL alter default privileges `_ for more details. - Overloaded functions -------------------- @@ -1591,3 +1565,4 @@ PostgREST translates `PostgreSQL error codes `_ for more details. + +The foregoing example may not be appropriate in all situations. For instance you may have a situation where different functions are intended to be called by different roles. In that case you will `not` want to grant `EXECUTE` to one specific role by default. Instead you will want to manually grant executability on a case by case basis. + +By default, 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. + +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. See `PostgreSQL documentation `_ for more details. + diff --git a/index.rst b/index.rst index eb7f581f91..dadf693228 100644 --- a/index.rst +++ b/index.rst @@ -150,9 +150,16 @@ Explanations of some key concepts in PostgREST. admin.rst +.. toctree:: + :caption: Best Practices + :hidden: + + best_practices.rst + - :doc:`Authentication ` - :doc:`Installation ` - :doc:`Administration ` +- :doc:`Best Practices ` Ecosystem --------- From 0d3dc8509c845b54a35e0e252d501bca45c7de46 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Thu, 12 Mar 2020 10:39:15 -0500 Subject: [PATCH 309/549] Add sompani to production list (#306) --- index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/index.rst b/index.rst index dadf693228..75f5c5bc1c 100644 --- a/index.rst +++ b/index.rst @@ -192,6 +192,7 @@ In Production Here are some companies that use PostgREST in production. +* `Sompani `_ * `Datrium `_ * `Nimbus `_ - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. From a34353653904311fb21d3033f25ced88f07baaf2 Mon Sep 17 00:00:00 2001 From: yang <27681135@qq.com> Date: Fri, 13 Mar 2020 01:58:07 +0800 Subject: [PATCH 310/549] Add type basic_auth.jwt_token. (#308) Or the function login will show error. --- auth.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/auth.rst b/auth.rst index 4b770cb24e..fc05b28226 100644 --- a/auth.rst +++ b/auth.rst @@ -406,6 +406,11 @@ As described in `JWT from SQL`_, we'll create a JWT inside our login function. N .. 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 $$ From 334dda611c4230c3c3758c4b051b14eacf294077 Mon Sep 17 00:00:00 2001 From: Patrik Keller Date: Tue, 17 Mar 2020 18:59:40 +0100 Subject: [PATCH 311/549] Tutorial: Providing images for (#307) --- how-tos/providing-images-for-img.rst | 126 +++++++++++++++++++++++++++ index.rst | 1 + 2 files changed, 127 insertions(+) create mode 100644 how-tos/providing-images-for-img.rst diff --git a/how-tos/providing-images-for-img.rst b/how-tos/providing-images-for-img.rst new file mode 100644 index 0000000000..8bb5100874 --- /dev/null +++ b/how-tos/providing-images-for-img.rst @@ -0,0 +1,126 @@ +Providing images for +========================== + +:author: `pkel `_ + +In this how-to, you will learn how to create an endpoint for providing images to HTML :code:`` tags without client side javascript. +The resulting HTML might look like this: + +.. code-block:: html + + Cute Kittens + +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 are more detailed solution that fixes a few shortcomings of the first approach. + +Minimal Example +--------------- + +PostgREST returns binary data on requests that set the :code:`Accept: application/octet-stream` header. +The general idea is to configure the reverse proxy in front of the API to set this header for all requests to :code:`/files/`. +We will show how to achieve this using Nginx. + +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 requesting :code:`/files?select=blob&id=eq.42` with the :code:`Accept: application/octet-stream` header. +Unfortunately, putting the URL into the :code:`src` of an :code:`` tag will not work. +That's because browsers do not send the required header. + +Luckily, we can configure our `Nginx reverse proxy <../admin.html>`_ to fix this problem for us. +We assume that PostgREST is running on port 3000. +We provide a new location :code:`/files/` that redirects requests to our endpoint with the :code:`Accept` header set to :code:`application/octet-stream`. + +.. code-block:: nginx + + server { + # rest of reverse proxy and web server configuration + ... + + location /files/ { + # /files//* ---> /files?select=blob&id=eq. + rewrite /files/([^/]+).* /files?select=blob&id=eq.$1 break; + # if id is missing + return 404; + # request binary output + proxy_set_header Accept application/octet-stream; + # usual proxy setup + 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://localhost:3000/; + } + +With this setup, we can request the cat image at :code:`localhost/files/42/cats.jpeg` without setting any headers. +In fact, you can replace :code:`cats.jpeg` with any other filename or simply omit it. +Putting the URL into the :code:`src` of an :code:`` tag should now work as expected. + +Improved Version +---------------- + +The basic solution has some shortcomings: + +1. The response :code:`Content-Type` header is set to :code:`application/octet-stream`. + This might confuse clients and users. +2. Download requests (e.g. Right Click -> Save Image As) to :code:`files/42` will propose :code:`42` 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, we store the media types and names of our files in the database. + +.. code-block:: postgres + + create table files( + id int primary key + , type text + , name text + , blob bytea + ); + +Next, we set up an RPC endpoint that sets 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 reverse proxy. + +.. code-block:: postgres + + create function file(id int) returns bytea 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 + 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`. +Consequently, we have to replace our previous rewrite rule in the Nginx recipe with the following. + +.. code-block:: nginx + + rewrite /files/([^/]+).* /rpc/file?id=$1 break; diff --git a/index.rst b/index.rst index 75f5c5bc1c..0f7deaea99 100644 --- a/index.rst +++ b/index.rst @@ -126,6 +126,7 @@ These are recipes that'll help you address specific use-cases. - :doc:`how-tos/casting-type-to-custom-json` - :doc:`how-tos/embedding-table-from-another-schema` +- :doc:`how-tos/providing-images-for-img` Topic guides ------------ From e23b7afa3c089201438fad08c6e1a571d60ded39 Mon Sep 17 00:00:00 2001 From: Duncan Ogilvie Date: Mon, 23 Mar 2020 17:48:09 +0100 Subject: [PATCH 312/549] Fix the 'Roles for Each Web User' example (#313) Add missing `ALTER TABLE ... ENABLE ROW LEVEL SECURITY;` --- auth.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/auth.rst b/auth.rst index fc05b28226..b762bc0210 100644 --- a/auth.rst +++ b/auth.rst @@ -60,6 +60,8 @@ You can use row-level security to flexibly restrict visibility and access for th 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 him or intended for him. Also we want to prevent a user from forging the message_from column with another person's name. @@ -95,7 +97,7 @@ SQL code can access claims through GUC variables set by PostgREST per request. F 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. +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 ~~~~~~~~~~~~~~~~~~~~~~~ From 36c453d6dedfd816d887d92a4159cd18e90e3137 Mon Sep 17 00:00:00 2001 From: Sam Khawase <215221+samkhawase@users.noreply.github.com> Date: Mon, 30 Mar 2020 19:48:20 +0200 Subject: [PATCH 313/549] Added the link to tutorial to configure and integrate Auth0 to PostgREST (#312) --- ecosystem.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ecosystem.rst b/ecosystem.rst index f3e230f537..feb09410e6 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -9,6 +9,8 @@ Community Tutorials * `PostgREST + PostGIS API tutorial in 5 minutes `_ - In this tutorial, GIS • OPS shows how to perform PostGIS calculations through PostgREST :ref:`s_procs` interface. +* `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. + .. _eco_example_apps: Example Apps From b7ee8f1cf0e2705a143fc246702d9a999278ceae Mon Sep 17 00:00:00 2001 From: fiatjaf Date: Wed, 1 Apr 2020 13:00:05 -0300 Subject: [PATCH 314/549] mention that JSON operators work for row filtering (#314) --- api.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/api.rst b/api.rst index 0cb77ee176..d05788ce22 100644 --- a/api.rst +++ b/api.rst @@ -200,17 +200,20 @@ You can specify a path for a ``json`` or ``jsonb`` column using the arrow operat GET /people?select=id,json_data->>blood_type,json_data->phones HTTP/1.1 [ - { "id": 1, "blood_type": "A+", "phones": [{"country_code": "61", "number": "917-929-5745"}] }, + { "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"}] } ] +That also works with filters: + .. code-block:: http - GET /people?select=id,json_data->phones->0->>number HTTP/1.1 + GET /people?select=id,json_data->blood_type&json_data->>blood_type=eq.A- HTTP/1.1 [ - { "id": 1, "number": "917-929-5745"}, - { "id": 2, "number": "512-446-4988"} + { "id": 1, "blood_type": "A-" }, + { "id": 3, "blood_type": "A-" }, + { "id": 7, "blood_type": "A-" } ] .. _computed_cols: From 9f5ab4a8ba6909a85a5e4b496ffd6b7f876c44b9 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 1 Apr 2020 11:35:38 -0500 Subject: [PATCH 315/549] Add reference for filtering on json column Fixes https://github.com/PostgREST/postgrest-docs/issues/266 --- api.rst | 25 ++++++++++++++++++++++++- releases/upcoming.rst | 6 ++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/api.rst b/api.rst index d05788ce22..f1a71d1233 100644 --- a/api.rst +++ b/api.rst @@ -190,6 +190,8 @@ Casting the columns is possible by suffixing them with the double colon ``::`` p {"full_name": "Jane Doe", "salary": "120000.00"} ] +.. _json_columns: + JSON Columns ~~~~~~~~~~~~ @@ -204,7 +206,16 @@ You can specify a path for a ``json`` or ``jsonb`` column using the arrow operat { "id": 2, "blood_type": "O+", "phones": [{"country_code": "43", "number": "512-446-4988"}, {"country_code": "43", "number": "213-891-5979"}] } ] -That also works with filters: +.. code-block:: http + + GET /people?select=id,json_data->phones->0->>number HTTP/1.1 + + [ + { "id": 1, "number": "917-929-5745"}, + { "id": 2, "number": "512-446-4988"} + ] + +This also works with filters: .. code-block:: http @@ -216,6 +227,18 @@ That also works with filters: { "id": 7, "blood_type": "A-" } ] +Note that ``->>`` is used to compare ``blood_type`` as ``text``. To compare with an integer value use ``->``: + +.. code-block:: http + + GET /people?select=id,json_data->age&json_data->age=gt.20 HTTP/1.1 + + [ + { "id": 11, "age": 25 }, + { "id": 12, "age": 30 }, + { "id": 15, "age": 35 } + ] + .. _computed_cols: Computed Columns diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 858861f46f..d896b201ba 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -30,9 +30,11 @@ Added + Allow overriding provided headers(Content-Type, Location, etc) by :ref:`guc_resp_hdrs` + Access to the ``Authorization`` header value through ``request.header.authorization`` -* Documentation reference for :ref:`s_proc_embed`. +* Documentation improvements -* Documentation reference for :ref:`mutation_embed`. + + Reference for :ref:`s_proc_embed`. + + Reference for :ref:`mutation_embed`. + + Reference for filters on :ref:`json_columns`. Fixed ----- From dd7e182b12853ca069f8a55303bdf9189b0d6623 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 3 Apr 2020 13:57:31 -0500 Subject: [PATCH 316/549] Update config.py version/release Fixes https://github.com/PostgREST/postgrest-docs/issues/260 --- conf.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/conf.py b/conf.py index f1f263a9eb..80008bf49a 100644 --- a/conf.py +++ b/conf.py @@ -46,17 +46,17 @@ # General information about the project. project = u'PostgREST' -copyright = u'2017, Joe Nelson' -author = u'Joe Nelson' +author = u'Joe Nelson, Steve Chavez' +copyright = u'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 = u'5.0' +version = u'7.0' # The full version, including alpha/beta/rc tags. -release = u'5.0.0' +release = u'7.0.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -225,7 +225,7 @@ # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'PostgREST.tex', u'PostgREST Documentation', - u'Joe Nelson', 'manual'), + author, 'manual'), ] # The name of an image file (relative to this directory) to place at the top of From 33768c509ca9b64098a19ba5b2efc3913e1644e4 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 6 Apr 2020 14:00:30 -0500 Subject: [PATCH 317/549] Add reference for rpc with array literals Fixes https://github.com/PostgREST/postgrest-docs/issues/258 --- api.rst | 103 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 63 insertions(+), 40 deletions(-) diff --git a/api.rst b/api.rst index f1a71d1233..88b4134dc7 100644 --- a/api.rst +++ b/api.rst @@ -912,16 +912,6 @@ For instance, assume we have created this function in the database. SELECT a + b; $$ LANGUAGE SQL IMMUTABLE; -The client can call it by posting an object like - -.. code-block:: http - - POST /rpc/add_them HTTP/1.1 - - { "a": 1, "b": 2 } - - 3 - .. important:: Whenever you create or change a function you must refresh PostgREST's schema cache. See the section :ref:`schema_reloading`. @@ -938,25 +928,18 @@ The client can call it by posting an object like "message":"function test.add_them(a => text, b => text) does not exist" } -You can also call a function that takes a single parameter of type json by sending the header :code:`Prefer: params=single-object` with your request. That way the JSON request body will be used as the single argument. - -.. code-block:: plpgsql - - CREATE FUNCTION mult_them(param json) RETURNS int AS $$ - SELECT (param->>'x')::int * (param->>'y')::int - $$ LANGUAGE SQL; +The client can call it by posting an object like .. code-block:: http - POST /rpc/mult_them HTTP/1.1 - Prefer: params=single-object + POST /rpc/add_them HTTP/1.1 - { "x": 4, "y": 2 } + { "a": 1, "b": 2 } - 8 + 3 -Procedures must be declared with named parameters, procedures declared like: +Procedures must be declared with named parameters. Procedures declared like: .. code-block:: plpgsql @@ -974,44 +957,84 @@ PostgreSQL has four procedural languages that are part of the core distribution: .. note:: - For versions prior to PostgreSQL 10, to pass a PostgreSQL native array you need to quote it as a string: + Why the `/rpc` prefix? One reason is to avoid name collisions between views and procedures. It also helps emphasize to API consumers that these functions are not normal restful things. The functions can have arbitrary and surprising behavior, not the standard "post creates a resource" thing that users expect from the other routes. - .. code-block:: http +Immutable and stable functions +------------------------------ - POST /rpc/native_array_func HTTP/1.1 +Procedures in PostgreSQL marked with :code:`stable` or :code:`immutable` `volatility `_ can only read, not modify, the database and PostgREST executes them in a read-only transaction compatible for read-replicas. Stable and immutable functions can be called with the HTTP GET verb if desired. - { "arg": "{1,2,3}" } +.. note:: - In these versions we recommend using function arguments of type json to accept arrays from the client: + 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/stable`` without failure. However the function will fail when called through PostgREST since it executes it in a read-only transaction. - .. code-block:: http +Because ``add_them`` was declared IMMUTABLE, we can alternately call the function with a GET request: - POST /rpc/json_array_func HTTP/1.1 +.. code-block:: http - { "arg": [1,2,3] } + GET /rpc/add_them?a=1&b=2 HTTP/1.1 - Starting from PostgreSQL 10, a json array from the client gets mapped normally to a PostgreSQL native array. +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``. -.. note:: +Calling functions with a single json parameter +---------------------------------------------- - Why the `/rpc` prefix? One reason is to avoid name collisions between views and procedures. It also helps emphasize to API consumers that these functions are not normal restful things. The functions can have arbitrary and surprising behavior, not the standard "post creates a resource" thing that users expect from the other routes. +You can also call a function that takes a single parameter of type json by sending the header :code:`Prefer: params=single-object` with your request. That way the JSON request body will be used as the single argument. -Immutable and stable functions ------------------------------- +.. code-block:: plpgsql -Procedures in PostgreSQL marked with :code:`stable` or :code:`immutable` `volatility `_ can only read, not modify, the database and PostgREST executes them in a read-only transaction compatible for read-replicas. Stable and immutable functions can be called with the HTTP GET verb if desired. + CREATE FUNCTION mult_them(param json) RETURNS int AS $$ + SELECT (param->>'x')::int * (param->>'y')::int + $$ LANGUAGE SQL; + +.. code-block:: http + + POST /rpc/mult_them HTTP/1.1 + Prefer: params=single-object + + { "x": 4, "y": 2 } + + 8 + +Calling functions with array parameters +--------------------------------------- + +You can call a function that takes an array parameter: + +.. code-block:: plpgsql + + CREATE FUNCTION native_array_func(arr int[]) RETURNS int[] as $$ + SELECT arr; + $$ LANGUAGE SQL; + +.. code-block:: http + + POST /rpc/native_array_func HTTP/1.1 + + { "arg": [1,2,3] } + + [1,2,3] .. 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/stable`` without failure. However the function will fail when called through PostgREST since it executes it in a read-only transaction. + For versions prior to PostgreSQL 10, to pass a PostgreSQL native array you need to quote it as a string: -Because ``add_them`` was declared IMMUTABLE, we can alternately call the function with a GET request: + .. code-block:: http + + POST /rpc/native_array_func HTTP/1.1 + + { "arg": "{1,2,3}" } + + In these versions we recommend using function parameters of type json to accept arrays from the client. + +For calling it with GET, you can pass the array as an `array literal `_; +as in ``{1,2,3}``. Note that the curly brackets have to be urlencoded(``{`` is ``%7B`` and ``}`` is ``%7D``). .. code-block:: http - GET /rpc/add_them?a=1&b=2 HTTP/1.1 + GET /rpc/native_array_func?arr=%7B1,2,3%7D' HTTP/1.1 -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``. + [1,2,3] Scalar functions ---------------- From 3292fce732bc1dff9647c2cad9aba2e7b6bfb9f4 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 7 Apr 2020 14:37:40 -0500 Subject: [PATCH 318/549] Update schema reloading section Fixes https://github.com/PostgREST/postgrest-docs/issues/243 --- admin.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/admin.rst b/admin.rst index a192b304b4..17486b155a 100644 --- a/admin.rst +++ b/admin.rst @@ -187,12 +187,28 @@ Schema Reloading Users are often confused by PostgREST's database schema cache. It is present because detecting foreign key relationships between tables (including how those relationships pass through views) is necessary, but costly. API requests consult the schema cache as part of :ref:`resource_embedding`. However if the schema changes while the server is running it results in a stale cache and leads to errors claiming that no relations are detected between tables. +.. important:: + + Since v5.0, PostgREST also makes use of the schema cache for stored functions metadata: parameters, return type, volatility. + It also uses the schema cache for resolving overloaded functions. You should refresh the cache if a change in any of the prior is done. + To refresh the cache without restarting the PostgREST server, send the server process a SIGUSR1 signal: .. code:: bash killall -SIGUSR1 postgrest +.. note:: + + To refresh the cache in docker: + + .. code:: bash + + docker kill -s SIGUSR1 + + # or in docker-compose + docker-compose kill -s SIGUSR1 + The above is the manual way to do it. To automate the schema reloads, use a database trigger like this: .. code-block:: postgresql From 87f883b7932940941dd174fe92c4712a7c7e4d92 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 11 Apr 2020 17:40:33 -0500 Subject: [PATCH 319/549] Add multiple schemas feature --- api.rst | 34 ++++++++++++++++++++++++++++++++++ configuration.rst | 14 +++++++++++++- releases/upcoming.rst | 9 ++++++--- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/api.rst b/api.rst index 88b4134dc7..9d06e45c8f 100644 --- a/api.rst +++ b/api.rst @@ -1371,6 +1371,8 @@ You can get raw output from a ``text`` column by using ``Accept: text/plain``. This follows the same rules as :ref:`binary_output`. +.. _open-api: + OpenAPI Support =============== @@ -1406,6 +1408,38 @@ You can use a tool like `Swagger UI `_ to create The OpenAPI information can go out of date as the schema changes under a running server. To learn how to refresh the cache see :ref:`schema_reloading`. +.. _multiple-schemas: + +Switching Schemas +================= + +You can switch schemas at runtime with the ``Accept-Profile`` and ``Content-Profile`` headers. You can only switch to a schema that is included in :ref:`db-schema`. +This is useful for **api versioning** and **schema-based multitenancy**. + +The schema to be used can be selected through the ``Accept-Profile`` header for GET or HEAD: + +.. code-block:: http + + GET /items HTTP/1.1 + Accept-Profile: tenant2 + +If you don't specify the ``Accept-Profile`` header, the first schema on :ref:`db-schema` will be used. + +For POST, PATCH, PUT, DELETE you can use the ``Content-Profile`` header for selecting the schema: + +.. code-block:: http + + POST /items HTTP/1.1 + Content-Profile: tenant2 + + {...} + +You can also select the schema for :ref:`s_procs` and :ref:`open-api`. + +.. note:: + + These headers are based on the nascent "Content Negotiation by Profile" spec: https://www.w3.org/TR/dx-prof-conneg + HTTP Logic ========== diff --git a/configuration.rst b/configuration.rst index fe9b9f7f26..35d93d3083 100644 --- a/configuration.rst +++ b/configuration.rst @@ -75,7 +75,19 @@ db-schema The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. - This schema gets added to the `search_path `_ of every request. + The chosen schema gets added to the `search_path `_ of every request. Example: + + .. code:: bash + + db-schema = "api" + + You can also specify a list of schemas that can be used for **schema-based multitenancy** and **api versioning** by :ref:`multiple-schemas`. Example: + + .. code:: bash + + db-schema = "tenant1, tenant2" + ##or + ##db-schema = "v1, v2" .. _db-anon-role: diff --git a/releases/upcoming.rst b/releases/upcoming.rst index d896b201ba..5fa71b0f2a 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -10,11 +10,14 @@ These are changes yet unreleased. If you'd like to try them out before a new off Added ----- -* Support for the :ref:`on_conflict ` query parameter to UPSERT based on a unique constraint. - |br| -- `@ykst `_ +* Support for :ref:`multiple-schemas` at runtime. + |br| -- `@steve-chavez `_, `@mahmoudkassem `_ * Support for :ref:`planned_count` and :ref:`estimated_count`. - |br| -- `@steve-chavez `_ + |br| -- `@steve-chavez `_, `@LorenzHenk `_ + +* Support for the :ref:`on_conflict ` query parameter to UPSERT based on a unique constraint. + |br| -- `@ykst `_ * Support for :ref:`Resource Embedding Disambiguation `. |br| -- `@steve-chavez `_ From 467b22cb298619ddf03e890e502fc4033da7de11 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 13 Apr 2020 15:02:40 -0500 Subject: [PATCH 320/549] Fix required pg minimum version Fixes https://github.com/PostgREST/postgrest-docs/issues/317 --- install.rst | 4 +++- tutorials/tut0.rst | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/install.rst b/install.rst index 685d282354..0e0e2ae700 100644 --- a/install.rst +++ b/install.rst @@ -24,11 +24,13 @@ The release page has pre-compiled binaries for Mac OS X, Windows, and several Li It usually lives in :code:`C:\Program Files\PostgreSQL\\bin`. See this `article `_ about how to modify the system path. +.. _pg-dependency: PostgreSQL dependency ===================== -To use PostgREST you will need an underlying database (PostgreSQL version 9.5 or greater is required). You can use something like Amazon `RDS `_ but installing your own locally is cheaper and more convenient for development. +To use PostgREST you will need an underlying database. We require PostgreSQL 9.4 or greater, but recommend at least 9.5 for row-level security features. +You can use something like Amazon `RDS `_ but installing your own locally is cheaper and more convenient for development. * `Instructions for OS X `_ * `Instructions for Ubuntu 14.04 `_ diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index 038b92f6fc..18884ea5f5 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -23,9 +23,7 @@ As you begin the tutorial, pop open the project `chat room `_. Next, let's pull and start the database image: From a5af8dc68fa31854e1cb199ddfcef54178f4b254 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 14 Apr 2020 11:35:32 -0500 Subject: [PATCH 321/549] Add return=rep to DELETE section --- api.rst | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 9d06e45c8f..03c97c96b3 100644 --- a/api.rst +++ b/api.rst @@ -1304,14 +1304,24 @@ Deletions To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instance deleting inactive users: -.. code-block:: HTTP +.. code-block:: http DELETE /user?active=is.false HTTP/1.1 +Deletions also support :code:`Prefer: return=representation` plus :ref:`v_filter`. + +.. code-block:: HTTP + + DELETE /user?id=eq.1 HTTP/1.1 + Prefer: return=representation + + {"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`. + .. _binary_output: Binary Output From 4d623986eadb65494c2c8a094bf805b5bcf22295 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 14 Apr 2020 13:24:48 -0500 Subject: [PATCH 322/549] Alternative to bulk call --- api.rst | 30 +++++++++++++++++++++++------- releases/upcoming.rst | 2 +- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/api.rst b/api.rst index 03c97c96b3..380aaf6937 100644 --- a/api.rst +++ b/api.rst @@ -1068,20 +1068,36 @@ It's possible to call a function in a bulk way, analoguosly to :ref:`bulk_insert .. code-block:: http POST /rpc/add_them HTTP/1.1 - Content-Type: application/json + Content-Type: text/csv Prefer: params=multiple-objects - [ - {"a": 1, "b": 2}, - {"a": 3, "b": 4} - ] - -Result: + a,b + 1,2 + 3,4 .. code-block:: json [ 3, 7 ] +If you have large payloads to process, it's preferrable you instead use a function with an array or json parameter, as this will be more efficient. + +.. 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:: http + + POST /rpc/plus_one HTTP/1.1 + Content-Type: application/json + + {"arr": [1,2,3,4]} + +.. code-block:: json + + [2,3,4,5] + It's also possible to :ref:`Specify Columns ` on functions calls. Function filters diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 5fa71b0f2a..8a77c509be 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -60,7 +60,7 @@ Fixed Changed ------- -* :ref:`bulk_call` should now be done by specifying a ``Prefer: params=multiple-objects`` header. +* :ref:`bulk_call` should now be done by specifying a ``Prefer: params=multiple-objects`` header. This fixes a performance regression when calling stored procedures. * Resource Embedding now outputs an error when multiple relationships between two tables are found, see :ref:`embed_disamb`. From 750c22cd1cb577010512d0224034d8020a1ae1de Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 16 Apr 2020 12:35:57 -0500 Subject: [PATCH 323/549] Add mounting config file to docker --- install.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install.rst b/install.rst index 0e0e2ae700..415757f456 100644 --- a/install.rst +++ b/install.rst @@ -78,6 +78,12 @@ These variables match the options shown in our :ref:`configuration` section, exc docker inspect -f "{{.Config.Env}}" postgrest/postgrest +You can also specify a config file by mounting the file to the container: + +.. code-block:: bash + + docker run -v /absolute/path/to/config:/etc/postgrest.conf postgrest/postgrest + There are two ways to run the PostgREST container: with an existing external database, or through docker-compose. Containerized PostgREST with native PostgreSQL From c2ff7ffb622cb660900285d9ede8215dba067a09 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 16 Apr 2020 13:26:02 -0500 Subject: [PATCH 324/549] Redorder insertions before stored procedures --- api.rst | 411 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 205 insertions(+), 206 deletions(-) diff --git a/api.rst b/api.rst index 380aaf6937..55dae2a3f5 100644 --- a/api.rst +++ b/api.rst @@ -400,7 +400,7 @@ This method is also useful for embedded resources, which we will cover in anothe .. _exact_count: Exact Count -~~~~~~~~~~~ +----------- In order to obtain the total size of the table or view (such as when rendering the last page link in a pagination control), specify ``Prefer: count=exact`` as a request header: @@ -422,7 +422,7 @@ Note that the larger the table the slower this query runs in the database. The s .. _planned_count: Planned Count -~~~~~~~~~~~~~ +------------- To avoid the shortcomings of :ref:`exact count `, PostgREST can leverage PostgreSQL statistics and get a fairly accurate and fast count. To do this, specify the ``Prefer: count=planned`` header. @@ -444,7 +444,7 @@ See `ANALYZE `_ for more de .. _estimated_count: Estimated Count -~~~~~~~~~~~~~~~ +--------------- When you are interested in the count, the relative error is important. If you have a :ref:`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. @@ -876,6 +876,208 @@ Here we specify ``central_addresses`` as the **target** and the ``billing_addres Similarly to the **target**, the **hint** can be a **table name**, **foreign key constraint name** or **column name**. +.. _insert_update: + +Insertions / Updates +==================== + +All tables and `auto-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:: HTTP + + POST /table_name HTTP/1.1 + + { "col1": "value1", "col2": "value2" } + +The response will include a :code:`Location` header describing where to find the new object. If the table is write-only then constructing the Location header will cause a permissions error. To successfully insert an item to a write-only table you will need to suppress the Location response header by including the request header :code:`Prefer: return=minimal`. + +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. + +URL encoded payloads can be posted with ``Content-Type: application/x-www-form-urlencoded``. + +.. code-block:: http + + POST /people HTTP/1.1 + Content-Type: application/x-www-form-urlencoded + + 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. + +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:: http + + PATCH /people?age=lt.13 HTTP/1.1 + + { "category": "child" } + +Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. + +.. warning:: + + Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`. + +.. warning:: + + Insertion on VIEWs with complex `RULEs `_ might not work out of the box with PostgREST. + It's recommended that you `use triggers instead of RULEs `_. + 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. + +.. _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. Note that using CSV requires less parsing on the server and is much faster. + +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:: http + + POST /people HTTP/1.1 + Content-Type: text/csv + + name,age,height + J Doe,62,70 + Jonas,10,55 + +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:: http + + POST /people HTTP/1.1 + Content-Type: application/json + + [ + { "name": "J Doe", "age": 62, "height": 70 }, + { "name": "Janus", "age": 10, "height": 55 } + ] + +.. _specify_columns: + +Specifying Columns +------------------ + +By using the :code:`columns` query parameter it's possible to specify the payload keys that will be inserted/updated +and ignore the rest of the payload. + +.. code-block:: http + + POST /datasets?columns=source,publication_date,figure HTTP/1.1 + Content-Type: application/json + + { + "source": "Natural Disaster Prevention and Control", + "publication_date": "2015-09-11", + "figure": 1100, + "location": "...", + "comment": "...", + "extra": "...", + "stuff": "..." + } + +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. + +UPSERT +------ + +You can make an UPSERT with :code:`POST` and the :code:`Prefer: resolution=merge-duplicates` header: + +.. code-block:: http + + POST /employees HTTP/1.1 + Prefer: resolution=merge-duplicates + + [ + { "id": 1, "name": "Old employee 1", "salary": 30000 }, + { "id": 2, "name": "Old employee 2", "salary": 42000 }, + { "id": 3, "name": "New employee 3", "salary": 50000 } + ] + +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 `_. + +.. 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:: http + + POST /employees?on_conflict=name HTTP/1.1 + Prefer: resolution=merge-duplicates + + [ + { "name": "Old employee 1", "salary": 40000 }, + { "name": "Old employee 2", "salary": 52000 }, + { "name": "New employee 3", "salary": 60000 } + ] + +PUT +~~~ + +A single row UPSERT can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: + +.. code-block:: http + + PUT /employees?id=eq.4 HTTP/1.1 + + { "id": 4, "name": "Sara B.", "salary": 60000 } + +All the columns must be specified in the request body, including the primary key columns. + +.. note:: + + Upsert features are only available starting from PostgreSQL 9.5 since it uses the `ON CONFLICT clause `_. + +.. _delete: + +Deletions +========= + +To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instance deleting inactive users: + +.. code-block:: http + + DELETE /user?active=is.false HTTP/1.1 + +Deletions also support :code:`Prefer: return=representation` plus :ref:`v_filter`. + +.. code-block:: HTTP + + DELETE /user?id=eq.1 HTTP/1.1 + Prefer: return=representation + + {"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`. + .. _custom_queries: Custom Queries @@ -886,7 +1088,6 @@ The PostgREST URL grammar limits the kinds of queries clients can perform. It pr * Table unions * More complicated joins than those provided by `Resource Embedding`_ * Geo-spatial queries that require an argument, like "points near (lat,lon)" -* More sophisticated full-text search than a simple use of the :sql:`fts` filter .. _s_procs: @@ -1135,208 +1336,6 @@ You can call overloaded functions with different number of arguments. .. code-block:: http GET /rpc/rental_duration?customer_id=232&from_date=2018-07-01 HTTP/1.1 -.. _insert_update: - -Insertions / Updates -==================== - -All tables and `auto-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:: HTTP - - POST /table_name HTTP/1.1 - - { "col1": "value1", "col2": "value2" } - -The response will include a :code:`Location` header describing where to find the new object. If the table is write-only then constructing the Location header will cause a permissions error. To successfully insert an item to a write-only table you will need to suppress the Location response header by including the request header :code:`Prefer: return=minimal`. - -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. - -URL encoded payloads can be posted with ``Content-Type: application/x-www-form-urlencoded``. - -.. code-block:: http - - POST /people HTTP/1.1 - Content-Type: application/x-www-form-urlencoded - - 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. - -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:: http - - PATCH /people?age=lt.13 HTTP/1.1 - - { "category": "child" } - -Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. - -.. warning:: - - Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`. - -.. warning:: - - Insertion on VIEWs with complex `RULEs `_ might not work out of the box with PostgREST. - It's recommended that you `use triggers instead of RULEs `_. - 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. - -.. _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. Note that using CSV requires less parsing on the server and is much faster. - -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:: http - - POST /people HTTP/1.1 - Content-Type: text/csv - - name,age,height - J Doe,62,70 - Jonas,10,55 - -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:: http - - POST /people HTTP/1.1 - Content-Type: application/json - - [ - { "name": "J Doe", "age": 62, "height": 70 }, - { "name": "Janus", "age": 10, "height": 55 } - ] - -.. _specify_columns: - -Specifying Columns ------------------- - -By using the :code:`columns` query parameter it's possible to specify the payload keys that will be inserted/updated -and ignore the rest of the payload. - -.. code-block:: http - - POST /datasets?columns=source,publication_date,figure HTTP/1.1 - Content-Type: application/json - - { - "source": "Natural Disaster Prevention and Control", - "publication_date": "2015-09-11", - "figure": 1100, - "location": "...", - "comment": "...", - "extra": "...", - "stuff": "..." - } - -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. - -UPSERT ------- - -You can make an UPSERT with :code:`POST` and the :code:`Prefer: resolution=merge-duplicates` header: - -.. code-block:: http - - POST /employees HTTP/1.1 - Prefer: resolution=merge-duplicates - - [ - { "id": 1, "name": "Old employee 1", "salary": 30000 }, - { "id": 2, "name": "Old employee 2", "salary": 42000 }, - { "id": 3, "name": "New employee 3", "salary": 50000 } - ] - -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 `_. - -.. 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:: http - - POST /employees?on_conflict=name HTTP/1.1 - Prefer: resolution=merge-duplicates - - [ - { "name": "Old employee 1", "salary": 40000 }, - { "name": "Old employee 2", "salary": 52000 }, - { "name": "New employee 3", "salary": 60000 } - ] - -PUT -~~~ - -A single row UPSERT can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: - -.. code-block:: http - - PUT /employees?id=eq.4 HTTP/1.1 - - { "id": 4, "name": "Sara B.", "salary": 60000 } - -All the columns must be specified in the request body, including the primary key columns. - -.. note:: - - Upsert features are only available starting from PostgreSQL 9.5 since it uses the `ON CONFLICT clause `_. - -.. _delete: - -Deletions -========= - -To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instance deleting inactive users: - -.. code-block:: http - - DELETE /user?active=is.false HTTP/1.1 - -Deletions also support :code:`Prefer: return=representation` plus :ref:`v_filter`. - -.. code-block:: HTTP - - DELETE /user?id=eq.1 HTTP/1.1 - Prefer: return=representation - - {"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`. - .. _binary_output: From 6b72da38e622e4488378fe7e681c4084568ebb69 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 16 Apr 2020 18:01:07 -0500 Subject: [PATCH 325/549] Move some api notes to best practices --- api.rst | 18 ------------------ best_practices.rst | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/api.rst b/api.rst index 55dae2a3f5..fd62a0f92c 100644 --- a/api.rst +++ b/api.rst @@ -103,18 +103,6 @@ The view will provide a new endpoint: GET /fresh_stories HTTP/1.1 -.. important:: - - 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 `_ will be bypassed unless a different, non-SUPERUSER owner is specified. - - .. code-block:: postgres - - -- Workaround: - -- non-SUPERUSER role to be used as the owner of the views - CREATE ROLE api_views_owner; - -- alter the view owner so RLS can work normally - ALTER VIEW sample_view OWNER TO api_views_owner; - .. _fts: Full-Text Search @@ -932,12 +920,6 @@ Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`. Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`. -.. warning:: - - Insertion on VIEWs with complex `RULEs `_ might not work out of the box with PostgREST. - It's recommended that you `use triggers instead of RULEs `_. - 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. - .. _bulk_insert: Bulk Insert diff --git a/best_practices.rst b/best_practices.rst index c8ecdbc092..34e9c473b5 100644 --- a/best_practices.rst +++ b/best_practices.rst @@ -20,3 +20,22 @@ By default, a function is executed with the privileges of the user who calls it. 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. See `PostgreSQL documentation `_ for more details. +Views with RLS +-------------- + +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 `_ will be bypassed unless a different, non-SUPERUSER owner is specified. + +.. code-block:: postgres + + -- Workaround: + -- non-SUPERUSER role to be used as the owner of the views + CREATE ROLE api_views_owner; + -- alter the view owner so RLS can work normally + ALTER VIEW sample_view OWNER TO api_views_owner; + +Views with Rules +---------------- + +Insertion on VIEWs with complex `RULEs `_ might not work out of the box with PostgREST. +It's recommended that you `use triggers instead of RULEs `_. +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. From 5eca88fd87a81bafa5923f5976b689b882fafba1 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 16 Apr 2020 18:01:27 -0500 Subject: [PATCH 326/549] Refine function privileges section --- best_practices.rst | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/best_practices.rst b/best_practices.rst index 34e9c473b5..5e1656a6c7 100644 --- a/best_practices.rst +++ b/best_practices.rst @@ -3,18 +3,30 @@ Function privileges ------------------- -By default, when a function is created, the right to execute it is is not restricted by role, but this probably isn't consistent with best practices for an API design. If you want functions to be executable exclusively by a given role upon their creation, issue psql instructions similar to this: +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 - -- To stop functions from being universally executable upon creation (note the IN SCHEMA part). + -- Assuming your schema is named "api" ALTER DEFAULT PRIVILEGES IN SCHEMA api REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; - -- To grant execution rights for functions to a specific role upon function creation. - ALTER DEFAULT PRIVILEGES IN SCHEMA api GRANT EXECUTE ON FUNCTIONS TO my_role; + + -- Or to stop functions from being executable in the whole database(note the removal of the IN SCHEMA part). + ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; See `PostgreSQL alter default privileges `_ for more details. -The foregoing example may not be appropriate in all situations. For instance you may have a situation where different functions are intended to be called by different roles. In that case you will `not` want to grant `EXECUTE` to one specific role by default. Instead you will want to manually grant executability on a case by case basis. +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 reset_password TO web_user; + + -- you can also GRANT EXECUTE on all functions to a privileged role + GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA api TO admin; + +Security definer +---------------- By default, 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. From 055b7820b35f234c23bb20ef4a069f9c082425ad Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 17 Apr 2020 17:09:40 -0500 Subject: [PATCH 327/549] Add schema structure diagram --- .gitignore | 3 ++ _static/db.png | Bin 0 -> 10150 bytes best_practices.rst | 7 ++++ diagrams/README.md | 30 +++++++++++++++ diagrams/db.tex | 71 ++++++++++++++++++++++++++++++++++++ {erd => diagrams}/film.er | 0 {erd => diagrams}/orders.er | 0 erd/README.md | 7 ---- 8 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 _static/db.png create mode 100644 diagrams/README.md create mode 100644 diagrams/db.tex rename {erd => diagrams}/film.er (100%) rename {erd => diagrams}/orders.er (100%) delete mode 100644 erd/README.md diff --git a/.gitignore b/.gitignore index bf70826eac..00042c2760 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ _build Pipfile.lock +*.aux +*.log +diagrams/db.pdf diff --git a/_static/db.png b/_static/db.png new file mode 100644 index 0000000000000000000000000000000000000000..bd463f2ef2a49d5c0f7a1dfca40cce621d35be3b GIT binary patch literal 10150 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 Date: Mon, 20 Apr 2020 13:36:48 -0500 Subject: [PATCH 328/549] Move HTTPS section from auth to admin It fits better into administration concerns --- admin.rst | 4 ++-- auth.rst | 13 +++---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/admin.rst b/admin.rst index 17486b155a..52bb51a0de 100644 --- a/admin.rst +++ b/admin.rst @@ -87,12 +87,12 @@ This is fine in small tables, but count performance degrades in big tables due t -- Pending nginx config: Remove any prefer header which contains the word count -.. _hardening_https: +.. _https: HTTPS ----- -See the :ref:`https` section of the authentication guide. +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 ------------- diff --git a/auth.rst b/auth.rst index b762bc0210..48de88125e 100644 --- a/auth.rst +++ b/auth.rst @@ -60,7 +60,7 @@ You can use row-level security to flexibly restrict visibility and access for th 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 him or intended for him. Also we want to prevent a user from forging the message_from column with another person's name. @@ -295,13 +295,6 @@ The last type of critique focuses on the misuse of JWT for maintaining web sessi 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. -.. _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. - Schema Isolation ================ @@ -457,8 +450,8 @@ The response would look like the snippet below. Try decoding the token at `jwt.i Permissions ~~~~~~~~~~~ -Your database roles need access to the schema, tables, views and functions in order to service HTTP requests. -Recall from the `Overview of Role System`_ that PostgREST uses special roles to process requests, namely the authenticator and +Your database roles need access to the schema, tables, views and functions in order to service HTTP requests. +Recall from the `Overview of Role System`_ 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 From 79f2af08e301d720694cf7599bbe5772473c9dae Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 20 Apr 2020 18:16:06 -0500 Subject: [PATCH 329/549] Add wording for schema structure --- _static/db.png | Bin 10150 -> 9232 bytes auth.rst | 2 +- best_practices.rst | 20 +++++++++++++------- diagrams/README.md | 12 +++++++++++- diagrams/db.png | Bin 0 -> 9232 bytes diagrams/db.tex | 6 +++--- 6 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 diagrams/db.png diff --git a/_static/db.png b/_static/db.png index bd463f2ef2a49d5c0f7a1dfca40cce621d35be3b..a3dd2d85abeecea82f09e27d0dbbccbd67678397 100644 GIT binary patch delta 6745 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
    ;CI!my`l6(rr4QTLw+sw|48kT>yff#>gNHVa zS7qOs>;2Al<;vsODy9k@tKi1Ki|$LAB26jX=Zy3&Zxe5PuTF3MjR0=`hBk zW`ynB`4C!5(Zi`AFWfWRBKCIa&l&gYuH4E%N}{4-ZgE7&*)$uxU}-0|w6`F-B8u&W zn>UfEJNBBHO!m}r%=WrVZS8l41Vz^VN3mWd#mmG<^4@wFWk^D=xZF~#sycPB$4ee4 z2iosjg`Iu2csuAUKcmh)jhPc79E-9mnG5zDH|tBG#8-$gpy!Pe-vO@aPrcIkx`hoP zRqBt-RH%C(3cVnWI~;Z(@1tXW!)_}sysbI1MK4Hhe%67bRg{SW{#GOb+w4CcpUvI= z&>c^r1=xZ;!dJ%_DE6jFu%n4)o0pT5M}9tNGQW0!qhqp8$@Y3@-m4_jp9Xvtl84@! zl)HbQWQ9_Wh{;2vdL)zx2=9{yUPOPhV@d>;SpUkDeFFR|NA|zS6gsX$k5kv^qPgIh z4^(`8F+@XTp?Cam9l-=Jo!-qed#ROh7Q$#!^D$1+*l6~Mo||u$!mV|rgj18RLU#G> z214U|4U2y(kj6it)p z`*X}jJF(U}@)s=;9~G$zs4SpHaZ}l=L!|M3Sz>V2o~C$IU$&YQOxaT?LEyDA+;4Sr zTR>SPzr@}xDpTgTsGxaoQo|`CGamVhXDKM@+2qUtU;Ha29WAi$V?L>luf$~eC2WYG zj=|8D&L+mUoK)t180iMQ;9k@>H(U*aKvw#`W15kHXijmK*onbcy|EQBVyq2B7vg+m z7MYVjrexw$tySK}zzX^Px@!+rw_LxcJ|!4AUc7z1TYd-$Rr;DGY--Y=mbGS@;#aM9 zPH|0W%Nuc+R3yNN#CHCGPtDvV@G0pS#nxl^;L~(J$bcX+$GCoJbC(0AgybiE%4E4g z^pXmThBtFGL?H6!eu4SqWg6wu`sYE|&mksi@y46klfG=&@8@0*6XILhrX(y1vUiK3 zX-3yDGApSMzAxX>y)*yg9Qb6fMy{sYU6Fl9_T_F~G>zlYpPoy)pys(eKqiddT2XoL zh`|y~Rty-|Aga~KYP(acW9nR`evpuIe;df@r)K8oXy*r!aqxlMe*mJwqQXF7aiEBl wv52IMgp`7es5DSmLPl7)BnI;L8uouA+&vwgor3<4#D5FSudKJFq1grg1HU+f1ONa4 diff --git a/auth.rst b/auth.rst index 48de88125e..29d49be497 100644 --- a/auth.rst +++ b/auth.rst @@ -298,7 +298,7 @@ PostgREST uses JWT mainly for authentication and authorization purposes and enco Schema Isolation ================ -A PostgREST instance is configured to expose all the tables, views, and stored procedures of a single schema specified in a server configuration file. This means private data or implementation details can go inside a private schema and be invisible to HTTP clients. You can then expose views and stored procedures which insulate the internal details from the outside world. It keeps you code easier to refactor, and provides a natural way to do API versioning. For an example of wrapping a private table with a public view see the :ref:`public_ui` section below. +You can isolate your api schema from internal implementation details, as explained in :ref:`schema_structure`. For an example of wrapping a private table with a public view see the :ref:`public_ui` section below. SQL User Management =================== diff --git a/best_practices.rst b/best_practices.rst index cd80ac5396..6a4b0d97c0 100644 --- a/best_practices.rst +++ b/best_practices.rst @@ -1,14 +1,20 @@ -Schema structure ----------------- +.. _schema_structure: + +Schema Structure +================ + +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 - :align: center .. _func_privs: Function privileges -------------------- +=================== 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: @@ -17,7 +23,7 @@ By default, when a function is created, the privilege to execute it is not restr -- Assuming your schema is named "api" ALTER DEFAULT PRIVILEGES IN SCHEMA api REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; - -- Or to stop functions from being executable in the whole database(note the removal of the IN SCHEMA part). + -- Or to stop functions from being PUBLICly executable in the whole database ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; See `PostgreSQL alter default privileges `_ for more details. @@ -40,7 +46,7 @@ By default, a function is executed with the privileges of the user who calls it. 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. See `PostgreSQL documentation `_ for more details. Views with RLS --------------- +============== 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 `_ will be bypassed unless a different, non-SUPERUSER owner is specified. @@ -53,7 +59,7 @@ Views are invoked with the privileges of the view owner, much like stored proced ALTER VIEW sample_view OWNER TO api_views_owner; Views with Rules ----------------- +================ Insertion on VIEWs with complex `RULEs `_ might not work out of the box with PostgREST. It's recommended that you `use triggers instead of RULEs `_. diff --git a/diagrams/README.md b/diagrams/README.md index ee4e31da53..36fd9911d0 100644 --- a/diagrams/README.md +++ b/diagrams/README.md @@ -15,7 +15,7 @@ The schema structure diagram is done with LaTeX. You can use a GUI like https:// Then use this command to generate the png file. ```bash -pdflatex --shell-escape -halt-on-error -output-directory ../_static db.tex +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/ @@ -28,3 +28,13 @@ 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/diagrams/db.png b/diagrams/db.png new file mode 100644 index 0000000000000000000000000000000000000000..f5eb82b214db6fb276f74f59667ec0662e35a511 GIT binary patch literal 9232 zcmaiZ1yEc~ug1cKFxVyW%Yp_5dIAL*jUEG69LU0I9aCbs*2#foo3kzKSd;k0D z)vI^wo;p)!x~IRHKHXiXd(OmYsw-fjlcECv04ya%S#1CS4*1&4Q4wA_2A+i>000`J zgN%%(gS9mPpcs>$Zm*eYL^OPUC3+Yki7OqYDU-s&5S;eb_}$#MO38zKO*yCNpYZQv zqj}|$<=}`+(`i4+;bX*w!V`LivBHnFyAfzf8M$tf}_~VLtWvh zqr+@}jr)DjZtqS28V?Ho^<1rneG$eAC= z-n>MiD9RM6 z8qRICY5oA=vkM-?>=)n4bSx~?VE6+`q&WO;$35SzG;VF4@WMF zni1qhc)|(@$H_vRw?Z;R_#L&(iQ*j*uzlmon}@#N3%xLZT`o$akK%&ho?jFbV>|PL z9R4kBpJW=2S$Kjg4mLe;j5Okie(76gg`*VxQnEvtGa6|dLTGT`*N^WQm5FR)+~uJ~ z_m#Q}`np`z=w9%PAxCoSG6^NJmD3-&jVWuwS|qP#LJmH961D@(5P~t73r(rPR5uI82QzwK=pncZ%m~qg=s^6qaODtiG+u zuj&47H;pGvB+baJ^Jk=>fvBqgH=pKvp-ZhxA+((EqVOo$V%d7x#sRe$2bPR#DX*{c znpaiBl?GLKyy4}_J9GC<`Ax_xd@J-6GWN5;Ye8R}C;JD08NE_EZ^m`zM8@}wPHJWw zYD_HTq?F!wR(W>uoZ`yNif*N}Ww(mW%2-O1D&tD4$_mOv%B@P+DnV&hV}c_C<0qMi zX+YXI&|=07Xc8nw>!;?epU7^{Je>JHO@!@EkuqZ`<1S;9HAuDFB2ZmQ>z5LZT#@#6 zb>DYdnOS`<^?KFI;=l2@YNN)?`csvq=9+UZ~8sr+-?*-uKDX*CXwTAlOs?wr87=fEfNr)gwHWHjVgTVYj2vPS|V z*CT_-9LO#csuRv?8Ed^wyPUn2j+Tamhf;6SiLO0 z{PdX|2MWi41>=t;PIz`YE>{!!i~M~pv)-{KgE2dK;LZk~I=ny4tCJpVKTUpSp5pj1 zJxV{qp;U(z%B1(}C=d*P941sqNsXDes~Dg9Xm7CpG*u#sL|M2rfY+AQ=Hzzh#nATf zQ@>5zW8NF@r&mY+8T%Q^+VJn{S~cUcGrRF+hlq|}%T*lV~xB8vR)_$%KNlbukl z5sS7?S*`!_N~K|>L875MHac!x+FYaVpt0(YNJvYtBAX5`mO+rtl_BaE?K|q<>jQ4{ zZPSWp21UPc_wg6AeqfwqY2bBd8DpvDbrfD zz%K90!h^$GWKBPO{M$zFsO_)q((u9E8)6x&t}GSHxk!6sS*^Im@=-Nx?stV(*?gwN z;4|iAycx+(_V4V)?DIxgj@M1u6$@N#Evqp}Nup`1bC*6Nnwlx*3h z@kO>u)?6_D%q-^e0XmU!h|c@6MYjNAl6yd*~0|__X5{X1J6}eS}paJ zm-CmUJAHT?Ouo4jIXOAIxhlq68Sxp}sWeRI&rUBM2H!q^G9xRgIlvc8cRL#7DC&vN z^A=n8hzuyd`Tnq+<8AC`g|bjzC9^;>&wPS@L@K)P{-a{Gq2tM*%Oa`JK-?lg3L-r! z*Ea}@Q^|B^pEOf%6ZKW6W*>GJjX#VeB zo5_hueyf_xm+Rc?))za7$z$-LtSvP>gDw)+Jl3ZB-Ej1@H79JWN1(lQ~qq>J@ zM)T$MeL?fMQAqj7;`Ywb#rNL%S3Om{3&or=7dH>v$5ukWcG|`LNd7iZQ(n%&#%69} z^4ZZ*LQL#rXNQWCa&mAmF*-UwGqbCuWo=2WwiZGLPSNS6-49K#X>|u6ZwFRLypKIkKx=(p_@R+ zgzx3^YB(BCZFL<0kE~sE#{_LQ&X?-VbG`M8>21hm&)ivsWvIjD`Sj+6X&;~At1#RL z6$M#9G!lj4D}m;!XygF^u+#n1;C#v?ykD6po=R$RC}2c{%B8D24){j^Y4BIk5TlfHgORAg5bW))pQ zUAHhEJ=77aO&Gzr$qKOzG-STpyTp}-Uoa_;F1)_Pp9|! zd4%v%V4gTOgO04-B>$e;TOz9#TNVki6Vy?wA97|%Ept4Bp*SwCcSUnmjQLAR{Em|q zn}zoy+zitF*h`s@r$evlB$%?;=)RT#7EJY3f`1WK#7(trp+6z+GbjTarHpQ7b1?99C%guDv08W_Zg&V?b^edkD{0!&tTKecNEm_Z*g-YhcCBhoS?zu=bqE zB9iNLkd#Ez#PqV483$W+?;0*J1M5M>>P=sxBdQ-;S$wby zwlIO&Egp?LEnYcAb?3$UVOm2W`$55Z`>@#?ofPIWEo-}33YG^6w=!rEgPG0S$CCmL zTYCrf#hAfg>#0?k?s*)GRgzI1l!=oxJYUb`2+-O&^%%hwv&+RWThfVuD6f5gf zGpj>=uA*a@WP=)7w>pmMsGf`wT`Y~7D2Ojg{y7&)8)tAE$Oi7*N6)W1d(% zx;Qo%gt$<*(nUcZT&RVTTw>fqlaz2HB2&R>BdIYO&A*sR!{1ETRHTolqG31FNTZ_` zlt={&5k5iiK+?jdY-%Hdzo60h$yn!s8L$|hzL->cWvnkX94U1&pHP1ZsKaWcqrpC* z(kVnG0$OCyVMGKVjg^>DYn&Qx6>2?heS)f7Jr1LQHw0b!GTI-l2-P>aIhBcc5~IE} zRn%@*&M;eWlxX%aBAj0xlGTDU*CRYy0m>5J`b;8iW3QV%bf?wHW*TXP!#$a(&mH)LAs_7YW^j_h8&I`zVYQbd43h zWR-JO5ex*4!yebwEcrBgsv^s3P)dH84ixQP`aLK8-}PlfA9A0uewqVE@%svYvpoDo zfzVfF*`7#=tJ$YAxn(Cg$jaZ{uC@=8C#Y=|}Yw^`ww!Y;`VnoL&Jj_0IrvjORz zFYd~AAz@FdzWPT$Mor%5;uGOWNOGh=bHr0|YWfG2iBMkv!YS#%b`fvu7`A(=`?-M( zxuT-dt6TSY1vl)=c_fc{?8%oJjG~UMxrM9JeM_-uS1*GpQzG^Ploe*6~t{48{XJL}fIL(63K4~D85|h%pu1{KxUh@3M z2QGCwx65aAMl38y$Isg2t%^9J8t8dS^}?wemRo{hXNb0!ag=2f##oyyl4f&yyO zc9lXc&fM(?*6;Zk`ki^2S;hFe5PLHy{Pxh+4#rSC5E_Rf10jkkb-EEoBbvy55R|AK#O# zPc2*r1rcD{)6xOylfWsWAO#tSA|=)+9gvs)MRHVPb!M-7Vej`XA$(z`f3sa!D-XiN zIw`0y7xtsY;S*wGuZ{AC+La%waL+yo^r*D))JaSdBC^QF*4o%pcIvpu;u925+fpxw zu~O;y=yl)HqZg!4A%ysxRDLR9t+6o++jET6F)lG)yq=Sk`i9cc4|)?jl@0rhuyrc_ z?bM;8?bO-V2w_jknWwb_YXIBP7$^wMFo6%Y7%Xe_L7J;Byrd`>QSvp8Lh2HT zZ>m2v=e&n(S&=2IofQl&U+8Egs?sBLs`@v2&Zu zX~ZOdQk2Z=VY0f}b~%>@VU=sZ9dD|u)&l4!8EEMF3w#)H?A1jS(d}uE2{eP}&jz)c z)3(;vo2=Kju=?@XSdp?Fvukb^uM?fRptN$m6Sjclnlxi21TKgARh~K@ohG$-IFkwg zR9opfB!NGsaYeiiZtjY_R1g4vVV(NTgdP1&=+k(EfDHdZkw0(s+Tv}~(MIFiKW3I# zh$kWVq72OrgPlcgWMi7VzQutpYY`t!Q2g)XCaT-rjyaFT{M_PjtC={O9p}= z->p^SyoA(i>1d=6*5kk9SvnY^h{i@zONTLt6<-gn6&rAsC&0hA%09%l^x6l=) zO4!*cu#O6>Z%%JtgBwXQ^rav=sRmiGKjOjm)Gwnr@BI?N_OwAGm@;^;k^65TzsB*u zMCTW+ka%C(>+Qr@_d( zdy`?;Seq13gJ}z13yL0bF=x_Ur-Fjp(Htg|J~>r) z5s{d%(C2Q>G2r#l{Vq*S6kR=dAA*z#-v4yZz9`=_Rhu*O^Pz{ltCT1}DTv9%yi@!$ zI-^7q$4@#JUGJWj{B*IZT^{O}hsj(41jq#vyYD?Y`$-B8 zm;GTBt?OWv$(IRX0Xw&EbovBCbtLz{c#NVFcVYp&Oas*gYLQYv{0l6 zh*s@NbjB~AbZ_xjhmVQ>AH65^xP2lQzkShMElC%v-=zaiO#gp;_WC9IF9&`GTRiP< z$4HD8cKQScjqaTI(f1gFE9w>&=5x;3sgCvZuf>@32*2#fKpeck+0`-C`XM1)Q%=D3 z%#Vd@ZU0O*AX(2xIi_5*CGcpRjFlMOpKK^MEg0_pJzV>8R6DwPVRpRp19T&BJ_$|r z{}B|#+>#qy{XGY|wsrWUD(KiA@)q$aKe4;FX$w8f8*B`IDS(9~jmi)`GDC;K`EaKv z1j-d@Q%j$kYp$7&McE8$-Gor3^ukMlKWkZXxx=k$-x-mD-dj9n$IksC^W=Z%`J zY!2e6(-rv)CK2g+UVCP?qTrmoby84#rc9nr&+?99CgphI`NrGZ`CYs1QBqt6geitd ziLL!0fj!EUjoeJW^f$vuIDk=3(zQVj+s`oFxyFNj`uCxS(S?wP%oy%!R>=Ot719v4 zctZu4Cw|AK08B$T(!g+T3!xtTll1efE%`&PX`0v@=El1y(VdQ)w9Yc6?EO{}@GkAOg*Pf(Sdtk!CIhJOc-9$7Jrmi=Cd+6QuXmaE0rLtNSv@(~KDcO|? z&bE!aQipi%Tenj|rlg!pQdZYj|4f{6Z_YOL@vpNbii%Wxk+1Y>f2^+;`Pr=naSihc zib@L|37f8q868UCa$MkRe`-BEcu?PpAH70lMoO{IVDkpmy6BP#O@Zg*qxNFyr&&x+ zs5dM1)Yg!p6fC0OM~R^HM*}DFlOu;J-jF5_B)6r}^WB4%yXmY{j@LONQl=Y}@{gM`0bSF(=vK3#Q*6X2=)i=CXmqrOfrbhAi-{OVhte)q$&Fd*#S|D zGg@qoNwY$^)Xyy-P8B`As)4G1CwjmH-Q$O-jOP=CkD30U^Cs3`;C%IS69~&bj&t#- zNZv*(SS1gyTcdC5ZH_ln8(vc;H9>8W zJ#9=DICY{WOyC-XqkYZXDUpjC?~YZ+Lai~xK)+RSrmkDBM|hU9;{tzW0FuUtN#o$v z0qpVfqcqc|FZL1q%M0x4>NpPLeg{eb+&6cItjQ=(kC+ti0!IqXy#0`&-3_J%W_KH3 zgiqBw0-n0}GdgKM%(}RYnB)agLiP*8#&sZKGZZt#v5)Ofgz5|MM>Ri8AaXcyu|Bea zf?orFR6;;wu*Jv5+Z)@v9FU97VFlP^{Vc+ySqoU<)JZQfK{zePV zaO`7F$n8J>9unqM|58xOLXMIF4Tkj|J}HPZAcxq49}$iM6wDPB4;i3=5_=+$yz-uh z=+11mTMB6LSFa;cdKba2&5_KA@U#t(z^lU&I$0g%lV1~*Q~mUa{a|<+W$IGn;MwRG zBajB#YQ>}uWZ`f<)rE`xrB;cHrfdm(_|YsVFAf?g5+A$Xdzl@U)j!iTh72i=eDBN1+Vf(Rk~SAIW7gHnR-qW#JK zP4_Svy3HLAK~T?onxMtMubm}@nhp7nEn|)}P?C{5V?}dbU_iIp{*!HM$@HL*%N={W znXu!{L{`+iPzS%(9JcY&9`@4Stf%EA-;=nz&-vf)>mnj#Vwu)<(;rfMe($e(vNJ=+ z)gNJ}z;j6T)sIi=6Gc4m(`wu0R87fhtJ{RBA~FO((BPyjlMTGW9QFzGCsLn+K4K z=W*SWki3-CL|OA#|8xNd-EPa{@gvOb6xu{a_40IDI#)A}f?D?^Nrsop&bR6GB2qc`(JOCZu_eU&8|nusD$lrSp|XC z-D>9n28D4Rv&Hl72Q+&FdB^&9IaBkqNdbPp`Iyd{R`zBup4h}@yzij|2jd+)$N#B^ z>Hg!Dvv5Y6ARV5EaF`Pyy_5?jYtuRE%ha?26I9Utb}JSCN`c-V^g*Tj3vgKqlEvVy z(H#sHqoKm98$$wB=r^Tcv&N9kx6Odx+YHK!v-^zi%P|zvT9HBj2=}O|CBu5hz!g$t z&|VfL{bW)5aV90vtvb$^#d*SlPE~RDQ0xJHMdlDuHqz)7f(5Nc zXh=PwAFKK?QIda|Vjwr1=vR%mS~ydEDI83rJBF4Ga%d^<6${#IZKaD_L!!1II5f(X zqm6n#5y>|`TW{hb(3Gkb3(98j&pmy3hGK%jm5K!^wvrzyRYDF~5kWe-SBe{5Rf7)U zLt}wRyipY!9EebV(x0^h)*;zW1iVqDEU*y^Z?l!4XM@ZHGYCzg9;jCgKWm*n`b>#A ze^+*6SmcV^?G86jfEH=5ahEgt&2p?Rrs;R}FZzFok8QCR;MdlCd5K-EzRfb} zdJS~f<*-0yyYH!Jofyiq^H`8rWN^R1wGv`zEh-T%bSCv?jzn>|eD1!Wc<8LQwm=_* zS9KEVmR5kdYir6&BK(D!CYQ;;T~KRvR~V86j#-7$4~=H^uKx} zTknQRc8@*4`x!B7pTxY?IeD^FICuSZ(^Y2ioB{iV802yF=rVHN`8fs}Uo7&d?8DbP znX!ZRVaA}#oi9E4{BtDo!Hd8S%}s;$T#W4BUjUcXzwH;nkV;A)GEr3jAf6AKI9kWw zRyxR=4)zmI3S`;&NuRYo@7d_mSiYNVY+8&+I}wJ;C)OTK8BBr3`}XP&|uR98Vlnj z?4&ezQxYBlHmBT4X<{%V(H2jz-Dr|@4;b&6%zFkeSZ~PUnIYnJibb)fMk3ds5Dulk|l($%;N;N9{F2{(lXlOUH^UH z>oA5c3qmAPli9yuB+*gHv%hY~N@!j;XOXYJ8AxBN`;Y(40H?sj=z3U@5XMJR-UFFc zi?>~QZD3h5n@xz)+lHLogvkYcF5%s1GNq#a1v9OID%-!3LOuqA65)U`>`BVgQHpIT z*b=8N#2DeF3{UiM!Zehl9*l+2w?U>zw;KIiGrnerUXXkA6l|y<#nRXQGLQx~#_Xsc z^5+<8^#U+mQTHRt4WB@U%xnb?J%^-os15MKn);$*Vcl9IOwl}eicBlvlJD03p*1`l zESHRpQYsF;C}cWlEh7f)!|2liDM|7lXm*NN@~E|GS(U7IwqwPWXfz9!!@k<$+p;2K zYh{UE(7S~3$0?K;^D z<~>5#AX}r=BuPCLoe%rLOQ`?VF58wwGQ&jRW&NMIa6RnnbZqft#L)dnacOawAe<^bD=%TzOCU`!(`boFSO?fqT$7FYZoZC%ty zi1h?9s`N{}tpgM0ju`q$PnJ6hHn02v-X3ls@sJ<>9~oW}1;S9aH?s_pm*z;f#A?3Q za~$W)_b(`UD(Q<)wywRe=K&0!a)zEZR-U%P*6y~i4*)kOHzyk>FB> Date: Tue, 21 Apr 2020 13:37:24 -0500 Subject: [PATCH 330/549] Consolidate schema structure page --- auth.rst | 2 +- diagrams/db.png | Bin 9232 -> 0 bytes index.rst | 48 +++++++++---------- best_practices.rst => schema_structure.rst | 52 ++++++++++++--------- 4 files changed, 56 insertions(+), 46 deletions(-) delete mode 100644 diagrams/db.png rename best_practices.rst => schema_structure.rst (68%) diff --git a/auth.rst b/auth.rst index 29d49be497..7aaa9eabda 100644 --- a/auth.rst +++ b/auth.rst @@ -298,7 +298,7 @@ PostgREST uses JWT mainly for authentication and authorization purposes and enco Schema Isolation ================ -You can isolate your api schema from internal implementation details, as explained in :ref:`schema_structure`. For an example of wrapping a private table with a public view see the :ref:`public_ui` section below. +You can isolate your api schema from internal implementation details, as explained in :ref:`schema_isolation`. For an example of wrapping a private table with a public view see the :ref:`public_ui` section below. SQL User Management =================== diff --git a/diagrams/db.png b/diagrams/db.png deleted file mode 100644 index f5eb82b214db6fb276f74f59667ec0662e35a511..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9232 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 - :doc:`configuration` -.. _how_tos: - -How-to guides -------------- - -These are recipes that'll help you address specific use-cases. - -.. toctree:: - :glob: - :caption: How-to guides - :hidden: - - how-tos/* - -- :doc:`how-tos/casting-type-to-custom-json` -- :doc:`how-tos/embedding-table-from-another-schema` -- :doc:`how-tos/providing-images-for-img` - Topic guides ------------ @@ -140,10 +122,10 @@ Explanations of some key concepts in PostgREST. auth.rst .. toctree:: - :caption: Installation + :caption: Schema Structure :hidden: - install.rst + schema_structure.rst .. toctree:: :caption: Administration @@ -152,15 +134,33 @@ Explanations of some key concepts in PostgREST. admin.rst .. toctree:: - :caption: Best Practices + :caption: Installation :hidden: - best_practices.rst + install.rst - :doc:`Authentication ` -- :doc:`Installation ` +- :doc:`Schema Structure ` - :doc:`Administration ` -- :doc:`Best Practices ` +- :doc:`Installation ` + +.. _how_tos: + +How-to guides +------------- + +These are recipes that'll help you address specific use-cases. + +.. toctree:: + :glob: + :caption: How-to guides + :hidden: + + how-tos/* + +- :doc:`how-tos/casting-type-to-custom-json` +- :doc:`how-tos/embedding-table-from-another-schema` +- :doc:`how-tos/providing-images-for-img` Ecosystem --------- diff --git a/best_practices.rst b/schema_structure.rst similarity index 68% rename from best_practices.rst rename to schema_structure.rst index 6a4b0d97c0..80e75b23ff 100644 --- a/best_practices.rst +++ b/schema_structure.rst @@ -1,7 +1,7 @@ -.. _schema_structure: +.. _schema_isolation: -Schema Structure +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. @@ -13,19 +13,15 @@ This allows you to change the internals of your schema and maintain backwards co .. _func_privs: -Function privileges -=================== +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 - -- Assuming your schema is named "api" ALTER DEFAULT PRIVILEGES IN SCHEMA api REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; - -- Or to stop functions from being PUBLICly executable in the whole database - ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; - See `PostgreSQL alter default privileges `_ for more details. After that, you'll need to grant EXECUTE privileges on functions explicitly: @@ -35,32 +31,46 @@ After that, you'll need to grant EXECUTE privileges on functions explicitly: GRANT EXECUTE ON FUNCTION login TO anonymous; GRANT EXECUTE ON FUNCTION reset_password TO web_user; - -- you can also GRANT EXECUTE on all functions to a privileged role - GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA api TO admin; - Security definer ---------------- -By default, 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. +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 `API roles `_ won't be able to succesfully 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 -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. See `PostgreSQL documentation `_ for more details. + -- login as a user wich has privileges on the private schemas -Views with RLS -============== + -- 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 `_ will be bypassed unless a different, non-SUPERUSER owner is specified. +For changing this, we can create a non-SUPERUSER role and make this role the view's owner. + .. code-block:: postgres - -- Workaround: - -- non-SUPERUSER role to be used as the owner of the views - CREATE ROLE api_views_owner; - -- alter the view owner so RLS can work normally + CREATE ROLE api_views_owner NOINHERIT; ALTER VIEW sample_view OWNER TO api_views_owner; -Views with Rules -================ +Rules +----- Insertion on VIEWs with complex `RULEs `_ might not work out of the box with PostgREST. It's recommended that you `use triggers instead of RULEs `_. 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 `_. From ef36ec10a19a387454780c3ac4e3999e1650f0e1 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 21 Apr 2020 14:17:13 -0500 Subject: [PATCH 331/549] Put warning in db-schema and clarify search_path --- api.rst | 7 ++----- configuration.rst | 12 ++++++++++-- releases/upcoming.rst | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/api.rst b/api.rst index fd62a0f92c..3a0a4da117 100644 --- a/api.rst +++ b/api.rst @@ -1421,18 +1421,15 @@ Switching Schemas ================= You can switch schemas at runtime with the ``Accept-Profile`` and ``Content-Profile`` headers. You can only switch to a schema that is included in :ref:`db-schema`. -This is useful for **api versioning** and **schema-based multitenancy**. -The schema to be used can be selected through the ``Accept-Profile`` header for GET or HEAD: +For GET or HEAD, the schema to be used can be selected through the ``Accept-Profile`` header: .. code-block:: http GET /items HTTP/1.1 Accept-Profile: tenant2 -If you don't specify the ``Accept-Profile`` header, the first schema on :ref:`db-schema` will be used. - -For POST, PATCH, PUT, DELETE you can use the ``Content-Profile`` header for selecting the schema: +For POST, PATCH, PUT and DELETE, you can use the ``Content-Profile`` header for selecting the schema: .. code-block:: http diff --git a/configuration.rst b/configuration.rst index 35d93d3083..00d0f7ef63 100644 --- a/configuration.rst +++ b/configuration.rst @@ -75,12 +75,12 @@ db-schema The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. - The chosen schema gets added to the `search_path `_ of every request. Example: - .. code:: bash db-schema = "api" + This schema gets added to the `search_path `_ of every request. + You can also specify a list of schemas that can be used for **schema-based multitenancy** and **api versioning** by :ref:`multiple-schemas`. Example: .. code:: bash @@ -89,6 +89,14 @@ db-schema ##or ##db-schema = "v1, v2" + .. warning:: + + Never expose private schemas in this way. See :ref:`schema_isolation`. + + If you don't :ref:`Switch Schemas `, the first schema in the list(``tenant1`` in this case) is chosen as the default schema. + + Only the chosen schema gets added to the `search_path `_ of every request. + .. _db-anon-role: db-anon-role diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 8a77c509be..55eb25bd28 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -10,7 +10,7 @@ These are changes yet unreleased. If you'd like to try them out before a new off Added ----- -* Support for :ref:`multiple-schemas` at runtime. +* Support for :ref:`Switching to a schema ` defined in :ref:`db-schema`. |br| -- `@steve-chavez `_, `@mahmoudkassem `_ * Support for :ref:`planned_count` and :ref:`estimated_count`. From 3af6df743bf97bf8c19ac190a39a571200f6d2c8 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 21 Apr 2020 14:38:31 -0500 Subject: [PATCH 332/549] Reorder calling func with array --- api.rst | 60 +++++++++++++++++++++------------------------------ ecosystem.rst | 4 ++-- 2 files changed, 26 insertions(+), 38 deletions(-) diff --git a/api.rst b/api.rst index 3a0a4da117..350f77123b 100644 --- a/api.rst +++ b/api.rst @@ -1179,45 +1179,50 @@ You can also call a function that takes a single parameter of type json by sendi 8 +.. _s_procs_array: + Calling functions with array parameters --------------------------------------- You can call a function that takes an array parameter: -.. code-block:: plpgsql +.. code-block:: postgres - CREATE FUNCTION native_array_func(arr int[]) RETURNS int[] as $$ - SELECT arr; - $$ LANGUAGE SQL; + create function plus_one(arr int[]) returns int[] as $$ + SELECT array_agg(n + 1) FROM unnest($1) AS n; + $$ language sql; .. code-block:: http - POST /rpc/native_array_func HTTP/1.1 + POST /rpc/plus_one HTTP/1.1 + Content-Type: application/json - { "arg": [1,2,3] } + {"arr": [1,2,3,4]} - [1,2,3] +.. code-block:: json -.. note:: + [2,3,4,5] - For versions prior to PostgreSQL 10, to pass a PostgreSQL native array you need to quote it as a string: +For calling the function with GET, you can pass the array as an `array literal `_, +as in ``{1,2,3,4}``. Note that the curly brackets have to be urlencoded(``{`` is ``%7B`` and ``}`` is ``%7D``). - .. code-block:: http +.. code-block:: http - POST /rpc/native_array_func HTTP/1.1 + GET /rpc/plus_one?arr=%7B1,2,3,4%7D' HTTP/1.1 - { "arg": "{1,2,3}" } + [2,3,4,5] - In these versions we recommend using function parameters of type json to accept arrays from the client. +.. note:: -For calling it with GET, you can pass the array as an `array literal `_; -as in ``{1,2,3}``. Note that the curly brackets have to be urlencoded(``{`` is ``%7B`` and ``}`` is ``%7D``). + 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:: http + .. code-block:: http - GET /rpc/native_array_func?arr=%7B1,2,3%7D' HTTP/1.1 + POST /rpc/plus_one HTTP/1.1 - [1,2,3] + { "arr": "{1,2,3,4}" } + + In these versions we recommend using function parameters of type json to accept arrays from the client. Scalar functions ---------------- @@ -1262,24 +1267,7 @@ It's possible to call a function in a bulk way, analoguosly to :ref:`bulk_insert [ 3, 7 ] -If you have large payloads to process, it's preferrable you instead use a function with an array or json parameter, as this will be more efficient. - -.. 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:: http - - POST /rpc/plus_one HTTP/1.1 - Content-Type: application/json - - {"arr": [1,2,3,4]} - -.. code-block:: json - - [2,3,4,5] +If you have large payloads to process, it's preferrable you instead use a function with an :ref:`array parameter ` or json parameter, as this will be more efficient. It's also possible to :ref:`Specify Columns ` on functions calls. diff --git a/ecosystem.rst b/ecosystem.rst index feb09410e6..0a717478bc 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -6,11 +6,11 @@ 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. -* `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. - .. _eco_example_apps: Example Apps From 469e01f77b90605d8cf6c9b2bb7d20e6256b5d75 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 21 Apr 2020 15:09:25 -0500 Subject: [PATCH 333/549] Add release page for v7.0.0 --- how-tos/providing-images-for-img.rst | 2 ++ index.rst | 1 + releases/v5.2.0.rst | 2 +- releases/v6.0.2.rst | 2 +- releases/{upcoming.rst => v7.0.0.rst} | 43 +++++++++++++++++++++++++-- 5 files changed, 45 insertions(+), 5 deletions(-) rename releases/{upcoming.rst => v7.0.0.rst} (69%) diff --git a/how-tos/providing-images-for-img.rst b/how-tos/providing-images-for-img.rst index 8bb5100874..be442287b6 100644 --- a/how-tos/providing-images-for-img.rst +++ b/how-tos/providing-images-for-img.rst @@ -1,3 +1,5 @@ +.. _providing_img: + Providing images for ========================== diff --git a/index.rst b/index.rst index 939f92a088..bdfa5e7751 100644 --- a/index.rst +++ b/index.rst @@ -185,6 +185,7 @@ Release Notes Here we'll include the most relevant changes so you can migrate to newer versions easily. You can see the full changelog of each release in the `PostgREST repository `_. +- :doc:`releases/v7.0.0` - :doc:`releases/v6.0.2` - :doc:`releases/v5.2.0` diff --git a/releases/v5.2.0.rst b/releases/v5.2.0.rst index 43bc188339..d49067b394 100644 --- a/releases/v5.2.0.rst +++ b/releases/v5.2.0.rst @@ -16,7 +16,7 @@ Thanks This release was made possible thanks to: -* `Daniel Babiak `_ +* `Daniel Babiak `_ * `Michel Pelletier `_ * Tsingson Qin * Jay Hannah diff --git a/releases/v6.0.2.rst b/releases/v6.0.2.rst index 19176d5b28..90cdfb514a 100644 --- a/releases/v6.0.2.rst +++ b/releases/v6.0.2.rst @@ -65,7 +65,7 @@ This release is sponsored by: :target: https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest :width: 13em -* Daniel Babiak +* `Daniel Babiak `_ * Evans Fernandes * Tsingson Qin * Michel Pelletier diff --git a/releases/upcoming.rst b/releases/v7.0.0.rst similarity index 69% rename from releases/upcoming.rst rename to releases/v7.0.0.rst index 55eb25bd28..57117c22f5 100644 --- a/releases/upcoming.rst +++ b/releases/v7.0.0.rst @@ -2,10 +2,10 @@
    -Upcoming -======== +v7.0.0 +====== -These are changes yet unreleased. If you'd like to try them out before a new official release, you can :ref:`build_source`. +You can donwload this release at the `PostgREST v7.0.0 release page `_. Added ----- @@ -35,9 +35,12 @@ Added * Documentation improvements + + Explanation for :ref:`Schema Structure `. + Reference for :ref:`s_proc_embed`. + Reference for :ref:`mutation_embed`. + Reference for filters on :ref:`json_columns`. + + How-to for :ref:`providing_img`. + + Added :ref:`community_tutorials` section. Fixed ----- @@ -67,3 +70,37 @@ Changed * ``server-proxy-uri`` config option has been renamed to :ref:`openapi-server-proxy-uri`. * Default Unix Socket file mode from 755 to 660 + +Thanks +------ + +This release was made possible thanks to: + +.. image:: ../_static/cybertec.png + :target: https://www.cybertec-postgresql.com/en/ + :width: 13em + +.. image:: ../_static/2ndquadrant.png + :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo + :width: 13em + +.. image:: ../_static/retool.png + :target: https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + +* `Daniel Babiak `_ +* Evans Fernandes +* `Jan Sommer `_ +* Tsingson Qin +* Michel Pelletier +* Jay Hannah +* Robert Stolarz +* Kofi Gumbs +* Nicholas DiBiase +* Christopher Reid +* Nathan Bouscal +* Daniel Rafaj +* David Fenko + + +If you like to join them please consider `supporting PostgREST development `_. From b6c03f3658b41101076ab21d4e9bedfc8db0b997 Mon Sep 17 00:00:00 2001 From: Steve Phillips Date: Mon, 27 Apr 2020 06:58:01 -0700 Subject: [PATCH 334/549] api.rst: formatting tweaks --- api.rst | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/api.rst b/api.rst index 350f77123b..0bab1152fd 100644 --- a/api.rst +++ b/api.rst @@ -702,7 +702,7 @@ Embedding on Stored Procedures If you have a :ref:`Stored Procedure ` that returns a table type, you can embed its related resources. -Here's a sample function(notice the ``RETURNS SETOF films``). +Here's a sample function (notice the ``RETURNS SETOF films``). .. code-block:: plpgsql @@ -742,9 +742,12 @@ Say you want to insert a **film** and then get some of its attributes plus embed Prefer: return=representation { - "id": 100, "director_id": 40, - "title": "127 hours", "year": 2010, - "rating": 7.6, "language": "english" + "id": 100, + "director_id": 40, + "title": "127 hours", + "year": 2010, + "rating": 7.6, + "language": "english" } Response: @@ -1122,13 +1125,13 @@ The client can call it by posting an object like 3 -Procedures must be declared with named parameters. Procedures declared like: +Procedures must be declared with named parameters. Procedures declared like .. code-block:: plpgsql CREATE FUNCTION non_named_args(integer, text, integer) ... -Can not be called with PostgREST, since we use `named notation `_ internally. +cannot be called with PostgREST, since we use `named notation `_ internally. Note that PostgreSQL converts identifier names to lowercase unless you quote them like: From 1315584de5dca7cf444116eb3f1c78034c453466 Mon Sep 17 00:00:00 2001 From: Nick Santos Date: Fri, 1 May 2020 16:18:03 -0700 Subject: [PATCH 335/549] Fix link to DigitalOcean PostgREST Contact List video Old playlist is gone - linked to first video in series instead --- ecosystem.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecosystem.rst b/ecosystem.rst index 0a717478bc..a71c41571a 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -3,7 +3,7 @@ Community Tutorials ------------------- -* `Building a Contacts List with PostgREST and Vue.js `_ - +* `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. From 622ff7b708f3a0d09eed97a6e5a71426c55bca02 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 4 May 2020 11:46:54 -0500 Subject: [PATCH 336/549] Note schema structure page is a work in progress --- schema_structure.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/schema_structure.rst b/schema_structure.rst index 80e75b23ff..8f64354f64 100644 --- a/schema_structure.rst +++ b/schema_structure.rst @@ -1,4 +1,8 @@ +.. note:: + + This page is a work in progress. + .. _schema_isolation: Schema Isolation From 8dc16eef27ab97654ab17c26f61029190bcd7634 Mon Sep 17 00:00:00 2001 From: Remo <59358383+monacoremo@users.noreply.github.com> Date: Wed, 6 May 2020 18:41:14 +0200 Subject: [PATCH 337/549] Documentation on how to run the test suite locally (#316) Uses the `with_tmp_db` script added in https://github.com/PostgREST/postgrest/pull/1476. --- install.rst | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/install.rst b/install.rst index 415757f456..2df5c9517d 100644 --- a/install.rst +++ b/install.rst @@ -229,10 +229,23 @@ When a pre-built binary does not exist for your system you can build the project PostgREST Test Suite -------------------- -Creating the Test Database -~~~~~~~~~~~~~~~~~~~~~~~~~~ +To properly run the test suite, you need a Postgres database that the tests can run against. There are several ways to set up this database. -To properly run postgrest tests one needs to create a database. To do so, use the test creation script :code:`create_test_database` in the :code:`test/` folder. +Testing with a temporary database +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have Postgres installed locally (:code:`initdb`, :code:`pg_ctl` and :code:`psql` should be on your PATH, no server needs to be running), you can run the test suite against a temporary database: + +.. code:: bash + + test/with_tmp_db stack test + +The :code:`with_tmp_db` script will set up a new Postgres cluster in a temporary directory, set the required environment variables and run the command that you passed it as an argument, :code:`stack test` in the example above. When the command is done, the temporary database is torn down and deleted again. + +Manually creating the Test Database +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To manually create a database for testing, use the test creation script :code:`create_test_database` in the :code:`test/` folder. The script expects the following parameters: @@ -254,8 +267,8 @@ The script will return the db uri to use in the tests--this uri corresponds to t Generating the user and the password allows one to create the database and run the tests against any PostgreSQL server without any modifications to the server. (Such as allowing accounts without a password or setting up trust authentication, or requiring the server to be on the same localhost the tests are run from). -Running the Tests -~~~~~~~~~~~~~~~~~ +Running the Tests with the manually created database +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To run the tests, one must supply the database uri in the environment variable :code:`POSTGREST_TEST_CONNECTION`. From 40bffc6950425d4e63726596670060d8d8cdddd6 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 12 May 2020 13:24:40 -0500 Subject: [PATCH 338/549] Add subheading to db-schema --- configuration.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/configuration.rst b/configuration.rst index 00d0f7ef63..2b066f8967 100644 --- a/configuration.rst +++ b/configuration.rst @@ -81,21 +81,22 @@ db-schema This schema gets added to the `search_path `_ of every request. +List of schemas +~~~~~~~~~~~~~~~ + You can also specify a list of schemas that can be used for **schema-based multitenancy** and **api versioning** by :ref:`multiple-schemas`. Example: .. code:: bash db-schema = "tenant1, tenant2" - ##or - ##db-schema = "v1, v2" - .. warning:: + If you don't :ref:`Switch Schemas `, the first schema in the list(``tenant1`` in this case) is chosen as the default schema. - Never expose private schemas in this way. See :ref:`schema_isolation`. + *Only the chosen schema* gets added to the `search_path `_ of every request. - If you don't :ref:`Switch Schemas `, the first schema in the list(``tenant1`` in this case) is chosen as the default schema. + .. warning:: - Only the chosen schema gets added to the `search_path `_ of every request. + Never expose private schemas in this way. See :ref:`schema_isolation`. .. _db-anon-role: From 20bb295237ada14c3c8d4cda6ccde9c37d1a6b53 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 20 May 2020 13:51:04 -0500 Subject: [PATCH 339/549] Add v7.0.1 release --- conf.py | 2 +- releases/v7.0.1.rst | 61 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 releases/v7.0.1.rst diff --git a/conf.py b/conf.py index 80008bf49a..03012ad36a 100644 --- a/conf.py +++ b/conf.py @@ -56,7 +56,7 @@ # The short X.Y version. version = u'7.0' # The full version, including alpha/beta/rc tags. -release = u'7.0.0' +release = u'7.0.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/releases/v7.0.1.rst b/releases/v7.0.1.rst new file mode 100644 index 0000000000..2d64ee5183 --- /dev/null +++ b/releases/v7.0.1.rst @@ -0,0 +1,61 @@ +.. |br| raw:: html + +
    + +v7.0.1 +====== + +You can see the full changelog at `PostgREST v7.0.1 release page `_. + +Fixed +----- + +* Fix overloaded computed columns on RPC + |br| -- `@wolfgangwalther `_ + +* Fix POST, PATCH, DELETE with ``?select=`` and ``Prefer: return=minimal`` and PATCH with empty body + |br| -- `@wolfgangwalther `_ + +* Fix missing ``openapi-server-proxy-uri`` config option + |br| -- `@steve-chavez `_ + +* Fix ``Content-Profile`` not working for POST RPC + |br| -- `@steve-chavez `_ + +* Fix PUT restriction for including all columns in payload + |br| -- `@steve-chavez `_ + + +Thanks +------ + +This release was made possible thanks to: + +.. image:: ../_static/cybertec.png + :target: https://www.cybertec-postgresql.com/en/ + :width: 13em + +.. image:: ../_static/2ndquadrant.png + :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo + :width: 13em + +.. image:: ../_static/retool.png + :target: https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em + +* `Daniel Babiak `_ +* Evans Fernandes +* `Jan Sommer `_ +* Tsingson Qin +* Michel Pelletier +* Jay Hannah +* Robert Stolarz +* Kofi Gumbs +* Nicholas DiBiase +* Christopher Reid +* Nathan Bouscal +* Daniel Rafaj +* David Fenko + + +If you'd like to join them, consider `supporting PostgREST development `_. From f8991b0794892766aeb4e68310c3e13bdf9106ab Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 20 May 2020 14:04:06 -0500 Subject: [PATCH 340/549] Fix broken link in schema structure --- schema_structure.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schema_structure.rst b/schema_structure.rst index 8f64354f64..7182912558 100644 --- a/schema_structure.rst +++ b/schema_structure.rst @@ -39,7 +39,7 @@ 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 `API roles `_ won't be able to succesfully execute the function. +If the function accesses private database objects, your :ref:`API roles ` won't be able to succesfully 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. From 516809f2de85e787fa2a278193884a897b9be70a Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 20 May 2020 14:07:35 -0500 Subject: [PATCH 341/549] Add favicon.ico --- _static/favicon.ico | Bin 0 -> 15086 bytes conf.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 _static/favicon.ico diff --git a/_static/favicon.ico b/_static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..a9e16d3a8ba108c83e65a0c90d321c0a194a9bf9 GIT binary patch literal 15086 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 z Date: Fri, 22 May 2020 13:33:49 -0500 Subject: [PATCH 342/549] Add package managers in installation Fixes https://github.com/PostgREST/postgrest-docs/issues/319 * Add development page * Clear installation page from admin and development concerns --- admin.rst | 18 +++- development.rst | 155 ++++++++++++++++++++++++++++++ index.rst | 7 ++ install.rst | 245 +++++++++++------------------------------------- 4 files changed, 234 insertions(+), 191 deletions(-) create mode 100644 development.rst diff --git a/admin.rst b/admin.rst index 52bb51a0de..01c4e87df6 100644 --- a/admin.rst +++ b/admin.rst @@ -126,11 +126,25 @@ Server Version When debugging a problem it's important to verify the PostgREST version. At any time you can make a request to the running server and determine exactly which version is deployed. Look for the :code:`Server` HTTP response header, which contains the version number. -HTTP Requests -------------- +Logging +------- The PostgREST server logs basic request information to stdout, including the requesting IP address and user agent, the URL requested, and HTTP response status. However this provides limited information for debugging server errors. It's helpful to get full information about both client requests and the corresponding SQL commands executed against the underlying database. +.. 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 /var/log/postgrest.log 2>&1 &' + + # another option is to pipe the output into "logger -t postgrest" + +HTTP Requests +------------- + A great way to inspect incoming HTTP requests including headers and query params is to sniff the network traffic on the port where PostgREST is running. For instance on a development server bound to port 3000 on localhost, run this: .. code:: bash diff --git a/development.rst b/development.rst new file mode 100644 index 0000000000..b3e7badc24 --- /dev/null +++ b/development.rst @@ -0,0 +1,155 @@ +.. _build_source: + +Build from Source +================= + +.. note:: + + We discourage building and using PostgREST on **Alpine Linux** because of a reported GHC memory leak on that platform. + +To help with development, you'll need to build from source. `Stack `_ makes it easy. 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 + CentOS/Fedora/Red Hat postgresql-devel, zlib-devel, gmp-devel + BSD postgresql95-client + OS X 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. + +* Check that the server is installed: :code:`postgrest --help`. + +Running the Test Suite +====================== + +To properly run the test suite, you need a Postgres database that the tests can run against. There are several ways to set up this database. + +Testing with a temporary database +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you have Postgres installed locally (:code:`initdb`, :code:`pg_ctl` and :code:`psql` should be on your PATH, no server needs to be running), you can run the test suite against a temporary database: + +.. code:: bash + + test/with_tmp_db stack test + +The :code:`with_tmp_db` script will set up a new Postgres cluster in a temporary directory, set the required environment variables and run the command that you passed it as an argument, :code:`stack test` in the example above. When the command is done, the temporary database is torn down and deleted again. + +Manually creating the Test Database +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To manually create a database for testing, use the test creation script :code:`create_test_database` in the :code:`test/` folder. + +The script expects the following parameters: + +.. code:: bash + + test/create_test_db connection_uri database_name [test_db_user] [test_db_user_password] + +Use the `connection URI `_ to specify the user, password, host, and port. Do not provide the database in the connection URI. The PostgreSQL role you are using to connect must be capable of creating new databases. + +The :code:`database_name` is the name of the database that :code:`stack test` will connect to. If the database of the same name already exists on the server, the script will first drop it and then re-create it. + +Optionally, specify the database user :code:`stack test` will use. The user will be given necessary permissions to reset the database after every test run. + +If the user is not specified, the script will generate the role name :code:`postgrest_test_` suffixed by the chosen database name, and will generate a random password for it. + +Optionally, if specifying an existing user to be used for the test connection, one can specify the password the user has. + +The script will return the db uri to use in the tests--this uri corresponds to the :code:`db-uri` parameter in the configuration file that one would use in production. + +Generating the user and the password allows one to create the database and run the tests against any PostgreSQL server without any modifications to the server. (Such as allowing accounts without a password or setting up trust authentication, or requiring the server to be on the same localhost the tests are run from). + +Running the Tests with the manually created database +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To run the tests, one must supply the database uri in the environment variable :code:`POSTGREST_TEST_CONNECTION`. + +Typically, one would create the database and run the test in the same command line, using the `postgres` superuser: + +.. code:: bash + + POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@database-host" test_db) stack test + +For repeated runs on the same database, one should export the connection variable: + +.. code:: bash + + export POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@database-host" test_db) + stack test + stack test + ... + +If the environment variable is empty or not specified, then the test runner will default to connection uri + +.. code:: bash + + postgres://postgrest_test@localhost/postgrest_test + +This connection assumes the test server on the :code:`localhost:code:` with the user `postgrest_test` without the password and the database of the same name. + +Destroying the Database +~~~~~~~~~~~~~~~~~~~~~~~ + +The test database will remain after the test, together with four new roles created on the PostgreSQL server. To permanently erase the created database and the roles, run the script :code:`test/delete_test_database`, using the same superuser role used for creating the database: + +.. code:: bash + + test/destroy_test_db connection_uri database_name + +Testing with Docker +~~~~~~~~~~~~~~~~~~~ + +The ability to connect to non-local PostgreSQL simplifies the test setup. One elegant way of testing is to use a disposable PostgreSQL in docker. + +For example, if local development is on a mac with Docker for Mac installed: + +.. code:: bash + + $ docker run --name db-scripting-test -e POSTGRES_PASSWORD=pwd -p 5434:5432 -d postgres + $ POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@localhost:5434" test_db) stack test + +Additionally, if one creates a docker container to run stack test (this is necessary on Mac OS Sierra with GHC below 8.0.1, where :code:`stack test` fails), one can run PostgreSQL in a separate linked container, or use the locally installed PostgreSQL app. + +Build the test container with :code:`test/Dockerfile.test`: + +.. code:: bash + + $ docker build -t pgst-test - < test/Dockerfile.test + $ mkdir .stack-work-docker ~/.stack-linux + +The first run of the test container will take a long time while the dependencies get cached. Creating the :code:`~/.stack-linux` folder and mapping it as a volume into the container ensures that we can run the container in disposable mode and not worry about subsequent runs being slow. :code:`.stack-work-docker` is also mapped into the container and must be specified when using stack from Linux, not to interfere with the :code:`.stack-work` for local development. (On Sierra, :code:`stack build` works, while :code:`stack test` fails with GHC 8.0.1). + +Linked containers: + +.. code:: bash + + $ docker run --name pg -e POSTGRES_PASSWORD=pwd -d postgres + $ docker run --rm -it -v `pwd`:`pwd` -v ~/.stack-linux:/root/.stack --link pg:pg -w="`pwd`" -v `pwd`/.stack-work-docker:`pwd`/.stack-work pgst-test bash -c "POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@pg" test_db) stack test" + +Stack test in Docker for Mac, PostgreSQL app on mac: + +.. code:: bash + + $ host_ip=$(ifconfig en0 | grep 'inet ' | cut -f 2 -d' ') + $ export POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres@$HOST" test_db) + $ docker run --rm -it -v `pwd`:`pwd` -v ~/.stack-linux:/root/.stack -v `pwd`/.stack-work-docker:`pwd`/.stack-work -e "HOST=$host_ip" -e "POSTGREST_TEST_CONNECTION=$POSTGREST_TEST_CONNECTION" -w="`pwd`" pgst-test bash -c "stack test" + $ test/destroy_test_db "postgres://postgres@localhost" test_db diff --git a/index.rst b/index.rst index bdfa5e7751..59d1b2928a 100644 --- a/index.rst +++ b/index.rst @@ -139,10 +139,17 @@ Explanations of some key concepts in PostgREST. install.rst +.. toctree:: + :caption: Development + :hidden: + + development.rst + - :doc:`Authentication ` - :doc:`Schema Structure ` - :doc:`Administration ` - :doc:`Installation ` +- :doc:`Development ` .. _how_tos: diff --git a/install.rst b/install.rst index 2df5c9517d..0028b2becb 100644 --- a/install.rst +++ b/install.rst @@ -1,65 +1,87 @@ -Binary Release -============== -[ `Download from release page `_ ] +Installation +============ -The release page has pre-compiled binaries for Mac OS X, Windows, and several Linux distributions. Extract the tarball and run the binary inside with the :code:`--help` flag to see usage instructions: +The release page has `pre-compiled binaries for Mac OS X, Windows, Linux and FreeBSD `_ . +The Linux binary is a static executable that can be run on any Linux distribution. -.. code-block:: bash +If you use **macOS Homebrew**, then you can install PostgREST from the `official repo `_. - # Untar the release (available at https://github.com/PostgREST/postgrest/releases/latest) +.. code:: bash - $ tar Jxf postgrest-[version]-[platform].tar.xz + brew install postgrest - # Try running it - $ ./postgrest --help +If you use **Arch Linux**, then you can install PostgREST from the `official repo `_. - # You should see a usage help message +.. code:: bash -.. note:: + pacman -S postgrest - 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. +If you use **Nix**, then you can install PostgREST from nixpkgs. - .. image:: _static/win-err-dialog.png +.. code:: bash - It usually lives in :code:`C:\Program Files\PostgreSQL\\bin`. See this `article `_ about how to modify the system path. + nix-env -i haskellPackages.postgrest -.. _pg-dependency: +When a pre-built binary does not exist for your system you can :ref:`build the project from source `. -PostgreSQL dependency -===================== +Running +~~~~~~~ -To use PostgREST you will need an underlying database. We require PostgreSQL 9.4 or greater, but recommend at least 9.5 for row-level security features. -You can use something like Amazon `RDS `_ but installing your own locally is cheaper and more convenient for development. +If you downloaded PostgREST from the release page, first extract the compressed file to obtain the executable. -* `Instructions for OS X `_ -* `Instructions for Ubuntu 14.04 `_ -* `Installer for Windows `_ +.. code-block:: bash + + # For UNIX platforms + tar Jxf postgrest-[version]-[platform].tar.xz + + # On Windows you should unzip the file -On Windows, PostgREST will fail to run unless the PostgreSQL binaries are on the system path. To test whether this is the case, run ``pg_config`` from the command line. You should see it output a list of paths. +Now you can run postgrest with the :code:`--help` flag to see usage instructions: -Configuration -============= +.. 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 + postgrest /path/to/postgrest.conf + + # You can also generate a sample config file with + # postgrest 2> 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`. -Running the Server -================== +.. note:: -PostgREST outputs basic request logging to stdout. 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: + 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. -.. code-block:: bash + .. image:: _static/win-err-dialog.png - ssh foo@example.com \ - 'postgrest foo.conf /var/log/postgrest.log 2>&1 &' + It usually lives in :code:`C:\Program Files\PostgreSQL\\bin`. See this `article `_ about how to modify the system path. - # another option is to pipe the output into "logger -t postgrest" + 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. + +.. _pg-dependency: + +PostgreSQL dependency +===================== + +To use PostgREST you will need an underlying database. We require PostgreSQL 9.4 or greater, but recommend at least 9.5 for row-level security features. +You can use something like Amazon `RDS `_ but installing your own locally is cheaper and more convenient for development. + +* `Instructions for OS X `_ +* `Instructions for Ubuntu 14.04 `_ +* `Installer for Windows `_ Docker ====== @@ -176,6 +198,7 @@ With this you can see the swagger-ui in your browser on port 8080. Deploying to Heroku =================== + Assuming your making modifications locally and then pushing to GitHub, it's easy to deploy to Heroku. 1. Create a new app on Heroku @@ -186,159 +209,3 @@ Assuming your making modifications locally and then pushing to GitHub, it's easy 6. Push your changes to GitHub 7. Set Heroku to automatically deploy from Master and then manually deploy the branch for the first build - -.. _build_source: - -Build from Source -================= - -.. note:: - - We discourage building and using PostgREST on **Alpine Linux** because of a reported GHC memory leak on that platform. - -When a pre-built binary does not exist for your system you can build the project from source. You'll also need to do this if you want to help with development. `Stack `_ makes it easy. 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 - CentOS/Fedora/Red Hat postgresql-devel, zlib-devel, gmp-devel - BSD postgresql95-client - OS X 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. - -* Check that the server is installed: :code:`postgrest --help`. - -PostgREST Test Suite --------------------- - -To properly run the test suite, you need a Postgres database that the tests can run against. There are several ways to set up this database. - -Testing with a temporary database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If you have Postgres installed locally (:code:`initdb`, :code:`pg_ctl` and :code:`psql` should be on your PATH, no server needs to be running), you can run the test suite against a temporary database: - -.. code:: bash - - test/with_tmp_db stack test - -The :code:`with_tmp_db` script will set up a new Postgres cluster in a temporary directory, set the required environment variables and run the command that you passed it as an argument, :code:`stack test` in the example above. When the command is done, the temporary database is torn down and deleted again. - -Manually creating the Test Database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To manually create a database for testing, use the test creation script :code:`create_test_database` in the :code:`test/` folder. - -The script expects the following parameters: - -.. code:: bash - - test/create_test_db connection_uri database_name [test_db_user] [test_db_user_password] - -Use the `connection URI `_ to specify the user, password, host, and port. Do not provide the database in the connection URI. The PostgreSQL role you are using to connect must be capable of creating new databases. - -The :code:`database_name` is the name of the database that :code:`stack test` will connect to. If the database of the same name already exists on the server, the script will first drop it and then re-create it. - -Optionally, specify the database user :code:`stack test` will use. The user will be given necessary permissions to reset the database after every test run. - -If the user is not specified, the script will generate the role name :code:`postgrest_test_` suffixed by the chosen database name, and will generate a random password for it. - -Optionally, if specifying an existing user to be used for the test connection, one can specify the password the user has. - -The script will return the db uri to use in the tests--this uri corresponds to the :code:`db-uri` parameter in the configuration file that one would use in production. - -Generating the user and the password allows one to create the database and run the tests against any PostgreSQL server without any modifications to the server. (Such as allowing accounts without a password or setting up trust authentication, or requiring the server to be on the same localhost the tests are run from). - -Running the Tests with the manually created database -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -To run the tests, one must supply the database uri in the environment variable :code:`POSTGREST_TEST_CONNECTION`. - -Typically, one would create the database and run the test in the same command line, using the `postgres` superuser: - -.. code:: bash - - POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@database-host" test_db) stack test - -For repeated runs on the same database, one should export the connection variable: - -.. code:: bash - - export POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@database-host" test_db) - stack test - stack test - ... - -If the environment variable is empty or not specified, then the test runner will default to connection uri - -.. code:: bash - - postgres://postgrest_test@localhost/postgrest_test - -This connection assumes the test server on the :code:`localhost:code:` with the user `postgrest_test` without the password and the database of the same name. - -Destroying the Database -~~~~~~~~~~~~~~~~~~~~~~~ - -The test database will remain after the test, together with four new roles created on the PostgreSQL server. To permanently erase the created database and the roles, run the script :code:`test/delete_test_database`, using the same superuser role used for creating the database: - -.. code:: bash - - test/destroy_test_db connection_uri database_name - -Testing with Docker -~~~~~~~~~~~~~~~~~~~ - -The ability to connect to non-local PostgreSQL simplifies the test setup. One elegant way of testing is to use a disposable PostgreSQL in docker. - -For example, if local development is on a mac with Docker for Mac installed: - -.. code:: bash - - $ docker run --name db-scripting-test -e POSTGRES_PASSWORD=pwd -p 5434:5432 -d postgres - $ POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@localhost:5434" test_db) stack test - -Additionally, if one creates a docker container to run stack test (this is necessary on Mac OS Sierra with GHC below 8.0.1, where :code:`stack test` fails), one can run PostgreSQL in a separate linked container, or use the locally installed PostgreSQL app. - -Build the test container with :code:`test/Dockerfile.test`: - -.. code:: bash - - $ docker build -t pgst-test - < test/Dockerfile.test - $ mkdir .stack-work-docker ~/.stack-linux - -The first run of the test container will take a long time while the dependencies get cached. Creating the :code:`~/.stack-linux` folder and mapping it as a volume into the container ensures that we can run the container in disposable mode and not worry about subsequent runs being slow. :code:`.stack-work-docker` is also mapped into the container and must be specified when using stack from Linux, not to interfere with the :code:`.stack-work` for local development. (On Sierra, :code:`stack build` works, while :code:`stack test` fails with GHC 8.0.1). - -Linked containers: - -.. code:: bash - - $ docker run --name pg -e POSTGRES_PASSWORD=pwd -d postgres - $ docker run --rm -it -v `pwd`:`pwd` -v ~/.stack-linux:/root/.stack --link pg:pg -w="`pwd`" -v `pwd`/.stack-work-docker:`pwd`/.stack-work pgst-test bash -c "POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres:pwd@pg" test_db) stack test" - -Stack test in Docker for Mac, PostgreSQL app on mac: - -.. code:: bash - - $ host_ip=$(ifconfig en0 | grep 'inet ' | cut -f 2 -d' ') - $ export POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://postgres@$HOST" test_db) - $ docker run --rm -it -v `pwd`:`pwd` -v ~/.stack-linux:/root/.stack -v `pwd`/.stack-work-docker:`pwd`/.stack-work -e "HOST=$host_ip" -e "POSTGREST_TEST_CONNECTION=$POSTGREST_TEST_CONNECTION" -w="`pwd`" pgst-test bash -c "stack test" - $ test/destroy_test_db "postgres://postgres@localhost" test_db From 0b998e91d69b3a5e220eb0b8683a3f3dc0ef9c05 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 22 May 2020 14:32:53 -0500 Subject: [PATCH 343/549] Add notice about single Linux static executable Reorder development page in the toctree --- index.rst | 18 ++++++++++-------- install.rst | 1 + releases/v7.0.1.rst | 8 ++++++++ 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/index.rst b/index.rst index 59d1b2928a..4dbbf94fb7 100644 --- a/index.rst +++ b/index.rst @@ -139,17 +139,10 @@ Explanations of some key concepts in PostgREST. install.rst -.. toctree:: - :caption: Development - :hidden: - - development.rst - - :doc:`Authentication ` - :doc:`Schema Structure ` - :doc:`Administration ` - :doc:`Installation ` -- :doc:`Development ` .. _how_tos: @@ -184,7 +177,16 @@ PostgREST has a growing ecosystem of examples, libraries, and experiments. Here * :ref:`eco_external_notification` * :ref:`eco_extensions` * :ref:`clientside_libraries` -* :ref:`eco_commercial` + +For helping with development, see the following page. + +* :doc:`Development ` + +.. toctree:: + :caption: Development + :hidden: + + development.rst Release Notes ------------- diff --git a/install.rst b/install.rst index 0028b2becb..fdc1923421 100644 --- a/install.rst +++ b/install.rst @@ -1,3 +1,4 @@ +.. _install: Installation ============ diff --git a/releases/v7.0.1.rst b/releases/v7.0.1.rst index 2d64ee5183..5c7d64eee7 100644 --- a/releases/v7.0.1.rst +++ b/releases/v7.0.1.rst @@ -25,6 +25,14 @@ Fixed * Fix PUT restriction for including all columns in payload |br| -- `@steve-chavez `_ +* Documentation improvements + + + Added package managers to :ref:`install`. + +Changed +------- + +* From this version onwards, the release page will only include a single Linux static executable that can be run on any Linux distribution. Thanks ------ From b965f32137dce9662c2ef21a2c56a496f186e965 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Sat, 23 May 2020 19:28:28 -0500 Subject: [PATCH 344/549] Update v7.0.1.rst --- releases/v7.0.1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releases/v7.0.1.rst b/releases/v7.0.1.rst index 5c7d64eee7..6adc232358 100644 --- a/releases/v7.0.1.rst +++ b/releases/v7.0.1.rst @@ -32,7 +32,7 @@ Fixed Changed ------- -* From this version onwards, the release page will only include a single Linux static executable that can be run on any Linux distribution. +* From this version onwards, the release page will include a single Linux static executable that can be run on any Linux distribution. Thanks ------ From f03be6d2d0996313aafcbd9af62750247b9a6beb Mon Sep 17 00:00:00 2001 From: Oskar Oldorf Date: Wed, 27 May 2020 19:36:16 +0200 Subject: [PATCH 345/549] Fix typo (#325) --- auth.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth.rst b/auth.rst index 7aaa9eabda..8d0f6df506 100644 --- a/auth.rst +++ b/auth.rst @@ -46,7 +46,7 @@ PostgreSQL manages database access permissions using the concept of roles. A rol Roles for Each Web User ~~~~~~~~~~~~~~~~~~~~~~~ -PostgREST can accommodate either viewpoint. If you treat a role as a single user then the the JWT-based role switching described above does most of what you need. When an authenticated user makes a request PostgREST will switch into the role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable. +PostgREST can accommodate either viewpoint. If you treat a role as a single user then the JWT-based role switching described above does most of what you need. When an authenticated user makes a request PostgREST will switch into the 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. From ff8fd648bbe23105a1a6a6d08ba928602fc1586a Mon Sep 17 00:00:00 2001 From: waltherjj <34773048+waltherjj@users.noreply.github.com> Date: Wed, 27 May 2020 17:04:39 +0200 Subject: [PATCH 346/549] Add new JS (Vue) client to /ecosystem Hey! We developed a Vue.js library, happy to be part of this. Thank you for postgREST! --- ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem.rst b/ecosystem.rst index a71c41571a..3791ea138b 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -71,6 +71,7 @@ Extensions Client-Side Libraries --------------------- +* `technowledgy/vue-postgrest `_ - Vue.js * `supabase/postgrest-js `_ - Isomorphic JS client * `SocialGouv/postgrester `_ - JS + Typescript * `Kong/py-postgrest `_ - Python From a8697341400d3688363fc8e08ad2fcc469ced7fd Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Fri, 5 Jun 2020 13:01:52 -0500 Subject: [PATCH 347/549] reference: Add response.status GUC (#329) Related to https://github.com/PostgREST/postgrest/pull/1541 --- api.rst | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 0bab1152fd..1999517218 100644 --- a/api.rst +++ b/api.rst @@ -1496,7 +1496,7 @@ Notice that the variable should be set to an *array* of single-key objects rathe .. _pre_req_headers: Setting headers via pre-request -------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By using a :ref:`pre-request` function, you can add headers to GET/POST/PATCH/PUT/DELETE responses. As an example, let's add some cache headers for all requests that come from an Internet Explorer(6 or 7) browser. @@ -1529,10 +1529,41 @@ Now when you make a GET request to a table or view, you'll get the cache headers ... +.. _guc_resp_status: -Errors and HTTP Status Codes +Setting Response Status Code ---------------------------- +You can set the ``response.status`` GUC 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:: http + + GET /rpc/teapot HTTP/1.1 + +.. 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). + +.. _raise_error: + +Raise errors with HTTP Status Codes +----------------------------------- + Stored procedures can return non-200 HTTP status codes by raising SQL exceptions. For instance, here's a saucy function that always responds with an error: .. code-block:: postgresql @@ -1558,6 +1589,10 @@ Calling the function returns HTTP 400 with the body "code":"P0001" } +.. note:: + + Keep in mind that ``RAISE EXCEPTION`` will abort the transaction and rollback all changes. If you don't want this, you can instead use the :ref:`response.status GUC `. + One way to customize the HTTP status code is by raising particular exceptions according to the PostgREST :ref:`error to status code mapping `. 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': From 94441c70c05ea9bdcc213f97752bf564bc959655 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 5 Jun 2020 14:35:45 -0500 Subject: [PATCH 348/549] Add upcoming page --- .github/PULL_REQUEST_TEMPLATE.md | 3 +++ releases/upcoming.rst | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 releases/upcoming.rst diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..1bd9f912ab --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,3 @@ + diff --git a/releases/upcoming.rst b/releases/upcoming.rst new file mode 100644 index 0000000000..73a1239224 --- /dev/null +++ b/releases/upcoming.rst @@ -0,0 +1,27 @@ +.. |br| raw:: html + +
    + +Upcoming +======== + +These are changes yet unreleased. If you'd like to try them out before a new official release, you can :ref:`build_source`. + +Added +----- + +* Allow http status override through the :ref:`response.status ` GUC. + |br| -- `@steve-chavez `_ + +Fixed +----- + +* Fix showing UNKNOWN on ``postgrest --help`` invocation. + |br| -- `@monacoremo `_ + +Changed +------- + +* Docker images are now optimized to be built from the scratch image. This reduces the compressed image size from over 30mb to about 4mb. + For more details, see `Docker image built with Nix `_. + |br| -- `@monacoremo `_ From 78bd10acc9aeb0c74e572839efcbd78e9f2f03a6 Mon Sep 17 00:00:00 2001 From: Bobbie Soedirgo Date: Thu, 11 Jun 2020 16:19:57 +0800 Subject: [PATCH 349/549] Add supabase/postgrest-rs client library --- ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem.rst b/ecosystem.rst index 3791ea138b..f21ca5548f 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -71,6 +71,7 @@ Extensions Client-Side Libraries --------------------- +* `supabase/postgrest-rs `_ - Rust * `technowledgy/vue-postgrest `_ - Vue.js * `supabase/postgrest-js `_ - Isomorphic JS client * `SocialGouv/postgrester `_ - JS + Typescript From 702df3218dc98504614769706d9f48c18a037cd7 Mon Sep 17 00:00:00 2001 From: Andrew Hall Date: Fri, 12 Jun 2020 18:45:31 +0100 Subject: [PATCH 350/549] add redux-postgrest to client-side libraries As per https://github.com/andytango/redux-postgrest/issues/7 --- ecosystem.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ecosystem.rst b/ecosystem.rst index f21ca5548f..0b3110d040 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -89,6 +89,8 @@ Client-Side Libraries * `PierreRochard/postgrest-angular `_ - TypeScript, generate UI from API description * `thejettdurham/postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp * `team142/ng-postgrest `_ - Angular app for browsing, editing data exposed over Postgrest. +* `andytango/redux-postgrest `_ - TypeScript/JS, client integrated with (React) Redux. + .. _eco_commercial: From 864a1440be88dbc2e268f7183aadb4a845bf08f2 Mon Sep 17 00:00:00 2001 From: Mathieu Passenaud Date: Tue, 3 Mar 2020 19:43:51 +0100 Subject: [PATCH 351/549] added https://www.mathieupassenaud.fr/codeless_backend/ tutorial --- ecosystem.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ecosystem.rst b/ecosystem.rst index 0b3110d040..9ac92095ca 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -11,6 +11,8 @@ Community Tutorials * `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 ` + .. _eco_example_apps: Example Apps From 7e3c14a71a501161c706104d10ea5ab8ecc30d9c Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 16 Jun 2020 13:52:43 -0500 Subject: [PATCH 352/549] Update community tutorials - Description to keycloak tutorial - Correct gisops tutorial link --- ecosystem.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ecosystem.rst b/ecosystem.rst index 9ac92095ca..77247d0916 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -8,10 +8,11 @@ Community Tutorials * `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 `_ - +* `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 ` +* `"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). .. _eco_example_apps: From c518585259645d3029e55d5bb8becdeaf0f935b4 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sun, 28 Jun 2020 13:46:39 -0500 Subject: [PATCH 353/549] Add FreeBSD port to install page --- install.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install.rst b/install.rst index fdc1923421..0481ed2837 100644 --- a/install.rst +++ b/install.rst @@ -12,6 +12,12 @@ If you use **macOS Homebrew**, then you can install PostgREST from the `official brew install postgrest +If you use **FreeBSD**, then you can install PostgREST from the `official ports `_. + +.. code:: bash + + pkg install hs-postgrest + If you use **Arch Linux**, then you can install PostgREST from the `official repo `_. .. code:: bash From 4c92fc7bcaabdc94a5f85b58bdbcc3a778e67717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Teemu=20Sirvio=CC=88?= Date: Thu, 9 Jul 2020 10:45:47 +0300 Subject: [PATCH 354/549] Update links to pg-safeupdate Change broken links to Bitbucket to GitHub. --- admin.rst | 2 +- ecosystem.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/admin.rst b/admin.rst index 01c4e87df6..a272fcfbb7 100644 --- a/admin.rst +++ b/admin.rst @@ -51,7 +51,7 @@ However it's very easy to delete the **entire table** by omitting the query para DELETE /logs HTTP/1.1 -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 `_ PostgreSQL extension. It raises an error if UPDATE or DELETE are executed without specifying conditions. To install it you can use the `PGXN `_ network: +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 `_ PostgreSQL extension. It raises an error if UPDATE or DELETE are executed without specifying conditions. To install it you can use the `PGXN `_ network: .. code-block:: bash diff --git a/ecosystem.rst b/ecosystem.rst index 77247d0916..9b33ba5467 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -61,7 +61,7 @@ These are PostgreSQL bridges that propagate LISTEN/NOTIFY to external queues for Extensions ---------- -* `pg-safeupdate `_ - Prevent full-table updates or deletes +* `pg-safeupdate `_ - Prevent full-table updates or deletes * `srid/spas `_ - allow file uploads and basic auth * `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server * `wildsurfer/postgrest-oauth-server `_ - OAuth2 server From 58116820254971f99d4eb5389cb13e9c7e4677d9 Mon Sep 17 00:00:00 2001 From: Tad Lispy Date: Fri, 17 Jul 2020 18:36:45 +0200 Subject: [PATCH 355/549] Fix #334: Alter default privileges (#338) * Suggest granting execute on all functions in schema api * Suggest to permanently alter default privileges on functions --- schema_structure.rst | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/schema_structure.rst b/schema_structure.rst index 7182912558..a51d08c921 100644 --- a/schema_structure.rst +++ b/schema_structure.rst @@ -20,20 +20,36 @@ This allows you to change the internals of your schema and maintain backwards co 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: +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 IN SCHEMA api REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; + ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; -See `PostgreSQL alter default privileges `_ for more details. +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 reset_password TO web_user; + 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 ---------------- From 7f67cf78e7232f91ca8d7a1f5ce956e67b558623 Mon Sep 17 00:00:00 2001 From: vbalasu Date: Tue, 28 Jul 2020 09:25:17 -0700 Subject: [PATCH 356/549] Update ecosystem.rst (#341) Added pg-notify-webook --- ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem.rst b/ecosystem.rst index 9b33ba5467..5fd90c41bd 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -47,6 +47,7 @@ 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. +* `vbalasu/pg-notify-webhook `_ - Trigger webhooks from postgres LISTEN/NOTIFY * `diogob/postgres-websockets `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY * `frafra/postgresql2websocket `_ - Websockets * `matthewmueller/pg-bridge `_ - Amazon SNS From c7e623549d0218e89f52b7b0a333669b0d8823fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Najmann?= Date: Tue, 4 Aug 2020 19:02:44 +0200 Subject: [PATCH 357/549] Add Windows installation using Scoop to install page (#342) --- install.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install.rst b/install.rst index 0481ed2837..a56b0c0d0b 100644 --- a/install.rst +++ b/install.rst @@ -30,6 +30,12 @@ If you use **Nix**, then you can install PostgREST from nixpkgs. nix-env -i haskellPackages.postgrest +If you use Windows, you can install PostgREST using `Scoop command-line installer `_. + +.. code:: bash + + scoop install postgrest + When a pre-built binary does not exist for your system you can :ref:`build the project from source `. Running From 49bc82bfcaed2167d7e7aaa72623fd7d397cc83a Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Thu, 13 Aug 2020 17:29:32 +0200 Subject: [PATCH 358/549] Clarify function volatility and location header (#339) * clarify location header only available with PK * clarify function volatility and GET/POST * improve stable/immutable for post and get * add 405 for read only transaction error --- api.rst | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/api.rst b/api.rst index 1999517218..6fd9c4ff91 100644 --- a/api.rst +++ b/api.rst @@ -882,7 +882,7 @@ To create a row in a database table post a JSON object whose keys are the names { "col1": "value1", "col2": "value2" } -The response will include a :code:`Location` header describing where to find the new object. If the table is write-only then constructing the Location header will cause a permissions error. To successfully insert an item to a write-only table you will need to suppress the Location response header by including the request header :code:`Prefer: return=minimal`. +If the table has a primary key, the response will include a :code:`Location` header describing where to find the new object. If the table is write-only then constructing the Location header will cause a permissions error. To successfully insert an item to a write-only table you will need to suppress the Location response header by including the request header :code:`Prefer: return=minimal`. 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. @@ -1148,13 +1148,15 @@ PostgreSQL has four procedural languages that are part of the core distribution: Immutable and stable functions ------------------------------ -Procedures in PostgreSQL marked with :code:`stable` or :code:`immutable` `volatility `_ can only read, not modify, the database and PostgREST executes them in a read-only transaction compatible for read-replicas. Stable and immutable functions can be called with the HTTP GET verb if desired. +PostgREST executes POST requests in a read/write transaction except for functions marked as ``IMMUTABLE`` or ``STABLE``. Those must not modify the database and are executed in a read-only transaction compatible for read-replicas. + +Procedures that do not modify the database can be called with the HTTP GET verb as well, if desired. PostgREST executes all GET requests in a read-only transaction. Modifying the database inside read-only transactions is not possible and calling volatile functions with GET will fail. .. 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/stable`` without failure. However the function will fail when called through PostgREST since it executes it in a read-only transaction. + 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. However, because of the read-only transaction this would still fail with PostgREST. -Because ``add_them`` was declared IMMUTABLE, we can alternately call the function with a GET request: +Because ``add_them`` is ``IMMUTABLE``, we can alternately call the function with a GET request: .. code-block:: http @@ -1227,6 +1229,8 @@ as in ``{1,2,3,4}``. Note that the curly brackets have to be urlencoded(``{`` is In these versions we recommend using function parameters of type json to accept arrays from the client. +.. _s_procs_variadic: + Scalar functions ---------------- @@ -1635,6 +1639,8 @@ PostgREST translates `PostgreSQL error codes Date: Sun, 16 Aug 2020 09:26:40 +0200 Subject: [PATCH 359/549] Updated the link to the blog post for PostgREST + Auth0 tutorial --- ecosystem.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ecosystem.rst b/ecosystem.rst index 5fd90c41bd..ec7f4ba24f 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -6,7 +6,7 @@ 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 + 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. From 92f76722cc6948908e1584fb7041446c100f336f Mon Sep 17 00:00:00 2001 From: Geoffrey van Wyk Date: Thu, 20 Aug 2020 20:45:00 +0200 Subject: [PATCH 360/549] Fix order of options in tar command (#345) With the current order, the tar command thinks the name of the archive is J. This happens on Windows 10 WSL2 with tar 1.29. --- tutorials/tut0.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index 18884ea5f5..a0c49bac2d 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -46,7 +46,7 @@ The pre-built binaries for download are :code:`.tar.xz` compressed files (except # download from https://github.com/PostgREST/postgrest/releases/latest - tar xfJ postgrest--.tar.xz + tar xJf postgrest--.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 From 5b59e79dc9392bdec93e673783dab4e45174a25b Mon Sep 17 00:00:00 2001 From: Vikas Prasad Date: Fri, 21 Aug 2020 00:41:39 +0530 Subject: [PATCH 361/549] Mention `--install-ghc` flag is needed only for the first time. (#347) --- development.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/development.rst b/development.rst index b3e7badc24..f5d2e33882 100644 --- a/development.rst +++ b/development.rst @@ -33,7 +33,8 @@ To help with development, you'll need to build from source. `Stack Date: Thu, 27 Aug 2020 03:23:54 +0300 Subject: [PATCH 362/549] Improvements for Auth0 documentation. (#350) Update for Auth0 integration with the OIDC flow using APIs --- auth.rst | 33 ++++++++------------------------- 1 file changed, 8 insertions(+), 25 deletions(-) diff --git a/auth.rst b/auth.rst index 8d0f6df506..8243e09504 100644 --- a/auth.rst +++ b/auth.rst @@ -204,42 +204,25 @@ JWT from Auth0 An external service like `Auth0 `_ 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, copy its client secret into your PostgREST configuration file as the :code:`jwt-secret`. (Old-style Auth0 secrets are Base64 encoded. For these secrets set :code:`secret-is-base64` to :code:`true`, or just refresh the Auth0 secret.) You can find the secret in the client settings of the Auth0 management console. +To use Auth0, create `an application `_ for your app and `an API `_ 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 `_ by setting it with the `audience parameter `_ 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:: - Make sure OIDC-conformant is toggled off. - - A recent Auth0 change sets it on by default. Turn it `off` here: - - Clients > `Your App` > Settings > Show Advanced Settings > OAuth > OIDC Conformant - - Ensure also that your client application does not pass in any `audience` configuration. - -Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata `_. Then, you will need to write a rule that will extract the role from the user metadata and include a :code:`role` claim in the payload of our user object. Afterwards, in your Auth0Lock code, include the :code:`role` claim in your `scope param `_. +Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata `_. Then, you will need to write `a rule `_ that will extract the role from the user's app_metadata and set it as a `custom claim `_ in the access token. Note that, you may use Auth0's `core authorization feature `_ for more complex use cases. Metadata solution is mentioned here for simplicity. .. code:: javascript - // Example Auth0 rule function (user, context, callback) { + + // Follow the documentations at http://postgrest.org/en/v7.0.0/configuration.html#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 || {}; - user.role = user.app_metadata.role; + context.accessToken[myRoleClaim] = user.app_metadata.role; callback(null, user, context); } - -.. code:: javascript - - // Example using Auth0Lock with role claim in scope - new Auth0Lock ( AUTH0_CLIENTID, AUTH0_DOMAIN, { - container: 'lock-container', - auth: { - params: { scope: 'openid role' }, - redirectUrl: FQDN + '/login', // Replace with your redirect url - responseType: 'token' - } - }) - .. _asym_keys: Asymmetric Keys From 57200320c3a783d93fd8b1b729c4cf6d8479024b Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Sun, 20 Sep 2020 17:13:11 -0400 Subject: [PATCH 363/549] add postgrest-vercel and postgrest-node --- ecosystem.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ecosystem.rst b/ecosystem.rst index ec7f4ba24f..e775f1d8cc 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -39,6 +39,7 @@ Example Apps * `SMRxT/postgrest-demo `_ - multi-tenant logging system * `PierreRochard/postgrest-boilerplate `_ - example auth back-end * `marmelab/ng-admin-postgrest `_ - automatic database admin panel +* `seveibar/postgrest-vercel `_ - Run postgrest on Vercel (Serverless/AWS Lambda) .. _eco_external_notification: @@ -69,6 +70,7 @@ Extensions * `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware * `criles25/postgrest-auth `_ - email based auth/signup * `ppKrauss/PostgREST-writeAPI `_ - generate Nginx rewrite rules to fit an OpenAPI spec +* `seveibar/postgrest-node `_ - Run a postgrest server in NodeJS via an npm module .. _clientside_libraries: From 32173bed4d4b14b9acf442a82d40e5d17335f2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gergely=20M=C3=A9sz=C3=A1ros?= Date: Fri, 25 Sep 2020 22:32:17 +0200 Subject: [PATCH 364/549] Missing library dependency Without zlib1g-dev the build process fails. --- development.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/development.rst b/development.rst index f5d2e33882..3b1ea2774b 100644 --- a/development.rst +++ b/development.rst @@ -15,7 +15,7 @@ To help with development, you'll need to build from source. `Stack Date: Thu, 22 Oct 2020 21:49:29 +0300 Subject: [PATCH 365/549] Added aiodata --- ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem.rst b/ecosystem.rst index e775f1d8cc..ea18b916a3 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -96,6 +96,7 @@ Client-Side Libraries * `thejettdurham/postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp * `team142/ng-postgrest `_ - Angular app for browsing, editing data exposed over Postgrest. * `andytango/redux-postgrest `_ - TypeScript/JS, client integrated with (React) Redux. +* `Exahilosys/aiodata `_ - Python, event-based proxy and caching client. .. _eco_commercial: From 15d5b8a292588583cfc6338bd5ddbe3946f4e063 Mon Sep 17 00:00:00 2001 From: Exahilosys Date: Sat, 24 Oct 2020 00:32:17 +0300 Subject: [PATCH 366/549] Moved aiodata to Extensions --- ecosystem.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ecosystem.rst b/ecosystem.rst index ea18b916a3..6a1f0ffddb 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -71,6 +71,7 @@ Extensions * `criles25/postgrest-auth `_ - email based auth/signup * `ppKrauss/PostgREST-writeAPI `_ - generate Nginx rewrite rules to fit an OpenAPI spec * `seveibar/postgrest-node `_ - Run a postgrest server in NodeJS via an npm module +* `Exahilosys/aiodata `_ - Python, event-based proxy and caching client. .. _clientside_libraries: @@ -96,8 +97,6 @@ Client-Side Libraries * `thejettdurham/postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp * `team142/ng-postgrest `_ - Angular app for browsing, editing data exposed over Postgrest. * `andytango/redux-postgrest `_ - TypeScript/JS, client integrated with (React) Redux. -* `Exahilosys/aiodata `_ - Python, event-based proxy and caching client. - .. _eco_commercial: From 06dba3953f388190a89d7bdc2145013aab067d2d Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Fri, 23 Oct 2020 22:06:49 +0200 Subject: [PATCH 367/549] calling variadic functions --- api.rst | 39 +++++++++++++++++++++++++++++++++++++-- releases/upcoming.rst | 2 ++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 6fd9c4ff91..9303d95280 100644 --- a/api.rst +++ b/api.rst @@ -1215,8 +1215,6 @@ as in ``{1,2,3,4}``. Note that the curly brackets have to be urlencoded(``{`` is GET /rpc/plus_one?arr=%7B1,2,3,4%7D' HTTP/1.1 - [2,3,4,5] - .. 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: @@ -1231,6 +1229,43 @@ as in ``{1,2,3,4}``. Note that the curly brackets have to be urlencoded(``{`` is .. _s_procs_variadic: +Calling 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:: http + + POST /rpc/plus_one HTTP/1.1 + Content-Type: application/json + + {"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:: http + + GET /rpc/plus_one?v=1&v=2&v=3&v=4 HTTP/1.1 + +Repeating also works in POST requests with ``Content-Type: application/x-www-form-urlencoded``: + +.. code-block:: http + + POST /rpc/plus_one HTTP/1.1 + Content-Type: application/x-www-form-urlencoded + + v=1&v=2&v=3&v=4 + Scalar functions ---------------- diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 73a1239224..c6d9ec27d8 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -12,6 +12,8 @@ Added * Allow http status override through the :ref:`response.status ` GUC. |br| -- `@steve-chavez `_ +* Allow :ref:`s_procs_variadic`. + |br| -- `@wolfgangwalther `_ Fixed ----- From fbef0078d5e7772ed796b3fb729b984c0be8f3bc Mon Sep 17 00:00:00 2001 From: Francois-Guillaume Ribreau Date: Mon, 2 Nov 2020 18:37:23 +0100 Subject: [PATCH 368/549] Update index.rst - add netwo - add how to guide :) - fix my name :') --- index.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/index.rst b/index.rst index 4dbbf94fb7..3e7b0b5186 100644 --- a/index.rst +++ b/index.rst @@ -161,6 +161,7 @@ These are recipes that'll help you address specific use-cases. - :doc:`how-tos/casting-type-to-custom-json` - :doc:`how-tos/embedding-table-from-another-schema` - :doc:`how-tos/providing-images-for-img` +- `How PostgreSQL triggers work when called with a PostgREST PATCH HTTP request `_ Ecosystem --------- @@ -209,6 +210,7 @@ Here are some companies that use PostgREST in production. - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. * `Catarse `_ * `Moat `_ +* `Netwo `_ * `Redsmin `_ * `Image-charts `_ * `MotionDynamic - Fast highly dynamic video generation at scale `_ @@ -226,7 +228,7 @@ Testimonials "It's so fast to develop, it feels like cheating!" - -- François-G. Ribreau + -- 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 From 9f15044e63ecbf2c91fd6e59ba932804fcb35bcb Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 22 Nov 2020 21:33:37 +0100 Subject: [PATCH 369/549] Add note about embedding of view-chains and the interaction with `db-extra-search-path` --- api.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 9303d95280..d2119a6ea6 100644 --- a/api.rst +++ b/api.rst @@ -670,7 +670,7 @@ As an example, let's create a view called ``nominations_view`` based on the *nom FROM nominations; -Since it contains ``competition_id`` and ``film_id``—and each one has a **foreign key** defined in its source table—we can embed *competitions* and *films*: +Since it contains ``competition_id`` and ``film_id`` — and each one has a **foreign key** defined in its source table — we can embed *competitions* and *films*: .. code-block:: http @@ -697,6 +697,13 @@ It's also possible to embed `Materialized Views Date: Sun, 29 Nov 2020 00:00:35 +0100 Subject: [PATCH 370/549] Add chocolatey installation method #369 --- install.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install.rst b/install.rst index a56b0c0d0b..d365cf362e 100644 --- a/install.rst +++ b/install.rst @@ -30,10 +30,11 @@ If you use **Nix**, then you can install PostgREST from nixpkgs. nix-env -i haskellPackages.postgrest -If you use Windows, you can install PostgREST using `Scoop command-line installer `_. +If you use Windows, you can install PostgREST using `Chocolatey `_ or `Scoop `_. .. code:: bash + choco install postgrest scoop install postgrest When a pre-built binary does not exist for your system you can :ref:`build the project from source `. From b83ee94fae6a325610e869c0b546c56373a2bec8 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 29 Nov 2020 12:31:28 +0100 Subject: [PATCH 371/549] Add nix-shell tools, fix circleci, resolves #322 --- .circleci/config.yml | 33 ++++++++++++ .gitignore | 1 + README.md | 15 +++--- circle.yml | 8 --- default.nix | 75 ++++++++++++++++++++------ postgrest.dict | 124 +++++++++++++++++++++---------------------- shell.nix | 16 ++++++ 7 files changed, 177 insertions(+), 95 deletions(-) create mode 100644 .circleci/config.yml delete mode 100644 circle.yml create mode 100644 shell.nix diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000000..2d7162fb6b --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,33 @@ +version: 2.1 + +jobs: + build: + docker: + - image: nixos/nix:2.3 + steps: + - checkout + - run: + name: Install build script + command: nix-env -f default.nix -iA build + - run: + name: Build docs + command: postgrest-docs-build + + spellcheck: + docker: + - image: nixos/nix:2.3 + steps: + - checkout + - run: + name: Install spellcheck script + command: nix-env -f default.nix -iA spellcheck + - run: + name: Run spellcheck + command: postgrest-docs-spellcheck + +workflows: + check: + jobs: + - build + - spellcheck + diff --git a/.gitignore b/.gitignore index 00042c2760..4c1f956008 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ Pipfile.lock *.aux *.log diagrams/db.pdf +misspellings diff --git a/README.md b/README.md index e8a06defb8..0bf6955132 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,17 @@ 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. -You can use [pipenv](https://pipenv.readthedocs.io) to build the docs locally: - -```bash - pipenv install - pipenv run python livereload_docs.py -``` - -Or if you use [nix](https://nixos.org/nix/), you can just run: +To build the docs locally, use [nix](https://nixos.org/nix/): ```bash nix-shell ``` -Both of these options will build the docs and start a livereload server on `http://localhost:5500`. +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 diff --git a/circle.yml b/circle.yml deleted file mode 100644 index ae7ac312b9..0000000000 --- a/circle.yml +++ /dev/null @@ -1,8 +0,0 @@ -dependencies: - pre: - - sudo apt-get install aspell - -test: - override: - - cat *.rst | grep -v '^\(\.\.\| \)' | sed 's/`.*`//g' |aspell -d en_US -p ./postgrest.dict list | tee misspellings - - test ! -s misspellings diff --git a/default.nix b/default.nix index 9bc5b1c42e..62a18d8147 100644 --- a/default.nix +++ b/default.nix @@ -1,17 +1,60 @@ -with import (builtins.fetchGit { - url = https://github.com/NixOS/nixpkgs-channels; - ref = "nixos-18.09-small"; - rev = "95fed28ac372c61eb83c87ad97c24b0f957827bf"; -}) {}; - -stdenv.mkDerivation { - name = "postgrest-docs"; - buildInputs = [ - python36Full - python36Packages.sphinx - python36Packages.sphinx_rtd_theme - python36Packages.livereload ]; - shellHook = '' - python livereload_docs.py && exit - ''; +let + # Commit of the Nixpkgs repository that we want to use. + nixpkgsVersion = { + date = "2020-10-27"; + rev = "cd63096d6d887d689543a0b97743d28995bc9bc3"; + tarballHash = "1wg61h4gndm3vcprdcg7rc4s1v3jkm5xd7lw8r2f67w502y94gcy"; + }; + + # 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 ]); +in +{ + inherit pkgs; + + build = + pkgs.writeShellScriptBin "postgrest-docs-build" + '' + set -euo pipefail + + # clean previous build, otherwise some errors might be supressed + rm -rf _build + + ${python}/bin/sphinx-build -W -b html -a -n . _build + ''; + + serve = + pkgs.writeShellScriptBin "postgrest-docs-serve" + '' + set -euo pipefail + + # 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 + + 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 + ''; } diff --git a/postgrest.dict b/postgrest.dict index 0faacadb5f..03dbef3d31 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -1,134 +1,134 @@ personal_ws-1.1 en 0 utf-8 AMQP -Auth -Bool -CSV -Codd -DDL -DoS -GHC -GUC -Github -Google -GraphQL -HMAC -HTTPS -HV -Haskell -Heroku -Homebrew -ILIKE -IP -JS -JSON -JWK -JWT -Kinesis -Logins -MVCC -Mithril -NGINX -Nginx -OAuth -ORM -OpenAPI -PaaS -PostGIS -PostgREST -PostgREST's -PostgreSQL -PostgreSQL's -RDS -RESTful -RLS -RSA -RabbitMQ -RestSharp -SHA -SIGUSR1 -SNS -SQL -SSL -Sencha -SuperAgent -Tcl -TypeScript -UI -Vondra -WAI -Websockets -ZeroMQ api aud +Auth auth authenticator balancer +Bool cd centric +Codd conf config cryptographically +CSV csv +DDL disjoined +DoS eq filename fts +GHC +Github +Google grantor +GraphQL gte +GUC +Haskell +Heroku +HMAC +Homebrew http +HTTPS +HV +ILIKE ilike +IP +JS +JSON json +JWK +JWT jwt +Kinesis localhost login +Logins logins lon lt lte middleware +Mithril multi +MVCC namespaced neq +NGINX +Nginx ngrep nullsfirst nullslast nxl nxr +OAuth +OpenAPI openapi +ORM ov +PaaS param params passphrase -pgSQL pgcrypto pgjwt +pgSQL phfts plfts +PostGIS +PostgreSQL +PostgreSQL's +PostgREST postgrest +PostgREST's pre +RabbitMQ +RDS reallyreallyreallyreallyverysafe refactor requester's +RESTful +RestSharp +RLS +RSA savepoint schemas +Sencha +SHA signup +SIGUSR sl +SNS sqitch +SQL sql sr +SSL startup stateful stdout +SuperAgent syslog +Tcl tsquery +TypeScript +UI +ui unicode +UPSERT +Upsert uri url urls verifier versioning +Vondra +WAI +Websockets webuser wildcard -Upsert -UPSERT -ui +ZeroMQ diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000000..37e6d7cbbf --- /dev/null +++ b/shell.nix @@ -0,0 +1,16 @@ +let + docs = + import ./default.nix; + + pkgs = + docs.pkgs; +in +pkgs.mkShell { + name = "postgrest-docs"; + + buildInputs = [ + docs.build + docs.serve + docs.spellcheck + ]; +} From 84e55970911084f0d4a16088ae7027e2c2996603 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 29 Nov 2020 21:51:22 +0100 Subject: [PATCH 372/549] Add postgrest-docs-dictcheck to remove obsolete words from postgrest.dict --- default.nix | 15 +++++++++++++++ postgrest.dict | 4 ---- shell.nix | 1 + 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/default.nix b/default.nix index 62a18d8147..b422be002b 100644 --- a/default.nix +++ b/default.nix @@ -57,4 +57,19 @@ in | 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 + + 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 \"{}\"" + ''; } diff --git a/postgrest.dict b/postgrest.dict index 03dbef3d31..c0a0e8c82d 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -9,7 +9,6 @@ balancer Bool cd centric -Codd conf config cryptographically @@ -70,7 +69,6 @@ OpenAPI openapi ORM ov -PaaS param params passphrase @@ -108,7 +106,6 @@ SQL sql sr SSL -startup stateful stdout SuperAgent @@ -130,5 +127,4 @@ Vondra WAI Websockets webuser -wildcard ZeroMQ diff --git a/shell.nix b/shell.nix index 37e6d7cbbf..81649475d8 100644 --- a/shell.nix +++ b/shell.nix @@ -12,5 +12,6 @@ pkgs.mkShell { docs.build docs.serve docs.spellcheck + docs.dictcheck ]; } From 634a0c70b7a4cc719d298d4601e26d87b305bd83 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 29 Nov 2020 23:26:06 +0100 Subject: [PATCH 373/549] Fix spelling to satisfy postgrest-docs-spellcheck --- admin.rst | 6 +-- api.rst | 50 ++++++++++---------- auth.rst | 2 +- configuration.rst | 14 +++--- development.rst | 8 ++-- ecosystem.rst | 20 ++++---- how-tos/casting-type-to-custom-json.rst | 6 +-- how-tos/providing-images-for-img.rst | 6 +-- index.rst | 2 +- install.rst | 4 +- postgrest.dict | 61 +++++++++++++++++++------ releases/upcoming.rst | 4 +- releases/v7.0.0.rst | 4 +- schema_structure.rst | 14 +++--- 14 files changed, 118 insertions(+), 83 deletions(-) diff --git a/admin.rst b/admin.rst index a272fcfbb7..d131708e45 100644 --- a/admin.rst +++ b/admin.rst @@ -145,7 +145,7 @@ The PostgREST server logs basic request information to stdout, including the req HTTP Requests ------------- -A great way to inspect incoming HTTP requests including headers and query params is to sniff the network traffic on the port where PostgREST is running. For instance on a development server bound to port 3000 on localhost, run this: +A great way to inspect incoming HTTP requests including headers and query parameters is to sniff the network traffic on the port where PostgREST is running. For instance on a development server bound to port 3000 on localhost, run this: .. code:: bash @@ -250,7 +250,7 @@ Now, whenever the structure of the database schema changes, PostgreSQL will noti Daemonizing =========== -For linux distros that use **systemd** (ubuntu, debian, archlinux) you can create a daemon in the following way. +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`` @@ -271,7 +271,7 @@ Then create the systemd service file in ``/etc/systemd/system/postgrest.service` .. code-block:: ini [Unit] - Description=REST API for any Postgres database + Description=REST API for any PostgreSQL database After=postgresql.service [Service] diff --git a/api.rst b/api.rst index d2119a6ea6..548c7094d1 100644 --- a/api.rst +++ b/api.rst @@ -255,7 +255,7 @@ A full-text search on the computed column: GET /people?full_name=fts.Beckett HTTP/1.1 -As mentioned, computed columns do not appear in the output by default. However you can include them by listing them in the vertical filtering :code:`select` param: +As mentioned, computed columns do not appear in the output by default. However you can include them by listing them in the vertical filtering :code:`select` parameter: .. code-block:: HTTP @@ -314,7 +314,7 @@ Here ``information.cpe`` is a column name. .. note:: - Some http libraries might encode URLs automatically(e.g. :code:`axios`). In these cases you should use double quotes + 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`. Ordering @@ -332,7 +332,7 @@ If no direction is specified it defaults to ascending order: GET /people?order=age HTTP/1.1 -If you care where nulls are sorted, add nullsfirst or nullslast: +If you care where nulls are sorted, add ``nullsfirst`` or ``nullslast``: .. code-block:: http @@ -359,7 +359,7 @@ PostgREST uses HTTP range headers to describe the size of results. Every respons 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. -There are two ways to apply a limit and offset rows: through request headers or query params. When using headers you specify the range of rows desired. This request gets the first twenty people. +There are two ways to apply a limit and offset rows: through request headers or query parameters. When using headers you specify the range of rows desired. This request gets the first twenty people. .. code-block:: http @@ -443,7 +443,7 @@ To help with these cases, PostgREST can get the exact count up until a threshold that threshold is surpassed. To use this behavior, you can specify the ``Prefer: count=estimated`` header. The **threshold** is defined by :ref:`max-rows`. -Here's an example. Suppose we set ``max-rows=1000`` and *smalltable* has 321 rows, then we'll get the exact count: +Here's an example. Suppose we set ``max-rows=1000`` and ``smalltable`` has 321 rows, then we'll get the exact count: .. code-block:: http @@ -455,7 +455,7 @@ Here's an example. Suppose we set ``max-rows=1000`` and *smalltable* has 321 row 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: +If we make a similar request on ``bigtable``, which has 3573458 rows, we would get the planned count: .. code-block:: http @@ -481,13 +481,13 @@ Use the Accept request header to specify the acceptable format (or formats) for GET /people HTTP/1.1 Accept: application/json -The current possibilities are +The current possibilities are: -* \*/\* -* text/csv -* application/json -* application/openapi+json -* application/octet-stream +* ``*/*`` +* ``text/csv`` +* ``application/json`` +* ``application/openapi+json`` +* ``application/octet-stream`` The server will default to JSON for API endpoints and OpenAPI on the root. @@ -776,7 +776,7 @@ Embedding Disambiguation ------------------------ For doing resource embedding, PostgREST infers the relationship between two tables based on a foreign key between them. -However, in cases where there's more than one foreign key between two tables, it's not possible to infer the relationship unambiguosly +However, in cases where there's more than one foreign key between two tables, it's not possible to infer the relationship unambiguously by just specifying the tables names. Target Disambiguation @@ -850,7 +850,7 @@ Hint Disambiguation ~~~~~~~~~~~~~~~~~~~ If specifying the **target** is not enough for unambiguous embedding, you can add a **hint**. For example, let's assume we create -two VIEWs of ``addresses``: ``central_addresses`` and ``eastern_addresses``. +two views of ``addresses``: ``central_addresses`` and ``eastern_addresses``. Since PostgREST supports :ref:`embedding_views` by detecting **source foreign keys** in the views, embedding with the foreign key as the **target** will not be enough for an unambiguous embed: @@ -914,7 +914,7 @@ URL encoded payloads can be posted with ``Content-Type: application/x-www-form-u 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. + 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. 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. @@ -1150,7 +1150,7 @@ PostgreSQL has four procedural languages that are part of the core distribution: .. note:: - Why the `/rpc` prefix? One reason is to avoid name collisions between views and procedures. It also helps emphasize to API consumers that these functions are not normal restful things. The functions can have arbitrary and surprising behavior, not the standard "post creates a resource" thing that users expect from the other routes. + Why the ``/rpc`` prefix? One reason is to avoid name collisions between views and procedures. It also helps emphasize to API consumers that these functions are not normal restful things. The functions can have arbitrary and surprising behavior, not the standard "post creates a resource" thing that users expect from the other routes. Immutable and stable functions ------------------------------ @@ -1171,10 +1171,10 @@ Because ``add_them`` is ``IMMUTABLE``, we can alternately call the function with 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``. -Calling functions with a single json parameter +Calling functions with a single JSON parameter ---------------------------------------------- -You can also call a function that takes a single parameter of type json by sending the header :code:`Prefer: params=single-object` with your request. That way the JSON request body will be used as the single argument. +You can also call a function that takes a single parameter of type JSON by sending the header :code:`Prefer: params=single-object` with your request. That way the JSON request body will be used as the single argument. .. code-block:: plpgsql @@ -1220,7 +1220,7 @@ as in ``{1,2,3,4}``. Note that the curly brackets have to be urlencoded(``{`` is .. code-block:: http - GET /rpc/plus_one?arr=%7B1,2,3,4%7D' HTTP/1.1 + GET /rpc/plus_one?arr=%7B1,2,3,4%7D' HTTP/1.1 .. note:: @@ -1232,14 +1232,14 @@ as in ``{1,2,3,4}``. Note that the curly brackets have to be urlencoded(``{`` is { "arr": "{1,2,3,4}" } - In these versions we recommend using function parameters of type json to accept arrays from the client. + In these versions we recommend using function parameters of type JSON to accept arrays from the client. .. _s_procs_variadic: Calling variadic functions -------------------------- -You can call a variadic function by passing a json array in a POST request: +You can call a variadic function by passing a JSON array in a POST request: .. code-block:: postgres @@ -1299,7 +1299,7 @@ PostgREST will detect if the function is scalar or table-valued and will shape t Bulk Call --------- -It's possible to call a function in a bulk way, analoguosly to :ref:`bulk_insert`. To do this, you need to add the +It's possible to call a function in a bulk way, analogously to :ref:`bulk_insert`. To do this, you need to add the ``Prefer: params=multiple-objects`` header to your request. .. code-block:: http @@ -1316,7 +1316,7 @@ It's possible to call a function in a bulk way, analoguosly to :ref:`bulk_insert [ 3, 7 ] -If you have large payloads to process, it's preferrable you instead use a function with an :ref:`array parameter ` or json parameter, as this will be more efficient. +If you have large payloads to process, it's preferable you instead use a function with an :ref:`array parameter ` or JSON parameter, as this will be more efficient. It's also possible to :ref:`Specify Columns ` on functions calls. @@ -1489,7 +1489,7 @@ HTTP Logic Accessing Request Headers, Cookies and JWT claims ------------------------------------------------- -You can access request headers, cookies and jwt claims by reading GUC variables set by PostgREST per request. They are named :code:`request.header.XYZ`, :code:`request.cookie.XYZ` and :code:`request.jwt.claim.XYZ`. +You can access request headers, cookies and JWT claims by reading GUC variables set by PostgREST per request. They are named :code:`request.header.XYZ`, :code:`request.cookie.XYZ` and :code:`request.jwt.claim.XYZ`. .. code-block:: postgresql @@ -1707,7 +1707,7 @@ PostgREST translates `PostgreSQL error codes `_ - 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 + 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. @@ -23,10 +23,10 @@ Example Apps * `tatut/postgrest-ui `_ - ClojureScript UI components for PostgREST * `priyank-purohit/PostGUI `_ - React Material UI admin panel * `Qu4tro/pgrst-dev-setup `_ - docker-compose and tmuxp setup for experimentation. -* `subzerocloud/postgrest-starter-kit `_ - Boilerplate for new project -* `NikolayS/postgrest-google-translate `_ - Calling to external translation service +* `subzerocloud/postgrest-starter-kit `_ - boilerplate for new project +* `NikolayS/postgrest-google-translate `_ - calling to external translation service * `CodeforAustralia/heritage-near-me `_ - Elm and PostgREST with PostGIS -* `timwis/handsontable-postgrest `_ - An excel-like database table editor +* `timwis/handsontable-postgrest `_ - an excel-like database table editor * `Recmo/PostgrestSkeleton `_ - Docker Compose, PostgREST, Nginx and Auth0 * `benoror/ember-postgrest-dynamic-ui `_ - generating Ember forms to edit data * `ruslantalpa/blogdemo `_ - blog api demo in a vagrant image @@ -39,7 +39,7 @@ Example Apps * `SMRxT/postgrest-demo `_ - multi-tenant logging system * `PierreRochard/postgrest-boilerplate `_ - example auth back-end * `marmelab/ng-admin-postgrest `_ - automatic database admin panel -* `seveibar/postgrest-vercel `_ - Run postgrest on Vercel (Serverless/AWS Lambda) +* `seveibar/postgrest-vercel `_ - run PostgREST on Vercel (Serverless/AWS Lambda) .. _eco_external_notification: @@ -48,7 +48,7 @@ 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. -* `vbalasu/pg-notify-webhook `_ - Trigger webhooks from postgres LISTEN/NOTIFY +* `vbalasu/pg-notify-webhook `_ - trigger webhooks from PostgreSQL's LISTEN/NOTIFY * `diogob/postgres-websockets `_ - expose web sockets for PostgreSQL's LISTEN/NOTIFY * `frafra/postgresql2websocket `_ - Websockets * `matthewmueller/pg-bridge `_ - Amazon SNS @@ -63,14 +63,14 @@ These are PostgreSQL bridges that propagate LISTEN/NOTIFY to external queues for Extensions ---------- -* `pg-safeupdate `_ - Prevent full-table updates or deletes +* `pg-safeupdate `_ - prevent full-table updates or deletes * `srid/spas `_ - allow file uploads and basic auth * `svmnotn/postgrest-auth `_ - OAuth2-inspired external auth server * `wildsurfer/postgrest-oauth-server `_ - OAuth2 server * `nblumoe/postgrest-oauth `_ - OAuth2 WAI middleware * `criles25/postgrest-auth `_ - email based auth/signup -* `ppKrauss/PostgREST-writeAPI `_ - generate Nginx rewrite rules to fit an OpenAPI spec -* `seveibar/postgrest-node `_ - Run a postgrest server in NodeJS via an npm module +* `ppKrauss/PostgREST-writeAPI `_ - generate nginx rewrite rules to fit an OpenAPI spec +* `seveibar/postgrest-node `_ - Run a PostgREST server in Node.js via npm module * `Exahilosys/aiodata `_ - Python, event-based proxy and caching client. .. _clientside_libraries: @@ -95,7 +95,7 @@ Client-Side Libraries * `clesiemo3/postgrestR `_ - R * `PierreRochard/postgrest-angular `_ - TypeScript, generate UI from API description * `thejettdurham/postgrest-sharp-client `_ (needs maintainer) - C#, RestSharp -* `team142/ng-postgrest `_ - Angular app for browsing, editing data exposed over Postgrest. +* `team142/ng-postgrest `_ - Angular app for browsing, editing data exposed over PostgREST. * `andytango/redux-postgrest `_ - TypeScript/JS, client integrated with (React) Redux. .. _eco_commercial: diff --git a/how-tos/casting-type-to-custom-json.rst b/how-tos/casting-type-to-custom-json.rst index b2b25128dc..1cc6061279 100644 --- a/how-tos/casting-type-to-custom-json.rst +++ b/how-tos/casting-type-to-custom-json.rst @@ -80,12 +80,12 @@ The result now is: } ] -You can use the same idea for creating custom CASTs for different types. +You can use the same idea for creating custom casts for different types. .. note:: - If you don't want to modify CASTs for built-in types, an option would be to `create a custom type `_ - for your own ``tsrange`` and add its own CAST. + If you don't want to modify casts for built-in types, an option would be to `create a custom type `_ + for your own ``tsrange`` and add its own cast. .. code-block:: postgres diff --git a/how-tos/providing-images-for-img.rst b/how-tos/providing-images-for-img.rst index be442287b6..a90c7939ee 100644 --- a/how-tos/providing-images-for-img.rst +++ b/how-tos/providing-images-for-img.rst @@ -1,11 +1,11 @@ .. _providing_img: -Providing images for -========================== +Providing images for ```` +============================== :author: `pkel `_ -In this how-to, you will learn how to create an endpoint for providing images to HTML :code:`` tags without client side javascript. +In this how-to, you will learn how to create an endpoint for providing images to HTML :code:`` tags without client side JavaScript. The resulting HTML might look like this: .. code-block:: html diff --git a/index.rst b/index.rst index 3e7b0b5186..65d0d54e4c 100644 --- a/index.rst +++ b/index.rst @@ -238,7 +238,7 @@ Testimonials -- 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 + 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 diff --git a/install.rst b/install.rst index d365cf362e..165a4b6ee2 100644 --- a/install.rst +++ b/install.rst @@ -51,7 +51,7 @@ If you downloaded PostgREST from the release page, first extract the compressed # On Windows you should unzip the file -Now you can run postgrest with the :code:`--help` flag to see usage instructions: +Now you can run PostgREST with the :code:`--help` flag to see usage instructions: .. code-block:: bash @@ -218,7 +218,7 @@ Assuming your making modifications locally and then pushing to GitHub, it's easy 1. Create a new app on Heroku 2. In Settings add the following buildpack :code:`https://github.com/PostgREST/postgrest-heroku` 3. Add the require Config Vars in Heroku (see https://github.com/PostgREST/postgrest/blob/master/app.json#L7-L57 for more details) -4. Modify your postgrest.conf file as required to match your Config Vars in Heroku +4. Modify your ``postgrest.conf`` file as required to match your Config Vars in Heroku 5. Create your :code:`Procfile` and add :code:`./env-to-config ./postgrest postgrest.conf` 6. Push your changes to GitHub 7. Set Heroku to automatically deploy from Master and then manually deploy the branch for the first build diff --git a/postgrest.dict b/postgrest.dict index c0a0e8c82d..0d043e8058 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -1,24 +1,35 @@ personal_ws-1.1 en 0 utf-8 +Adossi AMQP api +API's +Archlinux aud Auth auth authenticator balancer -Bool +Beles +Bouscal +buildpack cd centric -conf +changelog +ClojureScript config cryptographically CSV -csv +Daemonizing DDL +DiBiase disjoined +dockerize DoS eq +Fenko +Fernandes filename +FreeBSD fts GHC Github @@ -27,23 +38,24 @@ grantor GraphQL gte GUC +Gumbs Haskell Heroku HMAC Homebrew -http HTTPS HV -ILIKE ilike +io IP JS +js JSON -json JWK JWT jwt Kinesis +Kofi localhost login Logins @@ -51,31 +63,36 @@ logins lon lt lte +macOS middleware +misprediction Mithril multi MVCC +namespace namespaced neq -NGINX -Nginx +nginx ngrep -nullsfirst -nullslast +nixpkgs +npm nxl nxr OAuth +onwards OpenAPI openapi ORM ov -param -params passphrase +Pelletier +Petr pgcrypto pgjwt pgSQL phfts +phraseto +plainto plfts PostGIS PostgreSQL @@ -84,18 +101,24 @@ PostgREST postgrest PostgREST's pre +psql +Qin RabbitMQ +Rafaj RDS reallyreallyreallyreallyverysafe +Redux refactor requester's RESTful RestSharp RLS +RPC RSA savepoint schemas Sencha +Serverless SHA signup SIGUSR @@ -108,23 +131,35 @@ sr SSL stateful stdout +Stolarz SuperAgent syslog +systemd Tcl +tmuxp +todo +todos +Tsingson tsquery TypeScript UI ui unicode +unix UPSERT -Upsert uri url urls +variadic +Vercel verifier versioning Vondra +Vue WAI +webhooks +websearch Websockets webuser +wfts ZeroMQ diff --git a/releases/upcoming.rst b/releases/upcoming.rst index c6d9ec27d8..4ef4ea9af6 100644 --- a/releases/upcoming.rst +++ b/releases/upcoming.rst @@ -10,7 +10,7 @@ These are changes yet unreleased. If you'd like to try them out before a new off Added ----- -* Allow http status override through the :ref:`response.status ` GUC. +* Allow HTTP status override through the :ref:`response.status ` GUC. |br| -- `@steve-chavez `_ * Allow :ref:`s_procs_variadic`. |br| -- `@wolfgangwalther `_ @@ -24,6 +24,6 @@ Fixed Changed ------- -* Docker images are now optimized to be built from the scratch image. This reduces the compressed image size from over 30mb to about 4mb. +* Docker images are now optimized to be built from the scratch image. This reduces the compressed image size from over 30 MB to about 4 MB. For more details, see `Docker image built with Nix `_. |br| -- `@monacoremo `_ diff --git a/releases/v7.0.0.rst b/releases/v7.0.0.rst index 57117c22f5..d725790863 100644 --- a/releases/v7.0.0.rst +++ b/releases/v7.0.0.rst @@ -5,7 +5,7 @@ v7.0.0 ====== -You can donwload this release at the `PostgREST v7.0.0 release page `_. +You can download this release at the `PostgREST v7.0.0 release page `_. Added ----- @@ -45,7 +45,7 @@ Added Fixed ----- -* Allow embedding a VIEW when its source table foreign key is UNIQUE +* Allow embedding a view when its source table foreign key is UNIQUE |br| -- `@bwbroersma `_ * ``Accept: application/vnd.pgrst.object+json`` behavior is now enforced for POST/PATCH/DELETE regardless of ``Prefer: return=minimal`` diff --git a/schema_structure.rst b/schema_structure.rst index a51d08c921..c45f30f32b 100644 --- a/schema_structure.rst +++ b/schema_structure.rst @@ -20,7 +20,7 @@ This allows you to change the internals of your schema and maintain backwards co 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: +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 @@ -55,7 +55,7 @@ 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 succesfully execute the function. +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. @@ -84,13 +84,13 @@ For changing this, we can create a non-SUPERUSER role and make this role the vie .. code-block:: postgres - CREATE ROLE api_views_owner NOINHERIT; - ALTER VIEW sample_view OWNER TO api_views_owner; + CREATE ROLE api_views_owner NOINHERIT; + ALTER VIEW sample_view OWNER TO api_views_owner; Rules ----- -Insertion on VIEWs with complex `RULEs `_ might not work out of the box with PostgREST. -It's recommended that you `use triggers instead of RULEs `_. -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. +Insertion on views with complex `rules `_ might not work out of the box with PostgREST. +It's recommended that you `use triggers instead of rules `_. +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 `_. From 881170b9ff7aadb2ad57bd3d49a9035b4e8d9751 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 29 Nov 2020 23:42:08 +0100 Subject: [PATCH 374/549] Fix build warnings to satisfy postgrest-docs-build --- auth.rst | 24 +++++++++++++----------- conf.py | 2 +- install.rst | 4 ++-- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/auth.rst b/auth.rst index e67fc3a628..99a29c1f30 100644 --- a/auth.rst +++ b/auth.rst @@ -208,20 +208,22 @@ To use Auth0, create `an application `_ for .. note:: -Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata `_. Then, you will need to write `a rule `_ that will extract the role from the user's app_metadata and set it as a `custom claim `_ in the access token. Note that, you may use Auth0's `core authorization feature `_ for more complex use cases. Metadata solution is mentioned here for simplicity. + Our code requires a database role in the JWT. To add it you need to save the database role in Auth0 `app metadata `_. Then, you will need to write `a rule `_ that will extract the role from the user's app_metadata and set it as a `custom claim `_ in the access token. Note that, you may use Auth0's `core authorization feature `_ for more complex use cases. Metadata solution is mentioned here for simplicity. -.. code:: javascript + .. code:: javascript - function (user, context, callback) { + function (user, context, callback) { - // Follow the documentations at http://postgrest.org/en/v7.0.0/configuration.html#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); - } + // Follow the documentations at + // http://postgrest.org/en/latest/configuration.html#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); + } .. _asym_keys: diff --git a/conf.py b/conf.py index 67fba70416..29f4382cb4 100644 --- a/conf.py +++ b/conf.py @@ -288,4 +288,4 @@ # -- Custom setup --------------------------------------------------------- def setup(app): - app.add_stylesheet('css/custom.css') + app.add_css_file('css/custom.css') diff --git a/install.rst b/install.rst index 165a4b6ee2..9b4756abd3 100644 --- a/install.rst +++ b/install.rst @@ -18,7 +18,7 @@ If you use **FreeBSD**, then you can install PostgREST from the `official ports pkg install hs-postgrest -If you use **Arch Linux**, then you can install PostgREST from the `official repo `_. +If you use **Arch Linux**, then you can install PostgREST from the `community repo `_. .. code:: bash @@ -40,7 +40,7 @@ If you use Windows, you can install PostgREST using `Chocolatey `. Running -~~~~~~~ +------- If you downloaded PostgREST from the release page, first extract the compressed file to obtain the executable. From d6caa5dae99cdf42fdf5c1c09366f2294d5546c6 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sun, 29 Nov 2020 13:18:18 +0100 Subject: [PATCH 375/549] make docs full-width, resolves #351 --- _static/css/custom.css | 16 ++++++++++++++-- index.rst | 3 +++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/_static/css/custom.css b/_static/css/custom.css index 50359ce055..cd52bc8483 100644 --- a/_static/css/custom.css +++ b/_static/css/custom.css @@ -1,3 +1,15 @@ +.wy-nav-content { + max-width: initial; +} + +#postgrest-documentation { + max-width: 800px; +} + +#postgrest-documentation > h1 { + display: none; +} + div.wy-menu.rst-pro { display: none !important; } @@ -10,11 +22,11 @@ div.line-block { margin-bottom: 0px !important; } -#sponsors{ +#sponsors { text-align: center; } -#sponsors h1{ +#sponsors h2 { text-align: left; } diff --git a/index.rst b/index.rst index 65d0d54e4c..8bd6b3ec0f 100644 --- a/index.rst +++ b/index.rst @@ -1,5 +1,8 @@ .. title:: PostgREST Documentation +PostgREST Documentation +======================= + .. figure:: _static/logo.png .. image:: https://img.shields.io/github/stars/postgrest/postgrest.svg?style=social From b0ba2f709cd7f0fb055423c7afad96a9b95cc32e Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Mon, 30 Nov 2020 19:38:15 +0100 Subject: [PATCH 376/549] remove Makefile and Pipfile --- Makefile | 230 ------------------------------------------------------- Pipfile | 14 ---- 2 files changed, 244 deletions(-) delete mode 100644 Makefile delete mode 100644 Pipfile diff --git a/Makefile b/Makefile deleted file mode 100644 index b739a52fff..0000000000 --- a/Makefile +++ /dev/null @@ -1,230 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) - $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don\'t have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " epub3 to make an epub3" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - @echo " dummy to check syntax errors of document sources" - -.PHONY: clean -clean: - rm -rf $(BUILDDIR)/* - -.PHONY: html -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -.PHONY: dirhtml -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -.PHONY: singlehtml -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -.PHONY: pickle -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -.PHONY: json -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -.PHONY: htmlhelp -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -.PHONY: qthelp -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PostgREST.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PostgREST.qhc" - -.PHONY: applehelp -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -.PHONY: devhelp -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/PostgREST" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PostgREST" - @echo "# devhelp" - -.PHONY: epub -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -.PHONY: epub3 -epub3: - $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 - @echo - @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." - -.PHONY: latex -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -.PHONY: latexpdf -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: latexpdfja -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: text -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -.PHONY: man -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -.PHONY: texinfo -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -.PHONY: info -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -.PHONY: gettext -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -.PHONY: changes -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -.PHONY: linkcheck -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -.PHONY: doctest -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -.PHONY: coverage -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." - -.PHONY: xml -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -.PHONY: pseudoxml -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." - -.PHONY: dummy -dummy: - $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy - @echo - @echo "Build finished. Dummy builder generates no files." diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 52ea333a4d..0000000000 --- a/Pipfile +++ /dev/null @@ -1,14 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -sphinx = "*" -sphinx-rtd-theme = "*" -livereload = "*" - -[dev-packages] - -[requires] -python_version = "3.6" From a4fe838308774853038e9136c64ead56712c5a79 Mon Sep 17 00:00:00 2001 From: Joshua Taillon Date: Tue, 8 Dec 2020 12:58:25 -0700 Subject: [PATCH 377/549] Fix outdated environment variable for docker compose Also removed a deprecated `links` setting from the docker-compose, since that's not needed any more --- install.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/install.rst b/install.rst index 9b4756abd3..6e71745a73 100644 --- a/install.rst +++ b/install.rst @@ -172,13 +172,11 @@ To avoid having to install the database at all, you can run both it and the serv image: postgrest/postgrest ports: - "3000:3000" - links: - - db:db environment: PGRST_DB_URI: postgres://app_user:password@db:5432/app_db PGRST_DB_SCHEMA: public PGRST_DB_ANON_ROLE: app_user #In production this role should not be the same as the one used for the connection - PGRST_SERVER_PROXY_URI: "http://127.0.0.1:3000" + PGRST_OPENAPI_SERVER_PROXY_URI: "http://127.0.0.1:3000" depends_on: - db db: From c3ad07fcc0b93f5a952d85bdac0d3e1e24e5b141 Mon Sep 17 00:00:00 2001 From: Gurjeet Singh Date: Sun, 27 Dec 2020 03:42:54 -0800 Subject: [PATCH 378/549] Minor grammar fix --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 6e71745a73..88595fe822 100644 --- a/install.rst +++ b/install.rst @@ -211,7 +211,7 @@ With this you can see the swagger-ui in your browser on port 8080. Deploying to Heroku =================== -Assuming your making modifications locally and then pushing to GitHub, it's easy to deploy to Heroku. +Assuming you're making modifications locally and then pushing to GitHub, it's easy to deploy to Heroku. 1. Create a new app on Heroku 2. In Settings add the following buildpack :code:`https://github.com/PostgREST/postgrest-heroku` From 89942601e470a7fe8fd46c3888796aef94fa190d Mon Sep 17 00:00:00 2001 From: Max H <36088096+rxt30@users.noreply.github.com> Date: Thu, 21 Jan 2021 08:33:58 +0100 Subject: [PATCH 379/549] Fix headline numbering in tutorial 1 --- tutorials/tut1.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/tut1.rst b/tutorials/tut1.rst index e9fe78f547..fa89591f34 100644 --- a/tutorials/tut1.rst +++ b/tutorials/tut1.rst @@ -122,7 +122,7 @@ A request for the todos shows three of them, and all completed. } ] -Step 4. Add Expiration +Step 5. Add Expiration ---------------------- Currently our authentication token is valid for all eternity. The server, as long as it continues using the same JWT password, will honor the token. From 5728bbc4b9235e96397badb79bb205e256b55c87 Mon Sep 17 00:00:00 2001 From: Copple <10214025+kiwicopple@users.noreply.github.com> Date: Thu, 18 Feb 2021 22:16:19 +0800 Subject: [PATCH 380/549] Add Supabase to In Production and client libraries (#389) --- ecosystem.rst | 7 ++++++- index.rst | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/ecosystem.rst b/ecosystem.rst index c9ed9956de..53c0f399b4 100644 --- a/ecosystem.rst +++ b/ecosystem.rst @@ -78,9 +78,14 @@ Extensions Client-Side Libraries --------------------- +* `supabase/postgrest-js `_ - TypeScript/JavaScript * `supabase/postgrest-rs `_ - Rust +* `supabase/postgrest-dart `_ - Dart +* `supabase/postgrest-py `_ - Python +* `supabase/postgrest-csharp `_ - C# +* `supabase/postgrest-kt `_ - Kotlin +* `supabase/postgrest-swift `_ - Swift * `technowledgy/vue-postgrest `_ - Vue.js -* `supabase/postgrest-js `_ - Isomorphic JS client * `SocialGouv/postgrester `_ - JS + Typescript * `Kong/py-postgrest `_ - Python * `datrium/postgrest-pyclient `_ - Python diff --git a/index.rst b/index.rst index 8bd6b3ec0f..e779e54943 100644 --- a/index.rst +++ b/index.rst @@ -209,6 +209,7 @@ Here are some companies that use PostgREST in production. * `Sompani `_ * `Datrium `_ +* `Supabase `_ * `Nimbus `_ - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. * `Catarse `_ From 82e9895e1f9bda736fa51fd54474b35446e6058e Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 25 Feb 2021 18:19:51 -0500 Subject: [PATCH 381/549] Update CYBERTEC logo and url --- _static/cybertec-new.png | Bin 0 -> 353666 bytes index.rst | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 _static/cybertec-new.png diff --git a/_static/cybertec-new.png b/_static/cybertec-new.png new file mode 100644 index 0000000000000000000000000000000000000000..15ec8d4abc8c836e902c9f098b925939c91974ff GIT binary patch literal 353666 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