From 7611308b0ef9d525bbce84426ae04e5d1a93476b Mon Sep 17 00:00:00 2001 From: Joe Nelson Date: Sun, 9 Oct 2016 16:53:41 -0700 Subject: [PATCH 001/652] 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 000000000..e35d8850c --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +_build diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..b739a52ff --- /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 000000000..8c6802b3f --- /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 000000000..8c055322d --- /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/652] Adjustment --- index.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/index.rst b/index.rst index 8c055322d..23b0de321 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/652] 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 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 literal 0 HcmV?d00001 diff --git a/index.rst b/index.rst index 23b0de321..c3fbe2bcb 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 000000000..4d070f6fc --- /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/652] 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 8c6802b3f..bcc453897 100644 --- a/conf.py +++ b/conf.py @@ -108,7 +108,7 @@ todo_include_todos = False # 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/652] 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 c3fbe2bcb..7614a96d4 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 000000000..11fcfe2ac --- /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/652] 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 000000000..1e3afb42b --- /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 7614a96d4..4e6582abf 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/652] Limits and offsets --- api.rst | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 1e3afb42b..706370322 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/652] Alpine linux warning --- install.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/install.rst b/install.rst index 11fcfe2ac..5edde5e55 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/652] 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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 literal 0 HcmV?d00001 diff --git a/_static/security-roles.png b/_static/security-roles.png new file mode 100644 index 0000000000000000000000000000000000000000..f45ba8e9e11d0c19889513b0afc9f52bbed967ec GIT binary patch literal 46364 zcmeAS@N?(olHy`uVBq!ia0y~yV7$w~z{tnJ#=yXEX8PIp3=9%Fna<7up3cq+0Y&*~ znK`Kp3=yqUD>*Gf?_d9K_x;Yvr?Z|%rWu`A`D}CES?jvZWmDIQ6L_Ykcqq2Ibh!mP zwM|Jm70@N(v@)d4r;>lw`}cqM{eS-7?tR_7+H=p>?frIZ{?_les_&QTGsvu2GGPLP zh%Q5;V`54+b8eiwdV1U)3kPPs0}L-1O;6gIxSYOpNkrl4`QQ7eFI8m7-5c+bbMx2y zxBu@uJ%1?qijN_sGr_z+;JX9MeP)KU@8q2Lm>Dj2pZKYK*lF7brY6ag9F7bde(1j0 z+ibag!)?B4_pWW+dd+cz9z%teQ0E(7hOg6V?{SppAK@$Ixa21PadSA!WOVZDsqvKELB! zI>D0p48wv3cF*f92T#N-nsNB=+bwm;kwTTb^%wOUedjga^tbo!iG$bnzg?`<#B|2D z{K75jTnzTcO8 z(a!2weaVT3*P`dJ3ha5{dTsl?i;a=Y&i{X~dheRN?Rb*whO`;|YajZ>-?(Ue<>D89 zc7FfZy|R28+S4c8&CfFsioJDh|8KQcmWY4X+Vv~hBqH9QF|N@zIrPl`{yzhi17*(` z>)yK?ZM*Z3U%#~_^47Zo=f#bh7Z`Unh_@(TEe{kD2+_=5wR^~-^6$iF^2ZTNT zO#Acl;fA|HEjGChCO>)<`~`k(pZ~3FwR(rc6Y2V;*8ZFiw%(Q6c=xb;jYEOLEZM!{ zYun6R=gsn%>Z-js^xv1qOMe<`h4HWMHa+qx+4cXY0`~}Zg{eF>|K*K@pSZ_l(-I()takH3H zg!Y217hiGIw^+}TJjhGhzG5{iY97e(2p{v~QY!h_`_ELF?=XQ9HQncC`L?JM^SX z<&Wgqs@b#cla7Dy?<;BjvgXUL{}OY*%x0{t-s?Ey`NIusc^3-H2)Tay`i=APRVTj` z|G32puUMiy>$3d+X!J`Cq>+mQH&2)7{PKTz+aK1IMlZCz>V}FEEnke9is; zM9YM|CzmlDkj?&HP;{h^XYbbo3``3g%h;qJu>Emh`RFO8!DPCC^Gk!_28L}1IAt8t z?y|Eetx-N0SRkX)Xi^}S(%ij3K&08IK==-$=>kR@rv8J*7r5CDW~-PjJD_gBKka}N zi=#ychoM7`0KW=Hr}L%>Mq9)$HTx)LpWrNN-Lp_ZL&l0r)gkkOP?`^v0xcNePhA8Xe6#+dOdQ$|vxR&-@a+&(08Sm6ACZi5ER#Izaw(;B8G%HCiqYulS(B*VS@kY#~j4Ci&_?7vKM?av>G znuwikc;@P@@G-MvoAd6){0oyWF25jtG4(}OiS#elU$Rwlf4QBH6eJxnTqU90qkK$S zxYP5{lLRlr?}l$r+?#nwCvjs$+8To}&YQFSBJAG?zR}EK+{Sgg=j~y?1o1)(8EHS6 z<$aqEOe{#;p>l`y4sVR`I(hTf-<`{ke14Gn@mEE9jewop{O0|A`A5D#Uj8v#WQ$0X z$SM&r5jl}Sk(VN>B40&pMS?}LS*)529Oou@e^8cDe zYChH|*31vITUg`1)5+Ce)orQsQzucs(zaJpp~hFWSIN651$M0PEKzF{JTGv%bL|ns zNB&uCS)N(SS^Te(uT);Sdgbqx_zaB33UKe=#c>C1&P7cX5Py6Eac)r-e2 zJ-c}ABHv}bONJLtUWmNFdTH*W-G1Mv$|LXfA2WoBi`q}m;FHo;}6C!40af+ z7)&wPV(ep>B-tw2EAdz2vP8a7nejQ}i^eC7ZyL*{ol34cVRDYgnA=d=klkRp@%==( zq`1_FNf(norg$cE2_w7jw_-UZtI%o%6neecCm5YqEE@l;3>Y zbw6}#_LTVt)}MKQbpFx%hwSwaC?}{Dge-7MaGBr~5P8AO7rk?CTb;glIoGx{UuMf6*E#RT37-{Y_+ z-#l&msdo$Ry6$dQU7@l?MM*_V<({XJ_bjht-q$?pCJId`ns#c!s|mkU*DA;=#47!b zYKuD-p%%9+?%D1&GuG_RnfInaCeSbb-He#Ecg!mbEB$|Zf6^C~5IG~_r?o{RM?*_< zlBl+5`c#%FF;hQHZJR1NRd=etrhVYS6&b5#f-?gm14Bc2gZD1}73%ABHva9dTW91R z+Rm%?`sTCZeUSRf_(1vlr*~g3egAatW2qV`m0qRZ@3a2QlAE=3*5#;0QFT$~TlQsm zXHVYxK3o6widRQon7lr;i*47+U3aU#RFzeUe?9i4{AKm)@E7N$H6&yVGt7=i%#uix z;FB(t+$lX(`mD6J zws_j1Y3(}l5s7Pc)_z)J7jZg5e(Sppi5o04itnbK47+W1_uJOF9kDxNyKQ^-cGvFw zyQz8IVaxVHcg^Dx$J71Z`0exCdd?!f`gzdvqqSAPH+}crsj)+4C(m2)d-WCnFEhVk zXX3N5@hF~9@?o9!v8xBa?s{!?vMIzRs>HbHbouP^>xK63|DANctb988TJYV^bDnRR zzU#W>ddd9K_df3<<$34J%(sk}z2Cj>{vV#dTz{wi)%~;nXFi(+O9<;D=2+&}O)r}N zG~a64+04)Wy|trVrL{*s$aYF?#=IOSJ>FKQ@9o#x43*+bcO7y)^{9q+;!NoefOdxi-Hy{Tl{tLdO1h=LiO8)Hp(aO@^^2=r2ksU`mh1t}#Casu!!t=2DRX;Jc#gl6%?+$(v zq_^ac*H@3XK9l`Udq&$oty}eZ%bAyVF3(QibFO5$_Of`ZZd>V+G7V?t#cyQp?tD=! zJTqgd~oO{_iOQ5J-%m|&qJRUKU@Ap zU%W!(OZ=rhA(1CSXN2vjy7~RnX>GRfRoyLJMO_<@3LcgBdgE=iPAgtlKULpX`|b|0 zlH5IKcCA?(vvSWLtLI<8aD}*59DTz(U3#y1Y5uh^uaLNvN$b3Jv*xwl|5b7O>$eNC zldW~jr@jxnC%k8R{rrb@S*9Vfp|^!?~##>I{4 z&Ci-^oy(l{+hf~~$5-u{Qz`o4)oqU>CvLsoVLror<>D=iEtBUYyXL#z59`(Lv#(kH zd)@23XMbN8ekVL%%~}2Wav^&G`zJo1eWss3an9t-tMlLL?tCwN6?|g()}2evm(2gX zzoTa7_t4MT*Y`aBbH)7A_OtJo*KPQ_W1GVM3!5@_T5J>9v1QwwUz6Xa-|YJ2b$r#~ zS6`x@SmoK4JC`i}H~r*#^VyBFXDg~0W&U2x&=WFcx;VrA^^6WBObv`DA9Gx>{(1d9 zlXG+?JHxjLDqlFSvZ*h~HJBBVC}TZuYE%0Hzf8ZU{x;sR?#Axwj_*$foUu5lb4=*y zts{PiA0CK2^tCyiKVRv7ah(-o_QA_e=Up0B%$YI$$IrWSg-!=4T?#WQ+$37m%%$Nf zx%BU;m0DL8zJ}eG>>B|U$u5y8&mP`b(U7rR+0FfG@{}ae{*^~EWz9Rw zJwEvGSQjd3zE1J{ba%?LE9eJb;vgNOCy=HD-aiQRUVrX(@aAEPm#c!rvxwbgB?yXtBsC9&O*t{P5w=Zk{ ztl9ed;^($^tLELgcfoFMUDE$<@k-{nW-jL$3m-n*G+l7LiTeg+S&tgcJu~IbN(4*K zuBfhx$^CPA_o46Cgg>}kU2^xysgP^!cjad-Tf1C6-S3RoS-(1+-y$aJ=G%YleAWFs zeAoK#&vl>M*I56W`}Fr?`Ip>|-Ah#URr=JzZ=0)_n{U59Uw&8Xr{>g$l3#LtWUdJ= z@0-3rK#e2#-J(fqtAe60?znvHlISIKqccVqjkGsUh?soy-NydHprVt7?;m$Q-h1f! zme;2L7bz?i=-<%PF>%G}h$$7%-rW|8on(9|L(=Z1ZPB?;iCmdos$Qk1&WGMxt@q~O zs^T|OFB)9tx!`qu-_pyge@DvaA4t{6UXq@Z{wPl>rR;{yHKl`}T0$q4Zt`9AR&#gA zZb^t-iEdcS_p<#&-gQ?}PNtMGM6{`Vd4XTC2za{HkBt?8%A&#k|{-)@18 zfxd>VglWu2`fS{Ce8cf+kM7C8@7X_h z!d!{Ur20u8J@2Mm772Rwam(uEa`WY^KdaV$d{cDn(N6i9pCjjQeqOY1&i~5BZ;QO* z0_rjyzc1}Pbn|p@_&u?`TJN+gi?(ju@-Qm>=JVGzqQCQ>+^-2eT5TS~Vd%PlV#vC*unTKW0R<_~(ap9|f6 z@owTg>%Ez^b$`S3zp#B_TibT0?Qp!>J}XP1&$nLpp40gK;!ecf+mj#ny;r&S_w6Dt>e@2u#$(doq!oh4l= zQNgD#1zgs-xNlMRGX3-uDLsjw(zc}qri-Q~Cq<{vfB52g&XZM7^q!r3eD>k#TT{=? zR}oP35V|D%O!S}TCDkYmF|E8Ev;CP=4Ye(GzPjv+d$ucQUCf+4tDCx%!Zx~VM&Ar< z^?f=ut8vzGxyaHrzi!;i>Mik=*Nx3pl#PmA@Y^{*s{X`-#>Bpf%Qr6gxWjYZ%2vs9 zYp&WHJal5x)lZ8<7nd&U?O!`f_F~?df6ERBx+klvE#H1F^>@?6-}R~g4>K3Cm&SZt zSG%Ofy#BkbT!@_Vyz6`TDz&TE|2Fw~>}TdL=Ks$c9yeM!zF6?>fv{qfqCJ1;gt-%} zciWs%%2Bj2Px3z!k#Z+--C_R4F^gv|cJB+3@$D$=5$%k%x+XQf@{VWIe%sVe98gIW8eEwIu#GJ+U-z|#cpT&fgYzyoA_VWq%kM5O~ zlUZ}7r*Mbrt&R!Qn4~49`Ed8CFt7670JlKDW|v97c(2b^*Uk^yyJKse`;&E__ZENu zoh6Yy_tmG@?a~JPuF|}-?#$}9-F4;CjbGbxKO4rkpA&w+e7RM=z4WVFFT~#7yk5O8 z|4r+g11}HiA2!o}?e-%sz29r^(mHX=nP$RYZ~pmxsq5;sU+@3$Z4cexQ8ugd$;@xd zCt7hkxbaR(w_|8%WoS?pWQZ1Ru!`giV4KSD$^PT_e+?6k2=_mkbEbj4gQ-&GvI*-s zk7qR!OB5uZ@?PNhb&!1n-$my|6VxWjw>d4JShGQPmV={)-4wG^%%wsnKm7_^ZD60n z=6*Q!gMn0km@E6D?u+XUzDNkGC}$saitx>m*~Z#_Q1bD)M{$pSeqj7z#U<0q>?(Ow z>tIho>js}pzeu;%Wkrj#RJnwLJK|U5u9TkOquwlbL2l99CB=(gFTB3I{=|Ya0cTg7 zx^XJytjR%{b4{ry6GfBOrW&WuPV|2^;o*!2I!}roi#`8$^79Py+2@m|+t^S0zx6=u z!IFgxiNb<~fs0=3xv}6!^WEtxNvg3+LY7D_eV#h+bli0BiQfYa!lr~I1>Fkyx5jAM zt@YP-E|1-to8RuX{aMGgo^O-%E*w^RE^|*H-zCu}^-#vCbhZ4vJi(NisaF$hQ@yuL z-d#e>~ZC@jBjar?_Hdy_Ae|1k4}#)0yTTz4-Elw-OwS`z)I(8zPTqB?xcuFWh3Gekb%!`5rMl;s1?Jy-Jgl`WE!B z`DwLO`Khgnuv##$KxoJGm6|L0gZ(dCTr9c#>ayA8w@b4ZU0?VCL5>e_F=vRX1H zZLQfNTgp=s`sK;jsk}Tq{8C4y;tgLK%r$?$;lk!G?(eqf%>KOXW?AI7>9VtA_FHPd z=`OCUYxtcoBfDRE|L*DT+n(31&wKAV-_$;>eka52104&x51v(&KdH0uSEAs<>q>5X zm)&N%E|;&AR2Gpg&Uv(>oGCXcJt)G+>}iVBuaHlYeP6Ei{O~!RGxu|r_?1srxI(yG z*DvUno9;5DD5g5%UD&~r2hMk{#^tX|z`|Ek87ffE6O^)`uoo2md{=JuzUY>g| z`!-(UhndOWqNJ2(XAecckWFkV^h&r@V3zLpGW%8krKb&3-)%ke^;&Ohy!P$a{Z3`} zvJcpV67)AP#VN2oX^39HUDKkWPl^;cGFIiI6-&cJwL-HNCe!v1xm~nA4T59@|~+es*wL*|kk(%4O%i#rC!N>3^%-`Nw9$1Fnfq zci3#F*nHlrS=0Ah@J~1M*(OI1UQ3G}>piAMHm4q+n0{l@junrcKfdl6*R%)wHr{k<<7?ZrppZ z_R9KIyLF46|FQk^cgCx)x29ArWZ%pGc(%yw^yp8wb>tRX%h~YOs@*faS-bN3#`zyz z|0n#~@bUE7iPHHxZt3^h`t$en&u>}&XnCM9r~JkFeP?TbF8XhpweP|0m+v3im(=U% z$Hbe&*X(_=dCL}=jW)k8KTSX1b$ZWC+xvpsr6aE`&d`~Nro5oh^vw)N8kM;itP#<`gx5hW46K32*3xq68y`AMmI z6}bfrAYfBrRgqhen_7~nP?4LHS8P>bs{~eIl~-&964qBz04piUwpEJo4N!2-FG^J~ z)icpEP_pAvP*AWbN=dT{a&d!dFG@+XRmvzSDX`MlFE20GD>v55FG|-pw6wI;H!#vS zGSV$dNz*N^%qvN((9J7Wh8O}f$0fBmxhS)sBr`ux0c37sQhsTPt&$SRA_W+LxHGjP z6Jb+cG1Obh`MLTa8GS=N1AVyJAmc%zRsoq6sW}lYnYpQX#hLkeb|&WLHemG_G6)-O z5IQ3fI*rT?kaZ%-Ky}*agFJxbAxLO}MT1=2?6_?7!Ql!DV>>Q>D}`hR1_uUD7srqa z#y5Mp=Ufk+`~UyE-v$Q?WLa2uRy7(rpNTmmwDpLNLA0v5Q|#rg4WiTI|Eq3GTG^X= zTW3v&+|wwpDUsVa(~`22vis7MmMUva5Bb1!xU_9&>RN_UE5}{&y~YUk!U&+suc* z|DLlfcvfTgyx8vfC%fmr>*jv{z32STlwNuJeXe^xi+q5?7lm9Y-zzxxG59kgpgSY~ zt~!MZE*ShG#A@^6o6y2q!+A_FC|B}V0?8Ovu8Iq9Rh@n(*|5Uli(iHhG-2?9x5$SD zx4m1w9fg@%!BuhLYx03$7+f%22 zb00Wf??*VjF>X-<``tLDE&9jc<{oHXC(jIX_B^J&E{y!U_PS)m_rT3ct-MZnFXwd^vQ} zsIbH1=(o2)-xkSBXI{$PHwzS!u}d8Iy>@+0dHa6eG~MX(6Su6xPI>*YeVuxIn(n6) zw?u<(9p(CdE_-w8-)FL$l8>iFTCU!DgmwG+saLzxx4V70bhV4!{50Rv$6`xkgZ>{% ztNe3tt5Lp}xK_zSuA6Hr-`}{kZEfs{m6O+>nf -o&rpS(20AHt)}Ie(jfb?ctN9 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 C6r%qC literal 0 HcmV?d00001 diff --git a/auth.rst b/auth.rst new file mode 100644 index 000000000..9ddbc23a6 --- /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 4e6582abf..39bf27b4c 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/652] pre-request function docs --- auth.rst | 52 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/auth.rst b/auth.rst index 9ddbc23a6..cce349238 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/652] JWT generation --- auth.rst | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/auth.rst b/auth.rst index cce349238..e77dad5ae 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/652] SSL --- auth.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/auth.rst b/auth.rst index e77dad5ae..d02f6affb 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/652] 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 d02f6affb..9622b5e75 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 4d070f6fc..0baee6619 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/652] More about schema isolation --- auth.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/auth.rst b/auth.rst index 9622b5e75..89324bdd7 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/652] 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 706370322..0e9aa142e 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 89324bdd7..c5a44e7a7 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/652] 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 c5a44e7a7..005112755 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/652] Discuss group/user roles --- auth.rst | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/auth.rst b/auth.rst index 005112755..2c00ae42d 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/652] 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 2c00ae42d..cc1aecbbe 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/652] 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 cc1aecbbe..17ef0d913 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/652] Notes for future admin section --- index.rst | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/index.rst b/index.rst index 39bf27b4c..840f8e27a 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/652] 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 000000000..68f1aafec --- /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 17ef0d913..5ea384e7b 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 840f8e27a..d42418d55 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/652] Debugging steps (sql logging, ngrep) --- admin.rst | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/admin.rst b/admin.rst index 68f1aafec..5b1ae7319 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/652] Schema reloading --- admin.rst | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/admin.rst b/admin.rst index 5b1ae7319..b586e99da 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/652] 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 b586e99da..6835aa9c1 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/652] corrected url (#24) --- auth.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth.rst b/auth.rst index 5ea384e7b..d98fbc8bb 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/652] 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 6835aa9c1..0151c09af 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 0e9aa142e..e45952bf8 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/652] 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 0151c09af..bb81338ed 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/652] 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 bb81338ed..5ba5d4097 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 e45952bf8..73fa46cea 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 d98fbc8bb..f94533646 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 000000000..ae7ac312b --- /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 5edde5e55..430161bcf 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 0baee6619..e90839bfa 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 000000000..5966273cb --- /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/652] Schema cache rewrite --- admin.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/admin.rst b/admin.rst index 5ba5d4097..a7d145d7f 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/652] Content negotiation --- api.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/api.rst b/api.rst index 73fa46cea..1cceabdf1 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/652] Singular or plural --- api.rst | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/api.rst b/api.rst index 1cceabdf1..d9e6e7398 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/652] OpenAPI overview --- api.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api.rst b/api.rst index d9e6e7398..e7bc52398 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/652] 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 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 literal 0 HcmV?d00001 diff --git a/api.rst b/api.rst index e7bc52398..02817aeb4 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 000000000..d19fdf61b --- /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/652] 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 000000000..ee869c6da --- /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 bcc453897..8479fd301 100644 --- a/conf.py +++ b/conf.py @@ -284,3 +284,8 @@ texinfo_documents = [ # 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/652] TeX description --- conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf.py b/conf.py index 8479fd301..489726976 100644 --- a/conf.py +++ b/conf.py @@ -269,8 +269,8 @@ man_pages = [ # 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/652] Base nginx conf from Ruslan --- admin.rst | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/admin.rst b/admin.rst index a7d145d7f..b5bcb3521 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/652] Test suite instructions from @dsimunic --- install.rst | 143 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 122 insertions(+), 21 deletions(-) diff --git a/install.rst b/install.rst index 430161bcf..4bf888f48 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/652] 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 f94533646..2430a5009 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/652] Add queueing bridges to docs --- intro.rst | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/intro.rst b/intro.rst index e90839bfa..575b99fe7 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/652] Stored procs --- api.rst | 50 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/api.rst b/api.rst index 02817aeb4..748053997 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/652] 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 b5bcb3521..794647150 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 748053997..bb5aa845f 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 2430a5009..7a8ce2316 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 4bf888f48..b300cfd5c 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/652] Fill out the API ops --- api.rst | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/api.rst b/api.rst index bb5aa845f..380a09f1c 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/652] 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 794647150..4de4615fb 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 380a09f1c..5f5f12378 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/652] 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 4de4615fb..51b91acda 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 7a8ce2316..db8259b66 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/652] 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 b300cfd5c..20d41f90d 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/652] more about stored procedures (#26) --- api.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 5f5f12378..4e7b43923 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/652] 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 20d41f90d..22b9fbdde 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/652] Document SSH session termination precautions --- admin.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/admin.rst b/admin.rst index 51b91acda..239c4d1a6 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/652] 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 db8259b66..f9c4f0267 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/652] 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 d42418d55..f8bb6e414 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/652] 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 22b9fbdde..34d0ae7ec 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/652] 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 239c4d1a6..9f36dc4b9 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 4e7b43923..779e85bcf 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/652] 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 9f36dc4b9..e2fadd380 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 f9c4f0267..3c42c9113 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/652] pg-sns-bridge rename --- intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intro.rst b/intro.rst index 575b99fe7..ffec7d75c 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/652] 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 3c42c9113..6ae84a0aa 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/652] More prominent link to downloads --- install.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 34d0ae7ec..50c6ba815 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/652] 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 779e85bcf..f0f3af22a 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/652] 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 e2fadd380..751830018 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/652] 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 751830018..aec3d6c39 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 f0f3af22a..6ec65039f 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/652] 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 f8bb6e414..85935f068 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/652] Bump copyright year --- conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf.py b/conf.py index 489726976..479232894 100644 --- a/conf.py +++ b/conf.py @@ -46,7 +46,7 @@ master_doc = 'index' # 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/652] 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 aec3d6c39..24ce3df23 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 6ec65039f..edf2b8f4f 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 ffec7d75c..9ed102b53 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/652] 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 9ed102b53..3c7c9d7b7 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/652] More client side libs --- intro.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/intro.rst b/intro.rst index 3c7c9d7b7..6d94d44da 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/652] 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 24ce3df23..d157fc4d9 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 6d94d44da..b87681d72 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/652] Google translate example --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index b87681d72..813b99446 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/652] 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 d157fc4d9..95fd11b68 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/652] 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 813b99446..352838f3a 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/652] 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 95fd11b68..f70cf4d66 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/652] 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 6ae84a0aa..0d1f84214 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/652] 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 f70cf4d66..6b290105e 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/652] 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 000000000..bb9afe28e --- /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/652] 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 352838f3a..644bd20a3 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/652] Another site in production --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index 644bd20a3..50bbaacb8 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/652] 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 edf2b8f4f..46b9411ee 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/652] 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 6b290105e..e3cd25168 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 46b9411ee..7595c33fa 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/652] 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 e3cd25168..a1abf9cc9 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 7595c33fa..29fc3e111 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 0d1f84214..f9abc431d 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/652] Fix duplicate wording (#48) --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 29fc3e111..56c97991b 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/652] 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 56c97991b..c66ac8060 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/652] Add example for complex boolean logic (#52) --- api.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/api.rst b/api.rst index c66ac8060..e24963da1 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/652] subzero domain change (#47) --- intro.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/intro.rst b/intro.rst index 50bbaacb8..dfa30970e 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/652] Update subzero message --- intro.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/intro.rst b/intro.rst index dfa30970e..b74107156 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/652] 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 f9abc431d..ab271a11c 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/652] Confirmed more production users --- intro.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/intro.rst b/intro.rst index b74107156..a1dfc3f4f 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/652] Add some things people have said --- intro.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/intro.rst b/intro.rst index a1dfc3f4f..74e8d622f 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/652] Another queue bridge --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index 74e8d622f..860b59197 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/652] 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 e24963da1..6c01c9358 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/652] 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 6c01c9358..bed486362 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/652] 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 ab271a11c..6b490b2db 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/652] 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 50c6ba815..f1bb74f77 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/652] 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 bed486362..ed30a7239 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/652] 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 6b490b2db..f953d5aa6 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/652] 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 ed30a7239..461afae7a 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/652] 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 461afae7a..2df2c91ed 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/652] Update ecosystem --- intro.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/intro.rst b/intro.rst index 860b59197..2f3b0e0df 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/652] 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 f953d5aa6..d76ddcfe8 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/652] Deprecate {} for resource embedding --- api.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/api.rst b/api.rst index 2df2c91ed..0128e0b22 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/652] 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 0128e0b22..52b0e714b 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/652] 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 52b0e714b..51716b41f 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/652] 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 51716b41f..cd10a588e 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/652] 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 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 J);T3K0RUG}=RE)b literal 0 HcmV?d00001 diff --git a/admin.rst b/admin.rst index a1abf9cc9..ae7b1ab20 100644 --- a/admin.rst +++ b/admin.rst @@ -1,3 +1,5 @@ +.. _configuration: + Configuration ============= diff --git a/conf.py b/conf.py index 479232894..415ce36c2 100644 --- a/conf.py +++ b/conf.py @@ -54,9 +54,9 @@ author = u'Joe Nelson' # built documents. # # The short X.Y version. -version = u'0.4' +version = u'4.1' # The full version, including alpha/beta/rc tags. -release = u'0.4.0.0' +release = u'4.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/index.rst b/index.rst index 85935f068..28fa94a09 100644 --- a/index.rst +++ b/index.rst @@ -10,6 +10,11 @@ intro.rst +.. toctree:: + :caption: Tutorials + + tutorials/tut0.rst + .. toctree:: :caption: Installation diff --git a/install.rst b/install.rst index f1bb74f77..100496314 100644 --- a/install.rst +++ b/install.rst @@ -49,6 +49,8 @@ To use PostgREST you will need an underlying database (PostgreSQL version 9.3 or * `Instructions for Ubuntu 14.04 `_ * `Installer for Windows `_ +.. _build_source: + Build from Source ================= diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst new file mode 100644 index 000000000..305095355 --- /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
+
+
+
+

+ +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/652] 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 2f3b0e0df..dbafcfab7 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/652] 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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 VyfAReV_;xl@O1TaS?83{1OR|zTu1-_ literal 0 HcmV?d00001 diff --git a/index.rst b/index.rst index 28fa94a09..b3ab8b26f 100644 --- a/index.rst +++ b/index.rst @@ -14,6 +14,7 @@ :caption: Tutorials tutorials/tut0.rst + tutorials/tut1.rst .. toctree:: :caption: Installation diff --git a/tutorials/tut0.rst b/tutorials/tut0.rst index 305095355..a762bc7d1 100644 --- a/tutorials/tut0.rst +++ b/tutorials/tut0.rst @@ -1,3 +1,5 @@ +.. _tut0: + Tutorial 0 - Get it Running =========================== diff --git a/tutorials/tut1.rst b/tutorials/tut1.rst new file mode 100644 index 000000000..91c8976e1 --- /dev/null +++ b/tutorials/tut1.rst @@ -0,0 +1,245 @@ +.. _tut1: + +Tutorial 1 - The Golden Key +=========================== + +In :ref:`tut0` we created a read-only API with a single endpoint to list todos. There are many directions we can go to make this API more interesting, but one good place to start would be allowing some users to change data in addition to reading it. + +Step 1. Add a Trusted User +-------------------------- + +The previous tutorial created a :code:`web_anon` role in the database with which to execute anonymous web requests. Let's make a role called :code:`todo_user` for users who authenticate with the API. This role will have the authority to do anything to the todo list. + +.. code-block:: postgres + + -- run this in psql using the database created + -- in the previous tutorial + + create role todo_user nologin; + grant todo_user to postgres; + + grant usage on schema api to todo_user; + grant all on api.todos to todo_user; + grant usage, select on sequence api.todos_id_seq to todo_user; + +Step 2. Make a Secret +--------------------- + +Clients authenticate with the API using JSON Web Tokens. These are JSON objects which are cryptographically signed using a 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. + +.. note:: + + The `OpenSSL toolkit `_ 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/652] 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 91c8976e1..6ea26786c 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/652] 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 ae7b1ab20..2e3e7ba23 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/652] 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 a762bc7d1..6348f8e18 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/652] 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 cd10a588e..b785dc59f 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/652] 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 b785dc59f..6033ba46c 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/652] Clarify resource embedding constraints (#97) --- api.rst | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 6033ba46c..6c59acab2 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/652] 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 6ea26786c..983ac150f 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/652] 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 6c59acab2..5b45ab3c8 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/652] Add notice about reverting OIDC default (#99) --- auth.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/auth.rst b/auth.rst index d76ddcfe8..52e490db3 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/652] How to use docker --- install.rst | 100 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 24 deletions(-) diff --git a/install.rst b/install.rst index 100496314..ebd7beb02 100644 --- a/install.rst +++ b/install.rst @@ -16,30 +16,6 @@ The release page has precompiled binaries for Mac OS X, Windows, and several Lin # 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 ===================== @@ -49,6 +25,82 @@ To use PostgREST you will need an underlying database (PostgreSQL version 9.3 or * `Instructions for Ubuntu 14.04 `_ * `Installer for Windows `_ +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. + +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 inspect -f "{{.Config.Env}}" 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 +---------------------------------------------- + +The first way to run PostgREST in Docker is to connect it to an existing native database on the host. + +.. 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" \ + -e PGRST_DB_ANON_ROLE="postgres" \ + postgrest/postgrest + +The database connection string above is just an example. Adjust the role and password as necessary. You may need to edit PostgreSQL's :code:`pg_hba.conf` to grant the user local login access. + +.. note:: + + Docker on Mac does not support the :code:`--net=host` flag. Instead you'll need to create an IP address alias to the host. Requests for the IP address from inside the container are unable to resolve and fall back to resolution by the host. + + .. code-block:: bash + + sudo ifconfig lo0 10.0.0.10 alias + + You should then use 10.0.0.10 as the host in your database connection string. Also remember to include the IP address in the :code:`listen_address` within postgresql.conf. For instance: + + .. code-block:: bash + + listen_addresses = 'localhost,10.0.0.10' + +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: Build from 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/652] 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 ebd7beb02..b3a4ca428 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/652] 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 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 literal 0 HcmV?d00001 diff --git a/install.rst b/install.rst index b3a4ca428..7594ebc30 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/652] 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 5b45ab3c8..9b800d016 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/652] 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 2e3e7ba23..f4b291bd1 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 415ce36c2..394a14584 100644 --- a/conf.py +++ b/conf.py @@ -54,9 +54,9 @@ author = u'Joe Nelson' # 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 7594ebc30..979dc3c1e 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/652] 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 f4b291bd1..fdb075f2c 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/652] Unicode Fixes #62 --- api.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/api.rst b/api.rst index 9b800d016..7ba158dbb 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/652] 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 7ba158dbb..74e55f585 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/652] 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 979dc3c1e..7b7e45edf 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/652] 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 fdb075f2c..d6408aa9f 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 74e55f585..c05547b07 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 7b7e45edf..7030616cb 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 dbafcfab7..272c8d9ef 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 5966273cb..1143d7ca7 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/652] 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 9468a4efd..7957cb9b8 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 7030616cb..a7ab1bd4e 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 1143d7ca7..65b3387f7 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/652] 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 c05547b07..2a4480f93 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/652] Add items to ecosystem --- intro.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/intro.rst b/intro.rst index 272c8d9ef..c6509574b 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/652] 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 2a4480f93..11453fe51 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/652] 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 11453fe51..14775ac4a 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/652] 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 a7ab1bd4e..90e612bbb 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/652] Update intro.rst (#118) --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index c6509574b..9754d528b 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/652] 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 9754d528b..426c625cf 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/652] 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 14775ac4a..78d51da1a 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 b3ab8b26f..c59042d72 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 65b3387f7..bab9af1ef 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 983ac150f..84e357a38 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/652] Bump version --- conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf.py b/conf.py index 394a14584..2a9c6b857 100644 --- a/conf.py +++ b/conf.py @@ -54,9 +54,9 @@ author = u'Joe Nelson' # 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/652] 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 426c625cf..78178b6f1 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/652] Add Eric's testimonial --- intro.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/intro.rst b/intro.rst index 78178b6f1..317c5faaf 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/652] 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 6348f8e18..1979de812 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/652] 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 1979de812..dc3d6b101 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/652] Schema auto-reload instructions (#124) --- admin.rst | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/admin.rst b/admin.rst index d6408aa9f..b47e93853 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/652] Remove iAdvize from In Production list (#125) --- intro.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/intro.rst b/intro.rst index 317c5faaf..a8e78ed55 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/652] Add new oauth server --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index a8e78ed55..0e2307b5b 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/652] add triggerfs to production apps in examples --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index 0e2307b5b..ac0da6e35 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/652] 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 90e612bbb..95152e191 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/652] 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 95152e191..2cb7186ff 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/652] Add volume to docker-compose --- install.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install.rst b/install.rst index 2cb7186ff..c54ebdd17 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/652] Add app.settings.* config value feature --- install.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/install.rst b/install.rst index c54ebdd17..c5c81e17a 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/652] Add role-claim-key feature --- install.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/install.rst b/install.rst index c5c81e17a..f8cae8bb8 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/652] Add UPSERT feature --- api.rst | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/api.rst b/api.rst index 78d51da1a..778c0d44f 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/652] 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 778c0d44f..736722734 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/652] 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 736722734..952f87340 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/652] 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 952f87340..c42d7a7ef 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/652] 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 f8cae8bb8..1e3a58ebd 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/652] Add embeds alias feature --- api.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api.rst b/api.rst index c42d7a7ef..861af8dcf 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/652] Fix #132, remove deprecation notices --- api.rst | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/api.rst b/api.rst index 861af8dcf..752b7c7de 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/652] 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 752b7c7de..979874e56 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/652] 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 979874e56..270ccfd0f 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/652] Fix #127, RPC cache dependency --- api.rst | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/api.rst b/api.rst index 270ccfd0f..0130d4c48 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/652] Add example of filtering function response --- api.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api.rst b/api.rst index 0130d4c48..0bf4153e5 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/652] Fix #107, Alias column with `:` --- api.rst | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 0bf4153e5..9fff46ea5 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/652] Add casting example --- api.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/api.rst b/api.rst index 9fff46ea5..9afb86a87 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/652] 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 1e3a58ebd..5355d3b8c 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/652] 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 9afb86a87..3422e194d 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 5355d3b8c..d50e7286a 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/652] 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 3422e194d..827eddd73 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 d50e7286a..36ad6fb9f 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/652] 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 2a9c6b857..05837e17d 100644 --- a/conf.py +++ b/conf.py @@ -54,9 +54,9 @@ author = u'Joe Nelson' # 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/652] Add stored function explicit qualification --- api.rst | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index 827eddd73..d10908140 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/652] Add MotionDynamic (PostgREST + SubZero) --- intro.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/intro.rst b/intro.rst index ac0da6e35..20ac02739 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/652] 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 36ad6fb9f..58341baee 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/652] Add entries to the dict --- postgrest.dict | 3 +++ 1 file changed, 3 insertions(+) diff --git a/postgrest.dict b/postgrest.dict index bab9af1ef..83af75b11 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/652] Improve explicit qualification section --- api.rst | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/api.rst b/api.rst index d10908140..ab0c16ffc 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/652] Add json operators to vertical filtering section --- api.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/api.rst b/api.rst index ab0c16ffc..11ac6ae51 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/652] 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 dc3d6b101..8ff68e6e5 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/652] auth.rst typo: paramter -> parameter --- auth.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth.rst b/auth.rst index 7957cb9b8..203d046c3 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/652] 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 bb9afe28e..4a2bb4f58 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/652] Change default theme to readthedocs --- conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf.py b/conf.py index 05837e17d..f1f263a9e 100644 --- a/conf.py +++ b/conf.py @@ -108,7 +108,7 @@ todo_include_todos = False # 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/652] 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 4a2bb4f58..30be737cc 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 000000000..4ec06803f --- /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/652] 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 30be737cc..c8c5fb53d 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 000000000..6d32f9609 --- /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/652] 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 58341baee..278d94a14 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 8ff68e6e5..dea41538c 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/652] 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 278d94a14..c62d85d8b 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/652] 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 11ac6ae51..ea919b4be 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/652] 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 ea919b4be..0d438a933 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/652] 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 0d438a933..7180ad8b7 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/652] Hyperlink config settings --- install.rst | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/install.rst b/install.rst index c62d85d8b..b0df82b49 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/652] 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 7180ad8b7..d9344c790 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/652] 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 203d046c3..a8c012dd9 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/652] Fix #166, reorder docker instructions --- install.rst | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/install.rst b/install.rst index b0df82b49..ea6ab76f1 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/652] 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 a8c012dd9..ee254594a 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/652] 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 b47e93853..934f2d5d3 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/652] Highlight 32 chars long and fix indentation --- install.rst | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/install.rst b/install.rst index ea6ab76f1..a2805432e 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 + .. 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" - ] - } + { + "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/652] 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 a2805432e..36905b5cb 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/652] Fix #171, postgres logging in docker --- admin.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/admin.rst b/admin.rst index 934f2d5d3..7c5a4978a 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/652] 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 d9344c790..f20a15c75 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/652] 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 f20a15c75..b00821e6f 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/652] 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 7c5a4978a..79585a395 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 83af75b11..563e11994 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/652] Change server-host default --- install.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/install.rst b/install.rst index 36905b5cb..7e09df3f3 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/652] Reorder stored procedures section --- api.rst | 94 ++++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 70 insertions(+), 24 deletions(-) diff --git a/api.rst b/api.rst index b00821e6f..9099dcc30 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,16 +573,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. See the section :ref:`schema_reloading`. -.. 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-block:: http @@ -591,13 +581,41 @@ The client can call it by posting an object like { "a": 1, "b": 2 } -Because ``add_them`` is declared IMMUTABLE, we can alternately call the function with a GET request: + 3 + +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 - GET /rpc/add_them?a=1&b=2 HTTP/1.1 + POST /rpc/mult_them HTTP/1.1 + Prefer: params=single-object -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. + { "x": 4, "y": 2 } + + 8 + + +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: + +.. 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,20 +675,28 @@ 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 .. + +.. code-block:: http + + GET /rpc/best_films_2017?select=title,director:directors(*) HTTP/1.1 + +.. 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. -.. 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. - 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/652] 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 9099dcc30..705cfc31e 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/652] 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 705cfc31e..3c941b20a 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/652] 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 ee869c6da..75207887c 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/652] 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 3c941b20a..dac2e18b5 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 - # Combine both - GET /tsearch?my_tsv=phfts(english).The%20Fat%20Cats + GET /tsearch?my_tsv=plfts.The%20Fat%20Cats HTTP/1.1 - # "not" also working - GET /tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats +.. code-block:: http + + 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 7e09df3f3..3b7a23fd0 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/652] Add pipenv instructions --- .gitignore | 1 + Pipfile | 14 ++++++++++++++ README.md | 29 +++++++++------------------- default.nix | 3 +-- reload_docs.py => livereload_docs.py | 3 +++ 5 files changed, 28 insertions(+), 22 deletions(-) create mode 100644 Pipfile rename reload_docs.py => livereload_docs.py (68%) diff --git a/.gitignore b/.gitignore index e35d8850c..bf70826ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ _build +Pipfile.lock diff --git a/Pipfile b/Pipfile new file mode 100644 index 000000000..52ea333a4 --- /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 c8c5fb53d..5e705d82b 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: - -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 - ``` - -If you use [nix](https://nixos.org/nix/), you can just run: +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: + +```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 6d32f9609..c70878de8 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 4ec06803f..9983589aa 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/652] Note Postgres' (current) RLS limitations for views --- api.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api.rst b/api.rst index dac2e18b5..d0acc499f 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/652] Tweak view RLS warning --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index d0acc499f..08110d422 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/652] Further clarify view RLS interaction --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 08110d422..cac1eea1e 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/652] 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 cac1eea1e..6d27c0b86 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/652] 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 75207887c..588d99d4c 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 c59042d72..32e5a4224 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 - intro.rst +.. 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. + +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 20ac02739..000000000 --- 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/652] Clarify important note about SIGHUP --- admin.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin.rst b/admin.rst index 79585a395..f8bfd1a95 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/652] 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 ee254594a..a0ddc2ee5 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 3b7a23fd0..e641340f4 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/652] Update to include Heroku deploy steps --- install.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/install.rst b/install.rst index e641340f4..ee1c05f89 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/652] Added link to variables settings --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index ee1c05f89..56545bda0 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/652] renamed to postgrest.conf --- install.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.rst b/install.rst index 56545bda0..64bd48885 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/652] 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 64bd48885..aab99d31a 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/652] 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 aab99d31a..41ef57876 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/652] 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 6d27c0b86..566552f15 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/652] 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 dea41538c..c42a396fd 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 84e357a38..6abe84698 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/652] 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 c42a396fd..e061b4761 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/652] 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 6abe84698..bcef5e632 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/652] 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 566552f15..10c82e4dd 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/652] Add chinese translation link --- index.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/index.rst b/index.rst index 32e5a4224..9b4afc945 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/652] 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 10c82e4dd..7d58bea72 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/652] Add section for table/columns with spaces --- api.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/api.rst b/api.rst index 7d58bea72..722c79000 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/652] 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 41ef57876..32a7af31f 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/652] 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 9b4afc945..febf87223 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 32a7af31f..de4bf4187 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 000000000..c4aa0b7c2 --- /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/652] Add Simply Connected Systems --- index.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/index.rst b/index.rst index febf87223..faec2ae8f 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/652] 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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 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 000000000..ea372a507 --- /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/652] Add translations section --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 5e705d82b..f82cffcf5 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/652] Add repo in default.nix --- default.nix | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/default.nix b/default.nix index c70878de8..9bc5b1c42 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/652] Remove Explicit Qualification section --- api.rst | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/api.rst b/api.rst index 398c37b96..32e5d1a44 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/652] 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 32e5d1a44..41b7f75e2 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/652] 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 41b7f75e2..20c411365 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/652] 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 a0ddc2ee5..0800a0810 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/652] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f82cffcf5..f60577cdc 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/652] Add Nimbus - In Production --- index.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/index.rst b/index.rst index ee17eb075..07bd59d3b 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/652] 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 07bd59d3b..3d72f2994 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/652] 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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
%arJ6Q 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 DJ!bmP literal 8149 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 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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 literal 0 HcmV?d00001 diff --git a/index.rst b/index.rst index 3d72f2994..a16caa4dd 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/652] Correct url --- index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.rst b/index.rst index a16caa4dd..ee3570850 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/652] 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 d4c0ceef8..555afaa1c 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 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV4T3g#K6FC(|vL>0|NtRfk$L90|U1(2s1Lw znj^u$z#v)T8c`CQpH@kC9m=l%c4z{q5*^R6(hDQ2pZz$!n*$dy(bC;ffYq#eQCz4gKNABC##7JkffY`ru0 z0p|{`b)4nm0SBWtS8!kZ_}D9{vq*KvsgpBnIKx|KGsI2JV|p#0(R5l)$5=ph();RX z!nX|VciG>)eD|{P=bDf%r#WZ7l+^#ay#8N}-QS-)FJ8Sm#mvCaP|YT=fq{X6BZ7f} zfklUbfq_YkR4yYoie8er4GpX?dqH|h=Q0E^bMg|73ys;rB0huqCOuBS3^=YtbQJ@ba1IND2vhnh^->;th z9PSshqiE&j$q_}t8ugb-o0q${{kpc}Y4(9hF;@fbS9~~-9~JFi{WtRH(d6`VzZQyn zdhc^R+T4F6$?$aewVYL2nscYDeeQg;`E+UD#_I_zwg@;Kb?qP|5g5L!Tb8ZiqpTJX_9U~?SHHI>cVgO_p{&sSoC;R zzPy!9{edfi&d+)zpOs$vqLqp5NZ$Ms6fJXcMg|C*caU&WaXFDEkfSMl%V zjkuVxChxtp(At|4Bp=Kk?em|^}uZs(2NMVV8+XRfecq<7$%>6)5#OE&6Q8%9O> zy);?T;gpQb? z%Vcd|op0w3e!sT%uEN(zSEdD}Z}OjT_hQ!h<**z8 z$i4E;KhIpxGfraGSMCWO$hew*rt0;XrwwdtrcDbj^4Fg}XMyW#xvMkpn68W7YrtO@Fe5($}=IIV!oiZ>{dDp0;8y^_tQZdGmS{r+xnakTYI?zwvYQUA}lu z^!T~`9|}!2TwLXTrfR9_x0KJVm+#L@*Z#}6I`93ycKIrm4g4KnFI>1VqbhdU%|MN> z%UsUuScIB|Wx77sc_(AiA30S!e7W46iH{9WPrYrg!11l7D)VXk^LqK$XV#qk_u`P~ z%{NzOZ@MG-M6a~*3dc%!ZBFgS$NPgfZ|U#8$@3wo^yXdT z^fghjFW<<9xG&t(>1NVtA2{RU@-CK)xz2Nx=QgVy-23R@)AKnIr?+fhAflK)_wA2g z;_|U)c4SO_Gxgeylb^%o=2{&3_@G>zY1%8Xk~``Wog&kw-VcnDs%MMad{g=7$7esD zy!SmmdzPQc75PH3ZTBpH%uCri>+IEYat0x?t<(HPR`nfH<<~iPVbQXAQ5QN-SYEZf zvAiPqTG!P{dn&DXrYJpGu=x4Ds(+8WB`>OH{B#Yg`LX~1kLmXd&62}bF}{A#SYDde z^2#{;?CkpdSNyxYwY0o#p6|)2Y<00UeK_sz7WJ3u7n|l^%UM@cw*SYES6)9$PxG(6 zy*vNen%zcQ-sdET_U6xRE z@8UOU?7i>Qd#roi)M~pnXn(7z@~qqR>~+n>!mlSEi_e>DbII|Tmu7;ccX-jy zIi|1M<6`(#npMkYr2DbvhTO@2jXKQ_B8d$aCLuFO}b1>f49lxjMyT6ov7V0zG!u$ZSClzznh z`0;4rH~r0TyklRziTe5Qr@MOFa(Av8u88RES5|q7=elawx7n{=dHe0LH6cdR(%ch^ zKX>lh``akHBgjN*^}0=c7jIs-)R?|y(w=uSUxYqh(VUl|8MyU!T#DVL7&AM;O;;oJ zT{Yq+nB;{|S{u9hUg2-62mKN0)0eDX{CwZ3dv7GB+UKU~wEGL*@(ov2ogDY?YWM1} z@L!cnCwf0#XS;vf)yM@#(>AOBGnub3<;?YK*XB7c&3Zj;dSy|-y4=&#!aWMBYObF0 zSaKn(^H#)GP5vEa>fg?^ZM81{zxqz@Ege_WH?}O6v9>pB6uU1^ow(6&-u8{#`X@K2 zmM8pr>1A~M{It*|AHR9){h9Q(tNZw?>#67FO}sXz%Ea#G%W1cKH2u%|s)Lf6U&FbH z>g>H&)(J1${XKEf%}H7kY&%6K?mj!qH}qGc?d)ZjoxlA&>Rx~91v^`A&E@|;C;t

FQ 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 Date: Thu, 4 Jul 2019 21:14:18 +0100 Subject: [PATCH 235/652] 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 20c411365..085eec6ff 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/652] Add websearch_to_tsquery doc (#226) --- api.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api.rst b/api.rst index 085eec6ff..ebe0bdab6 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/652] 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 de4bf4187..ebc0ea979 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/652] 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 f8bfd1a95..12af107e4 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 0b45f8713..5c8a34dad 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 ebc0ea979..807b87c78 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 bcef5e632..71c288d11 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/652] 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 ebe0bdab6..34c8d4abf 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/652] 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 34c8d4abf..cf529570f 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/652] 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 cf529570f..728e485af 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/652] 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 728e485af..074135b58 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/652] 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 807b87c78..7f6e59676 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/652] 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 12af107e4..26dd090f4 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/652] 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 074135b58..d1599e02d 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/652] 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 26dd090f4..f7075f9a6 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 7f6e59676..98abbb3a6 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 563e11994..0faacadb5 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/652] 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 d1599e02d..34aa00188 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 0800a0810..bf6fa0981 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/652] 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 bf6fa0981..4b770cb24 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/652] 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 34aa00188..c54b01937 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/652] 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 c54b01937..8381f8564 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,16 +553,19 @@ 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:: + + 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 +------------------------------- + +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: .. code-block:: http GET /directors?select=films(title,year) HTTP/1.1 -.. 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`. - 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/652] 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 8381f8564..7161eeb48 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/652] Refine embedding views section --- api.rst | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/api.rst b/api.rst index 7161eeb48..efdff6b20 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/652] 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 efdff6b20..7c9ed9b9e 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 000000000..751706464 --- /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 5c8a34dad..f8649000d 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 9983589aa..d4d0ba6e8 100755 --- a/livereload_docs.py +++ b/livereload_docs.py @@ -6,4 +6,5 @@ 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')) +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/652] 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 7c9ed9b9e..ca8212c3e 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 000000000..f9d13afa7 --- /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 751706464..60265dd40 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 f8649000d..8f6bdf28f 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/652] 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 8f6bdf28f..c126cddaf 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 ea372a507..000000000 --- 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/652] Add server-unix-socket config --- install.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 98abbb3a6..72a0d9fad 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/652] Add materialized views mention --- api.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/api.rst b/api.rst index ca8212c3e..c5904779e 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/652] Add db-pool-timeout config --- install.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install.rst b/install.rst index 72a0d9fad..0e288d3ee 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/652] Add bulk call reference --- api.rst | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/api.rst b/api.rst index c5904779e..d0470ed16 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/652] Add specify columns reference --- api.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/api.rst b/api.rst index d0470ed16..82948fe79 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/652] 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 82948fe79..67a911b5b 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/652] 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 67a911b5b..5e7271de5 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/652] Fix #234, reference for raw-media-types --- install.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/install.rst b/install.rst index 0e288d3ee..0c71f25ed 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/652] 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 f7075f9a6..363851656 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 000000000..a64c88579 --- /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 c126cddaf..f60cccf67 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 ------------- +.. toctree:: + :caption: Ecosystem + :hidden: -* `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 `_. - -* `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/652] 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 f60cccf67..021c1bb5c 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/652] 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 000000000..1bc197385 --- /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 021c1bb5c..3d43f32b3 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/tut0.rst - tutorials/tut1.rst + tutorials/* + +- :doc:`tutorials/tut0` +- :doc:`tutorials/tut1` + +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 0c71f25ed..5c1a0777a 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 d4d0ba6e8..befcaa35e 100755 --- a/livereload_docs.py +++ b/livereload_docs.py @@ -7,4 +7,6 @@ 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')) +# 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/652] Collapse index sections and improve the wording --- ecosystem.rst | 54 ++++++++++++-------------------------- index.rst | 66 ++++++++++++++++++++++++++++++----------------- release_notes.rst | 32 ----------------------- 3 files changed, 58 insertions(+), 94 deletions(-) diff --git a/ecosystem.rst b/ecosystem.rst index a64c88579..1ffc96e37 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 3d43f32b3..3b49b816f 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: +In Production +------------- -Supporting development ----------------------- +Here are some companies that use PostgREST in production. -You can help PostgREST ongoing maintenance and development by: +* `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 `_ -- 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``) +.. * `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 c4aa0b7c2..e69de29bb 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/652] 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 1bc197385..f44019b1e 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 5c1a0777a..fc3f278cd 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/652] 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 555afaa1c..50359ce05 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 5e7271de5..97f3cf8b7 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 f44019b1e..d44096de4 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 3b49b816f..41b8be7aa 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 befcaa35e..03910bd6a 100755 --- a/livereload_docs.py +++ b/livereload_docs.py @@ -7,6 +7,7 @@ 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')) # 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 e69de29bb..000000000 diff --git a/releases/v5.2.0.rst b/releases/v5.2.0.rst new file mode 100644 index 000000000..43bc18833 --- /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 000000000..774ac61bb --- /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/652] Update index.rst --- index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.rst b/index.rst index 41b8be7aa..5be91d9d4 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/652] 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 000000000..20fdb43db --- /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 f60577cdc..e8a06defb 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/652] 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 774ac61bb..19176d5b2 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/652] 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 97f3cf8b7..42fa40a7f 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/652] 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 fc3f278cd..685d28235 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/652] 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 42fa40a7f..1880dc9fc 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 000000000..836a2c78c --- /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/652] 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 363851656..9d4841c8d 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 1880dc9fc..9284d3eed 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 836a2c78c..8096b97ea 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/652] 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 9284d3eed..7dba6729e 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 8096b97ea..886c4449b 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/652] 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 7dba6729e..19a7d8518 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/652] 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 19a7d8518..71948cc59 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 886c4449b..3085f0a92 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/652] 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 71948cc59..ca5187cb5 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 3085f0a92..72293b8fa 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/652] 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 f9d13afa7..b2b25128d 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 60265dd40..118d630d1 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 e061b4761..038b92f6f 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 71c288d11..e9fe78f54 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/652] 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 ca5187cb5..871d1a6f4 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/652] 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 d44096de4..267ed24a1 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/652] Remove keepalive nginx recommendation --- admin.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/admin.rst b/admin.rst index 9d4841c8d..8334fd02b 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/652] 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 267ed24a1..c02d8a62c 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/652] Typo (repeated word) --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 871d1a6f4..06082c2f7 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/652] 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 8334fd02b..a192b304b 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/652] 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 c02d8a62c..6317aa4db 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/652] 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 1ffc96e37..ba01d1435 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 5be91d9d4..967a33ac9 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/652] 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 967a33ac9..eb7f581f9 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/652] Add GISOPS community tutorial (#291) --- ecosystem.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ecosystem.rst b/ecosystem.rst index ba01d1435..6e0317bdf 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/652] 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 03910bd6a..7674d11d3 100755 --- a/livereload_docs.py +++ b/livereload_docs.py @@ -9,5 +9,5 @@ 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(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/652] 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 zcmeAS@N?(olHy`uVBq!ia0y~yU_8RW!0?cRiGhLPrgl&z0|NtFlDE4H!+#K5uy^@n z1_lKNPZ!6KiaBrZraOqZOEWyUc~|?(y(P-NAr4i6ERILpL~{~4J7?W6E!!BL)O+#C z<%lHlUe-5zT{d1g$i$@8CClhSeaAE26`~!a_nqg10~Vos^RD|Gxx$7=N(2xVrL68ZB8F{jrvl zhsUPsOUB>V&IYUxW;4z-k@5`)n6O2@v6_%=4bz%W{yFSEPlcmN!NYQWum8-dTCH;b z1{p^ImX$vbOa2QF|DES(z`EdQ!o2wwMbp)1*|q+>cwA}Tb^TdPkqrr44&DcCYzx2G z?XS_&dUjmK`X|q#%*BlfTn^lh%jNB*ln-zS>X>y4eDzhFWhTe5jA0AUm&wWQkAFxg zPCMFf%$7BQBhm59$H`aU&z_dzvQF;${$%Oc-mte;{Bw4_y?0#ddg#^I-tw9mLY@LF zD=of6oIG9~eY_-V?cK7lDGZ7&1#y=as@n5@c$u(l<*MYTuk1@c?b__`y{hKl)%yRl zukD{)n;o0#YRatb{{FyB>&5pzEM2F(y0%uAhauzJp~Ht?|JcekdAjumL8p#QKfZoz zPd;qo(IRm0W5jGVk@aCBzoW&^dev$K9X)rjsa3$K<=BhMHkzxZ%1N*CP-x+3Z9IK= z*NVv;imSeUiLUY4Z}-*Pnst%a8pW&DdVA*{pJKX1MemIF-m3Jzkk2t?+a0>BHr)yr zZ3Kg$T6b_9UXk=+?d*OV!J+6f(|`ZbUZe|BQow!2D>)PfhR|D11D_#)%$AxH!*Ta z%ay7Q7rolb)MJrtu+p~UnUJ+a}Klj@-WdWxtFZHLN`c?a7b^WidU8hztf2&V0csBQU zV7Cj0V&3=M{L@wwmIi*d%2>5><;vH5mNU9vhq|ZzUiFN-X=O>|;&*p;e*W~-d!-%I zKH+)u=C!r8`QGh%Kh@yji;W_RP8~*E5)V!}a;z+qz7YEK#s2vl6#m^f*(14mtwswA zXLw$UF^6K=?~R|Ccr{xDoQiUfpXZ7yPkuhTt9td{b} z>YK!sNY9YQoU7(1yx)bm80zh)d?}~D!~EpaB0(pPkNW2e-WOF@Bsi>}clS=%(e(8~ zw{z~6+MUda-=q-wf|rM9%Exz(-hy&+dROlYP3U?uPxJq~9PPmUS1K-T4Z2wEbFQ&b zE4B8rLE!x@4<0aFJi7MRir}U5?a%J{b-BMgz;*iOc~_6k+P1&)2)xHJkT3<8vZLwy!&z$+c ztHI~v;(odF0!l3cf0Mk$EuFS7c^Utxuyf-`biA`s_@#*xhjGt~-AWuzHrpnIEVT$Y zpY=#Jr|hVyo}R9z?r)Qms~96=0!}EXyx;fb*51d7k9Xb9C~~nr_-D?Q*X@Vuz7@aP zYS`HyVEgJsLA((|@M4A^$3DK7wf)$$Zm$gYqyD)biv=fLHovk+z-fnqrQzSt>|XL} zOE`|cy1qF|*Mryh;sMK4Q|nbO0luDhlrDNmF1)nu|C?NKxxJO2U!2-!8qBs+T2p{S z@l4A7h`+x@iTM&41r)F8$v}x1A_=J_XuHOF9cIM~b`_?b{xBt)<;!srke|9!gOutpjnr~lA z?>qdgoP08jfx&mlmunLzN18Zw!R?+%BgNsIkr}XuI9b{wtfDbe{<^hFZpurQsZK; zW&9bdXMERrc4gbziT+)ND~*37d@?w)HE-=VBjueU!sqMwGW(fJ%TF9p2#4&MK@;Q-E-KF#M zzlr%uq*|}7Q8_Z>XGLz=)q@i27H*Vlo$zDH-(R7>Z8Gam>|gq5x32HKOWyNCOoPjTW^XwJG^! z;KAM(_H{qiUk9$eme6JREH(DNQLRV$b+ff0A|WMbZ&mZo+x)lc>Bi5O!ULj9@TR)41mqg8eG1nEBW5j(xG4O4N!6ds%ZGmUE0^ck7ks+( z_lo%YPn@E$kInM#{$lU{t+}B0Q+UbS;_ob5q-tlibM#(Ze(%k@vlC;R_Xn~#wzRkN z%h~K$DSatP*0LzlzflLT|7eQ1-t>N^(fjo}vUXW#uZD|So(#x**8F`Yf1cdk zUH8i0&1>%dTaxgHmq9^V*L2o{JKFnyeT}Vi74hT@iaUPm+mA{pHYHyw|9kd&<@=zdq@a7XRmIuK^~QRQ0(zA{ zj`wdq?`XlwaKfdVXIaUubyL~nE=ec`E&u;*_VM(ROS-$)p1k&P&bRkx_p`s4#F6;x zX=CW_(`u^Tr_My2|I=Tp8aic5_c4!c91Ix&LA>F$(u;o{+OqZCBG%nHzg0U;d#-T` zO~28dCsu58efebx?-O8MUp-buyw=;Na5Jg6Il*Lq<-dXW1&@Q-$r|r$o-SsOJTLWHR;oWPK7I*r8&C2sBa`&n}$urLW8p&Eo=>^QUFt-Z{oibw-By1OBl%{d=+fLc9 zNBm>na>{VFxOj?Y-*uh3q<67c&d={j{g}(89Lk>l!mCcjCKDS%fv^O|< zqfXbC*U$AUBovSA`4=Cq7ua@1LNKsL(8X$W#8E_?B_ckgO7 z_R>vPE@od&{c-2*_4p}uXFC}fOcser_)MI%INDmwiQ}UG`WUa(inA`goUY&P_A$~d zuTN|0uJw;*O1_?XW8*we(d>J!i&djp7)(}7lkm}8-B7nphDlMU)#hrKfodQX4~Ii+ZJZs<8u1y`zGT3lCG_pQ&PGa8rZH1HES-q)jQidV7d#(#r1r> zpZM;HO%E62e0gWa{{9%w;3PMm$$16RgfxBvUs+5d#>CXZNs~sw!+re)K@0uO6lKJ zVp!m4`o-niGUW02pqih?a$XM-XCB7*{UYII{n}ADwWVRx#IDYayIUC zG|1fF=sVlYS8acJKz&W(${ zPvqKvJo5Kj{rdmfpURSoPIW6)#wKe|zo)F`Bk?d-N4=82D{o=A{14fm_pjBTJ7Qb= z^;fRk;x*X`DNnEcU#@A-UvwpJqIpuomNjeE2sjA@NuQl*oW7*Cq>}r5I+qmt6XV>B z3Fo&=taV(|T*nXUU|{c&>y3~54AUGz z9hNO2BH@mno}TmWYOLmn{;;H$;lTIaxBILt^GlQ8@>VTlXqcvzA0J=2?e3zMO)B|& zo=@y<|8+w$Gee^Mh18xG6;T^?^#3nR=VJIF_hFLx*Kczxw_doAec{BtU#k7*@@22Z zdaf4P`1)VQU$A90Kh7Tf*}c9l%lPM=uf26qDozD>2%dw21L9D;->mSznn+ggq2}s|VURsx7a(d*!xj;fVu%zy4g|Zuj43RA_MO{Isij2@G5eA8a3NnP2tnUuP?$o4nrp zJ9~b=s{SnhJ=XF=_WgBp*IY>{ek=lx1+QF}l#?Cj|38_*reiC!A!N_7X~tXats=ZY zT~OT#4hkANI&1uW`Pvt+Sn=a)bxQ)*t5$KrMYb=l&B)H2y(h)On7^T|=-Zo{7Z^)m{3FBj#E2 zswuThyELB9=Zr3WoP4un#iuj<3=L6^=S4!Ua?F%(+~UG-RPpmuYOwi}YC#bZmACE- z(z#Y;e*hbP!I)L4ch*nAhH30gnhqQb7cEkObfI)mN6){sQJz+ z`Sj%EuPc9^&#(XW_4Rkt-;eBVHx@i(zkZ(b=;B&;UUI{WuLtTU-Ai9CBYF6~LB^+*Ig(EU7Rs+FnWoG+DcAOGRb*GG z+T-$k*GUQ-O(`>dcHYxTEB|P;SSdekib<()!x~2e*51yOcXoej`~U52a%{Aw)(eZC z8@u#XQqO)nzQxDPZ)?0j@T9%F`!_y#bEmY>rbR&M?DBgWNfqMiKick{zwzesm+k5m zD|b6pefybsa%ax6m9v>rc$zdbTrA!nn>xvHaqQF6_Y=0un#R05cJ=zE9-TGYgs1Ox zJNN6g=jPcG(X%!wTV(j({(Abah3{Pvg=zsGN9Clr)MrsQXLc|3Q7pZE^Fw^f$z|D5 zemAQhDlNWS{4L#d^*#B&6`!{_zuh)3=Fw6<;WJT69Lp96Cd%~(g{%?r;&65C3fFr4 zcS2NcxR1NEvvTpQK(FjmY2SWodn-=b79rr{*zF?K*7Em5(a%rPU#o8DGsN?Q9LzG^ zTRgmuflE(Ir0A(sLcGrE_-kx;W`~;zvCdk$r?&R#(?8qy*VI*gbZ}QTbxoBm{=QIK zZ_k4I^&k1G1|BGi|p0iu^>HlZy z>py&m_!*$Fd76RcZuT!XzfIHppZ@LS#j?NK-~PE-p18&#&HASoD?8io{~w#Cy)u6I z@ZqI@QTthURx>{lSSecZBKD_wSXJpF!=rDvz2$A5`KJDt#ssUw|ITXaKRK$CaXe}F zdSBtB9&SFq3C=pVSq+yia{BeMtxx92CSKio=Na3s%{{v*ev-x!iDawC<@v0ut}d`W z|06Y8c;Bz`7S5wDH<)Z}v+vdKD800{q3Y8U&!{LVl~9(eokDrnT1_NP*9R|`uQ$5C z)$GQ!xphSk`Ppyj?8^+^nylq<=fdXihi)_Njy$fX|9VM8t#xUbhI0m+&is%5-&3cD zG#&RUwODX*!YoGnFV{8YrzO9XemODXfOvw-3f@~&uko&Y5oYtbaaksl;=%<PTBUx;z#?Z(pkJV|)HU|06K?&gN}6l6(VorAGunUUgM&w%|8vha zSKKju^~|{A8`!75yD7lc)ipIqV&RG<5s&8!|F!vRsq4dAoGd(lV~)5g$BZ}Sn<5<* zW6xhX1p=>LTwJ{AhG~W+N0Wf+oi+UHelFPhoPF)okk(xL$DdY928An}U4B<*Uf9>z zX;-(r-g)(2ZpoR=_J3~q?~SwjQ`R!8N8n&-j99Jxm2bc6!W3Pv-C2J4XLvnkZ-kH5m|477^l%A6{pO$Ve(KO~Z7W+KO=+oElD-j889NE*GKRpPS*l%v**YIKbolT0-Utc^wck;&8nsB$D zF${Bx>-_r9-A$kOPh)nq!v$kj1{3!+X8-=Jt^Zx??BU_JVEWQW$JxdCWPBA%p1hoL z*gJenwA?H8yU$KsvtJwiV}1P#74z@f2O0aSe4h0lX4k7N;JN#M^VFQY%?<{vy^6NB zwpve%R2|w6w5r zW!(S2zrQbf$t%OxzDT2G!kjrWP9DC#XAc}$P*po?nc*=Vu|IN0H7eFx?ChVl*Q(}v zS20)Gw|BPB;;k$?y5`tFKYx3>Tus6fo@E@G7Uy@1T(0}t)GN4r+nm!6Uu-X&|Ka>Z z%Y%GcMo|yA zqw^cwE^l#AaNx+y%DNU2A0NLz`}(>^@25a;tf6mT0+rIrX{1b1C~v4L;j2qmVzmPaiy<&o;&2&iTN; zY_Sbb5)KGS$~8)GEL&hGAt~u;Y8&Ye0?Yq>y?O-%Ud6`;2XEeH5v|P3+$`0ckb7%u z_VsnKyI;M39QE$|%-OSzeKwyeyjsFBZO)=KkAzMJuBa8-1gel1d<=P+9Uc%EnE7NH z!_4N3HJAhbb=pxO;I{SjR?%N}rVy->64>~--Y zRxf7Od8?$?)%;AT{=fdzAB`1Leio)|J=6c=;!$n=1S1s&)27O0_k~W*l%Ki$yMCOX zhnJU^S5N0I+4(c|Q!Kkfrx|@tPMaZZy~d04^siX6=9CF;;vU-S>gSo72%kTTUqYMfIakL<~AfI2Unc7-@Dll_4e$|KKkeBZT-C)XVz=# zJ^QllWb729Jlh`%e;M{Pect13sP4Rb_0svbHWi)iTfOtG`k&&*car!cQ$bBZ#U;DA zm3Q?s@L$Q++puBF4F6Nw1t}lul@9CcCGO}diofzzg+s~4rsl(ggIhy2G&FvEc-Z`6 ztA_8Uj;1E3-j$z!)Y$bOUp-GJ(ndV1bkUq8kKT6#S%k0myu78N?^V~IwU(ZBpdUr{`9`NXZj^2C9fJkRBo{ke z=)LRKGGXe}t1m7tUj4*7O~NfMZr`zAqBFmFPI^+bQ}n&Irsma%`*-ftJUKB@^uG#+ z(-%!vR@Rem*9Sy!odBZ6!ncsM#9ymDpBzpqQrZdtzu z1Y-XGy0lHcraggy%f#&8W#QtgZCArWE^K&XlzD&gqh)#i$&XXrT<@KkkY{JHLjK@e z$!9uF>yGz*mrzv8Uadd<@lL(H6Q?;i8?g4Se(-A!V|N_?AN7~DWv3<@{^4y_n}2HB z#T8tC6P9k4@tlzJI`cZWYrdi6{%Y+>ERF&yr&CUqHa_7sVTyaTZo<6Ux`-k@=}9ss zEM2P?ELoy*KCdfN9pv;RqwnkXJltRVJK}m+7nc>E^0s%}o{^jX7OwUGIfd(LouiNE zw|92Onl=|-Kl)L?sl#M`)#8qav2z|sSJ@|T=HI=}*JRD5DY6G0t8cA~e;59FZqkRX zH{PwD{F!T$f6t}7rVK7nLv2p^W3Q77F0K@Ss zvs2Uc%xpWZFW947rw>NcW&o7&6yLHv}#|Ij}xkRP71i3Cf zl>Knxdy$&<@8%N%MOqTLqy(RR)Hr&o^FOHJ{XO;O+AR01ud#-*xjemR8a?0X`oCOz z_42mml704t?=PL(>ODQrqUQA_rQ=Q9r_KMVn*~qLesbsPZMSBHD{J=t*=;F#Dw<{2Fq8cMM^xm)fc;aW@)%*=%r2Ur-}|Z{65;PtSqO$ zY5UR(%ex<@+pqdAcQ4pEEu8BT>*Xz5wyasBC25ZcI;mDCQ9vY>b~8(t!^b=TyYPNw2GwY^}=Ua+k?;Mt8mqb&RUioKV6$M)iPm$t(TqTN0%AJ-##2=`pbB@ zCS_-f)VrBYZZ0b-f1i@SEgW3fAMzpN{Xr*r`@d|ztV2GI~*;hS8en_>f z+Stif_xr`h8-EMt)R!MW#44@7XVM?r?nRkFM^3KjI2_*P+uT;|86~7+3aZs6sx5w2 zYW4lGfBpUX;Fw8sOEONaV9tHkw|V8Q(*i1aFS1r>tbe`nMOa|(?UkFIB%O>Ce)4R| ze5%^H^}zO)$8r~(r?pYQmw-!y=3~Xjnkvw&lg`4v$ddxfBs7G3Ev+rW!Cn$Rco$l6}(W&UmtFy~^?h(p&yB z2Y);0zIx70mgBj;T>+2y_9Qg{zbmTHVt@4b5=G;5cilyovOij_c<@3XH1ByYe{i3!+{MFZy~1739!R{f z>h*i=wBsM*V`A*r?Vh`ES65%}`3}?YINKl9KKD274tj0u+4}v_t!tTo@7$8e)YM$@ zBC>)fuEJ<;cwC3Q>bv>&H!cT-N5*-c-}Zjf=}6Cy{4Klu&Mga(UAkw*2FKcT_dOoA zM%4|wXXb5Rt1r<~aPaWr({8`FnqLVB%DlSl@rKmpcIp|&Sy!r0?|*UWU`)8H=iw9k zu5YP5=~^o8sZz)H+DeSUK{TlMula>58VCRG%#BzQ7HzGpds^@C^44?nFWipmR51Ph z=FAeqV}GJAM{l0O%wK;<;`4exlO-YTvf68YMXl+3@J`2D?Dmeei=#i>np_?{&u*X0 zwcvgg-ea*Eha`+E%cqrJTM{8<7VErBe}?t4&5Ge?k1m#Vue`8uk5#l*&93?loU@m{ zm9VfZ$$T*-!CqT*(t|DW63Pc8xQ+V%CFr`JdTIS#+TOe*t)pGe{qDh=O-nC4pP#dA z-K7@SZ8@RWNkDpS>6YsRmh8T$%O?wSAR%oEO27C&BSOC`sM zY`)_>GbsCZ`ky_j+{;<3XPyw$*?jVPm#VmD*h?9&oLp0mWp1VhGo-Gb*r#yKv_0Vz zc!I-mgD-oox1LV5_U|JcwUai@sBG_ldFJdrnQgCUg(w~?xfR84w)}D1@{+07lS*fV z*Z(TwnD%nUiSJMCtN(5FbQ3vVo-KBEkF{8m$H~Zks~P9CwW~K=YIRoF_WPE}N#XMb z$BsTf{wlaDE?a)uw7rh|?_6qie$~7C#NnnXy>?ntR=r-+o&R!^u07Z13F-p74{u6Y z_$27ep8Ge0=P5+2ZO`dGztr&9jOja0#ZJtX+3~rsQe?^IGWHddbFQ0eI{o^YmTx)j z>6GlBsi{k!*I!cIX<70jbTr6Ki5GZ1>u{=_{=>RO4JPvY zv%l~7mb(2h$JsnaSVZUu0x?Po7$T^z{d!MUz{K z?24y-TUSxyc=7d`{)oGGoV+DAYkv8_{(EQpc79|1Xs=`2KD|qE2wEEx>-%a?&C`-7 z(Z7#w-ptHBJzq6u&cta)3*N-Oxxp%47yVoQ?bbgJUghmd`8lon+lI7>ulml39ToD{ zRPop`|G?rkYH#luu4|U+6gX$Ov8cA^)P}RS>obD))y&9w`ZV{jQ;nSb#q*cvaSG`D z`LHjbGj>?b57kbJ#y>9CGCtumvYl5ZOWXaBIqR4rSiLm z&--@PpwHcC8EzKOsvo;Eh}LB;b;2yiiYE(zZG(d>iX*cF8?h_ z{oNR{?)s6swvtDGSsFLZ-)zdg@KVbQm4#9A>fEb7W$%lSbqp#|ia&UH=iBGXk5ygN zFK5-pu08pt*^+1BT9toycM*hY2h92Fnow<<{ems9K?meX`Cilc1Wg)Lfj~ONQ`$F!W-J0}IZSLg1 z6BX~;Fg3S_eYsZn@HD&sCegERO5^X_R{qb`-oDRPea#!oL{%ZNJ$rsfPk8_8?!&_3 z9?R$1nzP>Dj7&No9Z<+OQ^oW6r7!gc_m^9jzf)-8VEU(@Z3CJOyqf>({;|(9ZhYry z&Wo)-^FHUeBgex(KkVImZ+~5O_6zgko{yWIKi&+fe$H3KaZz&P$DKOX)A{abt)G81 z>`G$Xkz@N7-`Z)R5%R`ihLrI9^P9?xpGd8WTxF>aq6fn&b!txJ*lszNd*4J-M4v2j3}D@LYs!a*Pgh48>-2cG2>gBa z=gvlHp`UB~^!Pqa2?)|mns@Zbo~5^TTZMh;y}4HRtj4{)#o@(mUxaOEEmV2>Uf@3G zb=mFqJuhm@E?$`b|MRccFOPm)%h4j>J#qH$U$)O?M9+RU@9)ZKi>&^w7O-!hvhB#V zCbh)W)K-C6X1P(IX_dlfXJ+Q>&p39!-sMK+?zDBUUVg7BDPMf(xVhi_U+&Toa=naCn?);@`Wo` zTsU&??AW+c`qSPMr%p}!Wz1_EyTH%-_!pU-xuM4_P_G=+6*SX z|KE6urzQRWyHg_hU$3Uxlv5KHvxdfZUlqJFJNM|M^(Nwz%AbnYdf7fn5j-Z5tY-f1 z{ruY8sFkZEZ9)x~CAJpMSiNyqQ`4-*slok)(t5Vu{PSNV zUtd<+zjY&1&&xNvqmNZYzFf6pw#Co(GdC;MKVEnux7SI(Yng_p##8sdSqFn&Y=58p zbf>rKc|U#0H#e6}-Lz;|qV)L%J9od|D?CL$$nD6Zh4ZFWt@`vSi|yLEf63S9pVnBl z*+b}KvBu7=rp^YYs~QhpS>g7Lbz|Rwh^{p|9UUDx7A|)0_mbT_o4M}caVN&`$k>gK zy5yeAaBY&1wEk3d>isA6YlPv)+}X zZTofd#Azo_B#XZM_2IZTx48Qht3K)Lf=ZeU4_=%6wY}_ic-#9I%W6!Ds`@9~aEuEp zk{9xOd!uyiiqEe;aK2Bw`QzRBb+*OwnIA59zPLK?e%d;fe^+0AI~?`qBIAXe8ux=! zHqEFoXtK}$cwhWqoY29G#`($i37^;gOun;4^T~%FvE6$&p8t6FMpHjuL)v3&u4Nn( ztL*l_+xIa$*qoW+0#mK~cOOQE`$q6_6T9=5Cal_&1Df4NUi*7tj>vM?bsR76oQnwd z^gMlK-S6|!d3%!u>i$ljzvtqT?3#mfBd1N7^Z)*P1E&=hUyoc$(3e)$Rjo{w6^_1d zm?&<y=7BHZN2*!RI&@%pDekh2b#*?d~lDY*}m5cRG)9+-Rtf* z_riMHnLDzx4s2NKFLBl`arrm@E$N$c>TZ>OyRvHDE0%u(mOGz6NKG$Z`@3(xZFNn4 zb>6FkmCV1^c03fms=av9kJ5jAw$A?h%|FTiem&(N-=W#B-Ku`-?U<>5xZ>6Y?==U$ zGaM|s#P)5jj(Y07oY&`6zaF2XsZqW7!>15w-rWDX=U;4k9<{;t+Omrh$!AjjUv_W2 zBKa-f@26Ex+?l4bHJ{wmLe}}D|9Q98VXM@Z=aD}T6+T>D?>-}74xhI$>khU18Ov%J z7#uhho%|fzNf{P2mJb*{y(EzK6cM1r?AI=FP$~sTm3yREKCeEpLBO; zG5fV=&Fhz5tH0+ozqa(qm*-myjL-DShAnML{i`?Iac%0Ql~eR)L`6lNI(_>4ySve1 zyV-P)FY}%4!fDRsea-ap$vRleMj83M+;e}B3(YnxY)N|Lqz{rz|1w-)(Nm|(tf%GKbrOSqTK z0)q)5f*~6-FRKL}@@3e^_W5P%tJO-P$@eD~|8+a(y5q&qUR~k#bMgBNuFufhd`A1~ znOpL^9;ulY)ZUj z=6dbxwn|ROlsDl$vE}{+h0Ary*~eynYwhfQdqec~u6@QOr=-I9u0QXerM|^eFE?-B z+G|V~Dx@@LJlgZ)MoG%0gK?XeO*BrtBvfs7@a>k5FR%Y~O5G|OsqNY4f6(om-`Xsj z+mpPdqhp_MsN5>n@NDtKI-#Z|FIe`?S?fG|7TcbJgNv{5pPCgm_s**-g^ekjZuPC< z41P4(>xpd4t?NHh1WqpPUAwOM#-1o{2jfc?i+`4+<=m;hbnBqs#$`cHwJ#Q4civy8 zzv)(A&%--Memr;CZP;fjx97xvyD{t}iV7yW?G1+bb^p{?2RZ>%s+l#IJAjd;R>{F%NG~ zN7wU*l0O`p{Blp%<`0^M|Nk7EH*MFcPhM5qYNyAZ^XjTsxKdhVw)B<8Q`c7BSJu{# zTwhJR$$aN*$?n|Rg9)x!TG8I3Bk2THsX{NJn>+IE2pWoO}EB^Jl@uqcQD)ko^R!^A4L)Vwv$XG$`^7ZpLudO@?PZRq|6&7 zyfgPDyqj^ZEyh&5zf9(-WBaF9HtdI%|7M@PA@WvV<7DUQZGN2JHU}wk`u%P-Wa`Ba$^3smq){W!`_!=}*4h`Pq1|R{kh6`25O0p6{TMtnkl$ zf6~wIH#^z!=tth!MKjBfJMm~*z7Ax+T$!56AGWK2BV^tl??sP3Cx6v+UB8vz`*i=G zr_=Ogg7WXU%84z>`4YG=BsM*^CfcmN_rJy&=Xnlq_)br1o#bu!xh~zmvMFJ!+$LSS z>Bl6J;~r_1yKXCdaPIiizzOGW$u98Q6nlLMhw7XH&-<+@kp4d-wEyorqp81c-Z!6y zf`{$jTNi0(uKWM(defuJ$y+#%_;>gTZL&V|CwpFZ(ai+AZWW23yL+~(HCAw5Jh^4s z;>nt=KRCY&v&3xQS?^aKqIG}6p2f3er`P>`B&xeJq{bRFa-cHh^UTd#wwL{U)41q* zr2NKRJ6oq2qy@it$p5XFKVLpAZks@F?$f&`8*%1{|A9#lgr*q zTzBks;dnkLVyB&3>Wyb_?YB(Szb$_A)al!8tv_t`EfLmxxqHH!&#dX?1J z-M)8oUhj)9Gj_ymOFyx}%RY>;-+!{!t5p$6nwr~WU6vm`Qk%5<>-)v=g%wTghMh}g z*v;7=-D`CAx3IJC-~7Dh9+#f(L&Zw<^546Tuh4Wg-MQyS$J=c2^M6vG99ZYKws%*) zpL5tdy;*yrkNfWreq*S&WNQVp;jxa~_s<$srxgj#6_lOqyH(z9+2R-VTyv`5UAWnQ z$h`V@_5>H!Q%Px48jOsL7pcqvCHGpp_NAp;#25mmu4&T5-PwM)=f@278wWNot~~l) z==l-1bOrBl^-`wi$Br*kE>3qZWqN+xeYwG}Z{MsQ^*&NPqQbeetk3M|B(K{+^S3vD zSpMUM`i(1RUDq$t`mAR6PBii5t2Mtq{rh`;NvJ!&?8Z2$xsuaAH0C^ix1>eo@cegc z-zMi}$cyZH9#yq+TJ8Jzy4%}#t+!hG@J*Vpjlah!)wOeFLyv6hu|IZn$>VG1`d-bP zE+~BflES!OcsxjX+cYh1toWE~=SBtRTN{>$$L+3cVJ=%EX=0Vm6 z)he@B21b_q->biUYHj{`PVH*Hh|U&P{rCUGp6sqFueej=-~Z$V%f3CUXWxAuCvX2S zuE}BT^3|r?oI>x_Ii~!bvliU3pVw|NwJN~k_nE%>c+H=>pTm9QR2PN>|ERR~m>@Fu zMEHl2e|Ln%V)w85TDnzjGygM#mTi+3ML0ZAe|KB{^o!nd)5$eIPwly9zw7)TiR-1` z`F+X)giT!pb|-k*-M)Ca^R&&`zef~w#ZfPKXO*>EoQeovZtrF*4tQrU0g+k-Psie z|N1XZ?tijv{*HtVe$$o&**^PRCh%Fw?3?V>b4MK)CiNe>GUG?hw7s*Hr(d~pO{H~Z z`Mc*+woZkQE%?2YeYN9+ey>%nru(_{Gdn(cbbU^)pTFIfHFD?941p=z)|si+l>E#J zyLn0R*V&&tlg(8=MQ+pM`!va0@UxQIGu^9kZ*(?BN6!k}Q~iA1G;@)uOa90{diG%V zhbqH!>%O+ke&+QvmpGdjA==OQ9miR7M^y7tl&%ge@Un*LC{dFF%BcoH+ zefxe+;_hvy300rJtbBcb%I`mg1q)}_d^Gf2s`3AGSwLdV7w4$-mdS-j>qRW|?aY|B z@)cZD^SJwPGSjtQ3p>q(oMRc{JD-;;^~arAa+Brst;4Q6pEa*Oke9l`TP!}twk&a4 zzvS=aQ*S4?uea0HJn-Xh{ePM29aEJZt7u_1rFa8CmtiMSi#0+v+vF zq6t3f_wO1VJM9(jdiTgl`;>F%tUKG)zP}Ky3N&%`bkekc70x})X@9M}_W4+?%Cf4H z3Fpn$P7%Gzd%J?|?A_Y;u|Fq^WZOOz>Q)i7Z=Vz$&CC+KSW3dDH!H>0;{2Cv!}b3@ zJ>C?1I4FD03(cJ&Tzi`4%;|4^lsNgMorOnls@2n~%(e1NrA-O9E-1_}+m!z!@cf>Z z-=0oq`S?uN2TzHKs$aUi&ghbnWTnpfKfHfFUOIGCaB1J;W2G%W?HC%?U+{5Dd%0!C z4x4FnH^=rSJ52IkdVu^91RIn7gs#tz7R^t%JWn>ur(A1ih}4v$N16ryRCfR0_u=~_ zj=p)7(!UvJ=bx2-yv$dz{rLN+H^$+X`=p~kTx7fe((aYfXBBGm>a^EV=T|@f?D~Cd z{id4|3=C7U^*|#Fx{`I@x9^uZp8ECiFO{8BWaC^qxK5Ycs&8_;Awll?Zog;9vH#y`|9csiO!qxF zr{lfqkLJ6hE7hm5Ro|4z)I6#Edd7`keVn@|{8Zhp{c-sZ+2CJQw_dDV@gj1^m-)2{ zn(J-cTaPSLy7&3SQRmt>JtihS)+M}gB{pk!Tsr)eHFEm3o^RzFuLOo&-28=OR=L8* z$#3NB_z#LSyl4FU>qP(EWp^$~@5y<;-}?D4qvy}d%H?anm^KR>lGpe9ULybZ_uEHi zf7N_spRWD+=*OWrzrE)650qPGm>)k@F1zJP%(jK!PplOFwN+iX+?Q8k+gHxTH`U)< z`Pca^XWCi+-RI^-T@9XlKwe4xyUd<9IqORAf83nj|33G;udPMC!+uf2nwPV>?$4k1 z;$r5XbM9R8l$ZI-zw23K*7t?`-bBSUi&u6`-?MW2=>ty%&tKi^-Y5TR*^E7<5#M)S z_ddNnT4?{d`y10=y!`I=FS&lM(GACn;=3OD_gT$9r;7SZS9Oa1kW+~8yLDh+k>yJR z)ux2o=XQsGl-O{oWvO4xsgg@ulg{+>=AKvD`ulB7)V6c8ewLK)6P_OOcK37L!+UOC zo9q3m-!k~$iUo&G2j`k>`Mcd_@oBl6hrdP4c1!Ltocz~co;4`_#(vAEnran>+_yHK zn#Xoz&)s`-PnX4gU2#tJp{n`vnZY`% zZa>8~bNP>p%kr{R7Wpi@_XKIJ?y2dMx#Ry@A8L725;03-#kCIo&eEXsps|&A)1L%0 zGMH%k9WUH(5&p#g$*sSCy5Gch+ zM{i?bU~L4Q3y}5ss;-Gs$4RT=KYaI-e!Y3P?5Xx&jlcgk*Ke`i^XSo3@$NTK-j|Qf z@)Y;6wXxY(H+{iVP9C0?Jx9+8|Z@=*T-Xc?5oo%yrm41Bt=IwRk1>x@8SEVkc1bE%5I&=BAxW2lI zs;a8$F_*i2=g)|5nzJ-ZUC+b6y19h;&Cw#;NmX`_pZ!!gV!e6iw`Kck4(@pnET-ot zmBg+3R3_%_4GlR(7kmGDKW^j0cWxwxemk+LZhCLPV)o0D#%VQw^%xm?`HV}Z-dy0c zP(ZP5nQJ-EYMrZU9zu$3i2^}Iv#T{zA8hS-y!>43&bq|YFJluoo8C>6v)a3Ab=p+h zC%8tTf>cCjEfZX{LaJEGGiTKc!}L%t&sBd41yx*?wtfEc_w?UT?dyt9e3zVF5kLFc z#yKJk7d905)x0s@HM_KO!lLGFI`m`j!$PK(rP4ajcd&oC@HF=Jo_AqC*Qow- z{jn)>^}n7|p_$q%SDvzCjGglNe9PKhA2+^f{?lWtUB0|?)ugpmUy7fnyouVUy||`` z@f>0t?P9^cki(%24Qd~+F8;Y>(!|bbdjgjW8jI=8FuJlUP z6px+Rz5nmdC2q?#XP3{Z4T&i!-X%KwN|DsB*RR(8zrCj>=)KvlXJ%Nvn0MSK6$+mipK^PS*G6o+rMsTQ!{)Zhg;hEUq`h z=tgAw!zO6Sci#zO}!- z_`2x!%CG#(B$6NQo-JMX=3UOhwM!PIzr2)x?cb3-pE}f6u9WyZ&s9(AQEv8%uEaVdxRW(rz=u;ORb7VR!jC=~OQVz%6ZHRW81~F_5q9X8vyJ-Sf4~-e)`-Meb_NrvKWWz;o?C8L(J@Hft+%`QmH6cR4YSz~ ziZEQr=m<;p+aq=P%GwMG0UySUpL}8*vv+ao-Z#H7lZ~O_89U@0612twg9BTFiF`x; zv5Cs=vvzUb?_RS;$JyC=r5)2jk%s(Z6BM06q7x4sa9H{3@GbBf=pE@8hnrv;ihLT% b5BqQ>)q({)hfXjsFfe$!`njxgN@xNAx&3r# literal 0 HcmV?d00001 diff --git a/api.rst b/api.rst index 06082c2f7..2910f7e3a 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 000000000..d71e8e263 --- /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 000000000..bdd93de2e --- /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/652] 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 6e0317bdf..1e41ecbd6 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/652] Add postgrester client library --- ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem.rst b/ecosystem.rst index 1e41ecbd6..46cdca03a 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/652] Add supabase/postgrest-js client library --- ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem.rst b/ecosystem.rst index 46cdca03a..f3e230f53 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/652] 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 2910f7e3a..01f35676c 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/652] Correct join table paragraph --- api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api.rst b/api.rst index 01f35676c..03d91ae30 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/652] 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 6317aa4db..fe9b9f7f2 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 72293b8fa..19572008f 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/652] 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 19572008f..8f00c6396 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 `_ -* Reference for :ref:`mutation_embed`. +* Documentation reference for :ref:`s_proc_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/652] 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 03d91ae30..506cf35b9 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 8f00c6396..cf8bb706c 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/652] 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 506cf35b9..dd234b769 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 cf8bb706c..4da513f6e 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/652] Add overriding provided headers note --- api.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api.rst b/api.rst index dd234b769..3e8bbad05 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/652] Add unix socket changes --- releases/upcoming.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/releases/upcoming.rst b/releases/upcoming.rst index 4da513f6e..ae5ad4225 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/652] Add fixes to upcoming --- releases/upcoming.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/releases/upcoming.rst b/releases/upcoming.rst index ae5ad4225..fc7b7b3ef 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/652] 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 3e8bbad05..17960fa48 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 fc7b7b3ef..de5e751cd 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/652] 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 17960fa48..fb478dc12 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 de5e751cd..858861f46 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/652] 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 fb478dc12..0cb77ee17 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 eb7f581f9..dadf69322 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/652] Add sompani to production list (#306) --- index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/index.rst b/index.rst index dadf69322..75f5c5bc1 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/652] 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 4b770cb24..fc05b2822 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/652] 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 000000000..8bb510087 --- /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 75f5c5bc1..0f7deaea9 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/652] 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 fc05b2822..b762bc021 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/652] 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 f3e230f53..feb09410e 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/652] 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 0cb77ee17..d05788ce2 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/652] 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 d05788ce2..f1a71d123 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 858861f46..d896b201b 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/652] 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 f1f263a9e..80008bf49 100644 --- a/conf.py +++ b/conf.py @@ -46,17 +46,17 @@ master_doc = 'index' # 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 @@ latex_elements = { # 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/652] Add reference for rpc with array literals Fixes https://github.com/PostgREST/postgrest-docs/issues/258 --- api.rst | 107 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 65 insertions(+), 42 deletions(-) diff --git a/api.rst b/api.rst index f1a71d123..88b4134dc 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 @@ -972,26 +955,6 @@ Note that PostgreSQL converts identifier names to lowercase unless you quote the 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:: - - For versions prior to PostgreSQL 10, to pass a PostgreSQL native array you need to quote it as a string: - - .. code-block:: http - - POST /rpc/native_array_func HTTP/1.1 - - { "arg": "{1,2,3}" } - - In these versions we recommend using function arguments of type json to accept arrays from the client: - - .. code-block:: 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. - .. 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. @@ -1013,6 +976,66 @@ Because ``add_them`` was declared IMMUTABLE, we can alternately call the functio 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 +---------------------------------------------- + +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/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:: + + For versions prior to PostgreSQL 10, to pass a PostgreSQL native array you need to quote it as a string: + + .. 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/native_array_func?arr=%7B1,2,3%7D' HTTP/1.1 + + [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/652] 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 a192b304b..17486b155 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/652] 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 88b4134dc..9d06e45c8 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 fe9b9f7f2..35d93d308 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 d896b201b..5fa71b0f2 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/652] 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 685d28235..0e0e2ae70 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 038b92f6f..18884ea5f 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/652] 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 9d06e45c8..03c97c96b 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/652] 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 03c97c96b..380aaf693 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 5fa71b0f2..8a77c509b 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/652] Add mounting config file to docker --- install.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install.rst b/install.rst index 0e0e2ae70..415757f45 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/652] Redorder insertions before stored procedures --- api.rst | 411 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 205 insertions(+), 206 deletions(-) diff --git a/api.rst b/api.rst index 380aaf693..55dae2a3f 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/652] 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 55dae2a3f..fd62a0f92 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 c8ecdbc09..34e9c473b 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/652] 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 34e9c473b..5e1656a6c 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/652] 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 bf70826ea..00042c276 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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 Uz#7AC37W+9boFyt=akR{0J}*Yu>b%7 literal 0 HcmV?d00001 diff --git a/best_practices.rst b/best_practices.rst index 5e1656a6c..cd80ac539 100644 --- a/best_practices.rst +++ b/best_practices.rst @@ -1,3 +1,10 @@ + +Schema structure +---------------- + +.. image:: _static/db.png + :align: center + .. _func_privs: Function privileges diff --git a/diagrams/README.md b/diagrams/README.md new file mode 100644 index 000000000..ee4e31da5 --- /dev/null +++ b/diagrams/README.md @@ -0,0 +1,30 @@ +## ERD + +The ER diagrams were created with https://github.com/BurntSushi/erd/. + +You can go download erd from https://github.com/BurntSushi/erd/releases and then do: + +```bash +./erd_static-x86-64 -i diagrams/film.er -o _static/film.png +``` + +## LaTeX + +The schema structure diagram is done with LaTeX. You can use a GUI like https://www.mathcha.io/editor to create the .tex file. + +Then use this command to generate the png file. + +```bash +pdflatex --shell-escape -halt-on-error -output-directory ../_static db.tex + +## and move it to the static folder(it's not easy to do it in one go with the pdflatex) +mv db.png ../_static/ +``` + +LaTeX is used because it's a tweakable plain text format. + +You can install the full latex suite with `nix`: + +``` +nix-env -iA texlive.combined.scheme-full +``` diff --git a/diagrams/db.tex b/diagrams/db.tex new file mode 100644 index 000000000..907b3c814 --- /dev/null +++ b/diagrams/db.tex @@ -0,0 +1,71 @@ +\documentclass[convert]{standalone} +\usepackage{amsmath} +\usepackage{tikz} +\usepackage{mathdots} +\usepackage{yhmath} +\usepackage{cancel} +\usepackage{color} +\usepackage{siunitx} +\usepackage{array} +\usepackage{multirow} +\usepackage{amssymb} +\usepackage{gensymb} +\usepackage{tabularx} +\usepackage{booktabs} +\usetikzlibrary{fadings} +\usetikzlibrary{patterns} +\usetikzlibrary{shadows.blur} +\usetikzlibrary{shapes} + +\begin{document} + +\newcommand\customScale{0.4} + +\begin{tikzpicture}[x=0.75pt,y=0.75pt,yscale=-1,xscale=1, scale=\customScale, every node/.style={scale=\customScale}] + +%Shape: Can [id:dp7234864758664346] +\draw [fill={rgb, 255:red, 47; green, 97; blue, 144 } ,fill opacity=1 ] (497.5,51.5) -- (497.5,255.5) .. controls (497.5,275.66) and (423.18,292) .. (331.5,292) .. controls (239.82,292) and (165.5,275.66) .. (165.5,255.5) -- (165.5,51.5) .. controls (165.5,31.34) and (239.82,15) .. (331.5,15) .. controls (423.18,15) and (497.5,31.34) .. (497.5,51.5) .. controls (497.5,71.66) and (423.18,88) .. (331.5,88) .. controls (239.82,88) and (165.5,71.66) .. (165.5,51.5) ; +%Shape: Rectangle [id:dp7384065579958246] +\draw [fill={rgb, 255:red, 236; green, 227; blue, 227 } ,fill opacity=1 ] (189,115) -- (252.5,115) -- (252.5,155) -- (189,155) -- cycle ; +%Shape: Rectangle [id:dp24763906430298177] +\draw [fill={rgb, 255:red, 236; green, 227; blue, 227 } ,fill opacity=1 ] (292,118) -- (362,118) -- (362,158) -- (292,158) -- cycle ; +%Shape: Rectangle [id:dp3775601612537265] +\draw [fill={rgb, 255:red, 236; green, 227; blue, 227 } ,fill opacity=1 ] (397,114) -- (467,114) -- (467,154) -- (397,154) -- cycle ; +%Shape: Rectangle [id:dp7071457022893852] +\draw [fill={rgb, 255:red, 248; green, 231; blue, 28 } ,fill opacity=1 ] (269,199) -- (397.5,199) -- (397.5,273) -- (269,273) -- cycle ; +%Straight Lines [id:da8846759047437789] +\draw (268,234) -- (226.44,155.77) ; +\draw [shift={(225.5,154)}, rotate = 422.02] [color={rgb, 255:red, 0; green, 0; blue, 0 } ][line width=0.75] (10.93,-3.29) .. controls (6.95,-1.4) and (3.31,-0.3) .. (0,0) .. controls (3.31,0.3) and (6.95,1.4) .. (10.93,3.29) ; +%Straight Lines [id:da6908444738113828] +\draw (309.5,198) -- (307.6,161) ; +\draw [shift={(307.5,159)}, rotate = 447.06] [color={rgb, 255:red, 0; green, 0; blue, 0 } ][line width=0.75] (10.93,-3.29) .. controls (6.95,-1.4) and (3.31,-0.3) .. (0,0) .. controls (3.31,0.3) and (6.95,1.4) .. (10.93,3.29) ; +%Straight Lines [id:da7168757864413169] +\draw (398.5,233) -- (431.72,154.84) ; +\draw [shift={(432.5,153)}, rotate = 473.03] [color={rgb, 255:red, 0; green, 0; blue, 0 } ][line width=0.75] (10.93,-3.29) .. controls (6.95,-1.4) and (3.31,-0.3) .. (0,0) .. controls (3.31,0.3) and (6.95,1.4) .. (10.93,3.29) ; +%Up Down Arrow [id:dp14059754167108496] +\draw [fill={rgb, 255:red, 126; green, 211; blue, 33 } ,fill opacity=1 ] (312.5,288.5) -- (330,273) -- (347.5,288.5) -- (338.75,288.5) -- (338.75,319.5) -- (347.5,319.5) -- (330,335) -- (312.5,319.5) -- (321.25,319.5) -- (321.25,288.5) -- cycle ; + +% Text Node +\draw (201,129) node [anchor=north west][inner sep=0.75pt] [align=left] {tables}; +% Text Node +\draw (307,130) node [anchor=north west][inner sep=0.75pt] [align=left ] {tables}; +% Text Node +\draw (414,127) node [anchor=north west][inner sep=0.75pt] [align=left] {tables}; +% Text Node +\draw (272,203) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 0; green, 0; blue, 0 } ,opacity=1 ] [align=center] { \\ views \\ + \\ \ \ stored procedures}; + +% Text Node +\draw (322,178) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{api}}; +% Text Node +\draw (202,97) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{auth}}; +% Text Node +\draw (300,99) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{private}}; +% Text Node +\draw (414,97) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{other}}; +% Text Node +\draw (358,306) node [anchor=north west][inner sep=0.75pt] [align=left] {REST}; + +\end{tikzpicture} + + +\end{document} diff --git a/erd/film.er b/diagrams/film.er similarity index 100% rename from erd/film.er rename to diagrams/film.er diff --git a/erd/orders.er b/diagrams/orders.er similarity index 100% rename from erd/orders.er rename to diagrams/orders.er diff --git a/erd/README.md b/erd/README.md deleted file mode 100644 index d71e8e263..000000000 --- a/erd/README.md +++ /dev/null @@ -1,7 +0,0 @@ -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 -``` From 7714bd9623d2f93da140a727f1e81cdcb50dab2c Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 20 Apr 2020 13:36:48 -0500 Subject: [PATCH 328/652] 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 17486b155..52bb51a0d 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 b762bc021..48de88125 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/652] 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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 delta 7716 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 diff --git a/auth.rst b/auth.rst index 48de88125..29d49be49 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 cd80ac539..6a4b0d97c 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 ee4e31da5..36fd9911d 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 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 E04z%AMF0Q* literal 0 HcmV?d00001 diff --git a/diagrams/db.tex b/diagrams/db.tex index 907b3c814..f4580f891 100644 --- a/diagrams/db.tex +++ b/diagrams/db.tex @@ -19,7 +19,7 @@ \begin{document} -\newcommand\customScale{0.4} +\newcommand\customScale{0.35} \begin{tikzpicture}[x=0.75pt,y=0.75pt,yscale=-1,xscale=1, scale=\customScale, every node/.style={scale=\customScale}] @@ -57,11 +57,11 @@ % Text Node \draw (322,178) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{api}}; % Text Node -\draw (202,97) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{auth}}; +\draw (190,97) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{internal}}; % Text Node \draw (300,99) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{private}}; % Text Node -\draw (414,97) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{other}}; +\draw (417,101) node [anchor=north west][inner sep=0.75pt] [color={rgb, 255:red, 255; green, 255; blue, 255 } ,opacity=1 ] [align=left] {\large\textbf{core}}; % Text Node \draw (358,306) node [anchor=north west][inner sep=0.75pt] [align=left] {REST}; From 3328b6059fa8753240a3469349229b838c3282a2 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 21 Apr 2020 13:37:24 -0500 Subject: [PATCH 330/652] Consolidate schema structure page --- auth.rst | 2 +- diagrams/db.png | Bin 9232 -> 0 bytes index.rst | 68 ++++++++++----------- best_practices.rst => schema_structure.rst | 58 ++++++++++-------- 4 files changed, 69 insertions(+), 59 deletions(-) delete mode 100644 diagrams/db.png rename best_practices.rst => schema_structure.rst (68%) diff --git a/auth.rst b/auth.rst index 29d49be49..7aaa9eabd 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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 E04z%AMF0Q* diff --git a/index.rst b/index.rst index 0f7deaea9..939f92a08 100644 --- a/index.rst +++ b/index.rst @@ -110,6 +110,40 @@ Technical references for PostgREST's functionality. - :doc:`API ` - :doc:`configuration` +Topic guides +------------ + +Explanations of some key concepts in PostgREST. + +.. toctree:: + :caption: Authentication + :hidden: + + auth.rst + +.. toctree:: + :caption: Schema Structure + :hidden: + + schema_structure.rst + +.. toctree:: + :caption: Administration + :hidden: + + admin.rst + +.. toctree:: + :caption: Installation + :hidden: + + install.rst + +- :doc:`Authentication ` +- :doc:`Schema Structure ` +- :doc:`Administration ` +- :doc:`Installation ` + .. _how_tos: How-to guides @@ -128,40 +162,6 @@ 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` -Topic guides ------------- - -Explanations of some key concepts in PostgREST. - -.. toctree:: - :caption: Authentication - :hidden: - - auth.rst - -.. toctree:: - :caption: Installation - :hidden: - - install.rst - -.. toctree:: - :caption: Administration - :hidden: - - admin.rst - -.. toctree:: - :caption: Best Practices - :hidden: - - best_practices.rst - -- :doc:`Authentication ` -- :doc:`Installation ` -- :doc:`Administration ` -- :doc:`Best Practices ` - 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 6a4b0d97c..80e75b23f 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. 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. +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 - -- 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 + -- login as a user wich has privileges on the private schemas + + -- create a sample function + create or replace function login(email text, pass text) returns jwt_token as $$ + begin + -- access to a private schema called 'auth' + select auth.user_role(email, pass) into _role; + -- other operations + -- ... + end; + $$ language plpgsql security definer; + +Note the ``SECURITY DEFINER`` keywords at the end of the function. See `PostgreSQL documentation `_ for more details. + +Views +===== + +Views are invoked with the privileges of the view owner, much like stored procedures with the ``SECURITY DEFINER`` option. When created by a SUPERUSER role, all `row-level security `_ 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 + + 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/652] 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 fd62a0f92..3a0a4da11 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 35d93d308..00d0f7ef6 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 8a77c509b..55eb25bd2 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/652] Reorder calling func with array --- api.rst | 62 +++++++++++++++++++++------------------------------ ecosystem.rst | 4 ++-- 2 files changed, 27 insertions(+), 39 deletions(-) diff --git a/api.rst b/api.rst index 3a0a4da11..350f77123 100644 --- a/api.rst +++ b/api.rst @@ -1179,46 +1179,51 @@ 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 + + [2,3,4,5] + +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 + + 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 you need to quote it as a string: + 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 - POST /rpc/native_array_func HTTP/1.1 + POST /rpc/plus_one HTTP/1.1 - { "arg": "{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. -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/native_array_func?arr=%7B1,2,3%7D' HTTP/1.1 - - [1,2,3] - 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 feb09410e..0a717478b 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/652] 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 8bb510087..be442287b 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 939f92a08..bdfa5e775 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 43bc18833..d49067b39 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 19176d5b2..90cdfb514 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 55eb25bd2..57117c22f 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/652] api.rst: formatting tweaks --- api.rst | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/api.rst b/api.rst index 350f77123..0bab1152f 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/652] 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 0a717478b..a71c41571 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/652] 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 80e75b23f..8f64354f6 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/652] 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 415757f45..2df5c9517 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/652] Add subheading to db-schema --- configuration.rst | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/configuration.rst b/configuration.rst index 00d0f7ef6..2b066f896 100644 --- a/configuration.rst +++ b/configuration.rst @@ -81,22 +81,23 @@ 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" + + 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`. - 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 From 20bb295237ada14c3c8d4cda6ccde9c37d1a6b53 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 20 May 2020 13:51:04 -0500 Subject: [PATCH 339/652] 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 80008bf49..03012ad36 100644 --- a/conf.py +++ b/conf.py @@ -56,7 +56,7 @@ copyright = u'2017, ' + author # 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 000000000..2d64ee518 --- /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/652] 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 8f64354f6..718291255 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/652] 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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 literal 0 HcmV?d00001 diff --git a/conf.py b/conf.py index 03012ad36..67fba7041 100644 --- a/conf.py +++ b/conf.py @@ -132,7 +132,7 @@ html_theme = 'sphinx_rtd_theme' # 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 +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, From 5e44c8160fba2d3538f265c45a99f830f0bf5310 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 22 May 2020 13:33:49 -0500 Subject: [PATCH 342/652] 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 52bb51a0d..01c4e87df 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 000000000..b3e7badc2 --- /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 bdfa5e775..59d1b2928 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 2df5c9517..0028b2bec 100644 --- a/install.rst +++ b/install.rst @@ -1,21 +1,66 @@ -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. + +If you use **macOS Homebrew**, then you can install PostgREST from the `official repo `_. + +.. code:: bash + + brew install postgrest + +If you use **Arch Linux**, then you can install PostgREST from the `official repo `_. + +.. code:: bash + + pacman -S postgrest + +If you use **Nix**, then you can install PostgREST from nixpkgs. + +.. code:: bash + + nix-env -i haskellPackages.postgrest + +When a pre-built binary does not exist for your system you can :ref:`build the project from source `. + +Running +~~~~~~~ + +If you downloaded PostgREST from the release page, first extract the compressed file to obtain the executable. .. code-block:: bash - # Untar the release (available at https://github.com/PostgREST/postgrest/releases/latest) + # For UNIX platforms + tar Jxf postgrest-[version]-[platform].tar.xz - $ tar Jxf postgrest-[version]-[platform].tar.xz + # On Windows you should unzip the file - # Try running it - $ ./postgrest --help +Now you can run postgrest with the :code:`--help` flag to see usage instructions: + +.. code-block:: bash + + # Running postgrest binary + ./postgrest --help + + # Running postgrest installed from a package manager + postgrest --help # You should see a usage help message +The PostgREST server reads a configuration file as its only argument: + +.. code:: bash + + postgrest /path/to/postgrest.conf + + # You can also generate a sample config file with + # postgrest 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`. + .. 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. @@ -24,6 +69,8 @@ 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. + 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 @@ -36,31 +83,6 @@ You can use something like Amazon `RDS `_ but insta * `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 -============= - -The PostgREST server reads a configuration file as its only argument: - -.. code:: bash - - ./postgrest /path/to/postgrest.conf - -For a complete reference of the configuration file, see :ref:`configuration`. - -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: - -.. 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" - 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/652] 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 59d1b2928..4dbbf94fb 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 0028b2bec..fdc192342 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 2d64ee518..5c7d64eee 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/652] 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 5c7d64eee..6adc23235 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/652] Fix typo (#325) --- auth.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth.rst b/auth.rst index 7aaa9eabd..8d0f6df50 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/652] 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 a71c41571..3791ea138 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/652] 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 0bab1152f..199951721 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/652] 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 000000000..1bd9f912a --- /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 000000000..73a123922 --- /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/652] Add supabase/postgrest-rs client library --- ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem.rst b/ecosystem.rst index 3791ea138..f21ca5548 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/652] 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 f21ca5548..0b3110d04 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/652] 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 0b3110d04..9ac92095c 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/652] 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 9ac92095c..77247d091 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/652] Add FreeBSD port to install page --- install.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/install.rst b/install.rst index fdc192342..0481ed283 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/652] 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 01c4e87df..a272fcfbb 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 77247d091..9b33ba546 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/652] 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 718291255..a51d08c92 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/652] 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 9b33ba546..5fd90c41b 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/652] 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 0481ed283..a56b0c0d0 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/652] 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 199951721..6fd9c4ff9 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/652] 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 5fd90c41b..ec7f4ba24 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/652] 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 18884ea5f..a0c49bac2 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/652] 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 b3e7badc2..f5d2e3388 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/652] 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 8d0f6df50..8243e0950 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/652] add postgrest-vercel and postgrest-node --- ecosystem.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ecosystem.rst b/ecosystem.rst index ec7f4ba24..e775f1d8c 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/652] 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 f5d2e3388..3b1ea2774 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/652] Added aiodata --- ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem.rst b/ecosystem.rst index e775f1d8c..ea18b916a 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/652] Moved aiodata to Extensions --- ecosystem.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ecosystem.rst b/ecosystem.rst index ea18b916a..6a1f0ffdd 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/652] 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 6fd9c4ff9..9303d9528 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 73a123922..c6d9ec27d 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/652] 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 4dbbf94fb..3e7b0b518 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/652] 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 9303d9528..d2119a6ea 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/652] Add chocolatey installation method #369 --- install.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/install.rst b/install.rst index a56b0c0d0..d365cf362 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/652] Add nix-shell tools, fix circleci, resolves #322 --- .circleci/config.yml | 33 ++++++++++++ .gitignore | 1 + README.md | 15 +++--- circle.yml | 8 --- default.nix | 73 +++++++++++++++++++------ postgrest.dict | 124 +++++++++++++++++++++---------------------- shell.nix | 16 ++++++ 7 files changed, 176 insertions(+), 94 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 000000000..2d7162fb6 --- /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 00042c276..4c1f95600 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 e8a06defb..0bf695513 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 ae7ac312b..000000000 --- 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 9bc5b1c42..62a18d814 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"; -}) {}; +let + # Commit of the Nixpkgs repository that we want to use. + nixpkgsVersion = { + date = "2020-10-27"; + rev = "cd63096d6d887d689543a0b97743d28995bc9bc3"; + tarballHash = "1wg61h4gndm3vcprdcg7rc4s1v3jkm5xd7lw8r2f67w502y94gcy"; + }; -stdenv.mkDerivation { - name = "postgrest-docs"; - buildInputs = [ - python36Full - python36Packages.sphinx - python36Packages.sphinx_rtd_theme - python36Packages.livereload ]; - shellHook = '' - python livereload_docs.py && exit - ''; + # 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 0faacadb5..03dbef3d3 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 000000000..37e6d7cbb --- /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/652] 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 62a18d814..b422be002 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 03dbef3d3..c0a0e8c82 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 37e6d7cbb..81649475d 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/652] 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 a272fcfbb..d131708e4 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 d2119a6ea..548c7094d 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 b2b25128d..1cc606127 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 be442287b..a90c7939e 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 3e7b0b518..65d0d54e4 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 d365cf362..165a4b6ee 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 c0a0e8c82..0d043e805 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 c6d9ec27d..4ef4ea9af 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 57117c22f..d72579086 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 a51d08c92..c45f30f32 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/652] 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 e67fc3a62..99a29c1f3 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 67fba7041..29f4382cb 100644 --- a/conf.py +++ b/conf.py @@ -288,4 +288,4 @@ texinfo_documents = [ # -- 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 165a4b6ee..9b4756abd 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/652] 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 50359ce05..cd52bc848 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 65d0d54e4..8bd6b3ec0 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/652] 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 b739a52ff..000000000 --- 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 52ea333a4..000000000 --- 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/652] 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 9b4756abd..6e71745a7 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/652] Minor grammar fix --- install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/install.rst b/install.rst index 6e71745a7..88595fe82 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/652] 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 e9fe78f54..fa89591f3 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/652] 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 c9ed9956d..53c0f399b 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 8bd6b3ec0..e779e5494 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/652] 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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 UOEIW!tOmK))78&qol`;+08Y`L4FCWD literal 0 HcmV?d00001 diff --git a/index.rst b/index.rst index e779e5494..89c9961e6 100644 --- a/index.rst +++ b/index.rst @@ -30,8 +30,8 @@ PostgREST is a standalone web server that turns your PostgreSQL database directl Sponsors -------- -.. image:: _static/cybertec.png - :target: https://www.cybertec-postgresql.com/en/ +.. 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 From d77da6bd49ce19c3fb9472271e8f58ebf4dcd041 Mon Sep 17 00:00:00 2001 From: Alexis Panagiotopouos Date: Fri, 12 Mar 2021 13:02:42 +0200 Subject: [PATCH 382/652] 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 548c7094d..4c72eb03a 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/652] 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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 Zd`x#wN9^l5Ck6%v22WQ%mvv4FO#sh&p?LrR literal 0 HcmV?d00001 diff --git a/index.rst b/index.rst index 89c9961e6..f0fb38f8f 100644 --- a/index.rst +++ b/index.rst @@ -42,6 +42,12 @@ Sponsors :target: https://tryretool.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 + :width: 13em + +| + Motivation ---------- From 2a2a4fdd9282d0237425ce7125fb8bfba79e06e4 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Mon, 5 Apr 2021 21:52:31 +0200 Subject: [PATCH 384/652] 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 0bf695513..9f964a975 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/652] 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 ff9c987a9..000000000 --- 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 f0fb38f8f..c071b055b 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 88595fe82..87d650c9d 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 4ef4ea9af..c7fb082c9 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/652] 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 d131708e4..2a8d03d4f 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 4c72eb03a..d5951818a 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 99a29c1f3..745106043 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 8654866b1..dddc9d0d5 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 b422be002..a571acffb 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 53c0f399b..9344ba0f1 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 1cc606127..b4687bad2 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 a90c7939e..5a1463d5e 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 c071b055b..5614f859f 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 87d650c9d..8704afd40 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 c7fb082c9..b7fa22323 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 d49067b39..7cc90611b 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 90cdfb514..88a73ee5c 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 d72579086..34e5d46db 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 6adc23235..5186216e5 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 c45f30f32..d21b9332e 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 81649475d..4800d62ec 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 a0c49bac2..4d39ca5e7 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/652] Add linkcheck to CI --- .circleci/config.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.circleci/config.yml b/.circleci/config.yml index 2d7162fb6..0cf4e8783 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/652] 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 20fdb43db..be92f4d7a 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/652] 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 9f964a975..29050fb9a 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/652] 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 8704afd40..6a682d8fd 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/652] Remove subzero link --- ecosystem.rst | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ecosystem.rst b/ecosystem.rst index 9344ba0f1..776167834 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/652] 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 4d39ca5e7..04e6420a3 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/652] 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 zcmeAS@N?(olHy`uVBq!ia0y~yV02($V2I~nU|?X_!{gY)z`($g?&#~tz_78O`%fY( z0|SFXvPY0F14ES>14Ba#1H&%{28MHUaSW-r_4dL+P6h>m0|ur4C%%6u zP}?!VCtz24;GFA>`;85ZhA7Kd5n8X1Qe8e5qfTA3K>8kkxc7&vR?-bK-no1c=IR*74KQTf$GP;_~^`njxgN@xNA DHGgx# literal 0 HcmV?d00001 diff --git a/_static/gnuhost.png b/_static/gnuhost.png new file mode 100644 index 0000000000000000000000000000000000000000..79a4c9d728437897f1dfa19cc7cca083c0910cec GIT binary patch literal 9360 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 literal 0 HcmV?d00001 diff --git a/index.rst b/index.rst index 5614f859f..380746df8 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/652] docs: add elixir client library --- ecosystem.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ecosystem.rst b/ecosystem.rst index 776167834..5cc044ec8 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/652] Add Supabase-go Library Ecosystem --- ecosystem.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/ecosystem.rst b/ecosystem.rst index 5cc044ec8..92507af6c 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/652] 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 0cf4e8783..36c6ba737 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 a571acffb..039ae825b 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/652] 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 d5951818a..7e2c8d9af 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 745106043..3d0c915bf 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 dddc9d0d5..b7f4e475d 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 380746df8..b375f8b67 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 b7fa22323..698e4f4ed 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/652] Sort ecosystem --- ecosystem.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ecosystem.rst b/ecosystem.rst index 92507af6c..c89d12820 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/652] 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 29f4382cb..71122e56a 100644 --- a/conf.py +++ b/conf.py @@ -289,3 +289,7 @@ texinfo_documents = [ 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/652] 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 7e2c8d9af..7617b0841 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 0d043e805..6530644f4 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 698e4f4ed..1034cfbf5 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/652] 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 2a8d03d4f..03b7579c1 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 7617b0841..ae9006734 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 b375f8b67..8b8e69e9e 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 1034cfbf5..a3e10dc9f 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 000000000..a57cf83f3 --- /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/652] 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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 literal 0 HcmV?d00001 diff --git a/index.rst b/index.rst index 8b8e69e9e..795d1a479 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/652] 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 ae9006734..735aa2940 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 b7f4e475d..960002b67 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 a3e10dc9f..398370f05 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/652] 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 735aa2940..2dea57d3f 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 398370f05..cfebed6d2 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 a57cf83f3..9e994c75e 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/652] 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 960002b67..577d14ae9 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 cfebed6d2..8de3d97b1 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/652] 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 577d14ae9..b6b9db3d6 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 6530644f4..6d86f45b2 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 8de3d97b1..4a8f35688 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/652] 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 03b7579c1..4a1c7ab26 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 b6b9db3d6..066b117be 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 6d86f45b2..7c3644939 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 4a8f35688..d35310ceb 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/652] 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 2dea57d3f..3ec32d019 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 d35310ceb..81c3a8a57 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/652] 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 4a1c7ab26..7b5e5e95f 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 066b117be..bed5a750d 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 7c3644939..bc12f8438 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 81c3a8a57..cda81ff1b 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 9e994c75e..fe684b708 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/652] 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 bed5a750d..56aee4713 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 cda81ff1b..b8cf803e3 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/652] 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 3ec32d019..246d17a20 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/652] Mention environment variables in the configuration section --- configuration.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/configuration.rst b/configuration.rst index 56aee4713..e1138ac18 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/652] 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 e1138ac18..9a3711ef2 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/652] Add no downtime schema cache reload --- releases/upcoming.rst | 3 +++ schema_cache.rst | 19 +++++++++++-------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/releases/upcoming.rst b/releases/upcoming.rst index e64afadbb..3ab9d21c6 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 fe684b708..df8804d21 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 + +For docker you can do: + +.. code:: bash + + docker kill -s SIGUSR1 + + # or in docker-compose + docker-compose kill -s SIGUSR1 + .. note:: - To refresh the cache in docker: - - .. code:: bash - - docker kill -s SIGUSR1 - - # 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/652] Mention log-level=crit/error increase throughput --- configuration.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/configuration.rst b/configuration.rst index 9a3711ef2..5f074ac87 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/652] 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 3d0c915bf..d00f67368 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 3ab9d21c6..0c1ee72a6 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/652] 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 7b5e5e95f..7ab4f1277 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 0c1ee72a6..7c86d907e 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/652] 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 7c86d907e..fde6b391d 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 df8804d21..e03359a8c 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/652] Reorder configuration reloading --- configuration.rst | 70 ++++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 34 deletions(-) diff --git a/configuration.rst b/configuration.rst index 5f074ac87..c7355f0fb 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/652] 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 c7355f0fb..d70869d5a 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 fde6b391d..81f8bbe88 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 e03359a8c..04c8c84bf 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/652] 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 bc12f8438..a5c4b6d0c 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 81f8bbe88..21c84dc30 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/652] 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 795d1a479..eb23a7c92 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/652] 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 cd52bc848..6015b6d48 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 eb23a7c92..af28331db 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/2ndquadrant.png - :target: https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo - :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/retool.png - :target: https://retool.com/?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/gnuhost.png - :target: https://gnuhost.eu/?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/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/gnuhost.png + :target: https://gnuhost.eu/?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/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/oblivious.jpg + :target: https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest + :width: 13em -.. .. image:: _static/empty.png - :target: #sponsors - :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 | diff --git a/releases/v8.0.0.rst b/releases/v8.0.0.rst index 21c84dc30..55c5ad373 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/652] 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 246d17a20..ecc34487b 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 d70869d5a..b758b1265 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 af28331db..8b2033ea1 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 55c5ad373..b79098df1 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 04c8c84bf..e4c265703 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: -You can do automatic schema cache reloading in a pure SQL way with an `event trigger `_ and ``NOTIFY``. +Automatic Schema Cache Reloading +-------------------------------- + +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/652] 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 b758b1265..5b6a28e37 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 6a682d8fd..afe50b6e8 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/652] Update config.py version/release --- conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf.py b/conf.py index 71122e56a..6e017e016 100644 --- a/conf.py +++ b/conf.py @@ -54,9 +54,9 @@ copyright = u'2017, ' + author # 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/652] 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 7ab4f1277..dca785cbf 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 5b6a28e37..04dd236db 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 a47dfa503..6b55c98e1 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 e4c265703..f6fd23749 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/652] 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 dca785cbf..908514a9b 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/652] 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 c89d12820..2d96671d1 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 a5c4b6d0c..202a9505f 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/652] 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 ecc34487b..ea7b7bd63 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 8b2033ea1..65522fad5 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 000000000..5d9e0e437 --- /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/652] 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 2d96671d1..1104ac7a5 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 5d9e0e437..120b69eaa 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/652] 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 1104ac7a5..dd096a534 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 202a9505f..227ccefcb 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/652] 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 dd096a534..68d6f5bd9 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 227ccefcb..cfe0e3bea 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/652] 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 afe50b6e8..c7592f5e1 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/652] 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 908514a9b..68df23052 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 04dd236db..2da88d208 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 6b55c98e1..a02a20f69 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/652] 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 ea7b7bd63..6ac6001ed 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/652] 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 6ac6001ed..2a56d9aad 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 d00f67368..748a9fe29 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 2da88d208..2826e2d5d 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 cfe0e3bea..396e67b5e 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 120b69eaa..060d528b0 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/652] 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 2a56d9aad..e4255edda 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 060d528b0..37bc92dc0 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/652] 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 e4255edda..5826f7f45 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 2826e2d5d..c3a38301d 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 37bc92dc0..a7ac4b116 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/652] 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 c7592f5e1..2c03932e1 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/652] 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 5826f7f45..e85864d60 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 a7ac4b116..9610ba27e 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/652] 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 68df23052..df318e516 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 396e67b5e..5a2689239 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 9610ba27e..e5dc3541b 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/652] 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 68d6f5bd9..787a8b75d 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 65522fad5..eb891698b 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 e5dc3541b..36fd101b1 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 000000000..163f583b5 --- /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/652] 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 df318e516..d1352228a 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:: - DELETE /logs?time=lt.1991-08-06 HTTP/1.1 + .. code-tab:: http + + 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:: - DELETE /logs HTTP/1.1 + .. code-tab:: http + + 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:: - GET /people?id=eq.1 HTTP/1.1 - Accept: application/vnd.pgrst.object+json + .. code-tab:: http + + GET /people?id=eq.1 HTTP/1.1 + Accept: application/vnd.pgrst.object+json + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?id=eq.1" \ + -H "Accept: application/vnd.pgrst.object+json" This allows compound primary keys and makes the intent for singular response independent of a URL convention. diff --git a/api.rst b/api.rst index e85864d60..5e3fbe1ec 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:: - GET /people HTTP/1.1 + .. code-tab:: http + + GET /people HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: - GET /people?age=lt.13 HTTP/1.1 + .. code-tab:: http + + 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:: - GET /people?age=gte.18&student=is.true HTTP/1.1 + .. code-tab:: http + + 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:: - GET /people?or=(age.gte.14,age.lte.18) HTTP/1.1 + .. 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: -.. code-block:: http +.. tabs:: - GET /people?and=(grade.gte.90,student.is.true,or(age.gte.14,age.is.null)) HTTP/1.1 + .. 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: @@ -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=not.wfts(french).amusant HTTP/1.1 + 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 + + 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:: - GET /people?select=first_name,age HTTP/1.1 + .. code-tab:: http + + GET /people?select=first_name,age HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: - GET /people?select=id,json_data->>blood_type,json_data->phones HTTP/1.1 + .. code-tab:: http + + 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:: - GET /people?select=id,json_data->phones->0->>number HTTP/1.1 + .. code-tab:: http + + 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:: - GET /people?select=id,json_data->blood_type&json_data->>blood_type=eq.A- HTTP/1.1 + .. code-tab:: http + + GET /people?select=id,json_data->blood_type&json_data->>blood_type=eq.A- HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: - GET /people?select=id,json_data->age&json_data->age=gt.20 HTTP/1.1 + .. code-tab:: http + + GET /people?select=id,json_data->age&json_data->age=gt.20 HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: - GET /people?full_name=fts.Beckett HTTP/1.1 + .. code-tab:: http + + 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:: - GET /%D9%85%D9%88%D8%A7%D8%B1%D8%AF HTTP/1.1 + .. code-tab:: http + + 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:: - GET /Order%20Items?Unit%20Price=lt.200 HTTP/1.1 + .. code-tab:: http + + 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:: - GET /employees?name=in.(%22Hebdon,John%22,%22Williams,Mary%22) HTTP/1.1 + .. code-tab:: http + + 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:: - GET /vulnerabilities?%22information.cpe%22=like.*MS* HTTP/1.1 + .. code-tab:: http + + GET /vulnerabilities?%22information.cpe%22=like.*MS* HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: - GET /people?order=age.desc,height.asc HTTP/1.1 + .. code-tab:: http + + GET /people?order=age.desc,height.asc HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?order=age.desc,height.asc" If no direction is specified it defaults to ascending order: -.. code-block:: http +.. tabs:: - GET /people?order=age HTTP/1.1 + .. code-tab:: http + + 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 - GET /people?order=age.desc.nullslast 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 + + .. 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:: - GET /people?limit=15&offset=30 HTTP/1.1 + .. code-tab:: http + + 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:: - HEAD /bigtable?limit=25 HTTP/1.1 - Prefer: count=planned + .. code-tab:: http + + HEAD /bigtable?limit=25 HTTP/1.1 + Prefer: count=planned + + .. code-tab:: bash Curl + + 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:: - HEAD /smalltable?limit=25 HTTP/1.1 - Prefer: count=estimated + .. code-tab:: http + + 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:: - HEAD /bigtable?limit=25 HTTP/1.1 - Prefer: count=estimated + .. code-tab:: http + + 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:: - GET /rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers* HTTP/1.1 + .. code-tab:: http + + GET /rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers* HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: - GET /orders?select=name,billing_address(name) HTTP/1.1 + .. code-tab:: http + + GET /orders?select=name,billing_address(name) HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: - GET /orders?select=name,billing_address:billing_address_id(name) HTTP/1.1 + .. code-tab:: http + + GET /orders?select=name,billing_address:billing_address_id(name) HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: - GET /orders?select=*,billing_address(*) HTTP/1.1 + .. code-tab:: http + + 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:: - GET /orders?select=*,central_addresses!billing_address(*) HTTP/1.1 + .. 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 + +.. 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:: - PUT /employees?id=eq.4 HTTP/1.1 + .. code-tab:: http - { "id": 4, "name": "Sara B.", "salary": 60000 } + PUT /employees?id=eq.4 HTTP/1.1 + + { "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:: - DELETE /user?active=is.false HTTP/1.1 + .. code-tab:: http + + DELETE /user?active=is.false HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: - POST /rpc/function_name HTTP/1.1 + .. code-tab:: http + + POST /rpc/function_name HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: - POST /rpc/add_them HTTP/1.1 + .. code-tab:: http - { "a": 1, "b": 2 } + POST /rpc/add_them HTTP/1.1 + + { "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:: - GET /rpc/add_them?a=1&b=2 HTTP/1.1 + .. code-tab:: http + + 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:: - POST /rpc/mult_them HTTP/1.1 - Prefer: params=single-object + .. code-tab:: http - { "x": 4, "y": 2 } + POST /rpc/mult_them HTTP/1.1 + Prefer: params=single-object + + { "x": 4, "y": 2 } + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/mult_them" \ + -X POST -H "Content-Type: application/json" \ + -H "Prefer: params=single-object" \ + -d '{ "x": 4, "y": 2 }' + +.. code-block:: json 8 @@ -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:: - POST /rpc/plus_one HTTP/1.1 - Content-Type: application/json + .. code-tab:: http - {"arr": [1,2,3,4]} + POST /rpc/plus_one HTTP/1.1 + Content-Type: application/json + + {"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:: - GET /rpc/plus_one?arr=%7B1,2,3,4%7D' HTTP/1.1 + .. code-tab:: http + + 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 - { "arr": "{1,2,3,4}" } + POST /rpc/plus_one HTTP/1.1 + + { "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:: - GET /rpc/plus_one?v=1&v=2&v=3&v=4 HTTP/1.1 + .. code-tab:: http + + 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:: - POST /rpc/plus_one HTTP/1.1 - Content-Type: application/x-www-form-urlencoded + .. code-tab:: http - v=1&v=2&v=3&v=4 + POST /rpc/plus_one HTTP/1.1 + Content-Type: application/x-www-form-urlencoded + + v=1&v=2&v=3&v=4 + + .. code-tab:: bash Curl + + 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:: - GET /rpc/add_them?a=1&b=2 HTTP/1.1 + .. code-tab:: http + + 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:: - GET /rpc/best_films_2017 HTTP/1.1 + .. code-tab:: http + + GET /rpc/best_films_2017 HTTP/1.1 + + .. code-tab:: bash Curl + + 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 - 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?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:: - POST /rpc/closest_point HTTP/1.1 - Accept: application/octet-stream + .. code-tab:: http + + POST /rpc/closest_point HTTP/1.1 + Accept: application/octet-stream + + .. code-tab:: bash Curl + + 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:: - POST /rpc/overlapping_regions?select=geom_twkb HTTP/1.1 - Accept: application/octet-stream + .. 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:: @@ -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:: - GET /workers?select=custom_psv_format HTTP/1.1 - Accept: text/plain + .. code-tab:: http + + 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:: - OPTIONS /people HTTP/1.1 + .. code-tab:: http + + OPTIONS /people HTTP/1.1 + + .. code-tab:: bash Curl + + 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:: - GET /items HTTP/1.1 - Accept-Profile: tenant2 + .. code-tab:: http + + 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:: - POST /items HTTP/1.1 - Content-Profile: tenant2 + .. code-tab:: http - {...} + 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:: - GET /people HTTP/1.1 - User-Agent: Mozilla/4.01 (compatible; MSIE 6.0; Windows NT 5.1) + .. code-tab:: http + + 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:: - GET /rpc/teapot HTTP/1.1 + .. code-tab:: http + + GET /rpc/teapot HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/teapot" -i .. code-block:: http diff --git a/auth.rst b/auth.rst index 748a9fe29..8e3383cb0 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:: - POST /rpc/login HTTP/1.1 + .. code-tab:: http - { "email": "foo@bar.com", "pass": "foobar" } + POST /rpc/login HTTP/1.1 + + { "email": "foo@bar.com", "pass": "foobar" } + + .. code-tab:: bash Curl + + 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 6e017e016..be38720aa 100644 --- a/conf.py +++ b/conf.py @@ -28,7 +28,10 @@ import os # 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 039ae825b..40db65825 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 000000000..8d408d300 --- /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 000000000..f90b1c18d --- /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 163f583b5..7c1725324 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 f6fd23749..1e708876d 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:: - GET /rpc/plus_one?num=1 HTTP/1.1 + .. code-tab:: http + + GET /rpc/plus_one?num=1 HTTP/1.1 + + .. code-tab:: bash Curl + + 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/652] Fix broken styles --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7c1725324..5a5a51b65 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/652] 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 2c03932e1..2d789ad08 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 `_. - choco install postgrest - scoop install postgrest + .. code:: bash + + pacman -S postgrest + + .. tab:: Nix + + You can install PostgREST from nixpkgs. + + .. code:: bash + + nix-env -i haskellPackages.postgrest + + .. group-tab:: Windows + + You can install PostgREST using `Chocolatey `_ or `Scoop `_. + + .. code:: bash + + choco install postgrest + scoop install postgrest Running PostgREST ================= diff --git a/postgrest.dict b/postgrest.dict index 5a2689239..684faa934 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/652] 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 5e3fbe1ec..6917c937f 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 36fd101b1..48446f113 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/652] 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 6917c937f..661fba091 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/652] 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 661fba091..077a6665d 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 684faa934..fba460fd9 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 48446f113..2a958c7e2 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/652] 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 077a6665d..1feb1a60c 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 2a958c7e2..9cbba9fe3 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/652] 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 1feb1a60c..a58d4356a 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 9cbba9fe3..b62c2d63f 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/652] 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 a58d4356a..3529b3e19 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:: - GET /marks?name=in.(%22Quote:%5C%22%22,%22Backslash:%5C%5C%22) HTTP/1.1 + .. code-tab:: http + + 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:: - GET /films?select=title,actors(first_name,last_name)&actors.first_name=eq.Jehanne HTTP/1.1 + .. code-tab:: http + + 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:: - GET /films?select=title,actors!inner(first_name,last_name)&actors.first_name=eq.Jehanne HTTP/1.1 + .. 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 + + 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:: - GET /orders?select=*,central_addresses!billing_address!inner(*)¢ral_addresses.code="AB1000" HTTP/1.1 + .. code-tab:: http + + 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:: - DELETE /user?id=eq.1 HTTP/1.1 - Prefer: return=representation + .. code-tab:: http + + DELETE /user?id=eq.1 HTTP/1.1 + Prefer: return=representation + + .. code-tab:: bash Curl + + 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 c3a38301d..2826e2d5d 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 b62c2d63f..3f79dba41 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/652] 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 3529b3e19..31fcbb906 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 8e3383cb0..0c8c50726 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 2d789ad08..0a454835d 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/652] 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 d1352228a..054a31a39 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 31fcbb906..eb79826ab 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 787a8b75d..6d4aa68c1 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 eb891698b..aa48aa9c9 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 3f79dba41..000000000 --- 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 000000000..09187b46f --- /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/652] 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 eb79826ab..f5d5f796d 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 0c8c50726..cb2928e8c 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 fa89591f3..ec288fc97 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/652] 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 2826e2d5d..3b76ea765 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/652] Update config.py version/release --- conf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conf.py b/conf.py index be38720aa..f0390613d 100644 --- a/conf.py +++ b/conf.py @@ -57,9 +57,9 @@ copyright = u'2017, ' + author # 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/652] 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 3b76ea765..4bccf0d8c 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/652] 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 0a454835d..6c410455e 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/652] 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 7cc90611b..d32044526 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/652] 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 cb2928e8c..9df91c78f 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 6d4aa68c1..883db8bc0 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 aa48aa9c9..3d522585d 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 a02a20f69..7cdc36c58 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 09187b46f..7843f2403 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/652] 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 40db65825..651046e36 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/652] Update configuration page --- admin.rst | 2 +- api.rst | 14 +- auth.rst | 6 +- configuration.rst | 879 ++++++++++-------- .../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, 527 insertions(+), 383 deletions(-) diff --git a/admin.rst b/admin.rst index 054a31a39..6f906363f 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 f5d5f796d..2e39e0178 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 `_. 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. - - .. code:: bash - - db-schema = "api" - - 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" - - 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-anon-role: - -db-anon-role ------------- - - The database role to use when executing commands on behalf of unauthenticated clients. For more information, see :ref:`roles`. - -.. _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: - -db-pool-timeout ---------------- - - 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-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``. - -.. _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 an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section ` for more information. - -.. _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 an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section ` for more information. - -.. _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" - -.. _db-config: - -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 ------------ - - 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: - -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-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 = "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 = "warn" - - # All the "warn" level events plus all requests (every status code) are logged - log-level = "info" - - - Because currently there's no buffering for logging, the levels with minimal logging(``crit/error``) will increase throughput. - -.. _openapi-mode: - -openapi-mode ------------- - - 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 ------------------------- - - 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 a **XML** file by doing a request with ``Accept: image/png`` - or ``Accept: text/xml``, respectively. - - .. code:: bash - - raw-media-types="image/png, text/xml" - .. _env_variables_config: Environment Variables @@ -436,14 +72,14 @@ 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: +For example, you can configure :ref:`db-schemas` and :ref:`jwt-secret` like this: .. code:: postgresql - ALTER ROLE authenticator IN DATABASE SET pgrst.db_schema = "tenant1, tenant2, tenant3" + 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 options. +Note that underscores(``_``) need to be used instead of dashes(``-``) for the in-database config parameters. .. important:: @@ -469,3 +105,510 @@ To reload the in-database configuration from within the database, you can use a 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-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 +======================== ======= ================= ======== ========== + +.. _app.settings.*: + +app.settings.* +-------------- + + =============== ==================== + **Environment** PGRST_APP_SETTINGS_* + **In-Database** pgrst.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')`. + +.. _db-anon-role: + +db-anon-role +------------ + + =============== ================== + **Environment** PGRST_DB_ANON_ROLE + **In-Database** `n/a` + =============== ================== + + The database role to use when executing commands on behalf of unauthenticated clients. For more information, see :ref:`roles`. + +.. _db-channel: + +db-channel +---------- + + =============== ================ + **Environment** PGRST_DB_CHANNEL + **In-Database** `n/a` + =============== ================ + + The name of the notification channel that PostgREST uses for :ref:`schema_reloading` and configuration reloading. + +.. _db-channel-enabled: + +db-channel-enabled +------------------ + + =============== ======================== + **Environment** PGRST_DB_CHANNEL_ENABLED + **In-Database** `n/a` + =============== ======================== + + When this is set to :code:`true`, the notification channel specified in :ref:`db-channel` is enabled. + + You should set this to ``false`` when using PostgresSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section ` for more information. + +.. _db-config: + +db-config +--------- + + =============== =============== + **Environment** PGRST_DB_CONFIG + **In-Database** `n/a` + =============== =============== + + Enables the in-database configuration. + +.. _db-extra-search-path: + +db-extra-search-path +-------------------- + + =============== ========================== + **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: + +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-pre-request: + +db-pre-request +-------------- + + *For backwards compatibility, this config parameter is also available without prefix as "pre-request".* + + =============== ================= + **Environment** PGRST_DB_PRE_REQUEST + **In-Database** pgrst.db_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. + +.. _db-prepared-statements: + +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 +---------- + + *For backwards compatibility, this config parameter is also available in singular as "db-schema".* + + =============== ================= + **Environment** PGRST_DB_SCHEMAS + **In-Database** pgrst.db_schemas + =============== ================= + + The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. + + .. code:: bash + + db-schemas = "api" + + 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-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 + + # The transaction is always committed + db-tx-end = "commit" + + # The transaction is committed unless a "Prefer: tx=rollback" header is sent + db-tx-end = "commit-allow-override" + + # The transaction is always rolled back + db-tx-end = "rollback" + + # The transaction is rolled back unless a "Prefer: tx=commit" header is sent + db-tx-end = "rollback-allow-override" + +.. _db-uri: + +db-uri +------ + + =============== ================= + **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. + +.. _jwt-aud: + +jwt-aud +------- + + =============== ================= + **Environment** PGRST_JWT_AUD + **In-Database** pgrst.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. + +.. _jwt-role-claim-key: + +jwt-role-claim-key +------------------ + + *For backwards compatibility, this config parameter is also available without prefix as "role-claim-key".* + + =============== ================= + **Environment** PGRST_JWT_ROLE_CLAIM_KEY + **In-Database** pgrst.jwt_role_claim_key + =============== ================= + + A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. This can be used to consume a JWT provided by a third party service like Auth0, Okta or Keycloak. Usage examples: + + .. code:: bash + + # {"postgrest":{"roles": ["other", "author"]}} + # the DSL accepts characters that are alphanumerical or one of "_$@" as keys + jwt-role-claim-key = ".postgrest.roles[1]" + + # {"https://www.example.com/role": { "key": "author }} + # non-alphanumerical characters can go inside quotes(escaped in the config value) + jwt-role-claim-key = ".\"https://www.example.com/role\".key" + +.. _jwt-secret: + +jwt-secret +---------- + + =============== ================= + **Environment** PGRST_JWT_SECRET + **In-Database** pgrst.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-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 + + # Only startup and db connection recovery messages are logged + log-level = "crit" + + # All the "crit" level events plus server errors (status 5xx) are logged + log-level = "error" + + # All the "error" level events plus request errors (status 4xx) are logged + log-level = "warn" + + # All the "warn" level events plus all requests (every status code) are logged + log-level = "info" + + + Because currently there's no buffering for logging, the levels with minimal logging(``crit/error``) will increase throughput. + +.. _openapi-mode: + +openapi-mode +------------ + + =============== ================= + **Environment** PGRST_OPENAPI_MODE + **In-Database** pgrst.openapi_mode + =============== ================= + + Specifies how the OpenAPI output should be displayed. + + .. code:: bash + + # Follows the privileges of the JWT role claim (or from db-anon-role if the JWT is not sent) + # Shows information depending on the permissions that the role making the request has + openapi-mode = "follow-privileges" + + # Ignores the privileges of the JWT role claim (or from db-anon-role if the JWT is not sent) + # Shows all the exposed information, regardless of the permissions that the role making the request has + openapi-mode = "ignore-privileges" + + # Disables the OpenApi output altogether. + # Throws a `404 Not Found` error when accessing the API root path + openapi-mode = "disabled" + +.. _openapi-server-proxy-uri: + +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 + + { + "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" + ] + } + +.. _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`. + + 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. + + .. code:: bash + + raw-media-types="image/png, text/xml" + +.. _server-host: + +server-host +----------- + + =============== ================= + **Environment** PGRST_SERVER_HOST + **In-Database** pgrst.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 +----------- + + =============== ================= + **Environment** PGRST_SERVER_PORT + **In-Database** pgrst.server_port + =============== ================= + + The TCP port to bind the web server. + +.. _server-unix-socket: + +server-unix-socket +------------------ + + =============== ================= + **Environment** PGRST_SERVER_UNIX_SOCKET + **In-Database** pgrst.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-unix-socket-mode: + +server-unix-socket-mode +----------------------- + + =============== ================= + **Environment** PGRST_SERVER_UNIX_SOCKET_MODE + **In-Database** pgrst.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" diff --git a/how-tos/embedding-table-from-another-schema.rst b/how-tos/embedding-table-from-another-schema.rst index 118d630d1..6631982de 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 fba460fd9..16a91dd58 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 34e5d46db..3c2c0ef58 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 04e6420a3..27e9cb44d 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 ec288fc97..cb307dc4b 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/652] add user to logging example --- admin.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/admin.rst b/admin.rst index 6f906363f..20404d21d 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/652] 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 1215d9a3d..50f8ce7ee 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/652] 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 1e708876d..11d0f9510 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/652] 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 50f8ce7ee..ff6784c26 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 +======================= -To reload the in-database configuration from within the database, you can use a NOTIFY command. +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 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 7cdc36c58..9df17821e 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/652] 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 20404d21d..c319c29c4 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 892f73f77..867339cbc 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 ff6784c26..59501140c 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 27e9cb44d..d17b32b7c 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/652] Add order of precedence for config parameters --- configuration.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/configuration.rst b/configuration.rst index 59501140c..422093d36 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/652] 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 2e39e0178..6367d4a3b 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/652] 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 5a1463d5e..989e19749 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 3c2c0ef58..241f0e901 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 9df17821e..b99bd695f 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 7843f2403..d451bba7e 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 d17b32b7c..a0ffb936c 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/652] 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 000000000..3bce02afa --- /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 651046e36..5dffe887a 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 36fd9911d..67009dc31 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 7674d11d3..5d0f5db2e 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/652] 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 6c410455e..290596b0f 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/652] 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 290596b0f..eebeeb33a 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/652] 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 36c6ba737..000000000 --- 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 000000000..e2347a8c7 --- /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 000000000..4f4c76349 --- /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/652] 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 4f4c76349..b03dadad1 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/652] 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 5d0f5db2e..8dae16577 100755 --- a/livereload_docs.py +++ b/livereload_docs.py @@ -4,10 +4,7 @@ from subprocess import call ## 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/652] 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 6367d4a3b..92dd04fbe 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/652] 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 92dd04fbe..a95f1118c 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/652] 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 a95f1118c..2cac9c2a2 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/652] 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 eebeeb33a..5ab43ba11 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/652] 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 c319c29c4..d487062ed 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 16a91dd58..60c7055c1 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/652] 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 2cac9c2a2..6856e6e81 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/652] Update auth.rst (#507) --- docs/auth.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/auth.rst b/docs/auth.rst index 867339cbc..94e449db5 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/652] 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 d487062ed..f2f0ad8f8 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 422093d36..57c22199b 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/652] 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 6856e6e81..ab4f997fb 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/652] 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 000000000..7cba0a979 --- /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 3d522585d..8fa7b04f7 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 60c7055c1..1ac2dd976 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/652] 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 b03dadad1..fb815e2cc 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/652] 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 f2f0ad8f8..acdfc29a1 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/652] 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 6015b6d48..fc7f2edb6 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 acdfc29a1..e7a7448f9 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 ab4f997fb..7acaafa36 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 000000000..39fa807a4 --- /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 8fa7b04f7..ea08aa3ad 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 11d0f9510..4b59c7988 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 a0ffb936c..5a2ed1604 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 cb307dc4b..33ef764e7 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 1ac2dd976..2b15bd67b 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/652] 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 4b59c7988..9aa1f3a13 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/652] 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 7cba0a979..921be2ba5 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 2b15bd67b..a20158ba4 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/652] 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 883db8bc0..10923ab15 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 b4687bad2..000000000 --- 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 6631982de..000000000 --- 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 921be2ba5..06276f6c7 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 ea08aa3ad..9448cd089 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/652] Fix unordered list styles --- requirements.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5a5a51b65..57ed0b59f 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/652] 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 e7a7448f9..36b9e5e6f 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 7acaafa36..d1503fd42 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 39fa807a4..8024c1109 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 9448cd089..e2caa8096 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 000000000..101d8fd77 --- /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 a20158ba4..f9cc140fc 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/652] 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 10923ab15..6ae4e1c9f 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/652] 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 d1503fd42..7ae36a41a 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 f9cc140fc..f9861cce2 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/652] 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 fb815e2cc..865683ba1 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/652] 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 7ae36a41a..1b886af09 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/652] 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 1b886af09..727a2d315 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 6ae4e1c9f..8283d6c69 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 8024c1109..950c9a9fa 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 5ab43ba11..c68ae1586 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 b99bd695f..bdaea4916 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 9aa1f3a13..5e5fce3a6 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/652] Reorganize error groups --- docs/api.rst | 2 + docs/errors.rst | 111 ++++++++++++++++++++---------------------------- postgrest.dict | 4 +- 3 files changed, 51 insertions(+), 66 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 727a2d315..2eb7e7cd9 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 950c9a9fa..248051f98 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 +------------------ + +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 | +===============+=============================================================+ -| .. _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 | | +| .. _pgrstX00: | Internal errors related to the library that connects to the | +| | database. | +| PGRSTX00 | | +---------------+-------------------------------------------------------------+ diff --git a/postgrest.dict b/postgrest.dict index f9861cce2..5a3f17271 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/652] 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 2eb7e7cd9..3f95f1dca 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: -Binary Output -============= +Response Formats For Scalar Responses +===================================== -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 +For scalar return values such as + +* single-column selects on tables or +* scalar functions, + +you can set the additional content types + +* ``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 57c22199b..9d2a33619 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 248051f98..08782f4ec 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 88a73ee5c..f96550515 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/652] 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 3f95f1dca..cedbd4aa2 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/652] 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 cedbd4aa2..5b30e6bd1 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/652] 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 06276f6c7..c5095bd9f 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/652] 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 c5095bd9f..c439301d0 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/652] 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 9d2a33619..66061904b 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/652] Add bytea to working with types and improve img example (#542) --- docs/how-tos/providing-images-for-img.rst | 78 ++++++------------- .../working-with-postgresql-data-types.rst | 78 +++++++++++++++++++ docs/index.rst | 3 +- postgrest.dict | 1 + 4 files changed, 104 insertions(+), 56 deletions(-) diff --git a/docs/how-tos/providing-images-for-img.rst b/docs/how-tos/providing-images-for-img.rst index 989e19749..f1d892e0d 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: - -.. code-block:: html - - Cute Kittens - -In fact, the presented technique is suitable for providing not only images, but arbitrary files. +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. 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. +Afterwards we present a more detailed solution that fixes a few shortcomings of the first approach. + +.. warning:: + + Be careful when saving binaries in the database, having a separate storage service for these is preferable in most cases. See `Storing Binary files in the Database `_. 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. +That's because browsers do not send the required :code:`Accept: application/octet-stream` 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`. +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:: nginx +.. code-block:: html - 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. + 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 c439301d0..9cfcf5279 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 e2caa8096..c26e2477a 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 5a3f17271..fe4bf205a 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/652] 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 5b30e6bd1..886b46578 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/652] 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 c26e2477a..db3a181d5 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 101d8fd77..e09ccb247 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 000000000..ffec1ee2a --- /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 fe4bf205a..2262fe9bc 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/652] lowercase upsert --- docs/api.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 886b46578..e41e7513a 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/652] 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 e41e7513a..54426024e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1716,12 +1716,10 @@ 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 13ce0f33548c741fabd1800f2ffa3c313a00b4f7 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 15 Jun 2022 23:51:04 -0500 Subject: [PATCH 513/652] 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 54426024e..5d34ecf77 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 e09ccb247..3b7eaaf97 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 2262fe9bc..d1430e1f6 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/652] 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 08782f4ec..e3473e487 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/652] Modify GeoJSON examples from views to functions (#551) --- .../working-with-postgresql-data-types.rst | 138 +++++++++--------- 1 file changed, 70 insertions(+), 68 deletions(-) diff --git a/docs/how-tos/working-with-postgresql-data-types.rst b/docs/how-tos/working-with-postgresql-data-types.rst index 9cfcf5279..9ad0c934d 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 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 view for the FeatureCollection Object + -- 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/652] 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 000000000..62f0d0412 --- /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 db3a181d5..989c55f16 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/652] 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 8283d6c69..3d177848c 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/652] 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 5d34ecf77..d28ca2475 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/652] 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 d28ca2475..4e7c4813f 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/652] 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 4e7c4813f..31f627bb9 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/652] Add missing changes/fixes/features for v10 --- docs/api.rst | 290 +++++++++++---- 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, 676 insertions(+), 302 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 31f627bb9..50d78aec4 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: -.. important:: - - PostgREST needs `FOREIGN KEY constraints `_ to be able to do Resource Embedding. - .. image:: _static/film.png -As seen above in :ref:`v_filter` we can request the titles of all films like this: +.. 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 /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:: +.. _many-to-many: - 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`. - -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 66061904b..1e45e1065 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 9ad0c934d..fb317c97e 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 989c55f16..dcb1089fd 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 c68ae1586..ec3c97721 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 3b7eaaf97..000000000 --- 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 000000000..4d2a6aea8 --- /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 d1430e1f6..2105b5839 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/652] 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 | 282 +++++++++++------- .../working-with-postgresql-data-types.rst | 2 + docs/releases/v10.0.0.rst | 244 +++++---------- 3 files changed, 249 insertions(+), 279 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 50d78aec4..bc95ea337 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,24 +1008,45 @@ 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 + + CREATE TABLE technical_specs( + film_id INT REFERENCES films UNIQUE, + runtime TIME, + camera TEXT, + sound TEXT + ); + +Or if the foreign key is also a primary key. .. code-block:: postgresql @@ -1022,66 +1058,117 @@ PostgREST detects one-to-one relationships when a foreign key is also the primar 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 + 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 /directors?select=*,competitions:director_competition(name) HTTP/1.1 + GET /premieres?select=location,film(name) HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/directors?select=*,competitions:director_competition(name)" + curl "http://localhost:3000/premieres?select=location,film(name)" -Take into consideration that the opposite relationship will not be detected, so you need to create another function for that. +.. code-block:: json -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: + [ + { + "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 directors(films) RETURNS SETOF directors ROW 1 AS $$ - -- Override the relationship here - $$ STABLE LANGUAGE sql; + create function premieres(films) returns setof premieres as $$ + select * from premieres where film_id = $1.director_id + $$ 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. +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 /films?select=name,premieres(name) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=name,premieres(name)" + +.. code-block:: json + + [ + { + "name": "Pulp Ficiton", + "premieres": [{"location": "Cannes Festival"}] + }, + ".." + ] + +Computed relationships also allow you to override the ones that are automatically detected by PostgREST. + +For example, to override the :ref:`many-to-one relationship ` between ``films`` and ``directors``. + +.. code-block:: postgres + + 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``. + +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. - - In the future we'll include a way to manually specify views source foreign keys to address this limitation. - .. important:: - If view definitions change you must refresh PostgREST's schema cache for this to work properly. See the section :ref:`schema_reloading`. + - 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. + + 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`. .. _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 fb317c97e..68a50fa24 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 4d2a6aea8..7dc46feda 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 022101ae3c8b23760c107b23a5a6b21d0c575a4b 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 523/652] 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 bc95ea337..89a48524c 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 d507f2f0f4f8e8eddf2ebbd367c6a4c03cc1f39e Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 19 Sep 2022 21:09:32 -0500 Subject: [PATCH 524/652] 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 68a50fa24..04e6562de 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 fdb975c87c9e85b1ed4e6d1a4d81956c3c6cf623 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 21 Sep 2022 19:09:50 -0500 Subject: [PATCH 525/652] 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 89a48524c..8638445b7 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 2105b5839..39588be67 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -127,6 +127,7 @@ PostgREST postgrest PostgREST's pre +preflight psql Qin RabbitMQ From 73f759ef3a0b519c64ebe3a2d4e203a311c0a668 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 526/652] 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 865683ba1..18290bff2 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 8a98b4ed2671a89f444cafb37d0d780266a90777 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 527/652] 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 ec3c97721..39de4f063 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 50eb42bf462c4552d60144b7da948adbbae2b349 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 528/652] 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 000000000..4d0857d54 --- /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 a6af266ab7810cc45f32d74cbf97f9ab90e37ef6 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 14 Nov 2022 19:11:43 -0500 Subject: [PATCH 529/652] 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 5a2ed1604..7b2ce647c 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 485e4db4d79cb2431cafca6d7f68891cf0c96ea3 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 14 Nov 2022 19:16:42 -0500 Subject: [PATCH 530/652] 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 94e449db5..5a3d129d3 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 f3831427e573d770d08efcecd90307f9dad4bbc6 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 14 Nov 2022 19:33:25 -0500 Subject: [PATCH 531/652] 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 8638445b7..c40b747dd 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 3d177848c..69f9eb350 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 39588be67..96eade670 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 f6e3b77643a22c6f7611cd25435cf504d516e1f7 Mon Sep 17 00:00:00 2001 From: fjf2002 Date: Wed, 16 Nov 2022 23:10:18 +0100 Subject: [PATCH 532/652] =?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 5a3d129d3..fb9db9999 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 000000000..ea53a5d32 --- /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 dcb1089fd..d5311d4ac 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 96eade670..671c29911 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 8be87ed4ac1a0ebd672dab66616cfc635cbdd4fc 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 533/652] 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 ea53a5d32..4dec8ed1f 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 082b4183341010f526bb7c0d32ebc331f628191e Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Sat, 28 Jan 2023 20:20:48 -0500 Subject: [PATCH 534/652] 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 fb9db9999..eeb55c0b0 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 1e45e1065..988eae8c9 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 69f9eb350..2fe80a94e 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 378a225a9b364f60175744f003b8c0a9a04c8ffb Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 9 Feb 2023 14:56:38 -0500 Subject: [PATCH 535/652] 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 988eae8c9..b479e5a56 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 f96550515..47f870e44 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 6b65a5b15187db359a670793085b0a8d4ff31c5c Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 9 Feb 2023 16:48:17 -0500 Subject: [PATCH 536/652] Add HTTP status codes to PGRST errors (#590) --- docs/errors.rst | 283 +++++++++++++++++++++++++----------------------- postgrest.dict | 1 - 2 files changed, 149 insertions(+), 135 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index e3473e487..affeb12b2 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,89 @@ 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 | | | ++---------------+-------------+-------------------------------------------------------------+ +| .. _pgrst118: | 400 | Could not order the result using the related table because | +| | | there is no many-to-one or one-to-one relationship between | +| PGRST118 | | them. | ++---------------+-------------+-------------------------------------------------------------+ +| .. _pgrst119: | 400 | Could not use the spread operator on the related table | +| | | because there is no many-to-one or one-to-one relationship | +| PGRST119 | | between them. | ++---------------+-------------+-------------------------------------------------------------+ +| .. _pgrst120: | 400 | An embedded resource can only be filtered using the | +| | | ``is.null`` or ``not.is.null`` :ref:`operators `.| +| PGRST120 | | | ++---------------+-------------+-------------------------------------------------------------+ .. _pgrst2**: @@ -232,27 +243,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 +276,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 +301,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 671c29911..789cdcdf4 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -126,7 +126,6 @@ PostgreSQL PostgreSQL's PostgREST postgres -postgres's postgrest PostgREST's pre From 6089a462356fbc21c9849cebcd305b057d12a88c Mon Sep 17 00:00:00 2001 From: Ian Eyberg Date: Thu, 9 Feb 2023 15:43:11 -0800 Subject: [PATCH 537/652] adding link to ops unikernel package (#586) --- docs/ecosystem.rst | 1 + postgrest.dict | 2 ++ 2 files changed, 3 insertions(+) diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index 2fe80a94e..f9fadfb57 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -63,6 +63,7 @@ DevOps * `cloudstark/helm-charts `_ - helm chart to deploy PostgREST to a Kubernetes cluster via a Deployment and Service * `jbkarle/postgrest `_ - helm chart with a demo database for development and test purposes * `Limezest/postgrest-cloud-run `_ - expose a PostgreSQL database on Cloud SQL using Cloud Run +* `eyberg/postgrest `_ - run PostgREST as a Nanos unikernel .. _eco_external_notification: diff --git a/postgrest.dict b/postgrest.dict index 789cdcdf4..102642e31 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -89,6 +89,7 @@ multi MVCC namespace namespaced +Nanos neq nginx ngrep @@ -185,6 +186,7 @@ TypeScript UI ui unicode +unikernel unix updatable UPSERT From 87b383316d25932412dae20485f172430c9bb931 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Mon, 13 Feb 2023 11:06:22 -0500 Subject: [PATCH 538/652] 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 affeb12b2..2d13faa98 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 adeb622d31697ce11d9b15e3c614d6f159c3e662 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 539/652] 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 18290bff2..ac19ad081 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 cf8ced10146c005105853b87b60198114cbd43a1 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Mon, 13 Feb 2023 22:29:38 -0500 Subject: [PATCH 540/652] 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 c40b747dd..dbd38c6f2 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 6da204d5545055db8877c9b3c9de86d52c920e47 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 541/652] 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 ac19ad081..c3bc41829 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 f588bb6daf791bfc307c8f73f03c112a6294cfbb Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 13 Apr 2023 18:38:07 -0500 Subject: [PATCH 542/652] 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 dbd38c6f2..caaefa5c9 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 3ff9b81c7d8273e2b3d03029b67d58c721245f17 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Tue, 18 Apr 2023 12:43:15 -0500 Subject: [PATCH 543/652] 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 5e5fce3a6..c966a3990 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 516a4daacf3e86d5a4a9364eebbc6c3b37a4cfda Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Tue, 18 Apr 2023 17:13:40 -0500 Subject: [PATCH 544/652] 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 caaefa5c9..1e5e10c46 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 f55ced0306c7018a930c81fb635ba1aa4829b202 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 21 Apr 2023 17:04:31 -0500 Subject: [PATCH 545/652] 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 1e5e10c46..a9483635e 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 424eac4b3ea22cd2c4e8fa47988cb8485b04e92f Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 21 Apr 2023 11:29:05 -0500 Subject: [PATCH 546/652] 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 b479e5a56..a2b41a303 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 d5311d4ac..f138876d8 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 39de4f063..8ba9f56d4 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 4e49310d6e5cf7a2b6b77a071a02a5b6bc8104be Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Fri, 21 Apr 2023 19:20:46 -0500 Subject: [PATCH 547/652] 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 5d1e1cebf..3011e470e 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 55cc616a5ec8ae47e4b02f1449b83a3372ff38f6 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 1 May 2023 01:01:32 -0300 Subject: [PATCH 548/652] Authentication as a reference * Creates a Database Authorization topic * Moves JWT from Auth0 into Ecosystem * Moves SQL User Management to a how-to --- docs/auth.rst | 442 +++--------------- docs/configuration.rst | 4 +- docs/db_authz.rst | 89 ++++ docs/ecosystem.rst | 31 ++ ...ent-using-postgres-users-and-passwords.rst | 8 +- docs/how-tos/sql-user-management.rst | 228 +++++++++ docs/index.rst | 21 +- 7 files changed, 435 insertions(+), 388 deletions(-) create mode 100644 docs/db_authz.rst create mode 100644 docs/how-tos/sql-user-management.rst diff --git a/docs/auth.rst b/docs/auth.rst index eeb55c0b0..90df4c086 100644 --- a/docs/auth.rst +++ b/docs/auth.rst @@ -1,22 +1,47 @@ -.. _roles: +.. raw:: html -Overview of Role System -======================= +

Authentication

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 +.. _roles: + +Overview of role system ----------------------- There are three types of roles used by PostgREST, the **authenticator**, **anonymous** and **user** roles. The database administrator creates these roles and configures PostgREST to use them. .. image:: _static/security-roles.png -The authenticator 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`). +The authenticator role is used for connecting to the database and should be configured to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests. + + +.. code:: sql + + + CREATE ROLE authenticator LOGIN NOINHERIT NOCREATEDB NOCREATEROLE NOSUPERUSER; + +.. note:: + + The names "authenticator" and "anon" names are configurable and not sacred, we simply choose them for clarity. See :ref:`db-uri` and :ref:`db-anon-role`. + +.. _user_impersonation: + +User Impersonation +------------------ + +The picture below shows how the server handles authentication. If auth succeeds, it switches into the user role specified by the request, otherwise it switches into the anonymous role (if it's set in :ref:`db-anon-role`). .. image:: _static/security-anon-choice.png -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. +This role switching mechanism is called **user impersonation**. In PostgreSQL it's done with the ``SET ROLE`` statement. + +.. _jwt_impersonation: + +JWT-Based User Impersonation +---------------------------- + +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 @@ -35,127 +60,22 @@ Note that the database administrator must allow the authenticator role to switch .. code:: sql GRANT user123 TO authenticator; + -- similarly for the anonymous role + -- GRANT anonymous TO authenticator; -If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role 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. +If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role. The database administrator must set the anonymous role permissions correctly to prevent anonymous users from seeing or changing things they shouldn't. -Users and Groups ----------------- +.. _jwt_generation: -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. +JWT Generation +~~~~~~~~~~~~~~ -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. - -.. code-block:: postgres - - CREATE TABLE chat ( - message_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), - message_time TIMESTAMP NOT NULL DEFAULT now(), - message_from NAME NOT NULL DEFAULT current_user, - message_to NAME NOT NULL, - message_subject VARCHAR(64) NOT NULL, - message_body TEXT - ); - - ALTER TABLE chat ENABLE ROW LEVEL SECURITY; - -We want to enforce a policy that ensures a user can see only those messages sent by them or intended for them. Also we want to prevent a user from forging the message_from column with another person's name. - -PostgreSQL allows us to set this policy with row-level security: - -.. code-block:: postgres - - CREATE POLICY chat_policy ON chat - USING ((message_to = current_user) OR (message_from = current_user)) - WITH CHECK (message_from = current_user) - -Anyone accessing the generated API endpoint for the chat table will see exactly the rows they should, without our needing custom imperative server-side coding. - -.. warning:: - - Roles are namespaced per-cluster rather than per-database so they may be prone to collision. - -Web Users Sharing Role -~~~~~~~~~~~~~~~~~~~~~~ - -Alternately database roles can represent groups instead of (or in addition to) individual users. You may choose that all signed-in users for a web app share the role webuser. You can distinguish individual users by including extra claims in the JWT such as email. - -.. code:: json - - { - "role": "webuser", - "email": "john@doe.com" - } - -SQL code can access claims through 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. - -Hybrid User-Group Roles -~~~~~~~~~~~~~~~~~~~~~~~ - -You can mix the group and individual role policies. For instance we could still have a webuser role and individual users which inherit from it: - -.. code-block:: postgres - - CREATE ROLE webuser NOLOGIN; - -- grant this role access to certain tables etc - - CREATE ROLE user000 NOLOGIN; - GRANT webuser TO user000; - -- now user000 can do whatever webuser can - - GRANT user000 TO authenticator; - -- allow authenticator to switch into user000 role - -- (the role itself has nologin) - -.. _custom_validation: - -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 parameter :code:`db-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:: ini - - db-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-block:: postgres - - 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; +You can create a valid JWT either from inside your database(see :ref:`sql_user_management`) or via an external service(see :ref:`external_jwt`). .. _client_auth: Client Auth -=========== +~~~~~~~~~~~ To make an authenticated request the client must include an :code:`Authorization` HTTP header with the value :code:`Bearer `. For instance: @@ -173,84 +93,20 @@ To make an authenticated request the client must include an :code:`Authorization The ``Bearer`` header value can be used with or without capitalization(``bearer``). -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 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 -~~~~~~~~~~~~ - -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-block:: postgres - - CREATE TYPE jwt_token AS ( - token text - ); - - CREATE FUNCTION jwt_test() RETURNS public.jwt_token AS $$ - SELECT public.sign( - row_to_json(r), 'reallyreallyreallyreallyverysafe' - ) AS token - FROM ( - SELECT - 'my_role'::text as role, - extract(epoch from now())::integer + 300 AS exp - ) r; - $$ LANGUAGE sql; - -PostgREST exposes this function to clients via a POST request to ``/rpc/jwt_test``. - -.. note:: - - To avoid hard-coding the secret in stored procedures, save it as a property of the database. - - .. code-block:: postgres - - -- run this once - ALTER DATABASE mydb SET "app.jwt_secret" TO 'reallyreallyreallyreallyverysafe'; - - -- then all functions can refer to app.jwt_secret - SELECT sign( - row_to_json(r), current_setting('app.jwt_secret') - ) AS token - FROM ... - -JWT from Auth0 +Symmetric Keys ~~~~~~~~~~~~~~ -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. - -.. 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. - - .. code:: javascript - - function (user, context, callback) { - - // Follow the documentations at - // https://postgrest.org/en/latest/configuration.html#db-role-claim-key - // to set a custom role claim on PostgREST - // and use it as custom claim attribute in this rule - const myRoleClaim = 'https://myapp.com/role'; - - user.app_metadata = user.app_metadata || {}; - context.accessToken[myRoleClaim] = user.app_metadata.role; - callback(null, user, context); - } +Each token is cryptographically signed with a secret key. In the case of symmetric cryptography the signer and verifier share the same secret passphrase, which can be configured with :ref:`jwt-secret`. +If it is set to a simple string value like “reallyreallyreallyreallyverysafe” then PostgREST interprets it as an HMAC-SHA256 passphrase. .. _asym_keys: Asymmetric Keys ~~~~~~~~~~~~~~~ -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: +In asymmetric cryptography the signer uses the private key and the verifier the public key. + +As described in the :ref:`configuration` section, PostgREST accepts a ``jwt-secret`` config file parameter. However you can also specify a literal JSON Web Key (JWK) or set. For example, you can use an RSA-256 public key encoded as a JWK: .. code-block:: json @@ -287,7 +143,14 @@ You can specify the literal value as we saw earlier, or reference a filename to jwt-secret = "@rsa.jwk.pub" -JWT security +.. _jwt_validation: + +JWT Validation +~~~~~~~~~~~~~~ + +PostgREST honors the :code:`exp` claim for token expiration, rejecting expired tokens. + +JWT Security ~~~~~~~~~~~~ There are at least three types of common critiques against using JWT: 1) against the standard itself, 2) against using libraries with known security vulnerabilities, and 3) against using JWT for web sessions. We'll briefly explain each critique, how PostgREST deals with it, and give recommendations for appropriate user action. @@ -300,197 +163,26 @@ 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. -Schema Isolation -================ +.. _custom_validation: -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. +Custom Validation +----------------- -.. _sql_user_management: - -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 :code:`basic_auth` schema that you shouldn't expose publicly in the API. The public views and functions will live in a different schema which internally references this internal information. - -First we'll need a table to keep track of our users: - -.. code:: sql - - -- We put things inside the basic_auth schema to hide - -- them from public view. Certain public procs/views will - -- refer to helpers and tables inside. - create schema if not exists basic_auth; - - create table if not exists - basic_auth.users ( - email text primary key check ( email ~* '^.+@.+\..+$' ), - pass text not null check (length(pass) < 512), - role name not null check (length(role) < 512) - ); - -We would like the role to be a foreign key to actual database roles, however PostgreSQL does not support these constraints against the :code:`pg_roles` table. We'll use a trigger to manually enforce it. - -.. code-block:: plpgsql - - create or replace function - basic_auth.check_role_exists() returns trigger as $$ - begin - if not exists (select 1 from pg_roles as r where r.rolname = new.role) then - raise foreign_key_violation using message = - 'unknown database role: ' || new.role; - return null; - end if; - return new; - end - $$ language plpgsql; - - drop trigger if exists ensure_user_role_exists on basic_auth.users; - create constraint trigger ensure_user_role_exists - after insert or update on basic_auth.users - for each row - execute procedure basic_auth.check_role_exists(); - -Next we'll use the pgcrypto extension and a trigger to keep passwords safe in the :code:`users` table. - -.. code-block:: plpgsql - - create extension if not exists pgcrypto; - - create or replace function - basic_auth.encrypt_pass() returns trigger as $$ - begin - if tg_op = 'INSERT' or new.pass <> old.pass then - new.pass = crypt(new.pass, gen_salt('bf')); - end if; - return new; - end - $$ language plpgsql; - - drop trigger if exists encrypt_pass on basic_auth.users; - create trigger encrypt_pass - before insert or update on basic_auth.users - for each row - execute procedure basic_auth.encrypt_pass(); - -With the table in place we can make a helper to check a password against the encrypted column. It returns the database role for a user if the email and password are correct. - -.. code-block:: plpgsql - - create or replace function - basic_auth.user_role(email text, pass text) returns name - language plpgsql - as $$ - begin - return ( - select role from basic_auth.users - where users.email = user_role.email - and users.pass = crypt(user_role.pass, users.pass) - ); - end; - $$; - -.. _public_ui: - -Public User Interface ---------------------- - -In the previous section we created an internal table to store user information. Here we create a login function which takes an email address and password and returns JWT if the credentials match a user in the internal table. - -Permissions -~~~~~~~~~~~ - -Your database roles need access to the schema, tables, views and functions in order to service HTTP requests. -Recall from the `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. +PostgREST does not enforce any extra constraints besides :ref:`jwt_validation`. An example of an extra constraint would be to immediately revoke access for a certain user. Using :ref:`db-pre-request` you can specify a stored procedure to call immediately after :ref:`user_impersonation` and before the main query itself runs. .. code:: ini - db-anon-role = "anon" + db-pre-request = "public.check_user" -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. +In the function you can run arbitrary code to check the request and raise an exception to block it if desired. .. code-block:: postgres - -- add type - CREATE TYPE basic_auth.jwt_token AS ( - token text - ); - - -- login should be on your exposed schema - create or replace function - login(email text, pass text) returns basic_auth.jwt_token as $$ - declare - _role name; - result basic_auth.jwt_token; - begin - -- check email and password - select basic_auth.user_role(email, pass) into _role; - if _role is null then - raise invalid_password using message = 'invalid user or password'; - end if; - - select sign( - row_to_json(r), 'reallyreallyreallyreallyverysafe' - ) as token - from ( - select _role as role, login.email as email, - extract(epoch from now())::integer + 60*60 as exp - ) r - into result; - return result; - end; - $$ language plpgsql security definer; - - grant execute on function login(text,text) to anon; - -Since the above :code:`login` function is defined as `security definer `_, -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:: - - .. code-tab:: http - - POST /rpc/login HTTP/1.1 - - { "email": "foo@bar.com", "pass": "foobar" } - - .. code-tab:: bash Curl - - 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!) - -.. code:: json - - { - "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicGFzcyI6ImZvb2JhciJ9.37066TTRlh-1hXhnA9oO9Pj6lgL6zFuJU0iCHhuCFno" - } - - -Alternatives -~~~~~~~~~~~~ - -See the how-to :ref:`sql-user-management-using-postgres-users-and-passwords` for a similar way that completely avoids the table :code:`basic_auth.users`. + 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; diff --git a/docs/configuration.rst b/docs/configuration.rst index a2b41a303..cf4e6c8b2 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -7,8 +7,6 @@ Without configuration, PostgREST won't be able to serve requests. At the minimum 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`. - Config parameters are read in the following order: 1. From the config file. @@ -463,6 +461,8 @@ 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 user with whom PostgREST connects to the database is also known as the ``authenticator`` role. For more information see :ref:`roles`. + When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ 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. 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. diff --git a/docs/db_authz.rst b/docs/db_authz.rst new file mode 100644 index 000000000..8225a9a4c --- /dev/null +++ b/docs/db_authz.rst @@ -0,0 +1,89 @@ +.. raw:: html + +

Database Authorization

+ +Database authorization is the process of granting and verifying database access permissions. PostgreSQL manages 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 :ref:`jwt_impersonation` does most of what you need. When an authenticated user makes a request PostgREST will switch into the database role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable. + +You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example `_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users. + +.. code-block:: postgres + + CREATE TABLE chat ( + message_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), + message_time TIMESTAMP NOT NULL DEFAULT now(), + message_from NAME NOT NULL DEFAULT current_user, + message_to NAME NOT NULL, + message_subject VARCHAR(64) NOT NULL, + message_body TEXT + ); + + ALTER TABLE chat ENABLE ROW LEVEL SECURITY; + +We want to enforce a policy that ensures a user can see only those messages sent by them or intended for them. Also we want to prevent a user from forging the ``message_from`` column with another person's name. + +PostgreSQL allows us to set this policy with row-level security: + +.. code-block:: postgres + + CREATE POLICY chat_policy ON chat + USING ((message_to = current_user) OR (message_from = current_user)) + WITH CHECK (message_from = current_user) + +Anyone accessing the generated API endpoint for the chat table will see exactly the rows they should, without our needing custom imperative server-side coding. + +.. warning:: + + Roles are namespaced per-cluster rather than per-database so they may be prone to collision. + +Web Users Sharing Role +---------------------- + +Alternately database roles can represent groups instead of (or in addition to) individual users. You may choose that all signed-in users for a web app share the role ``webuser``. You can distinguish individual users by including extra claims in the JWT such as email. + +.. code:: json + + { + "role": "webuser", + "email": "john@doe.com" + } + +SQL code can access claims through 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. + +Hybrid User-Group Roles +----------------------- + +You can mix the group and individual role policies. For instance we could still have a webuser role and individual users which inherit from it: + +.. code-block:: postgres + + CREATE ROLE webuser NOLOGIN; + -- grant this role access to certain tables etc + + CREATE ROLE user000 NOLOGIN; + GRANT webuser TO user000; + -- now user000 can do whatever webuser can + + GRANT user000 TO authenticator; + -- allow authenticator to switch into user000 role + -- (the role itself has nologin) + diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index f9fadfb57..ba6292bd6 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -1,3 +1,34 @@ +.. _external_jwt: + +External JWT Generation +----------------------- + +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. + +.. 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. + + .. code:: javascript + + function (user, context, callback) { + + // Follow the documentations at + // https://postgrest.org/en/latest/configuration.html#db-role-claim-key + // to set a custom role claim on PostgREST + // and use it as custom claim attribute in this rule + const myRoleClaim = 'https://myapp.com/role'; + + user.app_metadata = user.app_metadata || {}; + context.accessToken[myRoleClaim] = user.app_metadata.role; + callback(null, user, context); + } + .. _community_tutorials: Community Tutorials 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 4dec8ed1f..108959ab7 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 @@ -59,7 +59,7 @@ In order to be able to work with postgres' SCRAM-SHA-256 password hashes, we als CREATE FUNCTION basic_auth.pbkdf2(salt bytea, pw text, count integer, desired_length integer, algorithm text) RETURNS bytea LANGUAGE plpgsql IMMUTABLE AS $$ - DECLARE + DECLARE hash_length integer; block_count integer; output bytea; @@ -97,7 +97,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)); @@ -210,8 +210,6 @@ anonymous roles. Below is an example of permissions that allow anonymous users t .. 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; @@ -299,7 +297,7 @@ Let's add a table, intended for the :code:`foo` user: Now try to get the table's contents with: .. tabs:: - + .. code-tab:: http GET /foobar HTTP/1.1 diff --git a/docs/how-tos/sql-user-management.rst b/docs/how-tos/sql-user-management.rst new file mode 100644 index 000000000..1f8716e86 --- /dev/null +++ b/docs/how-tos/sql-user-management.rst @@ -0,0 +1,228 @@ +.. _sql_user_management: + +SQL User Management +=================== + +As mentioned on :ref:`jwt_generation`, an external service can provide user management and coordinate with the PostgREST server using JWT. It’s also possible to support logins entirely through SQL. It’s a fair bit of work, so get ready. + +Storing Users and Passwords +--------------------------- + +The following table, functions, and triggers will live in a :code:`basic_auth` schema that you shouldn't expose publicly in the API. The public views and functions will live in a different schema which internally references this internal information. + +First we'll need a table to keep track of our users: + +.. code:: sql + + -- We put things inside the basic_auth schema to hide + -- them from public view. Certain public procs/views will + -- refer to helpers and tables inside. + create schema if not exists basic_auth; + + create table if not exists + basic_auth.users ( + email text primary key check ( email ~* '^.+@.+\..+$' ), + pass text not null check (length(pass) < 512), + role name not null check (length(role) < 512) + ); + +We would like the role to be a foreign key to actual database roles, however PostgreSQL does not support these constraints against the :code:`pg_roles` table. We'll use a trigger to manually enforce it. + +.. code-block:: plpgsql + + create or replace function + basic_auth.check_role_exists() returns trigger as $$ + begin + if not exists (select 1 from pg_roles as r where r.rolname = new.role) then + raise foreign_key_violation using message = + 'unknown database role: ' || new.role; + return null; + end if; + return new; + end + $$ language plpgsql; + + drop trigger if exists ensure_user_role_exists on basic_auth.users; + create constraint trigger ensure_user_role_exists + after insert or update on basic_auth.users + for each row + execute procedure basic_auth.check_role_exists(); + +Next we'll use the pgcrypto extension and a trigger to keep passwords safe in the :code:`users` table. + +.. code-block:: plpgsql + + create extension if not exists pgcrypto; + + create or replace function + basic_auth.encrypt_pass() returns trigger as $$ + begin + if tg_op = 'INSERT' or new.pass <> old.pass then + new.pass = crypt(new.pass, gen_salt('bf')); + end if; + return new; + end + $$ language plpgsql; + + drop trigger if exists encrypt_pass on basic_auth.users; + create trigger encrypt_pass + before insert or update on basic_auth.users + for each row + execute procedure basic_auth.encrypt_pass(); + +With the table in place we can make a helper to check a password against the encrypted column. It returns the database role for a user if the email and password are correct. + +.. code-block:: plpgsql + + create or replace function + basic_auth.user_role(email text, pass text) returns name + language plpgsql + as $$ + begin + return ( + select role from basic_auth.users + where users.email = user_role.email + and users.pass = crypt(user_role.pass, users.pass) + ); + end; + $$; + +.. _public_ui: + +Public User Interface +--------------------- + +In the previous section we created an internal table to store user information. Here we create a login function which takes an email address and password and returns JWT if the credentials match a user in the internal table. + +Permissions +~~~~~~~~~~~ + +Your database roles need access to the schema, tables, views and functions in order to service HTTP requests. +Recall from the :ref:`roles` that PostgREST uses special roles to process requests, namely the authenticator and +anonymous roles. Below is an example of permissions that allow anonymous users to create accounts and attempt to log in. + +.. code-block:: postgres + + create role anon noinherit; + create role authenticator noinherit; + grant anon to authenticator; + +Then, add ``db-anon-role`` to the configuration file to allow anonymous requests. + +.. code:: ini + + db-anon-role = "anon" + +JWT from SQL +~~~~~~~~~~~~ + +You can create JWT tokens in SQL using the `pgjwt extension `_. 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-block:: postgres + + CREATE TYPE jwt_token AS ( + token text + ); + + CREATE FUNCTION jwt_test() RETURNS public.jwt_token AS $$ + SELECT public.sign( + row_to_json(r), 'reallyreallyreallyreallyverysafe' + ) AS token + FROM ( + SELECT + 'my_role'::text as role, + extract(epoch from now())::integer + 300 AS exp + ) r; + $$ LANGUAGE sql; + +PostgREST exposes this function to clients via a POST request to ``/rpc/jwt_test``. + +.. note:: + + To avoid hard-coding the secret in stored procedures, save it as a property of the database. + + .. code-block:: postgres + + -- run this once + ALTER DATABASE mydb SET "app.jwt_secret" TO 'reallyreallyreallyreallyverysafe'; + + -- then all functions can refer to app.jwt_secret + SELECT sign( + row_to_json(r), current_setting('app.jwt_secret') + ) AS token + FROM ... + +Logins +~~~~~~ + +As described in `JWT from SQL`_, we'll create a JWT inside our login function. Note that you'll need to adjust the secret key which is hard-coded in this example to a secure (at least thirty-two character) secret of your choosing. + +.. code-block:: postgres + + -- add type + CREATE TYPE basic_auth.jwt_token AS ( + token text + ); + + -- login should be on your exposed schema + create or replace function + login(email text, pass text) returns basic_auth.jwt_token as $$ + declare + _role name; + result basic_auth.jwt_token; + begin + -- check email and password + select basic_auth.user_role(email, pass) into _role; + if _role is null then + raise invalid_password using message = 'invalid user or password'; + end if; + + select sign( + row_to_json(r), 'reallyreallyreallyreallyverysafe' + ) as token + from ( + select _role as role, login.email as email, + extract(epoch from now())::integer + 60*60 as exp + ) r + into result; + return result; + end; + $$ language plpgsql security definer; + + grant execute on function login(text,text) to anon; + +Since the above :code:`login` function is defined as `security definer `_, +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:: + + .. code-tab:: http + + POST /rpc/login HTTP/1.1 + + { "email": "foo@bar.com", "pass": "foobar" } + + .. code-tab:: bash Curl + + 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!) + +.. code:: json + + { + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicGFzcyI6ImZvb2JhciJ9.37066TTRlh-1hXhnA9oO9Pj6lgL6zFuJU0iCHhuCFno" + } + + +Alternatives +~~~~~~~~~~~~ + +See the how-to :ref:`sql-user-management-using-postgres-users-and-passwords` for a similar way that completely avoids the table :code:`basic_auth.users`. diff --git a/docs/index.rst b/docs/index.rst index f138876d8..e5d90de2f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -130,6 +130,12 @@ Reference guides Technical references for PostgREST's functionality. +.. toctree:: + :caption: Authentication + :hidden: + + auth.rst + .. toctree:: :caption: API :hidden: @@ -154,6 +160,7 @@ Technical references for PostgREST's functionality. errors.rst +- :doc:`Authentication ` - :doc:`API ` - :doc:`configuration` - :doc:`Schema Cache ` @@ -165,10 +172,10 @@ Topic guides Explanations of some key concepts in PostgREST. .. toctree:: - :caption: Authentication + :caption: Database Authorization :hidden: - auth.rst + db_authz.rst .. toctree:: :caption: Schema Structure @@ -188,7 +195,7 @@ Explanations of some key concepts in PostgREST. install.rst -- :doc:`Authentication ` +- :doc:`Database Authorization ` - :doc:`Schema Structure ` - :doc:`Administration ` - :doc:`Installation ` @@ -205,15 +212,17 @@ These are recipes that'll help you address specific use-cases. :caption: How-to guides :hidden: + how-tos/sql-user-management how-tos/working-with-postgresql-data-types + how-tos/sql-user-management-using-postgres-users-and-passwords 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/sql-user-management` - :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` +- :doc:`how-tos/providing-images-for-img` +- :doc:`how-tos/create-soap-endpoint` Ecosystem --------- From 638aa8369dc6332a7da1555ed1fd2b364ffd88fa Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 1 May 2023 01:14:33 -0300 Subject: [PATCH 549/652] fix broken link --- docs/releases/v10.0.0.rst | 2 +- docs/releases/v10.2.0.rst | 2 +- docs/releases/v7.0.0.rst | 2 +- docs/releases/v7.0.1.rst | 2 +- docs/releases/v8.0.0.rst | 2 +- docs/releases/v9.0.0.rst | 2 +- docs/releases/v9.0.1.rst | 2 +- postgrest.dict | 1 + 8 files changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/releases/v10.0.0.rst b/docs/releases/v10.0.0.rst index 7dc46feda..9177fcfbc 100644 --- a/docs/releases/v10.0.0.rst +++ b/docs/releases/v10.0.0.rst @@ -207,7 +207,7 @@ Big thanks from the `PostgREST team `_ :width: 13em * Evans Fernandes -* `Jan Sommer `_ +* Jan Sommer * `Franz Gusenbauer `_ * `Daniel Babiak `_ * Tsingson Qin diff --git a/docs/releases/v10.2.0.rst b/docs/releases/v10.2.0.rst index 3011e470e..ff7dc61ea 100644 --- a/docs/releases/v10.2.0.rst +++ b/docs/releases/v10.2.0.rst @@ -133,7 +133,7 @@ Big thanks from the `PostgREST team `_ :width: 13em * Evans Fernandes -* `Jan Sommer `_ +* Jan Sommer * `Franz Gusenbauer `_ * `Daniel Babiak `_ * Tsingson Qin diff --git a/docs/releases/v7.0.0.rst b/docs/releases/v7.0.0.rst index 241f0e901..03aaa3a42 100644 --- a/docs/releases/v7.0.0.rst +++ b/docs/releases/v7.0.0.rst @@ -90,7 +90,7 @@ This release was made possible thanks to: * `Daniel Babiak `_ * Evans Fernandes -* `Jan Sommer `_ +* Jan Sommer * Tsingson Qin * Michel Pelletier * Jay Hannah diff --git a/docs/releases/v7.0.1.rst b/docs/releases/v7.0.1.rst index 5186216e5..541c46245 100644 --- a/docs/releases/v7.0.1.rst +++ b/docs/releases/v7.0.1.rst @@ -53,7 +53,7 @@ This release was made possible thanks to: * `Daniel Babiak `_ * Evans Fernandes -* `Jan Sommer `_ +* Jan Sommer * Tsingson Qin * Michel Pelletier * Jay Hannah diff --git a/docs/releases/v8.0.0.rst b/docs/releases/v8.0.0.rst index bdaea4916..22b77a736 100644 --- a/docs/releases/v8.0.0.rst +++ b/docs/releases/v8.0.0.rst @@ -171,7 +171,7 @@ Big thanks from the `PostgREST team `_ :width: 13em * Evans Fernandes -* `Jan Sommer `_ +* Jan Sommer * `Franz Gusenbauer `_ * `Daniel Babiak `_ * Tsingson Qin diff --git a/docs/releases/v9.0.0.rst b/docs/releases/v9.0.0.rst index d451bba7e..236ebf0e3 100644 --- a/docs/releases/v9.0.0.rst +++ b/docs/releases/v9.0.0.rst @@ -110,7 +110,7 @@ Big thanks from the `PostgREST team `_ :width: 13em * Evans Fernandes -* `Jan Sommer `_ +* Jan Sommer * `Franz Gusenbauer `_ * `Daniel Babiak `_ * Tsingson Qin diff --git a/docs/releases/v9.0.1.rst b/docs/releases/v9.0.1.rst index ffec1ee2a..dfbaa5735 100644 --- a/docs/releases/v9.0.1.rst +++ b/docs/releases/v9.0.1.rst @@ -69,7 +69,7 @@ Big thanks from the `PostgREST team `_ :width: 13em * Evans Fernandes -* `Jan Sommer `_ +* Jan Sommer * `Franz Gusenbauer `_ * `Daniel Babiak `_ * Tsingson Qin diff --git a/postgrest.dict b/postgrest.dict index 102642e31..d99c43b30 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -156,6 +156,7 @@ Sencha Serverless Severin SHA +Sommer signup SIGUSR sl From 473035575cd29811cbb71e414290a84ac42fd863 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 2 May 2023 19:29:01 -0300 Subject: [PATCH 550/652] database as single source of truth philosophy make it clear at the index --- docs/index.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index e5d90de2f..81ae15fbf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -67,15 +67,15 @@ Sponsors | -Motivation ----------- +Database as Single Source of Truth +---------------------------------- -Using PostgREST is an alternative to manual CRUD programming. Custom API servers suffer problems. Writing business logic often duplicates, ignores or hobbles database structure. Object-relational mapping is a leaky abstraction leading to slow imperative code. The PostgREST philosophy establishes a single declarative source of truth: the data itself. +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 database 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. +It's easier to ask PostgreSQL to join data for you and let its query planner figure out the details than to loop through rows yourself. It's easier to assign permissions to database objects than to add guards in controllers. (This is especially true for cascading permissions in data dependencies.) It's easier to set constraints than to litter code with sanity checks. Leak-proof Abstraction ---------------------- From 764f0a035293da68df3b652fd0f23b3ade334783 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 3 May 2023 00:38:23 -0300 Subject: [PATCH 551/652] link authn to db authz --- docs/auth.rst | 2 +- docs/db_authz.rst | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/auth.rst b/docs/auth.rst index 90df4c086..81360d31e 100644 --- a/docs/auth.rst +++ b/docs/auth.rst @@ -2,7 +2,7 @@

Authentication

-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 :doc:`authorization happens in the database ` . It is PostgREST's job to **authenticate** requests -- i.e. verify that a client is who they say they are -- and then let the database **authorize** client actions. .. _roles: diff --git a/docs/db_authz.rst b/docs/db_authz.rst index 8225a9a4c..abb8e59c8 100644 --- a/docs/db_authz.rst +++ b/docs/db_authz.rst @@ -2,10 +2,15 @@

Database Authorization

-Database authorization is the process of granting and verifying database access permissions. PostgreSQL manages 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. +Database authorization is the process of granting and verifying database access permissions. PostgreSQL manages permissions using the concept of roles. + +Users and Groups +---------------- + +A role can be thought of as either a database user, or a group of database users, depending on how the role is set up. Roles for Each Web User ------------------------ +~~~~~~~~~~~~~~~~~~~~~~~ PostgREST can accommodate either viewpoint. If you treat a role as a single user then the :ref:`jwt_impersonation` does most of what you need. When an authenticated user makes a request PostgREST will switch into the database role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable. @@ -41,7 +46,7 @@ Anyone accessing the generated API endpoint for the chat table will see exactly Roles are namespaced per-cluster rather than per-database so they may be prone to collision. Web Users Sharing Role ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ Alternately database roles can represent groups instead of (or in addition to) individual users. You may choose that all signed-in users for a web app share the role ``webuser``. You can distinguish individual users by including extra claims in the JWT such as email. @@ -70,7 +75,7 @@ For PostgreSQL server version < 14 This allows JWT generation services to include extra information and your database code to react to it. For instance the RLS example could be modified to use this ``current_setting`` rather than ``current_user``. The second ``'true'`` argument tells ``current_setting`` to return NULL if the setting is missing from the current configuration. Hybrid User-Group Roles ------------------------ +~~~~~~~~~~~~~~~~~~~~~~~ You can mix the group and individual role policies. For instance we could still have a webuser role and individual users which inherit from it: From 4f4ae27a2d285cbb5110bcc313c3eb85f65ddc81 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 3 May 2023 08:45:05 -0300 Subject: [PATCH 552/652] clarify custom validation --- docs/auth.rst | 15 ++++++++------- docs/index.rst | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/auth.rst b/docs/auth.rst index 81360d31e..ea80deae5 100644 --- a/docs/auth.rst +++ b/docs/auth.rst @@ -143,10 +143,8 @@ You can specify the literal value as we saw earlier, or reference a filename to jwt-secret = "@rsa.jwk.pub" -.. _jwt_validation: - -JWT Validation -~~~~~~~~~~~~~~ +JWT Claims Validation +~~~~~~~~~~~~~~~~~~~~~ PostgREST honors the :code:`exp` claim for token expiration, rejecting expired tokens. @@ -168,19 +166,22 @@ PostgREST uses JWT mainly for authentication and authorization purposes and enco Custom Validation ----------------- -PostgREST does not enforce any extra constraints besides :ref:`jwt_validation`. An example of an extra constraint would be to immediately revoke access for a certain user. Using :ref:`db-pre-request` you can specify a stored procedure to call immediately after :ref:`user_impersonation` and before the main query itself runs. +PostgREST does not enforce any extra constraints besides JWT validation. An example of an extra constraint would be to immediately revoke access for a certain user. Using :ref:`db-pre-request` you can specify a stored procedure to call immediately after :ref:`user_impersonation` and before the main query itself runs. .. code:: ini db-pre-request = "public.check_user" -In the function you can run arbitrary code to check the request and raise an exception to block it if desired. +In the function you can run arbitrary code to check the request and raise an exception(see :ref:`raise_error`) to block it if desired. You can take advantage of :ref:`guc_req_headers_cookies_claims` for +doing custom logic based on the web user info. .. code-block:: postgres CREATE OR REPLACE FUNCTION check_user() RETURNS void AS $$ + DECLARE + email text := current_setting('request.jwt.claims', true)::json->>'email'; BEGIN - IF current_user = 'evil_user' THEN + IF email = 'evil.user@malicious.com' THEN RAISE EXCEPTION 'No, you are evil' USING HINT = 'Stop being so evil and maybe you can log in'; END IF; diff --git a/docs/index.rst b/docs/index.rst index 81ae15fbf..5034f843a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -70,7 +70,7 @@ Sponsors Database as Single Source of Truth ---------------------------------- -Using PostgREST is an alternative to manual CRUD programming. Custom API servers suffer problems. Writing business logic often duplicates, ignores or hobbles database structure. Object-relational mapping is a leaky abstraction leading to slow imperative code. The PostgREST philosophy establishes a single declarative source of truth: the database itself. +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 ----------------------- From d1d159d1e0bc576add273b07ae91d4b746ae0189 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 3 May 2023 16:39:43 -0300 Subject: [PATCH 553/652] join schema structure with db authz --- docs/auth.rst | 2 +- docs/db_authz.rst | 105 ++++++++++++++++++++++++++++++++++++-- docs/index.rst | 7 --- docs/releases/v7.0.0.rst | 2 +- docs/schema_structure.rst | 96 ---------------------------------- 5 files changed, 103 insertions(+), 109 deletions(-) delete mode 100644 docs/schema_structure.rst diff --git a/docs/auth.rst b/docs/auth.rst index ea80deae5..31eb5e31e 100644 --- a/docs/auth.rst +++ b/docs/auth.rst @@ -172,7 +172,7 @@ PostgREST does not enforce any extra constraints besides JWT validation. An exam db-pre-request = "public.check_user" -In the function you can run arbitrary code to check the request and raise an exception(see :ref:`raise_error`) to block it if desired. You can take advantage of :ref:`guc_req_headers_cookies_claims` for +In the function you can run arbitrary code to check the request and raise an exception(see :ref:`raise_error`) to block it if desired. Here you can take advantage of :ref:`guc_req_headers_cookies_claims` for doing custom logic based on the web user info. .. code-block:: postgres diff --git a/docs/db_authz.rst b/docs/db_authz.rst index abb8e59c8..73e8146b9 100644 --- a/docs/db_authz.rst +++ b/docs/db_authz.rst @@ -5,12 +5,12 @@ Database authorization is the process of granting and verifying database access permissions. PostgreSQL manages permissions using the concept of roles. Users and Groups ----------------- +================ A role can be thought of as either a database user, or a group of database users, depending on how the role is set up. Roles for Each Web User -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- PostgREST can accommodate either viewpoint. If you treat a role as a single user then the :ref:`jwt_impersonation` does most of what you need. When an authenticated user makes a request PostgREST will switch into the database role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable. @@ -46,7 +46,7 @@ Anyone accessing the generated API endpoint for the chat table will see exactly Roles are namespaced per-cluster rather than per-database so they may be prone to collision. Web Users Sharing Role -~~~~~~~~~~~~~~~~~~~~~~ +---------------------- Alternately database roles can represent groups instead of (or in addition to) individual users. You may choose that all signed-in users for a web app share the role ``webuser``. You can distinguish individual users by including extra claims in the JWT such as email. @@ -75,7 +75,7 @@ For PostgreSQL server version < 14 This allows JWT generation services to include extra information and your database code to react to it. For instance the RLS example could be modified to use this ``current_setting`` rather than ``current_user``. The second ``'true'`` argument tells ``current_setting`` to return NULL if the setting is missing from the current configuration. Hybrid User-Group Roles -~~~~~~~~~~~~~~~~~~~~~~~ +----------------------- You can mix the group and individual role policies. For instance we could still have a webuser role and individual users which inherit from it: @@ -92,3 +92,100 @@ You can mix the group and individual role policies. For instance we could still -- allow authenticator to switch into user000 role -- (the role itself has nologin) +.. _schema_isolation: + +Schemas +======= + +A PostgREST instance exposes all the tables, views, and stored procedures of the schemas configured in :ref:`db-schemas`. This means private data or implementation details can go inside private schemas and be invisible to HTTP clients. + +It is recommended that you don't expose tables on the schemas you expose, 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 + +Note that you must explicitly allow roles to access the exposed schemas: + +.. code-block:: postgres + + GRANT USAGE ON SCHEMA api TO webuser; + +.. _func_privs: + +Functions +========= + +By default, when a function is created, the privilege to execute it is not restricted by role. The function access is ``PUBLIC`` — executable by all roles (more details at `PostgreSQL Privileges page `_). This is not ideal for an API schema. To disable this behavior, you can run the following SQL statement: + +.. code-block:: postgres + + ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; + +This will change the privileges for all functions created in the future in all schemas. Currently there is no way to limit it to a single schema. In our opinion it's a good practice anyway. + +.. note:: + + It is however possible to limit the effect of this clause only to functions you define. You can put the above statement at the beginning of the API schema definition, and then at the end reverse it with: + + .. code-block:: postgres + + ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO PUBLIC; + + This will work because the :code:`alter default privileges` statement has effect on function created *after* it is executed. See `PostgreSQL alter default privileges `_ for more details. + +After that, you'll need to grant EXECUTE privileges on functions explicitly: + +.. code-block:: postgres + + GRANT EXECUTE ON FUNCTION login TO anonymous; + GRANT EXECUTE ON FUNCTION signup TO anonymous; + +You can also grant execute on all functions in a schema to a higher privileged role: + +.. code-block:: postgres + + GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA api TO web_user; + +Security definer +---------------- + +A function is executed with the privileges of the user who calls it. This means that the user has to have all permissions to do the operations the procedure performs. +If the function accesses private database objects, your :ref:`API roles ` won't be able to successfully execute the function. + +Another option is to define the function with the :code:`SECURITY DEFINER` option. Then only one permission check will take place, the permission to call the function, and the operations in the function will have the authority of the user who owns the function itself. + +.. code-block:: postgres + + -- login as a user wich has privileges on the private schemas + + -- create a sample function + create or replace function login(email text, pass text) returns jwt_token as $$ + begin + -- access to a private schema called 'auth' + select auth.user_role(email, pass) into _role; + -- other operations + -- ... + end; + $$ language plpgsql security definer; + +Note the ``SECURITY DEFINER`` keywords at the end of the function. See `PostgreSQL documentation `_ for more details. + +Views +===== + +Views are invoked with the privileges of the view owner, much like stored procedures with the ``SECURITY DEFINER`` option. When created by a SUPERUSER role, all `row-level security `_ 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 + + 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. +For more details, see this `github issue `_. diff --git a/docs/index.rst b/docs/index.rst index 5034f843a..c3e18938e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -177,12 +177,6 @@ Explanations of some key concepts in PostgREST. db_authz.rst -.. toctree:: - :caption: Schema Structure - :hidden: - - schema_structure.rst - .. toctree:: :caption: Administration :hidden: @@ -196,7 +190,6 @@ Explanations of some key concepts in PostgREST. install.rst - :doc:`Database Authorization ` -- :doc:`Schema Structure ` - :doc:`Administration ` - :doc:`Installation ` diff --git a/docs/releases/v7.0.0.rst b/docs/releases/v7.0.0.rst index 03aaa3a42..67357c9b2 100644 --- a/docs/releases/v7.0.0.rst +++ b/docs/releases/v7.0.0.rst @@ -35,7 +35,7 @@ Added * Documentation improvements - + Explanation for :doc:`Schema Structure <../schema_structure>`. + + Explanation for :doc:`Schema Structure <../db_authz>`. + Reference for :ref:`s_proc_embed`. + Reference for :ref:`mutation_embed`. + Reference for filters on :ref:`json_columns`. diff --git a/docs/schema_structure.rst b/docs/schema_structure.rst deleted file mode 100644 index d21b9332e..000000000 --- a/docs/schema_structure.rst +++ /dev/null @@ -1,96 +0,0 @@ - -.. note:: - - This page is a work in progress. - -.. _schema_isolation: - -Schema Isolation -================ - -A PostgREST instance exposes all the tables, views, and stored procedures of a single `PostgreSQL schema `_ (a namespace of database objects). This means private data or implementation details can go inside different private schemas and be invisible to HTTP clients. - -It is recommended that you don't expose tables on your API schema. Instead expose views and stored procedures which insulate the internal details from the outside world. -This allows you to change the internals of your schema and maintain backwards compatibility. It also keeps your code easier to refactor, and provides a natural way to do API versioning. - -.. image:: _static/db.png - -.. _func_privs: - -Functions -========= - -By default, when a function is created, the privilege to execute it is not restricted by role. The function access is ``PUBLIC`` — executable by all roles (more details at `PostgreSQL Privileges page `_). This is not ideal for an API schema. To disable this behavior, you can run the following SQL statement: - -.. code-block:: postgres - - ALTER DEFAULT PRIVILEGES REVOKE EXECUTE ON FUNCTIONS FROM PUBLIC; - -This will change the privileges for all functions created in the future in all schemas. Currently there is no way to limit it to a single schema. In our opinion it's a good practice anyway. - -.. note:: - - It is however possible to limit the effect of this clause only to functions you define. You can put the above statement at the beginning of the API schema definition, and then at the end reverse it with: - - .. code-block:: postgres - - ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO PUBLIC; - - This will work because the :code:`alter default privileges` statement has effect on function created *after* it is executed. See `PostgreSQL alter default privileges `_ for more details. - -After that, you'll need to grant EXECUTE privileges on functions explicitly: - -.. code-block:: postgres - - GRANT EXECUTE ON FUNCTION login TO anonymous; - GRANT EXECUTE ON FUNCTION signup TO anonymous; - -You can also grant execute on all functions in a schema to a higher privileged role: - -.. code-block:: postgres - - GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA api TO web_user; - -Security definer ----------------- - -A function is executed with the privileges of the user who calls it. This means that the user has to have all permissions to do the operations the procedure performs. -If the function accesses private database objects, your :ref:`API roles ` won't be able to successfully execute the function. - -Another option is to define the function with the :code:`SECURITY DEFINER` option. Then only one permission check will take place, the permission to call the function, and the operations in the function will have the authority of the user who owns the function itself. - -.. code-block:: postgres - - -- login as a user wich has privileges on the private schemas - - -- create a sample function - create or replace function login(email text, pass text) returns jwt_token as $$ - begin - -- access to a private schema called 'auth' - select auth.user_role(email, pass) into _role; - -- other operations - -- ... - end; - $$ language plpgsql security definer; - -Note the ``SECURITY DEFINER`` keywords at the end of the function. See `PostgreSQL documentation `_ for more details. - -Views -===== - -Views are invoked with the privileges of the view owner, much like stored procedures with the ``SECURITY DEFINER`` option. When created by a SUPERUSER role, all `row-level security `_ 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 - - 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. -For more details, see this `github issue `_. From 8ec925c33eca90225a7b81c22511ef8297494559 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 3 May 2023 16:57:17 -0300 Subject: [PATCH 554/652] clarify view security invoker feature Move RULEs limitation to Insertions --- docs/api.rst | 7 +++++++ docs/db_authz.rst | 20 ++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index a9483635e..e693c5a86 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1755,6 +1755,13 @@ 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. +.. important:: + + It's recommended that you `use triggers instead of rules `_. + Insertion on views with complex `rules `_ might not work out of the box with PostgREST due to its usage of CTEs. + If you want to keep using rules, a workaround is to wrap the view insertion in a stored procedure and call it through the :ref:`s_procs` interface. + For more details, see this `github issue `_. + .. _bulk_insert: Bulk Insert diff --git a/docs/db_authz.rst b/docs/db_authz.rst index 73e8146b9..0659c1887 100644 --- a/docs/db_authz.rst +++ b/docs/db_authz.rst @@ -173,19 +173,19 @@ Note the ``SECURITY DEFINER`` keywords at the end of the function. See `PostgreS 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 `_ policies will be bypassed. This is an unsuitable behavior for an API schema. -For changing this, we can create a non-SUPERUSER role and make this role the view's owner. +If you're on PostgreSQL >= 15, this behavior can be changed by specifying the ``security_invoker`` option. .. code-block:: postgres - CREATE ROLE api_views_owner NOINHERIT; + CREATE VIEW sample_view WITH (security_invoker = true) AS + SELECT * FROM sample_table; + +On PostgreSQL < 15, you can create a non-SUPERUSER role and make this role the view's owner. + +.. code-block:: postgres + + CREATE ROLE api_views_owner NOSUPERUSER NOBYPASSRLS; ALTER VIEW sample_view OWNER TO api_views_owner; -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 4fb13ebae6ce50cd830dd40661c1c958585f0534 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 3 May 2023 17:22:20 -0300 Subject: [PATCH 555/652] add tables to db authz --- docs/db_authz.rst | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/docs/db_authz.rst b/docs/db_authz.rst index 0659c1887..5e622b192 100644 --- a/docs/db_authz.rst +++ b/docs/db_authz.rst @@ -104,12 +104,28 @@ This allows you to change the internals of your schema and maintain backwards co .. image:: _static/db.png -Note that you must explicitly allow roles to access the exposed schemas: +You must explicitly allow roles to access the exposed schemas: .. code-block:: postgres GRANT USAGE ON SCHEMA api TO webuser; +Tables +====== + +To let web users access tables you must grant them privileges for the operations you want them to do. + +.. code-block:: postgres + + GRANT + SELECT + , INSERT + , UPDATE(message_body) + , DELETE + ON chat TO webuser; + +You can also choose on which table columns the operation is valid. In the above example, the web user can only update the ``message_body`` column. + .. _func_privs: Functions @@ -173,7 +189,7 @@ Note the ``SECURITY DEFINER`` keywords at the end of the function. See `PostgreS Views ===== -Views are invoked with the privileges of the view owner, much like stored procedures with the ``SECURITY DEFINER`` option. When created by a SUPERUSER role, all `row-level security `_ policies will be bypassed. This is an unsuitable behavior for an API schema. +Views are invoked with the privileges of the view owner, much like stored procedures with the ``SECURITY DEFINER`` option. When created by a SUPERUSER role, all `row-level security `_ policies will be bypassed. If you're on PostgreSQL >= 15, this behavior can be changed by specifying the ``security_invoker`` option. From 837df8b0450ab5921b7eacc72fffb9a58a34b4c3 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 3 May 2023 17:32:36 -0300 Subject: [PATCH 556/652] schema cache title is redundant on navbar Use a raw html title for correcting this --- docs/admin.rst | 2 +- docs/errors.rst | 6 +++--- docs/schema_cache.rst | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/admin.rst b/docs/admin.rst index 36b9e5e6f..3f427feac 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -272,7 +272,7 @@ To do this, set the configuration variable :ref:`admin-server-port` to the port 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. +The ``ready`` endpoint also checks the state of both the Database Connection and the :doc:`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``: diff --git a/docs/errors.rst b/docs/errors.rst index 2d13faa98..e4bc89000 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -23,7 +23,7 @@ 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 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 :doc:`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 @@ -137,8 +137,8 @@ Related to the connection with the database. | PGRST001 | | | +---------------+-------------+-------------------------------------------------------------+ | .. _pgrst002: | 503 | Could not connect with the database when building the | -| | | :ref:`schema_cache` due to the PostgreSQL service not | -| PGRST002 | | running. | +| | | :doc:`Schema Cache ` | +| PGRST002 | | due to the PostgreSQL service not running. | +---------------+-------------+-------------------------------------------------------------+ | .. _pgrst003: | 504 | The request timed out waiting for a pool connection | | | | to be available. See :ref:`db-pool-acquisition-timeout`. | diff --git a/docs/schema_cache.rst b/docs/schema_cache.rst index c966a3990..9751e4d51 100644 --- a/docs/schema_cache.rst +++ b/docs/schema_cache.rst @@ -1,7 +1,8 @@ .. _schema_cache: -Schema Cache -============ +.. raw:: html + +

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. From 9de94274cf3ca5959bff863064b545eccf942dfa Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 3 May 2023 17:44:36 -0300 Subject: [PATCH 557/652] shorten some paragraphs in errors page --- docs/errors.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index e4bc89000..2a5831c21 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 `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 and add an HTTP status code to the response. For instance, when querying a nonexistent table: .. code-block:: http @@ -23,7 +23,7 @@ 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 :doc:`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: +However, some errors do come from PostgREST itself (such as those related to the :doc:`Schema Cache `). These have the same structure as the PostgreSQL errors but are differentiated by the ``PGRST`` prefix in the ``code`` field. For instance, when querying a function that does not exist: .. code-block:: http @@ -116,7 +116,11 @@ PostgREST translates `PostgreSQL error codes ` | -| PGRST002 | | due to the PostgreSQL service not running. | +| PGRST002 | | due to the PostgreSQL service not running. | +---------------+-------------+-------------------------------------------------------------+ | .. _pgrst003: | 504 | The request timed out waiting for a pool connection | | | | to be available. See :ref:`db-pool-acquisition-timeout`. | From 560ec79a9021a61a84b193df54e1037ecb4eb233 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 4 May 2023 16:08:47 -0300 Subject: [PATCH 558/652] clarify schema cache and errors page --- docs/errors.rst | 20 +++++--- docs/releases/v8.0.0.rst | 4 +- docs/schema_cache.rst | 100 +++++++-------------------------------- 3 files changed, 34 insertions(+), 90 deletions(-) diff --git a/docs/errors.rst b/docs/errors.rst index 2a5831c21..5fe83b14d 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -1,9 +1,14 @@ .. _error_source: -Error Source +Error Format ============ -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 and add an HTTP status code to the response. For instance, when querying a nonexistent table: +PostgREST error messages follow the PostgreSQL error structure. It includes ``MESSAGE``, ``DETAIL``, ``HINT``, ``ERRCODE`` and will add an HTTP status code to the response. + +Errors from PostgreSQL +---------------------- + +PostgREST will forward errors coming from PostgreSQL. For instance, when querying a nonexistent table: .. code-block:: http @@ -23,7 +28,10 @@ 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 :doc:`Schema Cache `). These have the same structure as the PostgreSQL errors but are differentiated by the ``PGRST`` prefix in the ``code`` field. For instance, when querying a function that does not exist: +Errors from PostgREST +--------------------- + +Errors that come from PostgREST itself maintain the same structure. But differ in the ``PGRST`` prefix in the ``code`` field. For instance, when querying a function that does not exist in the :doc:`schema cache `: .. code-block:: http @@ -37,7 +45,7 @@ However, some errors do come from PostgREST itself (such as those related to the .. code-block:: json { - "hint": "If a new function was created in the database with this name and parameters, try reloading the schema cache.", + "hint": "...", "details": null "code": "PGRST202", "message": "Could not find the api.nonexistent_function() function in the schema cache" @@ -250,7 +258,7 @@ Related to a :ref:`stale schema cache `. Most of the time, these e +---------------+-------------+-------------------------------------------------------------+ | Code | HTTP status | Description | +===============+=============+=============================================================+ -| .. _pgrst200: | 400 | Caused by :ref:`stale_fk_relationships`, otherwise any of | +| .. _pgrst200: | 400 | Caused by stale foreign key relationships, otherwise any of | | | | the embedding resources or the relationship itself may not | | PGRST200 | | exist in the database. | +---------------+-------------+-------------------------------------------------------------+ @@ -258,7 +266,7 @@ Related to a :ref:`stale schema cache `. Most of the time, these e | | | See :ref:`embed_disamb`. | | PGRST201 | | | +---------------+-------------+-------------------------------------------------------------+ -| .. _pgrst202: | 404 | Caused by a :ref:`stale_function_signature`, otherwise | +| .. _pgrst202: | 404 | Caused by a stale function signature, otherwise | | | | the function may not exist in the database. | | PGRST202 | | | +---------------+-------------+-------------------------------------------------------------+ diff --git a/docs/releases/v8.0.0.rst b/docs/releases/v8.0.0.rst index 22b77a736..69fb70bd2 100644 --- a/docs/releases/v8.0.0.rst +++ b/docs/releases/v8.0.0.rst @@ -84,11 +84,11 @@ Changed * 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 +* Changed the error message for a not found RPC on a stale schema (see :ref:`stale_schema`) 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`. +* Changed the error message for the no relationship found error. See :ref:`stale_schema`. |br| -- `@laurenceisla `_ Fixed diff --git a/docs/schema_cache.rst b/docs/schema_cache.rst index 9751e4d51..cfd9f0bb2 100644 --- a/docs/schema_cache.rst +++ b/docs/schema_cache.rst @@ -4,8 +4,7 @@

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. +Some PostgREST features need metadata from the database schema. Getting this metadata requires expensive queries. To avoid repeating this work, PostgREST uses a schema cache. +--------------------------------------------+-------------------------------------------------------------------------------+ | Feature | Required Metadata | @@ -30,84 +29,19 @@ in order to avoid repeating this work, PostgREST uses a schema cache. .. _stale_schema: -The Stale Schema Cache ----------------------- +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. +One operational problem that comes a cache is that it can go stale. This can happen for PostgREST when you make changes to the metadata before mentioned. Requests that depend on the metadata will fail. -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 -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -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``. - -.. tabs:: - - .. 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: - -.. code-block:: json - - { - "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. - -.. _stale_function_signature: - -Stale Function Signature -~~~~~~~~~~~~~~~~~~~~~~~~ - -The same issue will occur on newly created functions on a running PostgREST. - -.. code-block:: plpgsql - - CREATE FUNCTION plus_one(num integer) - RETURNS integer AS $$ - SELECT num + 1; - $$ LANGUAGE SQL IMMUTABLE; - -.. tabs:: - - .. code-tab:: http - - GET /rpc/plus_one?num=1 HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/rpc/plus_one?num=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.plus_one(num) function in the schema cache" - } - -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. +You can solve this by reloading the cache manually or automatically. .. _schema_reloading: Schema Cache Reloading ---------------------- -To reload the cache without restarting the PostgREST server, send a SIGUSR1 signal to the server process. +To manually reload the cache without restarting the PostgREST server, send a SIGUSR1 signal to the server process. .. code:: bash @@ -123,27 +57,29 @@ For docker you can do: # 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. +There’s no downtime when reloading the schema cache. The reloading will happen on a background thread while serving requests. .. _schema_reloading_notify: Reloading with NOTIFY ~~~~~~~~~~~~~~~~~~~~~ -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: +PostgREST also allows you to reload its schema cache through PostgreSQL `NOTIFY `_. .. code-block:: postgresql 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`. +This is useful in environments where you can’t send the SIGUSR1 Unix Signal. Like on cloud managed containers or on Windows systems. + +The ``pgrst`` notification channel is enabled by default. For configuring the channel, see :ref:`db-channel` and :ref:`db-channel-enabled`. .. _auto_schema_reloading: Automatic Schema Cache Reloading -------------------------------- -You can do automatic schema cache reloading in a pure SQL way and forget about stale schema cache errors 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. For this use an `event trigger `_ and ``NOTIFY``. .. code-block:: postgresql @@ -161,9 +97,9 @@ You can do automatic schema cache reloading in a pure SQL way and forget about s ON ddl_command_end EXECUTE PROCEDURE pgrst_watch(); -Now, whenever the ``pgrst_watch`` trigger is fired in the database, PostgREST will automatically reload the schema cache. +Now, whenever the ``pgrst_watch`` trigger fires, PostgREST will auto-reload the schema cache. -To disable auto reloading, drop the trigger: +To disable auto reloading, drop the trigger. .. code-block:: postgresql @@ -172,12 +108,12 @@ To disable auto reloading, drop the trigger: 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. +You can refine the previous event trigger to only react to the events relevant to the schema cache. This also prevents unnecessary +reloading when creating temporary tables inside functions. .. code-block:: postgresql - -- watch create and alter + -- watch CREATE and ALTER CREATE OR REPLACE FUNCTION pgrst_ddl_watch() RETURNS event_trigger AS $$ DECLARE cmd record; @@ -204,7 +140,7 @@ reloading when creating temporary tables(``CREATE TEMP TABLE``) inside functions END LOOP; END; $$ LANGUAGE plpgsql; - -- watch drop + -- watch DROP CREATE OR REPLACE FUNCTION pgrst_drop_watch() RETURNS event_trigger AS $$ DECLARE obj record; From 9a3973be351c972208814e36b7d3063789726b67 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 5 May 2023 00:09:30 -0300 Subject: [PATCH 559/652] add transactions reference --- docs/api.rst | 264 ++------------------------------ docs/configuration.rst | 2 +- docs/db_authz.rst | 11 +- docs/index.rst | 17 ++- docs/releases/v9.0.0.rst | 2 +- docs/transactions.rst | 320 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 354 insertions(+), 262 deletions(-) create mode 100644 docs/transactions.rst diff --git a/docs/api.rst b/docs/api.rst index e693c5a86..e2b163ecc 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,6 +1,8 @@ .. role:: sql(code) :language: sql +.. _tables_views: + Tables and Views ================ @@ -2079,21 +2081,18 @@ 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 POST (and in some cases GET) to execute the function. +*"A single resource can be the equivalent of a database stored procedure, with the power to abstract state changes over any number of storage items"* -- `Roy T. Fielding `_ -.. tabs:: +Every stored procedure in the exposed database schema is accessible under the :code:`/rpc` prefix. Procedures can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). - .. code-tab:: http +.. note:: - POST /rpc/function_name HTTP/1.1 + The ``/rpc`` prefix is used to avoid name collisions between views and procedures. - .. code-tab:: bash Curl +POST on Stored Procedures +------------------------- - 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). - -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. Each key/value of the object will become an argument. For instance, assume we have created this function in the database. @@ -2128,39 +2127,18 @@ The client can call it by posting an object like 3 - -Procedures must be declared with named parameters. Procedures declared like - -.. code-block:: plpgsql - - CREATE FUNCTION non_named_args(integer, text, integer) ... - -cannot be called with PostgREST, since we use `named notation `_ internally. - -Note that PostgreSQL converts identifier names to lowercase unless you quote them like: - -.. 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:: - 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. + PostgreSQL converts identifier names to lowercase unless you quote them like: -Immutable and stable functions ------------------------------- + .. code-block:: postgres -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. + CREATE FUNCTION "someFunc"("someParam" text) ... -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. +GET on Stored Procedures +------------------------ -.. 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. - -Because ``add_them`` is ``IMMUTABLE``, we can alternately call the function with a GET request: +If the function doesn't modify the database, it will also run under the GET method(see :ref:`access_mode`). .. tabs:: @@ -2801,218 +2779,6 @@ 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_context: - -HTTP Context -============ - -.. _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.headers`, :code:`request.cookies` and :code:`request.jwt.claims`. - -.. code-block:: postgresql - - -- 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.cookies', true)::json->>'sessionId'; - - -- To read the value of the email claim in a jwt: - 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:: - - 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: - -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); - -.. _guc_resp_hdrs: - -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 - - 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:: - - 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: - -Setting headers via pre-request -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -By using a :ref:`db-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.headers', true)::json->>'user-agent'; - begin - if user_agent similar to '%MSIE (6.0|7.0)%' then - perform set_config('response.headers', - '[{"Cache-Control": "no-cache, no-store, must-revalidate"}]', false); - end if; - end; $$ language plpgsql; - - -- set this function on postgrest.conf - -- db-pre-request = custom_headers - -Now when you make a GET request to a table or view, you'll get the cache headers. - -.. tabs:: - - .. code-tab:: http - - 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 ----------------------------- - -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; - -.. tabs:: - - .. code-tab:: http - - GET /rpc/teapot HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/rpc/teapot" -i - -.. code-block:: http - - HTTP/1.1 418 I'm a teapot - - { - "message" : "The requested entity body is short and stout.", - "hint" : "Tip it over and pour it out." - } - -If the status code is standard, PostgREST will complete the status message(**I'm a teapot** in this example). - -.. _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 - - 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" - } - -.. 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': - -.. code-block:: sql - - RAISE sqlstate 'PT402' using - message = 'Payment Required', - detail = 'Quota exceeded', - hint = 'Upgrade your plan'; - -Returns: - -.. code-block:: http - - HTTP/1.1 402 Payment Required - Content-Type: application/json; charset=utf-8 - - { - "message": "Payment Required", - "details": "Quota exceeded", - "hint": "Upgrade your plan", - "code": "PT402" - } - .. _explain_plan: Execution plan diff --git a/docs/configuration.rst b/docs/configuration.rst index cf4e6c8b2..e43f2d003 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -368,7 +368,7 @@ db-pre-request **In-Database** pgrst.db_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. + A schema-qualified stored procedure name to call right after the :ref:`tx_settings` are set. See :ref:`pre-request`. .. _db-prepared-statements: diff --git a/docs/db_authz.rst b/docs/db_authz.rst index 5e622b192..6e3748aad 100644 --- a/docs/db_authz.rst +++ b/docs/db_authz.rst @@ -57,20 +57,19 @@ Alternately database roles can represent groups instead of (or in addition to) i "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: - -For PostgreSQL server version >= 14 +SQL code can access claims through PostgREST :ref:`tx_settings`. For instance to get the email claim, call this function: .. code:: sql current_setting('request.jwt.claims', true)::json->>'email'; +.. note:: -For PostgreSQL server version < 14 + For PostgreSQL < 14 -.. code:: sql + .. code:: sql - current_setting('request.jwt.claim.email', true); + 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. diff --git a/docs/index.rst b/docs/index.rst index c3e18938e..fa98bb6b5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -125,8 +125,8 @@ Are you new to PostgREST? This is the place to start! Also have a look at :doc:`Installation ` and :ref:`community_tutorials`. -Reference guides ----------------- +References +---------- Technical references for PostgREST's functionality. @@ -142,6 +142,12 @@ Technical references for PostgREST's functionality. api.rst +.. toctree:: + :caption: Transactions + :hidden: + + transactions.rst + .. toctree:: :caption: Configuration :hidden: @@ -162,14 +168,15 @@ Technical references for PostgREST's functionality. - :doc:`Authentication ` - :doc:`API ` +- :doc:`Transactions ` - :doc:`configuration` - :doc:`Schema Cache ` - :doc:`Errors ` -Topic guides +Explanations ------------ -Explanations of some key concepts in PostgREST. +Key concepts in PostgREST. .. toctree:: :caption: Database Authorization @@ -198,7 +205,7 @@ Explanations of some key concepts in PostgREST. How-to guides ------------- -These are recipes that'll help you address specific use-cases. +Recipes that'll help you address specific use-cases. .. toctree:: :glob: diff --git a/docs/releases/v9.0.0.rst b/docs/releases/v9.0.0.rst index 236ebf0e3..5963e8237 100644 --- a/docs/releases/v9.0.0.rst +++ b/docs/releases/v9.0.0.rst @@ -12,7 +12,7 @@ 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. +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 settings 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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/transactions.rst b/docs/transactions.rst new file mode 100644 index 000000000..d86ea9818 --- /dev/null +++ b/docs/transactions.rst @@ -0,0 +1,320 @@ +.. raw:: html + +

Transactions

+ +Every :doc:`authenticated ` request to an :doc:`API resource ` runs inside a transaction. The sequence of the transaction is as follows: + +.. code-block:: postgresql + + BEGIN; -- + -- + --
; + END; + +.. _access_mode: + +Access Mode +=========== + +The access mode on :ref:`tables_views` is determined by the HTTP method. + +.. list-table:: + :header-rows: 1 + + * - HTTP Method + - Access Method + * - GET, HEAD + - READ ONLY + * - POST, PATCH, PUT, DELETE + - READ WRITE + +:ref:`s_procs` additionally depend on the function `volatility `_. + +.. list-table:: + :header-rows: 2 + + * - + - Access Method + - + - + * - HTTP Method + - VOLATILE + - STABLE + - IMMUTABLE + * - GET, HEAD + - READ ONLY + - READ ONLY + - READ ONLY + * - POST + - READ WRITE + - READ ONLY + - READ ONLY + +Modifying the database inside READ ONLY transactions is not possible. PostgREST uses this fact to enforce HTTP semantics in GET and HEAD requests. + +.. note:: + + The volatility marker is a promise about the behavior of the function. PostgreSQL will let you mark a function that modifies the database as ``IMMUTABLE`` or ``STABLE`` without failure. But, because of the READ ONLY transaction the function will fail under PostgREST. + +The :ref:`options_requests` method doesn't start a transaction, so it's not relevant here. + +Isolation Level +=============== + +Every transaction uses the PostgreSQL default isolation level: READ COMMITTED. + +.. _tx_settings: + +Transaction-Scoped Settings +=========================== + +PostgREST uses settings tied to the transaction lifetime. These can be used to get data about the HTTP request. Or to modify the HTTP response. + +You can get these with ``current_setting`` + +.. code-block:: postgresql + + -- request settings use the ``request.`` prefix. + SELECT + current_setting('request.', true); + +And you can set them with ``set_config`` + +.. code-block:: postgresql + + -- response settings use the ``response.`` prefix. + SELECT + set_config('response.setting1', 'value1' ,true); + +Request Role and Search Path +----------------------------- + +Because of :ref:`user_impersonation`, PostgREST sets the standard ``role``. You can get this in different ways: + +.. code-block:: postgresql + + SELECT current_role; + + SELECT current_user; + + SELECT current_setting('role', true); + +Additionally it also sets the ``search_path`` based on :ref:`db-schemas` and :ref:`db-extra-search-path`. + + +.. _guc_req_headers_cookies_claims: + +Request Headers, Cookies and JWT claims +--------------------------------------- + +PostgREST stores the headers, cookies and headers as JSON. To get them: + +.. code-block:: postgresql + + -- To get all the headers sent in the request + SELECT current_setting('request.headers', true)::json; + + -- To get a single header, you can use JSON arrow operators + SELECT current_setting('request.headers', true)::json->>'user-agent'; + + -- value of sessionId in a cookie + SELECT current_setting('request.cookies', true)::json->>'sessionId'; + + -- value of the email claim in a jwt + SELECT current_setting('request.jwt.claims', true)::json->>'email'; + +.. note:: + + The ``role`` in ``request.jwt.claims`` defaults to the value of :ref:`db-anon-role`. + +.. _guc_req_path_method: + +Request Path and Method +----------------------- + +The path and method are stored as ``text``. + +.. code-block:: postgresql + + SELECT current_setting('request.path', true); + + SELECT current_setting('request.method', true); + +.. _guc_resp_hdrs: + +Response Headers +---------------- + +You can set ``response.headers`` to add headers to the HTTP response. For instance, this statement would add caching headers to the response: + +.. code-block:: sql + + -- tell client to cache response for two days + + SELECT set_config('response.headers', + '[{"Cache-Control": "public"}, {"Cache-Control": "max-age=259200"}]', true); + +.. code-block:: http + + HTTP/1.1 200 OK + Content-Type: application/json; charset=utf-8 + Cache-Control: no-cache, no-store, must-revalidate + +Notice that the ``response.headers`` should be set to an *array* of single-key objects rather than a single multiple-key object. This is because headers such as ``Cache-Control`` or ``Set-Cookie`` need repeating when setting many values. An object would not allow the repeated key. + +.. note:: + + PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overriden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you also set :ref:`raw-media-types` to something like ``text/html``. + +.. _guc_resp_status: + +Response Status Code +-------------------- + +You can set the ``response.status`` to override the default status code PostgREST provides. For instance, the following function would replace the default ``200`` status code. + +.. code-block:: postgres + + create or replace function teapot() returns json as $$ + begin + perform set_config('response.status', '418', true); + return json_build_object('message', 'The requested entity body is short and stout.', + 'hint', 'Tip it over and pour it out.'); + end; + $$ language plpgsql; + +.. tabs:: + + .. code-tab:: http + + GET /rpc/teapot HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/teapot" -i + +.. code-block:: http + + HTTP/1.1 418 I'm a teapot + + { + "message" : "The requested entity body is short and stout.", + "hint" : "Tip it over and pour it out." + } + +If the status code is standard, PostgREST will complete the status message(**I'm a teapot** in this example). + +Main query +========== + +The main query is produced by requesting the :doc:`API resources `. + +Transaction End +=============== + +If the transaction doesn't fail, it will always end in a COMMIT. Unless :ref:`db-tx-end` is configured to ROLLBACK in any case or conditionally with ``Prefer: tx=rollback``. This can be used for testing purposes. + +Aborting transactions +===================== + +Any database failure(like a failed constraint) will result in a rollback of the transaction. You can also do a RAISE inside a function to cause a rollback. + +.. _raise_error: + +Raise errors with HTTP Status Codes +----------------------------------- + +You 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 + + { + "message": "Payment Required", + "details": "Quota exceeded", + "hint": "Upgrade your plan", + "code": "PT402" + } + +.. _pre-request: + +Pre-Request +=========== + +The pre-request is a function that can run after the :ref:`tx_settings` are set and before the main query. It's enabled with :ref:`db-pre-request`. + +This provides an opportunity to modify settings or raise an exception to prevent the request from completing. + +.. _pre_req_headers: + +Setting headers via pre-request +------------------------------- + +As an example, let's add some cache headers for all requests that come from an Internet Explorer(6 or 7) browser. + +.. code-block:: postgresql + + create or replace function custom_headers() returns void as $$ + declare + user_agent text := current_setting('request.headers', true)::json->>'user-agent'; + begin + if user_agent similar to '%MSIE (6.0|7.0)%' then + perform set_config('response.headers', + '[{"Cache-Control": "no-cache, no-store, must-revalidate"}]', false); + end if; + end; $$ language plpgsql; + + -- set this function on postgrest.conf + -- db-pre-request = custom_headers + +Now when you make a GET request to a table or view, you'll get the cache headers. + +.. tabs:: + + .. code-tab:: http + + 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)" From 2e1dc0738a27e0d41a73ce0cc634a2c7a4f635d7 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 5 May 2023 10:00:49 -0300 Subject: [PATCH 560/652] readthedocs doesn't build anymore Pin the python version to solve it. --- .readthedocs.yaml | 4 ++++ requirements.txt | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 3bce02afa..4690fde9d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -4,3 +4,7 @@ sphinx: python: install: - requirements: requirements.txt +build: + os: ubuntu-22.04 + tools: + python: "3.11" diff --git a/requirements.txt b/requirements.txt index 57ed0b59f..4f76b0480 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,5 @@ docutils==0.16 sphinx>=4.3.0 sphinx-copybutton sphinx-rtd-theme>=0.5.1 -sphinx-tabs \ No newline at end of file +sphinx-tabs +urllib3==2.0.2 From 082e9b14e9f02ca6c30984edff3059a0ab007f2f Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 5 May 2023 11:22:32 -0300 Subject: [PATCH 561/652] transactions reference doesn't consider anon reqs Change authenticated to user impersonation. --- docs/transactions.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/transactions.rst b/docs/transactions.rst index d86ea9818..7a73131c0 100644 --- a/docs/transactions.rst +++ b/docs/transactions.rst @@ -2,7 +2,7 @@

Transactions

-Every :doc:`authenticated ` request to an :doc:`API resource ` runs inside a transaction. The sequence of the transaction is as follows: +After :ref:`user_impersonation`, every request to an :doc:`API resource ` runs inside a transaction. The sequence of the transaction is as follows: .. code-block:: postgresql @@ -84,7 +84,7 @@ And you can set them with ``set_config`` -- response settings use the ``response.`` prefix. SELECT - set_config('response.setting1', 'value1' ,true); + set_config('response.', 'value1' ,true); Request Role and Search Path ----------------------------- @@ -204,6 +204,8 @@ You can set the ``response.status`` to override the default status code PostgRES If the status code is standard, PostgREST will complete the status message(**I'm a teapot** in this example). +.. _main_query: + Main query ========== @@ -279,7 +281,7 @@ Returns: Pre-Request =========== -The pre-request is a function that can run after the :ref:`tx_settings` are set and before the main query. It's enabled with :ref:`db-pre-request`. +The pre-request is a function that can run after the :ref:`tx_settings` are set and before the :ref:`main_query`. It's enabled with :ref:`db-pre-request`. This provides an opportunity to modify settings or raise an exception to prevent the request from completing. @@ -292,7 +294,8 @@ As an example, let's add some cache headers for all requests that come from an I .. code-block:: postgresql - create or replace function custom_headers() returns void as $$ + create or replace function custom_headers() + returns void as $$ declare user_agent text := current_setting('request.headers', true)::json->>'user-agent'; begin From 36300da16dbd0237d853639539930589d518901e Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Sun, 7 May 2023 12:25:12 -0300 Subject: [PATCH 562/652] connection pool reference (#616) * add link in index to external jwt * change pgrst errors location for better reading --- docs/admin.rst | 24 ------------ docs/configuration.rst | 6 +-- docs/connection_pool.rst | 83 ++++++++++++++++++++++++++++++++++++++++ docs/errors.rst | 47 ++++++++++++----------- docs/index.rst | 8 ++++ docs/install.rst | 12 +++--- 6 files changed, 124 insertions(+), 56 deletions(-) create mode 100644 docs/connection_pool.rst diff --git a/docs/admin.rst b/docs/admin.rst index 3f427feac..ecd7972d1 100644 --- a/docs/admin.rst +++ b/docs/admin.rst @@ -132,19 +132,6 @@ 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. -.. _external_connection_poolers: - -Using External Connection Poolers ---------------------------------- - -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:: - - If prepared statements are enabled, PostgREST will quit after detecting that transaction or statement pooling is being used. - -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 ========= @@ -205,17 +192,6 @@ 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(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. - Database Logs ------------- diff --git a/docs/configuration.rst b/docs/configuration.rst index e43f2d003..25d8e9282 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -330,7 +330,7 @@ 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. + Number of maximum connections to keep open in PostgREST's database pool. .. _db-pool-acquisition-timeout: @@ -342,7 +342,7 @@ db-pool-acquisition-timeout **In-Database** `n/a` =============== ================= - Specifies the maximum time in seconds that the request will wait for the pool to free up a connection slot to the database. If it times out without acquiring a connection, then the request is aborted and a ``504`` error is returned. + Specifies the maximum time in seconds that the request will wait for the pool to free up a connection slot to the database. .. _db-pool-max-lifetime: @@ -354,7 +354,7 @@ 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. + Specifies the maximum time in seconds of an existing connection in the pool. .. _db-pre-request: diff --git a/docs/connection_pool.rst b/docs/connection_pool.rst new file mode 100644 index 000000000..9bb38c3c6 --- /dev/null +++ b/docs/connection_pool.rst @@ -0,0 +1,83 @@ +Connection Pool +--------------- + +Every request to an :doc:`API resource ` borrows a connection from the connection pool to start a :doc:`transaction `. + +A connection pool is a cache of reusable database connections. It allows serving many HTTP requests using few database connections. + +Minimizing connections it’s paramount to performance. Each PostgreSQL connection creates a process, having too many can exhaust available resources. + +.. _pool_growth_limit: + +Growth Limit +------------ + +If all the connections are being used, a new connection is added to the pool. The pool can grow until it reaches the :ref:`db-pool` size. + +Note it’s pointless to set this higher than the ``max_connections`` setting in your database. + +Connection lifetime +------------------- + +After a period of time, connections from the pool will be released and news ones will be created. This time is specified by :ref:`db-pool-max-lifetime`. + +The lifetime doesn't affect running requests. Only unused connections will be released. + +For knowing why a connection lifetime is necessary, see the following discussion: +https://www.postgresql.org/message-id/flat/CA%2Bmi_8bnvpxHZtb6EgHSHY-xn29W8VJMzjPU3fiCOv1bfjrNuA%40mail.gmail.com. + +Acquisition Timeout +------------------- + +If all the available connections in the pool are busy, an HTTP request will wait until reaching a timeout. You can configure this timeout with :ref:`db-pool-acquisition-timeout`. + +If the request reaches the timeout, it will be aborted with the following response: + +.. code-block:: http + + HTTP/1.1 504 Gateway Timeout + + {"code":"PGRST003", + "details":null, + "hint":null, + "message":"Timed out acquiring connection from connection pool."} + +Getting this error message is an indicator of a performance issue. To solve it, you can: + +- Reduce your queries execution time. + + - Check the request :ref:`explain_plan` to tune your query, this usually means adding indexes. + +- Reduce the amount of requests. + + - Reduce write requests. Do :ref:`bulk_insert` (or :ref:`upsert`) instead of inserting rows one by one. + - Reduce read requests. Use :ref:`resource_embedding`. Combine unrelated data into a single request using custom database views or functions. + - Use :ref:`s_procs` for combining read and write logic into a single request. + +- Increase the :ref:`pool growth limit `. + + - Not a panacea since connections can't grow infinitely. Try the previous recommendations before this. + +.. _automatic_recovery: + +Automatic Recovery +------------------ + +If the pool loses the connection to the database, it will retry reconnecting using exponential backoff. With 32 seconds being the maximum backoff time between retries. + +The retries happen immediately after a connection loss, if :ref:`db-channel-enabled` is set to true(the default). Otherwise they'll happen once a request arrives. + +To notify the client of the next retry, the server sends a ``503 Service Unavailable`` status with the ``Retry-After: x`` header. Where ``x`` is the number of seconds programmed for the next retry. + +.. _external_connection_poolers: + +Using External Connection Poolers +--------------------------------- + +It's possible to use external connection poolers, such as PgBouncer. Session pooling is compatible, while transaction pooling requires :ref:`db-prepared-statements` set to ``false``. Statement pooling is not compatible with PostgREST. + +Also set :ref:`db-channel-enabled` to ``false`` since ``LISTEN`` is not compatible with transaction pooling. Although it should not give any errors if left enabled. + +.. note:: + + It’s not recommended to use an external connection pooler. `Our benchmarks `_ indicate it provides much lower performance than PostgREST built-in pool. diff --git a/docs/errors.rst b/docs/errors.rst index 5fe83b14d..74c4a3f84 100644 --- a/docs/errors.rst +++ b/docs/errors.rst @@ -28,29 +28,6 @@ PostgREST will forward errors coming from PostgreSQL. For instance, when queryin "message": "relation \"api.nonexistent_table\" does not exist" } -Errors from PostgREST ---------------------- - -Errors that come from PostgREST itself maintain the same structure. But differ in the ``PGRST`` prefix in the ``code`` field. For instance, when querying a function that does not exist in the :doc:`schema cache `: - -.. code-block:: http - - POST /rpc/nonexistent_function HTTP/1.1 - -.. code-block:: http - - HTTP/1.1 404 Not Found - Content-Type: application/json; charset=utf-8 - -.. code-block:: json - - { - "hint": "...", - "details": null - "code": "PGRST202", - "message": "Could not find the api.nonexistent_function() function in the schema cache" - } - .. _status_codes: HTTP Status Codes @@ -119,6 +96,30 @@ PostgREST translates `PostgreSQL error codes `: + +.. code-block:: http + + POST /rpc/nonexistent_function HTTP/1.1 + +.. code-block:: http + + HTTP/1.1 404 Not Found + Content-Type: application/json; charset=utf-8 + +.. code-block:: json + + { + "hint": "...", + "details": null + "code": "PGRST202", + "message": "Could not find the api.nonexistent_function() function in the schema cache" + } + + .. _pgrst_errors: PostgREST Error Codes diff --git a/docs/index.rst b/docs/index.rst index fa98bb6b5..24ff6547b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -148,6 +148,12 @@ Technical references for PostgREST's functionality. transactions.rst +.. toctree:: + :caption: Connection Pool + :hidden: + + connection_pool.rst + .. toctree:: :caption: Configuration :hidden: @@ -169,6 +175,7 @@ Technical references for PostgREST's functionality. - :doc:`Authentication ` - :doc:`API ` - :doc:`Transactions ` +- :doc:`Connection pool ` - :doc:`configuration` - :doc:`Schema Cache ` - :doc:`Errors ` @@ -235,6 +242,7 @@ PostgREST has a growing ecosystem of examples, libraries, and experiments. Here ecosystem.rst +* :ref:`external_jwt` * :ref:`community_tutorials` * :ref:`templates` * :ref:`eco_example_apps` diff --git a/docs/install.rst b/docs/install.rst index 8ba9f56d4..508f830b0 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -289,7 +289,7 @@ Deploying to Heroku # wait until the add-on is available heroku pg:wait -a ${YOUR_APP_NAME} -4. Create the necessary user roles according to the +4. Create the necessary user roles according to the `PostgREST documentation `_: .. code-block:: bash @@ -330,7 +330,7 @@ Deploying to Heroku 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 @@ -340,7 +340,7 @@ Deploying to Heroku 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. + 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. @@ -368,9 +368,9 @@ Deploying to Heroku 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: @@ -378,4 +378,4 @@ Deploying to Heroku curl https://${YOUR_APP_NAME}.herokuapp.com/todos -X POST \ -H "Content-Type: application/json" \ - -d '{"task": "do bad thing"}' + -d '{"task": "do bad thing"}' From 79620396edc776ed8ebf969ae05c5af9afb99277 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sun, 7 May 2023 13:03:58 -0300 Subject: [PATCH 563/652] remove manual inclusion of pages on index * drop unavailable doc pages from release notes * rename releases pages so they order in TOC --- docs/_static/css/custom.css | 20 +++ docs/{ => explanations}/admin.rst | 10 +- docs/{ => explanations}/db_authz.rst | 7 +- docs/{ => explanations}/install.rst | 4 +- docs/index.rst | 139 +++------------- docs/{ => references}/api.rst | 9 +- docs/{ => references}/auth.rst | 11 +- docs/{ => references}/configuration.rst | 8 +- docs/{ => references}/connection_pool.rst | 2 +- docs/{ => references}/errors.rst | 22 +-- docs/{ => references}/schema_cache.rst | 5 +- docs/{ => references}/transactions.rst | 33 ++-- docs/releases/{v9.0.0.rst => v09.0.0.rst} | 8 +- docs/releases/{v9.0.1.rst => v09.0.1.rst} | 4 +- docs/releases/v10.0.0.rst | 8 +- docs/releases/v10.2.0.rst | 4 +- docs/releases/v5.2.0.rst | 26 --- docs/releases/v6.0.2.rst | 79 --------- docs/releases/v7.0.0.rst | 106 ------------ docs/releases/v7.0.1.rst | 69 -------- docs/releases/v8.0.0.rst | 191 ---------------------- docs/tutorials/tut0.rst | 2 +- postgrest.dict | 1 + 23 files changed, 112 insertions(+), 656 deletions(-) rename docs/{ => explanations}/admin.rst (98%) rename docs/{ => explanations}/db_authz.rst (98%) rename docs/{ => explanations}/install.rst (99%) rename docs/{ => references}/api.rst (99%) rename docs/{ => references}/auth.rst (98%) rename docs/{ => references}/configuration.rst (99%) rename docs/{ => references}/connection_pool.rst (99%) rename docs/{ => references}/errors.rst (99%) rename docs/{ => references}/schema_cache.rst (99%) rename docs/{ => references}/transactions.rst (96%) rename docs/releases/{v9.0.0.rst => v09.0.0.rst} (96%) rename docs/releases/{v9.0.1.rst => v09.0.1.rst} (99%) delete mode 100644 docs/releases/v5.2.0.rst delete mode 100644 docs/releases/v6.0.2.rst delete mode 100644 docs/releases/v7.0.0.rst delete mode 100644 docs/releases/v7.0.1.rst delete mode 100644 docs/releases/v8.0.0.rst diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index fc7f2edb6..7000d0fce 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -65,3 +65,23 @@ div.line-block { .wy-table-responsive { overflow: visible !important; } + +#tutorials span.caption-text { + display: none; +} + +#references span.caption-text { + display: none; +} + +#explanations span.caption-text { + display: none; +} + +#how-tos span.caption-text { + display: none; +} + +#ecosystem span.caption-text { + display: none; +} diff --git a/docs/admin.rst b/docs/explanations/admin.rst similarity index 98% rename from docs/admin.rst rename to docs/explanations/admin.rst index ecd7972d1..1315ae14a 100644 --- a/docs/admin.rst +++ b/docs/explanations/admin.rst @@ -1,5 +1,8 @@ .. _admin: +Admin +##### + Hardening PostgREST =================== @@ -140,11 +143,6 @@ 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 @@ -248,7 +246,7 @@ To do this, set the configuration variable :ref:`admin-server-port` to the port 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 :doc:`Schema Cache `. A request will return ``200 OK`` if it is ready or ``503`` if not. +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``: diff --git a/docs/db_authz.rst b/docs/explanations/db_authz.rst similarity index 98% rename from docs/db_authz.rst rename to docs/explanations/db_authz.rst index 6e3748aad..eb6b3041b 100644 --- a/docs/db_authz.rst +++ b/docs/explanations/db_authz.rst @@ -1,6 +1,7 @@ -.. raw:: html +.. _db_authz: -

Database Authorization

+Database Authorization +###################### Database authorization is the process of granting and verifying database access permissions. PostgreSQL manages permissions using the concept of roles. @@ -101,7 +102,7 @@ A PostgREST instance exposes all the tables, views, and stored procedures of the It is recommended that you don't expose tables on the schemas you expose, 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 +.. image:: ../_static/db.png You must explicitly allow roles to access the exposed schemas: diff --git a/docs/install.rst b/docs/explanations/install.rst similarity index 99% rename from docs/install.rst rename to docs/explanations/install.rst index 508f830b0..8d3a6455a 100644 --- a/docs/install.rst +++ b/docs/explanations/install.rst @@ -1,7 +1,7 @@ .. _install: 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. @@ -95,7 +95,7 @@ For a complete reference of the configuration file, see :ref:`configuration`. 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 + .. 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. diff --git a/docs/index.rst b/docs/index.rst index 24ff6547b..e0931674c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -95,18 +95,10 @@ 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 - releases/v7.0.1 - releases/v7.0.0 - releases/v6.0.2 - releases/v5.2.0 + releases/* Tutorials --------- @@ -116,14 +108,11 @@ Are you new to PostgREST? This is the place to start! .. toctree:: :glob: :caption: Tutorials - :hidden: + :maxdepth: 1 tutorials/* -- :doc:`tutorials/tut0` -- :doc:`tutorials/tut1` - -Also have a look at :doc:`Installation ` and :ref:`community_tutorials`. +Also have a look at :ref:`install` and :ref:`community_tutorials`. References ---------- @@ -131,54 +120,12 @@ References Technical references for PostgREST's functionality. .. toctree:: - :caption: Authentication - :hidden: + :glob: + :caption: References + :name: references + :maxdepth: 1 - auth.rst - -.. toctree:: - :caption: API - :hidden: - - api.rst - -.. toctree:: - :caption: Transactions - :hidden: - - transactions.rst - -.. toctree:: - :caption: Connection Pool - :hidden: - - connection_pool.rst - -.. toctree:: - :caption: Configuration - :hidden: - - configuration.rst - -.. toctree:: - :caption: Schema Cache - :hidden: - - schema_cache.rst - -.. toctree:: - :caption: Errors - :hidden: - - errors.rst - -- :doc:`Authentication ` -- :doc:`API ` -- :doc:`Transactions ` -- :doc:`Connection pool ` -- :doc:`configuration` -- :doc:`Schema Cache ` -- :doc:`Errors ` + references/* Explanations ------------ @@ -186,50 +133,25 @@ Explanations Key concepts in PostgREST. .. toctree:: - :caption: Database Authorization - :hidden: + :glob: + :caption: Explanations + :name: explanations + :maxdepth: 1 - db_authz.rst + explanations/* -.. toctree:: - :caption: Administration - :hidden: - - admin.rst - -.. toctree:: - :caption: Installation - :hidden: - - install.rst - -- :doc:`Database Authorization ` -- :doc:`Administration ` -- :doc:`Installation ` - -.. _how_tos: - -How-to guides -------------- +How-tos +------- Recipes that'll help you address specific use-cases. .. toctree:: :glob: :caption: How-to guides - :hidden: + :name: how-tos + :maxdepth: 1 - how-tos/sql-user-management - how-tos/working-with-postgresql-data-types - how-tos/sql-user-management-using-postgres-users-and-passwords - how-tos/providing-images-for-img - how-tos/create-soap-endpoint - -- :doc:`how-tos/sql-user-management` -- :doc:`how-tos/working-with-postgresql-data-types` -- :doc:`how-tos/sql-user-management-using-postgres-users-and-passwords` -- :doc:`how-tos/providing-images-for-img` -- :doc:`how-tos/create-soap-endpoint` + how-tos/* Ecosystem --------- @@ -238,28 +160,11 @@ PostgREST has a growing ecosystem of examples, libraries, and experiments. Here .. toctree:: :caption: Ecosystem - :hidden: + :name: ecosystem + :maxdepth: 1 ecosystem.rst -* :ref:`external_jwt` -* :ref:`community_tutorials` -* :ref:`templates` -* :ref:`eco_example_apps` -* :ref:`devops` -* :ref:`eco_external_notification` -* :ref:`eco_extensions` -* :ref:`clientside_libraries` - - -Release Notes -------------- - -Changes among versions. - -- :doc:`releases/v9.0.0` -- :doc:`releases/v8.0.0` - In Production ------------- diff --git a/docs/api.rst b/docs/references/api.rst similarity index 99% rename from docs/api.rst rename to docs/references/api.rst index e2b163ecc..0dd517fc2 100644 --- a/docs/api.rst +++ b/docs/references/api.rst @@ -1,3 +1,8 @@ +.. _api: + +API +### + .. role:: sql(code) :language: sql @@ -873,7 +878,7 @@ 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 +.. image:: ../_static/film.png .. important:: @@ -1552,7 +1557,7 @@ Target Disambiguation For example, suppose you have the following ``orders`` and ``addresses`` tables: -.. image:: _static/orders.png +.. image:: ../_static/orders.png And you try to embed ``orders`` with ``addresses`` (this is the **target**): diff --git a/docs/auth.rst b/docs/references/auth.rst similarity index 98% rename from docs/auth.rst rename to docs/references/auth.rst index 31eb5e31e..2501c3536 100644 --- a/docs/auth.rst +++ b/docs/references/auth.rst @@ -1,8 +1,7 @@ -.. raw:: html +Authentication +============== -

Authentication

- -PostgREST is designed to keep the database at the center of API security. All :doc:`authorization happens in the database ` . 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 :ref:`authorization happens in the database ` . It is PostgREST's job to **authenticate** requests -- i.e. verify that a client is who they say they are -- and then let the database **authorize** client actions. .. _roles: @@ -11,7 +10,7 @@ Overview of role system There are three types of roles used by PostgREST, the **authenticator**, **anonymous** and **user** roles. The database administrator creates these roles and configures PostgREST to use them. -.. image:: _static/security-roles.png +.. image:: ../_static/security-roles.png The authenticator role is used for connecting to the database and should be configured to have very limited access. It is a chameleon whose job is to "become" other users to service authenticated HTTP requests. @@ -32,7 +31,7 @@ User Impersonation The picture below shows how the server handles authentication. If auth succeeds, it switches into the user role specified by the request, otherwise it switches into the anonymous role (if it's set in :ref:`db-anon-role`). -.. image:: _static/security-anon-choice.png +.. image:: ../_static/security-anon-choice.png This role switching mechanism is called **user impersonation**. In PostgreSQL it's done with the ``SET ROLE`` statement. diff --git a/docs/configuration.rst b/docs/references/configuration.rst similarity index 99% rename from docs/configuration.rst rename to docs/references/configuration.rst index 25d8e9282..d27eb0bcb 100644 --- a/docs/configuration.rst +++ b/docs/references/configuration.rst @@ -1,7 +1,7 @@ .. _configuration: Configuration -============= +############# 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`. @@ -16,7 +16,7 @@ Config parameters are read in the following order: .. _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: @@ -55,14 +55,14 @@ You can run ``postgrest --example`` to display all possible configuration parame .. _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`. diff --git a/docs/connection_pool.rst b/docs/references/connection_pool.rst similarity index 99% rename from docs/connection_pool.rst rename to docs/references/connection_pool.rst index 9bb38c3c6..c7f6d7ed0 100644 --- a/docs/connection_pool.rst +++ b/docs/references/connection_pool.rst @@ -1,5 +1,5 @@ Connection Pool ---------------- +=============== Every request to an :doc:`API resource ` borrows a connection from the connection pool to start a :doc:`transaction `. diff --git a/docs/errors.rst b/docs/references/errors.rst similarity index 99% rename from docs/errors.rst rename to docs/references/errors.rst index 74c4a3f84..e481a70fd 100644 --- a/docs/errors.rst +++ b/docs/references/errors.rst @@ -1,12 +1,12 @@ .. _error_source: -Error Format -============ +Errors +###### PostgREST error messages follow the PostgreSQL error structure. It includes ``MESSAGE``, ``DETAIL``, ``HINT``, ``ERRCODE`` and will add an HTTP status code to the response. Errors from PostgreSQL ----------------------- +====================== PostgREST will forward errors coming from PostgreSQL. For instance, when querying a nonexistent table: @@ -31,7 +31,7 @@ PostgREST will forward errors coming from PostgreSQL. For instance, when queryin .. _status_codes: HTTP Status Codes -================= +----------------- PostgREST translates `PostgreSQL error codes `_ into HTTP status as follows: @@ -97,7 +97,7 @@ PostgREST translates `PostgreSQL error codes `: @@ -123,7 +123,7 @@ Errors that come from PostgREST itself maintain the same structure. But differ i .. _pgrst_errors: PostgREST Error Codes -===================== +--------------------- PostgREST error codes have the form ``PGRSTgxx`` @@ -134,7 +134,7 @@ PostgREST error codes have the form ``PGRSTgxx`` .. _pgrst0**: Group 0 - Connection --------------------- +~~~~~~~~~~~~~~~~~~~~ Related to the connection with the database. @@ -161,7 +161,7 @@ Related to the connection with the database. .. _pgrst1**: Group 1 - Api Request ---------------------- +~~~~~~~~~~~~~~~~~~~~~ Related to the HTTP request elements. @@ -252,7 +252,7 @@ Related to the HTTP request elements. .. _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 `. @@ -285,7 +285,7 @@ Related to a :ref:`stale schema cache `. Most of the time, these e .. _pgrst3**: Group 3 - JWT -------------- +~~~~~~~~~~~~~ Related to the authentication process using JWT. You can follow the :ref:`tut1` for an example on how to implement authentication and the :doc:`Authentication page ` for more information on this process. @@ -310,7 +310,7 @@ Related to the authentication process using JWT. You can follow the :ref:`tut1` .. _pgrst_X**: Group X - Internal ------------------- +~~~~~~~~~~~~~~~~~~ Internal errors. If you encounter any of these, you may have stumbled on a PostgREST bug, please `open an issue `_ and we'll be glad to fix it. diff --git a/docs/schema_cache.rst b/docs/references/schema_cache.rst similarity index 99% rename from docs/schema_cache.rst rename to docs/references/schema_cache.rst index cfd9f0bb2..44d9fd2d2 100644 --- a/docs/schema_cache.rst +++ b/docs/references/schema_cache.rst @@ -1,8 +1,7 @@ .. _schema_cache: -.. raw:: html - -

Schema Cache

+Schema Cache +============ Some PostgREST features need metadata from the database schema. Getting this metadata requires expensive queries. To avoid repeating this work, PostgREST uses a schema cache. diff --git a/docs/transactions.rst b/docs/references/transactions.rst similarity index 96% rename from docs/transactions.rst rename to docs/references/transactions.rst index 7a73131c0..565a2e6d0 100644 --- a/docs/transactions.rst +++ b/docs/references/transactions.rst @@ -1,6 +1,5 @@ -.. raw:: html - -

Transactions

+Transactions +============ After :ref:`user_impersonation`, every request to an :doc:`API resource ` runs inside a transaction. The sequence of the transaction is as follows: @@ -14,7 +13,7 @@ After :ref:`user_impersonation`, every request to an :doc:`API resource ` r .. _access_mode: Access Mode -=========== +----------- The access mode on :ref:`tables_views` is determined by the HTTP method. @@ -59,14 +58,14 @@ Modifying the database inside READ ONLY transactions is not possible. PostgREST The :ref:`options_requests` method doesn't start a transaction, so it's not relevant here. Isolation Level -=============== +--------------- Every transaction uses the PostgreSQL default isolation level: READ COMMITTED. .. _tx_settings: Transaction-Scoped Settings -=========================== +--------------------------- PostgREST uses settings tied to the transaction lifetime. These can be used to get data about the HTTP request. Or to modify the HTTP response. @@ -87,7 +86,7 @@ And you can set them with ``set_config`` set_config('response.', 'value1' ,true); Request Role and Search Path ------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Because of :ref:`user_impersonation`, PostgREST sets the standard ``role``. You can get this in different ways: @@ -105,7 +104,7 @@ Additionally it also sets the ``search_path`` based on :ref:`db-schemas` and :re .. _guc_req_headers_cookies_claims: Request Headers, Cookies and JWT claims ---------------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PostgREST stores the headers, cookies and headers as JSON. To get them: @@ -130,7 +129,7 @@ PostgREST stores the headers, cookies and headers as JSON. To get them: .. _guc_req_path_method: Request Path and Method ------------------------ +~~~~~~~~~~~~~~~~~~~~~~~ The path and method are stored as ``text``. @@ -143,7 +142,7 @@ The path and method are stored as ``text``. .. _guc_resp_hdrs: Response Headers ----------------- +~~~~~~~~~~~~~~~~ You can set ``response.headers`` to add headers to the HTTP response. For instance, this statement would add caching headers to the response: @@ -169,7 +168,7 @@ Notice that the ``response.headers`` should be set to an *array* of single-key o .. _guc_resp_status: Response Status Code --------------------- +~~~~~~~~~~~~~~~~~~~~ You can set the ``response.status`` to override the default status code PostgREST provides. For instance, the following function would replace the default ``200`` status code. @@ -207,24 +206,24 @@ If the status code is standard, PostgREST will complete the status message(**I'm .. _main_query: Main query -========== +---------- The main query is produced by requesting the :doc:`API resources `. Transaction End -=============== +--------------- If the transaction doesn't fail, it will always end in a COMMIT. Unless :ref:`db-tx-end` is configured to ROLLBACK in any case or conditionally with ``Prefer: tx=rollback``. This can be used for testing purposes. Aborting transactions -===================== +--------------------- Any database failure(like a failed constraint) will result in a rollback of the transaction. You can also do a RAISE inside a function to cause a rollback. .. _raise_error: Raise errors with HTTP Status Codes ------------------------------------ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can return non-200 HTTP status codes by raising SQL exceptions. For instance, here's a saucy function that always responds with an error: @@ -279,7 +278,7 @@ Returns: .. _pre-request: Pre-Request -=========== +----------- The pre-request is a function that can run after the :ref:`tx_settings` are set and before the :ref:`main_query`. It's enabled with :ref:`db-pre-request`. @@ -288,7 +287,7 @@ This provides an opportunity to modify settings or raise an exception to prevent .. _pre_req_headers: Setting headers via pre-request -------------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ As an example, let's add some cache headers for all requests that come from an Internet Explorer(6 or 7) browser. diff --git a/docs/releases/v9.0.0.rst b/docs/releases/v09.0.0.rst similarity index 96% rename from docs/releases/v9.0.0.rst rename to docs/releases/v09.0.0.rst index 5963e8237..5827f542a 100644 --- a/docs/releases/v9.0.0.rst +++ b/docs/releases/v09.0.0.rst @@ -1,6 +1,6 @@ -PostgREST 9.0.0 -=============== +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 `_. @@ -50,7 +50,7 @@ Error messages Documentation improvements ~~~~~~~~~~~~~~~~~~~~~~~~~~ -* Added ``curl`` snippets to the :doc:`API <../api>` page. +* Added ``curl`` snippets to the :ref:`api` page. * Added the :ref:`automatic_recovery` section. @@ -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 :doc:`../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 :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 `_. diff --git a/docs/releases/v9.0.1.rst b/docs/releases/v09.0.1.rst similarity index 99% rename from docs/releases/v9.0.1.rst rename to docs/releases/v09.0.1.rst index dfbaa5735..bac8795b1 100644 --- a/docs/releases/v9.0.1.rst +++ b/docs/releases/v09.0.1.rst @@ -1,6 +1,6 @@ -PostgREST 9.0.1 -=============== +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 `_. diff --git a/docs/releases/v10.0.0.rst b/docs/releases/v10.0.0.rst index 9177fcfbc..e703324a4 100644 --- a/docs/releases/v10.0.0.rst +++ b/docs/releases/v10.0.0.rst @@ -1,6 +1,6 @@ -PostgREST 10.0.0 -================ +10.0.0 +====== Features -------- @@ -73,9 +73,9 @@ Documentation improvements * Added the :ref:`file_descriptors` subsection. -* Added a reference page for :doc:`Error documentation
`. +* Added a reference page for :ref:`error_source`. -* Moved the :ref:`error_source` and the :ref:`status_codes` sections to the :doc:`errors reference page
`. +* Moved the :ref:`error_source` and the :ref:`status_codes` sections to the :ref:`errors reference page `. * Moved the *Casting type to custom JSON* how-to to the :ref:`casting_range_to_json` subsection. diff --git a/docs/releases/v10.2.0.rst b/docs/releases/v10.2.0.rst index ff7dc61ea..96121b410 100644 --- a/docs/releases/v10.2.0.rst +++ b/docs/releases/v10.2.0.rst @@ -1,6 +1,6 @@ -PostgREST 10.2.0 -================ +10.2.0 +====== This minor version adds bug fixes and some features that provide stability to v10.0.0. These release notes include the changes added in versions `10.1.0 `_, `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 `_. diff --git a/docs/releases/v5.2.0.rst b/docs/releases/v5.2.0.rst deleted file mode 100644 index d32044526..000000000 --- a/docs/releases/v5.2.0.rst +++ /dev/null @@ -1,26 +0,0 @@ -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/docs/releases/v6.0.2.rst b/docs/releases/v6.0.2.rst deleted file mode 100644 index 47f870e44..000000000 --- a/docs/releases/v6.0.2.rst +++ /dev/null @@ -1,79 +0,0 @@ -.. |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:`scalar_return_formats`. - |br| -- `@steve-chavez `_ - -* 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`. - |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 is 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://retool.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 `_. diff --git a/docs/releases/v7.0.0.rst b/docs/releases/v7.0.0.rst deleted file mode 100644 index 67357c9b2..000000000 --- a/docs/releases/v7.0.0.rst +++ /dev/null @@ -1,106 +0,0 @@ -.. |br| raw:: html - -
- -v7.0.0 -====== - -You can download this release at the `PostgREST v7.0.0 release page `_. - -Added ------ - -* 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`. - |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 `_ - -* Support for user defined socket permission via :ref:`server-unix-socket-mode` config option - |br| -- `@Dansvidania `_ - -* 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 improvements - - + Explanation for :doc:`Schema Structure <../db_authz>`. - + 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 ------ - -* 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 -------- - -* :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`. - -* ``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://retool.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 `_. diff --git a/docs/releases/v7.0.1.rst b/docs/releases/v7.0.1.rst deleted file mode 100644 index 541c46245..000000000 --- a/docs/releases/v7.0.1.rst +++ /dev/null @@ -1,69 +0,0 @@ -.. |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 `_ - -* Documentation improvements - - + Added package managers to :ref:`install`. - -Changed -------- - -* From this version onwards, the release page will include a single Linux static executable that can be run on any Linux distribution. - -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://retool.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 `_. diff --git a/docs/releases/v8.0.0.rst b/docs/releases/v8.0.0.rst deleted file mode 100644 index 69fb70bd2..000000000 --- a/docs/releases/v8.0.0.rst +++ /dev/null @@ -1,191 +0,0 @@ -.. |br| raw:: html - -
- -v8.0.0 -====== - -You can download this release at the `PostgREST v8.0.0 release page `_. - -Added ------ - -* Allow HTTP status override through the :ref:`response.status ` GUC. - |br| -- `@steve-chavez `_ - -* Allow :ref:`s_procs_variadic`. - |br| -- `@wolfgangwalther `_ - -* Allow :ref:`embedding_view_chains` recursively to any depth. - |br| -- `@wolfgangwalther `_ - -* 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`. - |br| -- `@steve-chavez `_ - -* 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. - |br| -- `@laurenceisla `_ - -* 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 `_ - -* :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 `_ - -* 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 `_ - -* 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 `_ - -* Documentation improvements - - + Added the :doc:`../schema_cache` page. - + Moved the :ref:`schema_reloading` reference from :doc:`../admin` to :doc:`../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`. - |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_schema`) 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_schema`. - |br| -- `@laurenceisla `_ - -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 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 `_ - -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/tutorials/tut0.rst b/docs/tutorials/tut0.rst index 7b2ce647c..2d86c871f 100644 --- a/docs/tutorials/tut0.rst +++ b/docs/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 :doc:`options <../configuration>`, but this is all we need. +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: diff --git a/postgrest.dict b/postgrest.dict index d99c43b30..7fed84b22 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -179,6 +179,7 @@ Tcl tmuxp todo todos +tos Tsingson tsquery tx From 637a6e281d2be8d18f1de040641424199ed8fe40 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 8 May 2023 11:55:46 -0300 Subject: [PATCH 564/652] Move part of admin to references - Add integrations section with Nginx and SystemD - Move Nginx and SystemD sections from admin on integrations --- docs/_static/css/custom.css | 4 + docs/explanations/admin.rst | 359 ---------------------------------- docs/index.rst | 11 ++ docs/integrations/nginx.rst | 147 ++++++++++++++ docs/integrations/systemd.rst | 51 +++++ docs/references/admin.rst | 121 ++++++++++++ postgrest.dict | 1 + 7 files changed, 335 insertions(+), 359 deletions(-) delete mode 100644 docs/explanations/admin.rst create mode 100644 docs/integrations/nginx.rst create mode 100644 docs/integrations/systemd.rst create mode 100644 docs/references/admin.rst diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index 7000d0fce..a1bd351fe 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -85,3 +85,7 @@ div.line-block { #ecosystem span.caption-text { display: none; } + +#integrations span.caption-text { + display: none; +} diff --git a/docs/explanations/admin.rst b/docs/explanations/admin.rst deleted file mode 100644 index 1315ae14a..000000000 --- a/docs/explanations/admin.rst +++ /dev/null @@ -1,359 +0,0 @@ -.. _admin: - -Admin -##### - -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-block:: nginx - - http { - # ... - # upstream configuration - upstream postgrest { - server localhost:3000; - } - # ... - server { - # ... - # expose to the outside world - location /api/ { - default_type application/json; - proxy_hide_header Content-Location; - add_header Content-Location /api/$upstream_http_content_location; - proxy_set_header Connection ""; - proxy_http_version 1.1; - proxy_pass http://postgrest/; - } - # ... - } - } - -.. note:: - - For ubuntu, if you already installed nginx through :code:`apt` you can add this to the config file in - :code:`/etc/nginx/sites-enabled/default`. - -.. _block_fulltable: - -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: - -.. tabs:: - - .. code-tab:: http - - 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! - -.. tabs:: - - .. code-tab:: http - - 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: - -.. 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. - -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`, responses ordinarily include a range but leave the total unspecified like - -.. code-block:: http - - HTTP/1.1 200 OK - Range-Unit: items - Content-Range: 0-14/* - -However including the request header :code:`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-block:: postgres - - -- Pending nginx config: Remove any prefer header which contains the word count - -.. _https: - -HTTPS ------ - -PostgREST aims to do one thing well: add an HTTP interface to a PostgreSQL database. To keep the code small and focused we do not implement HTTPS. Use a reverse proxy such as NGINX to add this, `here's how `_. Note that some Platforms as a Service like Heroku also add SSL automatically in their load balancer. - -Rate Limiting -------------- - -Nginx supports "leaky bucket" rate limiting (see `official docs `_). Using standard Nginx configuration, routes can be grouped into *request zones* for rate limiting. For instance we can define a zone for login attempts: - -.. code-block:: nginx - - limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s; - -This creates a shared memory zone called "login" to store a log of IP addresses that access the rate limited urls. The space reserved, 10 MB (:code:`10m`) will give us enough space to store a history of 160k requests. We have chosen to allow only allow one request per second (:code:`1r/s`). - -Next we apply the zone to certain routes, like a hypothetical stored procedure called :code:`login`. - -.. code-block:: nginx - - location /rpc/login/ { - # apply rate limiting - limit_req zone=login burst=5; - } - -The burst argument tells Nginx to start dropping requests if more than five queue up from a specific IP. - -Nginx rate limiting is general and indiscriminate. To rate limit each authenticated request individually you will need to add logic in a :ref:`Custom Validation ` function. - -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. - -.. _pgrst_logging: - -Logging -------- - -PostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, and HTTP response status. - -.. code:: - - 127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 - "" "curl/7.64.0" - 127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 - "" "curl/7.64.0" - -For diagnostic information about the server itself, PostgREST logs to ``stderr``. - -.. code:: - - 12/Jun/2021:17:47:39 -0500: Attempting to connect to the database... - 12/Jun/2021:17:47:39 -0500: Listening on port 3000 - 12/Jun/2021:17:47:39 -0500: Connection successful - 12/Jun/2021:17:47:39 -0500: Config re-loaded - 12/Jun/2021:17:47:40 -0500: Schema cache loaded - -.. note:: - - When running it in an SSH session you must detach it from stdout or it will be terminated when the session closes. The easiest technique is redirecting the output to a log file or to the syslog: - - .. code-block:: bash - - ssh foo@example.com \ - 'postgrest foo.conf /var/log/postgrest.log 2>&1 &' - - # 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 -------------- - -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 - - # 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 :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. - -.. code:: sql - - # send logs where the collector can access them - log_destination = "stderr" - - # collect stderr output to log files - logging_collector = on - - # save logs in pg_log/ under the pg data directory - log_directory = "pg_log" - - # (optional) new log file per day - log_filename = "postgresql-%Y-%m-%d.log" - - # log every kind of SQL statement - log_statement = "all" - -Restart the database and watch the log file in real-time to understand how HTTP requests are being translated into SQL commands. - -.. note:: - - On Docker you can enable the logs by using a custom ``init.sh``: - - .. code:: bash - - #!/bin/sh - echo "log_statement = 'all'" >> /var/lib/postgresql/data/postgresql.conf - - After that you can start the container and check the logs with ``docker logs``. - - .. code:: bash - - docker run -v "$(pwd)/init.sh":"/docker-entrypoint-initdb.d/init.sh" -d postgres - docker logs -f - -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 - -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 -=========== - -For Linux distributions that use **systemd** (Ubuntu, Debian, Archlinux) you can create a daemon in the following way. - -First, create postgrest configuration in ``/etc/postgrest/config`` - -.. code-block:: ini - - db-uri = "postgres://:@localhost:5432/" - db-schemas = "" - db-anon-role = "" - jwt-secret = "" - -Then create the systemd service file in ``/etc/systemd/system/postgrest.service`` - -.. code-block:: ini - - [Unit] - Description=REST API for any PostgreSQL database - After=postgresql.service - - [Service] - ExecStart=/bin/postgrest /etc/postgrest/config - ExecReload=/bin/kill -SIGUSR1 $MAINPID - - [Install] - WantedBy=multi-user.target - -After that, you can enable the service at boot time and start it with: - -.. code-block:: bash - - systemctl enable postgrest - systemctl start postgrest - - ## For reloading the service - ## systemctl restart postgrest - -.. _file_descriptors: - -File Descriptors ----------------- - -File descriptors are kernel resources that are used by HTTP connections (among others). File descriptors are limited per process. The kernel default limit is 1024, which is increased in some Linux distributions. -When under heavy traffic, PostgREST can reach this limit and start showing ``No file descriptors available`` errors. To clear these errors, you can increase the process' file descriptor limit. - -.. code-block:: ini - - [Service] - LimitNOFILE=10000 - -Alternate URL Structure -======================= - -As discussed in :ref:`singular_plural`, there are no special URL forms for singular resources in PostgREST, only operators for filtering. Thus there are no URLs like :code:`/people/1`. It would be specified instead as - -.. tabs:: - - .. code-tab:: http - - GET /people?id=eq.1 HTTP/1.1 - Accept: application/vnd.pgrst.object+json - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people?id=eq.1" \ - -H "Accept: application/vnd.pgrst.object+json" - -This allows compound primary keys and makes the intent for singular response independent of a URL convention. - -Nginx rewrite rules allow you to simulate the familiar URL convention. The following example adds a rewrite rule for all table endpoints, but you'll want to restrict it to those tables that have a numeric simple primary key named "id." - -.. code-block:: nginx - - # support /endpoint/:id url style - location ~ ^/([a-z_]+)/([0-9]+) { - - # make the response singular - proxy_set_header Accept 'application/vnd.pgrst.object+json'; - - # assuming an upstream named "postgrest" - proxy_pass http://postgrest/$1?id=eq.$2; - - } - -.. TODO -.. Administration -.. API Versioning -.. HTTP Caching -.. Upgrading diff --git a/docs/index.rst b/docs/index.rst index e0931674c..a90ece340 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -153,6 +153,17 @@ Recipes that'll help you address specific use-cases. how-tos/* +Integrations +------------ + +.. toctree:: + :glob: + :caption: Integrations + :name: integrations + :maxdepth: 1 + + integrations/* + Ecosystem --------- diff --git a/docs/integrations/nginx.rst b/docs/integrations/nginx.rst new file mode 100644 index 000000000..25ce6ca98 --- /dev/null +++ b/docs/integrations/nginx.rst @@ -0,0 +1,147 @@ +Nginx +===== + +PostgREST is a fast way to construct a RESTful API. Its default behavior is great for scaffolding in development. When it's time to go to production it works great too, as long as you take precautions. +PostgREST is a small sharp tool that focuses on performing the API-to-database mapping. We rely on a reverse proxy like Nginx for additional safeguards. + +The first step is to create an Nginx configuration file that proxies requests to an underlying PostgREST server. + +.. code-block:: nginx + + http { + # ... + # upstream configuration + upstream postgrest { + server localhost:3000; + } + # ... + server { + # ... + # expose to the outside world + location /api/ { + default_type application/json; + proxy_hide_header Content-Location; + add_header Content-Location /api/$upstream_http_content_location; + proxy_set_header Connection ""; + proxy_http_version 1.1; + proxy_pass http://postgrest/; + } + # ... + } + } + +.. note:: + + For ubuntu, if you already installed nginx through :code:`apt` you can add this to the config file in + :code:`/etc/nginx/sites-enabled/default`. + +.. _block_fulltable: + +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: + +.. tabs:: + + .. code-tab:: http + + 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! + +.. tabs:: + + .. code-tab:: http + + 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: + +.. 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. + +.. _https: + +HTTPS +----- + +PostgREST aims to do one thing well: add an HTTP interface to a PostgreSQL database. To keep the code small and focused we do not implement HTTPS. Use a reverse proxy such as NGINX to add this, `here's how `_. Note that some Platforms as a Service like Heroku also add SSL automatically in their load balancer. + +Rate Limiting +------------- + +Nginx supports "leaky bucket" rate limiting (see `official docs `_). Using standard Nginx configuration, routes can be grouped into *request zones* for rate limiting. For instance we can define a zone for login attempts: + +.. code-block:: nginx + + limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s; + +This creates a shared memory zone called "login" to store a log of IP addresses that access the rate limited urls. The space reserved, 10 MB (:code:`10m`) will give us enough space to store a history of 160k requests. We have chosen to allow only allow one request per second (:code:`1r/s`). + +Next we apply the zone to certain routes, like a hypothetical stored procedure called :code:`login`. + +.. code-block:: nginx + + location /rpc/login/ { + # apply rate limiting + limit_req zone=login burst=5; + } + +The burst argument tells Nginx to start dropping requests if more than five queue up from a specific IP. + +Nginx rate limiting is general and indiscriminate. To rate limit each authenticated request individually you will need to add logic in a :ref:`Custom Validation ` function. + +Alternate URL Structure +----------------------- + +As discussed in :ref:`singular_plural`, there are no special URL forms for singular resources in PostgREST, only operators for filtering. Thus there are no URLs like :code:`/people/1`. It would be specified instead as + +.. tabs:: + + .. code-tab:: http + + GET /people?id=eq.1 HTTP/1.1 + Accept: application/vnd.pgrst.object+json + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?id=eq.1" \ + -H "Accept: application/vnd.pgrst.object+json" + +This allows compound primary keys and makes the intent for singular response independent of a URL convention. + +Nginx rewrite rules allow you to simulate the familiar URL convention. The following example adds a rewrite rule for all table endpoints, but you'll want to restrict it to those tables that have a numeric simple primary key named "id." + +.. code-block:: nginx + + # support /endpoint/:id url style + location ~ ^/([a-z_]+)/([0-9]+) { + + # make the response singular + proxy_set_header Accept 'application/vnd.pgrst.object+json'; + + # assuming an upstream named "postgrest" + proxy_pass http://postgrest/$1?id=eq.$2; + + } + +.. TODO +.. Administration +.. API Versioning +.. HTTP Caching +.. Upgrading diff --git a/docs/integrations/systemd.rst b/docs/integrations/systemd.rst new file mode 100644 index 000000000..55b7d6cf7 --- /dev/null +++ b/docs/integrations/systemd.rst @@ -0,0 +1,51 @@ +systemd +======= + +For Linux distributions that use **systemd** (Ubuntu, Debian, Archlinux) you can create a daemon in the following way. + +First, create postgrest configuration in ``/etc/postgrest/config`` + +.. code-block:: ini + + db-uri = "postgres://:@localhost:5432/" + db-schemas = "" + db-anon-role = "" + jwt-secret = "" + +Then create the systemd service file in ``/etc/systemd/system/postgrest.service`` + +.. code-block:: ini + + [Unit] + Description=REST API for any PostgreSQL database + After=postgresql.service + + [Service] + ExecStart=/bin/postgrest /etc/postgrest/config + ExecReload=/bin/kill -SIGUSR1 $MAINPID + + [Install] + WantedBy=multi-user.target + +After that, you can enable the service at boot time and start it with: + +.. code-block:: bash + + systemctl enable postgrest + systemctl start postgrest + + ## For reloading the service + ## systemctl restart postgrest + +.. _file_descriptors: + +File Descriptors +---------------- + +File descriptors are kernel resources that are used by HTTP connections (among others). File descriptors are limited per process. The kernel default limit is 1024, which is increased in some Linux distributions. +When under heavy traffic, PostgREST can reach this limit and start showing ``No file descriptors available`` errors. To clear these errors, you can increase the process' file descriptor limit. + +.. code-block:: ini + + [Service] + LimitNOFILE=10000 diff --git a/docs/references/admin.rst b/docs/references/admin.rst new file mode 100644 index 000000000..31f86599b --- /dev/null +++ b/docs/references/admin.rst @@ -0,0 +1,121 @@ +.. _admin: + +Admin +##### + +.. _pgrst_logging: + +Logging +------- + +PostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, and HTTP response status. + +.. code:: + + 127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 - "" "curl/7.64.0" + 127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 - "" "curl/7.64.0" + +For diagnostic information about the server itself, PostgREST logs to ``stderr``. + +.. code:: + + 12/Jun/2021:17:47:39 -0500: Attempting to connect to the database... + 12/Jun/2021:17:47:39 -0500: Listening on port 3000 + 12/Jun/2021:17:47:39 -0500: Connection successful + 12/Jun/2021:17:47:39 -0500: Config re-loaded + 12/Jun/2021:17:47:40 -0500: Schema cache loaded + +.. note:: + + When running it in an SSH session you must detach it from stdout or it will be terminated when the session closes. The easiest technique is redirecting the output to a log file or to the syslog: + + .. code-block:: bash + + ssh foo@example.com \ + 'postgrest foo.conf /var/log/postgrest.log 2>&1 &' + + # another option is to pipe the output into "logger -t postgrest" + +Currently PostgREST doesn't log the SQL commands executed against the underlying database. + +Database Logs +~~~~~~~~~~~~~ + +To find the SQL operations, you can watch the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below. + +Find :code:`postgresql.conf` inside your PostgreSQL data directory (to find that, issue the command :code:`show data_directory;`). Either find the settings scattered throughout the file and change them to the following values, or append this block of code to the end of the configuration file. + +.. code:: sql + + # send logs where the collector can access them + log_destination = "stderr" + + # collect stderr output to log files + logging_collector = on + + # save logs in pg_log/ under the pg data directory + log_directory = "pg_log" + + # (optional) new log file per day + log_filename = "postgresql-%Y-%m-%d.log" + + # log every kind of SQL statement + log_statement = "all" + +Restart the database and watch the log file in real-time to understand how HTTP requests are being translated into SQL commands. + +.. note:: + + On Docker you can enable the logs by using a custom ``init.sh``: + + .. code:: bash + + #!/bin/sh + echo "log_statement = 'all'" >> /var/lib/postgresql/data/postgresql.conf + + After that you can start the container and check the logs with ``docker logs``. + + .. code:: bash + + docker run -v "$(pwd)/init.sh":"/docker-entrypoint-initdb.d/init.sh" -d postgres + docker logs -f + +Server Version +-------------- + +When debugging a problem it's important to verify the PostgREST version. Look for the :code:`Server` HTTP response header, which contains the version number. + +.. code:: + + Server: postgrest/11.0.1 + +.. _health_check: + +Health Check +------------ + +You can enable a health check to verify if PostgREST is available for client requests. Also to check the status of its internal state. + +To do this, set the configuration variable :ref:`admin-server-port` to the port number of your preference. Two endpoints ``live`` and ``ready`` will then be available. + +The ``live`` endpoint verifies if PostgREST is running on its configured port. A request will return ``200 OK`` if PostgREST is alive or ``503`` otherwise. + +The ``ready`` endpoint also checks the state of both the Database Connection and the :ref:`schema_cache`. A request will return ``200 OK`` if it is ready or ``503`` if not. + +For instance, to verify if PostgREST is running at ``localhost:3000`` while the ``admin-server-port`` is set to ``3001``: + +.. 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 + +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. diff --git a/postgrest.dict b/postgrest.dict index 7fed84b22..4313956b8 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -61,6 +61,7 @@ HTTP HTTPS HV Ibarluzea +Integrations ilike imatch io From a530030b714745fc6a2f43fad83e7203be48c448 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 8 May 2023 13:57:23 -0300 Subject: [PATCH 565/652] add integrations * Move Nginx and systemd to integration * Move External JWT Generatio to integration * greenplum short notice --- docs/ecosystem.rst | 33 ++------------------------------- docs/integrations/greenplum.rst | 6 ++++++ docs/integrations/jwt_gen.rst | 31 +++++++++++++++++++++++++++++++ postgrest.dict | 1 + 4 files changed, 40 insertions(+), 31 deletions(-) create mode 100644 docs/integrations/greenplum.rst create mode 100644 docs/integrations/jwt_gen.rst diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index ba6292bd6..7774b3d1f 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -1,34 +1,3 @@ -.. _external_jwt: - -External JWT Generation ------------------------ - -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. - -.. 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. - - .. code:: javascript - - function (user, context, callback) { - - // Follow the documentations at - // https://postgrest.org/en/latest/configuration.html#db-role-claim-key - // to set a custom role claim on PostgREST - // and use it as custom claim attribute in this rule - const myRoleClaim = 'https://myapp.com/role'; - - user.app_metadata = user.app_metadata || {}; - context.accessToken[myRoleClaim] = user.app_metadata.role; - callback(null, user, context); - } - .. _community_tutorials: Community Tutorials @@ -47,6 +16,8 @@ Community Tutorials * `How PostgreSQL triggers work when called with a PostgREST PATCH HTTP request `_ - A tutorial to see how the old and new values are set or not when doing a PATCH request to PostgREST. +* `REST Data Service on YugabyteDB / PostgreSQL `_ + .. _templates: Templates diff --git a/docs/integrations/greenplum.rst b/docs/integrations/greenplum.rst new file mode 100644 index 000000000..c8f02242e --- /dev/null +++ b/docs/integrations/greenplum.rst @@ -0,0 +1,6 @@ +Greenplum +######### + +`Greenplum `_ has been reported to work by adding ``LOGIN`` to the :ref:`anonymous and user roles `. + +For more details, see https://github.com/PostgREST/postgrest/issues/2021. diff --git a/docs/integrations/jwt_gen.rst b/docs/integrations/jwt_gen.rst new file mode 100644 index 000000000..f2b80517a --- /dev/null +++ b/docs/integrations/jwt_gen.rst @@ -0,0 +1,31 @@ +.. _external_jwt: + +External JWT Generation +----------------------- + +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. + +.. 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. + + .. code:: javascript + + function (user, context, callback) { + + // Follow the documentations at + // https://postgrest.org/en/latest/configuration.html#db-role-claim-key + // to set a custom role claim on PostgREST + // and use it as custom claim attribute in this rule + const myRoleClaim = 'https://myapp.com/role'; + + user.app_metadata = user.app_metadata || {}; + context.accessToken[myRoleClaim] = user.app_metadata.role; + callback(null, user, context); + } + diff --git a/postgrest.dict b/postgrest.dict index 4313956b8..6e1e1dd4c 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -48,6 +48,7 @@ Github Google grantor GraphQL +Greenplum gte GUC gucs From c82b23940be4e5c489bd218eadabac5217eaf50b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 12:51:06 -0300 Subject: [PATCH 566/652] add missing mention to cache reload on recovery --- docs/references/connection_pool.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/references/connection_pool.rst b/docs/references/connection_pool.rst index c7f6d7ed0..38dabeee1 100644 --- a/docs/references/connection_pool.rst +++ b/docs/references/connection_pool.rst @@ -67,6 +67,8 @@ If the pool loses the connection to the database, it will retry reconnecting usi The retries happen immediately after a connection loss, if :ref:`db-channel-enabled` is set to true(the default). Otherwise they'll happen once a request arrives. +The server reloads the :ref:`schema_cache` when recoverying. + To notify the client of the next retry, the server sends a ``503 Service Unavailable`` status with the ``Retry-After: x`` header. Where ``x`` is the number of seconds programmed for the next retry. .. _external_connection_poolers: From fc635d18ae869780bd523da3e2138e1b07250abd Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 9 May 2023 23:07:53 -0300 Subject: [PATCH 567/652] split the API reference --- docs/_static/css/custom.css | 4 + docs/index.rst | 7 + docs/references/admin.rst | 79 + docs/references/api.rst | 2871 +---------------- docs/references/api/cors.rst | 34 + docs/references/api/openapi.rst | 45 + docs/references/api/options.rst | 52 + docs/references/api/resource_embedding.rst | 836 +++++ .../api/resource_representation.rst | 150 + docs/references/api/schemas.rst | 107 + docs/references/api/stored_procedures.rst | 456 +++ docs/references/api/tables_views.rst | 1060 ++++++ docs/references/api/url_grammar.rst | 108 + docs/references/configuration.rst | 25 +- docs/references/connection_pool.rst | 2 +- postgrest.dict | 1 + 16 files changed, 2954 insertions(+), 2883 deletions(-) create mode 100644 docs/references/api/cors.rst create mode 100644 docs/references/api/openapi.rst create mode 100644 docs/references/api/options.rst create mode 100644 docs/references/api/resource_embedding.rst create mode 100644 docs/references/api/resource_representation.rst create mode 100644 docs/references/api/schemas.rst create mode 100644 docs/references/api/stored_procedures.rst create mode 100644 docs/references/api/tables_views.rst create mode 100644 docs/references/api/url_grammar.rst diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css index a1bd351fe..602ef678f 100644 --- a/docs/_static/css/custom.css +++ b/docs/_static/css/custom.css @@ -89,3 +89,7 @@ div.line-block { #integrations span.caption-text { display: none; } + +#api span.caption-text { + display: none; +} diff --git a/docs/index.rst b/docs/index.rst index a90ece340..09529b4b5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -125,6 +125,13 @@ Technical references for PostgREST's functionality. :name: references :maxdepth: 1 + references/auth.rst + references/api.rst + references/transactions.rst + references/connection_pool.rst + references/schema_cache.rst + references/errors.rst + references/configuration.rst references/* Explanations diff --git a/docs/references/admin.rst b/docs/references/admin.rst index 31f86599b..ed83e8c9d 100644 --- a/docs/references/admin.rst +++ b/docs/references/admin.rst @@ -89,6 +89,85 @@ When debugging a problem it's important to verify the PostgREST version. Look fo Server: postgrest/11.0.1 +.. _explain_plan: + +Execution plan +-------------- + +You can get the `EXPLAIN execution plan `_ of a request by adding the ``Accept: application/vnd.pgrst.plan`` header. +This is enabled by :ref:`db-plan-enabled` (false 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 output of the plan is generated in ``text`` format by default but you can change it to JSON by using the ``+json`` suffix. + +.. 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 + } + ] + } + } + ] + +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``. + +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 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. + .. _health_check: Health Check diff --git a/docs/references/api.rst b/docs/references/api.rst index 0dd517fc2..b83d24b94 100644 --- a/docs/references/api.rst +++ b/docs/references/api.rst @@ -3,2861 +3,16 @@ API ### -.. role:: sql(code) - :language: sql - -.. _tables_views: - -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 - -.. tabs:: - - .. code-tab:: http - - GET /people HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people" - -There are no deeply/nested/routes. Each route provides OPTIONS, GET, HEAD, POST, PATCH, and DELETE verbs depending entirely on database permissions. - -.. note:: - - Why not provide nested routes? Many APIs allow nesting to retrieve related information, such as :code:`/films/1/director`. We offer a more flexible mechanism (inspired by GraphQL) to embed related information. It can handle one-to-many and many-to-many relationships. This is covered in the section about :ref:`resource_embedding`. - -.. _h_filter: - -Horizontal Filtering (Rows) ---------------------------- - -You can filter result rows by adding conditions on columns. For instance, to return people aged under 13 years old: - -.. tabs:: - - .. code-tab:: http - - GET /people?age=lt.13 HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people?age=lt.13" - -You can evaluate multiple conditions on columns by adding more query string parameters. For instance, to return people who are 18 or older **and** are students: - -.. tabs:: - - .. code-tab:: http - - 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" - -.. _operators: - -Operators -~~~~~~~~~ - -These operators are available: - -============ ======================== ================================================================================== -Abbreviation In PostgreSQL Meaning -============ ======================== ================================================================================== -eq :code:`=` equals -gt :code:`>` greater than -gte :code:`>=` greater than or equal -lt :code:`<` less than -lte :code:`<=` less than or equal -neq :code:`<>` or :code:`!=` not equal -like :code:`LIKE` LIKE operator (to avoid `URL encoding `_ 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)` - – also supports commas in quoted strings like - :code:`?a=in.("hi,there","yes,you")` -is :code:`IS` checking for exact equality (null,true,false,unknown) -fts :code:`@@` :ref:`fts` using to_tsquery -plfts :code:`@@` :ref:`fts` using plainto_tsquery -phfts :code:`@@` :ref:`fts` using phraseto_tsquery -wfts :code:`@@` :ref:`fts` using websearch_to_tsquery -cs :code:`@>` contains e.g. :code:`?tags=cs.{example, new}` -cd :code:`<@` contained in e.g. :code:`?values=cd.{1,2,3}` -ov :code:`&&` overlap (have points in common), e.g. :code:`?period=ov.[2017-01-01,2017-06-30]` – - also supports array types, use curly braces instead of square brackets e.g. - :code: `?arr=ov.{1,3}` -sl :code:`<<` strictly left of, e.g. :code:`?range=sl.(1,10)` -sr :code:`>>` strictly right of -nxr :code:`&<` does not extend to the right of, e.g. :code:`?range=nxr.(1,10)` -nxl :code:`&>` does not extend to the left of -adj :code:`-|-` is adjacent to, e.g. :code:`?range=adj.(1,10)` -not :code:`NOT` negates another operator, see :ref:`logical_operators` -or :code:`OR` logical :code:`OR`, see :ref:`logical_operators` -and :code:`AND` logical :code:`AND`, see :ref:`logical_operators` -============ ======================== ================================================================================== - -For more complicated filters you will have to create a new view in the database, or use a stored procedure. For instance, here's a view to show "today's stories" including possibly older pinned stories: - -.. code-block:: postgresql - - CREATE VIEW fresh_stories AS - SELECT * - FROM stories - WHERE pinned = true - OR published > now() - interval '1 day' - ORDER BY pinned DESC, published DESC; - -The view will provide a new endpoint: - -.. tabs:: - - .. code-tab:: http - - GET /fresh_stories HTTP/1.1 - - .. code-tab:: bash Curl - - 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))" - -.. _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 -~~~~~~~~~~~~~~~~ - -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. - -.. tabs:: - - .. code-tab:: http - - GET /tsearch?my_tsv=fts(french).amusant HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/tsearch?my_tsv=fts(french).amusant" - -.. tabs:: - - .. 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 - - curl "http://localhost:3000/tsearch?my_tsv=not.wfts(french).amusant" - -Using `websearch_to_tsquery` requires PostgreSQL of version at least 11.0 and will raise an error in earlier versions of the database. - -.. _v_filter: - -Vertical Filtering (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. - -.. tabs:: - - .. code-tab:: http - - GET /people?select=first_name,age HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people?select=first_name,age" - -.. code-block:: json - - [ - {"first_name": "John", "age": 30}, - {"first_name": "Jane", "age": 20} - ] - -The default is :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. - -.. tabs:: - - .. 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"}, - {"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. - -.. tabs:: - - .. 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"}, - {"full_name": "Jane Doe", "salary": "120000.00"} - ] - -.. _json_columns: - -JSON Columns ------------- - -You can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `__. - -.. code-block:: postgres - - CREATE TABLE people ( - id int, - json_data json - ); - -.. tabs:: - - .. code-tab:: http - - 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"}] } - ] - -.. tabs:: - - .. code-tab:: http - - 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"}, - { "id": 2, "number": "512-446-4988"} - ] - -This also works with filters: - -.. 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 - - curl "http://localhost:3000/people?select=id,json_data->blood_type&json_data->>blood_type=eq.A-" - -.. code-block:: json - - [ - { "id": 1, "blood_type": "A-" }, - { "id": 3, "blood_type": "A-" }, - { "id": 7, "blood_type": "A-" } - ] - -Note that ``->>`` is used to compare ``blood_type`` as ``text``. To compare with an integer value use ``->``: - -.. tabs:: - - .. code-tab:: http - - GET /people?select=id,json_data->age&json_data->age=gt.20 HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people?select=id,json_data->age&json_data->age=gt.20" - -.. code-block:: json - - [ - { "id": 11, "age": 25 }, - { "id": 12, "age": 30 }, - { "id": 15, "age": 35 } - ] -.. _composite_array_columns: - -Composite / Array Columns -------------------------- - -The arrow operators(``->``, ``->>``) can also be used for accessing composite fields and array elements. - -.. code-block:: postgres - - CREATE TYPE coordinates ( - lat decimal(8,6), - long decimal(9,6) - ); - - CREATE TABLE countries ( - id int, - location coordinates, - languages text[] - ); - -.. 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:: - - 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 / 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: - -.. code-block:: postgres - - 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', full_name(people))); - -A full-text search on the computed column: - -.. tabs:: - - .. code-tab:: http - - 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: - -.. tabs:: - - .. code-tab:: http - - GET /people?select=*,full_name HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people?select=*,full_name" - -.. 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. 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 ---------------- - -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: - -.. tabs:: - - .. code-tab:: http - - 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: - -Table / Columns with spaces -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You can request table/columns with spaces in them by percent encoding the spaces with ``%20``: - -.. tabs:: - - .. code-tab:: http - - 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: - -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. - -.. tabs:: - - .. code-tab:: http - - 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. - -.. tabs:: - - .. code-tab:: http - - GET /vulnerabilities?%22information.cpe%22=like.*MS* HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/vulnerabilities?%22information.cpe%22=like.*MS*" - -If the value filtered by the ``in`` operator has a double quote (``"``), you can escape it using a backslash ``"\""``. A backslash itself can be used with a double backslash ``"\\"``. - -Here ``Quote:"`` and ``Backslash:\`` are percent-encoded values. Note that ``%5C`` is the percent-encoded backslash. - -.. tabs:: - - .. code-tab:: http - - 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:: - - 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 --------- - -The reserved word :sql:`order` reorders the response rows. It uses a comma-separated list of columns and directions: - -.. tabs:: - - .. code-tab:: http - - GET /people?order=age.desc,height.asc HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people?order=age.desc,height.asc" - -If no direction is specified it defaults to ascending order: - -.. tabs:: - - .. code-tab:: http - - 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``: - -.. tabs:: - - .. code-tab:: 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 - - .. 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. You can sort by nested fields of :ref:`json_columns` with the JSON operators. - -.. _limits: - -Limits and Pagination ---------------------- - -PostgREST uses HTTP range headers to describe the size of results. Every response contains the current range and, if requested, the total number of results: - -.. code-block:: http - - HTTP/1.1 200 OK - Range-Unit: items - Content-Range: 0-14/* - -Here items zero through fourteen are returned. This information is available in every response and can help you render pagination controls on the client. This is an RFC7233-compliant solution that keeps the response JSON cleaner. - -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. - -.. tabs:: - - .. 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: - -.. code-block:: http - - HTTP/1.1 200 OK - Range-Unit: items - Content-Range: 0-17/* - -You may also request open-ended ranges for an offset with no limit, e.g. :code:`Range: 10-`. - -The other way to request a limit or offset is with query parameters. For example - -.. tabs:: - - .. code-tab:: http - - 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. - -.. _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: - -.. tabs:: - - .. 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 - -.. code-block:: http - - HTTP/1.1 206 Partial Content - Range-Unit: items - Content-Range: 0-24/3573458 - -.. _planned_count: - -Planned Count -------------- - -To avoid the shortcomings of :ref:`exact count `, PostgREST can leverage PostgreSQL statistics and get a fairly accurate and fast count. -To do this, specify the ``Prefer: count=planned`` header. - -.. tabs:: - - .. code-tab:: http - - HEAD /bigtable?limit=25 HTTP/1.1 - Prefer: count=planned - - .. code-tab:: bash Curl - - curl "http://localhost:3000/bigtable?limit=25" -I \ - -H "Prefer: count=planned" - -.. code-block:: http - - HTTP/1.1 206 Partial Content - Content-Range: 0-24/3572000 - -Note that the accuracy of this count depends on how up-to-date are the PostgreSQL statistics tables. -For example in this case, to increase the accuracy of the count you can do ``ANALYZE bigtable``. -See `ANALYZE `_ for more details. - -.. _estimated_count: - -Estimated Count ---------------- - -When you are interested in the count, the relative error is important. If you have a :ref:`planned count ` of 1000000 and the exact count is -1001000, the error is small enough to be ignored. But with a planned count of 7, an exact count of 28 would be a huge misprediction. - -In general, when having smaller row-counts, the estimated count should be as close to the exact count as possible. - -To help with these cases, PostgREST can get the exact count up until a threshold and get the planned count when -that threshold is surpassed. To use this behavior, you can specify the ``Prefer: count=estimated`` header. The **threshold** is -defined by :ref:`db-max-rows`. - -Here's an example. Suppose we set ``db-max-rows=1000`` and ``smalltable`` has 321 rows, then we'll get the exact count: - -.. tabs:: - - .. code-tab:: http - - 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 - - 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: - -.. tabs:: - - .. code-tab:: http - - 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 - - HTTP/1.1 206 Partial Content - Content-Range: 0-24/3572000 - -.. _res_format: - -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. - -Use the Accept request header to specify the acceptable format (or formats) for the response: - -.. tabs:: - - .. code-tab:: http - - GET /people HTTP/1.1 - Accept: application/json - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people" \ - -H "Accept: application/json" - -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`: - -* ``application/octet-stream`` -* ``text/plain`` -* ``text/xml`` - -The server will default to JSON for API endpoints and OpenAPI on the root. - -.. _singular_plural: - -Singular or Plural ------------------- - -By default PostgREST returns all JSON results in an array, even when there is only one item. For example, requesting :code:`/items?id=eq.1` returns - -.. code:: json - - [ - { "id": 1 } - ] - -This can be inconvenient for client code. To return the first result as an object unenclosed by an array, specify :code:`vnd.pgrst.object` as part of the :code:`Accept` header - -.. tabs:: - - .. code-tab:: http - - GET /items?id=eq.1 HTTP/1.1 - Accept: application/vnd.pgrst.object+json - - .. code-tab:: bash Curl - - curl "http://localhost:3000/items?id=eq.1" \ - -H "Accept: application/vnd.pgrst.object+json" - -This returns - -.. code:: json - - { "id": 1 } - -When a singular response is requested but no entries are found, the server responds with an error message and 406 Not Acceptable status code rather than the usual empty array and 200 status: - -.. code-block:: json - - { - "message": "JSON object requested, multiple (or no) rows returned", - "details": "Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row", - "hint": null, - "code": "PGRST505" - } - -.. note:: - - Many APIs distinguish plural and singular resources using a special nested URL convention e.g. `/stories` vs `/stories/1`. Why do we use `/stories?id=eq.1`? The answer is because a singular resource is (for us) a row determined by a primary key, and primary keys can be compound (meaning defined across more than one column). The more familiar nested urls consider only a degenerate case of simple and overwhelmingly numeric primary keys. These so-called artificial keys are often introduced automatically by Object Relational Mapping libraries. - - Admittedly PostgREST could detect when there is an equality condition holding on all columns constituting the primary key and automatically convert to singular. However this could lead to a surprising change of format that breaks unwary client code just by filtering on an extra column. Instead we allow manually specifying singular vs plural to decouple that choice from the URL format. - -.. _resource_embedding: - -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 - -.. 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-one: - -Many-to-one relationships -------------------------- - -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:: - - .. code-tab:: http - - GET /films?select=title,directors(id,last_name) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/films?select=title,directors(id,last_name)" - -.. code-block:: json - - [ - { "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" - } - } - ] - -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:: - - .. code-tab:: http - - GET /films?select=title,director:directors(id,last_name) HTTP/1.1 - - .. code-tab:: bash Curl - - 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 --------------------------- - -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 - - create table roles( - film_id int references films(id) - , actor_id int references actors(id) - , primary key(film_id, actor_id) - ); - - -- the join table can also be detected if the composite key has additional columns - - 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) - ); - -.. tabs:: - - .. code-tab:: http - - GET /actors?select=first_name,last_name,films(title) HTTP/1.1 - - .. code-tab:: bash Curl - - 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 ------------------------- - -one-to-one relationships are detected if there's an unique constraint on a foreign key. - -.. code-block:: postgresql - - CREATE TABLE technical_specs( - film_id INT REFERENCES films UNIQUE, - runtime TIME, - camera TEXT, - sound TEXT - ); - -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 PRIMARY KEY REFERENCES films, - runtime TIME, - camera TEXT, - sound TEXT - ); - -.. tabs:: - - .. code-tab:: http - - GET /films?select=title,technical_specs(runtime) HTTP/1.1 - - .. code-tab:: bash Curl - - 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 ----------------------- - -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 foreign table premieres ( - id integer, - location text, - "date" date, - film_id integer - ) server import_csv options ( filename '/tmp/directors.csv', format 'csv'); - - 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.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 /films?select=name,premieres(name) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/films?select=name,premieres(name)" - -.. code-block:: json - - [ - { - "name": "Pulp Ficiton", - "premieres": [{"location": "Cannes Festival"}] - }, - ".." - ] - -Computed relationships also allow you to override the ones that are automatically detected by PostgREST. - -For example, to override the :ref:`many-to-one relationship ` between ``films`` and ``directors``. - -.. code-block:: postgres - - 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``. - -.. 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 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: - -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: - -.. tabs:: - - .. code-tab:: http - - GET /actors?select=roles(character,films(title,year)) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/actors?select=roles(character,films(title,year))" - -.. _embed_filters: - -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: - -.. tabs:: - - .. code-tab:: http - - GET /films?select=*,actors(*)&actors.order=last_name,first_name HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/films?select=*,actors(*)&actors.order=last_name,first_name" - -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: - -.. tabs:: - - .. code-tab:: http - - GET /films?select=*,roles(*)&roles.character=in.(Chico,Harpo,Groucho) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/films?select=*,roles(*)&roles.character=in.(Chico,Harpo,Groucho)" - -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 be used for a similar operation: - -.. tabs:: - - .. code-tab:: http - - GET /films?select=*,roles(*)&roles.or=(character.eq.Gummo,character.eq.Zeppo) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/films?select=*,roles(*)&roles.or=(character.eq.Gummo,character.eq.Zeppo)" - -Limit and offset operations are possible: - -.. tabs:: - - .. code-tab:: http - - GET /films?select=*,actors(*)&actors.limit=10&actors.offset=2 HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/films?select=*,actors(*)&actors.limit=10&actors.offset=2" - -Embedded resources can be aliased and filters can be applied on these aliases: - -.. tabs:: - - .. code-tab:: 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 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/films?select=*,90_comps:competitions(name),91_comps:competitions(name)&90_comps.year=eq.1990&91_comps.year=eq.1991" - -Filters can also be applied on nested embedded resources: - -.. tabs:: - - .. code-tab:: http - - GET /films?select=*,roles(*,actors(*))&roles.actors.order=last_name&roles.actors.first_name=like.*Tom* HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/films?select=*,roles(*,actors(*))&roles.actors.order=last_name&roles.actors.first_name=like.*Tom*" - -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: - -Embedding with Top-level Filtering ----------------------------------- - -By default, :ref:`embed_filters` don't change the top-level resource(``films``) rows at all: - -.. tabs:: - - .. code-tab:: http - - 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 - - [ - { - "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``: - -.. 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 - - curl "http://localhost:3000/films?select=title,actors!inner(first_name,last_name)&actors.first_name=eq.Jehanne" - -.. code-block:: json - - [ - { - "title": "The Haunted Castle", - "actors": [ - { - "first_name": "Jehanne", - "last_name": "d'Alcy" - } - ] - } - ] - -.. _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``: - -.. tabs:: - - .. code-tab:: http - - GET /box_office?select=bo_date,gross_revenue,films(title)&gross_revenue=gte.1000000 HTTP/1.1 - - .. code-tab:: bash Curl - - 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_views: - -Embedding Views ---------------- - -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. - -For instance, the following view has ``nominations``, ``films`` and ``competitions`` as source tables: - -.. code-block:: postgres - - CREATE VIEW nominations_view AS - 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 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=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=film_title,films(language),roles(character),actors(last_name,first_name)&rank=eq.5" - -It's also possible to embed `Materialized Views `_. - -.. important:: - - - 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. - + 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`. - -.. _embedding_view_chains: - -Embedding Chains of Views -------------------------- - -Views can also depend on other views, which in turn depend on the actual source table. For PostgREST to pick up those chains recursively to any depth, all the views must be in the search path, so either in the exposed schema (:ref:`db-schemas`) or in one of the schemas set in :ref:`db-extra-search-path`. This does not apply to the source table, which could be in a private schema as well. See :ref:`schema_isolation` for more details. - -.. _s_proc_embed: - -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``). - -.. code-block:: plpgsql - - CREATE FUNCTION getallfilms() RETURNS SETOF films AS $$ - SELECT * FROM films; - $$ LANGUAGE SQL IMMUTABLE; - -A request with ``directors`` embedded: - -.. tabs:: - - .. code-tab:: http - - GET /rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers* HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers*" - -.. code-block:: json - - [ - { "title": "Workers Leaving The Lumière Factory In Lyon", - "directors": { - "id": 2, - "last_name": "Lumière" - } - } - ] - -.. _mutation_embed: - -Embedding after Insertions/Updates/Deletions --------------------------------------------- - -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**. - -.. tabs:: - - .. code-tab:: 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" - } - - .. 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: - -.. code-block:: json - - { - "title": "127 hours", - "year": 2010, - "director": { - "first_name": "Danny", - "last_name": "Boyle" - } - } - -.. _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 unambiguously -by just specifying the tables names. - -.. _target_disamb: - -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**): - -.. tabs:: - - .. 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: - -.. 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**. - -.. tabs:: - - .. code-tab:: http - - GET /orders?select=name,billing_address(name) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/orders?select=name,billing_address(name)" - -.. code-block:: json - - [ - { - "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. - -.. tabs:: - - .. code-tab:: http - - GET /orders?select=name,billing_address:billing_address_id(name) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/orders?select=name,billing_address:billing_address_id(name)" - -.. code-block:: json - - [ - { - "name": "Personal Water Filter", - "billing_address": { - "name": "32 Glenlake Dr.Dearborn, MI 48124" - } - } - ] - -.. _hint_disamb: - -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``. - -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=*,central_addresses(*) HTTP/1.1 - - .. code-tab:: bash Curl - - 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 still specify ``central_addresses`` as the **target** and use the ``billing_address`` foreign key as the **hint**: - -.. 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 - -.. code-block:: http - - HTTP/1.1 200 OK - - [ ... ] - -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: - -.. tabs:: - - .. code-tab:: http - - 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" - -.. note:: - - If the relationship is so complex that hint disambiguation does not solve it, you can use :ref:`computed_relationships`. - -.. _insert: - -Insertions -========== - -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. - -.. tabs:: - - .. code-tab:: http - - POST /table_name HTTP/1.1 - - { "col1": "value1", "col2": "value2" } - - .. code-tab:: bash Curl - - curl "http://localhost:3000/table_name" \ - -X POST -H "Content-Type: application/json" \ - -d '{ "col1": "value1", "col2": "value2" }' - -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. - -URL encoded payloads can be posted with ``Content-Type: application/x-www-form-urlencoded``. - -.. tabs:: - - .. code-tab:: http - - POST /people HTTP/1.1 - Content-Type: application/x-www-form-urlencoded - - name=John+Doe&age=50&weight=80 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people" \ - -X POST -H "Content-Type: application/x-www-form-urlencoded" \ - -d "name=John+Doe&age=50&weight=80" - -.. note:: - - When inserting a row you must post a JSON object, not quoted JSON. - - .. code:: - - Yes - { "a": 1, "b": 2 } - - No - "{ \"a\": 1, \"b\": 2 }" - - Some JavaScript libraries will post the data incorrectly if you're not careful. For best results try one of the :ref:`clientside_libraries` built for PostgREST. - -.. important:: - - It's recommended that you `use triggers instead of rules `_. - Insertion on views with complex `rules `_ might not work out of the box with PostgREST due to its usage of CTEs. - If you want to keep using rules, a workaround is to wrap the view insertion in a stored procedure and call it through the :ref:`s_procs` interface. - For more details, see this `github issue `_. - -.. _bulk_insert: - -Bulk Insert ------------ - -Bulk insert works exactly like single row insert except that you provide either a JSON array of objects having uniform keys, or lines in CSV format. This not only minimizes the HTTP requests required but uses a single INSERT statement on the back-end for efficiency. - -To bulk insert CSV simply post to a table route with :code:`Content-Type: text/csv` and include the names of the columns as the first row. For instance - -.. tabs:: - - .. code-tab:: http - - POST /people HTTP/1.1 - Content-Type: text/csv - - name,age,height - J Doe,62,70 - Jonas,10,55 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people" \ - -X POST -H "Content-Type: text/csv" \ - --data-binary @- << EOF - name,age,height - J Doe,62,70 - Jonas,10,55 - EOF - -An empty field (:code:`,,`) is coerced to an empty string and the reserved word :code:`NULL` is mapped to the SQL null value. Note that there should be no spaces between the column names and commas. - -To bulk insert JSON post an array of objects having all-matching keys - -.. tabs:: - - .. code-tab:: http - - POST /people HTTP/1.1 - Content-Type: application/json - - [ - { "name": "J Doe", "age": 62, "height": 70 }, - { "name": "Janus", "age": 10, "height": 55 } - ] - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people" \ - -X POST -H "Content-Type: application/json" \ - -d @- << EOF - [ - { "name": "J Doe", "age": 62, "height": 70 }, - { "name": "Janus", "age": 10, "height": 55 } - ] - EOF - -.. _specify_columns: - -Specifying Columns ------------------- - -By using the :code:`columns` query parameter it's possible to specify the payload keys that will be inserted and ignore the rest of the payload. - -.. tabs:: - - .. code-tab:: 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": "..." - } - - .. code-tab:: bash Curl - - curl "http://localhost:3000/datasets?columns=source,publication_date,figure" \ - -X POST -H "Content-Type: application/json" \ - -d @- << EOF - { - "source": "Natural Disaster Prevention and Control", - "publication_date": "2015-09-11", - "figure": 1100, - "location": "...", - "comment": "...", - "extra": "...", - "stuff": "..." - } - EOF - -In this case, only **source**, **publication_date** and **figure** will be inserted. The rest of the JSON keys will be ignored. - -Using this also has the side-effect of being more efficient for :ref:`bulk_insert` since PostgREST will not process the JSON and -it'll send it directly to PostgreSQL. - -.. _update: - -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: - -.. tabs:: - - .. code-tab:: 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 } - ] - - .. code-tab:: bash Curl - - curl "http://localhost:3000/employees" \ - -X POST -H "Content-Type: application/json" \ - -H "Prefer: resolution=merge-duplicates" \ - -d @- << EOF - [ - { "id": 1, "name": "Old employee 1", "salary": 30000 }, - { "id": 2, "name": "Old employee 2", "salary": 42000 }, - { "id": 3, "name": "New employee 3", "salary": 50000 } - ] - EOF - -By default, upsert operates based on the primary key columns, you must specify all of them. You can also choose to ignore the duplicates with :code:`Prefer: resolution=ignore-duplicates`. This works best when the primary key is natural, but it's also possible to use it if the primary key is surrogate (example: "id serial primary key"). For more details read `this issue `_. - -.. 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. - -.. tabs:: - - .. code-tab:: 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 } - ] - - .. 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 - -.. _upsert_put: - -PUT ---- - -A single row upsert can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: - -.. tabs:: - - .. code-tab:: http - - PUT /employees?id=eq.4 HTTP/1.1 - - { "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. - -.. _delete: - -Deletions -========= - -To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instance deleting inactive users: - -.. tabs:: - - .. code-tab:: http - - DELETE /user?active=is.false HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/user?active=is.false" -X DELETE - -Deletions also support :code:`Prefer: return=representation` plus :ref:`v_filter`. - -.. tabs:: - - .. code-tab:: http - - DELETE /user?id=eq.1 HTTP/1.1 - Prefer: return=representation - - .. code-tab:: bash Curl - - curl "http://localhost:3000/user?id=eq.1" -X DELETE \ - -H "Prefer: return=representation" - -.. code-block:: json - - {"id": 1, "email": "johndoe@email.com"} - -.. warning:: - - Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`. - -.. _limited_update_delete: - -Limited 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 -============== - -The PostgREST URL grammar limits the kinds of queries clients can perform. It prevents arbitrary, potentially poorly constructed and slow client queries. It's good for quality of service, but means database administrators must create custom views and stored procedures to provide richer endpoints. The most common causes for custom endpoints are - -* Table unions -* More complicated joins than those provided by `Resource Embedding`_ -* Geo-spatial queries that require an argument, like "points near (lat,lon)" - -.. _s_procs: - -Stored Procedures -================= - -*"A single resource can be the equivalent of a database stored procedure, with the power to abstract state changes over any number of storage items"* -- `Roy T. Fielding `_ - -Every stored procedure in the exposed database schema is accessible under the :code:`/rpc` prefix. Procedures can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). - -.. note:: - - The ``/rpc`` prefix is used to avoid name collisions between views and procedures. - -POST on Stored Procedures -------------------------- - -To supply arguments in an API call, include a JSON object in the request payload. Each key/value of the object will become an argument. - -For instance, assume we have created this function in the database. - -.. code-block:: plpgsql - - CREATE FUNCTION add_them(a integer, b integer) - RETURNS integer AS $$ - SELECT a + b; - $$ LANGUAGE SQL IMMUTABLE; - -.. important:: - - Whenever you create or change a function you must refresh PostgREST's schema cache. See the section :ref:`schema_reloading`. - -The client can call it by posting an object like - -.. tabs:: - - .. code-tab:: http - - POST /rpc/add_them HTTP/1.1 - - { "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 - -.. note:: - - PostgreSQL converts identifier names to lowercase unless you quote them like: - - .. code-block:: postgres - - CREATE FUNCTION "someFunc"("someParam" text) ... - -GET on Stored Procedures ------------------------- - -If the function doesn't modify the database, it will also run under the GET method(see :ref:`access_mode`). - -.. tabs:: - - .. code-tab:: http - - 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``. - -.. _s_proc_single_json: - -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. - -.. code-block:: plpgsql - - CREATE FUNCTION mult_them(param json) RETURNS int AS $$ - SELECT (param->>'x')::int * (param->>'y')::int - $$ LANGUAGE SQL; - -.. tabs:: - - .. code-tab:: http - - POST /rpc/mult_them HTTP/1.1 - Prefer: params=single-object - - { "x": 4, "y": 2 } - - .. code-tab:: bash Curl - - curl "http://localhost:3000/rpc/mult_them" \ - -X POST -H "Content-Type: application/json" \ - -H "Prefer: params=single-object" \ - -d '{ "x": 4, "y": 2 }' - -.. code-block:: json - - 8 - -.. _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``, ``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. - -.. 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 XML, the parameter type must be ``xml`` and the header ``Content-Type: text/xml`` must be included in the request. - -To send raw binary, the parameter type must be ``bytea`` and the header ``Content-Type: application/octet-stream`` must be included in the request. - -.. code-block:: plpgsql - - CREATE TABLE files(blob bytea); - - CREATE FUNCTION upload_binary(bytea) RETURNS void AS $$ - INSERT INTO files(blob) VALUES ($1); - $$ LANGUAGE SQL; - -.. 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: - -Calling functions with array parameters ---------------------------------------- - -You can call a function that takes an array parameter: - -.. code-block:: postgres - - create function plus_one(arr int[]) returns int[] as $$ - SELECT array_agg(n + 1) FROM unnest($1) AS n; - $$ language sql; - -.. tabs:: - - .. code-tab:: http - - POST /rpc/plus_one HTTP/1.1 - Content-Type: application/json - - {"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 - - [2,3,4,5] - -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``). - -.. tabs:: - - .. code-tab:: http - - 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: - - .. tabs:: - - .. code-tab:: http - - POST /rpc/plus_one HTTP/1.1 - - { "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. - -.. _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; - -.. tabs:: - - .. code-tab:: http - - 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 - - [2,3,4,5] - -In a GET request, you can repeat the same parameter name: - -.. tabs:: - - .. code-tab:: http - - 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``: - -.. 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 - - .. code-tab:: bash Curl - - 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: - -.. tabs:: - - .. code-tab:: http - - 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 - -.. tabs:: - - .. code-tab:: http - - GET /rpc/best_films_2017 HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/rpc/best_films_2017" - -.. code-block:: json - - [ - { "title": "Okja", "rating": 7.4}, - { "title": "Call me by your name", "rating": 8}, - { "title": "Blade Runner 2049", "rating": 8.1} - ] - -To manually choose a return format such as binary, plain text or XML, see the section :ref:`scalar_return_formats`. - - -.. _bulk_call: - -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. - -.. tabs:: - - .. code-tab:: http - - 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 - - [ 3, 7 ] - -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. - -Function filters ----------------- - -A function that returns a table type response can be shaped using the same filters as the ones used for tables and views: - -.. code-block:: postgres - - CREATE FUNCTION best_films_2017() RETURNS SETOF films .. - -.. tabs:: - - .. code-tab:: http - - GET /rpc/best_films_2017?select=title,director:directors(*) HTTP/1.1 - - .. code-tab:: bash Curl - - 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 --------------------- - -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) .. - -.. tabs:: - - .. code-tab:: http - - GET /rpc/rental_duration?customer_id=232 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:: - - Overloaded functions with the same argument names but different types are not supported. - -.. _scalar_return_formats: - -Response Formats For Scalar Responses -===================================== - -For scalar return values such as - -* single-column selects on tables or -* scalar functions, - -you can set the additional content types - -* ``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:: - - .. 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" - -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 generate_xml_content(..) RETURNS xml .. - -.. tabs:: - - .. code-tab:: http - - POST /rpc/generate_xml_content HTTP/1.1 - Accept: text/xml - - .. code-tab:: bash Curl - - curl "http://localhost:3000/rpc/generate_xml_content" \ - -X POST -H "Accept: text/xml" - -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 get_descriptions(..) RETURNS SETOF TABLE(id int, description text) .. - -.. tabs:: - - .. code-tab:: http - - POST /rpc/get_descriptions?select=description HTTP/1.1 - Accept: text/plain - - .. code-tab:: bash Curl - - curl "http://localhost:3000/rpc/get_descriptions?select=description" \ - -X POST -H "Accept: text/plain" - -.. note:: - - If more than one row would be returned the binary/plain-text/xml results will be concatenated with no delimiter. - - -.. _open-api: - -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. - -.. 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 - - COMMENT ON SCHEMA mammals IS - 'A warm-blooded vertebrate animal of a class that is distinguished by the secretion of milk by females for the nourishment of the young'; - - COMMENT ON TABLE monotremes IS - 'Freakish mammals lay the best eggs for breakfast'; - - COMMENT ON COLUMN monotremes.has_venomous_claw IS - 'Sometimes breakfast is not worth it'; - -These unsavory comments will appear in the generated JSON as the fields, ``info.description``, ``definitions.monotremes.description`` and ``definitions.monotremes.properties.has_venomous_claw.description``. - -Also if you wish to generate a ``summary`` field you can do it by having a multiple line comment, the ``summary`` will be the first line and the ``description`` the lines that follow it: - -.. code-block:: plpgsql - - COMMENT ON TABLE entities IS - $$Entities summary - - Entities description that - spans - multiple lines$$; - -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:: - - 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 a table named ``people``, OPTIONS would show: - -.. tabs:: - - .. code-tab:: http - - OPTIONS /people HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people" -X OPTIONS -i - -.. code-block:: http - - HTTP/1.1 200 OK - Allow: OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE - -For a view, the methods are determined by the presence of INSTEAD OF TRIGGERS. - -.. table:: - :widths: auto - - +--------------------+-------------------------------------------------------------------------------------------------+ - | Method allowed | View's requirements | - +====================+=================================================================================================+ - | OPTIONS, GET, HEAD | None (Always allowed) | - +--------------------+-------------------------------------------------------------------------------------------------+ - | POST | INSTEAD OF INSERT TRIGGER | - +--------------------+-------------------------------------------------------------------------------------------------+ - | PUT | INSTEAD OF INSERT TRIGGER, INSTEAD OF UPDATE TRIGGER, also requires the presence of a | - | | primary key | - +--------------------+-------------------------------------------------------------------------------------------------+ - | PATCH | INSTEAD OF UPDATE TRIGGER | - +--------------------+-------------------------------------------------------------------------------------------------+ - | DELETE | INSTEAD OF DELETE TRIGGER | - +--------------------+-------------------------------------------------------------------------------------------------+ - | All the above methods are allowed for | - | `auto-updatable views `_ | - +--------------------+-------------------------------------------------------------------------------------------------+ - -For functions, the methods depend on their volatility. ``VOLATILE`` functions allow only ``OPTIONS,POST``, whereas the rest also permit ``GET,HEAD``. - -.. important:: - - Whenever you add or remove tables or views, or modify a view's INSTEAD OF TRIGGERS on the database, you must refresh PostgREST's schema cache for OPTIONS requests to work properly. See the section :ref:`schema_reloading`. - -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 -================= - -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-schemas`. - -For GET or HEAD, the schema to be used can be selected through the ``Accept-Profile`` header: - -.. tabs:: - - .. code-tab:: http - - 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: - -.. tabs:: - - .. code-tab:: http - - 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`. - -.. note:: - - These headers are based on the nascent "Content Negotiation by Profile" spec: https://www.w3.org/TR/dx-prof-conneg - -.. _explain_plan: - -Execution plan --------------- - -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:: - - .. 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 output of the plan is generated in ``text`` format by default but you can change it to JSON by using the ``+json`` suffix. - -.. 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 - } - ] - } - } - ] - -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``. - -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 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. +PostgREST exposes three database objects of a schema as resources: tables, views and stored procedures. + +.. toctree:: + :glob: + :maxdepth: 1 + + api/tables_views.rst + api/stored_procedures.rst + api/schemas.rst + api/resource_embedding.rst + api/openapi.rst + api/resource_representation.rst + api/* diff --git a/docs/references/api/cors.rst b/docs/references/api/cors.rst new file mode 100644 index 000000000..77ad54d68 --- /dev/null +++ b/docs/references/api/cors.rst @@ -0,0 +1,34 @@ +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 diff --git a/docs/references/api/openapi.rst b/docs/references/api/openapi.rst new file mode 100644 index 000000000..31d23b52a --- /dev/null +++ b/docs/references/api/openapi.rst @@ -0,0 +1,45 @@ +.. _open-api: + +OpenAPI +======= + +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 + + COMMENT ON SCHEMA mammals IS + 'A warm-blooded vertebrate animal of a class that is distinguished by the secretion of milk by females for the nourishment of the young'; + + COMMENT ON TABLE monotremes IS + 'Freakish mammals lay the best eggs for breakfast'; + + COMMENT ON COLUMN monotremes.has_venomous_claw IS + 'Sometimes breakfast is not worth it'; + +These unsavory comments will appear in the generated JSON as the fields, ``info.description``, ``definitions.monotremes.description`` and ``definitions.monotremes.properties.has_venomous_claw.description``. + +Also if you wish to generate a ``summary`` field you can do it by having a multiple line comment, the ``summary`` will be the first line and the ``description`` the lines that follow it: + +.. code-block:: plpgsql + + COMMENT ON TABLE entities IS + $$Entities summary + + Entities description that + spans + multiple lines$$; + +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:: + + 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`. + diff --git a/docs/references/api/options.rst b/docs/references/api/options.rst new file mode 100644 index 000000000..288c3b53d --- /dev/null +++ b/docs/references/api/options.rst @@ -0,0 +1,52 @@ +.. _options_requests: + +OPTIONS method +============== + +You can verify which HTTP methods are allowed on endpoints for tables and views by using an OPTIONS request. These methods are allowed depending on what operations *can* be done on the table or view, not on the database permissions assigned to them. + +For a table named ``people``, OPTIONS would show: + +.. tabs:: + + .. code-tab:: http + + OPTIONS /people HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people" -X OPTIONS -i + +.. code-block:: http + + HTTP/1.1 200 OK + Allow: OPTIONS,GET,HEAD,POST,PUT,PATCH,DELETE + +For a view, the methods are determined by the presence of INSTEAD OF TRIGGERS. + +.. table:: + :widths: auto + + +--------------------+-------------------------------------------------------------------------------------------------+ + | Method allowed | View's requirements | + +====================+=================================================================================================+ + | OPTIONS, GET, HEAD | None (Always allowed) | + +--------------------+-------------------------------------------------------------------------------------------------+ + | POST | INSTEAD OF INSERT TRIGGER | + +--------------------+-------------------------------------------------------------------------------------------------+ + | PUT | INSTEAD OF INSERT TRIGGER, INSTEAD OF UPDATE TRIGGER, also requires the presence of a | + | | primary key | + +--------------------+-------------------------------------------------------------------------------------------------+ + | PATCH | INSTEAD OF UPDATE TRIGGER | + +--------------------+-------------------------------------------------------------------------------------------------+ + | DELETE | INSTEAD OF DELETE TRIGGER | + +--------------------+-------------------------------------------------------------------------------------------------+ + | All the above methods are allowed for | + | `auto-updatable views `_ | + +--------------------+-------------------------------------------------------------------------------------------------+ + +For functions, the methods depend on their volatility. ``VOLATILE`` functions allow only ``OPTIONS,POST``, whereas the rest also permit ``GET,HEAD``. + +.. important:: + + Whenever you add or remove tables or views, or modify a view's INSTEAD OF TRIGGERS on the database, you must refresh PostgREST's schema cache for OPTIONS requests to work properly. See the section :ref:`schema_reloading`. diff --git a/docs/references/api/resource_embedding.rst b/docs/references/api/resource_embedding.rst new file mode 100644 index 000000000..231ddd377 --- /dev/null +++ b/docs/references/api/resource_embedding.rst @@ -0,0 +1,836 @@ +.. _resource_embedding: + +Resource Embedding +================== + +PostgREST allows including related resources in a single API call. This reduces the need for many API requests. + +**Foreign Keys** determine which tables and views can be returned together. For example, consider a database of films and their awards: + +.. image:: ../../_static/film.png + +.. important:: + + Whenever foreign keys change you must do :ref:`schema_reloading` for this feature to work. + +.. _many-to-one: + +Many-to-one relationships +------------------------- + +Since ``films`` has a **foreign key** to ``directors``, this establishes a many-to-one relationship. Thus, we're able to request all the films and the director for each film. + +.. tabs:: + + .. code-tab:: http + + GET /films?select=title,directors(id,last_name) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=title,directors(id,last_name)" + +.. code-block:: json + + [ + { "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" + } + } + ] + +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:: + + .. code-tab:: http + + GET /films?select=title,director:directors(id,last_name) HTTP/1.1 + + .. code-tab:: bash Curl + + 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 **foreign key reference** establishes the inverse one-to-many relationship. In this case, ``films`` returns 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 +-------------------------- + +The join table determines many-to-many relationships. It 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`` is: + +.. 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 join table can also be detected if the composite key has additional columns + + 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) + ); + +.. tabs:: + + .. code-tab:: http + + GET /actors?select=first_name,last_name,films(title) HTTP/1.1 + + .. code-tab:: bash Curl + + 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 +------------------------ + +One-to-one relationships are detected when: + +- The foreign key has a unique constraint. + +.. code-block:: postgresql + + CREATE TABLE technical_specs( + film_id INT REFERENCES films UNIQUE, + runtime TIME, + camera TEXT, + sound TEXT + ); + +- The foreign key is a primary 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, + runtime TIME, + camera TEXT, + sound TEXT + ); + +.. tabs:: + + .. code-tab:: http + + GET /films?select=title,technical_specs(runtime) HTTP/1.1 + + .. code-tab:: bash Curl + + 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 +---------------------- + +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 foreign table premieres ( + id integer, + location text, + "date" date, + film_id integer + ) server import_csv options ( filename '/tmp/directors.csv', format 'csv'); + + 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). 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. + +.. code-block:: postgres + + create function premieres(films) returns setof premieres as $$ + select * from premieres where film_id = $1.id + $$ stable language sql; + +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 /films?select=name,premieres(name) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=name,premieres(name)" + +.. code-block:: json + + [ + { + "name": "Pulp Ficiton", + "premieres": [{"location": "Cannes Festival"}] + }, + ".." + ] + +Computed relationships also allow you to override the ones that PostgREST auto-detects. + +For example, to override the :ref:`many-to-one relationship ` between ``films`` and ``directors``. + +.. code-block:: postgres + + create function directors(films) returns setof directors rows 1 as $$ + select * from directors where id = $1.director_id + $$ stable language sql; + +Thanks to overloaded functions, you can use the same function name for different parameters. 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 their intended design enable `inlining `_. + +.. 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, it will unnest the embedding and return repeated values for the top level resource. + +.. _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: + +.. tabs:: + + .. code-tab:: http + + GET /actors?select=roles(character,films(title,year)) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/actors?select=roles(character,films(title,year))" + +.. _embed_filters: + +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: + +.. tabs:: + + .. code-tab:: http + + GET /films?select=*,actors(*)&actors.order=last_name,first_name HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=*,actors(*)&actors.order=last_name,first_name" + +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: + +.. tabs:: + + .. code-tab:: http + + GET /films?select=*,roles(*)&roles.character=in.(Chico,Harpo,Groucho) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=*,roles(*)&roles.character=in.(Chico,Harpo,Groucho)" + +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 be used for a similar operation: + +.. tabs:: + + .. code-tab:: http + + GET /films?select=*,roles(*)&roles.or=(character.eq.Gummo,character.eq.Zeppo) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=*,roles(*)&roles.or=(character.eq.Gummo,character.eq.Zeppo)" + +Limit and offset operations are possible: + +.. tabs:: + + .. code-tab:: http + + GET /films?select=*,actors(*)&actors.limit=10&actors.offset=2 HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=*,actors(*)&actors.limit=10&actors.offset=2" + +Embedded resources can be aliased and filters can be applied on these aliases: + +.. tabs:: + + .. code-tab:: 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 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=*,90_comps:competitions(name),91_comps:competitions(name)&90_comps.year=eq.1990&91_comps.year=eq.1991" + +Filters can also be applied on nested embedded resources: + +.. tabs:: + + .. code-tab:: http + + GET /films?select=*,roles(*,actors(*))&roles.actors.order=last_name&roles.actors.first_name=like.*Tom* HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=*,roles(*,actors(*))&roles.actors.order=last_name&roles.actors.first_name=like.*Tom*" + +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: + +Embedding with Top-level Filtering +---------------------------------- + +By default, :ref:`embed_filters` don't change the top-level resource(``films``) rows at all: + +.. tabs:: + + .. code-tab:: http + + 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 + + [ + { + "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``: + +.. 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 + + curl "http://localhost:3000/films?select=title,actors!inner(first_name,last_name)&actors.first_name=eq.Jehanne" + +.. code-block:: json + + [ + { + "title": "The Haunted Castle", + "actors": [ + { + "first_name": "Jehanne", + "last_name": "d'Alcy" + } + ] + } + ] + +.. _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``: + +.. tabs:: + + .. code-tab:: http + + GET /box_office?select=bo_date,gross_revenue,films(title)&gross_revenue=gte.1000000 HTTP/1.1 + + .. code-tab:: bash Curl + + 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_views: + +Embedding Views +--------------- + +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. + +For instance, the following view has ``nominations``, ``films`` and ``competitions`` as source tables: + +.. code-block:: postgres + + CREATE VIEW nominations_view AS + 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 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=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=film_title,films(language),roles(character),actors(last_name,first_name)&rank=eq.5" + +It's also possible to embed `Materialized Views `_. + +.. important:: + + - 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. + + 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`. + +.. _embedding_view_chains: + +Embedding Chains of Views +------------------------- + +Views can also depend on other views, which in turn depend on the actual source table. For PostgREST to pick up those chains recursively to any depth, all the views must be in the search path, so either in the exposed schema (:ref:`db-schemas`) or in one of the schemas set in :ref:`db-extra-search-path`. This does not apply to the source table, which could be in a private schema as well. See :ref:`schema_isolation` for more details. + +.. _s_proc_embed: + +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``). + +.. code-block:: plpgsql + + CREATE FUNCTION getallfilms() RETURNS SETOF films AS $$ + SELECT * FROM films; + $$ LANGUAGE SQL IMMUTABLE; + +A request with ``directors`` embedded: + +.. tabs:: + + .. code-tab:: http + + GET /rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers* HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers*" + +.. code-block:: json + + [ + { "title": "Workers Leaving The Lumière Factory In Lyon", + "directors": { + "id": 2, + "last_name": "Lumière" + } + } + ] + +.. _mutation_embed: + +Embedding after Insertions/Updates/Deletions +-------------------------------------------- + +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**. + +.. tabs:: + + .. code-tab:: 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" + } + + .. 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: + +.. code-block:: json + + { + "title": "127 hours", + "year": 2010, + "director": { + "first_name": "Danny", + "last_name": "Boyle" + } + } + +.. _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 unambiguously +by just specifying the tables names. + +.. _target_disamb: + +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**): + +.. tabs:: + + .. 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: + +.. 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**. + +.. tabs:: + + .. code-tab:: http + + GET /orders?select=name,billing_address(name) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/orders?select=name,billing_address(name)" + +.. code-block:: json + + [ + { + "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. + +.. tabs:: + + .. code-tab:: http + + GET /orders?select=name,billing_address:billing_address_id(name) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/orders?select=name,billing_address:billing_address_id(name)" + +.. code-block:: json + + [ + { + "name": "Personal Water Filter", + "billing_address": { + "name": "32 Glenlake Dr.Dearborn, MI 48124" + } + } + ] + +.. _hint_disamb: + +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``. + +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=*,central_addresses(*) HTTP/1.1 + + .. code-tab:: bash Curl + + 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 still specify ``central_addresses`` as the **target** and use the ``billing_address`` foreign key as the **hint**: + +.. 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 + +.. code-block:: http + + HTTP/1.1 200 OK + + [ ... ] + +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: + +.. tabs:: + + .. code-tab:: http + + 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" + +.. note:: + + If the relationship is so complex that hint disambiguation does not solve it, you can use :ref:`computed_relationships`. + diff --git a/docs/references/api/resource_representation.rst b/docs/references/api/resource_representation.rst new file mode 100644 index 000000000..82f27d96b --- /dev/null +++ b/docs/references/api/resource_representation.rst @@ -0,0 +1,150 @@ +Resource Representation +####################### + +PostgREST uses proper HTTP content negotiation (`RFC7231 `_) to deliver a resource representation. +That is to say the same API endpoint can respond in different formats like JSON or CSV depending on the request. + +.. _res_format: + +Response Format +=============== + +Use the Accept request header to specify the acceptable format (or formats) for the response: + +.. tabs:: + + .. code-tab:: http + + GET /people HTTP/1.1 + Accept: application/json + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people" \ + -H "Accept: application/json" + +For tables and views the current possibilities are: + +* ``*/*`` +* ``text/csv`` +* ``application/json`` +* ``application/openapi+json`` +* ``application/geo+json`` + +The server will default to JSON for API endpoints and OpenAPI on the root. + +.. _singular_plural: + +Singular or Plural +------------------ + +By default PostgREST returns all JSON results in an array, even when there is only one item. For example, requesting :code:`/items?id=eq.1` returns + +.. code:: json + + [ + { "id": 1 } + ] + +This can be inconvenient for client code. To return the first result as an object unenclosed by an array, specify :code:`vnd.pgrst.object` as part of the :code:`Accept` header + +.. tabs:: + + .. code-tab:: http + + GET /items?id=eq.1 HTTP/1.1 + Accept: application/vnd.pgrst.object+json + + .. code-tab:: bash Curl + + curl "http://localhost:3000/items?id=eq.1" \ + -H "Accept: application/vnd.pgrst.object+json" + +This returns + +.. code:: json + + { "id": 1 } + +When a singular response is requested but no entries are found, the server responds with an error message and 406 Not Acceptable status code rather than the usual empty array and 200 status: + +.. code-block:: json + + { + "message": "JSON object requested, multiple (or no) rows returned", + "details": "Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row", + "hint": null, + "code": "PGRST505" + } + +.. note:: + + Many APIs distinguish plural and singular resources using a special nested URL convention e.g. `/stories` vs `/stories/1`. Why do we use `/stories?id=eq.1`? The answer is because a singular resource is (for us) a row determined by a primary key, and primary keys can be compound (meaning defined across more than one column). The more familiar nested urls consider only a degenerate case of simple and overwhelmingly numeric primary keys. These so-called artificial keys are often introduced automatically by Object Relational Mapping libraries. + + Admittedly PostgREST could detect when there is an equality condition holding on all columns constituting the primary key and automatically convert to singular. However this could lead to a surprising change of format that breaks unwary client code just by filtering on an extra column. Instead we allow manually specifying singular vs plural to decouple that choice from the URL format. + +.. _scalar_return_formats: + +Scalar Function Response Format +------------------------------- + +In the special case of a :ref:`scalar_functions` there are three additional formats: + +* ``application/octet-stream`` +* ``text/plain`` +* ``text/xml`` + +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:: + + .. 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" + +Example 2: You can request XML output when having a scalar function that returns a type of ``text/xml``. You are not forced to use select for this case. + +.. code-block:: postgres + + CREATE FUNCTION generate_xml_content(..) RETURNS xml .. + +.. tabs:: + + .. code-tab:: http + + POST /rpc/generate_xml_content HTTP/1.1 + Accept: text/xml + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/generate_xml_content" \ + -X POST -H "Accept: text/xml" + +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 get_descriptions(..) RETURNS SETOF TABLE(id int, description text) .. + +.. tabs:: + + .. code-tab:: http + + POST /rpc/get_descriptions?select=description HTTP/1.1 + Accept: text/plain + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/get_descriptions?select=description" \ + -X POST -H "Accept: text/plain" + +.. note:: + + If more than one row would be returned the binary/plain-text/xml results will be concatenated with no delimiter. diff --git a/docs/references/api/schemas.rst b/docs/references/api/schemas.rst new file mode 100644 index 000000000..359f5c959 --- /dev/null +++ b/docs/references/api/schemas.rst @@ -0,0 +1,107 @@ +.. _schemas: + +Schemas +======= + +PostgREST can expose a single or multiple schema's tables, views and functions. The :ref:`active database role ` must have the usage privilege on the schemas to access them. + +Single schema +------------- + +To expose a single schema, specify a single value in :ref:`db-schemas`. + +.. code:: bash + + db-schemas = "api" + +This schema is added to the `search_path `_ of every request using :ref:`tx_settings`. + +.. _multiple-schemas: + +Multiple schemas +---------------- + +To expose multiple schemas, specify a comma-separated list on :ref:`db-schemas`: + +.. code:: bash + + db-schemas = "tenant1, tenant2" + +To switch schemas, use the ``Accept-Profile`` and ``Content-Profile`` headers. + +If you don't specify a Profile header, the first schema in the list(``tenant1`` here) is selected as the default schema. + +Only the selected schema gets added to the `search_path `_ of every request. + +.. note:: + + These headers are based on the "Content Negotiation by Profile" spec: https://www.w3.org/TR/dx-prof-conneg + +GET/HEAD +~~~~~~~~ + +For GET or HEAD, select the schema with ``Accept-Profile``. + +.. tabs:: + + .. code-tab:: http + + GET /items HTTP/1.1 + Accept-Profile: tenant2 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/items" \ + -H "Accept-Profile: tenant2" + +Other methods +~~~~~~~~~~~~~ + +For POST, PATCH, PUT and DELETE, select the schema with ``Content-Profile``. + +.. tabs:: + + .. code-tab:: http + + 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`. + +Restricted schemas +~~~~~~~~~~~~~~~~~~ + +You can only switch to a schema included in :ref:`db-schemas`. Using another schema will result in an error: + +.. tabs:: + + .. code-tab:: http + + GET /items HTTP/1.1 + Accept-Profile: tenant3 + + {...} + + .. code-tab:: bash Curl + + curl "http://localhost:3000/items" \ + -H "Accept-Profile: tenant3" + +.. code-block:: + + { + "code":"PGRST106", + "details":null, + "hint":null, + "message":"The schema must be one of the following: tenant1, tenant2" + } + diff --git a/docs/references/api/stored_procedures.rst b/docs/references/api/stored_procedures.rst new file mode 100644 index 000000000..64fa86d49 --- /dev/null +++ b/docs/references/api/stored_procedures.rst @@ -0,0 +1,456 @@ +.. _s_procs: + +Stored Procedures +================= + +*"A single resource can be the equivalent of a database stored procedure, with the power to abstract state changes over any number of storage items"* -- `Roy T. Fielding `_ + +Every stored procedure in the :ref:`exposed schema ` and accessible by the :ref:`active database role ` is executable under the :code:`/rpc` prefix. Procedures can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). + +If they return table types, Stored Procedures can: + +- Use all the same :ref:`read filters as Tables and Views ` (horizontal/vertical filtering, counts, limits, etc.). +- Use :ref:`Resource Embedding `, if the returned table type has relationships to other tables. + +.. note:: + + Why the ``/rpc`` prefix? PostgreSQL allows a table or view to have the same name as a function. The prefix allows us to avoid routes collisions. + +Calling with POST +----------------- + +To supply arguments in an API call, include a JSON object in the request payload. Each key/value of the object will become an argument. + +For instance, assume we have created this function in the database. + +.. code-block:: plpgsql + + CREATE FUNCTION add_them(a integer, b integer) + RETURNS integer AS $$ + SELECT a + b; + $$ LANGUAGE SQL IMMUTABLE; + +.. important:: + + Whenever you create or change a function you must refresh PostgREST's schema cache. See the section :ref:`schema_reloading`. + +The client can call it by posting an object like + +.. tabs:: + + .. code-tab:: http + + POST /rpc/add_them HTTP/1.1 + + { "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 + +.. note:: + + PostgreSQL converts identifier names to lowercase unless you quote them like: + + .. code-block:: postgres + + CREATE FUNCTION "someFunc"("someParam" text) ... + +Calling with GET +---------------- + +If the function doesn't modify the database, it will also run under the GET method(see :ref:`access_mode`). + +.. tabs:: + + .. code-tab:: http + + 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``. + +.. _s_proc_single_json: + +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. + +.. code-block:: plpgsql + + CREATE FUNCTION mult_them(param json) RETURNS int AS $$ + SELECT (param->>'x')::int * (param->>'y')::int + $$ LANGUAGE SQL; + +.. tabs:: + + .. code-tab:: http + + POST /rpc/mult_them HTTP/1.1 + Prefer: params=single-object + + { "x": 4, "y": 2 } + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/mult_them" \ + -X POST -H "Content-Type: application/json" \ + -H "Prefer: params=single-object" \ + -d '{ "x": 4, "y": 2 }' + +.. code-block:: json + + 8 + +.. _s_proc_single_unnamed: + +Functions with a single unnamed parameter +----------------------------------------- + +You can make a POST request to a function with a single unnamed parameter to send raw ``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. + +.. 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 XML, the parameter type must be ``xml`` and the header ``Content-Type: text/xml`` must be included in the request. + +To send raw binary, the parameter type must be ``bytea`` and the header ``Content-Type: application/octet-stream`` must be included in the request. + +.. code-block:: plpgsql + + CREATE TABLE files(blob bytea); + + CREATE FUNCTION upload_binary(bytea) RETURNS void AS $$ + INSERT INTO files(blob) VALUES ($1); + $$ LANGUAGE SQL; + +.. 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: + +Functions with array parameters +------------------------------- + +You can call a function that takes an array parameter: + +.. code-block:: postgres + + create function plus_one(arr int[]) returns int[] as $$ + SELECT array_agg(n + 1) FROM unnest($1) AS n; + $$ language sql; + +.. tabs:: + + .. code-tab:: http + + POST /rpc/plus_one HTTP/1.1 + Content-Type: application/json + + {"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 + + [2,3,4,5] + +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``). + +.. tabs:: + + .. code-tab:: http + + 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: + + .. tabs:: + + .. code-tab:: http + + POST /rpc/plus_one HTTP/1.1 + + { "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. + +.. _s_procs_variadic: + +Variadic functions +------------------ + +You can call a variadic function by passing a JSON array in a POST request: + +.. code-block:: postgres + + create function plus_one(variadic v int[]) returns int[] as $$ + SELECT array_agg(n + 1) FROM unnest($1) AS n; + $$ language sql; + +.. tabs:: + + .. code-tab:: http + + 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 + + [2,3,4,5] + +In a GET request, you can repeat the same parameter name: + +.. tabs:: + + .. code-tab:: http + + 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``: + +.. 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 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/plus_one" \ + -X POST -H "Content-Type: application/x-www-form-urlencoded" \ + -d 'v=1&v=2&v=3&v=4' + +Table-Valued functions +---------------------- + +A function that returns a table type can be filtered using the same filters as :ref:`tables and views `. They can also use :ref:`Resource Embedding `. + +.. code-block:: postgres + + CREATE FUNCTION best_films_2017() RETURNS SETOF films .. + +.. tabs:: + + .. code-tab:: http + + GET /rpc/best_films_2017?select=title,director:directors(*) HTTP/1.1 + + .. code-tab:: bash Curl + + 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" + +.. _scalar_functions: + +Scalar functions +---------------- + +PostgREST will detect if the function is scalar or table-valued and will shape the response format accordingly: + +.. tabs:: + + .. code-tab:: http + + 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 + +.. tabs:: + + .. code-tab:: http + + GET /rpc/best_films_2017 HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/best_films_2017" + +.. code-block:: json + + [ + { "title": "Okja", "rating": 7.4}, + { "title": "Call me by your name", "rating": 8}, + { "title": "Blade Runner 2049", "rating": 8.1} + ] + +To manually choose a return format such as binary, plain text or XML, see the section :ref:`scalar_return_formats`. + +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) .. + +.. tabs:: + + .. code-tab:: http + + GET /rpc/rental_duration?customer_id=232 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:: + + Overloaded functions with the same argument names but different types are not supported. + +.. _bulk_call: + +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. + +.. tabs:: + + .. code-tab:: http + + 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 + + [ 3, 7 ] + +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. diff --git a/docs/references/api/tables_views.rst b/docs/references/api/tables_views.rst new file mode 100644 index 000000000..99a919e49 --- /dev/null +++ b/docs/references/api/tables_views.rst @@ -0,0 +1,1060 @@ +.. _tables_views: + +Tables and Views +################ + +All views and tables of the :ref:`exposed schema ` and accessible by the :ref:`active database role ` are available for querying. They are exposed in one-level deep routes. + +.. _read: + +Read +==== + +For instance the full contents of a table `people` is returned at + +.. tabs:: + + .. code-tab:: http + + GET /people HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people" + +There are no deeply/nested/routes. Each route provides OPTIONS, GET, HEAD, POST, PATCH, and DELETE verbs depending entirely on database permissions. + +.. note:: + + Why not provide nested routes? Many APIs allow nesting to retrieve related information, such as :code:`/films/1/director`. We offer a more flexible mechanism (inspired by GraphQL) to embed related information. It can handle one-to-many and many-to-many relationships. This is covered in the section about :ref:`resource_embedding`. + + +.. _h_filter: + +Horizontal Filtering (Rows) +--------------------------- + +You can filter result rows by adding conditions on columns. For instance, to return people aged under 13 years old: + +.. tabs:: + + .. code-tab:: http + + GET /people?age=lt.13 HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?age=lt.13" + +You can evaluate multiple conditions on columns by adding more query string parameters. For instance, to return people who are 18 or older **and** are students: + +.. tabs:: + + .. code-tab:: http + + 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" + +.. _operators: + +Operators +~~~~~~~~~ + +These operators are available: + +============ ======================== ================================================================================== +Abbreviation In PostgreSQL Meaning +============ ======================== ================================================================================== +eq :code:`=` equals +gt :code:`>` greater than +gte :code:`>=` greater than or equal +lt :code:`<` less than +lte :code:`<=` less than or equal +neq :code:`<>` or :code:`!=` not equal +like :code:`LIKE` LIKE operator (to avoid `URL encoding `_ 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)` + – also supports commas in quoted strings like + :code:`?a=in.("hi,there","yes,you")` +is :code:`IS` checking for exact equality (null,true,false,unknown) +fts :code:`@@` :ref:`fts` using to_tsquery +plfts :code:`@@` :ref:`fts` using plainto_tsquery +phfts :code:`@@` :ref:`fts` using phraseto_tsquery +wfts :code:`@@` :ref:`fts` using websearch_to_tsquery +cs :code:`@>` contains e.g. :code:`?tags=cs.{example, new}` +cd :code:`<@` contained in e.g. :code:`?values=cd.{1,2,3}` +ov :code:`&&` overlap (have points in common), e.g. :code:`?period=ov.[2017-01-01,2017-06-30]` – + also supports array types, use curly braces instead of square brackets e.g. + :code: `?arr=ov.{1,3}` +sl :code:`<<` strictly left of, e.g. :code:`?range=sl.(1,10)` +sr :code:`>>` strictly right of +nxr :code:`&<` does not extend to the right of, e.g. :code:`?range=nxr.(1,10)` +nxl :code:`&>` does not extend to the left of +adj :code:`-|-` is adjacent to, e.g. :code:`?range=adj.(1,10)` +not :code:`NOT` negates another operator, see :ref:`logical_operators` +or :code:`OR` logical :code:`OR`, see :ref:`logical_operators` +and :code:`AND` logical :code:`AND`, see :ref:`logical_operators` +============ ======================== ================================================================================== + +For more complicated filters you will have to create a new view in the database, or use a stored procedure. For instance, here's a view to show "today's stories" including possibly older pinned stories: + +.. code-block:: postgresql + + CREATE VIEW fresh_stories AS + SELECT * + FROM stories + WHERE pinned = true + OR published > now() - interval '1 day' + ORDER BY pinned DESC, published DESC; + +The view will provide a new endpoint: + +.. tabs:: + + .. code-tab:: http + + GET /fresh_stories HTTP/1.1 + + .. code-tab:: bash Curl + + 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))" + +.. _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 +~~~~~~~~~~~~~~~~ + +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. + +.. tabs:: + + .. code-tab:: http + + GET /tsearch?my_tsv=fts(french).amusant HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/tsearch?my_tsv=fts(french).amusant" + +.. tabs:: + + .. 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 + + curl "http://localhost:3000/tsearch?my_tsv=not.wfts(french).amusant" + +Using `websearch_to_tsquery` requires PostgreSQL of version at least 11.0 and will raise an error in earlier versions of the database. + +.. _v_filter: + +Vertical Filtering (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. + +.. tabs:: + + .. code-tab:: http + + GET /people?select=first_name,age HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?select=first_name,age" + +.. code-block:: json + + [ + {"first_name": "John", "age": 30}, + {"first_name": "Jane", "age": 20} + ] + +The default is ``*``, meaning all columns. This value will become more important below in :ref:`resource_embedding`. + +Renaming Columns +~~~~~~~~~~~~~~~~ + +You can rename the columns by prefixing them with an alias followed by the colon ``:`` operator. + +.. tabs:: + + .. 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"}, + {"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. + +.. tabs:: + + .. 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"}, + {"full_name": "Jane Doe", "salary": "120000.00"} + ] + +.. _json_columns: + +JSON Columns +------------ + +You can specify a path for a ``json`` or ``jsonb`` column using the arrow operators(``->`` or ``->>``) as per the `PostgreSQL docs `__. + +.. code-block:: postgres + + CREATE TABLE people ( + id int, + json_data json + ); + +.. tabs:: + + .. code-tab:: http + + 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"}] } + ] + +.. tabs:: + + .. code-tab:: http + + 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"}, + { "id": 2, "number": "512-446-4988"} + ] + +This also works with filters: + +.. 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 + + curl "http://localhost:3000/people?select=id,json_data->blood_type&json_data->>blood_type=eq.A-" + +.. code-block:: json + + [ + { "id": 1, "blood_type": "A-" }, + { "id": 3, "blood_type": "A-" }, + { "id": 7, "blood_type": "A-" } + ] + +Note that ``->>`` is used to compare ``blood_type`` as ``text``. To compare with an integer value use ``->``: + +.. tabs:: + + .. code-tab:: http + + GET /people?select=id,json_data->age&json_data->age=gt.20 HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?select=id,json_data->age&json_data->age=gt.20" + +.. code-block:: json + + [ + { "id": 11, "age": 25 }, + { "id": 12, "age": 30 }, + { "id": 15, "age": 35 } + ] +.. _composite_array_columns: + +Composite / Array Columns +------------------------- + +The arrow operators(``->``, ``->>``) can also be used for accessing composite fields and array elements. + +.. code-block:: postgres + + CREATE TYPE coordinates ( + lat decimal(8,6), + long decimal(9,6) + ); + + CREATE TABLE countries ( + id int, + location coordinates, + languages text[] + ); + +.. 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:: + + 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 / 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: + +.. code-block:: postgres + + 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', full_name(people))); + +A full-text search on the computed column: + +.. tabs:: + + .. code-tab:: http + + 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: + +.. tabs:: + + .. code-tab:: http + + GET /people?select=*,full_name HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?select=*,full_name" + +.. 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. 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``. + + +.. _ordering: + +Ordering +-------- + +The reserved word ``order`` reorders the response rows. It uses a comma-separated list of columns and directions: + +.. tabs:: + + .. code-tab:: http + + GET /people?order=age.desc,height.asc HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?order=age.desc,height.asc" + +If no direction is specified it defaults to ascending order: + +.. tabs:: + + .. code-tab:: http + + 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``: + +.. tabs:: + + .. code-tab:: 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 + + .. 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. You can sort by nested fields of :ref:`json_columns` with the JSON operators. + +.. _limits: + +Limits and Pagination +--------------------- + +PostgREST uses HTTP range headers to describe the size of results. Every response contains the current range and, if requested, the total number of results: + +.. code-block:: http + + HTTP/1.1 200 OK + Range-Unit: items + Content-Range: 0-14/* + +Here items zero through fourteen are returned. This information is available in every response and can help you render pagination controls on the client. This is an RFC7233-compliant solution that keeps the response JSON cleaner. + +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. + +.. tabs:: + + .. 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: + +.. code-block:: http + + HTTP/1.1 200 OK + Range-Unit: items + Content-Range: 0-17/* + +You may also request open-ended ranges for an offset with no limit, e.g. :code:`Range: 10-`. + +The other way to request a limit or offset is with query parameters. For example + +.. tabs:: + + .. code-tab:: http + + 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. + +.. _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: + +.. tabs:: + + .. 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 + +.. code-block:: http + + HTTP/1.1 206 Partial Content + Range-Unit: items + Content-Range: 0-24/3573458 + +.. _planned_count: + +Planned Count +------------- + +To avoid the shortcomings of :ref:`exact count `, PostgREST can leverage PostgreSQL statistics and get a fairly accurate and fast count. +To do this, specify the ``Prefer: count=planned`` header. + +.. tabs:: + + .. code-tab:: http + + HEAD /bigtable?limit=25 HTTP/1.1 + Prefer: count=planned + + .. code-tab:: bash Curl + + curl "http://localhost:3000/bigtable?limit=25" -I \ + -H "Prefer: count=planned" + +.. code-block:: http + + HTTP/1.1 206 Partial Content + Content-Range: 0-24/3572000 + +Note that the accuracy of this count depends on how up-to-date are the PostgreSQL statistics tables. +For example in this case, to increase the accuracy of the count you can do ``ANALYZE bigtable``. +See `ANALYZE `_ for more details. + +.. _estimated_count: + +Estimated Count +--------------- + +When you are interested in the count, the relative error is important. If you have a :ref:`planned count ` of 1000000 and the exact count is +1001000, the error is small enough to be ignored. But with a planned count of 7, an exact count of 28 would be a huge misprediction. + +In general, when having smaller row-counts, the estimated count should be as close to the exact count as possible. + +To help with these cases, PostgREST can get the exact count up until a threshold and get the planned count when +that threshold is surpassed. To use this behavior, you can specify the ``Prefer: count=estimated`` header. The **threshold** is +defined by :ref:`db-max-rows`. + +Here's an example. Suppose we set ``db-max-rows=1000`` and ``smalltable`` has 321 rows, then we'll get the exact count: + +.. tabs:: + + .. code-tab:: http + + 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 + + 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: + +.. tabs:: + + .. code-tab:: http + + 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 + + HTTP/1.1 206 Partial Content + Content-Range: 0-24/3572000 + +.. _update: + +Update +====== + +To update a row or rows in a table, use the PATCH verb. Use :ref:`h_filter` to specify which record(s) to update. Here is an example query setting the :code:`category` column to child for all people below a certain age. + +.. 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`. + +.. _insert: + +Insert +====== + +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. + +.. tabs:: + + .. code-tab:: http + + POST /table_name HTTP/1.1 + + { "col1": "value1", "col2": "value2" } + + .. code-tab:: bash Curl + + curl "http://localhost:3000/table_name" \ + -X POST -H "Content-Type: application/json" \ + -d '{ "col1": "value1", "col2": "value2" }' + +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. + +URL encoded payloads can be posted with ``Content-Type: application/x-www-form-urlencoded``. + +.. tabs:: + + .. code-tab:: http + + POST /people HTTP/1.1 + Content-Type: application/x-www-form-urlencoded + + name=John+Doe&age=50&weight=80 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people" \ + -X POST -H "Content-Type: application/x-www-form-urlencoded" \ + -d "name=John+Doe&age=50&weight=80" + +.. note:: + + When inserting a row you must post a JSON object, not quoted JSON. + + .. code:: + + Yes + { "a": 1, "b": 2 } + + No + "{ \"a\": 1, \"b\": 2 }" + + Some JavaScript libraries will post the data incorrectly if you're not careful. For best results try one of the :ref:`clientside_libraries` built for PostgREST. + +.. important:: + + It's recommended that you `use triggers instead of rules `_. + Insertion on views with complex `rules `_ might not work out of the box with PostgREST due to its usage of CTEs. + If you want to keep using rules, a workaround is to wrap the view insertion in a stored procedure and call it through the :ref:`s_procs` interface. + For more details, see this `github issue `_. + +.. _bulk_insert: + +Bulk Insert +----------- + +Bulk insert works exactly like single row insert except that you provide either a JSON array of objects having uniform keys, or lines in CSV format. This not only minimizes the HTTP requests required but uses a single INSERT statement on the back-end for efficiency. + +To bulk insert CSV simply post to a table route with :code:`Content-Type: text/csv` and include the names of the columns as the first row. For instance + +.. tabs:: + + .. code-tab:: http + + POST /people HTTP/1.1 + Content-Type: text/csv + + name,age,height + J Doe,62,70 + Jonas,10,55 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people" \ + -X POST -H "Content-Type: text/csv" \ + --data-binary @- << EOF + name,age,height + J Doe,62,70 + Jonas,10,55 + EOF + +An empty field (:code:`,,`) is coerced to an empty string and the reserved word :code:`NULL` is mapped to the SQL null value. Note that there should be no spaces between the column names and commas. + +To bulk insert JSON post an array of objects having all-matching keys + +.. tabs:: + + .. code-tab:: http + + POST /people HTTP/1.1 + Content-Type: application/json + + [ + { "name": "J Doe", "age": 62, "height": 70 }, + { "name": "Janus", "age": 10, "height": 55 } + ] + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people" \ + -X POST -H "Content-Type: application/json" \ + -d @- << EOF + [ + { "name": "J Doe", "age": 62, "height": 70 }, + { "name": "Janus", "age": 10, "height": 55 } + ] + EOF + +.. _specify_columns: + +Specifying Columns +------------------ + +By using the :code:`columns` query parameter it's possible to specify the payload keys that will be inserted and ignore the rest of the payload. + +.. tabs:: + + .. code-tab:: 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": "..." + } + + .. code-tab:: bash Curl + + curl "http://localhost:3000/datasets?columns=source,publication_date,figure" \ + -X POST -H "Content-Type: application/json" \ + -d @- << EOF + { + "source": "Natural Disaster Prevention and Control", + "publication_date": "2015-09-11", + "figure": 1100, + "location": "...", + "comment": "...", + "extra": "...", + "stuff": "..." + } + EOF + +In this case, only **source**, **publication_date** and **figure** will be inserted. The rest of the JSON keys will be ignored. + +Using this also has the side-effect of being more efficient for :ref:`bulk_insert` since PostgREST will not process the JSON and +it'll send it directly to PostgreSQL. + +.. _upsert: + +Upsert +====== + +You can make an upsert with :code:`POST` and the :code:`Prefer: resolution=merge-duplicates` header: + +.. tabs:: + + .. code-tab:: 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 } + ] + + .. code-tab:: bash Curl + + curl "http://localhost:3000/employees" \ + -X POST -H "Content-Type: application/json" \ + -H "Prefer: resolution=merge-duplicates" \ + -d @- << EOF + [ + { "id": 1, "name": "Old employee 1", "salary": 30000 }, + { "id": 2, "name": "Old employee 2", "salary": 42000 }, + { "id": 3, "name": "New employee 3", "salary": 50000 } + ] + EOF + +By default, upsert operates based on the primary key columns, you must specify all of them. You can also choose to ignore the duplicates with :code:`Prefer: resolution=ignore-duplicates`. This works best when the primary key is natural, but it's also possible to use it if the primary key is surrogate (example: "id serial primary key"). For more details read `this issue `_. + +.. 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. + +.. tabs:: + + .. code-tab:: 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 } + ] + + .. 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 + +.. _upsert_put: + +PUT +--- + +A single row upsert can be done by using :code:`PUT` and filtering the primary key columns with :code:`eq`: + +.. tabs:: + + .. code-tab:: http + + PUT /employees?id=eq.4 HTTP/1.1 + + { "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. + +.. _delete: + +Delete +====== + +To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instance deleting inactive users: + +.. tabs:: + + .. code-tab:: http + + DELETE /user?active=is.false HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/user?active=is.false" -X DELETE + +Deletions also support :code:`Prefer: return=representation` plus :ref:`v_filter`. + +.. tabs:: + + .. code-tab:: http + + DELETE /user?id=eq.1 HTTP/1.1 + Prefer: return=representation + + .. code-tab:: bash Curl + + curl "http://localhost:3000/user?id=eq.1" -X DELETE \ + -H "Prefer: return=representation" + +.. code-block:: json + + {"id": 1, "email": "johndoe@email.com"} + +.. warning:: + + Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`. + +.. _limited_update_delete: + +Limited Update/Delete +===================== + +You can limit the amount of affected rows by :ref:`update` or :ref:`delete` with the ``limit`` query parameter. For this, you must add an explicit ``order`` on a unique column(s). + +.. 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 `_. + diff --git a/docs/references/api/url_grammar.rst b/docs/references/api/url_grammar.rst new file mode 100644 index 000000000..c562f0a75 --- /dev/null +++ b/docs/references/api/url_grammar.rst @@ -0,0 +1,108 @@ +.. note:: + + This page is a work in progress. + +URL Grammar +=========== + +.. _custom_queries: + +Custom Queries +-------------- + +The PostgREST URL grammar limits the kinds of queries clients can perform. It prevents arbitrary, potentially poorly constructed and slow client queries. It's good for quality of service, but means database administrators must create custom views and stored procedures to provide richer endpoints. The most common causes for custom endpoints are + +* Table unions +* More complicated joins than those provided by :ref:`resource_embedding`. +* Geo-spatial queries that require an argument, like "points near (lat,lon)" + +Unicode support +--------------- + +PostgREST supports unicode in schemas, tables, columns and values. To access a table with unicode name, use percent encoding. + +To request this: + +.. code-block:: http + + GET /موارد HTTP/1.1 + +Do this: + +.. tabs:: + + .. code-tab:: http + + 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: + +Table / Columns with spaces +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can request table/columns with spaces in them by percent encoding the spaces with ``%20``: + +.. tabs:: + + .. code-tab:: http + + 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: + +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. + +.. tabs:: + + .. code-tab:: http + + 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. + +.. tabs:: + + .. code-tab:: http + + GET /vulnerabilities?%22information.cpe%22=like.*MS* HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/vulnerabilities?%22information.cpe%22=like.*MS*" + +If the value filtered by the ``in`` operator has a double quote (``"``), you can escape it using a backslash ``"\""``. A backslash itself can be used with a double backslash ``"\\"``. + +Here ``Quote:"`` and ``Backslash:\`` are percent-encoded values. Note that ``%5C`` is the percent-encoded backslash. + +.. tabs:: + + .. code-tab:: http + + 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:: + + 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`. + diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index d27eb0bcb..fdf922b3c 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -398,30 +398,7 @@ db-schemas **In-Database** pgrst.db_schemas =============== ================= - The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints. - - .. code:: bash - - db-schemas = "api" - - 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-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`. + The list of database schemas to expose to clients. See :ref:`schemas`. .. _db-tx-end: diff --git a/docs/references/connection_pool.rst b/docs/references/connection_pool.rst index 38dabeee1..d0dea7939 100644 --- a/docs/references/connection_pool.rst +++ b/docs/references/connection_pool.rst @@ -67,7 +67,7 @@ If the pool loses the connection to the database, it will retry reconnecting usi The retries happen immediately after a connection loss, if :ref:`db-channel-enabled` is set to true(the default). Otherwise they'll happen once a request arrives. -The server reloads the :ref:`schema_cache` when recoverying. +The server reloads the :ref:`schema_cache` when recovering. To notify the client of the next retry, the server sends a ``503 Service Unavailable`` status with the ``Retry-After: x`` header. Where ``x`` is the number of seconds programmed for the next retry. diff --git a/postgrest.dict b/postgrest.dict index 6e1e1dd4c..672343183 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -154,6 +154,7 @@ RSA Saleeba savepoint schemas +schema's Sencha Serverless Severin From c5d6526d996ec4c0723704d9cfd1d90b7f8fcb9c Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 13:51:51 -0300 Subject: [PATCH 568/652] move pg-safeupdate to own integration --- docs/integrations/nginx.rst | 31 --------------------- docs/integrations/pg-safeupdate.rst | 42 +++++++++++++++++++++++++++++ postgrest.dict | 1 + 3 files changed, 43 insertions(+), 31 deletions(-) create mode 100644 docs/integrations/pg-safeupdate.rst diff --git a/docs/integrations/nginx.rst b/docs/integrations/nginx.rst index 25ce6ca98..2dc7755b8 100644 --- a/docs/integrations/nginx.rst +++ b/docs/integrations/nginx.rst @@ -35,37 +35,6 @@ The first step is to create an Nginx configuration file that proxies requests to 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 ---------------------------- - -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: - -.. tabs:: - - .. code-tab:: http - - 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! - -.. tabs:: - - .. code-tab:: http - - 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: - .. code-block:: bash sudo -E pgxn install safeupdate diff --git a/docs/integrations/pg-safeupdate.rst b/docs/integrations/pg-safeupdate.rst new file mode 100644 index 000000000..c5f0928cd --- /dev/null +++ b/docs/integrations/pg-safeupdate.rst @@ -0,0 +1,42 @@ +pg-safeupdate +############# + +.. _block_fulltable: + +Block Full-Table Operations +--------------------------- + +If the :ref:`active role ` can delete table rows then the DELETE verb is allowed for clients. Here's an API request to delete old rows from a hypothetical logs table: + +.. tabs:: + + .. code-tab:: http + + 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 + +Note that it's very easy to delete the **entire table** by omitting the query parameter! + +.. tabs:: + + .. code-tab:: http + + 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: + +.. 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/postgrest.dict b/postgrest.dict index 672343183..2ae87ffac 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -152,6 +152,7 @@ RLS RPC RSA Saleeba +safeupdate savepoint schemas schema's From d593bcff802e13e14837d4313ec71e62040e8a9c Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 10 May 2023 15:29:11 -0500 Subject: [PATCH 569/652] Fix remaining info about pg-safeupdate in nginx docs --- docs/integrations/nginx.rst | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/integrations/nginx.rst b/docs/integrations/nginx.rst index 2dc7755b8..449716bbc 100644 --- a/docs/integrations/nginx.rst +++ b/docs/integrations/nginx.rst @@ -35,15 +35,6 @@ The first step is to create an Nginx configuration file that proxies requests to 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`. -.. 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. - .. _https: HTTPS From dec1e03a0e66c4edfe61c6580bd01a51d91c049c Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 18:57:19 -0300 Subject: [PATCH 570/652] add v11 release notes with docs improvs --- docs/conf.py | 4 +- docs/{integrations => explanations}/nginx.rst | 2 + docs/index.rst | 4 ++ docs/references/auth.rst | 2 + docs/references/connection_pool.rst | 2 + docs/references/transactions.rst | 2 + docs/releases/v11.0.0.rst | 72 +++++++++++++++++++ postgrest.dict | 1 + 8 files changed, 87 insertions(+), 2 deletions(-) rename docs/{integrations => explanations}/nginx.rst (99%) create mode 100644 docs/releases/v11.0.0.rst diff --git a/docs/conf.py b/docs/conf.py index f0390613d..2e48d664b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,9 +57,9 @@ copyright = u'2017, ' + author # built documents. # # The short X.Y version. -version = u'9.0' +version = u'11.0' # The full version, including alpha/beta/rc tags. -release = u'9.0.0' +release = u'11.0.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/integrations/nginx.rst b/docs/explanations/nginx.rst similarity index 99% rename from docs/integrations/nginx.rst rename to docs/explanations/nginx.rst index 449716bbc..ccbdebc47 100644 --- a/docs/integrations/nginx.rst +++ b/docs/explanations/nginx.rst @@ -1,3 +1,5 @@ +.. _nginx: + Nginx ===== diff --git a/docs/index.rst b/docs/index.rst index 09529b4b5..34890d57d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -158,8 +158,12 @@ Recipes that'll help you address specific use-cases. :name: how-tos :maxdepth: 1 + how-tos/sql-user-* + how-tos/working-* how-tos/* +.. _intgrs: + Integrations ------------ diff --git a/docs/references/auth.rst b/docs/references/auth.rst index 2501c3536..4eb8b93a1 100644 --- a/docs/references/auth.rst +++ b/docs/references/auth.rst @@ -1,3 +1,5 @@ +.. _authn: + Authentication ============== diff --git a/docs/references/connection_pool.rst b/docs/references/connection_pool.rst index d0dea7939..48683b90a 100644 --- a/docs/references/connection_pool.rst +++ b/docs/references/connection_pool.rst @@ -1,3 +1,5 @@ +.. _connection_pool: + Connection Pool =============== diff --git a/docs/references/transactions.rst b/docs/references/transactions.rst index 565a2e6d0..45a17b1cc 100644 --- a/docs/references/transactions.rst +++ b/docs/references/transactions.rst @@ -1,3 +1,5 @@ +.. _transactions: + Transactions ============ diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst new file mode 100644 index 000000000..2db22b082 --- /dev/null +++ b/docs/releases/v11.0.0.rst @@ -0,0 +1,72 @@ +11.0.0 +====== + +Features +-------- + +Documentation improvements +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- New :ref:`transactions` reference. +- New :ref:`connection_pool` reference. +- Split :ref:`api` into other sub-references. +- Split :ref:`authn` into :ref:`db_authz` and :ref:`sql_user_management`. +- Split :ref:`admin` into :ref:`intgrs` and :ref:`nginx`. + +Breaking changes +---------------- + +Bug fixes +--------- + +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 + +* `Roboflow `_ +* Evans Fernandes +* Jan Sommer +* `Franz Gusenbauer `_ +* Zac Miller +* 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 2ae87ffac..5a3c355bd 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -213,4 +213,5 @@ websearch Websockets webuser wfts +Zac ZeroMQ From c56108b8a9a4fb407adf8a08a06dfc87f86350fc Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 19:59:33 -0300 Subject: [PATCH 571/652] add breaking changes --- docs/references/api/stored_procedures.rst | 39 ----------------------- docs/releases/v11.0.0.rst | 9 ++++-- 2 files changed, 7 insertions(+), 41 deletions(-) diff --git a/docs/references/api/stored_procedures.rst b/docs/references/api/stored_procedures.rst index 64fa86d49..3bb971ab3 100644 --- a/docs/references/api/stored_procedures.rst +++ b/docs/references/api/stored_procedures.rst @@ -415,42 +415,3 @@ You can call overloaded functions with different number of arguments. .. important:: Overloaded functions with the same argument names but different types are not supported. - -.. _bulk_call: - -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. - -.. tabs:: - - .. code-tab:: http - - 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 - - [ 3, 7 ] - -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. diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index 2db22b082..83a3a5a94 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -13,11 +13,16 @@ Documentation improvements - Split :ref:`authn` into :ref:`db_authz` and :ref:`sql_user_management`. - Split :ref:`admin` into :ref:`intgrs` and :ref:`nginx`. +Bug fixes +--------- + Breaking changes ---------------- -Bug fixes ---------- +- Removed Bulk Call with ``Prefer: params=multiple-objects`` on Stored Procedures. +- To comply with `RFC 9110 `_, the Range header is now only considered on GET. + + Other methods will ignore it and instead should use the ``limit/offset``. + + PUT requests no longer return an error when this header is present (using limit/offset still triggers the error) Thanks ------ From 0bd43663fcf2d0e8c6b31dbb40b6f2ecc63f6c1e Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 20:06:06 -0300 Subject: [PATCH 572/652] add isdistinct --- docs/references/api/tables_views.rst | 1 + docs/releases/v11.0.0.rst | 5 +++++ postgrest.dict | 1 + 3 files changed, 7 insertions(+) diff --git a/docs/references/api/tables_views.rst b/docs/references/api/tables_views.rst index 99a919e49..aec5d9dcb 100644 --- a/docs/references/api/tables_views.rst +++ b/docs/references/api/tables_views.rst @@ -82,6 +82,7 @@ in :code:`IN` one of a list of values, e.g. :code:`?a= – also supports commas in quoted strings like :code:`?a=in.("hi,there","yes,you")` is :code:`IS` checking for exact equality (null,true,false,unknown) +isdistinct :code:`IS DISTINCT FROM` not equal, treating :code:`NULL` as a comparable value fts :code:`@@` :ref:`fts` using to_tsquery plfts :code:`@@` :ref:`fts` using plainto_tsquery phfts :code:`@@` :ref:`fts` using phraseto_tsquery diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index 83a3a5a94..3372bf58c 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -4,6 +4,11 @@ Features -------- +Horizontal Filtering +~~~~~~~~~~~~~~~~~~~~ + +- New ``isdistinct`` :ref:`operator `. It is a direct translation of `IS DINTINCT FROM `_. + Documentation improvements ~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/postgrest.dict b/postgrest.dict index 5a3c355bd..6803c01be 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -67,6 +67,7 @@ ilike imatch io IP +isdistinct JS js JSON From cb7d70c197e420cab365f4220294fe47f3c72ceb Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 20:17:46 -0300 Subject: [PATCH 573/652] add trace header --- docs/references/admin.rst | 28 ++++++++++++++++++++++++++++ docs/references/configuration.rst | 13 +++++++++++++ docs/releases/v11.0.0.rst | 5 +++++ 3 files changed, 46 insertions(+) diff --git a/docs/references/admin.rst b/docs/references/admin.rst index ed83e8c9d..2a8f01b7a 100644 --- a/docs/references/admin.rst +++ b/docs/references/admin.rst @@ -89,6 +89,34 @@ When debugging a problem it's important to verify the PostgREST version. Look fo Server: postgrest/11.0.1 +.. _trace_header: + +Trace Header +------------ + +You can enable tracing HTTP requests by setting :ref:`server-trace-header`. Specify the set header in the request, and the server will include it in the response. + +.. code:: bash + + server-trace-header = "X-Request-Id" + +.. tabs:: + + .. code-tab:: http + + GET /users + X-Request-Id: 123 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/users" \ + -H "X-Request-Id: 123" + +.. code:: + + HTTP/1.1 200 OK + X-Request-Id: 123 + .. _explain_plan: Execution plan diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index fdf922b3c..64906661b 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -174,6 +174,7 @@ openapi-server-proxy-uri String Y raw-media-types String Y server-host String !4 server-port Int 3000 +server-trace-header String Y server-unix-socket String server-unix-socket-mode String 660 =========================== ======= ================= ========== @@ -670,6 +671,18 @@ server-port The TCP port to bind the web server. +.. _server-trace-header: + +server-trace-header +------------------- + + =============== ================= + **Environment** PGRST_SERVER_TRACE_HEADER + **In-Database** `pgrst.server_trace_header` + =============== ================= + + The header name used to trace HTTP requests. See :ref:`trace_header`. + .. _server-unix-socket: server-unix-socket diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index 3372bf58c..da5aa1d3e 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -9,6 +9,11 @@ Horizontal Filtering - New ``isdistinct`` :ref:`operator `. It is a direct translation of `IS DINTINCT FROM `_. +Admin +~~~~~ + +- New :ref:`trace_header`. + Documentation improvements ~~~~~~~~~~~~~~~~~~~~~~~~~~ From 1de06f5332e9f73e62b4d3e56368243d5e1fcb94 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 20:40:46 -0300 Subject: [PATCH 574/652] add openapi overriding --- docs/references/api/openapi.rst | 50 +++++++++++++++++++++++++++++++ docs/references/configuration.rst | 12 ++++++++ docs/releases/v11.0.0.rst | 5 ++++ 3 files changed, 67 insertions(+) diff --git a/docs/references/api/openapi.rst b/docs/references/api/openapi.rst index 31d23b52a..f2ba5d476 100644 --- a/docs/references/api/openapi.rst +++ b/docs/references/api/openapi.rst @@ -43,3 +43,53 @@ 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`. +.. _override_openapi: + +Overriding OpenAPI response +--------------------------- + +You can override the default response with a function result. To do this, set the function on :ref:`db-root-spec`. + +.. code:: bash + + db-root-spec = "root" + +.. code:: postgres + + create or replace function root() returns json as $_$ + declare + openapi json = $$ + { + "swagger": "2.0", + "info":{ + "title":"Overridden", + "description":"This is a my own API" + } + } + $$; + begin + return openapi; + end + $_$ language plpgsql; + +.. tabs:: + + .. code-tab:: http + + GET / HTTP/1.1 + + .. code-tab:: bash Curl + + curl http://localhost:3000 + +.. code-block:: http + + HTTP/1.1 200 OK + + { + "swagger": "2.0", + "info":{ + "title":"Overridden", + "description":"This is a my own API" + } + } diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index 64906661b..557972a15 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -387,6 +387,18 @@ db-prepared-statements 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-root-spec: + +db-root-spec +------------ + + =============== ================= + **Environment** PGRST_DB_ROOT_SPEC + **In-Database** pgrst.db_root_spec + =============== ================= + + Function to override the OpenAPI response. See :ref:`override_openapi`. + .. _db-schemas: db-schemas diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index da5aa1d3e..7e67adb7e 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -9,6 +9,11 @@ Horizontal Filtering - New ``isdistinct`` :ref:`operator `. It is a direct translation of `IS DINTINCT FROM `_. +OpenAPI +~~~~~~~ + +- Allow :ref:`override_openapi`. + Admin ~~~~~ From e813375c7da5b72cbbd6d16bb69c89909e2ccac6 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 20:47:09 -0300 Subject: [PATCH 575/652] openapi title override --- docs/references/api/openapi.rst | 17 +++++++++++++---- docs/releases/v11.0.0.rst | 1 + 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/references/api/openapi.rst b/docs/references/api/openapi.rst index f2ba5d476..d8d3ce34b 100644 --- a/docs/references/api/openapi.rst +++ b/docs/references/api/openapi.rst @@ -35,20 +35,29 @@ Also if you wish to generate a ``summary`` field you can do it by having a multi spans multiple lines$$; +Similarly, you can override the API title by commenting the schema. + +.. code-block:: plpgsql + + COMMENT ON SCHEMA api IS + $$FooBar API + + A RESTful API that serves FooBar data.$$; + If you need to include the ``security`` and ``securityDefinitions`` options, set the :ref:`openapi-security-active` configuration to ``true``. You can use a tool like `Swagger UI `_ to create beautiful documentation from the description and to host an interactive web-based dashboard. The dashboard allows developers to make requests against a live PostgREST server, and provides guidance with request headers and example request bodies. .. important:: - The OpenAPI information can go out of date as the schema changes under a running server. 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. See :ref:`schema_reloading`. .. _override_openapi: -Overriding OpenAPI response ---------------------------- +Overriding Full OpenAPI Response +-------------------------------- -You can override the default response with a function result. To do this, set the function on :ref:`db-root-spec`. +You can override the whole default response with a function result. To do this, set the function on :ref:`db-root-spec`. .. code:: bash diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index 7e67adb7e..8ef67c98d 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -12,6 +12,7 @@ Horizontal Filtering OpenAPI ~~~~~~~ +- Allow :ref:`overriding the OpenAPI title `. - Allow :ref:`override_openapi`. Admin From 7832ddba52426cad026ab2a70d4c39cde5df7a51 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 21:14:50 -0300 Subject: [PATCH 576/652] add any/all modifiers --- docs/references/api/tables_views.rst | 33 ++++++++++++++++++++++++++++ docs/releases/v11.0.0.rst | 1 + 2 files changed, 34 insertions(+) diff --git a/docs/references/api/tables_views.rst b/docs/references/api/tables_views.rst index aec5d9dcb..837dd8232 100644 --- a/docs/references/api/tables_views.rst +++ b/docs/references/api/tables_views.rst @@ -100,6 +100,8 @@ adj :code:`-|-` is adjacent to, e.g. :code:`?range=adj.( not :code:`NOT` negates another operator, see :ref:`logical_operators` or :code:`OR` logical :code:`OR`, see :ref:`logical_operators` and :code:`AND` logical :code:`AND`, see :ref:`logical_operators` +all :code:`ALL` comparison matches all the values in the list, see :ref:`logical_operators` +any :code:`ANY` comparison matches any value in the list, see :ref:`logical_operators` ============ ======================== ================================================================================== 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: @@ -156,6 +158,37 @@ 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))" +.. _modifiers: + +Operator Modifiers +~~~~~~~~~~~~~~~~~~ + +You may further simplify the logic using the ``any/all`` modifiers of ``eq,like,ilike,gt,gte,lt,lte,match,imatch``. + +For instance, to avoid repeating the same column for ``or``, use ``any`` to get people with last names that start with O or P: + +.. tabs:: + + .. code-tab:: http + + GET /people?last_name=like(any).{O*,P*} HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?last_name=like(any).{O*,P*}" + +In a similar way, you can use ``all`` to avoid repeating the same column for ``and``. To get the people with last names that start with O and end with n: + +.. tabs:: + + .. code-tab:: http + + GET /people?last_name=like(all).{O*,*n} HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?last_name=like(all).{O*,*n}" + .. _pattern_matching: Pattern Matching diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index 8ef67c98d..c7cf789ea 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -8,6 +8,7 @@ Horizontal Filtering ~~~~~~~~~~~~~~~~~~~~ - New ``isdistinct`` :ref:`operator `. It is a direct translation of `IS DINTINCT FROM `_. +- New ``and/all`` :ref:`modifiers`. OpenAPI ~~~~~~~ From 8ae6b012f4abba8d7643b2b1c8e311885d01c31b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 21:23:34 -0300 Subject: [PATCH 577/652] add empty embed filter --- docs/references/api/resource_embedding.rst | 28 ++++++++++++++++++++-- docs/releases/v11.0.0.rst | 6 +++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/references/api/resource_embedding.rst b/docs/references/api/resource_embedding.rst index 231ddd377..e410ba447 100644 --- a/docs/references/api/resource_embedding.rst +++ b/docs/references/api/resource_embedding.rst @@ -418,8 +418,8 @@ The result will show the nested actors named Tom and order them by last name. Al .. _embedding_top_level_filter: -Embedding with Top-level Filtering ----------------------------------- +Top-level Filtering +------------------- By default, :ref:`embed_filters` don't change the top-level resource(``films``) rows at all: @@ -481,6 +481,30 @@ In order to filter the top level rows you need to add ``!inner`` to the embedded } ] +.. _empty_embed_filter: + +Empty Embed Filter +~~~~~~~~~~~~~~~~~~ + +If you want to filter the films by actors but don't want to include them in the response, empty the embedded columns. + +.. tabs:: + + .. code-tab:: http + + GET /films?select=title,actors!inner()&actors.first_name=eq.Jehanne HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=title,actors!inner()&actors.first_name=eq.Jehanne" + +.. code-block:: json + [ + { + "title": "The Haunted Castle", + } + ] + .. _embedding_partitioned_tables: Embedding Partitioned Tables diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index c7cf789ea..a346e1797 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -10,6 +10,11 @@ Horizontal Filtering - New ``isdistinct`` :ref:`operator `. It is a direct translation of `IS DINTINCT FROM `_. - New ``and/all`` :ref:`modifiers`. +Resource Embedding +~~~~~~~~~~~~~~~~~~ + +- New :ref:`empty_embed_filter`. + OpenAPI ~~~~~~~ @@ -38,6 +43,7 @@ Breaking changes - Removed Bulk Call with ``Prefer: params=multiple-objects`` on Stored Procedures. - To comply with `RFC 9110 `_, the Range header is now only considered on GET. + + Other methods will ignore it and instead should use the ``limit/offset``. + PUT requests no longer return an error when this header is present (using limit/offset still triggers the error) From d15dc680aff2b4b94d29b8818b56cb71e10e1e1b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 22:28:58 -0300 Subject: [PATCH 578/652] add top-level ordering --- docs/references/api/resource_embedding.rst | 19 +++++++++++++++++++ docs/releases/v11.0.0.rst | 1 + 2 files changed, 20 insertions(+) diff --git a/docs/references/api/resource_embedding.rst b/docs/references/api/resource_embedding.rst index e410ba447..ed44c47cb 100644 --- a/docs/references/api/resource_embedding.rst +++ b/docs/references/api/resource_embedding.rst @@ -505,6 +505,25 @@ If you want to filter the films by actors but don't want to include them in the } ] +.. _top_level_order: + +Top-level Ordering +------------------ + +On :ref:`Many-to-One ` and :ref:`One-to-One ` relationships, you can use a column of the "to-one" end to sort the top-level. + +For example, to arrange the films in descending order using the director's last name. + +.. tabs:: + + .. code-tab:: http + + GET /films?select=title,directors(last_name)&order=directors(last_name).desc HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=title,directors(last_name)&order=directors(last_name).desc" + .. _embedding_partitioned_tables: Embedding Partitioned Tables diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index a346e1797..5223369e1 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -14,6 +14,7 @@ Resource Embedding ~~~~~~~~~~~~~~~~~~ - New :ref:`empty_embed_filter`. +- New :ref:`top_level_order`. OpenAPI ~~~~~~~ From 9595ed845e4067622388dbf9bf311805bec5dae3 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 22:50:22 -0300 Subject: [PATCH 579/652] add null filtering embeds --- docs/references/admin.rst | 3 +- docs/references/api/resource_embedding.rst | 59 +++++++++++++++++++--- docs/releases/v11.0.0.rst | 3 +- 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/docs/references/admin.rst b/docs/references/admin.rst index 2a8f01b7a..6e00eb3f9 100644 --- a/docs/references/admin.rst +++ b/docs/references/admin.rst @@ -104,7 +104,8 @@ You can enable tracing HTTP requests by setting :ref:`server-trace-header`. Spec .. code-tab:: http - GET /users + GET /users HTTP/1.1 + X-Request-Id: 123 .. code-tab:: bash Curl diff --git a/docs/references/api/resource_embedding.rst b/docs/references/api/resource_embedding.rst index ed44c47cb..456e03dbf 100644 --- a/docs/references/api/resource_embedding.rst +++ b/docs/references/api/resource_embedding.rst @@ -481,24 +481,71 @@ In order to filter the top level rows you need to add ``!inner`` to the embedded } ] -.. _empty_embed_filter: +.. _null_embed: -Empty Embed Filter -~~~~~~~~~~~~~~~~~~ +Null filtering on Embedded Resources +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -If you want to filter the films by actors but don't want to include them in the response, empty the embedded columns. +Null filtering on the embedded resources can behave the same as ``!inner``. While providing more flexibility. + +For example, doing ``actors=not.is.null`` returns the same result as ``actors!inner(*)``: .. tabs:: .. code-tab:: http - GET /films?select=title,actors!inner()&actors.first_name=eq.Jehanne HTTP/1.1 + GET /films?select=title,actors(*)&actors=not.is.null HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/films?select=title,actors!inner()&actors.first_name=eq.Jehanne" + curl "http://localhost:3000/films?select=title,actors(*)&actors=not.is.null" + +The ``is.null`` filter can be used in embedded resources to perform an anti-join. To get all the films that do not have any nominations: + +.. tabs:: + + .. code-tab:: http + + GET /films?select=title,nominations()&nominations=is.null HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=title,nominations()&nominations=is.null" + + +Both ``is.null`` and ``not.is.null`` can be included inside the `or` operator. For instance, to get the films that have no actors **or** directors registered yet: + +.. tabs:: + + .. code-tab:: http + + GET /films?select=title,actors(*),directors(*)&or=(actors.is.null,directors.is.null) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=title,actors(*),directors(*)&or=(actors.is.null,directors.is.null)" + +.. _empty_embed: + +Empty Embed +~~~~~~~~~~~ + +You can leave an embedded resource empty, this helps with filtering in some cases. + +To filter the films by actors but not include them: + +.. tabs:: + + .. code-tab:: http + + GET /films?select=title,actors()&actors.first_name=eq.Jehanne&actors=not.is.null HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=title,actors()&actors.first_name=eq.Jehanne&actors=not.is.null" .. code-block:: json + [ { "title": "The Haunted Castle", diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index 5223369e1..7eba9688a 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -13,8 +13,9 @@ Horizontal Filtering Resource Embedding ~~~~~~~~~~~~~~~~~~ -- New :ref:`empty_embed_filter`. - New :ref:`top_level_order`. +- New :ref:`null_embed`. +- New :ref:`empty_embed`. OpenAPI ~~~~~~~ From f02be3fb769c5f0410df96fc30b8f9ecf4ec1192 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 23:11:45 -0300 Subject: [PATCH 580/652] add spread embed --- docs/references/api/resource_embedding.rst | 59 ++++++++++++++++++++++ docs/releases/v11.0.0.rst | 1 + 2 files changed, 60 insertions(+) diff --git a/docs/references/api/resource_embedding.rst b/docs/references/api/resource_embedding.rst index 456e03dbf..54de4d460 100644 --- a/docs/references/api/resource_embedding.rst +++ b/docs/references/api/resource_embedding.rst @@ -571,6 +571,65 @@ For example, to arrange the films in descending order using the director's last curl "http://localhost:3000/films?select=title,directors(last_name)&order=directors(last_name).desc" +.. _spread_embed: + +Spread embedded resource +------------------------ + +On many-to-one and one-to-one relationships, you can "spread" the embedded resource. That is, remove the surrounding JSON object for the embedded resource columns. + +.. tabs:: + + .. code-tab:: http + + GET /films?select=title,...directors(director_last_name:last_name)&title=like.*Workers* HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=title,...directors(director_last_name:last_name)&title=like.*Workers*" + +.. code-block:: json + + [ + { + "title": "Workers Leaving The Lumière Factory In Lyon", + "director_last_name": "Lumière" + } + ] + +Note that there is no ``"directors"`` object. Also the embed columns can be aliased normally. + +You can use this to get the columns of a join table in a many-to-many relationship. For instance, to get films and its actors, but including the ``character`` column from the roles table: + +.. tabs:: + + .. code-tab:: http + + GET /films?select=title,actors:roles(character,...actors(first_name,last_name))&title=like.*Lighthouse* HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/films?select=title,actors:roles(character,...actors(first_name,last_name))&title=like.*Lighthouse*" + +.. code-block:: json + + [ + { + "title": "The Lighthouse", + "actors": [ + { + "character": "Thomas Wake", + "first_name": "Willem", + "last_name": "Dafoe" + } + ] + } + ] + +.. note:: + + The spread operator ``...`` is borrowed from the Javascript `spread syntax `_. + .. _embedding_partitioned_tables: Embedding Partitioned Tables diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index 7eba9688a..a2de2b3b9 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -14,6 +14,7 @@ Resource Embedding ~~~~~~~~~~~~~~~~~~ - New :ref:`top_level_order`. +- New :ref:`spread_embed`. - New :ref:`null_embed`. - New :ref:`empty_embed`. From 84b01dea993e28fc71e6881e02a12cde507742a3 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 10 May 2023 23:44:09 -0300 Subject: [PATCH 581/652] add recursive m2m disamb --- diagrams/users.er | 13 +++++++ docs/_static/users.png | Bin 0 -> 14073 bytes docs/references/api/resource_embedding.rst | 38 +++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 diagrams/users.er create mode 100644 docs/_static/users.png diff --git a/diagrams/users.er b/diagrams/users.er new file mode 100644 index 000000000..e42ca312f --- /dev/null +++ b/diagrams/users.er @@ -0,0 +1,13 @@ +[Users] +*id +first_name +last_name +username + +[Subscriptions] +*+subscriber_id +*+subscribed_id +type + +Users 1--* Subscriptions +Subscriptions *--1 Users diff --git a/docs/_static/users.png b/docs/_static/users.png new file mode 100644 index 0000000000000000000000000000000000000000..f8b57fee753d46a31e6d918c868c8775fc8f97fb GIT binary patch literal 14073 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 Date: Thu, 11 May 2023 00:43:35 -0300 Subject: [PATCH 582/652] add bulk insert defaults --- docs/references/api/tables_views.rst | 50 ++++++++++++++++++++++++++++ docs/releases/v11.0.0.rst | 13 +++++--- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/docs/references/api/tables_views.rst b/docs/references/api/tables_views.rst index 837dd8232..989c1f8d8 100644 --- a/docs/references/api/tables_views.rst +++ b/docs/references/api/tables_views.rst @@ -875,6 +875,56 @@ To bulk insert JSON post an array of objects having all-matching keys ] EOF +.. _bulk_insert_default: + +Bulk Insert with Default Values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Any missing columns in the payload will be inserted as ``null`` values. To use the ``DEFAULT`` column value instead, use the ``Prefer: missing=default`` header. + +Having: + +.. code-block:: postgres + + create table foo ( + id bigint generated by default as identity primary key + , bar text + , baz int default 100 + ); + +A request: + +.. code-block:: http + + POST /foo?columns=id,bar,baz HTTP/1.1 + Content-Type: application/json + Prefer: return=representation + +.. code-block:: json + + [ + { "bar": "val1" + } + , { "bar": "val2" + , "baz": 15 + } + ] + +Will result in: + +.. code-block:: json + + [ + { "id": 1 + , "bar": "val1" + , "baz": 100 + } + , { "id": 2 + , "bar": "val2" + , "baz": 15 + } + ] + .. _specify_columns: Specifying Columns diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index a2de2b3b9..b24e61fa4 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -4,11 +4,10 @@ Features -------- -Horizontal Filtering -~~~~~~~~~~~~~~~~~~~~ +Inserts +~~~~~~~ -- New ``isdistinct`` :ref:`operator `. It is a direct translation of `IS DINTINCT FROM `_. -- New ``and/all`` :ref:`modifiers`. +- New :ref:`bulk_insert_default`. Resource Embedding ~~~~~~~~~~~~~~~~~~ @@ -18,6 +17,12 @@ Resource Embedding - New :ref:`null_embed`. - New :ref:`empty_embed`. +Horizontal Filtering +~~~~~~~~~~~~~~~~~~~~ + +- New ``isdistinct`` :ref:`operator `. It is a direct translation of `IS DINTINCT FROM `_. +- New ``and/all`` :ref:`modifiers`. + OpenAPI ~~~~~~~ From e0b0e988166d961142d220d1ec54a8d695b76f6c Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 11 May 2023 00:59:10 -0300 Subject: [PATCH 583/652] add impersonated settings --- docs/references/transactions.rst | 12 ++++++++++++ docs/releases/v11.0.0.rst | 5 +++++ 2 files changed, 17 insertions(+) diff --git a/docs/references/transactions.rst b/docs/references/transactions.rst index 45a17b1cc..1fc43360d 100644 --- a/docs/references/transactions.rst +++ b/docs/references/transactions.rst @@ -102,6 +102,18 @@ Because of :ref:`user_impersonation`, PostgREST sets the standard ``role``. You Additionally it also sets the ``search_path`` based on :ref:`db-schemas` and :ref:`db-extra-search-path`. +.. _impersonated_settings: + +Impersonated Role Settings +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :ref:`Impersonated Role ` settings are applied. For example, if you do: + +.. code-block:: postgresql + + ALTER ROLE webuser SET statement_timeout TO '5s'; + +Every ``webuser`` request gets its queries executed with a ``statement_timeout`` of 5 seconds. .. _guc_req_headers_cookies_claims: diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index b24e61fa4..bdccd0b0f 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -4,6 +4,11 @@ Features -------- +Transactions +~~~~~~~~~~~~ + +- New :ref:`impersonated_settings`. + Inserts ~~~~~~~ From 789cedff0898d6e6e168680e5e0accc05bdc7080 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 11 May 2023 01:17:55 -0300 Subject: [PATCH 584/652] add isolation level --- docs/references/transactions.rst | 77 ++++++++++++++++++++------------ docs/releases/v11.0.0.rst | 1 + 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/docs/references/transactions.rst b/docs/references/transactions.rst index 1fc43360d..dc3777725 100644 --- a/docs/references/transactions.rst +++ b/docs/references/transactions.rst @@ -59,10 +59,29 @@ Modifying the database inside READ ONLY transactions is not possible. PostgREST The :ref:`options_requests` method doesn't start a transaction, so it's not relevant here. +.. _isolation_lvl: + Isolation Level --------------- -Every transaction uses the PostgreSQL default isolation level: READ COMMITTED. +Every transaction uses the PostgreSQL default isolation level: READ COMMITTED. Unless you modify `default_transaction_isolation `_ for an impersonated role or function. + +Using :ref:`impersonated_settings`, change the isolation level for all the role's requests with: + +.. code-block:: postgresql + + ALTER ROLE webuser SET default_transaction_isolation TO 'repeatable read'; + +Or to change the isolation level per function call. + +.. code-block:: postgresql + + CREATE OR REPLACE FUNCTION myfunc() + RETURNS text as $$ + SELECT 'hello'; + $$ + LANGUAGE SQL + SET default_transaction_isolation TO 'serializable'; .. _tx_settings: @@ -87,34 +106,6 @@ And you can set them with ``set_config`` SELECT set_config('response.', 'value1' ,true); -Request Role and Search Path -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Because of :ref:`user_impersonation`, PostgREST sets the standard ``role``. You can get this in different ways: - -.. code-block:: postgresql - - SELECT current_role; - - SELECT current_user; - - SELECT current_setting('role', true); - -Additionally it also sets the ``search_path`` based on :ref:`db-schemas` and :ref:`db-extra-search-path`. - -.. _impersonated_settings: - -Impersonated Role Settings -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The :ref:`Impersonated Role ` settings are applied. For example, if you do: - -.. code-block:: postgresql - - ALTER ROLE webuser SET statement_timeout TO '5s'; - -Every ``webuser`` request gets its queries executed with a ``statement_timeout`` of 5 seconds. - .. _guc_req_headers_cookies_claims: Request Headers, Cookies and JWT claims @@ -153,6 +144,34 @@ The path and method are stored as ``text``. SELECT current_setting('request.method', true); +Request Role and Search Path +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Because of :ref:`user_impersonation`, PostgREST sets the standard ``role``. You can get this in different ways: + +.. code-block:: postgresql + + SELECT current_role; + + SELECT current_user; + + SELECT current_setting('role', true); + +Additionally it also sets the ``search_path`` based on :ref:`db-schemas` and :ref:`db-extra-search-path`. + +.. _impersonated_settings: + +Impersonated Role Settings +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :ref:`Impersonated Role ` settings are applied. For example, if you do: + +.. code-block:: postgresql + + ALTER ROLE webuser SET statement_timeout TO '5s'; + +Every ``webuser`` request gets its queries executed with a ``statement_timeout`` of 5 seconds. + .. _guc_resp_hdrs: Response Headers diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index bdccd0b0f..d8b04ffbb 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -8,6 +8,7 @@ Transactions ~~~~~~~~~~~~ - New :ref:`impersonated_settings`. +- New configurable :ref:`isolation_lvl`. Inserts ~~~~~~~ From ed147ce5c8c9310a4e31832bae2a01f94b867b93 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 11 May 2023 01:27:34 -0300 Subject: [PATCH 585/652] add bug fixes simply link to github releases page. bug fixes do not add anything for docs. --- docs/releases/v11.0.0.rst | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.0.rst index d8b04ffbb..6093452ed 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.0.rst @@ -7,14 +7,20 @@ Features Transactions ~~~~~~~~~~~~ -- New :ref:`impersonated_settings`. - New configurable :ref:`isolation_lvl`. +- New :ref:`impersonated_settings`. Inserts ~~~~~~~ - New :ref:`bulk_insert_default`. +Horizontal Filtering +~~~~~~~~~~~~~~~~~~~~ + +- New ``isdistinct`` :ref:`operator `. It is a direct translation of `IS DINTINCT FROM `_. +- New ``and/all`` :ref:`modifiers`. + Resource Embedding ~~~~~~~~~~~~~~~~~~ @@ -23,12 +29,6 @@ Resource Embedding - New :ref:`null_embed`. - New :ref:`empty_embed`. -Horizontal Filtering -~~~~~~~~~~~~~~~~~~~~ - -- New ``isdistinct`` :ref:`operator `. It is a direct translation of `IS DINTINCT FROM `_. -- New ``and/all`` :ref:`modifiers`. - OpenAPI ~~~~~~~ @@ -47,11 +47,13 @@ Documentation improvements - New :ref:`connection_pool` reference. - Split :ref:`api` into other sub-references. - Split :ref:`authn` into :ref:`db_authz` and :ref:`sql_user_management`. -- Split :ref:`admin` into :ref:`intgrs` and :ref:`nginx`. +- Split :ref:`admin` into :ref:`intgrs`, :ref:`nginx` and :ref:`connection_pool`. Bug fixes --------- +- Please check the "Fixed" section on https://github.com/PostgREST/postgrest/releases/tag/v11.0.0 and https://github.com/PostgREST/postgrest/releases/tag/v11.0.1. + Breaking changes ---------------- From d7907f4aa425606f8346a3eb92018756a740d0e3 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 11 May 2023 01:34:25 -0300 Subject: [PATCH 586/652] pin version to 11.0.1 --- docs/conf.py | 2 +- docs/releases/{v11.0.0.rst => v11.0.1.rst} | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) rename docs/releases/{v11.0.0.rst => v11.0.1.rst} (90%) diff --git a/docs/conf.py b/docs/conf.py index 2e48d664b..c754b47b5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,7 +59,7 @@ copyright = u'2017, ' + author # The short X.Y version. version = u'11.0' # The full version, including alpha/beta/rc tags. -release = u'11.0.0' +release = u'11.0.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/releases/v11.0.0.rst b/docs/releases/v11.0.1.rst similarity index 90% rename from docs/releases/v11.0.0.rst rename to docs/releases/v11.0.1.rst index 6093452ed..6818c5fdb 100644 --- a/docs/releases/v11.0.0.rst +++ b/docs/releases/v11.0.1.rst @@ -1,6 +1,8 @@ -11.0.0 +11.0.1 ====== +These release notes include the changes in versions `11.0.0 `_ and `11.0.1 `_. The detailed changelog and pre-compiled binaries are available on the `GitHub release pages `_. + Features -------- From 946b1139129c9c3be6a042a606a1d65741a9f85c Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 11 May 2023 21:14:42 -0300 Subject: [PATCH 587/652] add function inlining --- docs/index.rst | 5 +++ docs/references/api/stored_procedures.rst | 39 +++++++++++++++++++++++ docs/releases/v11.0.1.rst | 5 +++ postgrest.dict | 4 +++ 4 files changed, 53 insertions(+) diff --git a/docs/index.rst b/docs/index.rst index 34890d57d..35601c1c5 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -82,6 +82,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. +Business Logic in Database Functions +------------------------------------ + +There is nothing "business" about keeping data segregated across piles of layers. Use database functions to process data and get the performance benefits of data colocality and reduced network roundtrips. + One Thing Well -------------- diff --git a/docs/references/api/stored_procedures.rst b/docs/references/api/stored_procedures.rst index 3bb971ab3..fcf42fed5 100644 --- a/docs/references/api/stored_procedures.rst +++ b/docs/references/api/stored_procedures.rst @@ -340,6 +340,45 @@ A function that returns a table type can be filtered using the same filters as : curl "http://localhost:3000/rpc/best_films_2017?rating=gt.8&order=title.desc" +.. _function_inlining: + +Function Inlining +~~~~~~~~~~~~~~~~~ + +A function that follows the `rules for inlining `_ will also inline :ref:`filters `, :ref:`order ` and :ref:`limits `. + +For example, for the following function: + +.. code-block:: postgres + + create function getallprojects() returns setof projects + language sql stable + as $$ + select * from projects; + $$; + +Let's get its :ref:`explain_plan` when calling it with filters applied: + +.. tabs:: + + .. code-tab:: http + + GET /rpc/getallprojects?id=eq.1 HTTP/1.1 + Accept: application/vnd.pgrst.plan + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/getallprojects?id=eq.1" \ + -H "Accept: application/vnd.pgrst.plan" + +.. code-block:: psql + + Aggregate (cost=8.18..8.20 rows=1 width=112) + -> Index Scan using projects_pkey on projects (cost=0.15..8.17 rows=1 width=40) + Index Cond: (id = 1) + +Notice there's no "Function Scan" node in the plan, which tells us it has been inlined. + .. _scalar_functions: Scalar functions diff --git a/docs/releases/v11.0.1.rst b/docs/releases/v11.0.1.rst index 6818c5fdb..10257fa85 100644 --- a/docs/releases/v11.0.1.rst +++ b/docs/releases/v11.0.1.rst @@ -17,6 +17,11 @@ Inserts - New :ref:`bulk_insert_default`. +Functions +~~~~~~~~~ + +- Filters on Table-Valued Functions are now guaranteed to be inlined. See :ref:`function_inlining`. + Horizontal Filtering ~~~~~~~~~~~~~~~~~~~~ diff --git a/postgrest.dict b/postgrest.dict index 6803c01be..92a3b4b87 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -19,6 +19,7 @@ Cardano cd centric changelog +colocality ClojureScript cloudfared config @@ -62,6 +63,8 @@ HTTP HTTPS HV Ibarluzea +Inlining +inlined Integrations ilike imatch @@ -147,6 +150,7 @@ refactor Reloadable Remo requester's +roundtrips RESTful RestSharp RLS From 911634ed32d8d694851d0354fd4901ebe2471653 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 12 May 2023 01:13:35 -0300 Subject: [PATCH 588/652] move impersonated settings --- docs/references/auth.rst | 13 +++++++++++++ docs/references/transactions.rst | 13 ------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/references/auth.rst b/docs/references/auth.rst index 4eb8b93a1..9a53fd912 100644 --- a/docs/references/auth.rst +++ b/docs/references/auth.rst @@ -188,3 +188,16 @@ doing custom logic based on the web user info. END IF; END $$ LANGUAGE plpgsql; + +.. _impersonated_settings: + +Impersonated Role Settings +-------------------------- + +The :ref:`Impersonated Role ` settings are applied. For example, if you do: + +.. code-block:: postgresql + + ALTER ROLE webuser SET statement_timeout TO '5s'; + +Every ``webuser`` :ref:`transaction ` gets its queries executed with a ``statement_timeout`` of 5 seconds. diff --git a/docs/references/transactions.rst b/docs/references/transactions.rst index dc3777725..6409ce82c 100644 --- a/docs/references/transactions.rst +++ b/docs/references/transactions.rst @@ -159,19 +159,6 @@ Because of :ref:`user_impersonation`, PostgREST sets the standard ``role``. You Additionally it also sets the ``search_path`` based on :ref:`db-schemas` and :ref:`db-extra-search-path`. -.. _impersonated_settings: - -Impersonated Role Settings -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The :ref:`Impersonated Role ` settings are applied. For example, if you do: - -.. code-block:: postgresql - - ALTER ROLE webuser SET statement_timeout TO '5s'; - -Every ``webuser`` request gets its queries executed with a ``statement_timeout`` of 5 seconds. - .. _guc_resp_hdrs: Response Headers From ce82b0c79b831c01d8396c3374e1871234b9b2f7 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 12 May 2023 21:27:12 -0500 Subject: [PATCH 589/652] Re-add missing legacy gucs information --- docs/references/transactions.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/references/transactions.rst b/docs/references/transactions.rst index 6409ce82c..4c8e63ea4 100644 --- a/docs/references/transactions.rst +++ b/docs/references/transactions.rst @@ -131,6 +131,13 @@ PostgREST stores the headers, cookies and headers as JSON. To get them: The ``role`` in ``request.jwt.claims`` defaults to the value of :ref:`db-anon-role`. +.. _guc_legacy_names: + +Legacy settings +^^^^^^^^^^^^^^^ + +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: Request Path and Method From 05bd033539fbff22734229d40cc8c0202da78c6c Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Fri, 12 May 2023 21:48:01 -0500 Subject: [PATCH 590/652] Add JavaScript redirect from the old api.html to new pages and sections Uses the hashtag in the url to redirect to the sections where the content was moved to --- docs/references/api.rst | 99 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/docs/references/api.rst b/docs/references/api.rst index b83d24b94..b253f3be8 100644 --- a/docs/references/api.rst +++ b/docs/references/api.rst @@ -16,3 +16,102 @@ PostgREST exposes three database objects of a schema as resources: tables, views api/openapi.rst api/resource_representation.rst api/* + +.. raw:: html + + \ No newline at end of file From 2e6ebca8855fd1eafa0881e29c75ca9fe1b977b1 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Thu, 25 May 2023 22:54:23 -0500 Subject: [PATCH 591/652] Fix link to archlinux pkg --- docs/explanations/install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/explanations/install.rst b/docs/explanations/install.rst index 8d3a6455a..6944803a8 100644 --- a/docs/explanations/install.rst +++ b/docs/explanations/install.rst @@ -32,7 +32,7 @@ You can also use your OS package manager. .. tab:: Arch Linux - You can install PostgREST from the `community repo `_. + You can install PostgREST from the `community repo `_. .. code:: bash From 281d7a9223bb734a82ed3b88b875b884d2151fb1 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 19 May 2023 14:21:56 -0300 Subject: [PATCH 592/652] remove db functions section on index --- docs/index.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 35601c1c5..34890d57d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -82,11 +82,6 @@ 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. -Business Logic in Database Functions ------------------------------------- - -There is nothing "business" about keeping data segregated across piles of layers. Use database functions to process data and get the performance benefits of data colocality and reduced network roundtrips. - One Thing Well -------------- From 5e19594ea488ace95dd42545a0077b7f8f243011 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 22 May 2023 16:28:04 -0300 Subject: [PATCH 593/652] auth: explain why jwt is used --- docs/references/auth.rst | 2 +- postgrest.dict | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/references/auth.rst b/docs/references/auth.rst index 9a53fd912..a25b08be1 100644 --- a/docs/references/auth.rst +++ b/docs/references/auth.rst @@ -42,7 +42,7 @@ This role switching mechanism is called **user impersonation**. In PostgreSQL it JWT-Based User Impersonation ---------------------------- -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. +We use `JSON Web Tokens `_ to authenticate API requests, this allows us to be stateless and not require database lookups for verification. As you'll recall a JWT contains a list of cryptographically signed claims. All claims are allowed but PostgREST cares specifically about a claim called role. .. code:: json diff --git a/postgrest.dict b/postgrest.dict index 92a3b4b87..9f01bfde9 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -83,6 +83,7 @@ Kofi Kubernetes localhost login +lookups Logins logins lon From fff0eae77eb2af04c1c1dde4b1bca0a818540555 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 27 May 2023 17:43:15 -0500 Subject: [PATCH 594/652] Fix in-db config settings --- docs/references/configuration.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index 557972a15..faadd91d9 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -159,6 +159,7 @@ db-pool-acquisition-timeout Int 10 db-pool-max-lifetime Int 1800 db-pre-request String Y db-prepared-statements Boolean True Y +db-root-spec String Y db-schemas String public Y db-tx-end String commit db-uri String postgresql:// @@ -198,7 +199,7 @@ app.settings.* =============== ==================== **Environment** PGRST_APP_SETTINGS_* - **In-Database** pgrst.app_settings_* + **In-Database** `n/a` =============== ==================== Arbitrary settings that can be used to pass in secret keys directly as strings, or via OS environment variables. For instance: :code:`app.settings.jwt_secret = "$(MYAPP_JWT_SECRET)"` will take :code:`MYAPP_JWT_SECRET` from the environment and make it available to postgresql functions as :code:`current_setting('app.settings.jwt_secret')`. @@ -210,7 +211,7 @@ db-anon-role =============== ================== **Environment** PGRST_DB_ANON_ROLE - **In-Database** `n/a` + **In-Database** `pgrst.db_anon_role` =============== ================== The database role to use when executing commands on behalf of unauthenticated clients. For more information, see :ref:`roles`. From 8d706a342c200463e07260013b11222482e2c86f Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 27 May 2023 18:15:01 -0500 Subject: [PATCH 595/652] fix broken links --- docs/index.rst | 4 ++-- docs/references/api/stored_procedures.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 34890d57d..4020d6195 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -197,7 +197,6 @@ Here are some companies that use PostgREST in production. * `Drip Depot `_ * `Image-charts `_ * `Moat `_ -* `MotionDynamic - Fast highly dynamic video generation at scale `_ * `Netwo `_ * `Nimbus `_ - See how Nimbus uses PostgREST in `Paul Copplestone's blog post `_. @@ -206,8 +205,9 @@ Here are some companies that use PostgREST in production. * `Sompani `_ * `Supabase `_ -.. Certs are failing +.. Failing links * `eGull `_ + * `MotionDynamic - Fast highly dynamic video generation at scale `_ Testimonials ------------ diff --git a/docs/references/api/stored_procedures.rst b/docs/references/api/stored_procedures.rst index fcf42fed5..0597171eb 100644 --- a/docs/references/api/stored_procedures.rst +++ b/docs/references/api/stored_procedures.rst @@ -3,7 +3,7 @@ Stored Procedures ================= -*"A single resource can be the equivalent of a database stored procedure, with the power to abstract state changes over any number of storage items"* -- `Roy T. Fielding `_ +*"A single resource can be the equivalent of a database stored procedure, with the power to abstract state changes over any number of storage items"* -- `Roy T. Fielding `_ Every stored procedure in the :ref:`exposed schema ` and accessible by the :ref:`active database role ` is executable under the :code:`/rpc` prefix. Procedures can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). From 232c6dc702ba23ca444dca6afbf6f0e3295447bf Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 27 May 2023 22:00:14 -0500 Subject: [PATCH 596/652] dry config settings Environment variables names and in-db configs on the table. --- docs/references/configuration.rst | 262 +++++------------------------- 1 file changed, 43 insertions(+), 219 deletions(-) diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index faadd91d9..0f9ae8911 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -97,21 +97,10 @@ 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:`admin-server-port` - * :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` - * :ref:`server-unix-socket-mode` +- Any modification to the :ref:`file_config` will be applied during reload. +- Any modification to the :ref:`in_db_config` will be applied during reload. +- Not all settings are reloadable, the reloadable column on :ref:`config_full_list` specifies which ones are. +- It's not possible to change :ref:`env_variables_config` for a running process, hence reloading a Docker container configuration will not work. In these cases, you can restart the process or use :ref:`in_db_config`. .. _config_reloading_signal: @@ -135,73 +124,63 @@ To reload the configuration from within the database, you can use a NOTIFY comma 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`. +The ``"pgrst"`` notification channel is enabled by default. You can name the channel with :ref:`db-channel` and enable or disable it with :ref:`db-channel-enabled`. .. _config_full_list: List of parameters ================== -=========================== ======= ================= ========== -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 10 -db-pool-max-lifetime Int 1800 -db-pre-request String Y -db-prepared-statements Boolean True Y -db-root-spec String 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-trace-header String Y -server-unix-socket String -server-unix-socket-mode String 660 -=========================== ======= ================= ========== +=========================== ======= ================= ========== ================================= ============================== +Name Type Default Reloadable Environment variable In-database name +=========================== ======= ================= ========== ================================= ============================== +admin-server-port Int PGRST_ADMIN_SERVER_PORT +app.settings.* String Y PGRST_APP_SETTINGS_* +db-anon-role String Y PGRST_DB_ANON_ROLE pgrst.db_anon_role +db-channel String pgrst Y PGRST_DB_CHANNEL +db-channel-enabled Boolean True Y PGRST_DB_CHANNEL_ENABLED +db-config Boolean True Y PGRST_DB_CONFIG +db-extra-search-path String public Y PGRST_DB_EXTRA_SEARCH_PATH pgrst.db_extra_search_path +db-max-rows Int ∞ Y PGRST_DB_MAX_ROWS pgrst.db_max_rows +db-plan-enabled Boolean False Y PGRST_DB_PLAN_ENABLED pgrst.db_plan_enabled +db-pool Int 10 PGRST_DB_POOL +db-pool-acquisition-timeout Int 10 PGRST_DB_POOL_ACQUISITION_TIMEOUT +db-pool-max-lifetime Int 1800 PGRST_DB_POOL_MAX_LIFETIME +db-pre-request String Y PGRST_DB_PRE_REQUEST pgrst.db_pre_request +db-prepared-statements Boolean True Y PGRST_DB_PREPARED_STATEMENTS pgrst.db_prepared_statements +db-root-spec String Y PGRST_DB_ROOT_SPEC pgrst.db_root_spec +db-schemas String public Y PGRST_DB_SCHEMAS pgrst.db_schemas +db-tx-end String commit PGRST_DB_TX_END +db-uri String postgresql:// PGRST_DB_URI +db-use-legacy-gucs Boolean True Y PGRST_DB_USE_LEGACY_GUCS pgrst.db_use_legacy_gucs +jwt-aud String Y PGRST_JWT_AUD pgrst.jwt_aud +jwt-role-claim-key String .role Y PGRST_JWT_ROLE_CLAIM_KEY pgrst.jwt_role_claim_key +jwt-secret String Y PGRST_JWT_SECRET pgrst.jwt_secret +jwt-secret-is-base64 Boolean False Y PGRST_JWT_SECRET_IS_BASE64 pgrst.jwt_secret_is_base64 +log-level String error PGRST_LOG_LEVEL +openapi-mode String follow-privileges Y PGRST_OPENAPI_MODE pgrst.openapi_mode +openapi-security-active Boolean False Y PGRST_OPENAPI_SECURITY_ACTIVE pgrst.openapi_security_active +openapi-server-proxy-uri String Y PGRST_OPENAPI_SERVER_PROXY_URI pgrst.openapi_server_proxy_uri +raw-media-types String Y PGRST_RAW_MEDIA_TYPES pgrst.raw_media_types +server-host String !4 PGRST_SERVER_HOST +server-port Int 3000 PGRST_SERVER_PORT +server-trace-header String Y PGRST_SERVER_TRACE_HEADER pgrst.server_trace_header +server-unix-socket String PGRST_SERVER_UNIX_SOCKET +server-unix-socket-mode String 660 PGRST_SERVER_UNIX_SOCKET_MODE +=========================== ======= ================= ========== ================================= ============================== .. _admin-server-port: admin-server-port ----------------- - =============== ======================= - **Environment** PGRST_ADMIN_SERVER_PORT - **In-Database** `n/a` - =============== ======================= - -Specifies the port for the :ref:`health_check` endpoints. + Specifies the port for the :ref:`health_check` endpoints. .. _app.settings.*: app.settings.* -------------- - =============== ==================== - **Environment** PGRST_APP_SETTINGS_* - **In-Database** `n/a` - =============== ==================== - Arbitrary settings that can be used to pass in secret keys directly as strings, or via OS environment variables. For instance: :code:`app.settings.jwt_secret = "$(MYAPP_JWT_SECRET)"` will take :code:`MYAPP_JWT_SECRET` from the environment and make it available to postgresql functions as :code:`current_setting('app.settings.jwt_secret')`. .. _db-anon-role: @@ -209,11 +188,6 @@ app.settings.* db-anon-role ------------ - =============== ================== - **Environment** PGRST_DB_ANON_ROLE - **In-Database** `pgrst.db_anon_role` - =============== ================== - The database role to use when executing commands on behalf of unauthenticated clients. For more information, see :ref:`roles`. When unset anonymous access will be blocked. @@ -223,11 +197,6 @@ db-anon-role db-channel ---------- - =============== ================ - **Environment** PGRST_DB_CHANNEL - **In-Database** `n/a` - =============== ================ - The name of the notification channel that PostgREST uses for :ref:`schema_reloading` and configuration reloading. .. _db-channel-enabled: @@ -235,11 +204,6 @@ db-channel db-channel-enabled ------------------ - =============== ======================== - **Environment** PGRST_DB_CHANNEL_ENABLED - **In-Database** `n/a` - =============== ======================== - When this is set to :code:`true`, the notification channel specified in :ref:`db-channel` is enabled. You should set this to ``false`` when using PostgresSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section ` for more information. @@ -249,11 +213,6 @@ db-channel-enabled db-config --------- - =============== =============== - **Environment** PGRST_DB_CONFIG - **In-Database** `n/a` - =============== =============== - Enables the in-database configuration. .. _db-extra-search-path: @@ -261,11 +220,6 @@ db-config db-extra-search-path -------------------- - =============== ========================== - **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`. @@ -279,11 +233,6 @@ 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-plan-enabled: @@ -291,11 +240,6 @@ db-max-rows 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`. It's recommended to use this in testing environments only since it reveals internal database details. @@ -327,11 +271,6 @@ db-plan-enabled db-pool ------- - =============== ================= - **Environment** PGRST_DB_POOL - **In-Database** `n/a` - =============== ================= - Number of maximum connections to keep open in PostgREST's database pool. .. _db-pool-acquisition-timeout: @@ -339,11 +278,6 @@ db-pool db-pool-acquisition-timeout --------------------------- - =============== ================= - **Environment** PGRST_DB_POOL_ACQUISITION_TIMEOUT - **In-Database** `n/a` - =============== ================= - Specifies the maximum time in seconds that the request will wait for the pool to free up a connection slot to the database. .. _db-pool-max-lifetime: @@ -351,11 +285,6 @@ db-pool-acquisition-timeout 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. .. _db-pre-request: @@ -365,11 +294,6 @@ db-pre-request *For backwards compatibility, this config parameter is also available without prefix as "pre-request".* - =============== ================= - **Environment** PGRST_DB_PRE_REQUEST - **In-Database** pgrst.db_pre_request - =============== ================= - A schema-qualified stored procedure name to call right after the :ref:`tx_settings` are set. See :ref:`pre-request`. .. _db-prepared-statements: @@ -377,11 +301,6 @@ db-pre-request 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. @@ -393,11 +312,6 @@ db-prepared-statements db-root-spec ------------ - =============== ================= - **Environment** PGRST_DB_ROOT_SPEC - **In-Database** pgrst.db_root_spec - =============== ================= - Function to override the OpenAPI response. See :ref:`override_openapi`. .. _db-schemas: @@ -407,11 +321,6 @@ db-schemas *For backwards compatibility, this config parameter is also available in singular as "db-schema".* - =============== ================= - **Environment** PGRST_DB_SCHEMAS - **In-Database** pgrst.db_schemas - =============== ================= - The list of database schemas to expose to clients. See :ref:`schemas`. .. _db-tx-end: @@ -419,11 +328,6 @@ db-schemas db-tx-end --------- - =============== ================= - **Environment** PGRST_DB_TX_END - **In-Database** pgrst.db_tx_end - =============== ================= - Specifies how to terminate the database transactions. .. code:: bash @@ -445,11 +349,6 @@ db-tx-end db-uri ------ - =============== ================= - **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``. The user with whom PostgREST connects to the database is also known as the ``authenticator`` role. For more information see :ref:`roles`. @@ -463,11 +362,6 @@ db-uri 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. @@ -477,11 +371,6 @@ db-use-legacy-gucs jwt-aud ------- - =============== ================= - **Environment** PGRST_JWT_AUD - **In-Database** pgrst.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. .. _jwt-role-claim-key: @@ -491,11 +380,6 @@ jwt-role-claim-key *For backwards compatibility, this config parameter is also available without prefix as "role-claim-key".* - =============== ================= - **Environment** PGRST_JWT_ROLE_CLAIM_KEY - **In-Database** pgrst.jwt_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 @@ -513,11 +397,6 @@ jwt-role-claim-key jwt-secret ---------- - =============== ================= - **Environment** PGRST_JWT_SECRET - **In-Database** pgrst.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. @@ -531,11 +410,6 @@ jwt-secret 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: @@ -543,11 +417,6 @@ jwt-secret-is-base64 log-level --------- - =============== ================= - **Environment** PGRST_LOG_LEVEL - **In-Database** `n/a` - =============== ================= - Specifies the level of information to be logged while running PostgREST. .. code:: bash @@ -572,11 +441,6 @@ log-level openapi-mode ------------ - =============== ================= - **Environment** PGRST_OPENAPI_MODE - **In-Database** pgrst.openapi_mode - =============== ================= - Specifies how the OpenAPI output should be displayed. .. code:: bash @@ -598,11 +462,6 @@ openapi-mode 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: @@ -610,11 +469,6 @@ When this is set to :code:`true`, security options are included in the :ref:`Ope 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 @@ -638,11 +492,6 @@ openapi-server-proxy-uri 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:`scalar_return_formats`. @@ -659,11 +508,6 @@ raw-media-types server-host ----------- - =============== ================= - **Environment** PGRST_SERVER_HOST - **In-Database** `n/a` - =============== ================= - Where to bind the PostgREST web server. In addition to the usual address options, PostgREST interprets these reserved addresses with special meanings: * :code:`*` - any IPv4 or IPv6 hostname @@ -677,11 +521,6 @@ server-host server-port ----------- - =============== ================= - **Environment** PGRST_SERVER_PORT - **In-Database** `n/a` - =============== ================= - The TCP port to bind the web server. .. _server-trace-header: @@ -689,11 +528,6 @@ server-port server-trace-header ------------------- - =============== ================= - **Environment** PGRST_SERVER_TRACE_HEADER - **In-Database** `pgrst.server_trace_header` - =============== ================= - The header name used to trace HTTP requests. See :ref:`trace_header`. .. _server-unix-socket: @@ -701,11 +535,6 @@ server-trace-header server-unix-socket ------------------ - =============== ================= - **Environment** PGRST_SERVER_UNIX_SOCKET - **In-Database** `n/a` - =============== ================= - `Unix domain socket `_ where to bind the PostgREST web server. If specified, this takes precedence over :ref:`server-port`. Example: @@ -718,11 +547,6 @@ server-unix-socket server-unix-socket-mode ----------------------- - =============== ================= - **Environment** PGRST_SERVER_UNIX_SOCKET_MODE - **In-Database** `n/a` - =============== ================= - `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. From 1c15d3d35bed8a36ebc31522830930fda0a77e6c Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 27 May 2023 22:35:24 -0500 Subject: [PATCH 597/652] remove note and put it at the start --- docs/references/configuration.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index 0f9ae8911..440ada5d2 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -3,7 +3,9 @@ Configuration ############# -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`. +Config parameters can be provided via :ref:`file_config`, :ref:`env_variables_config` or :ref:`in_db_config`. Using :ref:`config_reloading` you can modify the parameters without restarting 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 `. 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. @@ -24,10 +26,6 @@ PostgREST can read a config file. There is no predefined location for this file, ./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: .. code:: From d59e2498be84b18eed47cf98f27a1ce8abc2c253 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sun, 28 May 2023 00:01:54 -0500 Subject: [PATCH 598/652] add pre-config function --- docs/references/configuration.rst | 55 +++++++++++++++++-------- docs/releases/unreleased.rst | 68 +++++++++++++++++++++++++++++++ postgrest.dict | 1 + 3 files changed, 108 insertions(+), 16 deletions(-) create mode 100644 docs/releases/unreleased.rst diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index 440ada5d2..a55b5fef3 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -57,36 +57,51 @@ 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.*``. +See the full list of environment variable names on :ref:`config_full_list`. + .. _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`. +Using a :ref:`pre-config ` function, you can configure the server with database settings. For example, you can configure :ref:`db-schemas` and :ref:`jwt-secret` like this: -For example, you can configure :ref:`db-schemas` and :ref:`jwt-secret` like this: +.. code-block:: -.. code:: postgresql + # postgrest.conf - ALTER ROLE authenticator SET pgrst.db_schemas = "tenant1, tenant2, tenant3" - ALTER ROLE authenticator IN DATABASE SET pgrst.jwt_secret = "REALLYREALLYREALLYREALLYVERYSAFE" + db-pre-config = "postgrest.pre_config" -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. + # or env vars -Note that underscores(``_``) need to be used instead of dashes(``-``) for the in-database config parameters. + PGRST_DB_PRE_CONFIG = "postgrest.pre_config" -.. important:: +.. code-block:: postgresql - 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. + -- create a dedicated schema, hidden from the API + create schema postgrest; + -- grant usage on this schema to the authenticator + grant usage on schema postgrest to authenticator; -When using both the configuration file and the in-database configuration, the latter takes precedence. + -- the function can configure postgREST by using set_config + create or replace function postgrest.pre_config() + returns void as $$ + select + set_config('pgrst.db_schemas', 'schema1, schema2', true) + , set_config('pgrst.db_jwt_secret', 'REALLYREALLYREALLYREALLYVERYSAFE', true); + $$ language sql; -.. danger:: +Note that underscores(``_``) need to be used instead of dashes(``-``) for the in-database config parameters. See the full list of in-database names on :ref:`config_full_list`. - 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. +You can disable the in-database configuration by setting :ref:`db-config` to ``false``. + +.. note:: + For backwards compatibility, you can do in-db config by modifying the :ref:`authenticator role `. This is no longer recommended as it requires SUPERUSER. + + .. code:: postgresql + + ALTER ROLE authenticator SET pgrst.db_schemas = "tenant1, tenant2, tenant3" + ALTER ROLE authenticator IN DATABASE SET pgrst.db_schemas = "tenant4, tenant5" -- database-specific setting, overrides the previous setting .. _config_reloading: @@ -97,7 +112,7 @@ It's possible to reload PostgREST's configuration without restarting the server. - Any modification to the :ref:`file_config` will be applied during reload. - Any modification to the :ref:`in_db_config` will be applied during reload. -- Not all settings are reloadable, the reloadable column on :ref:`config_full_list` specifies which ones are. +- Not all settings are reloadable, see the reloadable list on :ref:`config_full_list`. - It's not possible to change :ref:`env_variables_config` for a running process, hence reloading a Docker container configuration will not work. In these cases, you can restart the process or use :ref:`in_db_config`. .. _config_reloading_signal: @@ -138,6 +153,7 @@ db-anon-role String Y PGRST_DB_ANON_R db-channel String pgrst Y PGRST_DB_CHANNEL db-channel-enabled Boolean True Y PGRST_DB_CHANNEL_ENABLED db-config Boolean True Y PGRST_DB_CONFIG +db-pre-config String Y PGRST_DB_PRE_CONFIG pgrst.db_pre_config db-extra-search-path String public Y PGRST_DB_EXTRA_SEARCH_PATH pgrst.db_extra_search_path db-max-rows Int ∞ Y PGRST_DB_MAX_ROWS pgrst.db_max_rows db-plan-enabled Boolean False Y PGRST_DB_PLAN_ENABLED pgrst.db_plan_enabled @@ -213,6 +229,13 @@ db-config Enables the in-database configuration. +.. _db-pre-config: + +db-pre-config +------------- + + Name of the function that does in-database configuration. + .. _db-extra-search-path: db-extra-search-path diff --git a/docs/releases/unreleased.rst b/docs/releases/unreleased.rst new file mode 100644 index 000000000..8b88fa9ca --- /dev/null +++ b/docs/releases/unreleased.rst @@ -0,0 +1,68 @@ +Unreleased +========== + +Features +-------- + +Configuration +~~~~~~~~~~~~~ + +- New :ref:`in_db_config`. It no longer requires high privileges and can be used on cloud-hosted databases. + +Documentation improvements +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Bug fixes +--------- + +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 + +* `Roboflow `_ +* Evans Fernandes +* Jan Sommer +* `Franz Gusenbauer `_ +* Zac Miller +* 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 9f01bfde9..fa04fbd50 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -148,6 +148,7 @@ Rechkemmer reconnection Redux refactor +reloadable Reloadable Remo requester's From d87d80108ac3adea5706c1344ee650e8c8282de6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 May 2023 12:57:34 +0000 Subject: [PATCH 599/652] Bump cachix/install-nix-action from 20 to 21 Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 20 to 21. - [Release notes](https://github.com/cachix/install-nix-action/releases) - [Commits](https://github.com/cachix/install-nix-action/compare/v20...v21) --- 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 c3bc41829..9411dfad9 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@v20 + - uses: cachix/install-nix-action@v21 - 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@v20 + - uses: cachix/install-nix-action@v21 - 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@v20 + - uses: cachix/install-nix-action@v21 - run: nix-env -f default.nix -iA linkcheck - run: postgrest-docs-linkcheck From 48365188adb8556e3395cb3a3f20e66e42d70169 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 31 May 2023 12:33:08 -0500 Subject: [PATCH 600/652] Add redirect to many-to-many relationships from embedding through join tables --- docs/references/api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/references/api.rst b/docs/references/api.rst index b253f3be8..2a6479781 100644 --- a/docs/references/api.rst +++ b/docs/references/api.rst @@ -78,6 +78,7 @@ PostgREST exposes three database objects of a schema as resources: tables, views '#embedding-disambiguation': 'api/resource_embedding.html#embedding-disambiguation', '#target-disambiguation': 'api/resource_embedding.html#target-disambiguation', '#hint-disambiguation': 'api/resource_embedding.html#hint-disambiguation', + "#embedding-through-join-tables": "api/resource_embedding.html#many-to-many-relationships", // OpenAPI '#openapi-support': 'api/openapi.html', // Resource Representation From a7ff2294ac8d1bac3fc0d375452b6b935852c80a Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sat, 3 Jun 2023 00:11:40 -0500 Subject: [PATCH 601/652] references: move raise error to errors page --- docs/references/api/stored_procedures.rst | 2 +- docs/references/errors.rst | 56 ++++++++++++++++++++ docs/references/transactions.rst | 63 ++--------------------- 3 files changed, 61 insertions(+), 60 deletions(-) diff --git a/docs/references/api/stored_procedures.rst b/docs/references/api/stored_procedures.rst index 0597171eb..842ddf7a2 100644 --- a/docs/references/api/stored_procedures.rst +++ b/docs/references/api/stored_procedures.rst @@ -5,7 +5,7 @@ Stored Procedures *"A single resource can be the equivalent of a database stored procedure, with the power to abstract state changes over any number of storage items"* -- `Roy T. Fielding `_ -Every stored procedure in the :ref:`exposed schema ` and accessible by the :ref:`active database role ` is executable under the :code:`/rpc` prefix. Procedures can perform any operations allowed by PostgreSQL (read data, modify data, and even DDL operations). +Every stored procedure in the :ref:`exposed schema ` and accessible by the :ref:`active database role ` is executable under the :code:`/rpc` prefix. Procedures can perform any operations allowed by PostgreSQL (read data, modify data, :ref:`raise errors `, and even DDL operations). If they return table types, Stored Procedures can: diff --git a/docs/references/errors.rst b/docs/references/errors.rst index e481a70fd..0c0a40c72 100644 --- a/docs/references/errors.rst +++ b/docs/references/errors.rst @@ -96,6 +96,62 @@ PostgREST translates `PostgreSQL error codes `. 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 + + { + "message": "Payment Required", + "details": "Quota exceeded", + "hint": "Upgrade your plan", + "code": "PT402" + } + + Errors from PostgREST ===================== diff --git a/docs/references/transactions.rst b/docs/references/transactions.rst index 4c8e63ea4..bbf4b93b2 100644 --- a/docs/references/transactions.rst +++ b/docs/references/transactions.rst @@ -23,7 +23,7 @@ The access mode on :ref:`tables_views` is determined by the HTTP method. :header-rows: 1 * - HTTP Method - - Access Method + - Access Mode * - GET, HEAD - READ ONLY * - POST, PATCH, PUT, DELETE @@ -35,7 +35,7 @@ The access mode on :ref:`tables_views` is determined by the HTTP method. :header-rows: 2 * - - - Access Method + - Access Mode - - * - HTTP Method @@ -235,7 +235,7 @@ If the status code is standard, PostgREST will complete the status message(**I'm Main query ---------- -The main query is produced by requesting the :doc:`API resources `. +The main query is produced by requesting :ref:`tables_views` or :ref:`s_procs`. Transaction End --------------- @@ -245,62 +245,7 @@ If the transaction doesn't fail, it will always end in a COMMIT. Unless :ref:`db Aborting transactions --------------------- -Any database failure(like a failed constraint) will result in a rollback of the transaction. You can also do a RAISE inside a function to cause a rollback. - -.. _raise_error: - -Raise errors with HTTP Status Codes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -You 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 - - { - "message": "Payment Required", - "details": "Quota exceeded", - "hint": "Upgrade your plan", - "code": "PT402" - } +Any database failure(like a failed constraint) will result in a rollback of the transaction. You can also :ref:`RAISE an error inside a function ` to cause a rollback. .. _pre-request: From e7d63a2b27733f6bff5524cc60e78ece56e098cc Mon Sep 17 00:00:00 2001 From: paulocr Date: Mon, 5 Jun 2023 15:02:52 -0600 Subject: [PATCH 602/652] Fix boolean configuration example --- docs/references/configuration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index a55b5fef3..bdd382e9e 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -43,7 +43,7 @@ The configuration file must contain a set of key value pairs: # The secret to verify the JWT for authenticated requests with. # Needs to be 32 characters minimum. jwt-secret = "reallyreallyreallyreallyverysafe" - jwt-secret-is-base64 = False + jwt-secret-is-base64 = false # Port the postgrest process is listening on for http requests server-port = 80 From c62d24c993dcd286377f35f4aa02c47b8a811c85 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 7 Jun 2023 12:12:38 -0500 Subject: [PATCH 603/652] references: add dynamic pool --- docs/references/configuration.rst | 10 ++++++++++ docs/references/connection_pool.rst | 24 +++++++++++++----------- docs/releases/unreleased.rst | 5 +++++ 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index bdd382e9e..9b44ac3ab 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -158,6 +158,7 @@ db-extra-search-path String public Y PGRST_DB_EXTRA_ db-max-rows Int ∞ Y PGRST_DB_MAX_ROWS pgrst.db_max_rows db-plan-enabled Boolean False Y PGRST_DB_PLAN_ENABLED pgrst.db_plan_enabled db-pool Int 10 PGRST_DB_POOL +db-pool-max-idletime Int 30 PGRST_DB_POOL_MAX_IDLETIME db-pool-acquisition-timeout Int 10 PGRST_DB_POOL_ACQUISITION_TIMEOUT db-pool-max-lifetime Int 1800 PGRST_DB_POOL_MAX_LIFETIME db-pre-request String Y PGRST_DB_PRE_REQUEST pgrst.db_pre_request @@ -294,6 +295,15 @@ db-pool Number of maximum connections to keep open in PostgREST's database pool. +.. _db-pool-max-idletime: + +db-pool-max-idletime +-------------------- + + *For backwards compatibility, this config parameter is also available as “db-pool-timeout”.* + + Time in seconds to close idle pool connections. + .. _db-pool-acquisition-timeout: db-pool-acquisition-timeout diff --git a/docs/references/connection_pool.rst b/docs/references/connection_pool.rst index 48683b90a..fbccccaa7 100644 --- a/docs/references/connection_pool.rst +++ b/docs/references/connection_pool.rst @@ -9,29 +9,31 @@ A connection pool is a cache of reusable database connections. It allows serving Minimizing connections it’s paramount to performance. Each PostgreSQL connection creates a process, having too many can exhaust available resources. -.. _pool_growth_limit: +.. _dyn_conn_pool: -Growth Limit ------------- +Dynamic Connection Pool +----------------------- -If all the connections are being used, a new connection is added to the pool. The pool can grow until it reaches the :ref:`db-pool` size. +To converve system resources, PostgREST uses a dynamic connection pool. This enables the number of connections in the pool to increase and decrease depending on request traffic. -Note it’s pointless to set this higher than the ``max_connections`` setting in your database. +If all the connections are being used, a new connection is added to the pool. The pool can grow until it reaches the :ref:`db-pool` size. Note it’s pointless to set this higher than the ``max_connections`` setting in your database. + +If a connection is unused for a period of time(determined by :ref:`db-pool-max-idletime`, 30 seconds by default), it will be closed. Connection lifetime ------------------- -After a period of time, connections from the pool will be released and news ones will be created. This time is specified by :ref:`db-pool-max-lifetime`. +Long-lived PostgreSQL connections can consume considerable memory(see `here `_ for more details). +Under a busy system, the :ref:`db-pool-max-idletime` won't be reached and the connection pool can have many long-lived connections. -The lifetime doesn't affect running requests. Only unused connections will be released. +To avoid this problem and save resources, a connection max lifetime(determined by :ref:`db-pool-max-lifetime`, 30 minutes by default) is enforced. -For knowing why a connection lifetime is necessary, see the following discussion: -https://www.postgresql.org/message-id/flat/CA%2Bmi_8bnvpxHZtb6EgHSHY-xn29W8VJMzjPU3fiCOv1bfjrNuA%40mail.gmail.com. +After the max lifetime is reached, connections from the pool will be released and news ones will be created. This doesn't affect running requests. Only unused connections will be released. Acquisition Timeout ------------------- -If all the available connections in the pool are busy, an HTTP request will wait until reaching a timeout. You can configure this timeout with :ref:`db-pool-acquisition-timeout`. +If all the available connections in the pool are busy, an HTTP request will wait until reaching a timeout(determined by :ref:`db-pool-acquisition-timeout`, 10 seconds by default). If the request reaches the timeout, it will be aborted with the following response: @@ -69,7 +71,7 @@ If the pool loses the connection to the database, it will retry reconnecting usi The retries happen immediately after a connection loss, if :ref:`db-channel-enabled` is set to true(the default). Otherwise they'll happen once a request arrives. -The server reloads the :ref:`schema_cache` when recovering. +The server reloads the :ref:`schema_cache` and :ref:`configuration` when recovering. To notify the client of the next retry, the server sends a ``503 Service Unavailable`` status with the ``Retry-After: x`` header. Where ``x`` is the number of seconds programmed for the next retry. diff --git a/docs/releases/unreleased.rst b/docs/releases/unreleased.rst index 8b88fa9ca..48e4ed895 100644 --- a/docs/releases/unreleased.rst +++ b/docs/releases/unreleased.rst @@ -4,6 +4,11 @@ Unreleased Features -------- +Connection Pool +~~~~~~~~~~~~~~~ + +- New :ref:`db-pool-max-idletime`. It enables a :ref:`dyn_conn_pool`. + Configuration ~~~~~~~~~~~~~ From 188461af7d62ba120806792969605ce261d10865 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 7 Jun 2023 12:15:37 -0500 Subject: [PATCH 604/652] references: prepared statements mention --- docs/references/transactions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/transactions.rst b/docs/references/transactions.rst index bbf4b93b2..85f462fa2 100644 --- a/docs/references/transactions.rst +++ b/docs/references/transactions.rst @@ -235,7 +235,7 @@ If the status code is standard, PostgREST will complete the status message(**I'm Main query ---------- -The main query is produced by requesting :ref:`tables_views` or :ref:`s_procs`. +The main query is generated by requesting :ref:`tables_views` or :ref:`s_procs`. All generated queries use prepared statements(:ref:`db-prepared-statements`). Transaction End --------------- From fb59b82c35af874400ff478639d31f9dfca23384 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 7 Jun 2023 13:43:23 -0500 Subject: [PATCH 605/652] references: updated automatic recovery * use bullets were possible and join paragraphs. * add bug fixes to the unreleased page --- docs/references/connection_pool.rst | 47 ++++++++++++++--------------- docs/releases/unreleased.rst | 6 ++-- postgrest.dict | 2 ++ 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/docs/references/connection_pool.rst b/docs/references/connection_pool.rst index fbccccaa7..a196ee689 100644 --- a/docs/references/connection_pool.rst +++ b/docs/references/connection_pool.rst @@ -3,22 +3,20 @@ Connection Pool =============== -Every request to an :doc:`API resource ` borrows a connection from the connection pool to start a :doc:`transaction `. - -A connection pool is a cache of reusable database connections. It allows serving many HTTP requests using few database connections. +A connection pool is a cache of reusable database connections. It allows serving many HTTP requests using few database connections. Every request to an :doc:`API resource ` borrows a connection from the pool to start a :doc:`transaction `. Minimizing connections it’s paramount to performance. Each PostgreSQL connection creates a process, having too many can exhaust available resources. +.. _pool_growth_limit: .. _dyn_conn_pool: Dynamic Connection Pool ----------------------- -To converve system resources, PostgREST uses a dynamic connection pool. This enables the number of connections in the pool to increase and decrease depending on request traffic. +To conserve system resources, PostgREST uses a dynamic connection pool. This enables the number of connections in the pool to increase and decrease depending on request traffic. -If all the connections are being used, a new connection is added to the pool. The pool can grow until it reaches the :ref:`db-pool` size. Note it’s pointless to set this higher than the ``max_connections`` setting in your database. - -If a connection is unused for a period of time(determined by :ref:`db-pool-max-idletime`, 30 seconds by default), it will be closed. +- If all the connections are being used, a new connection is added. The pool can grow until it reaches the :ref:`db-pool` size. Note it’s pointless to set this higher than the ``max_connections`` setting in your database. +- If a connection is unused for a period of time(determined by :ref:`db-pool-max-idletime`, 30 seconds by default), it will be released. Connection lifetime ------------------- @@ -27,8 +25,7 @@ Long-lived PostgreSQL connections can consume considerable memory(see `here `. + - Reduce write requests. Do :ref:`bulk_insert` (or :ref:`upsert`) instead of inserting rows one by one. + - Reduce read requests. Use :ref:`resource_embedding`. Combine unrelated data into a single request using custom database views or functions. + - Use :ref:`s_procs` for combining read and write logic into a single request. - - Not a panacea since connections can't grow infinitely. Try the previous recommendations before this. + - Increase the :ref:`db-pool` size. + + - Not a panacea since connections can't grow infinitely. Try the previous recommendations before this. .. _automatic_recovery: Automatic Recovery ------------------ -If the pool loses the connection to the database, it will retry reconnecting using exponential backoff. With 32 seconds being the maximum backoff time between retries. +The server will retry reconnecting to the database if connection loss happens. -The retries happen immediately after a connection loss, if :ref:`db-channel-enabled` is set to true(the default). Otherwise they'll happen once a request arrives. - -The server reloads the :ref:`schema_cache` and :ref:`configuration` when recovering. - -To notify the client of the next retry, the server sends a ``503 Service Unavailable`` status with the ``Retry-After: x`` header. Where ``x`` is the number of seconds programmed for the next retry. +- It will retry forever with exponential backoff. 32 seconds being the maximum backoff time between retries. Each of these attempts are :ref:`logged `. +- It will only stop retrying if the server deems the error to be fatal. This can be a password authentication failure or an internal error. +- The retries happen immediately after a connection loss, if :ref:`db-channel-enabled` is set to true(the default). Otherwise they'll happen once a request arrives. +- To ensure a valid state, the server reloads the :ref:`schema_cache` and :ref:`configuration` when recovering. +- To notify the client of the next retry, the server sends a ``503 Service Unavailable`` status with the ``Retry-After: x`` header. Where ``x`` is the number of seconds programmed for the next retry. .. _external_connection_poolers: diff --git a/docs/releases/unreleased.rst b/docs/releases/unreleased.rst index 48e4ed895..1044e92d8 100644 --- a/docs/releases/unreleased.rst +++ b/docs/releases/unreleased.rst @@ -14,12 +14,12 @@ Configuration - New :ref:`in_db_config`. It no longer requires high privileges and can be used on cloud-hosted databases. -Documentation improvements -~~~~~~~~~~~~~~~~~~~~~~~~~~ - Bug fixes --------- +- Fix dropping schema cache reload notifications. +- Stop automatic recovery when the error is "no password supplied". + Thanks ------ diff --git a/postgrest.dict b/postgrest.dict index fa04fbd50..35bdccaf9 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -66,6 +66,8 @@ Ibarluzea Inlining inlined Integrations +idletime +IDLETIME ilike imatch io From b1f50537e98a9697e05cd136fca405d56573432f Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 7 Jun 2023 19:33:42 -0500 Subject: [PATCH 606/652] clarify procs intro --- docs/references/api/stored_procedures.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/api/stored_procedures.rst b/docs/references/api/stored_procedures.rst index 842ddf7a2..45c5e62f0 100644 --- a/docs/references/api/stored_procedures.rst +++ b/docs/references/api/stored_procedures.rst @@ -5,7 +5,7 @@ Stored Procedures *"A single resource can be the equivalent of a database stored procedure, with the power to abstract state changes over any number of storage items"* -- `Roy T. Fielding `_ -Every stored procedure in the :ref:`exposed schema ` and accessible by the :ref:`active database role ` is executable under the :code:`/rpc` prefix. Procedures can perform any operations allowed by PostgreSQL (read data, modify data, :ref:`raise errors `, and even DDL operations). +Procedures can perform any operations allowed by PostgreSQL (read data, modify data, :ref:`raise errors `, and even DDL operations). Every stored procedure in the :ref:`exposed schema ` and accessible by the :ref:`active database role ` is executable under the :code:`/rpc` prefix. If they return table types, Stored Procedures can: From ee6b9c522476be3648d5a488707a329ea1e07003 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 7 Jun 2023 18:47:12 -0500 Subject: [PATCH 607/652] pin version to 11.0.1 --- docs/conf.py | 4 ++-- docs/references/api/stored_procedures.rst | 2 +- docs/references/configuration.rst | 16 ++++++++-------- docs/references/connection_pool.rst | 18 +++++++++--------- docs/releases/{unreleased.rst => v11.1.0.rst} | 4 ++-- 5 files changed, 22 insertions(+), 22 deletions(-) rename docs/releases/{unreleased.rst => v11.1.0.rst} (98%) diff --git a/docs/conf.py b/docs/conf.py index c754b47b5..5125f8299 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,9 +57,9 @@ copyright = u'2017, ' + author # built documents. # # The short X.Y version. -version = u'11.0' +version = u'11.1' # The full version, including alpha/beta/rc tags. -release = u'11.0.1' +release = u'11.1.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/references/api/stored_procedures.rst b/docs/references/api/stored_procedures.rst index 45c5e62f0..e5098864b 100644 --- a/docs/references/api/stored_procedures.rst +++ b/docs/references/api/stored_procedures.rst @@ -65,7 +65,7 @@ The client can call it by posting an object like Calling with GET ---------------- -If the function doesn't modify the database, it will also run under the GET method(see :ref:`access_mode`). +If the function doesn't modify the database, it will also run under the GET method (see :ref:`access_mode`). .. tabs:: diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index 9b44ac3ab..50e71c17d 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -158,8 +158,8 @@ db-extra-search-path String public Y PGRST_DB_EXTRA_ db-max-rows Int ∞ Y PGRST_DB_MAX_ROWS pgrst.db_max_rows db-plan-enabled Boolean False Y PGRST_DB_PLAN_ENABLED pgrst.db_plan_enabled db-pool Int 10 PGRST_DB_POOL -db-pool-max-idletime Int 30 PGRST_DB_POOL_MAX_IDLETIME db-pool-acquisition-timeout Int 10 PGRST_DB_POOL_ACQUISITION_TIMEOUT +db-pool-max-idletime Int 30 PGRST_DB_POOL_MAX_IDLETIME db-pool-max-lifetime Int 1800 PGRST_DB_POOL_MAX_LIFETIME db-pre-request String Y PGRST_DB_PRE_REQUEST pgrst.db_pre_request db-prepared-statements Boolean True Y PGRST_DB_PREPARED_STATEMENTS pgrst.db_prepared_statements @@ -295,6 +295,13 @@ db-pool Number of maximum connections to keep open in PostgREST's database pool. +.. _db-pool-acquisition-timeout: + +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. + .. _db-pool-max-idletime: db-pool-max-idletime @@ -304,13 +311,6 @@ db-pool-max-idletime Time in seconds to close idle pool connections. -.. _db-pool-acquisition-timeout: - -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. - .. _db-pool-max-lifetime: db-pool-max-lifetime diff --git a/docs/references/connection_pool.rst b/docs/references/connection_pool.rst index a196ee689..c1ff099bd 100644 --- a/docs/references/connection_pool.rst +++ b/docs/references/connection_pool.rst @@ -13,24 +13,24 @@ Minimizing connections it’s paramount to performance. Each PostgreSQL connecti Dynamic Connection Pool ----------------------- -To conserve system resources, PostgREST uses a dynamic connection pool. This enables the number of connections in the pool to increase and decrease depending on request traffic. +To conserve system resources, PostgREST uses a dynamic connection pool. This enables the number of connections in the pool to increase and decrease depending on request traffic. -- If all the connections are being used, a new connection is added. The pool can grow until it reaches the :ref:`db-pool` size. Note it’s pointless to set this higher than the ``max_connections`` setting in your database. -- If a connection is unused for a period of time(determined by :ref:`db-pool-max-idletime`, 30 seconds by default), it will be released. +- If all the connections are being used, a new connection is added. The pool can grow until it reaches the :ref:`db-pool` size. Note that it’s pointless to set this higher than the ``max_connections`` setting in your database. +- If a connection is unused for a period of time (determined by :ref:`db-pool-max-idletime`, 30 seconds by default), it will be released. Connection lifetime ------------------- -Long-lived PostgreSQL connections can consume considerable memory(see `here `_ for more details). +Long-lived PostgreSQL connections can consume considerable memory (see `here `_ for more details). Under a busy system, the :ref:`db-pool-max-idletime` won't be reached and the connection pool can have many long-lived connections. -To avoid this problem and save resources, a connection max lifetime(determined by :ref:`db-pool-max-lifetime`, 30 minutes by default) is enforced. -After the max lifetime is reached, connections from the pool will be released and news ones will be created. This doesn't affect running requests, only unused connections will be released. +To avoid this problem and save resources, a connection max lifetime (determined by :ref:`db-pool-max-lifetime`, 30 minutes by default) is enforced. +After the max lifetime is reached, connections from the pool will be released and new ones will be created. This doesn't affect running requests, only unused connections will be released. Acquisition Timeout ------------------- -If all the available connections in the pool are busy, an HTTP request will wait until reaching a timeout(determined by :ref:`db-pool-acquisition-timeout`, 10 seconds by default). +If all the available connections in the pool are busy, an HTTP request will wait until reaching a timeout (determined by :ref:`db-pool-acquisition-timeout`, 10 seconds by default). If the request reaches the timeout, it will be aborted with the following response: @@ -68,9 +68,9 @@ Automatic Recovery The server will retry reconnecting to the database if connection loss happens. -- It will retry forever with exponential backoff. 32 seconds being the maximum backoff time between retries. Each of these attempts are :ref:`logged `. +- It will retry forever with exponential backoff, with a maximum backoff time of 32 seconds between retries. Each of these attempts are :ref:`logged `. - It will only stop retrying if the server deems the error to be fatal. This can be a password authentication failure or an internal error. -- The retries happen immediately after a connection loss, if :ref:`db-channel-enabled` is set to true(the default). Otherwise they'll happen once a request arrives. +- The retries happen immediately after a connection loss, if :ref:`db-channel-enabled` is set to true (the default). Otherwise they'll happen once a request arrives. - To ensure a valid state, the server reloads the :ref:`schema_cache` and :ref:`configuration` when recovering. - To notify the client of the next retry, the server sends a ``503 Service Unavailable`` status with the ``Retry-After: x`` header. Where ``x`` is the number of seconds programmed for the next retry. diff --git a/docs/releases/unreleased.rst b/docs/releases/v11.1.0.rst similarity index 98% rename from docs/releases/unreleased.rst rename to docs/releases/v11.1.0.rst index 1044e92d8..55cba22e2 100644 --- a/docs/releases/unreleased.rst +++ b/docs/releases/v11.1.0.rst @@ -1,5 +1,5 @@ -Unreleased -========== +11.1.0 +====== Features -------- From 018427e2b63648a8e1674f85403fe848d1d137d6 Mon Sep 17 00:00:00 2001 From: David Baynard Date: Fri, 9 Jun 2023 21:23:04 +0100 Subject: [PATCH 608/652] Note content-type for singular objects (#639) * Clarify vnd.pgrst.object+json is also Content-Type The previous docs explained how the `Accept` header could be set to `application/vnd.pgrst.object+json`. This change indicates the resulting response `Content-Type` begins likewise. While robust clients will correctly identify that `application/vnd.pgrst.object+json` data are json, many others which naively check the `Content-Type` is `application/json` will fail to recognize what should be a valid `Content-Type`. --- docs/references/api/resource_representation.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/references/api/resource_representation.rst b/docs/references/api/resource_representation.rst index 82f27d96b..c11d8e872 100644 --- a/docs/references/api/resource_representation.rst +++ b/docs/references/api/resource_representation.rst @@ -66,6 +66,8 @@ This returns { "id": 1 } +with a :code:`Content-Type: application/vnd.pgrst.object+json`. + When a singular response is requested but no entries are found, the server responds with an error message and 406 Not Acceptable status code rather than the usual empty array and 200 status: .. code-block:: json From 396afa106ee885f6b58e5895252a839c574d54cf Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 12 Jun 2023 19:36:06 -0500 Subject: [PATCH 609/652] Remove old or unmaintained links from the ecosystem page --- docs/ecosystem.rst | 42 ++---------------------------------------- postgrest.dict | 1 + 2 files changed, 3 insertions(+), 40 deletions(-) diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index 7774b3d1f..11c75a4c9 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -18,6 +18,8 @@ Community Tutorials * `REST Data Service on YugabyteDB / PostgreSQL `_ +* `Build data-driven applications with Workers and PostgreSQL `_ - A tutorial on how to integrate with PostgREST and PostgreSQL using Cloudfare Workers. + .. _templates: Templates @@ -31,28 +33,10 @@ Templates Example Apps ------------ -* `chronicle `_ - tracking a tree of personal memories -* `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 -* `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 * `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 -* `postgrest-sessions-example `_ - example for cookie-based sessions -* `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 @@ -74,14 +58,10 @@ External Notification These are PostgreSQL bridges that propagate LISTEN/NOTIFY to external queues for further processing. This allows stored procedures to initiate actions outside the database such as sending emails. -* `pg-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: @@ -91,25 +71,14 @@ 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-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 --------------------- -* `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-ex `_ - Elixir @@ -117,15 +86,8 @@ Client-Side Libraries * `postgrest-js `_ - TypeScript/JavaScript * `postgrest-kt `_ - Kotlin * `postgrest-py `_ - 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 -* `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 diff --git a/postgrest.dict b/postgrest.dict index 35bdccaf9..fad0d5b6b 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -21,6 +21,7 @@ centric changelog colocality ClojureScript +Cloudfare cloudfared config CORS From fdf85773e76705b8fd33e6b287ad1bf441952c9f Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 14 Jun 2023 14:25:54 -0500 Subject: [PATCH 610/652] Fix jwt_secret in example --- docs/references/configuration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index 50e71c17d..a0683c774 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -88,7 +88,7 @@ Using a :ref:`pre-config ` function, you can configure the server returns void as $$ select set_config('pgrst.db_schemas', 'schema1, schema2', true) - , set_config('pgrst.db_jwt_secret', 'REALLYREALLYREALLYREALLYVERYSAFE', true); + , set_config('pgrst.jwt_secret', 'REALLYREALLYREALLYREALLYVERYSAFE', true); $$ language sql; Note that underscores(``_``) need to be used instead of dashes(``-``) for the in-database config parameters. See the full list of in-database names on :ref:`config_full_list`. From 772046242e2b0c359a26f2ff683ed6a901ddfd0c Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Wed, 14 Jun 2023 14:55:28 -0500 Subject: [PATCH 611/652] Add postgrest-docs-dictcheck to CI --- .github/workflows/ci.yaml | 9 +++++++++ .gitignore | 1 + default.nix | 4 +++- postgrest.dict | 32 -------------------------------- 4 files changed, 13 insertions(+), 33 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9411dfad9..d26de3e70 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -29,6 +29,15 @@ jobs: - run: nix-env -f 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@v21 + - run: nix-env -f default.nix -iA dictcheck + - run: postgrest-docs-dictcheck + linkcheck: name: Run linkcheck if: github.base_ref == 'main' diff --git a/.gitignore b/.gitignore index 4c1f95600..ccba409df 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ Pipfile.lock *.log diagrams/db.pdf misspellings +unuseddict diff --git a/default.nix b/default.nix index 5dffe887a..42fbaca76 100644 --- a/default.nix +++ b/default.nix @@ -73,7 +73,9 @@ in | tail -n+2 \ | tr '\n' '\0' \ | xargs -0 -n 1 -i \ - sh -c "grep \"{}\" $FILES > /dev/null || echo \"{}\"" + sh -c "grep \"{}\" $FILES > /dev/null || echo \"{}\"" \ + | tee unuseddict + test ! -s unuseddict ''; linkcheck = diff --git a/postgrest.dict b/postgrest.dict index fad0d5b6b..52956e941 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -1,6 +1,4 @@ personal_ws-1.1 en 0 utf-8 -Adossi -AMQP api API's Archlinux @@ -10,7 +8,6 @@ auth authenticator backoff balancer -Beles booleans Bouscal buildpack @@ -19,21 +16,16 @@ Cardano cd centric changelog -colocality -ClojureScript Cloudfare -cloudfared config CORS CPUs cryptographically CSV -Daemonizing DDL DevOps DiBiase dockerize -DoS eq ETH Ethereum @@ -54,7 +46,6 @@ Greenplum gte GUC gucs -Gumbs Haskell Heroku HMAC @@ -80,9 +71,6 @@ JSON JWK JWT jwt -JWTs -Kinesis -Kofi Kubernetes localhost login @@ -92,23 +80,18 @@ logins lon lt lte -middleware misprediction -Mithril multi -MVCC namespace namespaced Nanos neq nginx -ngrep nixpkgs npm nxl nxr OAuth -onwards OpenAPI openapi ORM @@ -117,7 +100,6 @@ passphrase Pawel PBKDF Pelletier -Petr PgBouncer pgcrypto pgjwt @@ -148,16 +130,13 @@ Rafaj RDS reallyreallyreallyreallyverysafe Rechkemmer -reconnection Redux refactor reloadable Reloadable Remo requester's -roundtrips RESTful -RestSharp RLS RPC RSA @@ -166,16 +145,12 @@ safeupdate savepoint schemas schema's -Sencha -Serverless Severin SHA Sommer signup SIGUSR sl -SNS -sqitch SQL sql sr @@ -183,14 +158,10 @@ SSL stateful stdout Stolarz -subselect -SuperAgent SvelteKit SwaggerUI syslog systemd -Tcl -tmuxp todo todos tos @@ -212,16 +183,13 @@ uri url urls variadic -Vercel verifier versioning Vondra Vue -WAI webhooks websearch Websockets webuser wfts Zac -ZeroMQ From ab986b77d8379968cced0720abaa8b1be044a20c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 10:22:57 -0500 Subject: [PATCH 612/652] Bump cachix/install-nix-action from 21 to 22 (#648) --- .github/workflows/ci.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index d26de3e70..782b3eceb 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@v21 + - uses: cachix/install-nix-action@v22 - 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@v21 + - uses: cachix/install-nix-action@v22 - run: nix-env -f default.nix -iA spellcheck - run: postgrest-docs-spellcheck @@ -34,7 +34,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v21 + - uses: cachix/install-nix-action@v22 - run: nix-env -f default.nix -iA dictcheck - run: postgrest-docs-dictcheck @@ -44,7 +44,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v21 + - uses: cachix/install-nix-action@v22 - run: nix-env -f default.nix -iA linkcheck - run: postgrest-docs-linkcheck From 9780790cee18866f41795ab17ec2adaa016b4303 Mon Sep 17 00:00:00 2001 From: Joonas Date: Wed, 21 Jun 2023 12:09:11 +0300 Subject: [PATCH 613/652] Change example getallfilms to stable instead of immutable --- docs/references/api/resource_embedding.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/api/resource_embedding.rst b/docs/references/api/resource_embedding.rst index 392655a8d..1507fd841 100644 --- a/docs/references/api/resource_embedding.rst +++ b/docs/references/api/resource_embedding.rst @@ -742,7 +742,7 @@ Here's a sample function (notice the ``RETURNS SETOF films``). CREATE FUNCTION getallfilms() RETURNS SETOF films AS $$ SELECT * FROM films; - $$ LANGUAGE SQL IMMUTABLE; + $$ LANGUAGE SQL STABLE; A request with ``directors`` embedded: From 14de9f1dfd1d3dc974f96d2086727ab1893ccadd Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Thu, 22 Jun 2023 09:14:23 -0500 Subject: [PATCH 614/652] references: add computed fields page (#650) --- docs/references/api.rst | 7 +- docs/references/api/computed_fields.rst | 86 ++++++++++++++++++++++++ docs/references/api/tables_views.rst | 89 +++++++++---------------- 3 files changed, 122 insertions(+), 60 deletions(-) create mode 100644 docs/references/api/computed_fields.rst diff --git a/docs/references/api.rst b/docs/references/api.rst index 2a6479781..c156ffa67 100644 --- a/docs/references/api.rst +++ b/docs/references/api.rst @@ -12,9 +12,10 @@ PostgREST exposes three database objects of a schema as resources: tables, views api/tables_views.rst api/stored_procedures.rst api/schemas.rst + api/computed_fields.rst api/resource_embedding.rst - api/openapi.rst api/resource_representation.rst + api/openapi.rst api/* .. raw:: html @@ -34,7 +35,7 @@ PostgREST exposes three database objects of a schema as resources: tables, views '#casting-columns': 'api/tables_views.html#casting-columns', '#json-columns': 'api/tables_views.html#json-columns', '#composite-array-columns': 'api/tables_views.html#composite-array-columns', - '#computed-virtual-columns': 'api/tables_views.html#computed-virtual-columns', + '#computed-virtual-columns': 'api/computed_fields.html#computed-fields', '#ordering': 'api/tables_views.html#ordering', '#limits-and-pagination': 'api/tables_views.html#limits-and-pagination', '#exact-count': 'api/tables_views.html#exact-count', @@ -115,4 +116,4 @@ PostgREST exposes three database objects of a schema as resources: tables, views if (willRedirectTo) { window.location.href = willRedirectTo; } - \ No newline at end of file + diff --git a/docs/references/api/computed_fields.rst b/docs/references/api/computed_fields.rst new file mode 100644 index 000000000..194a48f3c --- /dev/null +++ b/docs/references/api/computed_fields.rst @@ -0,0 +1,86 @@ +.. _computed_cols: + +Computed Fields +############### + +Computed fields are virtual columns that are not stored in a table. PostgreSQL makes it possible to implement them using functions on table types. + +.. code-block:: postgres + + CREATE TABLE people ( + first_name text + , last_name text + , job text + ); + + -- a computed field that combines data from two columns + CREATE FUNCTION full_name(people) + RETURNS text AS $$ + SELECT $1.first_name || ' ' || $1.last_name; + $$ LANGUAGE SQL; + +:ref:`h_filter` can be applied to computed fields. For example, we can do a :ref:`fts` on :code:`full_name`: + +.. code-block:: postgres + + -- (optional) you can add an index on the computed field to speed up the query + CREATE INDEX people_full_name_idx ON people + USING GIN (to_tsvector('english', full_name(people))); + +.. tabs:: + + .. code-tab:: http + + GET /people?full_name=fts.Beckett HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?full_name=fts.Beckett" + +.. code-block:: json + + [ + {"first_name": "Samuel", "last_name": "Beckett", "job": "novelist"} + ] + + +Computed fields won't appear on the response by default but you can use :ref:`v_filter` to include them: + +.. tabs:: + + .. code-tab:: http + + GET /people?select=full_name,job HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?select=full_name,job" + +.. code-block:: json + + [ + {"full_name": "Samuel Beckett", "job": "novelist"} + ] + + +:ref:`ordering` on computed fields is also possible: + +.. tabs:: + + .. code-tab:: http + + GET /people?order=full_name.desc HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/people?order=full_name.desc" + +.. 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. When placing the computed column in the :ref:`exposed schema ` you can use an **unnamed** parameter, as in the example above, to prevent it from being exposed as an :ref:`RPC ` under ``/rpc``. + +.. note:: + + - PostgreSQL 12 introduced `generated columns `_, which can also compute a value based on other columns. However they're stored, not virtual. + - "computed fields" are documented on https://www.postgresql.org/docs/current/rowtypes.html#ROWTYPES-USAGE (search for "computed fields") + - On previous PostgREST versions this feature was documented with the name of "computed columns". diff --git a/docs/references/api/tables_views.rst b/docs/references/api/tables_views.rst index 989c1f8d8..fa796c813 100644 --- a/docs/references/api/tables_views.rst +++ b/docs/references/api/tables_views.rst @@ -31,8 +31,8 @@ There are no deeply/nested/routes. Each route provides OPTIONS, GET, HEAD, POST, .. _h_filter: -Horizontal Filtering (Rows) ---------------------------- +Horizontal Filtering +-------------------- You can filter result rows by adding conditions on columns. For instance, to return people aged under 13 years old: @@ -249,10 +249,10 @@ Using `websearch_to_tsquery` requires PostgreSQL of version at least 11.0 and wi .. _v_filter: -Vertical Filtering (Columns) ----------------------------- +Vertical Filtering +------------------ -When certain columns are wide (such as those holding binary data), it is more efficient for the server to withhold them in a response. The client can specify which columns are required using the 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 :code:`select` parameter. .. tabs:: @@ -455,57 +455,6 @@ The arrow operators(``->``, ``->>``) can also be used for accessing composite fi CREATE INDEX ON mytable ((to_jsonb(data) -> 'identification' ->> 'registration_number')); -.. _computed_cols: - -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: - -.. code-block:: postgres - - 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', full_name(people))); - -A full-text search on the computed column: - -.. tabs:: - - .. code-tab:: http - - 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: - -.. tabs:: - - .. code-tab:: http - - GET /people?select=*,full_name HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/people?select=*,full_name" - -.. 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. 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``. - - .. _ordering: Ordering @@ -557,7 +506,17 @@ 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 sort by nested fields of :ref:`json_columns` with the JSON operators. +You can also sort on fields of :ref:`composite_array_columns` or :ref:`json_columns`. + +.. tabs:: + + .. code-tab:: http + + GET /countries?order=location->>lat HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/countries?order=location->>lat" .. _limits: @@ -1142,3 +1101,19 @@ Using ``offset`` to target a different subset of rows is also possible. 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 `_. +.. raw:: html + + From 991b151b6907d3c02680e12d2bb250d285d7d7b8 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Thu, 22 Jun 2023 14:44:26 -0500 Subject: [PATCH 615/652] improve connection pool wording --- docs/references/connection_pool.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/connection_pool.rst b/docs/references/connection_pool.rst index c1ff099bd..7464f9072 100644 --- a/docs/references/connection_pool.rst +++ b/docs/references/connection_pool.rst @@ -22,7 +22,7 @@ Connection lifetime ------------------- Long-lived PostgreSQL connections can consume considerable memory (see `here `_ for more details). -Under a busy system, the :ref:`db-pool-max-idletime` won't be reached and the connection pool can have many long-lived connections. +Under a busy system, the :ref:`db-pool-max-idletime` won't be reached and the connection pool can be full of long-lived connections. To avoid this problem and save resources, a connection max lifetime (determined by :ref:`db-pool-max-lifetime`, 30 minutes by default) is enforced. After the max lifetime is reached, connections from the pool will be released and new ones will be created. This doesn't affect running requests, only unused connections will be released. From 06a389810703034bc34f72f828cc9665dd376b8a Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 26 Jun 2023 17:30:28 -0500 Subject: [PATCH 616/652] nix: add HISTFILE --- .gitignore | 1 + shell.nix | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index ccba409df..bab47b52e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ Pipfile.lock diagrams/db.pdf misspellings unuseddict +.history diff --git a/shell.nix b/shell.nix index 4800d62ec..e8d38d96e 100644 --- a/shell.nix +++ b/shell.nix @@ -15,4 +15,8 @@ pkgs.mkShell { docs.dictcheck docs.linkcheck ]; + + shellHook = '' + export HISTFILE=.history + ''; } From d59f841d15993c5f865737e427625ad54e7c398c Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 26 Jun 2023 17:47:01 -0500 Subject: [PATCH 617/652] add request body handling --- .../api/resource_representation.rst | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/references/api/resource_representation.rst b/docs/references/api/resource_representation.rst index c11d8e872..8a1f80529 100644 --- a/docs/references/api/resource_representation.rst +++ b/docs/references/api/resource_representation.rst @@ -150,3 +150,24 @@ Example 3: If the stored procedure returns non-scalar values, you need to do a : .. note:: If more than one row would be returned the binary/plain-text/xml results will be concatenated with no delimiter. + +.. _req_body: + +Request Body +============ + +The server handles the following request body media types: + +* ``application/json`` +* ``application/x-www-form-urlencoded`` +* ``text/csv`` + +For :ref:`tables_views` this works on ``POST``, ``PATCH`` and ``PUT`` methods. For :ref:`s_procs`, it works on ``POST`` methods. + +For stored procedures there are three additional types: + +* ``application/octet-stream`` +* ``text/plain`` +* ``text/xml`` + +See :ref:`s_proc_single_unnamed`. From 3a41d55be6b36a16955c497492192ee7fbd30a7d Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Tue, 27 Jun 2023 15:26:53 -0500 Subject: [PATCH 618/652] Fix inserting with defaults --- docs/references/api/tables_views.rst | 39 +++++++++++++++++++--------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/docs/references/api/tables_views.rst b/docs/references/api/tables_views.rst index fa796c813..5a8ca92e6 100644 --- a/docs/references/api/tables_views.rst +++ b/docs/references/api/tables_views.rst @@ -853,21 +853,36 @@ Having: A request: -.. code-block:: http +.. tabs:: - POST /foo?columns=id,bar,baz HTTP/1.1 - Content-Type: application/json - Prefer: return=representation + .. code-tab:: http -.. code-block:: json + POST /foo?columns=id,bar,baz HTTP/1.1 + Content-Type: application/json + Prefer: missing=default, return=representation - [ - { "bar": "val1" - } - , { "bar": "val2" - , "baz": 15 - } - ] + [ + { "bar": "val1" + } + , { "bar": "val2" + , "baz": 15 + } + ] + + .. code-tab:: bash Curl + + curl "http://localhost:3000/foo?columns=id,bar,baz" \ + -H "Content-Type: application/json" \ + -H "Prefer: missing=default, return=representation" \ + -d @- << EOF + [ + { "bar": "val1" + } + , { "bar": "val2" + , "baz": 15 + } + ] + EOF Will result in: From 1a9e105227f4e724814d57b4fe2f98c9dd803d6b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 28 Jun 2023 12:12:27 -0500 Subject: [PATCH 619/652] add headings to computed fields --- docs/references/api/computed_fields.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/references/api/computed_fields.rst b/docs/references/api/computed_fields.rst index 194a48f3c..5379d51d5 100644 --- a/docs/references/api/computed_fields.rst +++ b/docs/references/api/computed_fields.rst @@ -19,6 +19,9 @@ Computed fields are virtual columns that are not stored in a table. PostgreSQL m SELECT $1.first_name || ' ' || $1.last_name; $$ LANGUAGE SQL; +Horizontal Filtering on Computed Fields +======================================= + :ref:`h_filter` can be applied to computed fields. For example, we can do a :ref:`fts` on :code:`full_name`: .. code-block:: postgres @@ -43,6 +46,8 @@ Computed fields are virtual columns that are not stored in a table. PostgreSQL m {"first_name": "Samuel", "last_name": "Beckett", "job": "novelist"} ] +Vertical Filtering on Computed Fields +===================================== Computed fields won't appear on the response by default but you can use :ref:`v_filter` to include them: @@ -62,6 +67,8 @@ Computed fields won't appear on the response by default but you can use :ref:`v_ {"full_name": "Samuel Beckett", "job": "novelist"} ] +Ordering on Computed Fields +=========================== :ref:`ordering` on computed fields is also possible: From ab8ccf43a62151225d3d3d496743a75d0cfcc851 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Sun, 2 Jul 2023 14:51:47 -0500 Subject: [PATCH 620/652] add domain representations (#655) * add domain representations * Update docs/references/api/domain_representations.rst Co-authored-by: Alexander Ljungberg * Update docs/references/api/domain_representations.rst Co-authored-by: Alexander Ljungberg * Update docs/references/api/domain_representations.rst Co-authored-by: Alexander Ljungberg * Update docs/references/api/domain_representations.rst Co-authored-by: Alexander Ljungberg * Update docs/references/api/domain_representations.rst Co-authored-by: Alexander Ljungberg * Update docs/references/api/domain_representations.rst Co-authored-by: Alexander Ljungberg * add custom domain subheading and TOC this way it can be seen at the beggining there are advantages over views and computed fields. * important note for scache reloading * clarify advantages * why not create a type? --------- Co-authored-by: Alexander Ljungberg --- docs/references/api.rst | 1 + .../references/api/domain_representations.rst | 200 ++++++++++++++++++ postgrest.dict | 1 + 3 files changed, 202 insertions(+) create mode 100644 docs/references/api/domain_representations.rst diff --git a/docs/references/api.rst b/docs/references/api.rst index c156ffa67..0796ba1af 100644 --- a/docs/references/api.rst +++ b/docs/references/api.rst @@ -13,6 +13,7 @@ PostgREST exposes three database objects of a schema as resources: tables, views api/stored_procedures.rst api/schemas.rst api/computed_fields.rst + api/domain_representations.rst api/resource_embedding.rst api/resource_representation.rst api/openapi.rst diff --git a/docs/references/api/domain_representations.rst b/docs/references/api/domain_representations.rst new file mode 100644 index 000000000..f303f6cf6 --- /dev/null +++ b/docs/references/api/domain_representations.rst @@ -0,0 +1,200 @@ +.. _domain_reps: + +Domain Representations +###################### + +Domain Representations separates "how the data is presented" from "how the data is stored". It works by creating `domains `_ and `casts `_, the latter act on the former to present and receive the data in different formats. + +.. contents:: + :depth: 1 + :local: + :backlinks: none + +Custom Domain +============= + +Suppose you want to use a ``uuid`` type for a primary key and want to present it shortened to web users. + +For this, let's create a domain based on ``uuid``. + +.. code-block:: postgres + + create domain app_uuid as uuid; + + -- and use it as our table PK. + create table profiles( + id app_uuid + , name text + ); + + -- some data for the example + insert into profiles values ('846c4ffd-92ce-4de7-8d11-8e29929f4ec4', 'John Doe'); + +Domain Response Format +====================== + +We can shorten the ``uuid`` with ``base64`` encoding. Let's use JSON as our response format for this example. + +To change the domain format for JSON, create a function that converts ``app_uuid`` to ``json``. + +.. code-block:: postgres + + -- the name of the function is arbitrary + CREATE OR REPLACE FUNCTION json(app_uuid) RETURNS json AS $$ + select to_json(encode(uuid_send($1),'base64')); + $$ LANGUAGE SQL IMMUTABLE; + + -- check it works + select json('846c4ffd-92ce-4de7-8d11-8e29929f4ec4'::app_uuid); + json + ---------------------------- + "hGxP/ZLOTeeNEY4pkp9OxA==" + +Then create a CAST to tell PostgREST to convert it automatically whenever a JSON response is requested. + +.. code-block:: postgres + + CREATE CAST (app_uuid AS json) WITH FUNCTION json(app_uuid) AS IMPLICIT; + +With this you can obtain the data in the shortened format. + +.. tabs:: + + .. code-tab:: http + + GET /profiles HTTP/1.1 + Accept: application/json + + .. code-tab:: bash Curl + + curl "http://localhost:3000/profiles" \ + -H "Accept: application/json" + +.. code-block:: json + + [{"id":"hGxP/ZLOTeeNEY4pkp9OxA==","name":"John Doe"}] + +.. note:: + + - Casts on domains are ignored by PostgreSQL, their interpretation is left to the application. We're discussing the possibility of including the Domain Representations behavior on `pgsql-hackers `_. + - It would make more sense to use ``base58`` encoding as it's URL friendly but for simplicity we use ``base64`` (supported natively in PostgreSQL). + +.. important:: + + After creating a cast over a domain, you must refresh PostgREST schema cache. See :ref:`schema_reloading`. + +Domain Filter Format +==================== + +For :ref:`h_filter` to work with the shortened format, you need a different conversion. + +PostgREST considers the URL query string to be, in the most generic sense, ``text``. So let's create a function that converts ``text`` to ``app_uuid``. + +.. code-block:: postgres + + -- the name of the function is arbitrary + CREATE OR REPLACE FUNCTION app_uuid(text) RETURNS app_uuid AS $$ + select substring(decode($1,'base64')::text from 3)::uuid; + $$ LANGUAGE SQL IMMUTABLE; + + -- plus a CAST to tell PostgREST to use this function + CREATE CAST (text AS app_uuid) WITH FUNCTION app_uuid(text) AS IMPLICIT; + +Now you can filter as usual. + +.. tabs:: + + .. code-tab:: http + + GET /profiles?id=eq.hGxP/ZLOTeeNEY4pkp9OxA== HTTP/1.1 + Accept: application/json + + .. code-tab:: bash Curl + + curl "http://localhost:3000/profiles?id=eq.ZLOTeeNEY4pkp9OxA==" \ + -H "Accept: application/json" + +.. code-block:: json + + [{"id":"hGxP/ZLOTeeNEY4pkp9OxA==","name":"John Doe"}] + +.. note:: + + If there's no CAST from ``text`` to ``app_uuid`` defined, the filter will still work with the native uuid format (``846c4ffd-92ce-4de7-8d11-8e29929f4ec4``). + +Domain Request Body Format +========================== + +To accept the shortened format in a JSON request body, for example when creating a new record, define a ``json`` to ``app_uuid`` conversion. + +.. code-block:: postgres + + -- the name of the function is arbitrary + CREATE OR REPLACE FUNCTION app_uuid(json) RETURNS public.app_uuid AS $$ + -- here we reuse the previous app_uuid(text) function + select app_uuid($1 #>> '{}'); + $$ LANGUAGE SQL IMMUTABLE; + + CREATE CAST (json AS public.app_uuid) WITH FUNCTION app_uuid(json) AS IMPLICIT; + +Now we can :ref:`insert` (or :ref:`update`) as usual. + +.. tabs:: + + .. code-tab:: http + + POST /profiles HTTP/1.1 + Content-Type: application/json + Prefer: return=representation + + {"id":"zH7HbFJUTfy/GZpwuirpuQ==","name":"Jane Doe"} + + .. code-tab:: bash Curl + + curl "http://localhost:3000/profiles" \ + -H "Prefer: return=representation" \ + -H "Content-Type: application/json" \ + -d @- <`_ also allow us to change the format of the underlying type. However they come with drawbacks that increase complexity. + +1) Formatting the column in the view makes it `non-updatable `_ since Postgres doesn't know how to reverse the transform. This can be worked around using INSTEAD OF triggers. +2) When filtering by this column, we get full table scans for the same reason (also applies to :ref:`computed_cols`) . The performance loss here can be avoided with a computed index, or using a materialized generated column. +3) If the formatted column is used as a foreign key, PostgREST can no longer detect that relationship and :ref:`resource_embedding` breaks. This can be worked around with :ref:`computed_relationships`. + +Domain Representations avoid all the above drawbacks. Their only drawback is that for existing tables, you have to change the column types. But this should be a fast operation since domains are binary coercible with their underlying types. A table rewrite won't be required. + +.. note:: + + Why not create a `base type `_ instead? ``CREATE TYPE app_uuid (INTERNALLENGTH = 22, INPUT = app_uuid_parser, OUTPUT = app_uuid_formatter)``. + + Creating base types need superuser, which is restricted on cloud hosted databases. Additionally this way lets “how the data is presented” dictate “how the data is stored” which would be backwards. diff --git a/postgrest.dict b/postgrest.dict index 52956e941..bf07b7b20 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -16,6 +16,7 @@ Cardano cd centric changelog +coercible Cloudfare config CORS From 4706472df96913479231bd28f56892690d98750d Mon Sep 17 00:00:00 2001 From: Grant <109616836+4dex@users.noreply.github.com> Date: Wed, 5 Jul 2023 09:48:47 -0600 Subject: [PATCH 621/652] Update connection_pool.rst typo --- docs/references/connection_pool.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/connection_pool.rst b/docs/references/connection_pool.rst index 7464f9072..aa0a904f6 100644 --- a/docs/references/connection_pool.rst +++ b/docs/references/connection_pool.rst @@ -5,7 +5,7 @@ Connection Pool A connection pool is a cache of reusable database connections. It allows serving many HTTP requests using few database connections. Every request to an :doc:`API resource ` borrows a connection from the pool to start a :doc:`transaction `. -Minimizing connections it’s paramount to performance. Each PostgreSQL connection creates a process, having too many can exhaust available resources. +Minimizing connections is paramount to performance. Each PostgreSQL connection creates a process, having too many can exhaust available resources. .. _pool_growth_limit: .. _dyn_conn_pool: From c74b9c4efa834f31e45e819dd33fb7dadd921c90 Mon Sep 17 00:00:00 2001 From: Steve Chavez Date: Mon, 10 Jul 2023 17:04:23 -0500 Subject: [PATCH 622/652] add version querying (#657) --- docs/references/admin.rst | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/references/admin.rst b/docs/references/admin.rst index 6e00eb3f9..33110028c 100644 --- a/docs/references/admin.rst +++ b/docs/references/admin.rst @@ -19,6 +19,7 @@ For diagnostic information about the server itself, PostgREST logs to ``stderr`` .. code:: + 12/Jun/2021:17:47:39 -0500: Starting PostgREST 11.1.0... 12/Jun/2021:17:47:39 -0500: Attempting to connect to the database... 12/Jun/2021:17:47:39 -0500: Listening on port 3000 12/Jun/2021:17:47:39 -0500: Connection successful @@ -83,12 +84,34 @@ Restart the database and watch the log file in real-time to understand how HTTP Server Version -------------- -When debugging a problem it's important to verify the PostgREST version. Look for the :code:`Server` HTTP response header, which contains the version number. +When debugging a problem it's important to verify the running PostgREST version. There are three ways to do this: + +- Look for the :code:`Server` HTTP response header that is returned on every request. .. code:: + HEAD /users HTTP/1.1 + Server: postgrest/11.0.1 +- Query ``application_name`` on `pg_stat_activity `_. + +.. code-block:: psql + + select distinct application_name + from pg_stat_activity + where application_name ilike '%postgrest%'; + + application_name + ------------------------------ + PostgREST 11.1.0 + +.. note:: + + The server sets the `fallback_application_name `_ for this query to work. To override the value set ``application_name`` on the connection string. + +- The ``stderr`` logs also contain the version, as noted on :ref:`pgrst_logging`. + .. _trace_header: Trace Header From a6aa2adf04d54a7c77f78a46fe763fc9bede4cca Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Thu, 20 Jul 2023 09:02:33 -0500 Subject: [PATCH 623/652] Remove embedding disambiguation in favor of computed rels (#660) * Add SQL snippets for ERDs --------- Co-authored-by: steve-chavez --- diagrams/employees.er | 7 + diagrams/presidents.er | 7 + docs/_static/employees.png | Bin 0 -> 8244 bytes docs/_static/presidents.png | Bin 0 -> 8331 bytes docs/references/api/resource_embedding.rst | 605 +++++++++++++-------- docs/references/errors.rst | 2 +- postgrest.dict | 1 + 7 files changed, 393 insertions(+), 229 deletions(-) create mode 100644 diagrams/employees.er create mode 100644 diagrams/presidents.er create mode 100644 docs/_static/employees.png create mode 100644 docs/_static/presidents.png diff --git a/diagrams/employees.er b/diagrams/employees.er new file mode 100644 index 000000000..7010a670e --- /dev/null +++ b/diagrams/employees.er @@ -0,0 +1,7 @@ +[Employees] +*id +first_name +last_name ++supervisor_id + +Employees 1--* Employees diff --git a/diagrams/presidents.er b/diagrams/presidents.er new file mode 100644 index 000000000..b49100961 --- /dev/null +++ b/diagrams/presidents.er @@ -0,0 +1,7 @@ +[Presidents] +*id +first_name +last_name ++predecessor_id + +Presidents 1--? Presidents diff --git a/docs/_static/employees.png b/docs/_static/employees.png new file mode 100644 index 0000000000000000000000000000000000000000..0a72df2a28ccb7c5042db28cdbbc6a0a158874a1 GIT binary patch literal 8244 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 S>|kJEVDNPHb6Mw<&;$VX&=Ii! literal 0 HcmV?d00001 diff --git a/docs/_static/presidents.png b/docs/_static/presidents.png new file mode 100644 index 0000000000000000000000000000000000000000..2164ff3de19ead9700797c517a93ec2486dbdbf4 GIT binary patch literal 8331 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 literal 0 HcmV?d00001 diff --git a/docs/references/api/resource_embedding.rst b/docs/references/api/resource_embedding.rst index 1507fd841..be4ea15f2 100644 --- a/docs/references/api/resource_embedding.rst +++ b/docs/references/api/resource_embedding.rst @@ -1,7 +1,7 @@ .. _resource_embedding: Resource Embedding -================== +################## PostgREST allows including related resources in a single API call. This reduces the need for many API requests. @@ -11,12 +11,13 @@ PostgREST allows including related resources in a single API call. This reduces .. important:: - Whenever foreign keys change you must do :ref:`schema_reloading` for this feature to work. + - PostgREST respects composite foreign keys. + - Whenever foreign keys change you must do :ref:`schema_reloading` for this feature to work. .. _many-to-one: Many-to-one relationships -------------------------- +========================= Since ``films`` has a **foreign key** to ``directors``, this establishes a many-to-one relationship. Thus, we're able to request all the films and the director for each film. @@ -82,7 +83,7 @@ Since the table name is plural, we can be more accurate by making it singular wi .. _one-to-many: One-to-many relationships -------------------------- +========================= The **foreign key reference** establishes the inverse one-to-many relationship. In this case, ``films`` returns as a JSON array because of the “to-many” end. @@ -119,7 +120,7 @@ The **foreign key reference** establishes the inverse one-to-many relationship. .. _many-to-many: Many-to-many relationships --------------------------- +========================== The join table determines many-to-many relationships. It must contain foreign keys to other two tables and they must be part of its composite key. @@ -167,11 +168,11 @@ For the many-to-many relationship between ``films`` and ``actors``, the join tab .. _one-to-one: One-to-one relationships ------------------------- +======================== -One-to-one relationships are detected when: +One-to-one relationships are detected in two ways. -- The foreign key has a unique constraint. +- When the foreign key has a unique constraint. .. code-block:: postgresql @@ -182,7 +183,7 @@ One-to-one relationships are detected when: sound TEXT ); -- The foreign key is a primary key. +- When the foreign key is a primary key. .. code-block:: postgresql @@ -216,12 +217,10 @@ One-to-one relationships are detected when: .. _computed_relationships: -Computed relationships ----------------------- +Computed Relationships +====================== -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`. +You can manually define relationships between resources using functions. This is useful for database objects that can't define foreign keys, like `Foreign Data Wrappers `_. Assuming there's a foreign table ``premieres`` that we want to relate to ``films``. @@ -292,6 +291,9 @@ We consider any value greater than 1 as "many" so this defines a one-to-many rel ".." ] +Overriding Relationships +------------------------ + Computed relationships also allow you to override the ones that PostgREST auto-detects. For example, to override the :ref:`many-to-one relationship ` between ``films`` and ``directors``. @@ -318,10 +320,359 @@ Computed relationships have good performance as their intended design enable `in - 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, it will unnest the embedding and return repeated values for the top level resource. +.. _embed_disamb: +.. _hint_disamb: +.. _target_disamb: +.. _complex_rels: + +Complex Relationships +===================== + +As mentioned on :ref:`resource_embedding`, the server does joins based on **Foreign Keys**. +When there are many foreign keys between tables, it needs disambiguation to resolve which foreign key columns to use for the join. + +:ref:`computed_relationships` can do the job here, they can choose join columns arbitrarily. + +.. note:: + + Previous versions addressed complex relationships with `Embedding disambiguation `_ but this is now deprecated. Follow the solutions in this section when a ``300 Multiple Choices`` error is returned. + +.. _multiple_m2o: + +Multiple Many-To-One +-------------------- + +.. tabs:: + + .. group-tab:: ERD + + .. image:: ../../_static/orders.png + + .. code-tab:: postgresql SQL + + create table addresses ( + id int primary key generated always as identity, + name text, + city text, + state text, + postal_code char(5) + ); + + create table orders ( + id int primary key generated always as identity, + name text, + billing_address_id int references addresses(id), + shipping_address_id int references addresses(id) + ); + +To successfully embed ``orders`` with ``addresses``, you need to create computed relationships for the foreign keys columns you want to use: + +.. code-block:: postgresql + + create function billing_address(orders) returns setof addresses rows 1 as $$ + select * from addresses where id = $1.billing_address_id + $$ stable language sql; + + create function shipping_address(orders) returns setof addresses rows 1 as $$ + select * from addresses where id = $1.shipping_address_id + $$ stable language sql; + +Now, we can unambiguously embed the billing and shipping addresses. + +.. tabs:: + + .. code-tab:: http + + GET /orders?select=name,billing_address(name),shipping_address(name) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/orders?select=name,billing_address(name),shipping_address(name)" + +.. code-block:: json + + [ + { + "name": "Personal Water Filter", + "billing_address": { + "name": "32 Glenlake Dr.Dearborn, MI 48124" + }, + "shipping_address": { + "name": "30 Glenlake Dr.Dearborn, MI 48124" + } + } + ] + +.. _multiple_o2m: + +Multiple One-To-Many +-------------------- + +Let's take the tables from :ref:`multiple_m2o`. +To embed ``addresses`` with ``orders``, you need to create computed relationships like these ones: + +.. code-block:: postgresql + + create function billing_orders(addresses) returns setof orders as $$ + select * from orders where billing_address_id = $1.id + $$ stable language sql; + + create function shipping_orders(addresses) returns setof orders as $$ + select * from orders where shipping_address_id = $1.id + $$ stable language sql; + +Then, the request would look like: + +.. tabs:: + + .. code-tab:: http + + GET /addresses?select=name,billing_orders(name),shipping_orders(name)&id=eq.1 HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/addresses?select=name,billing_orders(name),shipping_orders(name)&id=eq.1" + +.. code-block:: json + + [ + { + "name": "32 Glenlake Dr.Dearborn, MI 48124", + "billing_orders": [ + { "name": "Personal Water Filter" }, + { "name": "Coffee Machine" } + ], + "shipping_orders": [ + { "name": "Coffee Machine" } + ] + } + ] + +.. _recursive_o2o_embed: + +Recursive One-To-One +-------------------- + +.. tabs:: + + .. group-tab:: ERD + + .. image:: ../../_static/presidents.png + + .. code-tab:: postgresql SQL + + create table presidents ( + id int primary key generated always as identity, + first_name text, + last_name text, + predecessor_id int references presidents(id) unique + ); + +To get either side of the Recursive One-To-One relationship, create the functions: + +.. code-block:: postgresql + + create or replace function predecessor(presidents) returns setof presidents rows 1 as $$ + select * from presidents where id = $1.predecessor_id + $$ stable language sql; + + create or replace function successor(presidents) returns setof presidents rows 1 as $$ + select * from presidents where predecessor_id = $1.id + $$ stable language sql; + +Now, to query a president with their predecessor and successor: + +.. tabs:: + + .. code-tab:: http + + GET /presidents?select=last_name,predecessor(last_name),successor(last_name)&id=eq.2 HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/presidents?select=last_name,predecessor(last_name),successor(last_name)&id=eq.2" + +.. code-block:: json + + [ + { + "last_name": "Adams", + "predecessor": { + "last_name": "Washington" + }, + "successor": { + "last_name": "Jefferson" + } + } + ] + +.. _recursive_o2m_embed: + +Recursive One-To-Many +--------------------- + +.. tabs:: + + .. group-tab:: ERD + + .. image:: ../../_static/employees.png + + .. code-tab:: postgresql SQL + + create table employees ( + id int primary key generated always as identity, + first_name text, + last_name text, + supervisor_id int references employees(id) + ); + +To get the One-To-Many embedding, that is, the supervisors with their supervisees, create a function like this one: + +.. code-block:: postgresql + + create or replace function supervisees(employees) returns setof employees as $$ + select * from employees where supervisor_id = $1.id + $$ stable language sql; + +Now, the query would be: + +.. tabs:: + + .. code-tab:: http + + GET /employees?select=last_name,supervisees(last_name)&id=eq.1 HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/employees?select=last_name,supervisees(last_name)&id=eq.1" + +.. code-block:: json + + [ + { + "name": "Taylor", + "supervisees": [ + { "name": "Johnson" }, + { "name": "Miller" } + ] + } + ] + +.. _recursive_m2o_embed: + +Recursive Many-To-One +---------------------- + +Let's take the same ``employees`` table from :ref:`recursive_o2m_embed`. +To get the Many-To-One relationship, that is, the employees with their respective supervisor, you need to create a function like this one: + +.. code-block:: postgresql + + create or replace function supervisor(employees) returns setof employees rows 1 as $$ + select * from employees where id = $1.supervisor_id + $$ stable language sql; + +Then, the query would be: + +.. tabs:: + + .. code-tab:: http + + GET /employees?select=last_name,supervisor(last_name)&id=eq.3 HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/employees?select=last_name,supervisor(last_name)&id=eq.3" + +.. code-block:: json + + [ + { + "last_name": "Miller", + "supervisor": { + "last_name": "Taylor" + } + } + ] + +.. _recursive_m2m_embed: + +Recursive Many-To-Many +---------------------- + +.. tabs:: + + .. group-tab:: ERD + + .. image:: ../../_static/users.png + + .. code-tab:: postgresql SQL + + create table users ( + id int primary key generated always as identity, + first_name text, + last_name text, + username text unique + ); + + create table subscriptions ( + subscriber_id int references users(id), + subscribed_id int references users(id), + type text, + primary key (subscriber_id, subscribed_id) + ); + +To get all the subscribers of a user as well as the ones they're following, define these functions: + +.. code-block:: postgresql + + create or replace function subscribers(users) returns setof users as $$ + select u.* + from users u, + subscriptions s + where s.subscriber_id = u.id and + s.subscribed_id = $1.id + $$ stable language sql; + + create or replace function following(users) returns setof users as $$ + select u.* + from users u, + subscriptions s + where s.subscribed_id = u.id and + s.subscriber_id = $1.id + $$ stable language sql; + +Then, the request would be: + +.. tabs:: + + .. code-tab:: http + + GET /users?select=username,subscribers(username),following(username)&id=eq.4 HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/users?select=username,subscribers(username),following(username)&id=eq.4" + +.. code-block:: json + + [ + { + "username": "the_top_artist", + "subscribers": [ + { "username": "patrick109" }, + { "username": "alicia_smith" } + ], + "following": [ + { "username": "top_streamer" } + ] + } + ] + .. _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: @@ -338,7 +689,7 @@ If you want to embed through join tables but need more control on the intermedia .. _embed_filters: 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: @@ -419,7 +770,7 @@ The result will show the nested actors named Tom and order them by last name. Al .. _embedding_top_level_filter: Top-level Filtering -------------------- +=================== By default, :ref:`embed_filters` don't change the top-level resource(``films``) rows at all: @@ -484,7 +835,7 @@ In order to filter the top level rows you need to add ``!inner`` to the embedded .. _null_embed: Null filtering on Embedded Resources -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +------------------------------------ Null filtering on the embedded resources can behave the same as ``!inner``. While providing more flexibility. @@ -528,7 +879,7 @@ Both ``is.null`` and ``not.is.null`` can be included inside the `or` operator. F .. _empty_embed: Empty Embed -~~~~~~~~~~~ +----------- You can leave an embedded resource empty, this helps with filtering in some cases. @@ -555,7 +906,7 @@ To filter the films by actors but not include them: .. _top_level_order: Top-level Ordering ------------------- +================== On :ref:`Many-to-One ` and :ref:`One-to-One ` relationships, you can use a column of the "to-one" end to sort the top-level. @@ -574,7 +925,7 @@ For example, to arrange the films in descending order using the director's last .. _spread_embed: Spread embedded resource ------------------------- +======================== On many-to-one and one-to-one relationships, you can "spread" the embedded resource. That is, remove the surrounding JSON object for the embedded resource columns. @@ -633,7 +984,7 @@ You can use this to get the columns of a join table in a many-to-many relationsh .. _embedding_partitioned_tables: Embedding Partitioned Tables ----------------------------- +============================ Embedding can also be done between `partitioned tables `_ and other tables. @@ -679,7 +1030,7 @@ Since it contains the ``films_id`` foreign key, it is possible to embed ``box_of .. _embedding_views: Embedding Views ---------------- +=============== 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. @@ -725,14 +1076,14 @@ It's also possible to embed `Materialized Views ` that returns a table type, you can embed its related resources. @@ -770,7 +1121,7 @@ A request with ``directors`` embedded: .. _mutation_embed: Embedding after Insertions/Updates/Deletions --------------------------------------------- +============================================ You can embed related resources after doing :ref:`insert`, :ref:`update` or :ref:`delete`. @@ -819,205 +1170,3 @@ Response: "last_name": "Boyle" } } - -.. _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 unambiguously -by just specifying the tables names. - -.. _target_disamb: - -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**): - -.. tabs:: - - .. 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: - -.. 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**. - -.. tabs:: - - .. code-tab:: http - - GET /orders?select=name,billing_address(name) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/orders?select=name,billing_address(name)" - -.. code-block:: json - - [ - { - "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. - -.. tabs:: - - .. code-tab:: http - - GET /orders?select=name,billing_address:billing_address_id(name) HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/orders?select=name,billing_address:billing_address_id(name)" - -.. code-block:: json - - [ - { - "name": "Personal Water Filter", - "billing_address": { - "name": "32 Glenlake Dr.Dearborn, MI 48124" - } - } - ] - -.. _hint_disamb: - -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``. - -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=*,central_addresses(*) HTTP/1.1 - - .. code-tab:: bash Curl - - 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 still specify ``central_addresses`` as the **target** and use the ``billing_address`` foreign key as the **hint**: - -.. 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 - -.. code-block:: http - - HTTP/1.1 200 OK - - [ ... ] - -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: - -.. tabs:: - - .. code-tab:: http - - 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" - -.. note:: - - If the relationship is so complex that hint disambiguation does not solve it, you can use :ref:`computed_relationships`. - -.. _recursive_m2m_disamb: - -Recursive Many-To-Many Disambiguation -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Use hints plus the :ref:`spread_embed` to disambiguate a recursive many-to-many relationship. Having the following: - -.. image:: ../../_static/users.png - -Note that ``subscriptions`` has more than one foreign key to ``users``. - -To get all the subscribers of a user: - -.. tabs:: - - .. code-tab:: http - - GET /users?select=username,subscribers:subscriptions!subscribed_id(...users!subscriber_id(username))&id=eq.4 HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/users?select=username,subscribers:subscriptions!subscribed_id(...users!subscriber_id(username))&id=eq.4" - -.. code-block:: json - - [ - { - "username": "the_top_artist", - "subscribers": [ - { "username": "patrick109" }, - { "username": "alicia_smith" } - ] - } - ] - -.. note:: - - We're working on a better interface for recursive relationships to reduce the request verbosity. diff --git a/docs/references/errors.rst b/docs/references/errors.rst index 0c0a40c72..d03db69fb 100644 --- a/docs/references/errors.rst +++ b/docs/references/errors.rst @@ -320,7 +320,7 @@ Related to a :ref:`stale schema cache `. Most of the time, these e | PGRST200 | | exist in the database. | +---------------+-------------+-------------------------------------------------------------+ | .. _pgrst201: | 300 | An ambiguous embedding request was made. | -| | | See :ref:`embed_disamb`. | +| | | See :ref:`complex_rels`. | | PGRST201 | | | +---------------+-------------+-------------------------------------------------------------+ | .. _pgrst202: | 404 | Caused by a stale function signature, otherwise | diff --git a/postgrest.dict b/postgrest.dict index bf07b7b20..685cc788f 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -159,6 +159,7 @@ SSL stateful stdout Stolarz +supervisees SvelteKit SwaggerUI syslog From 4c4a1de832ee07166b773e743150a8605ab1d611 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Mon, 10 Jul 2023 17:57:16 -0500 Subject: [PATCH 624/652] to_jsonb index no longer required on json/jsonb --- docs/references/api/tables_views.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/references/api/tables_views.rst b/docs/references/api/tables_views.rst index 5a8ca92e6..19f651d40 100644 --- a/docs/references/api/tables_views.rst +++ b/docs/references/api/tables_views.rst @@ -449,7 +449,7 @@ The arrow operators(``->``, ``->>``) can also be used for accessing composite fi .. 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: + When using the ``->`` and ``->>`` operators on composite and array columns, 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 From 220683cef001305d0ab456b8d3f8f28f8b1767b9 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Tue, 11 Jul 2023 10:31:51 -0500 Subject: [PATCH 625/652] add HEAD optimization --- docs/references/api/tables_views.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/references/api/tables_views.rst b/docs/references/api/tables_views.rst index 19f651d40..3d4bb9b31 100644 --- a/docs/references/api/tables_views.rst +++ b/docs/references/api/tables_views.rst @@ -685,6 +685,14 @@ If we make a similar request on ``bigtable``, which has 3573458 rows, we would g HTTP/1.1 206 Partial Content Content-Range: 0-24/3572000 +.. _head_req: + +HEAD +---- + +A HEAD method will behave identically to GET except that no body will be returned (`RFC 2616 `_) . +As an optimization, the generated query won't execute an aggregate (to avoid unnecessary data transfer). + .. _update: Update From fefd0e2e83885ea4bce57c750a11668034c16b05 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 20 Jul 2023 23:04:19 -0500 Subject: [PATCH 626/652] dry config and make it responsive * shorten connection pool * move securing execution plan * shorten config intro page --- docs/ecosystem.rst | 2 +- docs/references/admin.rst | 28 +++ docs/references/configuration.rst | 376 ++++++++++++++++++++++------ docs/references/connection_pool.rst | 6 +- postgrest.dict | 4 +- 5 files changed, 335 insertions(+), 81 deletions(-) diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index 11c75a4c9..da7b4eb66 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -18,7 +18,7 @@ Community Tutorials * `REST Data Service on YugabyteDB / PostgreSQL `_ -* `Build data-driven applications with Workers and PostgreSQL `_ - A tutorial on how to integrate with PostgREST and PostgreSQL using Cloudfare Workers. +* `Build data-driven applications with Workers and PostgreSQL `_ - A tutorial on how to integrate with PostgREST and PostgreSQL using Cloudflare Workers. .. _templates: diff --git a/docs/references/admin.rst b/docs/references/admin.rst index 33110028c..34ba883ba 100644 --- a/docs/references/admin.rst +++ b/docs/references/admin.rst @@ -220,6 +220,34 @@ The other available parameters are ``analyze``, ``verbose``, ``settings``, ``buf Note that akin to the EXPLAIN command, the changes will be committed when using the ``analyze`` option. To avoid this, you can use the :ref:`db-tx-end` and the ``Prefer: tx=rollback`` header. +Securing the Execution Plan +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It's recommended to only activate :ref:`db-plan-enabled` on testing environments since it reveals internal database details. +However, if you choose to use it in production you can add a :ref:`db-pre-request` to filter the requests that can use this feature. + +For example, to only allow requests from an IP address to get the execution plans: + +.. code-block:: postgresql + + -- Assuming a proxy(Nginx, Cloudflare, etc) passes an "X-Forwarded-For" header(https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) + create or replace function filter_plan_requests() + returns void as $$ + declare + headers json := current_setting('request.headers', true)::json; + client_ip text := coalesce(headers->>'x-forwarded-for', ''); + accept text := coalesce(headers->>'accept', ''); + begin + if accept like 'application/vnd.pgrst.plan%' and client_ip != '144.96.121.73' then + raise insufficient_privilege using + message = 'Not allowed to use application/vnd.pgrst.plan'; + end if; + end; $$ language plpgsql; + + -- set this function on your postgrest.conf + -- db-pre-request = filter_plan_requests + + .. _health_check: Health Check diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index a0683c774..338e0aa1b 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -3,24 +3,26 @@ Configuration ############# -Config parameters can be provided via :ref:`file_config`, :ref:`env_variables_config` or :ref:`in_db_config`. Using :ref:`config_reloading` you can modify the parameters without restarting the server. +Configuration parameters can be provided via: -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 `. +- :ref:`file_config`. +- :ref:`env_variables_config`, overriding values from the config file. +- :ref:`in_db_config`, overriding values from both the config file and environment variables. -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. +Using :ref:`config_reloading` you can modify the parameters without restarting the server. -Config parameters are read in the following order: -1. From the config file. -2. From environment variables, overriding values from the config file. -3. From the database, overriding values from both the config file and environment variables. +Minimum parameters +================== + +The server is able to start without any config parameters, but it won't be able to serve requests unless it has :ref:`a role to serve anonymous requests with ` - or :ref:`a secret to use for JWT authentication `. .. _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: +There is no predefined location for the config file, you must specify the file path as the one and only argument to the server: .. code:: bash @@ -46,7 +48,7 @@ The configuration file must contain a set of key value pairs: jwt-secret-is-base64 = false # Port the postgrest process is listening on for http requests - server-port = 80 + server-port = 3000 You can run ``postgrest --example`` to display all possible configuration parameters and how to use them in a configuration file. @@ -55,7 +57,9 @@ You can run ``postgrest --example`` to display all possible configuration parame 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.*``. +Environment variables are capitalized, have a ``PGRST_`` prefix, and use underscores. For example: ``PGRST_DB_URI`` corresponds to ``db-uri`` and ``PGRST_APP_SETTINGS_*`` to ``app.settings.*``. + +`libpq environment variables `_ are also supported for constructing the connection string, see :ref:`db-uri`. See the full list of environment variable names on :ref:`config_full_list`. @@ -64,7 +68,7 @@ See the full list of environment variable names on :ref:`config_full_list`. In-Database Configuration ========================= -Using a :ref:`pre-config ` function, you can configure the server with database settings. For example, you can configure :ref:`db-schemas` and :ref:`jwt-secret` like this: +You can also configure the server with database settings by using a :ref:`pre-config ` function. For example, you can configure :ref:`db-schemas` and :ref:`jwt-secret` like this: .. code-block:: @@ -144,51 +148,19 @@ The ``"pgrst"`` notification channel is enabled by default. You can name the cha List of parameters ================== -=========================== ======= ================= ========== ================================= ============================== -Name Type Default Reloadable Environment variable In-database name -=========================== ======= ================= ========== ================================= ============================== -admin-server-port Int PGRST_ADMIN_SERVER_PORT -app.settings.* String Y PGRST_APP_SETTINGS_* -db-anon-role String Y PGRST_DB_ANON_ROLE pgrst.db_anon_role -db-channel String pgrst Y PGRST_DB_CHANNEL -db-channel-enabled Boolean True Y PGRST_DB_CHANNEL_ENABLED -db-config Boolean True Y PGRST_DB_CONFIG -db-pre-config String Y PGRST_DB_PRE_CONFIG pgrst.db_pre_config -db-extra-search-path String public Y PGRST_DB_EXTRA_SEARCH_PATH pgrst.db_extra_search_path -db-max-rows Int ∞ Y PGRST_DB_MAX_ROWS pgrst.db_max_rows -db-plan-enabled Boolean False Y PGRST_DB_PLAN_ENABLED pgrst.db_plan_enabled -db-pool Int 10 PGRST_DB_POOL -db-pool-acquisition-timeout Int 10 PGRST_DB_POOL_ACQUISITION_TIMEOUT -db-pool-max-idletime Int 30 PGRST_DB_POOL_MAX_IDLETIME -db-pool-max-lifetime Int 1800 PGRST_DB_POOL_MAX_LIFETIME -db-pre-request String Y PGRST_DB_PRE_REQUEST pgrst.db_pre_request -db-prepared-statements Boolean True Y PGRST_DB_PREPARED_STATEMENTS pgrst.db_prepared_statements -db-root-spec String Y PGRST_DB_ROOT_SPEC pgrst.db_root_spec -db-schemas String public Y PGRST_DB_SCHEMAS pgrst.db_schemas -db-tx-end String commit PGRST_DB_TX_END -db-uri String postgresql:// PGRST_DB_URI -db-use-legacy-gucs Boolean True Y PGRST_DB_USE_LEGACY_GUCS pgrst.db_use_legacy_gucs -jwt-aud String Y PGRST_JWT_AUD pgrst.jwt_aud -jwt-role-claim-key String .role Y PGRST_JWT_ROLE_CLAIM_KEY pgrst.jwt_role_claim_key -jwt-secret String Y PGRST_JWT_SECRET pgrst.jwt_secret -jwt-secret-is-base64 Boolean False Y PGRST_JWT_SECRET_IS_BASE64 pgrst.jwt_secret_is_base64 -log-level String error PGRST_LOG_LEVEL -openapi-mode String follow-privileges Y PGRST_OPENAPI_MODE pgrst.openapi_mode -openapi-security-active Boolean False Y PGRST_OPENAPI_SECURITY_ACTIVE pgrst.openapi_security_active -openapi-server-proxy-uri String Y PGRST_OPENAPI_SERVER_PROXY_URI pgrst.openapi_server_proxy_uri -raw-media-types String Y PGRST_RAW_MEDIA_TYPES pgrst.raw_media_types -server-host String !4 PGRST_SERVER_HOST -server-port Int 3000 PGRST_SERVER_PORT -server-trace-header String Y PGRST_SERVER_TRACE_HEADER pgrst.server_trace_header -server-unix-socket String PGRST_SERVER_UNIX_SOCKET -server-unix-socket-mode String 660 PGRST_SERVER_UNIX_SOCKET_MODE -=========================== ======= ================= ========== ================================= ============================== - .. _admin-server-port: admin-server-port ----------------- + =============== ======================= + **Type** Int + **Default** `n/a` + **Reloadable** N + **Environment** PGRST_ADMIN_SERVER_PORT + **In-Database** `n/a` + =============== ======================= + Specifies the port for the :ref:`health_check` endpoints. .. _app.settings.*: @@ -196,6 +168,14 @@ admin-server-port app.settings.* -------------- + =============== ======================= + **Type** String + **Default** `n/a` + **Reloadable** & + **Environment** PGRST_APP_SETTINGS_* + **In-Database** `n/a` + =============== ======================= + Arbitrary settings that can be used to pass in secret keys directly as strings, or via OS environment variables. For instance: :code:`app.settings.jwt_secret = "$(MYAPP_JWT_SECRET)"` will take :code:`MYAPP_JWT_SECRET` from the environment and make it available to postgresql functions as :code:`current_setting('app.settings.jwt_secret')`. .. _db-anon-role: @@ -203,6 +183,14 @@ app.settings.* db-anon-role ------------ + =============== ======================= + **Type** String + **Default** `n/a` + **Reloadable** Y + **Environment** PGRST_DB_ANON_ROLE + **In-Database** pgrst.db_anon_role + =============== ======================= + The database role to use when executing commands on behalf of unauthenticated clients. For more information, see :ref:`roles`. When unset anonymous access will be blocked. @@ -212,6 +200,14 @@ db-anon-role db-channel ---------- + =============== ======================= + **Type** String + **Default** pgrst + **Reloadable** Y + **Environment** PGRST_DB_CHANNEL + **In-Database** `n/a` + =============== ======================= + The name of the notification channel that PostgREST uses for :ref:`schema_reloading` and configuration reloading. .. _db-channel-enabled: @@ -219,6 +215,14 @@ db-channel db-channel-enabled ------------------ + =============== ======================= + **Type** Boolean + **Default** True + **Reloadable** Y + **Environment** PGRST_DB_CHANNEL_ENABLED + **In-Database** `n/a` + =============== ======================= + When this is set to :code:`true`, the notification channel specified in :ref:`db-channel` is enabled. You should set this to ``false`` when using PostgresSQL behind an external connection pooler such as PgBouncer working in transaction pooling mode. See :ref:`this section ` for more information. @@ -228,6 +232,14 @@ db-channel-enabled db-config --------- + =============== ======================= + **Type** Boolean + **Default** True + **Reloadable** Y + **Environment** PGRST_DB_CONFIG + **In-Database** `n/a` + =============== ======================= + Enables the in-database configuration. .. _db-pre-config: @@ -235,6 +247,14 @@ db-config db-pre-config ------------- + =============== ======================= + **Type** String + **Default** `n/a` + **Reloadable** Y + **Environment** PGRST_DB_PRE_CONFIG + **In-Database** pgrst.db_pre_config + =============== ======================= + Name of the function that does in-database configuration. .. _db-extra-search-path: @@ -242,6 +262,14 @@ db-pre-config db-extra-search-path -------------------- + =============== ========================== + **Type** String + **Default** public + **Reloadable** Y + **Environment** PGRST_DB_EXTRA_SEARCH_PATH + **In-Database** pgrst.db_extra_search_path + =============== ========================== + Extra schemas to add to the `search_path `_ 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`. @@ -253,6 +281,14 @@ db-extra-search-path db-max-rows ----------- + =============== ========================== + **Type** Int + **Default** ∞ + **Reloadable** Y + **Environment** PGRST_DB_MAX_ROWS + **In-Database** pgrst.db_max_rows + =============== ========================== + *For backwards compatibility, this config parameter is also available without prefix as "max-rows".* A hard limit to the number of rows PostgREST will fetch from a view, table, or stored procedure. Limits payload size for accidental or malicious requests. @@ -262,37 +298,29 @@ db-max-rows db-plan-enabled --------------- + =============== ========================== + **Type** Boolean + **Default** False + **Reloadable** Y + **Environment** PGRST_DB_PLAN_ENABLED + **In-Database** pgrst.db_plan_enabled + =============== ========================== + When this is set to :code:`true`, the execution plan of a request can be retrieved by using the :code:`Accept: application/vnd.pgrst.plan` header. See :ref:`explain_plan`. - 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 ------- + =============== ========================== + **Type** Int + **Default** 10 + **Reloadable** N + **Environment** PGRST_DB_POOL + **In-Database** n/a + =============== ========================== + Number of maximum connections to keep open in PostgREST's database pool. .. _db-pool-acquisition-timeout: @@ -300,6 +328,14 @@ db-pool db-pool-acquisition-timeout --------------------------- + =============== ================================= + **Type** Int + **Default** 10 + **Reloadable** N + **Environment** PGRST_DB_POOL_ACQUISITION_TIMEOUT + **In-Database** `n/a` + =============== ================================= + Specifies the maximum time in seconds that the request will wait for the pool to free up a connection slot to the database. .. _db-pool-max-idletime: @@ -307,6 +343,14 @@ db-pool-acquisition-timeout db-pool-max-idletime -------------------- + =============== ================================= + **Type** Int + **Default** 30 + **Reloadable** N + **Environment** PGRST_DB_POOL_MAX_IDLETIME + **In-Database** `n/a` + =============== ================================= + *For backwards compatibility, this config parameter is also available as “db-pool-timeout”.* Time in seconds to close idle pool connections. @@ -316,6 +360,14 @@ db-pool-max-idletime db-pool-max-lifetime -------------------- + =============== ================================= + **Type** Int + **Default** 1800 + **Reloadable** N + **Environment** PGRST_DB_POOL_MAX_LIFETIME + **In-Database** `n/a` + =============== ================================= + Specifies the maximum time in seconds of an existing connection in the pool. .. _db-pre-request: @@ -323,6 +375,14 @@ db-pool-max-lifetime db-pre-request -------------- + =============== ================================= + **Type** String + **Default** `n/a` + **Reloadable** Y + **Environment** PGRST_DB_PRE_REQUEST + **In-Database** pgrst.db_pre_request + =============== ================================= + *For backwards compatibility, this config parameter is also available without prefix as "pre-request".* A schema-qualified stored procedure name to call right after the :ref:`tx_settings` are set. See :ref:`pre-request`. @@ -332,6 +392,14 @@ db-pre-request db-prepared-statements ---------------------- + =============== ================================= + **Type** Boolean + **Default** True + **Reloadable** Y + **Environment** PGRST_DB_PREPARED_STATEMENTS + **In-Database** pgrst.db_prepared_statements + =============== ================================= + Enables or disables prepared statements. When disabled, the generated queries will be parameterized (invulnerable to SQL injection) but they will not be prepared (cached in the database session). Not using prepared statements will noticeably decrease performance, so it's recommended to always have this setting enabled. @@ -343,6 +411,14 @@ db-prepared-statements db-root-spec ------------ + =============== ================================= + **Type** String + **Default** `n/a` + **Reloadable** Y + **Environment** PGRST_DB_ROOT_SPEC + **In-Database** pgrst.db_root_spec + =============== ================================= + Function to override the OpenAPI response. See :ref:`override_openapi`. .. _db-schemas: @@ -350,6 +426,14 @@ db-root-spec db-schemas ---------- + =============== ================================= + **Type** String + **Default** public + **Reloadable** Y + **Environment** PGRST_DB_SCHEMAS + **In-Database** pgrst.db_schemas + =============== ================================= + *For backwards compatibility, this config parameter is also available in singular as "db-schema".* The list of database schemas to expose to clients. See :ref:`schemas`. @@ -359,6 +443,14 @@ db-schemas db-tx-end --------- + =============== ================================= + **Type** String + **Default** commit + **Reloadable** N + **Environment** PGRST_DB_TX_END + **In-Database** `n/a` + =============== ================================= + Specifies how to terminate the database transactions. .. code:: bash @@ -380,7 +472,19 @@ db-tx-end 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``. + =============== ================================= + **Type** String + **Default** postgresql:// + **Reloadable** N + **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``. + + 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 see :ref:`roles`. @@ -393,6 +497,14 @@ db-uri db-use-legacy-gucs ------------------ + =============== ================================= + **Type** Boolean + **Default** True + **Reloadable** Y + **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. @@ -402,6 +514,14 @@ db-use-legacy-gucs jwt-aud ------- + =============== ================================= + **Type** String + **Default** `n/a` + **Reloadable** Y + **Environment** PGRST_JWT_AUD + **In-Database** pgrst.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. .. _jwt-role-claim-key: @@ -409,6 +529,14 @@ jwt-aud jwt-role-claim-key ------------------ + =============== ================================= + **Type** String + **Default** .role + **Reloadable** Y + **Environment** PGRST_JWT_ROLE_CLAIM_KEY + **In-Database** pgrst.jwt_role_claim_key + =============== ================================= + *For backwards compatibility, this config parameter is also available without prefix as "role-claim-key".* A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. This can be used to consume a JWT provided by a third party service like Auth0, Okta or Keycloak. Usage examples: @@ -428,6 +556,14 @@ jwt-role-claim-key jwt-secret ---------- + =============== ================================= + **Type** String + **Default** `n/a` + **Reloadable** Y + **Environment** PGRST_JWT_SECRET + **In-Database** pgrst.jwt_secret + =============== ================================= + The secret or `JSON Web Key (JWK) (or set) `_ 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. @@ -441,6 +577,14 @@ jwt-secret jwt-secret-is-base64 -------------------- + =============== ================================= + **Type** Boolean + **Default** False + **Reloadable** Y + **Environment** PGRST_JWT_SECRET_IS_BASE64 + **In-Database** pgrst.jwt_secret_is_base64 + =============== ================================= + When this is set to :code:`true`, the value derived from :code:`jwt-secret` will be treated as a base64 encoded secret. .. _log-level: @@ -448,6 +592,14 @@ jwt-secret-is-base64 log-level --------- + =============== ================================= + **Type** String + **Default** error + **Reloadable** N + **Environment** PGRST_LOG_LEVEL + **In-Database** `n/a` + =============== ================================= + Specifies the level of information to be logged while running PostgREST. .. code:: bash @@ -472,6 +624,14 @@ log-level openapi-mode ------------ + =============== ================================= + **Type** String + **Default** follow-privileges + **Reloadable** Y + **Environment** PGRST_OPENAPI_MODE + **In-Database** pgrst.openapi_mode + =============== ================================= + Specifies how the OpenAPI output should be displayed. .. code:: bash @@ -493,6 +653,14 @@ openapi-mode openapi-security-active ----------------------- + =============== ================================= + **Type** Boolean + **Default** False + **Reloadable** Y + **Environment** PGRST_OPENAPI_SECURITY_ACTIVE + **In-Database** pgrst.openapi_security_active + =============== ================================= + When this is set to :code:`true`, security options are included in the :ref:`OpenAPI output `. .. _openapi-server-proxy-uri: @@ -500,6 +668,14 @@ When this is set to :code:`true`, security options are included in the :ref:`Ope openapi-server-proxy-uri ------------------------ + =============== ================================= + **Type** String + **Default** `n/a` + **Reloadable** N + **Environment** PGRST_OPENAPI_SERVER_PROXY_URI + **In-Database** pgrst.openapi_server_proxy_uri + =============== ================================= + Overrides the base URL used within the OpenAPI self-documentation hosted at the API root path. Use a complete URI syntax :code:`scheme:[//[user:password@]host[:port]][/]path[?query][#fragment]`. Ex. :code:`https://postgrest.com` .. code:: json @@ -523,6 +699,14 @@ openapi-server-proxy-uri raw-media-types --------------- + =============== ================================= + **Type** String + **Default** `n/a` + **Reloadable** Y + **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:`scalar_return_formats`. @@ -539,6 +723,14 @@ raw-media-types server-host ----------- + =============== ================================= + **Type** String + **Default** !4 + **Reloadable** N + **Environment** PGRST_SERVER_HOST + **In-Database** `n/a` + =============== ================================= + Where to bind the PostgREST web server. In addition to the usual address options, PostgREST interprets these reserved addresses with special meanings: * :code:`*` - any IPv4 or IPv6 hostname @@ -552,6 +744,14 @@ server-host server-port ----------- + =============== ================================= + **Type** Int + **Default** 3000 + **Reloadable** N + **Environment** PGRST_SERVER_PORT + **In-Database** `n/a` + =============== ================================= + The TCP port to bind the web server. .. _server-trace-header: @@ -559,6 +759,14 @@ server-port server-trace-header ------------------- + =============== ================================= + **Type** String + **Default** `n/a` + **Reloadable** Y + **Environment** PGRST_SERVER_TRACE_HEADER + **In-Database** pgrst.server_trace_header + =============== ================================= + The header name used to trace HTTP requests. See :ref:`trace_header`. .. _server-unix-socket: @@ -566,6 +774,14 @@ server-trace-header server-unix-socket ------------------ + =============== ================================= + **Type** String + **Default** `n/a` + **Reloadable** N + **Environment** PGRST_SERVER_UNIX_SOCKET + **In-Database** `n/a` + =============== ================================= + `Unix domain socket `_ where to bind the PostgREST web server. If specified, this takes precedence over :ref:`server-port`. Example: @@ -578,6 +794,14 @@ server-unix-socket server-unix-socket-mode ----------------------- + =============== ================================= + **Type** String + **Default** 660 + **Reloadable** N + **Environment** PGRST_SERVER_UNIX_SOCKET_MODE + **In-Database** `n/a` + =============== ================================= + `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. diff --git a/docs/references/connection_pool.rst b/docs/references/connection_pool.rst index aa0a904f6..db7ed0160 100644 --- a/docs/references/connection_pool.rst +++ b/docs/references/connection_pool.rst @@ -16,7 +16,7 @@ Dynamic Connection Pool To conserve system resources, PostgREST uses a dynamic connection pool. This enables the number of connections in the pool to increase and decrease depending on request traffic. - If all the connections are being used, a new connection is added. The pool can grow until it reaches the :ref:`db-pool` size. Note that it’s pointless to set this higher than the ``max_connections`` setting in your database. -- If a connection is unused for a period of time (determined by :ref:`db-pool-max-idletime`, 30 seconds by default), it will be released. +- If a connection is unused for a period of time (:ref:`db-pool-max-idletime`), it will be released. Connection lifetime ------------------- @@ -24,13 +24,13 @@ Connection lifetime Long-lived PostgreSQL connections can consume considerable memory (see `here `_ for more details). Under a busy system, the :ref:`db-pool-max-idletime` won't be reached and the connection pool can be full of long-lived connections. -To avoid this problem and save resources, a connection max lifetime (determined by :ref:`db-pool-max-lifetime`, 30 minutes by default) is enforced. +To avoid this problem and save resources, a connection max lifetime (:ref:`db-pool-max-lifetime`) is enforced. After the max lifetime is reached, connections from the pool will be released and new ones will be created. This doesn't affect running requests, only unused connections will be released. Acquisition Timeout ------------------- -If all the available connections in the pool are busy, an HTTP request will wait until reaching a timeout (determined by :ref:`db-pool-acquisition-timeout`, 10 seconds by default). +If all the available connections in the pool are busy, an HTTP request will wait until reaching a timeout (:ref:`db-pool-acquisition-timeout`). If the request reaches the timeout, it will be aborted with the following response: diff --git a/postgrest.dict b/postgrest.dict index 685cc788f..cc6bc534f 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -17,7 +17,8 @@ cd centric changelog coercible -Cloudfare +conf +Cloudflare config CORS CPUs @@ -124,6 +125,7 @@ postgrest PostgREST's pre preflight +plpgsql psql Qin RabbitMQ From 1b036dfab9b42dce24af76f74546cefda07747d3 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 4 Aug 2023 18:18:23 -0500 Subject: [PATCH 627/652] release page for v11.2.0 Closes #644 --- docs/releases/v11.2.0.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docs/releases/v11.2.0.rst diff --git a/docs/releases/v11.2.0.rst b/docs/releases/v11.2.0.rst new file mode 100644 index 000000000..48809ae62 --- /dev/null +++ b/docs/releases/v11.2.0.rst @@ -0,0 +1,4 @@ +11.2.0 +====== + +Starting from this version, the release notes will be posted on PostgREST's GitHub release page. Please see https://github.com/PostgREST/postgrest/releases/tag/v11.2.0. From e82dc41a6a691d258e7378d58cc4426511ad71de Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 4 Aug 2023 18:36:38 -0500 Subject: [PATCH 628/652] add link to github discussions closes #401 --- docs/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 4020d6195..d803c4a34 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -90,7 +90,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. For discussions, use the Github `discussions page `_ or join our `chat room `_. You can also report or search for bugs/features on the Github `issues `_ page. .. toctree:: :glob: From 43a8745e628454f52ce44dc3ffd63326f9bf03f5 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 4 Aug 2023 19:09:47 -0500 Subject: [PATCH 629/652] link connection string from connection pool Closes #647 --- docs/references/connection_pool.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/references/connection_pool.rst b/docs/references/connection_pool.rst index db7ed0160..0876dbd27 100644 --- a/docs/references/connection_pool.rst +++ b/docs/references/connection_pool.rst @@ -7,6 +7,11 @@ A connection pool is a cache of reusable database connections. It allows serving Minimizing connections is paramount to performance. Each PostgreSQL connection creates a process, having too many can exhaust available resources. +Connection String +----------------- + +For connecting to the database, the pool requires a connection string. You can can configure it using :ref:`db-uri`. + .. _pool_growth_limit: .. _dyn_conn_pool: From 291b6f3587b468a33e430455523ac22737b59fe5 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 4 Aug 2023 19:38:21 -0500 Subject: [PATCH 630/652] add different ways to specify the connection string Closes #576 --- docs/references/configuration.rst | 34 ++++++++++++++++++++++++++----- postgrest.dict | 1 + 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index 338e0aa1b..e0c4864ef 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -480,18 +480,42 @@ 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. + The standard `PostgreSQL connection string `_, there are different ways to specify it: - 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``. +URI Format +~~~~~~~~~~ - 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. + .. code:: - The user with whom PostgREST connects to the database is also known as the ``authenticator`` role. For more information see :ref:`roles`. + "postgres://authenticator:mysecretpassword@localhost:5433/postgres?parameters=val" - 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. + - Under this format symbols and unusual characters in the password or other fields should be percent encoded to avoid a parse error. + - If enforcing an SSL connection to the database is required you can use `sslmode `_ in the URI, for example ``postgres://user:pass@host:5432/dbname?sslmode=require``. + - The user with whom PostgREST connects to the database is also known as the ``authenticator`` role. For more information see :ref:`roles`. + - When running PostgREST on the same machine as PostgreSQL, it is also possible to connect to the database using a `Unix socket `_ 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. + +Keyword/Value Format +~~~~~~~~~~~~~~~~~~~~ + + .. code:: + + "host=localhost port=5433 user=authenticator password=mysecretpassword dbname=postgres" + +LIBPQ Environment Variables +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + .. code:: + + PGHOST=localhost PGPORT=5433 PGUSER=authenticator PGDATABASE=postgres + + Any parameter that is not set in the above formats is read from `libpq environment variables `_. The default connection string is ``postgresql://``, which reads **all** parameters from the environment. + +External config file +~~~~~~~~~~~~~~~~~~~~ Choosing a value for this parameter beginning with the at sign such as ``@filename`` (e.g. ``@./configs/my-config``) loads the connection string out of an external file. + .. _db-use-legacy-gucs: db-use-legacy-gucs diff --git a/postgrest.dict b/postgrest.dict index cc6bc534f..3d968a3fe 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -78,6 +78,7 @@ localhost login lookups Logins +LIBPQ logins lon lt From a6f93f55b1c4196432d41f4b171a28f15972817f Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 4 Aug 2023 21:15:26 -0500 Subject: [PATCH 631/652] clear supported pg versions closes #592 --- docs/explanations/install.rst | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/explanations/install.rst b/docs/explanations/install.rst index 6944803a8..df76e8aa5 100644 --- a/docs/explanations/install.rst +++ b/docs/explanations/install.rst @@ -55,6 +55,18 @@ You can also use your OS package manager. choco install postgrest scoop install postgrest + +.. _pg-dependency: + +Supported PostgreSQL versions +============================= + +=============== ================================= +**Supported** PostgreSQL >= 9.6 +=============== ================================= + +PostgREST works with all PostgreSQL versions starting from 9.6. + Running PostgREST ================= @@ -101,13 +113,6 @@ For a complete reference of the configuration file, see :ref:`configuration`. 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.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 54bfefdb55fd45b529e7d9507f9485c14571a38b Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 4 Aug 2023 21:23:18 -0500 Subject: [PATCH 632/652] move heroku outside Installation --- docs/explanations/install.rst | 123 ---------------------------------- docs/integrations/heroku.rst | 122 +++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 123 deletions(-) create mode 100644 docs/integrations/heroku.rst diff --git a/docs/explanations/install.rst b/docs/explanations/install.rst index df76e8aa5..fad838b60 100644 --- a/docs/explanations/install.rst +++ b/docs/explanations/install.rst @@ -261,126 +261,3 @@ You can build PostgREST from source with `Stack `_: - - .. 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"}' diff --git a/docs/integrations/heroku.rst b/docs/integrations/heroku.rst new file mode 100644 index 000000000..614950c1d --- /dev/null +++ b/docs/integrations/heroku.rst @@ -0,0 +1,122 @@ +.. _deploy_heroku: + +Heroku +====== + +1. Log into Heroku using the `Heroku CLI `_: + + .. 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 b0aaf7afe851a6f2ab864bbca0de29b91afd85c0 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 4 Aug 2023 22:32:20 -0500 Subject: [PATCH 633/652] mention header names are lowercased Closes #627 --- docs/references/transactions.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/references/transactions.rst b/docs/references/transactions.rst index 85f462fa2..1e732cd7a 100644 --- a/docs/references/transactions.rst +++ b/docs/references/transactions.rst @@ -113,6 +113,10 @@ Request Headers, Cookies and JWT claims PostgREST stores the headers, cookies and headers as JSON. To get them: +.. important:: + + The headers names are lowercased. e.g. If the request sends ``User-Agent: x`` this will be obtainable as ``current_setting('request.headers', true)::json->>'user-agent'``. + .. code-block:: postgresql -- To get all the headers sent in the request From 459f3f817396aa3711546a22e6457c93b0a54a36 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 4 Aug 2023 22:43:22 -0500 Subject: [PATCH 634/652] add apache apisix tutorial --- docs/ecosystem.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index da7b4eb66..423f0341b 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -20,6 +20,8 @@ Community Tutorials * `Build data-driven applications with Workers and PostgreSQL `_ - A tutorial on how to integrate with PostgREST and PostgreSQL using Cloudflare Workers. +* `A poor man's API `_ - Shows how to integrate PostgREST with Apache APISIX as an alternative to Nginx. + .. _templates: Templates From afee9b73a477b5655c304d44f0b5692fe5dc1412 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 4 Aug 2023 23:08:52 -0500 Subject: [PATCH 635/652] add return=minimal and organize other returns Closes #624 --- docs/references/api/tables_views.rst | 121 +++++++++++++++++++++------ 1 file changed, 94 insertions(+), 27 deletions(-) diff --git a/docs/references/api/tables_views.rst b/docs/references/api/tables_views.rst index 3d4bb9b31..469ad612f 100644 --- a/docs/references/api/tables_views.rst +++ b/docs/references/api/tables_views.rst @@ -693,33 +693,6 @@ HEAD A HEAD method will behave identically to GET except that no body will be returned (`RFC 2616 `_) . As an optimization, the generated query won't execute an aggregate (to avoid unnecessary data transfer). -.. _update: - -Update -====== - -To update a row or rows in a table, use the PATCH verb. Use :ref:`h_filter` to specify which record(s) to update. Here is an example query setting the :code:`category` column to child for all people below a certain age. - -.. 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`. - .. _insert: Insert @@ -743,10 +716,77 @@ To create a row in a database table post a JSON object whose keys are the names -X POST -H "Content-Type: application/json" \ -d '{ "col1": "value1", "col2": "value2" }' +.. code:: + + HTTP/1.1 201 Created + +No request body will be returned by default. + +.. note:: + + You can use the ``Prefer: return=minimal`` header to get the same behavior. This is only provided for completeness because it's basically a no-op. + +Prefer: return=headers-only +--------------------------- + If the table has a primary key, the response can contain a :code:`Location` header describing where to find the new object by including the header :code:`Prefer: return=headers-only` in the request. Make sure that the table is not write-only, otherwise constructing the :code:`Location` header will cause a permissions error. +.. tabs:: + + .. code-tab:: http + + POST /projects HTTP/1.1 + Prefer: return=headers-only + + {"id":33, "name": "x"} + + .. code-tab:: bash Curl + + curl "http://localhost:3000/projects" \ + -X POST -H "Content-Type: application/json" -H "Prefer: return=headers-only" \ + -d '{"id":33, "name": "x"}' + +.. code-block:: http + + HTTP/1.1 201 Created + Location: /projects?id=eq.34 + +Prefer: return=representation +----------------------------- + 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. +.. tabs:: + + .. code-tab:: http + + POST /projects HTTP/1.1 + Content-Type: application/json; charset=utf-8 + Prefer: return=representation + + {"id":33, "name": "x"} + + .. code-tab:: bash Curl + + curl "http://localhost:3000/projects" \ + -X POST -H "Content-Type: application/json" -H "Prefer: return=representation" \ + -d '{"id":33, "name": "x"}' + +.. code:: + + HTTP/1.1 201 Created + Transfer-Encoding: chunked + + [ + { + "id": 33, + "name": "x" + } + ] + +x-www-form-urlencoded +--------------------- + URL encoded payloads can be posted with ``Content-Type: application/x-www-form-urlencoded``. .. tabs:: @@ -952,6 +992,33 @@ 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: + +Update +====== + +To update a row or rows in a table, use the PATCH verb. Use :ref:`h_filter` to specify which record(s) to update. Here is an example query setting the :code:`category` column to child for all people below a certain age. + +.. 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 From 49d4b9b7db4d50b3851c6467a78c331db44d94ae Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 4 Aug 2023 23:11:12 -0500 Subject: [PATCH 636/652] fix dict --- postgrest.dict | 3 +++ 1 file changed, 3 insertions(+) diff --git a/postgrest.dict b/postgrest.dict index 3d968a3fe..c5070d1dd 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -1,6 +1,7 @@ personal_ws-1.1 en 0 utf-8 api API's +APISIX Archlinux aud Auth @@ -186,6 +187,7 @@ Upsert upsert uri url +urlencoded urls variadic verifier @@ -197,4 +199,5 @@ websearch Websockets webuser wfts +www Zac From 095d43ff75a4701e0e8beb9f841a30249a9557fd Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Fri, 4 Aug 2023 23:51:41 -0500 Subject: [PATCH 637/652] add dynamic schemas example Closes #644 --- docs/references/api/schemas.rst | 49 +++++++++++++++++++++++++++++++ docs/references/configuration.rst | 2 +- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/docs/references/api/schemas.rst b/docs/references/api/schemas.rst index 359f5c959..4c62de451 100644 --- a/docs/references/api/schemas.rst +++ b/docs/references/api/schemas.rst @@ -105,3 +105,52 @@ You can only switch to a schema included in :ref:`db-schemas`. Using another sch "message":"The schema must be one of the following: tenant1, tenant2" } + +Dynamic schemas +~~~~~~~~~~~~~~~ + +To add schemas dynamically, you can use :ref:`in_db_config` plus :ref:`config reloading ` and :ref:`schema cache reloading `. Here are some options for how to do this: + +- If the schemas' names have a pattern, like a ``tenant_`` prefix, do: + +.. code-block:: postgresql + + create or replace function postgrest.pre_config() + returns void as $$ + select + set_config('pgrst.db_schemas', string_agg(nspname, ','), true) + from pg_namespace + where nspname like 'tenant_%'; + $$ language sql; + +- If there's no name pattern but they're created with a particular role (``CREATE SCHEMA mine AUTHORIZATION joe``), do: + +.. code-block:: postgresql + + create or replace function postgrest.pre_config() + returns void as $$ + select + set_config('pgrst.db_schemas', string_agg(nspname, ','), true) + from pg_namespace + where nspowner = 'joe'::regrole; + $$ language sql; + +- Otherwise, you might need to create a table that stores the allowed schemas. + +.. code-block:: postgresql + + create table postgrest.config (schemas text); + + create or replace function postgrest.pre_config() + returns void as $$ + select + set_config('pgrst.db_schemas', schemas, true) + from postgrest.config; + $$ language sql; + +Then each time you add an schema, do: + +.. code-block:: postgresql + + NOTIFY pgrst, 'reload config'; + NOTIFY pgrst, 'reload schema'; diff --git a/docs/references/configuration.rst b/docs/references/configuration.rst index e0c4864ef..5da6db953 100644 --- a/docs/references/configuration.rst +++ b/docs/references/configuration.rst @@ -255,7 +255,7 @@ db-pre-config **In-Database** pgrst.db_pre_config =============== ======================= - Name of the function that does in-database configuration. + Name of the function that does :ref:`in_db_config`. .. _db-extra-search-path: From 637165ba5bd88d02f4e20076b4b6c50f1f24fa37 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Sun, 6 Aug 2023 21:00:16 -0500 Subject: [PATCH 638/652] use the foreign key join term --- docs/references/api/resource_embedding.rst | 327 +++++++++++---------- 1 file changed, 174 insertions(+), 153 deletions(-) diff --git a/docs/references/api/resource_embedding.rst b/docs/references/api/resource_embedding.rst index be4ea15f2..18c1076bf 100644 --- a/docs/references/api/resource_embedding.rst +++ b/docs/references/api/resource_embedding.rst @@ -5,19 +5,31 @@ Resource Embedding PostgREST allows including related resources in a single API call. This reduces the need for many API requests. -**Foreign Keys** determine which tables and views can be returned together. For example, consider a database of films and their awards: +.. _fk_join: -.. image:: ../../_static/film.png +Foreign Key Joins +================= + +The server uses **Foreign Keys** to determine which tables and views can be joined together. + +- For joining tables, it reads foreign keys (respecting composite keys) and generates a join condition based on the foreign key columns. +- For joining views, it reads the base tables of the views' definition, and generates a join condition based on the foreign key columns of the base tables. .. important:: - - PostgREST respects composite foreign keys. - Whenever foreign keys change you must do :ref:`schema_reloading` for this feature to work. +Relationships +============= + +For example, consider a database of films and their awards: + +.. image:: ../../_static/film.png + .. _many-to-one: Many-to-one relationships -========================= +------------------------- Since ``films`` has a **foreign key** to ``directors``, this establishes a many-to-one relationship. Thus, we're able to request all the films and the director for each film. @@ -83,7 +95,7 @@ Since the table name is plural, we can be more accurate by making it singular wi .. _one-to-many: One-to-many relationships -========================= +------------------------- The **foreign key reference** establishes the inverse one-to-many relationship. In this case, ``films`` returns as a JSON array because of the “to-many” end. @@ -120,7 +132,7 @@ The **foreign key reference** establishes the inverse one-to-many relationship. .. _many-to-many: Many-to-many relationships -========================== +-------------------------- The join table determines many-to-many relationships. It must contain foreign keys to other two tables and they must be part of its composite key. @@ -168,7 +180,7 @@ For the many-to-many relationship between ``films`` and ``actors``, the join tab .. _one-to-one: One-to-one relationships -======================== +------------------------ One-to-one relationships are detected in two ways. @@ -220,7 +232,7 @@ One-to-one relationships are detected in two ways. Computed Relationships ====================== -You can manually define relationships between resources using functions. This is useful for database objects that can't define foreign keys, like `Foreign Data Wrappers `_. +You can manually define relationships between using functions. This is useful for database objects that can't define foreign keys, like `Foreign Data Wrappers `_. Assuming there's a foreign table ``premieres`` that we want to relate to ``films``. @@ -325,13 +337,21 @@ Computed relationships have good performance as their intended design enable `in .. _target_disamb: .. _complex_rels: -Complex Relationships -===================== +FK Joins on Multiple Foreign Key Relationships +============================================== -As mentioned on :ref:`resource_embedding`, the server does joins based on **Foreign Keys**. -When there are many foreign keys between tables, it needs disambiguation to resolve which foreign key columns to use for the join. +When there are multiple foreign keys between tables, :ref:`fk_join` need disambiguation to resolve which foreign key columns to use for the join. -:ref:`computed_relationships` can do the job here, they can choose join columns arbitrarily. +.. code:: + + HTTP/1.1 300 Multiple Choices + + { + "code": "PGRST201", + "details": [ "..." ], + "hint": "...", + "message": "Could not embed because more than one relationship was found for 'sites' and 'big_projects'" + } .. note:: @@ -365,7 +385,7 @@ Multiple Many-To-One shipping_address_id int references addresses(id) ); -To successfully embed ``orders`` with ``addresses``, you need to create computed relationships for the foreign keys columns you want to use: +To successfully join ``orders`` with ``addresses``, you need to create computed relationships for the foreign keys columns you want to use: .. code-block:: postgresql @@ -377,7 +397,7 @@ To successfully embed ``orders`` with ``addresses``, you need to create computed select * from addresses where id = $1.shipping_address_id $$ stable language sql; -Now, we can unambiguously embed the billing and shipping addresses. +Now, we can unambiguously join the billing and shipping addresses. .. tabs:: @@ -409,7 +429,7 @@ Multiple One-To-Many -------------------- Let's take the tables from :ref:`multiple_m2o`. -To embed ``addresses`` with ``orders``, you need to create computed relationships like these ones: +To join ``addresses`` with ``orders``, you need to create computed relationships like these ones: .. code-block:: postgresql @@ -669,6 +689,144 @@ Then, the request would be: } ] +.. _embedding_partitioned_tables: + +FK Joins on Partitioned Tables +============================== + +Foreign Key joins 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 join ``box_office`` and ``films``: + +.. tabs:: + + .. code-tab:: http + + GET /box_office?select=bo_date,gross_revenue,films(title)&gross_revenue=gte.1000000 HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/box_office?select=bo_date,gross_revenue,films(title)&gross_revenue=gte.1000000" + +.. note:: + + * FK joins 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:`computed_relationships` 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_views: + +FK Joins on Views +================= + +PostgREST will infer the relationships of a view based on its base tables. Base 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. + +For instance, the following view has ``nominations``, ``films`` and ``competitions`` as base tables: + +.. code-block:: postgres + + CREATE VIEW nominations_view AS + 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 this view contains ``nominations.film_id``, which has a **foreign key** relationship to ``films``, then we can join the ``films`` table. Similarly, because the view contains ``films.id``, then we can also join the ``roles`` and the ``actors`` tables (the last one in a many-to-many relationship): + +.. tabs:: + + .. code-tab:: http + + 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=film_title,films(language),roles(character),actors(last_name,first_name)&rank=eq.5" + +It's also possible to foreign key join `Materialized Views `_. + +.. important:: + + - It's not guaranteed that FK joins will work on all kinds of views. In particular, FK joins won't work on views that contain UNIONs. + + + Why? PostgREST detects base 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`. + +.. _embedding_view_chains: + +FK Joins on Chains of Views +--------------------------- + +Views can also depend on other views, which in turn depend on the actual base table. For PostgREST to pick up those chains recursively to any depth, all the views must be in the search path, so either in the exposed schema (:ref:`db-schemas`) or in one of the schemas set in :ref:`db-extra-search-path`. This does not apply to the base table, which could be in a private schema as well. See :ref:`schema_isolation` for more details. + +.. _s_proc_embed: + +FK Joins on Table-Valued Functions +================================== + +If you have a :ref:`Stored Procedure ` that returns a table type, you can do a Foreign Key join on the result. + +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 STABLE; + +A request with ``directors`` embedded: + +.. tabs:: + + .. code-tab:: http + + GET /rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers* HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers*" + +.. code-block:: json + + [ + { "title": "Workers Leaving The Lumière Factory In Lyon", + "directors": { + "id": 2, + "last_name": "Lumière" + } + } + ] + .. _nested_embedding: Nested Embedding @@ -981,143 +1139,6 @@ You can use this to get the columns of a join table in a many-to-many relationsh The spread operator ``...`` is borrowed from the Javascript `spread syntax `_. -.. _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``: - -.. tabs:: - - .. code-tab:: http - - GET /box_office?select=bo_date,gross_revenue,films(title)&gross_revenue=gte.1000000 HTTP/1.1 - - .. code-tab:: bash Curl - - 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_views: - -Embedding Views -=============== - -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. - -For instance, the following view has ``nominations``, ``films`` and ``competitions`` as source tables: - -.. code-block:: postgres - - CREATE VIEW nominations_view AS - 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 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=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=film_title,films(language),roles(character),actors(last_name,first_name)&rank=eq.5" - -It's also possible to embed `Materialized Views `_. - -.. important:: - - - 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. - + 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`. - -.. _embedding_view_chains: - -Embedding Chains of Views -========================= - -Views can also depend on other views, which in turn depend on the actual source table. For PostgREST to pick up those chains recursively to any depth, all the views must be in the search path, so either in the exposed schema (:ref:`db-schemas`) or in one of the schemas set in :ref:`db-extra-search-path`. This does not apply to the source table, which could be in a private schema as well. See :ref:`schema_isolation` for more details. - -.. _s_proc_embed: - -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``). - -.. code-block:: plpgsql - - CREATE FUNCTION getallfilms() RETURNS SETOF films AS $$ - SELECT * FROM films; - $$ LANGUAGE SQL STABLE; - -A request with ``directors`` embedded: - -.. tabs:: - - .. code-tab:: http - - GET /rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers* HTTP/1.1 - - .. code-tab:: bash Curl - - curl "http://localhost:3000/rpc/getallfilms?select=title,directors(id,last_name)&title=like.*Workers*" - -.. code-block:: json - - [ - { "title": "Workers Leaving The Lumière Factory In Lyon", - "directors": { - "id": 2, - "last_name": "Lumière" - } - } - ] - .. _mutation_embed: Embedding after Insertions/Updates/Deletions From 3f112553b08b0cc06793b42229658a8148c4ccce Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 7 Aug 2023 18:25:49 -0500 Subject: [PATCH 639/652] Add missing ERD/SQL tabs and fix some requests --- diagrams/README.md | 2 + diagrams/boxoffice.er | 15 ++ diagrams/employees.er | 5 + diagrams/film.er | 11 ++ diagrams/orders.er | 5 + diagrams/premieres.er | 16 ++ diagrams/presidents.er | 5 + diagrams/users.er | 5 + docs/_static/boxoffice.png | Bin 0 -> 11482 bytes docs/_static/employees.png | Bin 8244 -> 8550 bytes docs/_static/film.png | Bin 51752 -> 56781 bytes docs/_static/orders.png | Bin 17892 -> 16680 bytes docs/_static/premieres.png | Bin 0 -> 8990 bytes docs/_static/presidents.png | Bin 8331 -> 9166 bytes docs/_static/users.png | Bin 14073 -> 15354 bytes docs/references/api/resource_embedding.rst | 173 +++++++++++++-------- 16 files changed, 173 insertions(+), 64 deletions(-) create mode 100644 diagrams/boxoffice.er create mode 100644 diagrams/premieres.er create mode 100644 docs/_static/boxoffice.png create mode 100644 docs/_static/premieres.png diff --git a/diagrams/README.md b/diagrams/README.md index 67009dc31..fa4a1e7fe 100644 --- a/diagrams/README.md +++ b/diagrams/README.md @@ -8,6 +8,8 @@ You can go download erd from https://github.com/BurntSushi/erd/releases and then ./erd_static-x86-64 -i diagrams/film.er -o docs/_static/film.png ``` +The fonts used belong to the GNU FreeFont family. You can download them here: http://ftp.gnu.org/gnu/freefont/ + ## LaTeX The schema structure diagram is done with LaTeX. You can use a GUI like https://www.mathcha.io/editor to create the .tex file. diff --git a/diagrams/boxoffice.er b/diagrams/boxoffice.er new file mode 100644 index 000000000..7d6b0a1c7 --- /dev/null +++ b/diagrams/boxoffice.er @@ -0,0 +1,15 @@ +entity {font: "FreeSans"} +relationship {font: "FreeMono"} + +[Box_Office] +*bo_date +*+film_id +gross_revenue + +[Films] +*id ++director_id +title +`...` + +Box_Office +--1 Films diff --git a/diagrams/employees.er b/diagrams/employees.er index 7010a670e..4632f5bac 100644 --- a/diagrams/employees.er +++ b/diagrams/employees.er @@ -1,3 +1,8 @@ +# Build using: -e ortho + +entity {font: "FreeSans"} +relationship {font: "FreeMono"} + [Employees] *id first_name diff --git a/diagrams/film.er b/diagrams/film.er index d19fdf61b..cc54c4800 100644 --- a/diagrams/film.er +++ b/diagrams/film.er @@ -1,3 +1,6 @@ +entity {font: "FreeSans"} +relationship {font: "FreeSerif"} + [Films] *id +director_id @@ -31,6 +34,12 @@ year *+film_id rank +[Technical_Specs] +*+film_id +runtime +camera +sound + Roles *--1 Actors Roles *--1 Films @@ -38,3 +47,5 @@ Nominations *--1 Competitions Nominations *--1 Films Films *--1 Directors + +Films 1--1 Technical_Specs diff --git a/diagrams/orders.er b/diagrams/orders.er index bdd93de2e..86f0805e9 100644 --- a/diagrams/orders.er +++ b/diagrams/orders.er @@ -1,3 +1,8 @@ +# Build using: -e ortho + +entity {font: "FreeSans"} +relationship {font: "FreeMono"} + [Addresses] *id name diff --git a/diagrams/premieres.er b/diagrams/premieres.er new file mode 100644 index 000000000..6099e74fd --- /dev/null +++ b/diagrams/premieres.er @@ -0,0 +1,16 @@ +entity {font: "FreeSans"} +relationship {font: "FreeMono"} + +[Premieres] +*id +location +date ++film_id + +[Films] +*id ++director_id +title +`...` + +Premieres *--1 Films diff --git a/diagrams/presidents.er b/diagrams/presidents.er index b49100961..ca7cf71ba 100644 --- a/diagrams/presidents.er +++ b/diagrams/presidents.er @@ -1,3 +1,8 @@ +# Build using: -e ortho + +entity {font: "FreeSans"} +relationship {font: "FreeMono"} + [Presidents] *id first_name diff --git a/diagrams/users.er b/diagrams/users.er index e42ca312f..8ec011302 100644 --- a/diagrams/users.er +++ b/diagrams/users.er @@ -1,3 +1,8 @@ +# Build using: -e ortho + +entity {font: "FreeSans"} +relationship {font: "FreeMono"} + [Users] *id first_name diff --git a/docs/_static/boxoffice.png b/docs/_static/boxoffice.png new file mode 100644 index 0000000000000000000000000000000000000000..87249a6659f7c1f1a050f800fec72e16a52a441a GIT binary patch literal 11482 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 literal 0 HcmV?d00001 diff --git a/docs/_static/employees.png b/docs/_static/employees.png index 0a72df2a28ccb7c5042db28cdbbc6a0a158874a1..b21153cc5af6c662788cceeef1871d096f57bd3d 100644 GIT binary patch literal 8550 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 S>|kJEVDNPHb6Mw<&;$VX&=Ii! diff --git a/docs/_static/film.png b/docs/_static/film.png index 99843b8709145b1f89bf14ea93b2134a9f2b4a2f..03b9b2b749d9d7cc2a3d1a98995ef8bd86796b79 100644 GIT binary patch literal 56781 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
uyF0I>gwvl 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 diff --git a/docs/_static/orders.png b/docs/_static/orders.png index db709c873da885c3c73b42678bbcc279f582c836..0ac19aacd2c7ddfe9cbf1ae6d670fc8725b76206 100644 GIT binary patch literal 16680 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 b5BqQ>)q({)hfXjsFfe$!`njxgN@xNAx&3r# diff --git a/docs/_static/premieres.png b/docs/_static/premieres.png new file mode 100644 index 0000000000000000000000000000000000000000..794e96767de072d6414630fc9fbfbeb24d4f9d9b GIT binary patch literal 8990 zcmeAS@N?(olHy`uVBq!ia0y~yV9aM=U^u|R#=yXEn(@(W1_lPUByV>YhW{YAVDIwD z3=9eko-U3d6?5L+tquu!`iuR;`)`MQ6C4~=UHlt6CM8uif0@C(-aRJp_vQP4-@IKpud*`zyqv2Gi;K$=)#z6Y+d7?lCs3}|9oDbT1OUE*2H5ylFQ1R#gz2)_*RFlH7b_~ z@t(1MTH=!v6V<*a7JbaSx5si`^*hVS%!;QEKFZxYWBs&+YF*~f_jGocbR3=C;n5s{ zU}o*D$iKgD?;-D0DU*x_cezTI#qRybW*VnwSx=ENPCqB}Rl@Gq3+3W==ajOkPb+V_ zpJaa|`t;%q!{jSZZ*R{xzpfZ*v$=0icUM=z+gqkN_x4D}?kYKWak2YZNj|}eX%T0( zc>4MA#Z|pjEpBⓈ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

yy3Q_C#;ZVy&+J-Tv2 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 literal 0 HcmV?d00001 diff --git a/docs/_static/presidents.png b/docs/_static/presidents.png index 2164ff3de19ead9700797c517a93ec2486dbdbf4..09c4e6f791899e28fe803767b04435fbf2a64d8a 100644 GIT binary patch literal 9166 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 literal 8331 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 diff --git a/docs/_static/users.png b/docs/_static/users.png index f8b57fee753d46a31e6d918c868c8775fc8f97fb..d94f097fc01a41246fdc32b747369519bfe600e4 100644 GIT binary patch literal 15354 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. - When the foreign key has a unique constraint. -.. code-block:: postgresql + .. code-block:: postgresql - CREATE TABLE technical_specs( - film_id INT REFERENCES films UNIQUE, - runtime TIME, - camera TEXT, - sound TEXT - ); - -- When the foreign key is a primary 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, - runtime TIME, - camera TEXT, - sound TEXT - ); + CREATE TABLE technical_specs( + film_id INT REFERENCES films UNIQUE, + runtime TIME, + camera TEXT, + sound TEXT + ); .. tabs:: .. code-tab:: http - GET /films?select=title,technical_specs(runtime) HTTP/1.1 + GET /films?select=title,technical_specs(camera) HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/films?select=title,technical_specs(runtime)" + curl "http://localhost:3000/films?select=title,technical_specs(camera)" .. code-block:: json @@ -236,20 +269,26 @@ You can manually define relationships between using functions. This is useful fo Assuming there's a foreign table ``premieres`` that we want to relate to ``films``. -.. code-block:: postgres +.. tabs:: - create foreign table premieres ( - id integer, - location text, - "date" date, - film_id integer - ) server import_csv options ( filename '/tmp/directors.csv', format 'csv'); + .. group-tab:: ERD - create function film(premieres) returns setof films rows 1 as $$ - select * from films where id = $1.film_id - $$ stable language sql; + .. image:: ../../_static/premieres.png -The above function defines a relationship between ``premieres`` (the parameter) and ``films`` (the return type). Since there's a ``rows 1``, this defines a many-to-one relationship. + .. code-tab:: postgresql SQL + + create foreign table premieres ( + id integer, + location text, + "date" date, + film_id integer + ) server import_csv options ( filename '/tmp/directors.csv', format 'csv'); + + create function film(premieres) returns setof films rows 1 as $$ + select * from films where id = $1.film_id + $$ stable language sql; + +The above function (see the **SQL** tab) defines a relationship between ``premieres`` (the parameter) and ``films`` (the return type). 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:: @@ -698,24 +737,30 @@ Foreign Key joins can also be done between `partitioned tables Date: Mon, 7 Aug 2023 22:04:17 -0500 Subject: [PATCH 640/652] Revert disamb using FK instead of computed rels --- docs/references/api/resource_embedding.rst | 75 +++++++--------------- 1 file changed, 24 insertions(+), 51 deletions(-) diff --git a/docs/references/api/resource_embedding.rst b/docs/references/api/resource_embedding.rst index 63dda347e..3870f298a 100644 --- a/docs/references/api/resource_embedding.rst +++ b/docs/references/api/resource_embedding.rst @@ -372,7 +372,6 @@ Computed relationships have good performance as their intended design enable `in - 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, it will unnest the embedding and return repeated values for the top level resource. .. _embed_disamb: -.. _hint_disamb: .. _target_disamb: .. _complex_rels: @@ -392,14 +391,8 @@ When there are multiple foreign keys between tables, :ref:`fk_join` need disambi "message": "Could not embed because more than one relationship was found for 'sites' and 'big_projects'" } -.. note:: - - Previous versions addressed complex relationships with `Embedding disambiguation `_ but this is now deprecated. Follow the solutions in this section when a ``300 Multiple Choices`` error is returned. - -.. _multiple_m2o: - -Multiple Many-To-One --------------------- +Instead of the **table name**, you can specify the **foreign key constraint name** or the **column name** that is part of the foreign key. +For example, let's use the following tables: .. tabs:: @@ -420,23 +413,15 @@ Multiple Many-To-One create table orders ( id int primary key generated always as identity, name text, - billing_address_id int references addresses(id), - shipping_address_id int references addresses(id) + billing_address_id int, + shipping_address_id int, + constraint billing_address + foreign key(billing_address_id) references addresses(id), + constraint shipping_address + foreign key(shipping_address_id) references addresses(id) ); -To successfully join ``orders`` with ``addresses``, you need to create computed relationships for the foreign keys columns you want to use: - -.. code-block:: postgresql - - create function billing_address(orders) returns setof addresses rows 1 as $$ - select * from addresses where id = $1.billing_address_id - $$ stable language sql; - - create function shipping_address(orders) returns setof addresses rows 1 as $$ - select * from addresses where id = $1.shipping_address_id - $$ stable language sql; - -Now, we can unambiguously join the billing and shipping addresses. +To successfully join ``orders`` with the billing and shipping ``addresses``, use the corresponding foreign key constraints: .. tabs:: @@ -462,48 +447,36 @@ Now, we can unambiguously join the billing and shipping addresses. } ] -.. _multiple_o2m: +.. _hint_disamb: -Multiple One-To-Many --------------------- +Multiple FK Relationships to Many Resources +------------------------------------------- -Let's take the tables from :ref:`multiple_m2o`. -To join ``addresses`` with ``orders``, you need to create computed relationships like these ones: - -.. code-block:: postgresql - - create function billing_orders(addresses) returns setof orders as $$ - select * from orders where billing_address_id = $1.id - $$ stable language sql; - - create function shipping_orders(addresses) returns setof orders as $$ - select * from orders where shipping_address_id = $1.id - $$ stable language sql; - -Then, the request would look like: +Additionally, let's create two views for ``addresses``: ``central_addresses`` and ``eastern_addresses``. +Using the the view name is not enough to join ``orders`` with any of them. +To solve this, you need to add the foreign key as a hint: .. tabs:: .. code-tab:: http - GET /addresses?select=name,billing_orders(name),shipping_orders(name)&id=eq.1 HTTP/1.1 + GET /orders?select=name,central_addresses!billing_address(name),central_addresses!shipping_address(name) HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/addresses?select=name,billing_orders(name),shipping_orders(name)&id=eq.1" + curl "http://localhost:3000/orders?select=name,central_addresses!billing_address(name),central_addresses!shipping_address(name)" .. code-block:: json [ { - "name": "32 Glenlake Dr.Dearborn, MI 48124", - "billing_orders": [ - { "name": "Personal Water Filter" }, - { "name": "Coffee Machine" } - ], - "shipping_orders": [ - { "name": "Coffee Machine" } - ] + "name": "Personal Water Filter", + "billing_address": { + "name": "32 Glenlake Dr.Dearborn, MI 48124" + }, + "shipping_address": { + "name": "30 Glenlake Dr.Dearborn, MI 48124" + } } ] From 9ed7ff88465d1749cb85eb292562aee9b55e4812 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 9 Aug 2023 20:17:32 -0500 Subject: [PATCH 641/652] clarify FK joins --- docs/_static/premieres.png | Bin 8990 -> 0 bytes docs/references/api/resource_embedding.rst | 305 +++++++++++---------- 2 files changed, 163 insertions(+), 142 deletions(-) delete mode 100644 docs/_static/premieres.png diff --git a/docs/_static/premieres.png b/docs/_static/premieres.png deleted file mode 100644 index 794e96767de072d6414630fc9fbfbeb24d4f9d9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8990 zcmeAS@N?(olHy`uVBq!ia0y~yV9aM=U^u|R#=yXEn(@(W1_lPUByV>YhW{YAVDIwD z3=9eko-U3d6?5L+tquu!`iuR;`)`MQ6C4~=UHlt6CM8uif0@C(-aRJp_vQP4-@IKpud*`zyqv2Gi;K$=)#z6Y+d7?lCs3}|9oDbT1OUE*2H5ylFQ1R#gz2)_*RFlH7b_~ z@t(1MTH=!v6V<*a7JbaSx5si`^*hVS%!;QEKFZxYWBs&+YF*~f_jGocbR3=C;n5s{ zU}o*D$iKgD?;-D0DU*x_cezTI#qRybW*VnwSx=ENPCqB}Rl@Gq3+3W==ajOkPb+V_ zpJaa|`t;%q!{jSZZ*R{xzpfZ*v$=0icUM=z+gqkN_x4D}?kYKWak2YZNj|}eX%T0( zc>4MA#Z|pjEpBⓈ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

yy3Q_C#;ZVy&+J-Tv2 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 diff --git a/docs/references/api/resource_embedding.rst b/docs/references/api/resource_embedding.rst index 3870f298a..6e12171c2 100644 --- a/docs/references/api/resource_embedding.rst +++ b/docs/references/api/resource_embedding.rst @@ -10,10 +10,12 @@ PostgREST allows including related resources in a single API call. This reduces Foreign Key Joins ================= -The server uses **Foreign Keys** to determine which tables and views can be joined together. +The server uses **Foreign Keys** to determine which database objects can be joined together. It supports joining tables, views and +table-valued functions. -- For joining tables, it reads foreign keys (respecting composite keys) and generates a join condition based on the foreign key columns. -- For joining views, it reads the base tables of the views' definition, and generates a join condition based on the foreign key columns of the base tables. +- For tables, it generates a join condition using the foreign keys columns (respecting composite keys). +- For views, it generates a join condition using the views' base tables foreign key columns. +- For table-valued functions, it generates a join condition based on the foreign key columns of the function return type. .. important:: @@ -55,6 +57,13 @@ For example, consider a database of films and their awards: language text ); + CREATE TABLE technical_specs( + film_id INT REFERENCES films UNIQUE, + runtime TIME, + camera TEXT, + sound TEXT + ); + create table roles( film_id int references films(id), actor_id int references actors(id), @@ -80,7 +89,7 @@ For example, consider a database of films and their awards: Many-to-one relationships ------------------------- -Since ``films`` has a **foreign key** to ``directors``, this establishes a many-to-one relationship. Thus, we're able to request all the films and the director for each film. +Since ``films`` has a **foreign key** to ``directors``, this establishes a many-to-one relationship. This enables us to request all the films and the director for each film. .. tabs:: @@ -183,9 +192,19 @@ The **foreign key reference** establishes the inverse one-to-many relationship. Many-to-many relationships -------------------------- -The join table determines many-to-many relationships. It must contain foreign keys to other two tables and they must be part of its composite key. +The join table determines many-to-many relationships. It must contain foreign keys to other two tables and they must be part of its composite key. In the :ref:`sample film database `, ``roles`` is taken as a join table. -Thus, it can detect the join table ``roles`` between ``films`` and ``actors``: +The join table is also detected if the composite key has additional columns. + +.. code-block:: postgresql + + create table roles( + id int generated always as identity, + , film_id int references films(id) + , actor_id int references actors(id) + , character text, + , primary key(id, film_id, actor_id) + ); .. tabs:: @@ -209,18 +228,6 @@ Thus, it can detect the join table ``roles`` between ``films`` and ``actors``: ".." ] -The join table can also be detected if the composite key has additional columns: - -.. code-block:: postgresql - - create table roles( - id int generated always as identity, - , film_id int references films(id) - , actor_id int references actors(id) - , character text, - , primary key(id, film_id, actor_id) - ); - .. _one-to-one: One-to-one relationships @@ -228,7 +235,7 @@ One-to-one relationships One-to-one relationships are detected in two ways. -- When the foreign key is a primary key as specified in the :ref:`DB structure example `. +- When the foreign key is a primary key as specified in the :ref:`sample film database `. - When the foreign key has a unique constraint. .. code-block:: postgresql @@ -265,30 +272,24 @@ One-to-one relationships are detected in two ways. Computed Relationships ====================== -You can manually define relationships between using functions. This is useful for database objects that can't define foreign keys, like `Foreign Data Wrappers `_. +You can manually define relationships by using functions. This is useful for database objects that can't define foreign keys, like `Foreign Data Wrappers `_. Assuming there's a foreign table ``premieres`` that we want to relate to ``films``. -.. tabs:: +.. code-block:: postgresql - .. group-tab:: ERD + create foreign table premieres ( + id integer, + location text, + "date" date, + film_id integer + ) server import_csv options ( filename '/tmp/directors.csv', format 'csv'); - .. image:: ../../_static/premieres.png + create function film(premieres) returns setof films rows 1 as $$ + select * from films where id = $1.film_id + $$ stable language sql; - .. code-tab:: postgresql SQL - - create foreign table premieres ( - id integer, - location text, - "date" date, - film_id integer - ) server import_csv options ( filename '/tmp/directors.csv', format 'csv'); - - create function film(premieres) returns setof films rows 1 as $$ - select * from films where id = $1.film_id - $$ stable language sql; - -The above function (see the **SQL** tab) defines a relationship between ``premieres`` (the parameter) and ``films`` (the return type). Since there's a ``rows 1``, this defines a many-to-one relationship. +The above function defines a relationship between ``premieres`` (the parameter) and ``films`` (the return type). 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:: @@ -363,36 +364,31 @@ Thanks to overloaded functions, you can use the same function name for different select * from directors where film_school_id = $1.id $$ stable language sql; -Computed relationships have good performance as their intended design enable `inlining `_. +Computed relationships have good performance as their intended design enable `function inlining `_. .. 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. + - Always use ``SETOF`` when creating computed relationships. Functions can return a table without using ``SETOF``, but bear in mind that PostgreSQL will not inline them. - 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, it will unnest the embedding and return repeated values for the top level resource. .. _embed_disamb: .. _target_disamb: +.. _hint_disamb: .. _complex_rels: -FK Joins on Multiple Foreign Key Relationships -============================================== +Foreign Key Joins on Multiple Foreign Key Relationships +======================================================= When there are multiple foreign keys between tables, :ref:`fk_join` need disambiguation to resolve which foreign key columns to use for the join. +To do this, you can specify a foreign key by using the ``!hint`` syntax. -.. code:: +.. _multiple_m2o: - HTTP/1.1 300 Multiple Choices +Multiple Many-To-One +-------------------- - { - "code": "PGRST201", - "details": [ "..." ], - "hint": "...", - "message": "Could not embed because more than one relationship was found for 'sites' and 'big_projects'" - } - -Instead of the **table name**, you can specify the **foreign key constraint name** or the **column name** that is part of the foreign key. -For example, let's use the following tables: +For example, suppose you have the following ``orders`` and ``addresses`` tables: .. tabs:: @@ -415,23 +411,41 @@ For example, let's use the following tables: name text, billing_address_id int, shipping_address_id int, - constraint billing_address - foreign key(billing_address_id) references addresses(id), - constraint shipping_address - foreign key(shipping_address_id) references addresses(id) + constraint billing foreign key(billing_address_id) references addresses(id), + constraint shipping foreign key(shipping_address_id) references addresses(id) ); -To successfully join ``orders`` with the billing and shipping ``addresses``, use the corresponding foreign key constraints: +Since the ``orders`` table has two foreign keys to the ``addresses`` table, a foreign key join is ambiguous and PostgREST will respond with an error: .. tabs:: .. code-tab:: http - GET /orders?select=name,billing_address(name),shipping_address(name) HTTP/1.1 + GET /orders?select=*,addresses(*) HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/orders?select=name,billing_address(name),shipping_address(name)" + curl "http://localhost:3000/orders?select=*,addresses(*)" -i + + +.. code-block:: http + + HTTP/1.1 300 Multiple Choices + + {..} + + +To successfully join ``orders`` with ``addresses``, you can specify the foreign key name like so: + +.. tabs:: + + .. code-tab:: http + + GET /orders?select=name,billing_address:addresses!billing(name),shipping_address:addresses!shipping(name) HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/orders?select=name,billing_address:addresses!billing(name),shipping_address:addresses!shipping(name)" .. code-block:: json @@ -447,43 +461,49 @@ To successfully join ``orders`` with the billing and shipping ``addresses``, use } ] -.. _hint_disamb: +Note that ``!billing`` and ``!shipping`` are foreign keys names, which have been named explicitly in the :ref:`SQL definition above `. -Multiple FK Relationships to Many Resources -------------------------------------------- +.. _multiple_o2m: -Additionally, let's create two views for ``addresses``: ``central_addresses`` and ``eastern_addresses``. -Using the the view name is not enough to join ``orders`` with any of them. -To solve this, you need to add the foreign key as a hint: +Multiple One-To-Many +-------------------- + +Let's take the tables from :ref:`multiple_m2o`. To get the opposite one-to-many relationship, we can also specify the foreign key name: .. tabs:: .. code-tab:: http - GET /orders?select=name,central_addresses!billing_address(name),central_addresses!shipping_address(name) HTTP/1.1 + GET /addresses?select=name,billing_orders:orders!billing(name),shipping_orders!shipping(name)&id=eq.1 HTTP/1.1 .. code-tab:: bash Curl - curl "http://localhost:3000/orders?select=name,central_addresses!billing_address(name),central_addresses!shipping_address(name)" + curl "http://localhost:3000/addresses?select=name,billing_orders:orders!billing(name),shipping_orders!shipping(name)&id=eq.1" .. code-block:: json [ { - "name": "Personal Water Filter", - "billing_address": { - "name": "32 Glenlake Dr.Dearborn, MI 48124" - }, - "shipping_address": { - "name": "30 Glenlake Dr.Dearborn, MI 48124" - } + "name": "32 Glenlake Dr.Dearborn, MI 48124", + "billing_orders": [ + { "name": "Personal Water Filter" }, + { "name": "Coffee Machine" } + ], + "shipping_orders": [ + { "name": "Coffee Machine" } + ] } ] +Recursive Relationships +----------------------- + +To disambiguate recursive relationships, PostgREST requires :ref:`computed_relationships`. + .. _recursive_o2o_embed: Recursive One-To-One --------------------- +~~~~~~~~~~~~~~~~~~~~ .. tabs:: @@ -541,7 +561,7 @@ Now, to query a president with their predecessor and successor: .. _recursive_o2m_embed: Recursive One-To-Many ---------------------- +~~~~~~~~~~~~~~~~~~~~~ .. tabs:: @@ -593,7 +613,7 @@ Now, the query would be: .. _recursive_m2o_embed: Recursive Many-To-One ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ Let's take the same ``employees`` table from :ref:`recursive_o2m_embed`. To get the Many-To-One relationship, that is, the employees with their respective supervisor, you need to create a function like this one: @@ -630,7 +650,7 @@ Then, the query would be: .. _recursive_m2m_embed: Recursive Many-To-Many ----------------------- +~~~~~~~~~~~~~~~~~~~~~~ .. tabs:: @@ -703,8 +723,8 @@ Then, the request would be: .. _embedding_partitioned_tables: -FK Joins on Partitioned Tables -============================== +Foreign Key Joins on Partitioned Tables +======================================= Foreign Key joins can also be done between `partitioned tables `_ and other tables. @@ -749,17 +769,17 @@ Since it contains the ``films_id`` foreign key, it is possible to join ``box_off .. note:: - * FK joins 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:`computed_relationships` can be used if this is needed. + * Foreign key joins 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:`computed_relationships` 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_views: -FK Joins on Views -================= +Foreign Key Joins on Views +========================== -PostgREST will infer the relationships of a view based on its base tables. Base 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. +PostgREST will infer the foreign keys of a view using its base tables. Base tables are the ones referenced in the ``FROM`` and ``JOIN`` clauses of the view definition. +The foreign keys' columns must be present in the top ``SELECT`` clause of the view for this to work. For instance, the following view has ``nominations``, ``films`` and ``competitions`` as base tables: @@ -792,7 +812,7 @@ It's also possible to foreign key join `Materialized Views `_. This may fail depending on the complexity of the view. @@ -802,15 +822,15 @@ It's also possible to foreign key join `Materialized Views ` that returns a table type, you can do a Foreign Key join on the result. @@ -845,6 +865,59 @@ A request with ``directors`` embedded: } ] +.. _mutation_embed: + +Foreign Key Joins on Writes +=========================== + +You can join related database objects after doing :ref:`insert`, :ref:`update` or :ref:`delete`. + +Say you want to insert a **film** and then get some of its attributes plus join its **director**. + +.. tabs:: + + .. code-tab:: 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" + } + + .. 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: + +.. code-block:: json + + { + "title": "127 hours", + "year": 2010, + "director": { + "first_name": "Danny", + "last_name": "Boyle" + } + } + .. _nested_embedding: Nested Embedding @@ -1157,55 +1230,3 @@ You can use this to get the columns of a join table in a many-to-many relationsh The spread operator ``...`` is borrowed from the Javascript `spread syntax `_. -.. _mutation_embed: - -Embedding after Insertions/Updates/Deletions -============================================ - -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**. - -.. tabs:: - - .. code-tab:: 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" - } - - .. 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: - -.. code-block:: json - - { - "title": "127 hours", - "year": 2010, - "director": { - "first_name": "Danny", - "last_name": "Boyle" - } - } From c229bee681459ac4b07c36dee093c0dfd8845739 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 9 Aug 2023 23:48:18 -0500 Subject: [PATCH 642/652] add disamb error --- docs/references/api/resource_embedding.rst | 28 +++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/docs/references/api/resource_embedding.rst b/docs/references/api/resource_embedding.rst index 6e12171c2..4aa29d663 100644 --- a/docs/references/api/resource_embedding.rst +++ b/docs/references/api/resource_embedding.rst @@ -15,7 +15,7 @@ table-valued functions. - For tables, it generates a join condition using the foreign keys columns (respecting composite keys). - For views, it generates a join condition using the views' base tables foreign key columns. -- For table-valued functions, it generates a join condition based on the foreign key columns of the function return type. +- For table-valued functions, it generates a join condition based on the foreign key columns of the returned table type. .. important:: @@ -381,7 +381,7 @@ Foreign Key Joins on Multiple Foreign Key Relationships ======================================================= When there are multiple foreign keys between tables, :ref:`fk_join` need disambiguation to resolve which foreign key columns to use for the join. -To do this, you can specify a foreign key by using the ``!hint`` syntax. +To do this, you can specify a foreign key by using the ``!`` syntax. .. _multiple_m2o: @@ -432,10 +432,28 @@ Since the ``orders`` table has two foreign keys to the ``addresses`` table, a fo HTTP/1.1 300 Multiple Choices - {..} +.. code-block:: json + { + "code": "PGRST201", + "details": [ + { + "cardinality": "many-to-one", + "embedding": "orders with addresses", + "relationship": "billing using orders(billing_address_id) and addresses(id)" + }, + { + "cardinality": "many-to-one", + "embedding": "orders with addresses", + "relationship": "shipping using orders(shipping_address_id) and addresses(id)" + } + ], + "hint": "Try changing 'addresses' to one of the following: 'addresses!billing', 'addresses!shipping'. Find the desired relationship in the 'details' key.", + "message": "Could not embed because more than one relationship was found for 'orders' and 'addresses'" + } -To successfully join ``orders`` with ``addresses``, you can specify the foreign key name like so: +To successfully join ``orders`` with ``addresses``, we can follow the error ``hint`` which tells us to add the foreign key name as ``!billing`` or ``!shipping``. +Note that the foreign keys have been named explicitly in the :ref:`SQL definition above `. To make the result clearer we'll also alias the tables: .. tabs:: @@ -461,8 +479,6 @@ To successfully join ``orders`` with ``addresses``, you can specify the foreign } ] -Note that ``!billing`` and ``!shipping`` are foreign keys names, which have been named explicitly in the :ref:`SQL definition above `. - .. _multiple_o2m: Multiple One-To-Many From daad21e9ec1b1275ef627804a298396b2d8a2679 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 10 Aug 2023 01:15:59 -0500 Subject: [PATCH 643/652] add note to impersonated role settings --- docs/references/auth.rst | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/docs/references/auth.rst b/docs/references/auth.rst index a25b08be1..3e7428f2e 100644 --- a/docs/references/auth.rst +++ b/docs/references/auth.rst @@ -37,6 +37,24 @@ The picture below shows how the server handles authentication. If auth succeeds, This role switching mechanism is called **user impersonation**. In PostgreSQL it's done with the ``SET ROLE`` statement. +.. _impersonated_settings: + +Impersonated Role Settings +-------------------------- + +The impersonated role has its settings applied. For example, if you do: + +.. code-block:: postgresql + + ALTER ROLE webuser SET statement_timeout TO '5s'; + +Every ``webuser`` :ref:`transaction ` gets its queries executed with a ``statement_timeout`` of 5 seconds. + +.. note:: + + Settings that have a high privilege context (like ``superuser``) won't be applied, only settings that have a ``user`` context will be. This is so we don't cause permission errors. + For more details see `Understanding Postgres Parameter Context `_. + .. _jwt_impersonation: JWT-Based User Impersonation @@ -71,7 +89,7 @@ If the client included no JWT (or one without a role claim) then PostgREST switc JWT Generation ~~~~~~~~~~~~~~ -You can create a valid JWT either from inside your database(see :ref:`sql_user_management`) or via an external service(see :ref:`external_jwt`). +You can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_jwt`). .. _client_auth: @@ -188,16 +206,3 @@ doing custom logic based on the web user info. END IF; END $$ LANGUAGE plpgsql; - -.. _impersonated_settings: - -Impersonated Role Settings --------------------------- - -The :ref:`Impersonated Role ` settings are applied. For example, if you do: - -.. code-block:: postgresql - - ALTER ROLE webuser SET statement_timeout TO '5s'; - -Every ``webuser`` :ref:`transaction ` gets its queries executed with a ``statement_timeout`` of 5 seconds. From b33bfb656d5d1ec233130b82f0db4b183d4b49eb Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 10 Aug 2023 01:24:02 -0500 Subject: [PATCH 644/652] add preference-applied for return=* --- docs/references/api/tables_views.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/references/api/tables_views.rst b/docs/references/api/tables_views.rst index 469ad612f..518a58e2e 100644 --- a/docs/references/api/tables_views.rst +++ b/docs/references/api/tables_views.rst @@ -750,6 +750,7 @@ If the table has a primary key, the response can contain a :code:`Location` head HTTP/1.1 201 Created Location: /projects?id=eq.34 + Preference-Applied: return=headers-only Prefer: return=representation ----------------------------- @@ -775,7 +776,7 @@ On the other end of the spectrum you can get the full created object back in the .. code:: HTTP/1.1 201 Created - Transfer-Encoding: chunked + Preference-Applied: return=representation [ { From 78d9880607907a35639aa5ae11c27c3ec5bbb054 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 10 Aug 2023 02:11:41 -0500 Subject: [PATCH 645/652] add nulls=stripped --- .../api/resource_representation.rst | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/references/api/resource_representation.rst b/docs/references/api/resource_representation.rst index 8a1f80529..76c0c5511 100644 --- a/docs/references/api/resource_representation.rst +++ b/docs/references/api/resource_representation.rst @@ -85,6 +85,43 @@ 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. +Stripped Nulls +-------------- + +By default PostgREST returns all JSON null values. For example, requesting ``/projects?id=gt.10`` returns + +.. code:: json + + [ + { "id": 11, "name": "OSX", "client_id": 1, "another_col": "val" }, + { "id": 12, "name": "ProjectX", "client_id": null, "another_col": null }, + { "id": 13, "name": "Y", "client_id": null, "another_col": null } + ] + +On large result sets, the unused keys with ``null`` values can waste bandwith unnecessarily. To remove them, specify ``nulls=stripped`` as a parameter of ``application/vnd.pgrst.array``: + +.. tabs:: + + .. code-tab:: http + + GET /projects?id=gt.10 HTTP/1.1 + Accept: application/vnd.pgrst.array+json;nulls=stripped + + .. code-tab:: bash Curl + + curl "http://localhost:3000/projects?id=gt.10" \ + -H "Accept: application/vnd.pgrst.array+json;nulls=stripped" + +This returns + +.. code:: json + + [ + { "id": 11, "name": "OSX", "client_id": 1, "another_col": "val" }, + { "id": 12, "name": "ProjectX" }, + { "id": 13, "name": "Y"} + ] + .. _scalar_return_formats: Scalar Function Response Format From cacbbc31fee8bb072f870b77266179b7c84c2dc9 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Thu, 10 Aug 2023 02:27:21 -0500 Subject: [PATCH 646/652] add setof record functions --- docs/references/api/stored_procedures.rst | 33 +++++++++++++++++++++++ postgrest.dict | 1 + 2 files changed, 34 insertions(+) diff --git a/docs/references/api/stored_procedures.rst b/docs/references/api/stored_procedures.rst index e5098864b..9aa28d9e2 100644 --- a/docs/references/api/stored_procedures.rst +++ b/docs/references/api/stored_procedures.rst @@ -420,6 +420,39 @@ PostgREST will detect if the function is scalar or table-valued and will shape t To manually choose a return format such as binary, plain text or XML, see the section :ref:`scalar_return_formats`. +.. _untyped_functions: + +Untyped functions +----------------- + +Functions that return ``record`` or ``SETOF record`` are supported: + +.. code-block:: postgres + + create function projects_setof_record() returns setof record as $$ + select * from projects; + $$ language sql; + +.. tabs:: + + .. code-tab:: http + + GET /rpc/projects_setof_record HTTP/1.1 + + .. code-tab:: bash Curl + + curl "http://localhost:3000/rpc/projects_setof_record" + +.. code-block:: json + + [{"id":1,"name":"Windows 7","client_id":1}, + {"id":2,"name":"Windows 10","client_id":1}, + {"id":3,"name":"IOS","client_id":2}] + +However note that they will fail when trying to use :ref:`v_filter` and :ref:`h_filter` on them. + +So while they can be used for quick tests, it's recommended to always choose a strict return type for the function. + Overloaded functions -------------------- diff --git a/postgrest.dict b/postgrest.dict index c5070d1dd..f4aa4dd42 100644 --- a/postgrest.dict +++ b/postgrest.dict @@ -182,6 +182,7 @@ unicode unikernel unix updatable +Untyped UPSERT Upsert upsert From 005f9e4d59fc7e283c18f2a685a7a86b956bdf20 Mon Sep 17 00:00:00 2001 From: steve-chavez Date: Wed, 4 Oct 2023 12:24:04 -0300 Subject: [PATCH 647/652] releases: add v11.2.1 --- docs/releases/v11.2.1.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 docs/releases/v11.2.1.rst diff --git a/docs/releases/v11.2.1.rst b/docs/releases/v11.2.1.rst new file mode 100644 index 000000000..e5cf84b9b --- /dev/null +++ b/docs/releases/v11.2.1.rst @@ -0,0 +1,4 @@ +11.2.1 +====== + +v11.2.1 is a maintenance release that addresses bugs introduced in v11.2.0. Also PostgreSQL 16 is now tested and confirmed to work. For more details, see https://github.com/PostgREST/postgrest/releases/tag/v11.2.1. From 23790b7e8d2db403f8dbdfa245291ce423c2c6d5 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 11 Sep 2023 15:19:10 -0500 Subject: [PATCH 648/652] Remove former sponsor --- docs/index.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index d803c4a34..bd4a19d69 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -42,10 +42,6 @@ Sponsors :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 From f975830d313c8a367ecc3acb2884b492b7db0f7b Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Mon, 6 Nov 2023 22:49:53 -0500 Subject: [PATCH 649/652] add Neon as new sponsor (#693) --- docs/_static/neon.jpg | Bin 0 -> 79105 bytes docs/index.rst | 8 ++++++-- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 docs/_static/neon.jpg diff --git a/docs/_static/neon.jpg b/docs/_static/neon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5f4819a24830e1db09694ba18ca8d4b16ba634ae GIT binary patch literal 79105 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 KPMVPae-i+?QWjbO literal 0 HcmV?d00001 diff --git a/docs/index.rst b/docs/index.rst index bd4a19d69..b007bf8a7 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -46,12 +46,16 @@ Sponsors :target: https://gnuhost.eu/?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/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 + .. image:: _static/neon.jpg + :target: https://neon.tech/?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 From 6af706facd69de3fad3fd86875d16a3b662a1fe6 Mon Sep 17 00:00:00 2001 From: Laurence Isla Date: Tue, 7 Nov 2023 17:24:46 -0500 Subject: [PATCH 650/652] Remove former sponsors (#694) --- docs/index.rst | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index b007bf8a7..83232238b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -38,17 +38,11 @@ Sponsors :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/gnuhost.png :target: https://gnuhost.eu/?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/supabase.png :target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage From c1c45060baeb439cd3b56cc79b831732cf4efb95 Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sat, 17 Feb 2024 12:53:41 +0100 Subject: [PATCH 651/652] 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 => docs.yaml} | 14 +++++++------- .readthedocs.yaml | 2 +- .gitignore => docs/.gitignore | 2 +- README.md => docs/README.md | 0 {diagrams => docs/_diagrams}/README.md | 4 ++-- {diagrams => docs/_diagrams}/boxoffice.er | 0 {diagrams => docs/_diagrams}/db.tex | 0 {diagrams => docs/_diagrams}/employees.er | 0 {diagrams => docs/_diagrams}/film.er | 0 {diagrams => docs/_diagrams}/orders.er | 0 {diagrams => docs/_diagrams}/premieres.er | 0 {diagrams => docs/_diagrams}/presidents.er | 0 {diagrams => docs/_diagrams}/users.er | 0 default.nix => docs/default.nix | 13 +++++++++---- .../extensions}/sphinx-copybutton.nix | 0 {extensions => docs/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 20 files changed, 22 insertions(+), 17 deletions(-) rename .github/workflows/{ci.yaml => docs.yaml} (77%) rename .gitignore => docs/.gitignore (79%) rename README.md => docs/README.md (100%) rename {diagrams => docs/_diagrams}/README.md (91%) rename {diagrams => docs/_diagrams}/boxoffice.er (100%) rename {diagrams => docs/_diagrams}/db.tex (100%) rename {diagrams => docs/_diagrams}/employees.er (100%) rename {diagrams => docs/_diagrams}/film.er (100%) rename {diagrams => docs/_diagrams}/orders.er (100%) rename {diagrams => docs/_diagrams}/premieres.er (100%) rename {diagrams => docs/_diagrams}/presidents.er (100%) rename {diagrams => docs/_diagrams}/users.er (100%) rename default.nix => docs/default.nix (79%) 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/docs.yaml similarity index 77% rename from .github/workflows/ci.yaml rename to .github/workflows/docs.yaml index 782b3eceb..f9bf2fd3a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/docs.yaml @@ -1,14 +1,14 @@ -name: CI +name: Docs on: push: branches: - main - - v* + - rel-* pull_request: branches: - main - - v* + - rel-* jobs: build: @@ -17,7 +17,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: cachix/install-nix-action@v22 - - run: nix-env -f default.nix -iA build + - run: nix-env -f docs/default.nix -iA build - run: postgrest-docs-build spellcheck: @@ -26,7 +26,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: cachix/install-nix-action@v22 - - run: nix-env -f default.nix -iA spellcheck + - run: nix-env -f docs/default.nix -iA spellcheck - run: postgrest-docs-spellcheck dictcheck: @@ -35,7 +35,7 @@ jobs: steps: - uses: actions/checkout@v3 - uses: cachix/install-nix-action@v22 - - run: nix-env -f default.nix -iA dictcheck + - run: nix-env -f docs/default.nix -iA dictcheck - run: postgrest-docs-dictcheck linkcheck: @@ -45,6 +45,6 @@ jobs: steps: - uses: actions/checkout@v3 - uses: cachix/install-nix-action@v22 - - run: nix-env -f default.nix -iA linkcheck + - run: nix-env -f docs/default.nix -iA linkcheck - run: postgrest-docs-linkcheck diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 4690fde9d..0d7162c8f 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ sphinx: configuration: docs/conf.py python: install: - - requirements: requirements.txt + - requirements: docs/requirements.txt build: os: ubuntu-22.04 tools: diff --git a/.gitignore b/docs/.gitignore similarity index 79% rename from .gitignore rename to docs/.gitignore index bab47b52e..503e5d524 100644 --- a/.gitignore +++ b/docs/.gitignore @@ -2,7 +2,7 @@ _build Pipfile.lock *.aux *.log -diagrams/db.pdf +_diagrams/db.pdf misspellings unuseddict .history 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 91% rename from diagrams/README.md rename to docs/_diagrams/README.md index fa4a1e7fe..7014ae378 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 ``` The fonts used belong to the GNU FreeFont family. You can download them here: http://ftp.gnu.org/gnu/freefont/ @@ -20,7 +20,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/boxoffice.er b/docs/_diagrams/boxoffice.er similarity index 100% rename from diagrams/boxoffice.er rename to docs/_diagrams/boxoffice.er 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/employees.er b/docs/_diagrams/employees.er similarity index 100% rename from diagrams/employees.er rename to docs/_diagrams/employees.er 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/diagrams/premieres.er b/docs/_diagrams/premieres.er similarity index 100% rename from diagrams/premieres.er rename to docs/_diagrams/premieres.er diff --git a/diagrams/presidents.er b/docs/_diagrams/presidents.er similarity index 100% rename from diagrams/presidents.er rename to docs/_diagrams/presidents.er diff --git a/diagrams/users.er b/docs/_diagrams/users.er similarity index 100% rename from diagrams/users.er rename to docs/_diagrams/users.er diff --git a/default.nix b/docs/default.nix similarity index 79% rename from default.nix rename to docs/default.nix index 42fbaca76..08f654e59 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 \ @@ -82,7 +86,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 8dae16577..741633d43 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 68d83f74630091611d394cebb6b936b2e2b6ca1f Mon Sep 17 00:00:00 2001 From: Wolfgang Walther Date: Sat, 17 Feb 2024 12:57:18 +0100 Subject: [PATCH 652/652] 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 5125f8299..38256b280 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,48 +18,45 @@ 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('.')) +# 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'11.1' +version = "11.1" # The full version, including alpha/beta/rc tags. -release = u'11.1.0' +release = "11.1.0" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -70,38 +67,38 @@ language = None # 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 @@ todo_include_todos = False # 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 @@ man_pages = [ # (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 08f654e59..f57a96813 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 f90b1c18d..fbc164419 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 741633d43..67a2165d6 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 e8d38d96e..8e116f031 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";

+ Windows +

It isn't fun. Learn more here.

+

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

+