Compare commits

..
221 Commits
Author SHA1 Message Date
Wolfgang Walther 6a506d18c0 ci: Actually pass GHC_VERSION to arm build script 2024-05-09 21:54:11 +02:00
Wolfgang Walther 711202c2a0 ci: Make arm scripts fail on error 2024-05-09 21:43:42 +02:00
Wolfgang Walther 9dd3f3c094 ci: Tag arm docker image properly for releases 2024-05-09 21:42:18 +02:00
Wolfgang Walther cf4aa20d7c ci: Extract changelog properly for releases 2024-05-09 21:42:17 +02:00
Wolfgang WaltherandWolfgang Walther 1afd98a739 bump version to 12.0.3 2024-05-09 18:56:47 +02:00
Taimoor ZaeemandWolfgang Walther 077e4a857e fix: fix wrong http status on pg error 42P17 infinite recursion 2024-05-09 18:56:47 +02:00
Laurence IslaandWolfgang Walther 903537d39d fix: OpenAPI now tags a FK correctly on O2O relationships 2024-05-09 18:56:47 +02:00
Taimoor ZaeemandWolfgang Walther 67f6b1c7e8 fix: remove rejected mediatype application/vnd.pgrst.object+json from response 2024-05-09 18:56:47 +02:00
steve-chavezandWolfgang Walther 977710f442 fix: clarify PGRST204 error message 2024-05-09 18:56:47 +02:00
Wolfgang WaltherandWolfgang Walther 687558937c fix: Avoid casting to table type when select= and media type handler are used
Previously using a generic mimetype handler failed when any kind of select= was given, because
we tried to cast the select-result to the original table type. With this change, this cast is
only applied when select=* is given implicitly or explicitly. This is the only case where this
makes sense, because this guarantees that correct columns are selected in the correct order for
this cast to succeed.

Resolves #3160
2024-05-09 18:56:47 +02:00
Wolfgang WaltherandWolfgang Walther 491c7a0891 fix: Return 406 instead of 415 for non-acceptable media type
415 is for Content-Type and 406 for Accept headers.
2024-05-09 18:56:47 +02:00
steve-chavezandWolfgang Walther e4984dc751 fix: wrong subquery error returning as 400 status 2024-05-09 18:56:47 +02:00
steve-chavezandWolfgang Walther 1b0f29a8cc fix: misleading "Starting.." logs on scache reload 2024-05-09 18:56:47 +02:00
Wolfgang Walther a0d3ebf590 ci: Fetch tags before checking whether tag exists 2024-05-09 14:53:16 +02:00
Wolfgang WaltherandWolfgang Walther ac217c120f ci: Automate patch releases and pre-releases
This work by automatically pushing a new tag on main and release
branches after each commit. The tag will be "devel" on main and the
version from postgrest.cabal for release branches. The release
workflow then runs as a tag pipeline, making the actual release.

For release branches, the tag will only be created if a tag for this
version doesn't exist, yet. This means to actually make a new patch
release, we still need to bump the version in postgrest.cabal. We
can automate this later as part of our backport-bot.

This is a back-port of the following commits:
- dd8d51ab
- fe0f2f70
- 58d81334
- 9fe90bf9
- c67f1c39
- 8433f981
- d9ba9a82
- a57d12b1
- b006016d
2024-05-09 14:08:42 +02:00
renovate[bot]andWolfgang Walther 961db7c7ef chore(deps): update actions/checkout action to v4.1.5 2024-05-09 13:37:11 +02:00
Wolfgang Walther c128f03b61 ci: Improve performance for nix jobs in CI
Defaulting to max-jobs = auto should improve build times by using more
cores.

Setting always-allow-substitutes to true should cause all nix
derivations to be cached on cachix, which should improve performance of
the MacOS job dramatically, when no rebuilds need to happen.
2024-05-09 13:10:05 +02:00
Wolfgang Walther b8b34f05df ci: Make artifact-from-cirrus action succeed when cirrus job doesn't start up in PR 2024-05-09 13:10:05 +02:00
Wolfgang Walther 608ee5a9ad ci: Lift restriction to nix 2.13.6 2024-05-05 19:01:52 +02:00
renovate[bot]andWolfgang Walther 9737c5c5f6 chore(deps): update all dependencies 2024-05-05 18:25:18 +02:00
Wolfgang Walther e520f32297 ci: Refactor get_cirrusci_freebsd script to GitHub action
This should make it more reliable and also easier to re-use, if we need
to.

Resolves #2555
2024-05-04 15:02:25 +02:00
renovate[bot]andWolfgang Walther 19dcf7b344 chore(deps): update all dependencies 2024-05-04 12:20:53 +02:00
renovate[bot]andWolfgang Walther a2b7b0e69d chore(deps): update all dependencies 2024-04-26 08:31:43 +02:00
renovate[bot]andWolfgang Walther e01f08392f chore(deps): update actions/download-artifact action to v4.1.7 2024-04-24 20:53:27 +02:00
renovate[bot]andWolfgang Walther e6282d88d8 chore(deps): update all dependencies 2024-04-23 08:07:48 +02:00
Wolfgang Walther 5cab03ad5d ci: Use explicit runner image versions 2024-04-22 21:25:54 +02:00
renovate[bot]andWolfgang Walther b71739ee50 chore(deps): update all dependencies 2024-04-20 12:45:26 +02:00
renovate[bot]andWolfgang Walther 0d60a79532 chore(deps): update codecov/codecov-action action to v4.2.0 2024-04-04 07:17:34 +02:00
renovate[bot]andWolfgang Walther d38e5cdb02 chore(deps): update codecov/codecov-action action to v4.1.1 2024-03-26 19:56:25 +01:00
renovate[bot]andWolfgang Walther 908cdf0b6f chore(deps): update actions/cache action to v4.0.2 2024-03-19 21:59:00 +01:00
renovate[bot]andWolfgang Walther ee55efe0c9 chore(deps): update nixbuild/nix-quick-install-action action to v27 2024-03-15 17:13:16 +01:00
renovate[bot]andWolfgang Walther e60bae8e32 chore(deps): update cachix/install-nix-action action to v26 2024-03-08 16:29:32 +01:00
renovate[bot]andWolfgang Walther ffe44a2e71 chore(deps): update ubuntu:jammy docker digest to 77906da 2024-03-07 16:28:05 +01:00
renovate[bot]andWolfgang Walther dcc6772ce1 chore(deps): update actions/download-artifact action to v4.1.4 2024-03-02 12:10:17 +01:00
Wolfgang Walther 91ffee1d2e chore: Add full semver comment to used actions 2024-03-01 08:58:33 +01:00
renovate[bot]andWolfgang Walther 1b52ce1958 chore(deps): update actions/cache digest to ab5e6d0 2024-03-01 08:12:03 +01:00
Wolfgang Walther 8186e78691 nix: Remove broken docker-based nix development environment
It used to be possible to spin up a nix environment via docker container this way,
but the upstream nixos/nix image has changed and the docker build doesn't succeed
anymore. Since nobody complained about that, we can assume it is not being used
anyway.
2024-02-29 08:22:52 +01:00
renovate[bot]andWolfgang Walther b9186b6d9a chore(deps): update all dependencies 2024-02-27 09:10:29 +01:00
Wolfgang Walther b663a9aa37 ci: Use actions/download-artifact instead of dawidd6/action-download-artifact 2024-02-24 21:58:19 +01:00
renovate[bot]andWolfgang Walther 4d874fb725 chore(deps): pin dependencies 2024-02-24 21:55:30 +01:00
renovate[bot]andWolfgang Walther 68e145550e chore(deps): update all dependencies 2024-02-24 20:38:54 +01:00
Wolfgang Walther 3406e62a77 ci: Remove nix actions cache and prepopulate job
The nix actions cache currently leads to repeated "no space left on
devices" errors for jobs in CI.

The prepopulate job is useless without the nix actions cache, so it
will go away at the same time.
2024-02-24 19:52:25 +01:00
Wolfgang Walther 3e5e8a22e4 ci: Switch action-get-latest-tag to github-action-get-previous-tag
The former is not maintained anymore and produces deprecation warnings in CI.
2024-02-24 19:04:23 +01:00
Wolfgang Walther 414a0daca1 chore(deps): update codecov/codecov-action action to v4 2024-02-24 18:43:11 +01:00
Wolfgang Walther 30f18cf22c docs: Display version number more prominently
Resolves https://github.com/PostgREST/postgrest-docs/issues/734
2024-02-19 21:50:54 +01:00
Wolfgang Walther fd9192836e chore: Add .docs-build to .gitignore to allow switching branches easily
This is only used on the latest main branch, but switching to release branches
is annoying otherwise.
2024-02-19 21:49:48 +01:00
Wolfgang Walther 02d8990ce2 ci: Change names for release branches from rel-MAJOR.MINOR to vMAJOR
This naming scheme gives us the best support for readthedocs.

References #2814
2024-02-17 17:46:11 +01:00
Wolfgang Walther 06821fdbca ci: Refactor conditions to check for branch events 2024-02-17 17:45:44 +01:00
Wolfgang Walther b77a08ff10 ci: Add freebsd executable to releases
This was temporarily disabled, because of timeouts in Cirrus. This seems to work well again.
2024-02-17 15:24:05 +01:00
Wolfgang Walther 278be7074e ci: Fix FreeBSD build on cirrus 2024-02-17 15:24:05 +01:00
Wolfgang Walther b9d46812f8 Merge branch 'docs/v12.0' into rel-12.0 2024-02-17 13:46:34 +01:00
Wolfgang Walther 7f410f3be0 chore: Run postgrest-style on docs files
This prevents CI from failing after the merge.
2024-02-17 13:43:11 +01:00
Wolfgang Walther 8aa8e682a2 chore: Prepare merge of postgrest-docs into postgrest main repo
This avoids some merge conflicts to allow git blame to detect renames properly.
2024-02-17 13:43:11 +01:00
Laurence Isla 867dc58ecc Update sponsor logo 2024-01-15 20:33:30 -05:00
Laurence Isla 6962e41a37 Fix Sphinx version error in RTD (#739) 2024-01-15 20:33:30 -05:00
ParashoeandGitHub 8605edcb6f Spelling correction (#740)
Spell correct "callounter" to "callcounter"
2024-01-15 20:18:20 -05:00
Gabriel MirandaandSteve Chavez efcf36d970 Fix secret command to avoid non-ascii alphanumerics 2023-12-30 23:10:37 -05:00
Laurence Isla f95c5127df Update sponsor image (#728) 2023-12-21 15:22:04 -05:00
Laurence Isla a4e00ffdf6 bump version to 12.0.2 2023-12-20 18:21:19 -05:00
steve-chavezandLaurence Isla cd62e39cd1 fix: empty row on handler function
Closes https://github.com/PostgREST/postgrest/issues/3126
2023-12-20 16:33:34 -05:00
steve-chavezandLaurence Isla ec0f99c686 fix: aggregates not working for all schemas
Closes https://github.com/PostgREST/postgrest/issues/3124
2023-12-20 16:33:31 -05:00
Laurence Isla b2cd365866 chore: add new sponsor 2023-12-20 16:29:37 -05:00
dependabot[bot]andLaurence Isla fcc330311f build(deps): bump actions/upload-artifact from 3 to 4 (#3123) 2023-12-20 16:29:37 -05:00
dependabot[bot]andLaurence Isla e557161b84 build(deps): bump dawidd6/action-download-artifact from 2 to 3 (#3122) 2023-12-20 16:29:37 -05:00
dependabot[bot]andLaurence Isla 962268fd6b build(deps): bump actions/download-artifact from 3 to 4 (#3121) 2023-12-20 16:29:37 -05:00
Laurence Isla cb501a9041 Add warning and fix to htmx how-to 2023-12-19 13:32:51 -05:00
steve-chavez a7d2c8e6ad docs: soap how-to is outdated
`*/*` can now be handled with media type handlers
2023-12-19 00:00:56 -05:00
Laurence Islaandsteve-chavez 40e16bd468 Add new sponsor 2023-12-18 18:23:39 -05:00
Laurence Isla 49e257617e Priorize query parameters instead of headers in limits and pagination 2023-12-13 11:48:53 -05:00
Laurence Isla 474bb93e89 Add meta tags using an Open Graph extension 2023-12-13 11:48:41 -05:00
Laurence Isla d75538ec4a Add missing curl requests in Preferences section 2023-12-13 11:48:32 -05:00
Laurence Islaandsteve-chavez fc58262cb2 Add new sponsor (#723) 2023-12-12 23:05:43 -05:00
steve-chavez fdfc095b0e fix: correct any media type handler 2023-12-12 22:46:56 -05:00
Laurence Islaandsteve-chavez eb582fb4b0 Add missing in-db configuration for jwt-cache-max-lifetime 2023-12-12 22:46:46 -05:00
Laurence Isla cd38da56d5 bump version to 12.0.1 2023-12-12 19:20:47 -05:00
steve-chavezandLaurence Isla 229a4e4cd6 fix: any handler sets a default application/json
Now it sets application/octet-stream as the generic type.
2023-12-12 18:46:40 -05:00
Laurence Isla c3f7440e33 fix: add jwt_cache_max_lifetime as an in-database configuration option (#3102) 2023-12-12 18:46:40 -05:00
Laurence Isla 4e03ee252b chore: add new sponsor 2023-12-12 18:46:40 -05:00
Andrei DziahelandLaurence Isla 0ac4d0d0a9 fix: replace json parser error with generic msg (#3090) 2023-12-12 18:46:40 -05:00
Andrei DziahelandLaurence Isla ceda77c5de ci: cancel in-progress loadtests for PRs as well 2023-12-12 18:46:40 -05:00
Andrei DziahelandLaurence Isla a6e4a0078b ci: test only head of branch 2023-12-12 18:46:40 -05:00
Laurence Isla 15f4157cc5 changelog: move missplaced unreleased fix (#3094) 2023-12-12 18:46:40 -05:00
Laurence Isla 50ea1999b2 fix: allow using special characters in json keys (#3081)
* increase memory size test
2023-12-12 18:46:40 -05:00
Andrei DziahelandLaurence Isla c92f16a2cb ci: similar jobs use a single cache for Nix stores
Jobs that use the setup-nix action will share a single cache, which is created by a previous warm up Job.
2023-12-12 18:46:40 -05:00
dependabot[bot]andLaurence Isla 1cebc03313 build(deps): bump cachix/cachix-action in /.github/actions/setup-nix (#3085) 2023-12-12 18:46:40 -05:00
steve-chavez 9f252678a4 fix: add missing pgrst.server_timing_enabled 2023-12-12 10:57:47 -05:00
steve-chavez 802131aa96 fix: any media type should be bytea 2023-12-05 17:49:46 -05:00
steve-chavez 275bdee47c fix server-timing header section
Also link it to jwt caching
2023-12-02 01:36:26 -05:00
steve-chavez f70e852a0a bump to 12.0.0 2023-12-01 19:41:04 -05:00
steve-chavez 00580bc8cb bump version to 12.0.0 2023-12-01 19:18:07 -05:00
steve-chavez 70c0d88fa7 bring back schema isolation on its own page 2023-12-01 19:03:23 -05:00
steve-chavez 2b4e0cc10a move return representation to preferences page 2023-12-01 18:32:38 -05:00
steve-chavez 1268b0a258 split pagination/count into own page 2023-12-01 16:24:52 -05:00
steve-chavez 11f40c384a use the clearer START TRANSACTION 2023-12-01 16:24:52 -05:00
Tim AbdullaandGitHub 2bb9cee996 Add documentation for aggregate functions (#701) 2023-12-01 11:38:42 -05:00
steve-chavez a38eee399e chore: improve mt handlers snippets 2023-12-01 10:59:48 -05:00
steve-chavez ee4321623f chore: improve intro of media type handlers 2023-12-01 10:51:46 -05:00
steve-chavez 49903ca6db chore: wording of media type handlers 2023-12-01 10:08:12 -05:00
steve-chavez d15e357d2e reference: media type handlers 2023-11-30 23:27:06 -05:00
Laurence IslaandGitHub 62512268cd Add PostgREST installation using package managers to tut0 2023-11-30 18:20:54 -05:00
Laurence IslaandGitHub 84152b483a Remove db-use-legacy-gucs 2023-11-29 15:37:25 -05:00
steve-chavez 9fe11249f9 feat: custom SQL handler for the "*/*" media type 2023-11-28 23:14:38 -05:00
Laurence IslaandGitHub 7640de34e2 refactor: use a data type instead of Map for Server Timing 2023-11-28 18:26:56 -05:00
Andrei DziahelandSteve Chavez 31ce39ba36 ci: cabal+GHC: tweak caching
Makes it to cache only relevant directories, adds `dist-newstyle` to prevent needless rebuilding even harder and shortens cache key name by supplying `hashFiles`multiple arguments
2023-11-28 12:48:57 -05:00
Laurence IslaandGitHub ca5eb64deb chore: fix server-timing metric order and doctest 2023-11-28 12:45:39 -05:00
Andrei DziahelandSteve Chavez a72241ad4e ci: fixes job caches not producing after recent cabal upgrade
Recent cabal adopted XDG guidelines and stores data
across multiple directories under $HOME.
Creating ~/.cabal manually returns old behavior and allows caching single directory again.
2023-11-28 11:52:52 -05:00
steve-chavez b080f59bac test: server timing on root and options method 2023-11-28 10:25:25 -05:00
steve-chavez 558e9d40e8 changelog: join server-timing entries 2023-11-28 10:25:25 -05:00
steve-chavez 958339b8d3 chore: change server timing render to response
"render" is a loaded term than might be thought as the generation
of a full HTML page. While for this phase we only process the status
and the headers.

Changing it to "response" so is not misleading at least. Users can check the
docs for clarification.
2023-11-28 10:25:25 -05:00
steve-chavez 3d56f8435d chore: delete dead error codes 2023-11-28 10:25:25 -05:00
steve-chavez e620ae5e9c pin sphinx-tabs ver 2023-11-28 07:46:13 -05:00
steve-chavez bd6d8624ab fix: sphinx breaking change
see https://github.com/sphinx-doc/sphinx/issues/10474
2023-11-28 07:46:13 -05:00
steve-chavez 03e384d55b remove release notes 2023-11-28 07:46:13 -05:00
Laurence IslaandSteve Chavez dfa875c8c7 fix: do not log internal db errors like 'acquisition timeout' when log-level=crit 2023-11-27 23:06:40 -05:00
Laurence IslaandSteve Chavez 33891e3a73 feat: log all internal database errors to stderr 2023-11-27 23:06:40 -05:00
Laurence IslaandSteve Chavez 8483459d59 chore: add missing --example for server-timing-enabled 2023-11-24 17:41:16 -05:00
Andrei DziahelandGitHub 7b5c2f03fc introducing server-timing-header config parameter (#700) 2023-11-24 16:50:28 -05:00
Laurence IslaandSteve Chavez b538ab9823 feat: add timing for the api request parse 2023-11-24 16:29:25 -05:00
Laurence IslaandSteve Chavez 850b15fe13 fix: change timing name from 'query' to 'transaction' 2023-11-24 16:29:25 -05:00
Andrei DziahelandSteve Chavez 9eda78eda5 server-port can be 0
Introduces special value of 0 for port to be assigned randomy
2023-11-23 22:35:28 -05:00
Andrei DziahelandGitHub df97a5071f Display an actual TCP port app is bound to (#3034) 2023-11-23 18:17:16 -05:00
Tim AbdullaandGitHub 1c60b50e2e Add aggregate functions (#2925)
The aggregate functions SUM(), MAX(), MIN(), AVG(), and COUNT() are now supported.
2023-11-23 14:03:03 -05:00
Andrei DziahelandGitHub c3301a1653 feat: implement server-timing-enabled config parameter (#3064) 2023-11-22 17:57:50 -05:00
Andrei DziahelandGitHub abcb21c69a ci: cache Nix store with cache-nix-action (#2992)
* ci: try nix-community/cache-nix-action

* ci: add cache-id param to setup-nix action

* ci: tidy up cache keys for non-nix jobs

* ci: merge-nix-caches-linux job

* ci: merge caches other way around

* ci: reduce number of caches

Should prevent disk overflow

* ci: change cache id prefix for merge

* ci: comment out cache merging job

* ci: revert cache id prefix

* ci: setup-nix: use latest cache-nix-action

Among others, makes action logs look more tidy (see https://github.com/nix-community/cache-nix-action/commit/17d19d3d8be918757589635bc1a6830be0b129d2)

* ci: use test-pg cache key for loadtest
2023-11-21 18:56:23 -05:00
steve-chavez 475b4601ca split role settings from function settings
Also add doc for GRANT SET ON PARAMETER, see
https://github.com/PostgREST/postgrest/pull/3058.
2023-11-21 17:49:35 -05:00
Taimoor ZaeemandSteve Chavez 49e3d2c4b4 add documentation for statement_timeout set on functions 2023-11-21 17:49:35 -05:00
steve-chavez f7bf2157f3 feat: apply super settings on impersonated roles
If they have GRANT SET ON PARAMETER <setting> TO authenticator
2023-11-21 10:54:56 -05:00
Taimoor ZaeemandGitHub 125f10a60f feat: add statement_timeout set on functions (#3056) 2023-11-17 12:36:51 -05:00
Taimoor ZaeemandGitHub 5502dce556 add documentation for timezone preference (#698) 2023-11-15 08:24:25 -05:00
Anwar KnyaneandGitHub 084e8c2950 make the jwt generation easier :) (#697) 2023-11-14 11:31:33 -05:00
Taimoor ZaeemandGitHub 3c1a7f2641 feat: add timezone in Prefer header (#3024)
* increase reloading timeout in io-tests
* increase memory test by 1M
2023-11-13 14:16:27 -05:00
omahsandGitHub f10d4139fe nix: fix typos in README 2023-11-08 11:43:42 -05:00
Laurence IslaandGitHub 7e7e494b31 Remove former sponsors (#694) 2023-11-07 17:24:46 -05:00
Laurence IslaandSteve Chavez 99b705d44f chore: remove former sponsors 2023-11-07 16:25:47 -05:00
Laurence IslaandGitHub e0586c3afa add Neon as new sponsor (#693) 2023-11-06 22:49:53 -05:00
Laurence IslaandGitHub 379d9d6298 chore: add Neon as sponsor 2023-11-06 20:02:58 -05:00
Laurence IslaandSteve Chavez 2c35257eec chore: add prefix info to the PR template 2023-11-03 19:42:06 -05:00
steve-chavez ed90774f53 test: query the empty string 2023-11-03 12:58:54 -05:00
Laurence Isla 96ca177c31 transaction-scoped settings are now shown clearly in the postgres logs 2023-11-02 15:50:22 -05:00
Laurence Isla 226400a5bc break:remove the db-use-legacy-gucs config
BREAKING CHANGE

All PostgreSQL versions will use JSON GUCs for headers, cookies and JWT claims.
2023-11-02 15:50:22 -05:00
Taimoor ZaeemandGitHub 57ceabb260 add documentation for server-cors-allowed-origins config (#688) 2023-10-30 11:08:12 -05:00
b85ee953b7 doc: media type handlers (#689)
* reference: media type handlers

* Use function instead of computed field in editable task

---------

Co-authored-by: Laurence Isla <lau.isla.c@gmail.com>
2023-10-28 12:00:37 -05:00
Laurence IslaandGitHub b235227119 allow all origins when server-cors-allowed-origins is an empty string 2023-10-27 21:30:56 -05:00
Taimoor ZaeemandGitHub 5c9c7f4ff4 fix: HTTP status responses for upserts
* PUT returns 201 instead of 200 when rows are inserted
* POST with "Prefer: resolution=merge-duplicates" returns 200 instead of 201 when no rows are inserted
2023-10-26 20:49:20 -05:00
steve-chavez 82b38341bf feat: sql handlers for custom media types
* test text/html and drop HtmlRawOutputSpec.hs
* all tests passing, removed all pendingWith
* make functions compatible with pg <= 12
* move custom media types tests to own spec
* anyelement aggregate
* apply aggregates without a final function
* overriding works
* overriding anyelement with particular agg
* cannot override vendored media types
* plan spec works with custom aggregate
* renamed media types to make clear which ones are overridable
* correct content negotiation with same weight
* text/tab-separated-values media type
* text/csv with BOM plus content-disposition header
2023-10-26 09:04:02 -05:00
steve-chavez 4a90e9fbd9 break: db-root-spec default app/openapi+json media
BREAKING CHANGE

Can be done later with custom media types
2023-10-26 09:04:02 -05:00
steve-chavez 14d030b96c break: remove binary field logic, raw-media-types
BREAKING CHANGE

Can be done later with custom media types
2023-10-26 09:04:02 -05:00
Wolfgang Walther 6920a88dc0 nix: Fix postgrest-release on rel-branches pushing to the right branch 2023-10-25 19:09:21 +02:00
Wolfgang Walther 966f8df33f changelog: update to 11.2.2 2023-10-25 19:08:04 +02:00
Wolfgang Walther 98bd1de1cd nix: Allow running postgrest-release on rel- branches 2023-10-25 16:29:03 +02:00
Taimoor ZaeemandGitHub d94286c185 feat: add config to specify CORS origins (#2986) 2023-10-24 09:38:25 -05:00
Taimoor ZaeemandGitHub 618f93dec1 test: fix typos in batch upsert tests 2023-10-21 16:29:43 -05:00
Laurence IslaandGitHub 317619bf62 fix: regression by reverting fix that returned 206 when first-pos=length in Range header 2023-10-21 16:28:08 -05:00
steve-chavez eb238ad678 test: DRY scache/config sleep in io tests
Also increase schema cache sleep to 0.2 as 0.1 tends to fail in CI.
2023-10-20 17:53:54 -05:00
steve-chavez 54786a6c04 fix: unnecessary count() on RPC returning single 2023-10-19 15:54:22 -05:00
steve-chavez 00f3cb3746 test: prove RPC returning single always gives 1 2023-10-19 15:54:22 -05:00
Andrei DziahelandGitHub e977032847 build: add cabal.project.freeze (#3004) 2023-10-19 09:26:46 -05:00
dependabot[bot]andSteve Chavez 5c314e3f02 Bump urllib3 from 2.0.6 to 2.0.7
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.6...2.0.7)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-18 13:09:04 -05:00
Taimoor ZaeemandGitHub f10b4c3268 test: add tests for batch upserts (#3005) 2023-10-13 09:47:26 -03:00
Andrei DziahelandGitHub dc01c748ae feat: add more perf counters to Server-Timing (#2983) 2023-10-12 09:55:30 -03:00
Taimoor ZaeemandGitHub bae8dc3283 add documentation for JWT caching (#683) 2023-10-10 23:22:41 -03:00
Laurence IslaandGitHub 056c748c5f refactor: DRY and enforce error format 2023-10-10 11:42:36 -05:00
Steve ChavezandGitHub 93f2b0093c references: remark NOLOGIN impersonated roles 2023-10-07 19:20:48 -03:00
Taimoor ZaeemandGitHub 5ba370fe0a add documentation for handling preference (#684) 2023-10-07 00:23:41 -03:00
Kam Ting HoiandGitHub 818387f24e fix: range request with 0 rows and 0 offset return status 416 (#2991) 2023-10-07 00:10:17 -03:00
steve-chavez a9d6c318fd nix: instructions for static binary 2023-10-06 19:54:05 -03:00
steve-chavez 2aa58164ab refactor: move errors to ApiRequestError 2023-10-06 01:16:45 -03:00
steve-chavez df08d7f3ff refactor: rename JSONParseError to PGRSTParseError
The error name is too generic otherwise.
2023-10-06 01:16:45 -03:00
steve-chavez 3dd292be46 refactor: DRY application/json on Error.hs
Small refactor to help with:

https://github.com/PostgREST/postgrest/issues/2901
2023-10-06 01:16:45 -03:00
Laurence IslaandGitHub e29260d754 Version not set when invalid URI or key/value format 2023-10-05 15:18:25 -05:00
steve-chavez d9857e1c54 releases: add v11.2.1 2023-10-04 12:25:09 -03:00
steve-chavez 0703f27d2b changelog: update to 11.2.1 2023-10-04 12:12:30 -03:00
dependabot[bot]andSteve Chavez 8a7889b9b1 Bump urllib3 from 2.0.2 to 2.0.6
Bumps [urllib3](https://github.com/urllib3/urllib3) from 2.0.2 to 2.0.6.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/2.0.2...2.0.6)

---
updated-dependencies:
- dependency-name: urllib3
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-10-03 23:41:04 -03:00
Laurence IslaandGitHub af0e369c65 fix: regression that rejects URI connection strings with certain unescaped characters in the password 2023-10-03 10:57:21 -05:00
Laurence IslaandGitHub 910950dbac fix: RPCs not embedding correctly when using overloaded functions for computed relationships 2023-10-02 16:00:33 -05:00
Andrei DziahelandSteve Chavez 4c44782d15 refactor: complete purifying Response module 2023-09-30 09:35:50 -03:00
steve-chavez 3b1eb51744 ci: fix MacOS CI 2023-09-29 19:50:15 -03:00
steve-chavez cf7ee67dfe fix: arrow filter on RPC returning TABLE+composite 2023-09-28 20:26:38 -03:00
steve-chavez 290d90609b fix: unnecessary set default_transaction_isolation 2023-09-27 17:48:48 -03:00
Taimoor ZaeemandSteve Chavez 3c1cdd434a feat: add handling=strict/lenient for Prefer header 2023-09-27 15:00:52 -03:00
steve-chavez 2825ac059e refactor: make Response module pure 2023-09-27 11:50:22 -03:00
Taimoor ZaeemandGitHub a6e3eda5b2 feat: implement JWT caching (#2928) 2023-09-25 14:46:55 -03:00
steve-chavez 10231bc1a8 reorganize errors page 2023-09-23 20:29:01 -03:00
Laurence IslaandSteve Chavez 90e3a5e29f Add test option for PostgreSQL 16 2023-09-21 15:38:46 -03:00
Andrei DziahelandSteve Chavez add4dfeed5 ci: loadtest PRs against latest releases
Leveraging the previously-improved postgrest-loadtest-against
introduces loadtesting PRS against latest release as well.
2023-09-21 14:08:53 -03:00
Andrei DziahelandSteve Chavez fca039a54d nix:postgrest-loadtest-against multiple branches
Improve postgrest-loadtest-against script to accept multiple refs
to perform loadtests against *all* of them
2023-09-21 14:08:53 -03:00
Laurence IslaandSteve Chavez 1b4dae5a7a Add missing changelog entries 2023-09-19 00:15:48 -03:00
Laurence IslaandGitHub 37a3f818dd fix: error when requesting "Prefer: count=<type>" with null filters on embedded resources 2023-09-16 15:02:59 -05:00
Andrei DziahelandSteve Chavez c3169b7dc2 ci: less strict conditions for loadtest inmain 2023-09-15 16:35:20 -03:00
Taimoor ZaeemandSteve Chavez d64b71cbf0 refactor: rename test module NullsStrip.hs to NullsStripSpec.hs 2023-09-15 14:39:12 -03:00
Taimoor ZaeemandSteve Chavez c195eece65 feat: add Server-Timing header with JWT duration 2023-09-15 14:39:12 -03:00
Laurence IslaandGitHub fa182c216e fix: bug when Null Filtering on embedded resources
When doing Null Filtering, the to-one embed resources were not included if they had a NULL value in any of the selected fields.
2023-09-14 21:30:48 -05:00
Andrei DziahelandSteve Chavez 30474c424c ci: loadtest commits to main against latest release
Introduces loadtest job for main branch
that runs loadtest against latest release
2023-09-14 17:10:48 -03:00
Laurence IslaandGitHub 2888f351d1 Remove former sponsor 2023-09-12 22:41:39 -05:00
Taimoor ZaeemandGitHub 69eaef461f nix: add postgrest-docs-check (#676) 2023-09-12 23:18:17 -03:00
steve-chavez 8d3c9f8435 refactor: move media type logic to Plan module 2023-09-12 19:24:26 -03:00
steve-chavez d8a91453d1 fix: inconsistent Preference-Applied
* Don't apply `tx=commit` if the transaction doesn't commit
* Apply `count=exact`
* Also simplifies the Preference-Applied logic, removing the need for
  some functions.
2023-09-12 13:52:49 -03:00
dependabot[bot]andGitHub 50c87fec88 Bump actions/checkout from 3 to 4 (#678) 2023-09-11 18:33:29 -03:00
Laurence IslaandGitHub 4ee5410c3d Remove former sponsor 2023-09-11 15:19:10 -05:00
dependabot[bot]andGitHub 3f5e840baf build(deps): bump cachix/install-nix-action (#2935) 2023-09-11 03:00:29 -03:00
dependabot[bot]andGitHub 2358b6670f build(deps): bump actions/checkout from 3 to 4 (#2936) 2023-09-11 02:34:35 -03:00
Taimoor ZaeemandGitHub 96b8fccd2a allow add-headers-with-raise (#675) 2023-09-06 13:00:01 -03:00
Laurence IslaandGitHub d719cbf59d Fix and add images to htmx how-to 2023-09-05 12:38:51 -05:00
steve-chavez a6f1eefe55 fix linkcheck 2023-09-04 19:23:07 -03:00
steve-chavez 3dead2ac8b clarify access mode 2023-09-04 19:23:07 -03:00
steve-chavez 5a1ed11924 note on current_setting inconsistency
Closes #362.
2023-09-04 19:23:07 -03:00
dependabot[bot]andSteve Chavez 46eae105b7 Bump cachix/install-nix-action from 22 to 23
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 22 to 23.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Commits](https://github.com/cachix/install-nix-action/compare/v22...v23)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-09-04 17:14:42 -03:00
Taimoor ZaeemandSteve Chavez 8eed576826 fix: range request with first position same as length return status 206 2023-09-04 15:50:00 -03:00
Laurence IslaandSteve Chavez d643aab261 Add html-htmx how-to 2023-09-02 20:26:55 -05:00
Taimoor ZaeemandGitHub 07fef25591 feat: allow full response control when raising exceptions 2023-09-01 14:02:03 -05:00
Haowu GeandSteve Chavez 9352a72195 Add a regular account to reduce operational security risks 2023-08-29 14:22:11 -05:00
Taimoor ZaeemandSteve Chavez 7dc6e2b899 fix: duplicate headers in response 2023-08-25 13:52:50 -05:00
Taimoor ZaeemandSteve Chavez ff3a3a9100 add db-pool-automatic-recovery 2023-08-25 13:34:48 -05:00
Taimoor ZaeemandGitHub 57fa2719dd feat: add db-pool-automatic-recovery configuration to disable connection retrying 2023-08-23 21:08:04 -05:00
Diogo BiazusandGitHub b8b3145c5c fix: schema cache and configuration reloading with NOTIFY not working on Windows 2023-08-21 10:25:44 -05:00
steve-chavez 531a183b44 nix: postgrest-coverage notice 2023-08-17 13:43:48 -05:00
steve-chavez 739f056b0a nix: add postgrest-repl command 2023-08-17 13:43:48 -05:00
Steve Chavez 32b77cae6c Revert "nix: Update nixpkgs to 2023-08-04"
This reverts commit 2434724edd.
2023-08-15 23:55:35 -05:00
Laurence IslaandSteve Chavez 46548dd09b Fix typo and version 2023-08-14 22:54:39 -05:00
Taimoor ZaeemandSteve Chavez 2434724edd nix: Update nixpkgs to 2023-08-04 2023-08-14 15:00:20 -05:00
Taimoor ZaeemandSteve Chavez 87d6a0d0fe fix: application/vnd.pgrst.array not accepted as a valid mediatype 2023-08-11 12:41:04 -05:00
163 changed files with 7324 additions and 3221 deletions
+16
View File
@@ -0,0 +1,16 @@
freebsd_instance:
image_family: freebsd-14-0
build_task:
# Don't change this name without adjusting .github/workflows/ci.yaml
name: Build FreeBSD (Stack)
install_script: pkg install -y postgresql13-client hs-stack git
stack_cache:
folders: /.stack
fingerprint_script: cat postgrest.cabal stack.yaml.lock
reupload_on_changes: false
build_script: stack build -j 1 --local-bin-path . --copy-bins
bin_artifacts:
path: postgrest
+12
View File
@@ -3,4 +3,16 @@ When submitting a new feature or fix:
- Add a new entry to the CHANGELOG - https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md#unreleased
- If relevant, update the docs
- Use a prefix for the PR title or commits, e.g. "fix: description of the fix".
+ `fix`, bug fixes
+ `feat`, new features added
+ `perf`, performance improvements
+ `nix`, related to the Nix development environment
+ `ci`, related to the Continuous Integration modules
+ `test`, related to the testing modules
+ `refactor`, refactoring code
+ `deprecate`, deprecating a feature
+ `chore`, maintenance (changelog, build process, etc.)
+ Other prefixes may be used if necessary
- If there's a breaking change, add `BREAKING CHANGE` and an explanation to your commit message
-->
@@ -0,0 +1,119 @@
name: Artifact from Cirrus
description: Waits for a specific Cirrus CI run to complete, then downloads the artifact and uploads it to the current workflow. This will silently succeed if Cirrus CI did not schedule a task within 2 minutes.
inputs:
download:
description: Name of Artifact to download from Cirrus CI
required: true
task:
description: Name of Cirrus Task
required: true
token:
description: GitHub Token
required: true
upload:
description: Name of Artifact to upload on GitHub Actions
required: true
runs:
using: composite
steps:
- shell: bash
run: echo "GH_TOKEN=${{ inputs.token }}" >> "$GITHUB_ENV"
- name: Wait for Check Suite to be created
id: check-suite
env:
# GITHUB_SHA does weird things for pull request, so we roll our own:
COMMIT: ${{ github.event.pull_request.head.sha || github.sha }}
shell: bash
run: |
get_check_runs_url() {
gh api "repos/{owner}/{repo}/commits/${COMMIT}/check-suites" \
| jq -r '.check_suites[] | select(.app.slug == "cirrus-ci") | .check_runs_url'
}
for _ in $(seq 1 12); do
check_runs_url="$(get_check_runs_url)"
if [ -z "$check_runs_url" ]; then
echo "Cirrus CI task has not started, yet. Waiting..."
sleep 10
else
echo "check_runs_url=$check_runs_url" >> "$GITHUB_OUTPUT"
exit 0
fi
done
>&2 echo "Cirrus CI check suite not found. Is Cirrus CI enabled for this repo?"
- name: Find task by name
id: find-task
if: steps.check-suite.outputs.check_runs_url
shell: bash
run: |
get_number_of_tasks() {
gh api "${{ steps.check-suite.outputs.check_runs_url }}" \
| jq -r '.check_runs | map(select(.name == "${{ inputs.task }}")) | length'
}
tasks="$(get_number_of_tasks)"
case "$tasks" in
0)
echo "Task not found, assuming it's skipped intentionally..."
exit 0
;;
1)
echo "task_found=1" >> "$GITHUB_OUTPUT"
exit 0
;;
*)
>&2 echo "More than 1 task with the same name found. Don't know what to do..."
exit 1
;;
esac
- name: Wait for Cirrus CI to complete task
if: steps.find-task.outputs.task_found
shell: bash
run: |
get_conclusion() {
gh api "${{ steps.check-suite.outputs.check_runs_url }}" \
| jq -r '.check_runs[] | select(.name == "${{ inputs.task }}" and .status == "completed") | .conclusion'
}
while true; do
conclusion="$(get_conclusion)"
if [ -z "$conclusion" ]; then
echo "Cirrus CI task has not completed, yet. Waiting..."
sleep 30
else
if [ "$conclusion" == "success" ]; then
break
else
exit 1
fi
fi
done
- name: Download artifact from Cirrus CI
if: steps.find-task.outputs.task_found
id: download
shell: bash
run: |
get_external_id() {
gh api "${{ steps.check-suite.outputs.check_runs_url }}" \
| jq -er '.check_runs[] | select(.name == "${{ inputs.task }}") | .external_id'
}
archive="$(mktemp)"
artifacts="$(mktemp -d)"
until curl --no-progress-meter --fail -o "${archive}" \
"https://api.cirrus-ci.com/v1/artifact/task/$(get_external_id)/${{ inputs.download }}.zip"
do
# This happens when a tag is pushed on the same commit. In this case the
# job is immediately marked as "completed" for us, so we end up here after a few
# seconds - but the actual Cirrus CI task is still running and didn't produce its artifact, yet.
echo "Artifact not found on Cirrus CI, yet. Waiting..."
sleep 30
done
unzip "${archive}" -d "${artifacts}"
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
- name: Save artifact to GitHub Actions
if: steps.find-task.outputs.task_found
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: ${{ inputs.upload }}
path: ${{ steps.download.outputs.artifacts }}
if-no-files-found: error
+25
View File
@@ -0,0 +1,25 @@
name: Setup Nix
description: Installs nix, sets up cachix and installs a subset of tooling.
inputs:
authToken:
description: Token to pass to cachix
tools:
description: Tools to install with nix-env -iA <tools>
runs:
using: composite
steps:
- uses: nixbuild/nix-quick-install-action@60e9c39264d4714139af3cdf15f691b19eec3530 # v28
with:
nix_conf: |-
always-allow-substitutes = true
max-jobs = auto
- uses: cachix/cachix-action@18cf96c7c98e048e10a83abd92116114cd8504be # v14
with:
name: postgrest
authToken: ${{ inputs.authToken }}
- if: ${{ inputs.tools }}
run: nix-env -f default.nix -iA ${{ inputs.tools }}
shell: bash
+18
View File
@@ -0,0 +1,18 @@
codecov:
branch: main
require_ci_to_pass: false
comment: false
coverage:
status:
project:
default:
target: auto
threshold: 1%
only_pulls: false
patch:
default:
target: auto
threshold: 1%
only_pulls: true
+11
View File
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
- package-ecosystem: github-actions
directory: /.github/actions/setup-nix
schedule:
interval: weekly
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Tag a release that will be built with Github Actions. The version is
# read from 'postgrest.cabal'.
version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
echo "Tagging version v$version"
git tag -f "v$version"
echo "Pushing tag..."
git push -f origin "refs/tags/v$version"
+69
View File
@@ -0,0 +1,69 @@
#!/bin/bash
set -Eeuo pipefail
# This script builds PostgREST in a remote ARM server
[ -z "$1" ] && { echo "Missing 1st argument: PostgREST github commit SHA"; exit 1; }
[ -z "$2" ] && { echo "Missing 2nd argument: Build environment directory name"; exit 1; }
[ -z "$3" ] && { echo "Missing 3rd argument: GHC version"; exit 1; }
PGRST_GITHUB_COMMIT="$1"
SCRIPT_DIR="$2"
DOCKER_BUILD_DIR="$SCRIPT_DIR/docker-env"
# latest is a shortcut documented on https://www.haskell.org/ghcup/guide/#tags-and-shortcuts
CABAL_VERSION="latest"
GHC_VERSION="$3"
install_packages() {
sudo apt-get update -y
sudo apt-get upgrade -y
sudo apt-get install -y git build-essential curl libffi-dev libffi7 libgmp-dev libgmp10 libncurses-dev libncurses5 libtinfo5 llvm libnuma-dev zlib1g-dev libpq-dev jq gcc
sudo apt-get clean
}
install_ghcup() {
export BOOTSTRAP_HASKELL_NONINTERACTIVE=1
export BOOTSTRAP_HASKELL_MINIMAL=1
curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | sh
source ~/.ghcup/env
}
install_cabal() {
ghcup upgrade
ghcup install cabal $CABAL_VERSION
ghcup set cabal $CABAL_VERSION
}
install_ghc() {
ghcup upgrade
ghcup install ghc $GHC_VERSION
ghcup set ghc $GHC_VERSION
}
install_packages
# Add ghcup to the PATH for this session
[ -f ~/.ghcup/env ] && source ~/.ghcup/env
ghcup --version || install_ghcup
ghcup set cabal $CABAL_VERSION || install_cabal
ghcup set ghc $GHC_VERSION || install_ghc
cd ~/$SCRIPT_DIR
# Clone the repository and build the project
git clone https://github.com/PostgREST/postgrest.git
cd postgrest
git checkout $PGRST_GITHUB_COMMIT
cabal v2-update && cabal v2-build
# Copy the built binary to the Dockerfile directory
PGRST_BIN=$(cabal exec which postgrest | tail -1)
cp $PGRST_BIN ~/$DOCKER_BUILD_DIR
# Move and compress the built binary
mkdir -p ~/$SCRIPT_DIR/result
mv $PGRST_BIN ~/$SCRIPT_DIR/result
cd ~/$SCRIPT_DIR
tar -cJf result.tar.xz result
+18
View File
@@ -0,0 +1,18 @@
# PostgREST docker hub image
FROM ubuntu:noble@sha256:3f85b7caad41a95462cf5b787d8a04604c8262cdcdf9a472b8c52ef83375fe15 AS postgrest
RUN apt-get update -y \
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
COPY postgrest /usr/bin/postgrest
EXPOSE 3000
USER 1000
# Use the array form to avoid running the command using bash, which does not handle `SIGTERM` properly.
# See https://docs.docker.com/compose/faq/#why-do-my-services-take-10-seconds-to-recreate-or-stop
CMD ["postgrest"]
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
set -Eeuo pipefail
# This script publishes the Docker ARM images to Docker Hub.
[ -z "$1" ] && { echo "Missing 1st argument: PostgREST github commit SHA"; exit 1; }
[ -z "$2" ] && { echo "Missing 2nd argument: Docker repo"; exit 1; }
[ -z "$3" ] && { echo "Missing 3rd argument: Docker username"; exit 1; }
[ -z "$4" ] && { echo "Missing 4th argument: Docker password"; exit 1; }
[ -z "$5" ] && { echo "Missing 5th argument: Build environment directory name"; exit 1; }
[ -z "$6" ] && { echo "Missing 6th argument: PostgREST version"; exit 1; }
PGRST_GITHUB_COMMIT="$1"
DOCKER_REPO="$2"
DOCKER_USER="$3"
DOCKER_PASS="$4"
SCRIPT_DIR="$5"
PGRST_VERSION="$6"
DOCKER_BUILD_DIR="$SCRIPT_DIR/docker-env"
clean_env()
{
sudo docker logout
}
# Login to Docker
sudo docker logout
{ echo $DOCKER_PASS | sudo docker login -u $DOCKER_USER --password-stdin; } || { echo "Couldn't login to docker"; exit 1; }
trap clean_env sigint sigterm exit
# Move to the docker build environment
cd ~/$DOCKER_BUILD_DIR
# Push final images to Docker hub
# NOTE: This command publishes a separate ARM image because the builds cannot
# be added to the manifest if they are not in the registry beforehand.
# This image must be manually deleted from Docker Hub at the end of the process.
sudo docker buildx build --build-arg PGRST_GITHUB_COMMIT=$PGRST_GITHUB_COMMIT \
-t $DOCKER_REPO/postgrest:$PGRST_VERSION-arm \
--push .
# Add the arm images to the manifest
# NOTE: This assumes that there already is a `postgrest:<version>` image
# for the amd64 architecture pushed to Docker Hub
sudo docker buildx imagetools create --append -t $DOCKER_REPO/postgrest:$PGRST_VERSION $DOCKER_REPO/postgrest:$PGRST_VERSION-arm
[ "$PGRST_VERSION" != "devel" ] && sudo docker buildx imagetools create --append -t $DOCKER_REPO/postgrest:latest $DOCKER_REPO/postgrest:$PGRST_VERSION-arm
sudo docker logout
+78
View File
@@ -0,0 +1,78 @@
name: Cachix
# This workflow serves to
# - keep cachix up to date with the main branch
# - incrementally update cachix for large dependency
# updates, e.g. after running postgrest-nixpkgs-upgrade,
# which can cause the main CI workflow to time out
on:
workflow_dispatch:
push:
branches:
- main
- rel-*
tags:
- v*
jobs:
Seed-Cachix:
strategy:
fail-fast: false
matrix:
include:
- os: Linux
runs-on: ubuntu-22.04
- os: MacOS
runs-on: macos-12
name: Seed ${{ matrix.os }}
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Install cachix tooling
run: |
nix-env -f default.nix -iA devTools.pushCachix.bin
postgrest-push-cachix
- name: Seed dynamic postgrest build
run: |
nix-build -A postgrestPackage
postgrest-push-cachix
- name: Seed style tools
run: |
nix-build -A style
postgrest-push-cachix
- name: Seed test tools
run: |
nix-build -A tests
postgrest-push-cachix
- name: Seed static toolchain
if: matrix.os == 'Linux'
run: |
nix-build -A packagesStatic.haskellPackages.hello
postgrest-push-cachix
- name: Seed static postgresql build (for libpq)
if: matrix.os == 'Linux'
run: |
nix-build -A packagesStatic.pkgs.postgresql
postgrest-push-cachix
- name: Seed static postgrest build
if: matrix.os == 'Linux'
run: |
nix-build -A postgrestStatic
postgrest-push-cachix
- name: Build and push everything to Cachix
run: |
nix-build
postgrest-push-cachix
+554
View File
@@ -0,0 +1,554 @@
name: CI
on:
push:
branches:
- main
- v[0-9]+
tags:
- devel
- v*
pull_request:
branches:
- main
- v[0-9]+
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
# Terminate all previous runs of the same workflow for pull requests
cancel-in-progress: "${{ github.event_name == 'pull_request' }}"
jobs:
Lint-Style:
name: Lint & check code style
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
tools: style
- name: Run linter (check locally with `nix-shell --run postgrest-lint`)
run: postgrest-lint
- name: Run style check (auto-format with `nix-shell --run postgrest-style`)
run: postgrest-style-check
Test-Nix:
name: Test (Nix)
runs-on: ubuntu-22.04
defaults:
run:
# Hack for enabling color output, see:
# https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
tools: tests
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
run: postgrest-coverage
- name: Upload coverage to codecov
uses: codecov/codecov-action@5ecb98a3c6b747ed38dc09f787459979aebb39be # v4.3.1
with:
files: ./coverage/codecov.json
token: ${{ secrets.CODECOV_TOKEN }}
- name: Run doctests
if: always()
run: nix-shell --run postgrest-test-doctests
- name: Check the spec tests for idempotence
if: always()
run: postgrest-test-spec-idempotence
Test-Pg-Nix:
strategy:
fail-fast: false
matrix:
pgVersion: [9.6, 10, 11, 12, 13, 14, 15, 16]
name: Test PG ${{ matrix.pgVersion }} (Nix)
runs-on: ubuntu-22.04
defaults:
run:
# Hack for enabling color output, see:
# https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
tools: tests withTools
- name: Run spec tests
if: always()
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-spec
- name: Run IO tests
if: always()
run: postgrest-with-postgresql-${{ matrix.pgVersion }} -f test/io/fixtures.sql postgrest-test-io -vv
Test-Memory-Nix:
name: Test memory (Nix)
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
tools: memory
- name: Run memory tests
run: postgrest-test-memory
Build-Static-Nix:
name: Build Linux static (Nix)
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
tools: tests
- name: Build static executable
run: nix-build -A postgrestStatic
- name: Check static executable
run: postgrest-check-static result/bin/postgrest
- name: Save built executable as artifact
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: postgrest-linux-static-x64
path: result/bin/postgrest
if-no-files-found: error
- name: Build Docker image
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
- name: Save built Docker image as artifact
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: postgrest-docker-x64
path: postgrest-docker.tar.gz
if-no-files-found: error
Build-Macos-Nix:
name: Build MacOS (Nix)
runs-on: macos-12
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
- name: Build everything
run: |
nix-build
Build-Stack:
strategy:
fail-fast: false
matrix:
include:
- name: Linux
runs-on: ubuntu-22.04
cache: |
~/.stack
.stack-work
artifact: postgrest-ubuntu-x64
- name: MacOS
runs-on: macos-12
cache: |
~/.stack
.stack-work
artifact: postgrest-macos-x64
- name: Windows
runs-on: windows-2022
cache: |
~\AppData\Roaming\stack
~\AppData\Local\Programs\stack
.stack-work
deps: Add-Content $env:GITHUB_PATH $env:PGBIN
artifact: postgrest-windows-x64
name: Build ${{ matrix.name }} (Stack)
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Stack working files cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: ${{ matrix.cache }}
key: cache-stack-${{ runner.os }}-${{ hashFiles('stack.yaml.lock') }}
- name: Install dependencies
if: ${{ matrix.deps }}
run: ${{ matrix.deps }}
- name: Build with Stack
run: stack build --local-bin-path result --copy-bins
- name: Save built executable as artifact
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: ${{ matrix.artifact }}
path: |
result/postgrest
result/postgrest.exe
if-no-files-found: error
Get-FreeBSD-CirrusCI:
name: Get FreeBSD build from CirrusCI
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- uses: ./.github/actions/artifact-from-cirrus
with:
token: ${{ github.token }}
task: Build FreeBSD (Stack)
download: bin
upload: postgrest-freebsd-x64
Build-Cabal:
strategy:
matrix:
ghc: ['9.0.2', '9.2.4']
fail-fast: false
name: Build Linux (Cabal, GHC ${{ matrix.ghc }})
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Workaround runner image issue
# https://github.com/actions/runner-images/issues/7061
run: sudo chown -R "$USER" /usr/local/.ghcup
- name: ghcup
run: |
ghcup install ghc ${{ matrix.ghc }}
ghcup set ghc ${{ matrix.ghc }}
- name: Copy cabal.project & fix caching
run: |
mkdir ~/.cabal
cp cabal.project.non-nix cabal.project
- name: Cache
uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v4.0.2
with:
path: |
~/.cabal/packages
~/.cabal/store
dist-newstyle
key: cache-cabal-${{ runner.os }}-${{ matrix.ghc }}-${{ hashFiles('**/*.cabal', '**/cabal.project') }}
restore-keys: |
cache-cabal-${{ runner.os }}-${{ matrix.ghc }}-
- name: Install dependencies
run: |
cabal update
cabal build --only-dependencies --enable-tests --enable-benchmarks
- name: Build
run: cabal build --enable-tests --enable-benchmarks all
Build-Cabal-Arm:
strategy:
matrix:
ghc: ['9.2.4']
fail-fast: false
name: Build aarch64 (Cabal, GHC ${{ matrix.ghc }})
if: "${{ github.event_name == 'push' }}"
runs-on: ubuntu-22.04
outputs:
remotepath: ${{ steps.Remote-Dir.outputs.remotepath }}
env:
GITHUB_COMMIT: ${{ github.sha }}
GHC_VERSION: ${{ matrix.ghc }}
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- id: Remote-Dir
name: Unique directory name for the remote build
run: echo "remotepath=postgrest-build-$(uuidgen)" >> "$GITHUB_OUTPUT"
- name: Copy script files to the remote server
uses: appleboy/scp-action@master
with:
host: ${{ secrets.SSH_ARM_HOST }}
username: ubuntu
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
source: ".github/scripts/arm/*"
target: ${{ steps.Remote-Dir.outputs.remotepath }}
strip_components: 3
- name: Build ARM
uses: appleboy/ssh-action@master
env:
REMOTE_DIR: ${{ steps.Remote-Dir.outputs.remotepath }}
with:
host: ${{ secrets.SSH_ARM_HOST }}
username: ubuntu
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
command_timeout: 120m
script_stop: true
envs: GITHUB_COMMIT,REMOTE_DIR,GHC_VERSION
script: bash ~/$REMOTE_DIR/build.sh "$GITHUB_COMMIT" "$REMOTE_DIR" "$GHC_VERSION"
- name: Download binaries from remote server
uses: nicklasfrahm/scp-action@main
with:
direction: download
host: ${{ secrets.SSH_ARM_HOST }}
username: ubuntu
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
source: "${{ steps.Remote-Dir.outputs.remotepath }}/result.tar.xz"
target: "result.tar.xz"
- name: Extract downloaded binaries
run: tar -xvf result.tar.xz && rm result.tar.xz
- name: Save aarch64 executable as artifact
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: postgrest-ubuntu-aarch64
path: result/postgrest
if-no-files-found: error
Tag-Release:
name: Tag Release
if: startsWith(github.ref, 'refs/heads/')
permissions:
contents: write
runs-on: ubuntu-22.04
needs:
- Lint-Style
- Test-Nix
- Test-Pg-Nix
- Test-Memory-Nix
- Build-Static-Nix
- Build-Stack
- Get-FreeBSD-CirrusCI
- Build-Cabal-Arm
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
with:
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
- name: Tag latest commit
run: |
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
if [[ "$cabal_version" == *.*.* ]]; then
git fetch --tags
if [ -z "$(git tag --list "v$cabal_version")" ]; then
git tag "v$cabal_version"
git push origin "v$cabal_version"
fi
else
git tag -f "devel"
git push -f origin "devel"
fi
Prepare-Release:
name: Prepare release
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-22.04
needs:
- Lint-Style
- Test-Nix
- Test-Pg-Nix
- Test-Memory-Nix
- Build-Static-Nix
- Build-Stack
- Get-FreeBSD-CirrusCI
- Build-Cabal-Arm
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Check the version to be released
run: |
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
if [ "${GITHUB_REF_NAME}" != "devel" ] && [ "${GITHUB_REF_NAME}" != "v$cabal_version" ]; then
echo "Tagged version ($GITHUB_REF_NAME) does not match the one in postgrest.cabal (v$cabal_version). Aborting release..."
exit 1
fi
- name: Identify changes from CHANGELOG.md
run: |
if [ "${GITHUB_REF_NAME}" == "devel" ]; then
echo "Getting unreleased changes..."
sed -n "1,/## Unreleased/d;/## \[/q;p" CHANGELOG.md > CHANGES.md
else
version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
echo "Propper release, getting changes for version $version ..."
sed -n "1,/## \[$version\]/d;/## \[/q;p" CHANGELOG.md > CHANGES.md
fi
echo "Relevant extract from CHANGELOG.md:"
cat CHANGES.md
- name: Save CHANGES.md as artifact
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: release-changes
path: CHANGES.md
if-no-files-found: error
Release-GitHub:
name: Release on GitHub
permissions:
contents: write
runs-on: ubuntu-22.04
needs: Prepare-Release
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Download all artifacts
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
path: artifacts
- name: Create release bundle with archives for all builds
run: |
find artifacts -type f -iname postgrest -exec chmod +x {} \;
mkdir -p release-bundle
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-linux-static-x64.tar.xz" \
-C artifacts/postgrest-linux-static-x64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-x64.tar.xz" \
-C artifacts/postgrest-macos-x64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x64.tar.xz" \
-C artifacts/postgrest-freebsd-x64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-ubuntu-aarch64.tar.xz" \
-C artifacts/postgrest-ubuntu-aarch64 postgrest
zip "release-bundle/postgrest-${GITHUB_REF_NAME}-windows-x64.zip" \
artifacts/postgrest-windows-x64/postgrest.exe
- name: Save release bundle
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: release-bundle
path: release-bundle
if-no-files-found: error
- name: Publish release on GitHub
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "Releasing version ${GITHUB_REF_NAME} on GitHub..."
if [ "${GITHUB_REF_NAME}" == "devel" ]; then
# To replace the existing release, we must first delete the old assets,
# then modify the release, then add the new assets.
gh release view devel --json assets \
| jq -r '.assets[] | .name' \
| xargs -rn1 \
gh release delete-asset -y devel
gh release edit devel \
-t devel \
--verify-tag \
-F artifacts/release-changes/CHANGES.md \
--prerelease
gh release upload --clobber devel release-bundle/*
else
gh release create "${GITHUB_REF_NAME}" \
-t "${GITHUB_REF_NAME}" \
--verify-tag \
-F artifacts/release-changes/CHANGES.md \
release-bundle/*
fi
Release-Docker:
name: Release on Docker Hub
runs-on: ubuntu-22.04
needs:
- Prepare-Release
env:
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
DOCKER_USER: ${{ vars.DOCKER_USER }}
DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Download Docker image
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
name: postgrest-docker-x64
- name: Publish images on Docker Hub
run: |
docker login -u "$DOCKER_USER" -p "$DOCKER_PASS"
docker load -i postgrest-docker.tar.gz
docker tag postgrest:latest "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
docker push "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}"
# Only tag 'latest' for full releases
if [ "${GITHUB_REF_NAME}" != "devel" ]; then
echo "Pushing to 'latest' tag for full release of ${GITHUB_REF_NAME} ..."
docker tag postgrest:latest "$DOCKER_REPO"/postgrest:latest
docker push "$DOCKER_REPO"/postgrest:latest
else
echo "Skipping push to 'latest' tag for pre-release..."
fi
# TODO: Enable dockerhub description update again, once a solution for the permission problem is found:
# https://github.com/docker/hub-feedback/issues/1927
# - name: Update descriptions on Docker Hub
# env:
# DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
# run: |
# if [[ -z "$ISPRERELEASE" ]]; then
# echo "Updating description on Docker Hub..."
# postgrest-release-dockerhub-description
# else
# echo "Skipping updating description for pre-release..."
# fi
Release-Docker-Arm:
name: Release Arm Builds on Docker Hub
runs-on: ubuntu-22.04
needs:
- Build-Cabal-Arm
- Release-Docker
env:
GITHUB_COMMIT: ${{ github.sha }}
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
DOCKER_USER: ${{ vars.DOCKER_USER }}
DOCKER_PASS: ${{ secrets.DOCKER_PASS }}
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Publish images for ARM builds on Docker Hub
uses: appleboy/ssh-action@master
env:
REMOTE_DIR: ${{ needs.Build-Cabal-Arm.outputs.remotepath }}
with:
host: ${{ secrets.SSH_ARM_HOST }}
username: ubuntu
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
script_stop: true
envs: GITHUB_COMMIT,DOCKER_REPO,DOCKER_USER,DOCKER_PASS,REMOTE_DIR,GITHUB_REF_NAME
script: bash ~/$REMOTE_DIR/docker-publish.sh "$GITHUB_COMMIT" "$DOCKER_REPO" "$DOCKER_USER" "$DOCKER_PASS" "$REMOTE_DIR" "$GITHUB_REF_NAME"
Clean-Arm-Server:
name: Remove copied files from server
needs:
- Build-Cabal-Arm
- Release-Docker-Arm
if: success() ||
needs.Build-Cabal-Arm.result == 'failure' ||
needs.Build-Cabal-Arm.result == 'cancelled' ||
(needs.Build-Cabal-Arm.result == 'success' && !startsWith(github.ref, 'refs/tags/v'))
runs-on: ubuntu-22.04
env:
REMOTE_DIR: ${{ needs.Build-Cabal-Arm.outputs.remotepath }}
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- name: Remove uploaded files from server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_ARM_HOST }}
username: ubuntu
key: ${{ secrets.SSH_ARM_PRIVATE_KEY }}
fingerprint: ${{ secrets.SSH_ARM_FINGERPRINT }}
envs: REMOTE_DIR
script: rm -rf $REMOTE_DIR
+12 -12
View File
@@ -13,38 +13,38 @@ on:
jobs:
build:
name: Build docs
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- uses: cachix/install-nix-action@8887e596b4ee1134dae06b98d573bd674693f47c # v26
- run: nix-env -f docs/default.nix -iA build
- run: postgrest-docs-build
spellcheck:
name: Run spellcheck
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- uses: cachix/install-nix-action@8887e596b4ee1134dae06b98d573bd674693f47c # v26
- run: nix-env -f docs/default.nix -iA spellcheck
- run: postgrest-docs-spellcheck
dictcheck:
name: Run dictcheck
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- uses: cachix/install-nix-action@8887e596b4ee1134dae06b98d573bd674693f47c # v26
- run: nix-env -f docs/default.nix -iA dictcheck
- run: postgrest-docs-dictcheck
linkcheck:
name: Run linkcheck
if: github.base_ref == 'main'
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: cachix/install-nix-action@13d8dd58da0234aa297dedd986986ccb8e7f3e24 # v31
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
- uses: cachix/install-nix-action@8887e596b4ee1134dae06b98d573bd674693f47c # v26
- run: nix-env -f docs/default.nix -iA linkcheck
- run: postgrest-docs-linkcheck
+70
View File
@@ -0,0 +1,70 @@
name: Loadtest
on:
push:
branches:
- main
tags:
- v*
pull_request:
branches:
- main
jobs:
Loadtest-PR-Nix:
name: Loadtest PR (Nix)
if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-22.04
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
with:
fetch-depth: 0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
tools: loadtest
- uses: WyriHaximus/github-action-get-previous-tag@04e8485ecb6487243907e330d522ff60f02283ce # v1.4.0
id: get-latest-tag
with:
prefix: v
- name: Run loadtest
run: |
postgrest-loadtest-against main ${{ steps.get-latest-tag.outputs.tag }}
postgrest-loadtest-report > loadtest/loadtest.md
- name: Upload report
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: loadtest.md
path: loadtest/loadtest.md
if-no-files-found: error
Loadtest-Merge-Nix:
name: Loadtest Merge (Nix)
if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@44c2b7a8a4ea60a981eaca3cf939b5f4305c123b # v4.1.5
with:
fetch-depth: 0
- uses: WyriHaximus/github-action-get-previous-tag@04e8485ecb6487243907e330d522ff60f02283ce # v1.4.0
id: get-latest-tag
with:
prefix: v
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
tools: loadtest
- name: Run loadtest
run: |
postgrest-loadtest-against ${{ steps.get-latest-tag.outputs.tag }}
postgrest-loadtest-report > loadtest/loadtest.md
- name: Upload report
uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
with:
name: loadtest.md
path: loadtest/loadtest.md
if-no-files-found: error
+33
View File
@@ -0,0 +1,33 @@
name: Upload Reports
on:
workflow_run:
workflows: ["Loadtest"]
types:
- completed
jobs:
upload:
name: Loadtest
permissions:
checks: write
runs-on: ubuntu-22.04
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- name: Download from Artifacts
uses: actions/download-artifact@65a9edc5881444af0b9093a5e628f2fe47ea3b2e # v4.1.7
with:
github-token: ${{ github.token }}
run-id: ${{ github.event.workflow_run.id }}
name: loadtest.md
path: artifacts
- name: Upload to GitHub Checks
uses: LouisBrunner/checks-action@6b626ffbad7cc56fd58627f774b9067e6118af23 # v2.0.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
sha: ${{ github.event.workflow_run.head_sha }}
name: Loadtest Results
conclusion: neutral
output: |
{"summary":""}
output_text_description_file: artifacts/loadtest.md
View File
+35 -20
View File
@@ -4,40 +4,40 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
## Sponsors
<table>
<table align="center">
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest" target="_blank">
<img width="222px" src="static/cybertec-new.png">
<img width="296px" src="static/cybertec-new.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo" target="_blank">
<img width="296px" src="static/2ndquadrant.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/retool.png">
</a>
</td>
</tr>
<tr></tr>
<tr>
<td align="center" valign="middle">
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/gnuhost.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="static/supabase.png">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/neon.jpg">
</a>
</td>
</tr>
<tr></tr>
<tr>
<td align="center" valign="middle">
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/code-build.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/oblivious.jpg">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png">
</a>
</td>
</tr>
@@ -75,7 +75,22 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
<tr>
<td align="center" valign="middle">
<a href="https://www.timescale.com?utm_campaign=postgrest&utm_source=sponsor&utm_medium=referral&utm_content=github" target="_blank">
<img width="222px" src="static/timescaledb.png">
<img width="222px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/timescaledb.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img max-width="222px" height="88" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/retool.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo" target="_blank">
<img width="222px" src="static/2ndquadrant.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="222px" src="static/oblivious.jpg">
</a>
</td>
</tr>
+68
View File
@@ -5,6 +5,74 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
## [12.0.3] - 2024-05-09
### Fixed
- #3149, Misleading "Starting PostgREST.." logs on schema cache reloading - @steve-chavez
- #3205, Fix wrong subquery error returning a status of 400 Bad Request - @steve-chavez
- #3224, Return status code 406 for non-accepted media type instead of code 415 - @wolfgangwalther
- #3160, Fix using select= query parameter for custom media type handlers - @wolfgangwalther
- #3361, Clarify PGRST204(column not found) error message - @steve-chavez
- #3373, Remove rejected mediatype `application/vnd.pgrst.object+json` from response - @taimoorzaeem
- #3418, Fix OpenAPI not tagging a FK column correctly on O2O relationships - @laurenceisla
- #3256, Fix wrong http status for pg error `42P17 infinite recursion` - @taimoorzaeem
## [12.0.2] - 2023-12-20
### Fixed
- #3124, Fix table's media type handlers not working for all schemas - @steve-chavez
- #3126, Fix empty row on media type handler function - @steve-chavez
## [12.0.1] - 2023-12-12
### Fixed
- #3054, Fix not allowing special characters in JSON keys - @laurenceisla
- #2344, Replace JSON parser error with a clearer generic message - @develop7
- #3100, Add missing in-database configuration option for `jwt-cache-max-lifetime` - @laurenceisla
- #3089, The any media type handler now sets `Content-Type: application/octet-stream` by default instead of `Content-Type: application/json` - @steve-chavez
## [12.0.0] - 2023-12-01
### Added
- #1614, Add `db-pool-automatic-recovery` configuration to disable connection retrying - @taimoorzaeem
- #2492, Allow full response control when raising exceptions - @taimoorzaeem, @laurenceisla
- #2771, #2983, #3062, #3055 Add `Server-Timing` response header - @taimoorzaeem, @develop7, @laurenceisla
- #2698, Add config `jwt-cache-max-lifetime` and implement JWT caching - @taimoorzaeem
- #2943, Add `handling=strict/lenient` for Prefer header - @taimoorzaeem
- #2441, Add config `server-cors-allowed-origins` to specify CORS origins - @taimoorzaeem
- #2825, SQL handlers for custom media types - @steve-chavez
+ Solves #1548, #2699, #2763, #2170, #1462, #1102, #1374, #2901
- #2799, Add timezone in Prefer header - @taimoorzaeem
- #3001, Add `statement_timeout` set on functions - @taimoorzaeem
- #3045, Apply superuser settings on impersonated roles if they have PostgreSQL 15 `GRANT SET ON PARAMETER` privilege - @steve-chavez
- #915, Add support for aggregate functions - @timabdulla
+ The aggregate functions SUM(), MAX(), MIN(), AVG(), and COUNT() are now supported.
+ It's disabled by default, you can enable it with `db-aggregates-enabled`.
- #3057, Log all internal database errors to stderr - @laurenceisla
### Fixed
- #3015, Fix unnecessary count() on RPC returning single - @steve-chavez
- #1070, Fix HTTP status responses for upserts - @taimoorzaeem
+ `PUT` returns `201` instead of `200` when rows are inserted
+ `POST` with `Prefer: resolution=merge-duplicates` returns `200` instead of `201` when no rows are inserted
- #3019, Transaction-Scoped Settings are now shown clearly in the Postgres logs - @laurenceisla
+ Shows `set_config('pgrst.setting_name', $1)` instead of `setconfig($1, $2)`
+ Does not apply to role settings and `app.settings.*`
- #2420, Fix bogus message when listening on port 0 - @develop7
- #3067, Fix Acquision Timeout errors logging to stderr when `log-level=crit` - @laurenceisla
### Changed
- Removed [raw-media-types config](https://postgrest.org/en/v11.1/references/configuration.html#raw-media-types) - @steve-chavez
- Removed `application/octet-stream`, `text/plain`, `text/xml` [builtin support for scalar results](https://postgrest.org/en/v11.1/references/api/resource_representation.html#scalar-function-response-format) - @steve-chavez
- Removed default `application/openapi+json` media type for [db-root-spec](https://postgrest.org/en/v11.1/references/configuration.html#db-root-spec) - @steve-chavez
- Removed [db-use-legacy-gucs](https://postgrest.org/en/v11.2/references/configuration.html#db-use-legacy-gucs) - @laurenceisla
## [11.2.2] - 2023-10-25
### Fixed
+26 -20
View File
@@ -1,6 +1,8 @@
![Logo](static/bigger-logo.png "Logo")
[![Donate](https://img.shields.io/badge/Donate-Patreon-orange.svg?colorB=F96854)](https://www.patreon.com/postgrest)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/postgrest)
[![Join the chat at https://gitter.im/begriffs/postgrest](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/begriffs/postgrest)
[![Docs](https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat)](http://postgrest.org)
[![Docker Stars](https://img.shields.io/docker/pulls/postgrest/postgrest.svg)](https://hub.docker.com/r/postgrest/postgrest/)
[![Build Status](https://github.com/postgrest/postgrest/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)
@@ -13,40 +15,40 @@ API than you are likely to write from scratch.
## Sponsors
<table>
<table align="center">
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest" target="_blank">
<img width="222px" src="static/cybertec-new.png">
<img width="296px" src="static/cybertec-new.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo" target="_blank">
<img width="296px" src="static/2ndquadrant.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/retool.png">
</a>
</td>
</tr>
<tr></tr>
<tr>
<td align="center" valign="middle">
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/gnuhost.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="static/supabase.png">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/neon.jpg">
</a>
</td>
</tr>
<tr></tr>
<tr>
<td align="center" valign="middle">
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/code-build.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/oblivious.jpg">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png">
</a>
</td>
</tr>
@@ -143,7 +145,11 @@ and the [API guide](http://postgrest.org/en/stable/api.html).
## Supporting development
You can help PostgREST ongoing maintenance and development by making a regular donation through Patreon https://www.patreon.com/postgrest
You can help PostgREST ongoing maintenance and development by:
- Making a regular donation through Patreon https://www.patreon.com/postgrest
- Alternatively, you can make a one-time donation via Paypal https://www.paypal.me/postgrest
Every donation will be spent on making PostgREST better for the whole community.
+1
View File
@@ -0,0 +1 @@
index-state: hackage.haskell.org 2023-10-13T13:54:33Z
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

+22 -5
View File
@@ -28,7 +28,11 @@ 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 = ["sphinx_tabs.tabs", "sphinx_copybutton"]
extensions = [
"sphinx_tabs.tabs",
"sphinx_copybutton",
"sphinxext.opengraph",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
@@ -45,7 +49,7 @@ source_suffix = ".rst"
master_doc = "index"
# This is overriden by readthedocs with the version tag anyway
version = "11.2"
version = "12.0"
# To avoid repetition in <title> we set this to an empty string.
release = ""
@@ -59,7 +63,7 @@ copyright = "2017, " + author
#
# 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
language = "en"
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
@@ -70,7 +74,7 @@ language = None
# 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", "shared/*.rst"]
# The reST default role (used for this markup: `text`) to use for all
# documents.
@@ -287,7 +291,20 @@ def setup(app):
app.add_css_file("css/custom.css")
user_agent = "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0"
# taken from https://github.com/sphinx-doc/sphinx/blob/82dad44e5bd3776ecb6fd8ded656bc8151d0e63d/sphinx/util/requests.py#L42
user_agent = "Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0"
# sphinx-tabs configuration
sphinx_tabs_disable_tab_closing = True
# sphinxext-opengraph configuration
ogp_image = "_images/logo.png"
ogp_use_first_image = True
ogp_enable_meta_description = True
ogp_description_length = 300
## RTD sets html_baseurl, ensures we use the correct env for canonical URLs
## Useful to generate correct meta tags for Open Graph
## Refs: https://github.com/readthedocs/readthedocs.org/issues/10226, https://github.com/urllib3/urllib3/pull/3064
html_baseurl = os.environ.get("READTHEDOCS_CANONICAL_URL", "/")
+15 -8
View File
@@ -1,9 +1,9 @@
let
# Commit of the Nixpkgs repository that we want to use.
nixpkgsVersion = {
date = "2021-06-02";
rev = "84aa23742f6c72501f9cc209f29c438766f5352d";
tarballHash = "0h7xl6q0yjrbl9vm3h6lkxw692nm8bg3wy65gm95a2mivhrdjpxp";
date = "2023-03-25";
rev = "dbf5322e93bcc6cfc52268367a8ad21c09d76fea";
tarballHash = "0lwk4v9dkvd28xpqch0b0jrac4xl9lwm6snrnzx8k5lby72kmkng";
};
# Nix files that describe the Nixpkgs repository. We evaluate the expression
@@ -15,12 +15,9 @@ let
})
{ };
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) ]);
python = pkgs.python3.withPackages (ps: [ ps.sphinx ps.sphinx_rtd_theme ps.livereload ps.sphinx-tabs ps.sphinx-copybutton ps.sphinxext-opengraph ]);
in
{
rec {
inherit pkgs;
build =
@@ -90,4 +87,14 @@ in
${python}/bin/sphinx-build --color -b linkcheck . _build
'';
check =
pkgs.writeShellScriptBin "postgrest-docs-check"
''
set -euo pipefail
${build}/bin/postgrest-docs-build
${dictcheck}/bin/postgrest-docs-dictcheck
${linkcheck}/bin/postgrest-docs-linkcheck
${spellcheck}/bin/postgrest-docs-spellcheck
'';
}
+3 -3
View File
@@ -6,7 +6,7 @@ Community Tutorials
* `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ -
In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.
* `PostgREST + Auth0: Create REST API in minutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest-1-introduction/>`_ - 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 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
* `PostgREST + PostGIS API tutorial in 5 minutes <https://gis-ops.com/postgrest-postgis-api-tutorial-geospatial-api-in-5-minutes/>`_ -
In this tutorial, GIS • OPS shows how to perform PostGIS calculations through PostgREST :ref:`s_procs` interface.
@@ -20,7 +20,7 @@ Community Tutorials
* `Build data-driven applications with Workers and PostgreSQL <https://developers.cloudflare.com/workers/tutorials/postgres/>`_ - A tutorial on how to integrate with PostgREST and PostgreSQL using Cloudflare Workers.
* `A poor man's API <https://blog.frankel.ch/poor-man-api>`_ - Shows how to integrate PostgREST with Apache APISIX as an alternative to Nginx.
.. * `A poor man's API <https://blog.frankel.ch/poor-man-api>`_ - Shows how to integrate PostgREST with Apache APISIX as an alternative to Nginx.
.. _templates:
@@ -87,7 +87,7 @@ Client-Side Libraries
* `postgrest-go <https://github.com/supabase-community/postgrest-go>`_ - Go
* `postgrest-js <https://github.com/supabase/postgrest-js>`_ - TypeScript/JavaScript
* `postgrest-kt <https://github.com/supabase-community/postgrest-kt>`_ - Kotlin
* `postgrest-py <https://github.com/supabase/postgrest-py>`_ - Python
* `postgrest-py <https://github.com/supabase-community/postgrest-py>`_ - Python
* `postgrest-rs <https://github.com/supabase-community/postgrest-rs>`_ - Rust
* `postgrest-swift <https://github.com/supabase-community/postgrest-swift>`_ - Swift
* `redux-postgrest <https://github.com/andytango/redux-postgrest>`_ - TypeScript/JS, client integrated with (React) Redux.
+2 -11
View File
@@ -15,7 +15,7 @@ Roles for Each Web User
PostgREST can accommodate either viewpoint. If you treat a role as a single user then the :ref:`jwt_impersonation` does most of what you need. When an authenticated user makes a request PostgREST will switch into the database role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable.
You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example <https://www.enterprisedb.com:443/blog/application-users-vs-row-level-security>`_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users.
You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example <https://www.2ndquadrant.com/en/blog/application-users-vs-row-level-security/>`_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users.
.. code-block:: postgres
@@ -92,19 +92,10 @@ 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
You must explicitly allow roles to access the exposed schemas:
You must explicitly allow roles to access the exposed schemas in :ref:`db-schemas`.
.. code-block:: postgres
+4 -50
View File
@@ -3,58 +3,12 @@
Installation
############
The release page has `pre-compiled binaries for Mac OS X, Windows, Linux and FreeBSD <https://github.com/PostgREST/postgrest/releases/latest>`_ .
The release page has `pre-compiled binaries for macOS, Windows, Linux and FreeBSD <https://github.com/PostgREST/postgrest/releases/latest>`_ .
The Linux binary is a static executable that can be run on any Linux distribution.
You can also use your OS package manager.
.. tabs::
.. group-tab:: Mac OSX
You can install PostgREST from the `Homebrew official repo <https://formulae.brew.sh/formula/postgrest>`_.
.. code:: bash
brew install postgrest
.. group-tab:: FreeBSD
You can install PostgREST from the `official ports <https://www.freshports.org/www/hs-postgrest>`_.
.. code:: bash
pkg install hs-postgrest
.. group-tab:: Linux
.. tabs::
.. tab:: Arch Linux
You can install PostgREST from the `community repo <https://archlinux.org/packages/extra/x86_64/postgrest/>`_.
.. code:: bash
pacman -S postgrest
.. tab:: Nix
You can install PostgREST from nixpkgs.
.. code:: bash
nix-env -i haskellPackages.postgrest
.. group-tab:: Windows
You can install PostgREST using `Chocolatey <https://community.chocolatey.org/packages/postgrest>`_ or `Scoop <https://github.com/ScoopInstaller/Scoop>`_.
.. code:: bash
choco install postgrest
scoop install postgrest
.. include:: ../shared/installation.rst
.. _pg-dependency:
@@ -233,7 +187,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 <https://github.com/commercialhaskell/stack>`_. It will install any necessary Haskell dependencies on your system.
* `Install Stack <https://docs.haskellstack.org/en/stable/#how-to-install-stack>`_ for your platform
* `Install Stack <https://docs.haskellstack.org/en/stable/README/#how-to-install-stack>`_ for your platform
* Install Library Dependencies
===================== =======================================
@@ -242,7 +196,7 @@ You can build PostgREST from source with `Stack <https://github.com/commercialha
Ubuntu/Debian libpq-dev, libgmp-dev, zlib1g-dev
CentOS/Fedora/Red Hat postgresql-devel, zlib-devel, gmp-devel
BSD postgresql12-client
OS X libpq, gmp
macOS libpq, gmp
===================== =======================================
* Build and install binary
+15
View File
@@ -0,0 +1,15 @@
.. note::
This page is a work in progress.
.. _schema_isolation:
Schema Isolation
================
A PostgREST instance exposes all the tables, views, and stored procedures of a single `PostgreSQL schema <https://www.postgresql.org/docs/current/ddl-schemas.html>`_ (a namespace of database objects). This means private data or implementation details can go inside different private schemas and be invisible to HTTP clients.
It is recommended that you don't expose tables on your API schema. Instead expose views and stored procedures which insulate the internal details from the outside world.
This allows you to change the internals of your schema and maintain backwards compatibility. It also keeps your code easier to refactor, and provides a natural way to do API versioning.
.. image:: ../_static/db.png
-33
View File
@@ -1,33 +0,0 @@
{ 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 ];
};
}
-29
View File
@@ -1,29 +0,0 @@
{ 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;
};
}
+11 -32
View File
@@ -5,22 +5,20 @@ Create a SOAP endpoint
:author: `fjf2002 <https://github.com/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.
PostgREST supports :ref:`custom_media`. With a bit of work, SOAP endpoints become possible.
Minimal Example
---------------
This example will simply return the request body, inside a tag ``therequestbodywas``.
Add the following function to your PostgreSQL database:
.. code-block:: postgres
CREATE OR REPLACE FUNCTION my_soap_endpoint(xml) RETURNS xml AS $$
create domain "text/xml" as pg_catalog.xml;
CREATE OR REPLACE FUNCTION my_soap_endpoint(xml) RETURNS "text/xml" AS $$
DECLARE
nsarray CONSTANT text[][] := ARRAY[
ARRAY['soapenv', 'http://schemas.xmlsoap.org/soap/envelope/']
@@ -79,25 +77,6 @@ and should roughly look like:
</soapenv:Body>
</soapenv:Envelope>
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
------------------------
@@ -121,7 +100,7 @@ potentially disclosing internals to the client, but instead handle the errors di
xmlelement(NAME "soapenv:Body", body)
);
$function$;
-- helper function
CREATE OR REPLACE FUNCTION _soap_exception(
faultcode text,
@@ -137,9 +116,9 @@ potentially disclosing internals to the client, but instead handle the errors di
)
);
$function$;
CREATE OR REPLACE FUNCTION fraction_to_decimal(xml)
RETURNS xml
RETURNS "text/xml"
LANGUAGE plpgsql
AS $function$
DECLARE
@@ -207,14 +186,14 @@ The output should roughly look like:
</soapenv:Body>
</soapenv:Envelope>
References
----------
For more information concerning PostgREST, cf.
- :ref:`s_proc_single_unnamed`
- :ref:`scalar_return_formats`
- :ref:`Nginx reverse proxy <admin>`
- :ref:`custom_media`. See :ref:`any_handler`, if you need to support an ``application/soap+xml`` media type or if you want to respond with XML without sending a media type.
- :ref:`Nginx reverse proxy <nginx>`
For SOAP reference, visit
@@ -0,0 +1,321 @@
.. _providing_html_htmx:
Providing HTML Content Using Htmx
=================================
:author: `Laurence Isla <https://github.com/laurenceisla>`_
This how-to shows a way to return HTML content and use the `htmx library <https://htmx.org/>`_ to handle the AJAX requests.
Htmx expects an HTML response and uses it to replace an element inside the DOM (see the `htmx introduction <https://htmx.org/docs/#introduction>`_ in the docs).
.. image:: ../_static/how-tos/htmx-demo.gif
.. warning::
This is a proof of concept showing what can be achieved using both technologies.
We are working on `plmustache <https://github.com/PostgREST/plmustache>`_ which will further improve the HTML aspect of this how-to.
Preparatory Configuration
-------------------------
We will make a to-do app based on the :ref:`tut0`, so make sure to complete it before continuing.
To simplify things, we won't be using authentication, so grant all permissions on the ``todos`` table to the ``web_anon`` user.
.. code-block:: postgres
grant all on api.todos to web_anon;
grant usage, select on sequence api.todos_id_seq to web_anon;
Next, add the ``text/html`` as a :ref:`custom_media`. With this, PostgREST can identify the request made by your web browser (with the ``Accept: text/html`` header)
and return a raw HTML document file.
.. code-block:: postgres
create domain "text/html" as text;
Creating an HTML Response
-------------------------
Let's create a function that returns a basic HTML file, using `Tailwind CSS <https://v2.tailwindcss.com/>`_ for styling.
.. code-block:: postgres
create or replace function api.index() returns "text/html" as $$
select $html$
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PostgREST + HTMX To-Do List</title>
<!-- Tailwind for CSS styling -->
<link href="https://unpkg.com/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
</head>
<body class="bg-gray-900">
<div class="flex justify-center">
<div class="max-w-lg mt-5 p-6 bg-gray-800 border border-gray-800 rounded-lg shadow-xl">
<h5 class="mb-3 text-2xl font-bold tracking-tight text-white">PostgREST + HTMX To-Do List</h5>
</div>
</div>
</body>
</html>
$html$;
$$ language sql;
The web browser will open the web page at ``http://localhost:3000/rpc/index``.
.. image:: ../_static/how-tos/htmx-simple.jpg
.. _html_htmx_list_create:
Listing and Creating To-Dos
---------------------------
Now, let's show a list of the to-dos already inserted in the database.
For that, we'll also need a function to help us sanitize the HTML content that may be present in the task.
.. code-block:: postgres
create or replace function api.sanitize_html(text) returns text as $$
select replace(replace(replace(replace(replace($1, '&', '&amp;'), '"', '&quot;'),'>', '&gt;'),'<', '&lt;'), '''', '&apos;')
$$ language sql;
create or replace function api.html_todo(api.todos) returns text as $$
select format($html$
<li class="py-3">
<span class="ml-2 %2$s">
%3$s
</span>
</li>
$html$,
$1.id,
case when $1.done then 'line-through text-gray-400' else '' end,
api.sanitize_html($1.task)
);
$$ language sql stable;
create or replace function api.html_all_todos() returns text as $$
select coalesce(
'<ul id="todo-list" role="list" class="divide-y divide-gray-700 text-gray-100">'
|| string_agg(api.html_todo(t), '' order by t.id) ||
'</ul>',
'<p class="text-gray-100">There is nothing else to do.</p>'
)
from api.todos t;
$$ language sql;
These two functions are used to build the to-do list template. We won't use them as PostgREST endpoints.
- The ``api.html_todo`` function uses the table ``api.todos`` as a parameter and formats each item into a list element ``<li>``.
The PostgreSQL `format <https://www.postgresql.org/docs/current/functions-string.html#FUNCTIONS-STRING-FORMAT>`_ is useful to that end.
It replaces the values according to the position in the template, e.g. ``%1$s`` will be replaced with the value of ``$1.id`` (the first parameter).
- The ``api.html_all_todos`` function returns the ``<ul>`` wrapper for all the list elements.
It uses `string_arg <https://www.postgresql.org/docs/current/functions-aggregate.html>`_ to concatenate all the to-dos in a single text value.
It also returns an alternative message, instead of a list, when the ``api.todos`` table is empty.
Next, let's add an endpoint to register a to-do in the database and modify the ``/rpc/index`` page accordingly.
.. code-block:: postgres
create or replace function api.add_todo(_task text) returns "text/html" as $$
insert into api.todos(task) values (_task);
select api.html_all_todos();
$$ language sql;
create or replace function api.index() returns "text/html" as $$
select $html$
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PostgREST + HTMX To-Do List</title>
<!-- Tailwind for CSS styling -->
<link href="https://unpkg.com/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<!-- htmx for AJAX requests -->
<script src="https://unpkg.com/htmx.org"></script>
</head>
<body class="bg-gray-900"
hx-headers='{"Accept": "text/html"}'>
<div class="flex justify-center">
<div class="max-w-lg mt-5 p-6 bg-gray-800 border border-gray-800 rounded-lg shadow-xl">
<h5 class="mb-3 text-2xl font-bold tracking-tight text-white">PostgREST + HTMX To-Do List</h5>
<form hx-post="/rpc/add_todo"
hx-target="#todo-list-area"
hx-trigger="submit"
hx-on="htmx:afterRequest: this.reset()">
<input class="bg-gray-50 border text-sm rounded-lg block w-full p-2.5 mb-3 bg-gray-700 border-gray-600 placeholder-gray-400 text-white focus:ring-blue-500 focus:border-blue-500"
type="text" name="_task" placeholder="Add a todo...">
</form>
<div id="todo-list-area">
$html$
|| api.html_all_todos() ||
$html$
<div>
</div>
</div>
</body>
</html>
$html$;
$$ language sql;
- The ``/rpc/add_todo`` endpoint allows us to add a new to-do using the ``_task`` parameter and returns an ``html`` with all the to-dos in the database.
- The ``/rpc/index`` now adds the ``hx-headers='{"Accept": "text/html"}'`` tag to the ``<body>``.
This will make sure that all htmx elements inside the body send this header, otherwise PostgREST won't recognize it as HTML.
There is also a ``<form>`` element that uses the htmx library. Let's break it down:
+ ``hx-post="/rpc/add_todo"``: sends an AJAX POST request to the ``/rpc/add_todo`` endpoint, with the value of the ``_task`` from the ``<input>`` element.
+ ``hx-target="#todo-list-area"``: the HTML content returned from the request will go inside ``<div id="todo-list-area"></div>`` (which is the list of to-dos).
+ ``hx-trigger="submit"``: htmx will do this request when submitting the form (by pressing enter while inside the ``<input>``).
+ ``hx-on="htmx:afterRequest: this.reset()">``: this is a Javascript command that clears the form `after the request is done <https://htmx.org/events/#htmx:afterRequest>`_.
With this, the ``http://localhost:3000/rpc/index`` page lists all the todos and adds new ones by submitting tasks in the input element.
Don't forget to refresh the :ref:`schema cache <schema_reloading>`.
.. image:: ../_static/how-tos/htmx-insert.gif
Editing and Deleting To-Dos
---------------------------
Now, let's modify ``api.html_todo`` and make it more functional.
.. code-block:: postgres
create or replace function api.html_todo(api.todos) returns text as $$
select format($html$
<li class="py-3">
<div class="flex justify-between items-center">
<div id="todo-edit-area-%1$s" class="pr-5">
<form id="edit-task-state-%1$s"
hx-post="/rpc/change_todo_state"
hx-vals='{"_id": %1$s, "_done": %4$s}'
hx-target="#todo-list-area"
hx-trigger="click">
<span class="ml-2 %2$s cursor-pointer">
%3$s
</span>
</form>
</div>
<div>
<button class="p-1.5 rounded-full hover:bg-gray-700 focus:ring-gray-800"
hx-get="/rpc/html_editable_task"
hx-vals='{"_id": "%1$s"}'
hx-target="#todo-edit-area-%1$s"
hx-trigger="click">
<svg class="w-4 h-4 text-blue-300" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 18">
<path d="M12.687 14.408a3.01 3.01 0 0 1-1.533.821l-3.566.713a3 3 0 0 1-3.53-3.53l.713-3.566a3.01 3.01 0 0 1 .821-1.533L10.905 2H2.167A2.169 2.169 0 0 0 0 4.167v11.666A2.169 2.169 0 0 0 2.167 18h11.666A2.169 2.169 0 0 0 16 15.833V11.1l-3.313 3.308Zm5.53-9.065.546-.546a2.518 2.518 0 0 0 0-3.56 2.576 2.576 0 0 0-3.559 0l-.547.547 3.56 3.56Z"/>
<path d="M13.243 3.2 7.359 9.081a.5.5 0 0 0-.136.256L6.51 12.9a.5.5 0 0 0 .59.59l3.566-.713a.5.5 0 0 0 .255-.136L16.8 6.757 13.243 3.2Z"/>
</svg>
</button>
<button class="p-1.5 rounded-full hover:bg-gray-700 focus:ring-gray-800"
hx-post="/rpc/delete_todo"
hx-vals='{"_id": %1$s}'
hx-target="#todo-list-area"
hx-trigger="click">
<svg class="w-4 h-4 text-red-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 18 20">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h16M7 8v8m4-8v8M7 1h4a1 1 0 0 1 1 1v3H6V2a1 1 0 0 1 1-1ZM3 5h12v13a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5Z"/>
</svg>
</button>
</div>
</div>
</li>
$html$,
$1.id,
case when $1.done then 'line-through text-gray-400' else '' end,
api.sanitize_html($1.task),
(not $1.done)::text
);
$$ language sql stable;
Let's deconstruct the new htmx features added:
- The ``<form>`` element is configured as follows:
+ ``hx-post="/rpc/change_todo_state"``: does an AJAX POST request to that endpoint. It will toggle the ``done`` state of the to-do.
+ ``hx-vals='{"_id": %1$s, "_done": %4$s}'``: adds the parameters to the request.
This is an alternative to using hidden inputs inside the ``<form>``.
+ ``hx-trigger="click"``: htmx does the request after clicking on the element.
- For the first ``<button>``:
+ ``hx-get="/rpc/html_editable_task"``: it does an AJAX GET request to that endpoint.
It returns an HTML with an input that will allow us to edit the task.
+ ``hx-target="#todo-edit-area"``: the returned HTML will replace the element with this id.
In this case, this replaces an individual task, not the whole list.
+ ``hx-vals='{"id": "eq.%1$s"}'``: adds the query parameters to the GET request.
Note that this needs the ``eq.`` operator because it represents a table column not a function parameter.
- For the second ``<button>``:
+ ``hx-post="/rpc/delete_todo"``: this post request will delete the corresponding to-do.
Clicking on the first button will enable the task editing.
That's why we create the ``api.html_editable_task`` function as an endpoint:
.. code-block:: postgres
create or replace function api.html_editable_task(_id int) returns "text/html" as $$
select format ($html$
<form id="edit-task-%1$s"
hx-post="/rpc/change_todo_task"
hx-headers='{"Accept": "text/html"}'
hx-vals='{"_id": %1$s}'
hx-target="#todo-list-area"
hx-trigger="submit,focusout">
<input class="bg-gray-50 border text-sm rounded-lg block w-full p-2.5 bg-gray-700 border-gray-600 text-white focus:ring-blue-500 focus:border-blue-500"
id="task-%1$s" type="text" name="_task" value="%2$s" autofocus>
</form>
$html$,
id,
api.sanitize_html(task)
)
from api.todos
where id = _id;
$$ language sql;
In this example, this will return an input field that allows us to edit the corresponding to-do task.
Finally, let's add the endpoints that will modify and delete the to-dos in the database.
.. code-block:: postgres
create or replace function api.change_todo_state(_id int, _done boolean) returns "text/html" as $$
update api.todos set done = _done where id = _id;
select api.html_all_todos();
$$ language sql;
create or replace function api.change_todo_task(_id int, _task text) returns "text/html" as $$
update api.todos set task = _task where id = _id;
select api.html_all_todos();
$$ language sql;
create or replace function api.delete_todo(_id int) returns "text/html" as $$
delete from api.todos where id = _id;
select api.html_all_todos();
$$ language sql;
All of those functions return an HTML list of to-dos that will replace the outdated one:
- The ``api.change_todo_state`` function updates the ``done`` column using the ``_id`` and the ``_done`` values from the request.
- The ``api.delete_todo`` function deletes a to-do using the ``_id`` value from the request.
- The ``api.change_todo_task`` function modifies the ``task`` column using the ``_id`` and the ``_task`` value from the request.
After refreshing the :ref:`schema cache <schema_reloading>`, the page at ``http://localhost:3000/rpc/index`` will allow us to edit, delete and complete any to-do.
.. image:: ../_static/how-tos/htmx-edit-delete.gif
With that, we completed the to-do list functionality.
+36 -10
View File
@@ -26,18 +26,42 @@ First, we need a public table for storing the files.
, 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:`<img>` tag will not work.
That's because browsers do not send the required :code:`Accept: application/octet-stream` header.
Let's assume this table contains an image of two cute kittens with id 42. We can retrieve this image in binary format from our PostgREST API by using :ref:`custom_media`:
.. code-block:: postgres
create domain "application/octet-stream" as bytea;
create or replace function file(id int) returns "application/octet-stream" as $$
select blob from files where id = file.id;
$$ language sql;
Now we can request the RPC endpoint :code:`/rpc/file?id=42` with the :code:`Accept: application/octet-stream` header.
.. code-block:: bash
curl "localhost:3000/rpc/file?id=42" -H "Accept: application/octet-stream"
Unfortunately, putting the URL into the :code:`src` of an :code:`<img>` tag will not work. That's because browsers do not send the required :code:`Accept: application/octet-stream` header.
Instead, the :code:`Accept: image/webp` header is sent by many web browsers by default.
Luckily we can change the accepted media type in the function like so:
.. code-block:: postgres
create domain "image/webp" as bytea;
create or replace function file(id int) returns "image/webp" as $$
select blob from files where id = file.id;
$$ language sql;
Luckily we can specify the accepted media types in the :ref:`raw-media-types` configuration variable.
In this case, the :code:`Accept: image/webp` header is sent by many web browsers by default, so let's add it to the configuration variable, like this: :code:`raw-media-types="image/webp"`.
Now, the image will be displayed in the HTML page:
.. code-block:: html
<img src="http://localhost:3000/files?select=blob&id=eq.42" alt="Cute Kittens"/>
<img src="http://localhost:3000/file?id=42" alt="Cute Kittens"/>
Improved Version
----------------
@@ -60,13 +84,15 @@ First, in addition to the minimal example, we need to store the media types and
add column type text,
add column name text;
Next, we set up an RPC endpoint that sets the content type and filename.
Next, we set modify the function to set the content type and filename.
We use this opportunity to configure some basic, client-side caching.
For production, you probably want to configure additional caches, e.g. on the :ref:`reverse proxy <admin>`.
.. code-block:: postgres
create function file(id int) returns bytea as
create domain "*/*" as bytea;
create function file(id int) returns "*/*" as
$$
declare headers text;
declare blob bytea;
@@ -79,7 +105,7 @@ For production, you probably want to configure additional caches, e.g. on the :r
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
if FOUND -- special var, see https://www.postgresql.org/docs/current/plpgsql-statements.html#PLPGSQL-STATEMENTS-DIAGNOSTICS
then return(blob);
else raise sqlstate 'PT404' using
message = 'NOT FOUND',
@@ -62,7 +62,7 @@ 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``.
The response has the date in the time zone configured by the server: ``UTC -05:00`` (see :ref:`prefer_timezone`).
You can use other comparative filters and also all the `PostgreSQL special date/time input values <https://www.postgresql.org/docs/current/datatype-datetime.html#DATATYPE-DATETIME-SPECIAL-TABLE>`_ as illustrated in this example:
@@ -506,33 +506,26 @@ 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, set the ``Accept: application/octet-stream`` header and select only the
``bytea`` type column.
To get the image from the database, use :ref:`custom_media` like so:
.. code-block:: postgres
create domain "image/png" as bytea;
create or replace get_image(id int) returns "image/png" as $$
select file from files where id = $1;
$$ language sql;
.. 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"
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::
.. code-tab:: http
GET /files?select=file&id=eq.1 HTTP/1.1
GET /get_image?id=1 HTTP/1.1
Accept: image/png
.. code-tab:: bash Curl
curl "http://localhost:3000/files?select=file&id=eq.1" \
curl "http://localhost:3000/get_image?id=1" \
-H "Accept: image/png"
See :ref:`providing_img` for a step-by-step example on how to handle images in HTML.
+28 -10
View File
@@ -16,9 +16,15 @@ PostgREST Documentation
.. image:: https://img.shields.io/docker/pulls/postgrest/postgrest.svg
:target: https://hub.docker.com/r/postgrest/postgrest/
.. image:: https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg
:target: https://gitter.im/begriffs/postgrest
.. image:: https://img.shields.io/badge/Donate-Patreon-orange.svg?colorB=F96854
:target: https://www.patreon.com/postgrest
.. image:: https://img.shields.io/badge/Donate-PayPal-green.svg
:target: https://www.paypal.com/paypalme/postgrest
|
PostgREST is a standalone web server that turns your PostgreSQL database directly into a RESTful API. The structural constraints and permissions in the database determine the API endpoints and operations.
@@ -33,17 +39,25 @@ Sponsors
:width: 13em
.. image:: _static/gnuhost.png
:target: https://euronodes.com/?utm_source=sponsor&utm_campaign=postgrest
:target: https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest
:width: 13em
.. image:: _static/neon.jpg
:target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest
:width: 13em
|
.. image:: _static/code-build.webp
:target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
:width: 13em
.. image:: _static/supabase.png
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
:width: 13em
.. image:: _static/neon.jpg
:target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest
.. image:: _static/tembo.png
:target: https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest
:width: 13em
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
@@ -78,15 +92,12 @@ 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. For discussions, use the Github `discussions page <https://github.com/PostgREST/postgrest/discussions>`_. You can also report or search for bugs/features on the Github `issues <https://github.com/PostgREST/postgrest/issues>`_ page.
The project has a friendly and growing community. For discussions, use the Github `discussions page <https://github.com/PostgREST/postgrest/discussions>`_ or join our `chat room <https://gitter.im/begriffs/postgrest>`_. You can also report or search for bugs/features on the Github `issues <https://github.com/PostgREST/postgrest/issues>`_ page.
.. toctree::
:glob:
:caption: Release Notes
:reversed:
:maxdepth: 1
Release Notes
-------------
releases/*
The release notes are published on `PostgREST's GitHub release page <https://github.com/PostgREST/postgrest/releases>`_.
Tutorials
---------
@@ -184,12 +195,19 @@ Here are some companies that use PostgREST in production.
* `Datrium <https://www.datrium.com>`_
* `Drip Depot <https://www.dripdepot.com>`_
* `Image-charts <https://www.image-charts.com>`_
* `Moat <https://www.moat.com>`_
* `Netwo <https://www.netwo.io>`_
* `Nimbus <https://www.nimbusforwork.com>`_
- See how Nimbus uses PostgREST in `Paul Copplestone's blog post <https://paul.copplest.one/blog/nimbus-tech-2019-04.html>`_.
* `OpenBooking <https://www.openbooking.ch>`_
* `Redsmin <https://www.redsmin.com>`_
* `Sompani <https://www.sompani.com>`_
* `Supabase <https://supabase.com>`_
.. Failing links
* `eGull <http://www.egull.co>`_
* `MotionDynamic - Fast highly dynamic video generation at scale <https://motiondynamic.tech>`_
Testimonials
------------
+6
View File
@@ -0,0 +1,6 @@
Greenplum
#########
`Greenplum <https://greenplum.org/>`_ has been reported to work by adding ``LOGIN`` to the :ref:`anonymous and user roles <roles>`.
For more details, see https://github.com/PostgREST/postgrest/issues/2021.
+8
View File
@@ -12,6 +12,12 @@ First, create postgrest configuration in ``/etc/postgrest/config``
db-anon-role = "<your_anon_role>"
jwt-secret = "<your_secret>"
Create a dedicated ``postgrest`` user with:
.. code-block:: ini
sudo useradd -M -U -d /nonexistent -s /usr/sbin/nologin postgrest
Then create the systemd service file in ``/etc/systemd/system/postgrest.service``
.. code-block:: ini
@@ -21,6 +27,8 @@ Then create the systemd service file in ``/etc/systemd/system/postgrest.service`
After=postgresql.service
[Service]
User=postgrest
Group=postgrest
ExecStart=/bin/postgrest /etc/postgrest/config
ExecReload=/bin/kill -SIGUSR1 $MAINPID
+12 -25
View File
@@ -10,54 +10,52 @@ authenticator
backoff
balancer
booleans
Bouscal
buildpack
BOM
Bytea
Cardano
casted
cd
centric
changelog
coercible
conf
Cloudflare
config
cors
CORS
CPUs
cryptographically
CSV
durations
DDL
DOM
DevOps
DiBiase
dockerize
eq
ETH
Ethereum
EveryLayout
Fenko
Fernandes
filename
FreeBSD
fts
GC
GeoJSON
GHC
Github
Google
grantor
GraphQL
Greenplum
gte
GUC
GUCs
gucs
Haskell
Heroku
HMAC
htmx
Htmx
Homebrew
hstore
HTTP
HTTPS
HV
Ibarluzea
Inlining
inlined
Integrations
@@ -84,6 +82,7 @@ logins
lon
lt
lte
macOS
misprediction
multi
namespace
@@ -101,9 +100,7 @@ openapi
ORM
ov
passphrase
Pawel
PBKDF
Pelletier
PgBouncer
pgcrypto
pgjwt
@@ -117,7 +114,6 @@ phraseto
plainto
plfts
poolers
POSIX
PostGIS
PostgreSQL
PostgreSQL's
@@ -129,52 +125,43 @@ pre
preflight
plpgsql
psql
Qin
RabbitMQ
Rafaj
RDS
reallyreallyreallyreallyverysafe
Rechkemmer
Redux
refactor
reloadable
Reloadable
Remo
requester's
RESTful
RLS
RPC
RSA
Saleeba
safeupdate
savepoint
schemas
schema's
Severin
SHA
Sommer
signup
SIGUSR
sl
spreaded
Spreaded
SQL
sql
sr
SSL
stateful
stdout
Stolarz
supervisees
SvelteKit
SwaggerUI
syslog
systemd
todo
todos
tos
Tsingson
tsquery
tx
Tyll
TypeScript
UI
ui
@@ -182,6 +169,7 @@ unicode
unikernel
unix
updatable
unfulfillable
Untyped
UPSERT
Upsert
@@ -201,4 +189,3 @@ Websockets
webuser
wfts
www
Zac
+39 -2
View File
@@ -106,9 +106,11 @@ When debugging a problem it's important to verify the running PostgREST version.
------------------------------
PostgREST 11.1.0
.. note::
.. important::
The server sets the `fallback_application_name <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-FALLBACK-APPLICATION-NAME>`_ for this query to work. To override the value set ``application_name`` on the connection string.
- The server sets the `fallback_application_name <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-FALLBACK-APPLICATION-NAME>`_ to the connection URI for this query to work. To override the value set ``application_name`` on the connection string.
- The version will only be set if it's a valid URI (`RFC 3986 <https://datatracker.ietf.org/doc/html/rfc3986>`_). This means any special characters must be urlencoded.
- The version will not be set if the connection string is in `keyword/value format <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-KEYWORD-VALUE>`_.
- The ``stderr`` logs also contain the version, as noted on :ref:`pgrst_logging`.
@@ -141,6 +143,41 @@ You can enable tracing HTTP requests by setting :ref:`server-trace-header`. Spec
HTTP/1.1 200 OK
X-Request-Id: 123
.. _server-timing_header:
Server-Timing Header
--------------------
You can enable the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>`_ header by setting :ref:`server-timing-enabled` on.
This header communicates metrics of the different phases in the request-response cycle.
.. tabs::
.. code-tab:: http
GET /users HTTP/1.1
.. code-tab:: bash Curl
curl "http://localhost:3000/users" -i
.. code::
HTTP/1.1 200 OK
Server-Timing: jwt;dur=14.9, parse;dur=71.1, plan;dur=109.0, transaction;dur=353.2, response;dur=4.4
- All the durations (``dur``) are in milliseconds.
- The ``jwt`` stage is when :ref:`jwt_impersonation` is done. This duration can be lowered with :ref:`jwt_caching`.
- On the ``parse`` stage, the :ref:`url_grammar` is parsed.
- On the ``plan`` stage, the :ref:`schema_cache` is used to generate the :ref:`main_query` of the transaction.
- The ``transaction`` stage corresponds to the database transaction. See :ref:`transactions`.
- The ``response`` stage is where the response status and headers are computed.
.. note::
We're working on lowering the duration of the ``parse`` and ``plan`` stages on https://github.com/PostgREST/postgrest/issues/2816.
.. _explain_plan:
Execution plan
+5 -1
View File
@@ -14,9 +14,13 @@ PostgREST exposes three database objects of a schema as resources: tables, views
api/schemas.rst
api/computed_fields.rst
api/domain_representations.rst
api/pagination_count.rst
api/resource_embedding.rst
api/resource_representation.rst
api/media_type_handlers.rst
api/aggregate_functions.rst
api/openapi.rst
api/preferences.rst
api/*
.. raw:: html
@@ -100,7 +104,7 @@ PostgREST exposes three database objects of a schema as resources: tables, views
'#immutable-and-stable-functions': 'transactions.html#access-mode',
'#http-context': 'transactions.html#transaction-scoped-settings',
'#accessing-request-headers-cookies-and-jwt-claims': 'transactions.html#request-headers-cookies-and-jwt-claims',
'#legacy-guc-variable-names': 'transactions.html#legacy-settings',
'#legacy-guc-variable-names': 'transactions.html#transaction-scoped-settings',
'#accessing-request-path-and-method': 'transactions.html#request-path-and-method',
'#setting-response-headers': 'transactions.html#response-headers',
'#setting-headers-via-pre-request': 'transactions.html#setting-headers-via-pre-request',
+342
View File
@@ -0,0 +1,342 @@
.. _aggregate_functions:
Aggregate Functions
###################
Aggregate functions allow you to summarize data by performing calculations across groups of rows. For instance, if you have an ``orders`` table that has an ``amount`` column, you could use an aggregate function to get the sum of the ``amount`` column, either for all rows, or for each group of rows that share specific values, for instance all rows that share the same ``order_date``.
.. note::
Aggregate functions are *disabled* by default in PostgREST, as without appropriate safeguards, aggregate functions can create performance problems. See :ref:`db-aggregates-enabled` for further details.
PostgREST supports the following aggregate functions: ``avg()``, ``count()``, ``max()``, ``min()``, and ``sum()``. Please refer to the `section on aggregate functions in the PostgreSQL documentation <https://www.postgresql.org/docs/current/functions-aggregate.html>`_ for a detailed explanation of these functions.
To use an aggregate function, you append the function to a value in the ``select`` parameter, like so:
.. tabs::
.. code-tab:: http
GET /orders?select=amount.sum() HTTP/1.1
.. code-tab:: bash Curl
curl "http://localhost:3000/orders?select=amount.sum()"
With the above query, PostgREST will return a single row with a single column named ``sum`` that contains the sum of all the values in the ``amount`` column:
.. code-block:: json
[
{
"sum": 1234.56
}
]
You can use multiple aggregate functions by just adding more columns with aggregate functions to the ``select`` parameter.
To group by other columns, you simply add those columns to the ``select`` parameter. For instance:
.. tabs::
.. code-tab:: http
GET /orders?select=amount.sum(),amount.avg(),order_date HTTP/1.1
.. code-tab:: bash Curl
curl "http://localhost:3000/orders?select=amount.sum(),amount.avg(),order_date"
This will return a row for each unique value in the ``order_date`` column, with the sum and average of the ``amount`` column for all rows that share the same ``order_date``:
.. code-block:: json
[
{
"sum": 1234.56,
"avg": 123.45,
"order_date": "2023-01-01"
},
{
"sum": 2345.67,
"avg": 234.56,
"order_date": "2023-01-02"
}
]
.. note::
Aggregate functions work alongside other PostgREST features, like :ref:`h_filter`, :ref:`json_columns`, and :ref:`ordering`. Please note at this time aggregate functions are not compatible with :ref:`domain_reps`. Additionally, PostgreSQL's ``HAVING`` clause and ordering by aggregated columns are not yet supported.
The Case of ``count()``
===========================
.. note::
Before the addition of aggregate functions, it was possible to count by adding ``count`` (without parentheses) to the ``select`` parameter. While this is still supported, it may be deprecated in the future, and thus use of this legacy feature is **not recommended.** Please use ``count()`` (with parentheses) instead.
``count()`` is treated specially, as it can be used without an associated column. Take for example the following query:
.. tabs::
.. code-tab:: http
GET /orders?select=count(),order_date HTTP/1.1
.. code-tab:: bash Curl
curl "http://localhost:3000/orders?select=count(),order_date"
This would return a row for each unique value in the ``order_date`` column, with the count of all rows that share the same ``order_date``:
.. code-block:: json
[
{
"count": 4,
"order_date": "2023-01-01"
},
{
"count": 2,
"order_date": "2023-01-02"
}
]
When ``count()`` is used with an associated column, its behaviour is slightly different: It will return the count of all values that are not ``NULL``. This is due to how PostgreSQL itself implements the ``count()`` function.
Renaming and Casting
====================
Renaming Aggregates
-------------------
Just like with other columns, you can rename aggregated columns too. See :ref:`renaming_columns` for details.
Renaming columns is especially helpful in the context of aggregate functions, as by default a column with an aggregate function applied will take on the name of the applied aggregate function. You may want to provide a more semantically meaningful name or prevent collisions when using multiple aggregate functions of the same type.
Casting Aggregates
------------------
When applying an aggregate function to a column, you are able to cast both the value of the input to the aggregate function *and* the value of the output from the aggregate function. In both cases, the syntax works as described in :ref:`casting_columns`, with the only difference being the placement of the cast.
Casting the Value of the Input
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For instance, imagine that the ``orders`` table has a JSON column, ``order_details``, and this column contains a JSON object that has a key, ``tax_amount``. Let's say you want to get the sum of the tax amount for every order. You can use the ``->`` or ``->>`` operators to extract the value with this key (see :ref:`json_columns`), but these operators will return values of the types JSON and ``text`` respectively, and neither of these types can be used with ``sum()``.
Therefore, you will need to first cast the input value to a type that is compatible with ``sum()`` (e.g. ``numeric``). Casting the input value is done in exactly the same way as casting any other value:
.. tabs::
.. code-tab:: http
GET /orders?select=order_details->tax_amount::numeric.sum() HTTP/1.1
.. code-tab:: bash Curl
curl "http://localhost:3000/orders?select=order_details->tax_amount::numeric.sum()"
With this, you will receive the sum of the casted ``tax_amount`` value:
.. code-block:: json
[
{
"sum": 1234.56
}
]
Casting the Value of the Output
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now let's return to an example involving the ``amount`` column of the ``orders`` table. Imagine that we want to get the rounded average of the ``amount`` column. One way to do this is to use the ``avg()`` aggregate function and then to cast the output value of the function to ``int``. To cast the value of the output of the function, we simply place the cast *after* the aggregate function:
.. tabs::
.. code-tab:: http
GET /orders?select=amount.avg()::int HTTP/1.1
.. code-tab:: bash Curl
curl "http://localhost:3000/orders?select=amount.avg()::int"
You will then receive the rounded average as the result:
.. code-block:: json
[
{
"avg": 201
}
]
Of course, you can use both input and output casts at the same time, if you so desire.
Using Aggregate Functions with Resource Embedding
=================================================
Aggregate functions can be used in conjunction with :ref:`resource_embedding`. You can use embedded resources as grouping columns, use aggregate functions within the context of an embedded resource, or use columns from a spreaded resource as grouping columns or as inputs to aggregate functions.
Using Embedded Resources as Grouping Columns
--------------------------------------------
Using an embedded resource as a grouping column allows you to use data from an association to group the results of an aggregation.
For example, imagine that the ``orders`` table from the examples above is related to a ``customers`` table. If you want to get the sum of the ``amount`` column grouped by the ``name`` column from the ``customers`` table, you can include the customer name, using the standard :ref:`resource_embedding` syntax, and perform a sum on the ``amount`` column.
.. tabs::
.. code-tab:: http
GET /orders?select=amount.sum(),customers(name) HTTP/1.1
.. code-tab:: bash Curl
curl "http://localhost:3000/orders?select=amount.sum(),customers(name)"
You will then get the summed amount, along with the embedded customer resource:
.. code-block:: json
[
{
"sum": 100,
"customers": {
"name": "Customer A"
}
},
{
"sum": 200,
"customers": {
"name": "Customer B"
}
}
]
.. note::
The previous example uses a has-one association to demonstrate this functionality, but you may also use has-many associations as grouping columns, although there are few obvious use cases for this.
Using Aggregate Functions Within the Context of an Embedded Resource
--------------------------------------------------------------------
When embedding a resource, you can apply aggregate functions to columns from the associated resource to perform aggregations within the context of an embedded resource.
Continuing with the example relationship between ``orders`` and ``customers`` from the previous section, imagine that you want to fetch the ``name``, ``city``, and ``state`` for each customer, along with the sum of amount of the customer's orders, grouped by the order date. This can be done in the following way:
.. tabs::
.. code-tab:: http
GET /customers?select=name,city,state,orders(amount.sum(),order_date) HTTP/1.1
.. code-tab:: bash Curl
curl "http://localhost:3000/customers?select=name,city,state,orders(amount.sum(),order_date)"
.. code-block:: json
[
{
"name": "Customer A",
"city": "New York",
"state": "NY",
"orders": [
{
"sum": 215.22,
"order_date": "2023-09-01"
},
{
"sum": 905.73,
"order_date": "2023-09-02"
}
]
},
{
"name": "Customer B",
"city": "Los Angeles",
"state": "CA",
"orders": [
{
"sum": 329.71,
"order_date": "2023-09-01"
},
{
"sum": 425.87,
"order_date": "2023-09-03"
}
]
}
]
In this example, the ``amount`` column is summed and grouped by the ``order_date`` *within* the context of the embedded resource. That is, the ``name``, ``city``, and ``state`` from the ``customers`` table have no bearing on the aggregation performed in the context of the ``orders`` association; instead, each aggregation can be seen as being performed independently on just the orders belonging to a particular customer, using only the data from the embedded resource for both grouping and aggregation.
Using Columns from a Spreaded Resource
--------------------------------------
When you :ref:`spread an embedded resource <spread_embed>`, the columns from the spreaded resource are treated as if they were columns of the top-level resource, both when using them as grouping columns and when applying aggregate functions to them.
Grouping with Columns from a Spreaded Resource
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
For instance, assume you want to sum the ``amount`` column from the ``orders`` table, using the ``city`` and ``state`` columns from the ``customers`` table as grouping columns. To achieve this, you may select these two columns from the ``customers`` table and spread them; they will then be used as grouping columns:
.. tabs::
.. code-tab:: http
GET /orders?select=amount.sum(),...customers(city,state) HTTP/1.1
.. code-tab:: bash Curl
curl "http://localhost:3000/orders?select=amount.sum(),...customers(city,state)
The result will be the same as if ``city`` and ``state`` were columns from the ``orders`` table:
.. code-block:: json
[
{
"sum": 2000.29,
"city": "New York",
"state": "NY"
},
{
"sum": 9241.21,
"city": "Los Angeles",
"state": "CA"
}
]
Aggregate Functions with Columns from a Spreaded Resource
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Now imagine that the ``customers`` table has a ``joined_date`` column that represents the date that the customer joined. You want to get both the most recent and the oldest ``joined_date`` for customers that placed an order on every distinct order date. This can be expressed as follows:
.. tabs::
.. code-tab:: http
GET /orders?select=order_date,...customers(joined_date.max(),joined_date.min()) HTTP/1.1
.. code-tab:: bash Curl
curl "http://localhost:3000/orders?select=order_date,...customers(joined_date.max(),joined_date.min())
As columns from a spreaded resource are treated as if they were columns from the top-level resource, the ``max()`` and ``min()`` are applied *within* the context of the top-level, rather than within the context of the embedded resource, as in the previous section.
The result will be the same as if the aggregations were applied to columns from the top-level:
.. code-block:: json
[
{
"order_date": "2023-11-01",
"max": "2023-10-15",
"min": "2013-10-01"
},
{
"order_date": "2023-11-02",
"max": "2023-10-30",
"min": "2016-02-11"
}
]
+18 -2
View File
@@ -1,7 +1,10 @@
.. _cors:
CORS
====
####
By default, PostgREST sets highly permissive cross origin resource sharing, that is why it accepts Ajax requests from any domain. This behavior can be configured by using :ref:`server_cors_allowed_origins`.
PostgREST sets highly permissive cross origin resource sharing, that is why it accepts Ajax requests from any domain.
It also handles `preflight requests <https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request>`_ done by the browser, which are cached using the returned ``Access-Control-Max-Age: 86400`` header (86400 seconds = 24 hours). This is useful to reduce the latency of the subsequent requests.
@@ -32,3 +35,16 @@ A ``POST`` preflight request would look like this:
Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD
Access-Control-Allow-Headers: Authorization, Content-Type, Accept, Accept-Language, Content-Language
Access-Control-Max-Age: 86400
.. _allowed_origins:
Allowed Origins
===============
With the following config setting, PostgREST will accept CORS requests from domains :code:`http://example.com` and :code:`http://example2.com`.
.. code-block::
server-cors-allowed-origins="http://example.com, http://example2.com"
+312
View File
@@ -0,0 +1,312 @@
.. _custom_media:
Media Type Handlers
###################
Media Type Handlers allow PostgREST to deliver custom media types. These handlers extend the :ref:`builtin ones <builtin_media>` and can also override them.
Media types are expressed as type aliases using `domains <https://www.postgresql.org/docs/current/sql-createdomain.html>`_ and their name must comply to `RFC 6838 requirements <https://datatracker.ietf.org/doc/html/rfc6838#section-4.2>`_.
.. code-block:: postgres
CREATE DOMAIN "application/json" AS json;
Using these domains, :ref:`functions <s_procs>` can become handlers and `user-defined aggregates <https://www.postgresql.org/docs/current/xaggr.html>`_ can serve as handlers for :ref:`tables_views` and :ref:`table_functions`.
.. important::
- PostgREST vendor media types (``application/vnd.pgrst.plan``, ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``) cannot be overriden.
- Long media types like ``application/vnd.openxmlformats-officedocument.wordprocessingml.document`` cannot be expressed as domains since they surpass `PostgreSQL identifier length <https://www.postgresql.org/docs/current/limits.html#LIMITS-TABLE>`_.
For these you can use the :ref:`any_handler`.
Handler Function
================
As an example, let's obtain the `TWKB <https://postgis.net/docs/ST_AsTWKB.html>`_ compressed binary format for a PostGIS geometry.
.. code-block:: postgres
create extension postgis;
create table lines (
id int primary key
, name text
, geom geometry(LINESTRING, 4326)
);
insert into lines values (1, 'line-1', 'LINESTRING(1 1,5 5)'::geometry), (2, 'line-2', 'LINESTRING(2 2,6 6)'::geometry);
For this you can create a vendor media type.
.. code-block:: postgres
create domain "application/vnd.twkb" as bytea;
And use it as a return type on a function, to make it a handler.
.. code-block:: postgres
create or replace function get_line (id int)
returns "application/vnd.twkb" as $$
select st_astwkb(geom) from lines where id = get_line.id;
$$ language sql;
.. note::
For PostgreSQL <= 12, you'll need a cast on the function body :code:`st_astwkb(geom)::"application/vnd.twkb"`.
Now you can request the ``TWKB`` output like so:
.. code-block:: bash
curl 'localhost:3000/rpc/get_line?id=1' -i \
-H "Accept: application/vnd.twkb"
HTTP/1.1 200 OK
Content-Type: application/vnd.twkb
# binary output
Note that PostgREST will automatically set the ``Content-Type`` to ``application/vnd.twkb``.
Handlers for Tables/Views
=========================
To benefit from a compressed format like ``TWKB``, it makes more sense to obtain many rows instead of one. Let's allow that by adding a handler for the table.
User-defined aggregates can be turned into handlers by using domain media types as the return type of their transition or final functions.
Let's create a transition function for this example.
.. code-block:: postgres
create or replace function twkb_handler_transition (state bytea, next lines)
returns "application/vnd.twkb" as $$
select state || st_astwkb(next.geom);
$$ language sql;
Now we'll use it on a new aggregate defined for the ``lines`` table.
.. code-block:: postgres
create or replace aggregate twkb_agg (lines) (
initcond = ''
, stype = "application/vnd.twkb"
, sfunc = twkb_handler_transition
);
Make a quick test on SQL to see it working.
.. code-block:: psql
SELECT twkb_agg(l) from lines l;
twkb_agg
---------------------------------------------------------------
\xa20002c09a0cc09a0c80ea3080ea30a2000280b51880b51880ea3080ea30
(1 row)
Now you can request the table endpoint with the ``twkb`` media type:
.. code-block:: bash
curl 'localhost:3000/lines' -i \
-H "Accept: application/vnd.twkb"
HTTP/1.1 200 OK
Content-Type: application/vnd.twkb
# binary output
If you have a table-valued function returning the same table type, the handler can also act upon on it.
.. code-block:: postgres
create or replace function get_lines ()
returns setof lines as $$
select * from lines;
$$ language sql;
.. code-block:: bash
curl 'localhost:3000/get_lines' -i \
-H "Accept: application/vnd.twkb"
HTTP/1.1 200 OK
Content-Type: application/vnd.twkb
# binary output
Overriding a Builtin Handler
============================
Let's override the existing ``text/csv`` handler for the table to provide a more complex CSV output.
It'll include a `Byte order mark (BOM) <https://en.wikipedia.org/wiki/Byte_order_mark>`_ plus a ``Content-Disposition`` header to set a name for the downloaded file.
Create a domain for the standard ``text/csv`` media type.
.. code-block:: postgres
create domain "text/csv" as text;
And a transition function that returns the domain.
.. code-block:: postgres
create or replace function bom_csv_trans (state text, next lines)
returns "text/csv" as $$
select state || next.id::text || ',' || next.name || ',' || next.geom::text || E'\n';
$$ language sql;
This time we'll add a final function. This will add the CSV header, the BOM and the ``Content-Disposition`` header.
.. code-block:: postgres
create or replace function bom_csv_final (data "text/csv")
returns "text/csv" as $$
-- set the Content-Disposition header
select set_config('response.headers', '[{"Content-Disposition": "attachment; filename=\"lines.csv\""}]', true);
select
-- EFBBBF is the BOM in UTF8 https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8
convert_from (decode (E'EFBBBF', 'hex'),'UTF8') ||
-- the header for the CSV
(E'id,name,geom\n' || data);
$$ language sql;
Now use the transition and final function as part of the new aggregate.
.. code-block:: postgres
create or replace aggregate bom_csv_agg (lines) (
initcond = ''
, stype = "text/csv"
, sfunc = bom_csv_trans
, finalfunc = bom_csv_final
);
.. code-block:: psql
select bom_csv_agg(l) from lines l;
bom_csv_agg
-----------------------------------------------------------------------------------------------------
id,name,geom +
1,line-1,0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440+
2,line-2,0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840+
(1 row)
And request it like:
.. code-block:: bash
curl 'localhost:3000/lines' -i \
-H "Accept: text/csv"
HTTP/1.1 200 OK
Content-Type: text/csv
Content-Disposition: attachment; filename="lines.csv"
id,name,geom
1,line-1,0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440
2,line-2,0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840
.. _any_handler:
The "Any" Handler
=================
For more flexibility, you can also define a catch-all handler by using a domain named ``*/*`` (any media type). This obeys to the following rules:
- Responds to all media types and even to requests that don't include an ``Accept`` header.
- Sets the ``Content-Type`` header to ``application/octet-stream`` by default, but this can be overridden inside the function with :ref:`guc_resp_hdrs`.
- This overrides all other handlers (:ref:`builtin <builtin_media>` or custom), so it's better to do it for an isolated function or view.
Let's define an any handler for a view that will always respond with ``XML`` output. It will accept ``text/xml``, ``application/xml``, ``*/*`` and reject other media types.
.. code-block:: postgres
create domain "*/*" as bytea;
-- we'll use an .xml suffix for the view to be clear its output is always XML
create view "lines.xml" as
select * from lines;
-- transition function
create or replace function lines_xml_trans (state "*/*", next "lines.xml")
returns "*/*" as $$
select state || xmlelement(name line, xmlattributes(next.id as id, next.name as name), next.geom)::text::bytea || E'\n' ;
$$ language sql;
-- final function
create or replace function lines_xml_final (data "*/*")
returns "*/*" as $$
declare
-- get the Accept header
req_accept text := current_setting('request.headers', true)::json->>'accept';
begin
-- when we need to override the default Content-Type (application/octet-stream) set by PostgREST
if req_accept = '*/*' then
perform set_config('response.headers', json_build_array(json_build_object('Content-Type', 'text/xml'))::text, true);
elsif req_accept IN ('application/xml', 'text/xml') then
perform set_config('response.headers', json_build_array(json_build_object('Content-Type', req_accept))::text, true);
else
-- we'll reject other non XML media types, we need to reject manually since */* will command PostgREST to accept all media types
raise sqlstate 'PT415' using message = 'Unsupported Media Type';
end if;
return data;
end; $$ language plpgsql;
-- new aggregate
create or replace aggregate lines_xml_agg ("lines.xml") (
stype = "*/*"
, sfunc = lines_xml_trans
, finalfunc = lines_xml_final
);
Test it on SQL:
.. code-block:: psql
select (encode(lines_xml_agg(x), 'escape'))::xml from "lines.xml" x;
encode
------------------------------------------------------------------------------------------------------------------------------
<line id="1" name="line-1">0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440</line>+
<line id="2" name="line-2">0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840</line>+
Now we can omit the ``Accept`` header and it will respond with XML.
.. code-block:: bash
curl 'localhost:3000/lines.xml' -i
HTTP/1.1 200 OK
Content-Type: text/xml
<line id="1" name="line-1">0102000020E610000002000000000000000000F03F000000000000F03F00000000000014400000000000001440</line>
<line id="2" name="line-2">0102000020E6100000020000000000000000000040000000000000004000000000000018400000000000001840</line>
And it will accept only XML media types.
.. code-block:: bash
curl 'localhost:3000/lines.xml' -i \
-H "Accept: text/xml"
HTTP/1.1 200 OK
Content-Type: text/xml
.. code-block:: bash
curl 'localhost:3000/lines.xml' -i \
-H "Accept: application/xml"
HTTP/1.1 200 OK
Content-Type: text/xml
.. code-block:: bash
curl 'localhost:3000/lines.xml' -i \
-H "Accept: unknown/media"
HTTP/1.1 415 Unsupported Media Type
+188
View File
@@ -0,0 +1,188 @@
Pagination and Count
####################
Pagination controls the number of rows returned for an :doc:`API resource <../api>` response. Combined with the count, you can traverse all the rows of a response.
.. _limits:
Limits and Pagination
---------------------
PostgREST uses HTTP range headers to describe the size of results. Every response contains the current range and, if requested, the total number of results:
.. code-block:: http
HTTP/1.1 200 OK
Range-Unit: items
Content-Range: 0-14/*
Here items zero through fourteen are returned. This information is available in every response and can help you render pagination controls on the client. This is an RFC7233-compliant solution that keeps the response JSON cleaner.
Query Parameters
~~~~~~~~~~~~~~~~
One way to request limits and offsets is by using query parameters. For example:
.. 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.
Range Header
~~~~~~~~~~~~
You can use headers to 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-`.
.. _prefer_count:
Counting
--------
In order to obtain the total size of the table (such as when rendering the last page link in a pagination control), you can specify a ``Prefer: count=<value>`` header. The values can be ``exact``, ``planned`` and ``estimated``.
This also works on views and :ref:`table_functions`.
.. _exact_count:
Exact Count
~~~~~~~~~~~
To get the exact count, use ``Prefer: count=exact``.
.. 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 <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 <https://www.postgresql.org/docs/current/sql-analyze.html>`_ for more details.
.. _estimated_count:
Estimated Count
~~~~~~~~~~~~~~~
When you are interested in the count, the relative error is important. If you have a :ref:`planned count <planned_count>` of 1000000 and the exact count is
1001000, the error is small enough to be ignored. But with a planned count of 7, an exact count of 28 would be a huge misprediction.
In general, when having smaller row-counts, the estimated count should be as close to the exact count as possible.
To help with these cases, PostgREST can get the exact count up until a threshold and get the planned count when
that threshold is surpassed. To use this behavior, you can specify the ``Prefer: count=estimated`` header. The **threshold** is
defined by :ref:`db-max-rows`.
Here's an example. Suppose we set ``db-max-rows=1000`` and ``smalltable`` has 321 rows, then we'll get the exact count:
.. 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
+225
View File
@@ -0,0 +1,225 @@
.. _preferences:
Prefer Header
#############
PostgREST honors the Prefer HTTP header specified on `RFC 7240 <https://www.rfc-editor.org/rfc/rfc7240.html>`_. It allows clients to specify required and optional behaviors for their requests.
The following preferences are supported.
- ``Prefer: handling``. See :ref:`prefer_handling`.
- ``Prefer: timezone``. See :ref:`prefer_timezone`.
- ``Prefer: return``. See :ref:`prefer_return`.
- ``Prefer: count``. See :ref:`prefer_count`.
- ``Prefer: resolution``. See :ref:`prefer_resolution`.
- ``Prefer: missing``. See :ref:`bulk_insert_default`.
.. _prefer_handling:
Strict or Lenient Handling
==========================
The server ignores unrecognized or unfulfillable preferences by default. You can control this behavior with the ``handling`` preference. It can take two values: ``lenient`` (the default) or ``strict``.
``handling=strict`` will throw an error if you specify invalid preferences. For instance:
.. tabs::
.. code-tab:: http
GET /projects HTTP/1.1
Prefer: handling=strict, foo, bar
.. code-tab:: bash Curl
curl -i "http://localhost:3000/projects" \
-H "Prefer: handling=strict, foo, bar"
.. code-block:: http
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
.. code-block:: json
{
"code": "PGRST122",
"message": "Invalid preferences given with handling=strict",
"details": "Invalid preferences: foo, bar",
"hint": null
}
``handling=lenient`` ignores invalid preferences.
.. tabs::
.. code-tab:: http
GET /projects HTTP/1.1
Prefer: handling=lenient, foo, bar
.. code-tab:: bash Curl
curl -i "http://localhost:3000/projects" \
-H "Prefer: handling=lenient, foo, bar"
.. code-block:: http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
.. _prefer_timezone:
Timezone
========
The ``timezone`` preference allows you to change the `PostgreSQL timezone <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-TIMEZONE>`_. It accepts all timezones in `pg_timezone_names <https://www.postgresql.org/docs/current/view-pg-timezone-names.html>`_.
.. tabs::
.. code-tab:: http
GET /timestamps HTTP/1.1
Prefer: timezone=America/Los_Angeles
.. code-tab:: bash Curl
curl -i "http://localhost:3000/timestamps" \
-H "Prefer: timezone=America/Los_Angeles"
.. code-block:: http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Preference-Applied: timezone=America/Los_Angeles
.. code-block:: json
[
{"t":"2023-10-18T05:37:59.611-07:00"},
{"t":"2023-10-18T07:37:59.611-07:00"},
{"t":"2023-10-18T09:37:59.611-07:00"}
]
For an invalid timezone, PostgREST returns values with the default timezone (configured on ``postgresql.conf`` or as a setting on the :ref:`authenticator <roles>`).
.. tabs::
.. code-tab:: http
GET /timestamps HTTP/1.1
Prefer: timezone=Jupiter/Red_Spot
.. code-tab:: bash Curl
curl -i "http://localhost:3000/timestamps" \
-H "Prefer: timezone=Jupiter/Red_Spot"
.. code-block:: http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
.. code-block:: json
[
{"t":"2023-10-18T12:37:59.611+00:00"},
{"t":"2023-10-18T14:37:59.611+00:00"},
{"t":"2023-10-18T16:37:59.611+00:00"}
]
Note that there's no ``Preference-Applied`` in the response.
However, with ``handling=strict``, an invalid timezone preference will throw an :ref:`error <pgrst122>`.
.. tabs::
.. code-tab:: http
GET /timestamps HTTP/1.1
Prefer: handling=strict, timezone=Jupiter/Red_Spot
.. code-tab:: bash Curl
curl -i "http://localhost:3000/timestamps" \
-H "Prefer: handling=strict, timezone=Jupiter/Red_Spot"
.. code-block:: http
HTTP/1.1 400 Bad Request
.. _prefer_return:
Return Representation
=====================
The ``return`` preference can be used to obtain information about affected resource when it's :ref:`inserted <insert>`, :ref:`updated <update>` or :ref:`deleted <delete>`.
This helps avoid a subsequent GET request.
Minimal
-------
With ``Prefer: return=minimal``, no response body will be returned. This is the default mode for all write requests.
Headers Only
------------
If the table has a primary key, the response can contain a :code:`Location` header describing where to find the new object by including the header :code:`Prefer: return=headers-only` in the request. Make sure that the table is not write-only, otherwise constructing the :code:`Location` header will cause a permissions error.
.. tabs::
.. code-tab:: http
POST /projects HTTP/1.1
Prefer: return=headers-only
{"id":33, "name": "x"}
.. code-tab:: bash Curl
curl -i "http://localhost:3000/projects" -X POST \
-H "Content-Type: application/json" \
-H "Prefer: return=headers-only" \
-d '{"id":33, "name": "x"}'
.. code-block:: http
HTTP/1.1 201 Created
Location: /projects?id=eq.34
Preference-Applied: return=headers-only
Full
----
On the other end of the spectrum you can get the full created object back in the response to your request by including the header :code:`Prefer: return=representation`. That way you won't have to make another HTTP call to discover properties that may have been filled in on the server side. You can also apply the standard :ref:`v_filter` to these results.
.. 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 -i "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
Preference-Applied: return=representation
[
{
"id": 33,
"name": "x"
}
]
+38 -73
View File
@@ -23,15 +23,44 @@ Use the Accept request header to specify the acceptable format (or formats) for
curl "http://localhost:3000/people" \
-H "Accept: application/json"
For tables and views the current possibilities are:
.. _builtin_media:
* ``*/*``
* ``text/csv``
* ``application/json``
* ``application/openapi+json``
* ``application/geo+json``
Builtin Media Type Handlers
===========================
The server will default to JSON for API endpoints and OpenAPI on the root.
Builtin handlers are offered for common standard media types.
* ``text/csv`` and ``application/json``, for all API endpoints. See :ref:`tables_views` and :ref:`s_procs`.
* ``application/openapi+json``, for the root endpoint. See :ref:`open-api`.
* ``application/geo+json``, see :ref:`ww_postgis`.
* ``*/*``, resolves to ``application/json`` for API endpoints and to ``application/openapi+json`` for the root endpoint.
The following vendor media types handlers are also supported.
* ``application/vnd.pgrst.plan``, see :ref:`explain_plan`.
* ``application/vnd.pgrst.object`` and ``application/vnd.pgrst.array``, see :ref:`singular_plural` and :ref:`stripped_nulls`.
Any unrecognized media type will throw an error.
.. tabs::
.. code-tab:: http
GET /people HTTP/1.1
Accept: unknown/unknown
.. code-tab:: bash Curl
curl "http://localhost:3000/people" \
-H "Accept: unknown/unknown"
.. code-block:: http
HTTP/1.1 415 Unsupported Media Type
{"code":"PGRST107","details":null,"hint":null,"message":"None of these media types are available: unknown/unknown"}
To extend the accepted media types, you can use :ref:`custom_media`.
.. _singular_plural:
@@ -85,6 +114,8 @@ 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:
Stripped Nulls
--------------
@@ -122,72 +153,6 @@ This returns
{ "id": 13, "name": "Y"}
]
.. _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.
.. _req_body:
Request Body
+4 -2
View File
@@ -311,7 +311,9 @@ Repeating also works in POST requests with ``Content-Type: application/x-www-for
-X POST -H "Content-Type: application/x-www-form-urlencoded" \
-d 'v=1&v=2&v=3&v=4'
Table-Valued functions
.. _table_functions:
Table-Valued Functions
----------------------
A function that returns a table type can be filtered using the same filters as :ref:`tables and views <tables_views>`. They can also use :ref:`Resource Embedding <s_proc_embed>`.
@@ -418,7 +420,7 @@ 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`.
To manually choose a return format such as binary, see :ref:`custom_media`.
.. _untyped_functions:
+15 -235
View File
@@ -91,7 +91,7 @@ cs :code:`@>` contains e.g. :code:`?tags=cs.{example,
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}`
: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)`
@@ -196,7 +196,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 <https://www.postgresql.org/docs/current/functions-matching.html>`__.
To ensure best performance on larger data sets, an `appropriate index <https://www.postgresql.org/docs/current/pgtrgm.html#id-1.11.7.44.8>`__ 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 <https://www.postgresql.org/docs/current/pgtrgm.html#PGTRGM-INDEX>`__ should be used and even then, it depends on the pattern value and actual data statistics whether an existing index will be used by the query planner or not.
.. _fts:
@@ -273,6 +273,8 @@ When certain columns are wide (such as those holding binary data), it is more ef
The default is ``*``, meaning all columns. This value will become more important below in :ref:`resource_embedding`.
.. _renaming_columns:
Renaming Columns
~~~~~~~~~~~~~~~~
@@ -518,173 +520,6 @@ You can also sort on fields of :ref:`composite_array_columns` or :ref:`json_colu
curl "http://localhost:3000/countries?order=location->>lat"
.. _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 <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 <https://www.postgresql.org/docs/current/sql-analyze.html>`_ for more details.
.. _estimated_count:
Estimated Count
---------------
When you are interested in the count, the relative error is important. If you have a :ref:`planned count <planned_count>` of 1000000 and the exact count is
1001000, the error is small enough to be ignored. But with a planned count of 7, an exact count of 28 would be a huge misprediction.
In general, when having smaller row-counts, the estimated count should be as close to the exact count as possible.
To help with these cases, PostgREST can get the exact count up until a threshold and get the planned count when
that threshold is surpassed. To use this behavior, you can specify the ``Prefer: count=estimated`` header. The **threshold** is
defined by :ref:`db-max-rows`.
Here's an example. Suppose we set ``db-max-rows=1000`` and ``smalltable`` has 321 rows, then we'll get the exact count:
.. 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
.. _head_req:
HEAD
@@ -720,70 +555,7 @@ To create a row in a database table post a JSON object whose keys are the names
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
Preference-Applied: return=headers-only
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
Preference-Applied: return=representation
[
{
"id": 33,
"name": "x"
}
]
No response body will be returned by default but you can use :ref:`prefer_return` to get the affected resource.
x-www-form-urlencoded
---------------------
@@ -1014,12 +786,14 @@ To update a row or rows in a table, use the PATCH verb. Use :ref:`h_filter` to s
-X PATCH -H "Content-Type: application/json" \
-d '{ "category": "child" }'
Updates also support :code:`Prefer: return=representation` plus :ref:`v_filter`.
Updates also support :ref:`prefer_return` plus :ref:`v_filter`.
.. warning::
Beware of accidentally updating every row in a table. To learn to prevent that see :ref:`block_fulltable`.
.. _prefer_resolution:
.. _upsert:
Upsert
@@ -1131,7 +905,7 @@ To delete rows in a table, use the DELETE verb plus :ref:`h_filter`. For instanc
curl "http://localhost:3000/user?active=is.false" -X DELETE
Deletions also support :code:`Prefer: return=representation` plus :ref:`v_filter`.
Deletions also support :ref:`prefer_return` plus :ref:`v_filter`.
.. tabs::
@@ -1200,6 +974,12 @@ Using ``offset`` to target a different subset of rows is also possible.
const redirects = {
// Tables and Views
'#computed-virtual-columns': 'computed_fields.html#computed-fields',
'#limits-and-pagination': 'pagination_count.html#limits-and-pagination',
'#exact-count': 'pagination_count.html#exact-count',
'#planned-count': 'pagination_count.html#planned-count',
'#estimated-count': 'pagination_count.html#estimated-count',
'#prefer-return-headers-only': 'preferences.html#headers-only',
'#prefer-return-representation': 'preferences.html#full',
};
let willRedirectTo = redirects[hash];
+2
View File
@@ -2,6 +2,8 @@
This page is a work in progress.
.. _url_grammar:
URL Grammar
===========
+17 -24
View File
@@ -21,6 +21,8 @@ The authenticator role is used for connecting to the database and should be conf
CREATE ROLE authenticator LOGIN NOINHERIT NOCREATEDB NOCREATEROLE NOSUPERUSER;
CREATE ROLE anonymous NOLOGIN;
CREATE ROLE webuser NOLOGIN;
.. note::
@@ -37,23 +39,9 @@ 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 <transactions>` 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 <https://www.enterprisedb.com/blog/understanding-postgres-parameter-context>`_.
The impersonated roles will have their settings applied. See :ref:`impersonated_settings`.
.. _jwt_impersonation:
@@ -112,6 +100,19 @@ 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_caching:
JWT Caching
-----------
PostgREST validates ``JWTs`` on every request. We can cache ``JWTs`` to avoid this performance overhead.
To enable JWT caching, the config :code:`jwt-cache-max-lifetime` is to be set. It is the maximum number of seconds for which the cache stores the JWT validation results. The cache uses the :code:`exp` claim to set the cache entry lifetime. If the JWT does not have an :code:`exp` claim, it uses the config value. See :ref:`jwt-cache-max-lifetime` for more details.
.. note::
You can use the :ref:`server-timing_header` to see the effect of JWT caching.
Symmetric Keys
~~~~~~~~~~~~~~
@@ -165,15 +166,7 @@ You can specify the literal value as we saw earlier, or reference a filename to
JWT Claims Validation
~~~~~~~~~~~~~~~~~~~~~
PostgREST honors the following `JWT claims <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4>`_:
- ``exp`` Expiration Time
- ``iat`` Issued At
- ``nbf`` Not Before
- ``aud`` Audience, see :ref:`jwt-aud`
.. note::
PostgREST allows for a 30-second clock skew when validating the ``exp`` and ``iat`` claims. In other words, it gives an extra 30 seconds before the token is rejected if there is a slight discrepancy in the timestamps.
PostgREST honors the :code:`exp` claim for token expiration, rejecting expired tokens.
JWT Security
~~~~~~~~~~~~
+81 -36
View File
@@ -178,6 +178,28 @@ 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-aggregates-enabled:
db-aggregates-enabled
---------------------
=============== =======================
**Type** Boolean
**Default** False
**Reloadable** Y
**Environment** PGRST_DB_AGGREGATES_ENABLED
**In-Database** pgrst.db_aggregates_enabled
=============== =======================
When this is set to :code:`true`, the use of :ref:`aggregate_functions` is allowed.
It is recommended that this be set to ``false`` unless proper safeguards are in place to prevent potential performance problems from arising. For example, it is possible that a user may request the ``max()`` of an unindexed column in a table with millions of rows. At best, this would result in a slow query, and at worst, it could be abused to prevent other users from accessing your API (i.e. a form of denial-of-service attack.)
Proper safeguards could include:
- Use of a statement timeout. See :ref:`impersonated_settings`.
- Use of the `pg_plan_filter extension <https://github.com/pgexperts/pg_plan_filter>`_ to block excessively expensive queries.
.. _db-anon-role:
db-anon-role
@@ -370,6 +392,23 @@ db-pool-max-lifetime
Specifies the maximum time in seconds of an existing connection in the pool.
.. _db-pool-automatic-recovery:
db-pool-automatic-recovery
--------------------------
=============== =================================
**Type** Boolean
**Default** True
**Reloadable** Y
**Environment** PGRST_DB_POOL_AUTOMATIC_RECOVERY
**In-Database** `n/a`
=============== =================================
Enables or disables connection retrying.
When disabled, PostgREST would terminate immediately after connection loss instead of retrying indefinitely. See :ref:`this section <automatic_recovery>` for more information.
.. _db-pre-request:
db-pre-request
@@ -515,24 +554,6 @@ 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
------------------
=============== =================================
**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 :ref:`legacy names <guc_legacy_names>` (string with dashes, invalid starting from PostgreSQL v14) with text values instead of the :ref:`new names <guc_req_headers_cookies_claims>` (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
@@ -611,6 +632,21 @@ jwt-secret-is-base64
When this is set to :code:`true`, the value derived from :code:`jwt-secret` will be treated as a base64 encoded secret.
.. _jwt-cache-max-lifetime:
jwt-cache-max-lifetime
----------------------
=============== =================================
**Type** Int
**Default** 0
**Reloadable** Y
**Environment** PGRST_JWT_CACHE_MAX_LIFETIME
**In-Database** pgrst.jwt_cache_max_lifetime
=============== =================================
Maximum number of seconds of lifetime for cached entries. The default :code:`0` disables caching. See :ref:`jwt_caching`.
.. _log-level:
log-level
@@ -718,29 +754,22 @@ openapi-server-proxy-uri
]
}
.. _raw-media-types:
.. _server_cors_allowed_origins:
raw-media-types
---------------
server-cors-allowed-origins
---------------------------
=============== =================================
=============== ===================================
**Type** String
**Default** `n/a`
**Reloadable** Y
**Environment** PGRST_RAW_MEDIA_TYPES
**In-Database** pgrst.raw_media_types
=============== =================================
**Reloadable** N
**Environment** PGRST_SERVER_CORS_ALLOWED_ORIGINS
**In-Database** `pgrst.server_cors_allowed_origins`
=============== ===================================
This serves to extend the `Media Types <https://en.wikipedia.org/wiki/Media_type>`_ that PostgREST currently accepts through an ``Accept`` header.
Specifies allowed CORS origins in this config. See :ref:`cors`.
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: font/woff2``, respectively.
.. code:: bash
raw-media-types="image/png, font/woff2"
When this is not set or set to :code:`""`, PostgREST **accepts** CORS requests from any domain.
.. _server-host:
@@ -776,7 +805,7 @@ server-port
**In-Database** `n/a`
=============== =================================
The TCP port to bind the web server.
The TCP port to bind the web server. Use ``0`` to automatically assign a port.
.. _server-trace-header:
@@ -793,6 +822,22 @@ server-trace-header
The header name used to trace HTTP requests. See :ref:`trace_header`.
.. _server-timing-enabled:
server-timing-enabled
---------------------
=============== =================================
**Type** Boolean
**Default** False
**Reloadable** Y
**Environment** PGRST_SERVER_TIMING_ENABLED
**In-Database** pgrst.server_timing_enabled
=============== =================================
Enables the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>`_ header.
See :ref:`server-timing_header`.
.. _server-unix-socket:
server-unix-socket
+2 -1
View File
@@ -10,7 +10,7 @@ Minimizing connections is paramount to performance. Each PostgreSQL connection c
Connection String
-----------------
For connecting to the database, the pool requires a connection string. You can can configure it using :ref:`db-uri`.
For connecting to the database, the pool requires a connection string. You can configure it using :ref:`db-uri`.
.. _pool_growth_limit:
.. _dyn_conn_pool:
@@ -78,6 +78,7 @@ 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.
- To ensure a valid state, the server reloads the :ref:`schema_cache` and :ref:`configuration` when recovering.
- To notify the client of the next retry, the server sends a ``503 Service Unavailable`` status with the ``Retry-After: x`` header. Where ``x`` is the number of seconds programmed for the next retry.
- Automatic recovery can be disabled by setting :ref:`db-pool-automatic-recovery` to ``false``.
.. _external_connection_poolers:
+119 -69
View File
@@ -8,24 +8,25 @@ PostgREST error messages follow the PostgreSQL error structure. It includes ``ME
Errors from PostgreSQL
======================
PostgREST will forward errors coming from PostgreSQL. For instance, when querying a nonexistent table:
PostgREST will forward errors coming from PostgreSQL. For instance, on a failed constraint:
.. code-block:: http
GET /nonexistent_table?id=eq.1 HTTP/1.1
POST /projects HTTP/1.1
.. code-block:: http
HTTP/1.1 404 Not Found
HTTP/1.1 400 Bad Request
Content-Type: application/json; charset=utf-8
.. code-block:: json
{
"hint": null,
"details": null,
"code": "42P01",
"message": "relation \"api.nonexistent_table\" does not exist"
"code": "23502",
"details": "Failing row contains (null, foo, null).",
"hint": null,
"message": "null value in column \"id\" of relation \"projects\" violates not-null constraint"
}
.. _status_codes:
@@ -90,72 +91,18 @@ PostgREST translates `PostgreSQL error codes <https://www.postgresql.org/docs/cu
+--------------------------+-------------------------+---------------------------------+
| 42P01 | 404 | undefined table |
+--------------------------+-------------------------+---------------------------------+
| 42P17 | 500 | infinite recursion |
+--------------------------+-------------------------+---------------------------------+
| 42501 | | if authenticated 403, | insufficient privileges |
| | | else 401 | |
+--------------------------+-------------------------+---------------------------------+
| other | 400 | |
+--------------------------+-------------------------+---------------------------------+
.. _raise_error:
RAISE errors with HTTP Status Codes
-----------------------------------
You can return custom HTTP status codes by raising SQL exceptions inside :ref:`functions <s_procs>`. For instance, here's a saucy function that always responds with an error:
.. code-block:: postgresql
CREATE OR REPLACE FUNCTION just_fail() RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION 'I refuse!'
USING DETAIL = 'Pretty simple',
HINT = 'There is nothing you can do.';
END
$$;
Calling the function returns HTTP 400 with the body
.. code-block:: json
{
"message":"I refuse!",
"details":"Pretty simple",
"hint":"There is nothing you can do.",
"code":"P0001"
}
One way to customize the HTTP status code is by raising particular exceptions according to the PostgREST :ref:`error to status code mapping <status_codes>`. For example, :code:`RAISE insufficient_privilege` will respond with HTTP 401/403 as appropriate.
For even greater control of the HTTP status code, raise an exception of the ``PTxyz`` type. For instance to respond with HTTP 402, raise ``PT402``:
.. code-block:: sql
RAISE sqlstate 'PT402' using
message = 'Payment Required',
detail = 'Quota exceeded',
hint = 'Upgrade your plan';
Returns:
.. code-block:: http
HTTP/1.1 402 Payment Required
Content-Type: application/json; charset=utf-8
{
"message": "Payment Required",
"details": "Quota exceeded",
"hint": "Upgrade your plan",
"code": "PT402"
}
Errors from PostgREST
=====================
Errors that come from PostgREST itself maintain the same structure. But differ in the ``PGRST`` prefix in the ``code`` field. For instance, when querying a function that does not exist in the :doc:`schema cache <schema_cache>`:
Errors that come from PostgREST itself maintain the same structure but differ in the ``PGRST`` prefix in the ``code`` field. For instance, when querying a function that does not exist in the :doc:`schema cache <schema_cache>`:
.. code-block:: http
@@ -181,7 +128,7 @@ Errors that come from PostgREST itself maintain the same structure. But differ i
PostgREST Error Codes
---------------------
PostgREST error codes have the form ``PGRSTgxx``
PostgREST error codes have the form ``PGRSTgxx``.
- ``PGRST`` is the prefix that differentiates the error from a PostgreSQL error.
- ``g`` is the error group
@@ -272,10 +219,6 @@ Related to the HTTP request elements.
| | | 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 <upsert_put>`, when |
| | | :ref:`limits and offsets <limits>` are used. |
| PGRST114 | | |
@@ -304,6 +247,14 @@ Related to the HTTP request elements.
| | | ``is.null`` or ``not.is.null`` :ref:`operators <operators>`.|
| PGRST120 | | |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst121: | 400 | PostgREST can't parse the JSON objects in RAISE |
| | | ``PGRST`` error. See :ref:`raise headers <raise_headers>`. |
| PGRST121 | | |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst122: | 400 | Invalid preferences found in ``Prefer`` header with |
| | | ``Prefer: handling=strict``. See :ref:`prefer_handling`. |
| PGRST122 | | |
+---------------+-------------+-------------------------------------------------------------+
.. _pgrst2**:
@@ -377,3 +328,102 @@ Internal errors. If you encounter any of these, you may have stumbled on a Postg
| | | to the database. |
| PGRSTX00 | | |
+---------------+-------------+-------------------------------------------------------------+
Custom Errors
=============
You can customize the errors by using the `RAISE statement <https://www.postgresql.org/docs/current/plpgsql-errors-and-messages.html#PLPGSQL-STATEMENTS-RAISE>`_ on functions.
.. _raise_error:
RAISE errors with HTTP Status Codes
-----------------------------------
Custom status codes can be done by raising SQL exceptions inside :ref:`functions <s_procs>`. For instance, here's a saucy function that always responds with an error:
.. code-block:: postgresql
CREATE OR REPLACE FUNCTION just_fail() RETURNS void
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION 'I refuse!'
USING DETAIL = 'Pretty simple',
HINT = 'There is nothing you can do.';
END
$$;
Calling the function returns HTTP 400 with the body
.. code-block:: json
{
"message":"I refuse!",
"details":"Pretty simple",
"hint":"There is nothing you can do.",
"code":"P0001"
}
One way to customize the HTTP status code is by raising particular exceptions according to the PostgREST :ref:`error to status code mapping <status_codes>`. For example, :code:`RAISE insufficient_privilege` will respond with HTTP 401/403 as appropriate.
For even greater control of the HTTP status code, raise an exception of the ``PTxyz`` type. For instance to respond with HTTP 402, raise ``PT402``:
.. code-block:: sql
RAISE sqlstate 'PT402' using
message = 'Payment Required',
detail = 'Quota exceeded',
hint = 'Upgrade your plan';
Returns:
.. code-block:: http
HTTP/1.1 402 Payment Required
Content-Type: application/json; charset=utf-8
{
"message": "Payment Required",
"details": "Quota exceeded",
"hint": "Upgrade your plan",
"code": "PT402"
}
.. _raise_headers:
Add HTTP Headers with RAISE
---------------------------
For full control over headers and status you can raise a ``PGRST`` SQLSTATE error. You can achieve this by adding the ``code``, ``message``, ``detail`` and ``hint`` in the postgresql error message field as a JSON object. Here, the ``details`` and ``hint`` are optional. Similarly, the ``status`` and ``headers`` must be added to the SQL error detail field as a JSON object. For instance:
.. code-block:: sql
RAISE sqlstate 'PGRST' USING
message = '{"code":"123","message":"Payment Required","details":"Quota exceeded","hint":"Upgrade your plan"}',
detail = '{"status":402,"headers":{"X-Powered-By":"Nerd Rage"}}';
Returns:
.. code-block:: http
HTTP/1.1 402 Payment Required
Content-Type: application/json; charset=utf-8
X-Powered-By: Nerd Rage
{
"message": "Payment Required",
"details": "Quota exceeded",
"hint": "Upgrade your plan",
"code": "123"
}
For non standard HTTP status, you can optionally add ``status_text`` to describe the status code. For status code ``419`` the detail field may look like this:
.. code-block:: sql
detail = '{"status":419,"status_text":"Page Expired","headers":{"X-Powered-By":"Nerd Rage"}}';
If PostgREST can't parse the JSON objects ``message`` and ``detail``, it will throw a ``PGRST121`` error. See :ref:`Errors from PostgREST<pgrst1**>`.
+1 -1
View File
@@ -31,7 +31,7 @@ Some PostgREST features need metadata from the database schema. Getting this met
Stale Schema Cache
------------------
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.
One operational problem that comes with a cache is that it can go stale. This can happen for PostgREST when you make changes to the metadata before mentioned. Requests that depend on the metadata will fail.
You can solve this by reloading the cache manually or automatically.
+105 -51
View File
@@ -7,16 +7,48 @@ After :ref:`user_impersonation`, every request to an :doc:`API resource <api>` r
.. code-block:: postgresql
BEGIN; -- <Access Mode> <Isolation Level>
START TRANSACTION; -- <Access Mode> <Isolation Level>
-- <Transaction-scoped settings>
-- <Main Query>;
END;
-- <Main Query>
END; -- <Transaction End>
.. _access_mode:
Access Mode
-----------
The access mode determines whether the transaction can modify the database or not. There are 2 possible values: READ ONLY and READ WRITE.
Modifying the database inside READ ONLY transactions is not possible. PostgREST uses this fact to enforce HTTP semantics in GET and HEAD requests. Consider the following:
.. code-block:: postgresql
CREATE SEQUENCE callcounter_count START 1;
CREATE VIEW callcounter AS
SELECT nextval('callcounter_count');
Since the ``callcounter`` view modifies the sequence, calling it with GET or HEAD will result in an error:
.. tabs::
.. code-tab:: http
GET /callcounter HTTP/1.1
.. code-tab:: bash Curl
curl "http://localhost:3000/callcounter"
.. code-block:: http
HTTP/1.1 405 Method Not Allowed
{"code":"25006","details":null,"hint":null,"message":"cannot execute nextval() in a read-only transaction"}
Access Mode on Tables and Views
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The access mode on :ref:`tables_views` is determined by the HTTP method.
.. list-table::
@@ -29,6 +61,9 @@ The access mode on :ref:`tables_views` is determined by the HTTP method.
* - POST, PATCH, PUT, DELETE
- READ WRITE
Access Mode on Functions
~~~~~~~~~~~~~~~~~~~~~~~~
:ref:`s_procs` additionally depend on the function `volatility <https://www.postgresql.org/docs/current/xfunc-volatility.html>`_.
.. list-table::
@@ -51,13 +86,10 @@ The access mode on :ref:`tables_views` is determined by the HTTP method.
- 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.
- The volatility marker is a promise about the behavior of the function. PostgreSQL will let you mark a function that modifies the database as ``IMMUTABLE`` or ``STABLE`` without failure. But, because of the READ ONLY transaction the function will fail under PostgREST.
- The :ref:`options_requests` method doesn't start a transaction, so it's not relevant here.
.. _isolation_lvl:
@@ -66,12 +98,12 @@ Isolation Level
Every transaction uses the PostgreSQL default isolation level: READ COMMITTED. Unless you modify `default_transaction_isolation <https://www.postgresql.org/docs/15/runtime-config-client.html#GUC-DEFAULT-TRANSACTION-ISOLATION>`_ for an impersonated role or function.
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';
Every ``webuser`` gets its queries executed with ``default_transaction_isolation`` set to REPEATABLE READ.
Or to change the isolation level per function call.
.. code-block:: postgresql
@@ -113,10 +145,6 @@ 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
@@ -131,45 +159,21 @@ PostgREST stores the headers, cookies and headers as JSON. To get them:
-- value of the email claim in a jwt
SELECT current_setting('request.jwt.claims', true)::json->>'email';
.. note::
.. important::
The ``role`` in ``request.jwt.claims`` defaults to the value of :ref:`db-anon-role`.
- The headers names are lowercased. e.g. If the request sends ``User-Agent: x`` this will be obtainable as ``current_setting('request.headers', true)::json->>'user-agent'``.
- The ``role`` in ``request.jwt.claims`` defaults to the value of :ref:`db-anon-role`.
- Settings don't become NULL after the transaction is committed, instead they're set to a an empty string ``''``.
.. _guc_legacy_names:
+ This is considered expected behavior by PostgreSQL. For more details, see `this discussion <https://www.postgresql.org/message-id/flat/CAB_pDVVa84w7hXhzvyuMTb8f5kKV3bee_p9QTZZ58Rg7zYM7sw%40mail.gmail.com>`_.
+ To avoid this inconsistency, you can create a wrapper function like:
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 (see below).
You can opt in to use the JSON GUCs mentioned above by setting the ``db-use-legacy-gucs`` to false.
.. raw:: html
<p>
<details>
<summary>Old GUCs</summary>
.. code-block:: postgresql
-- To read the value of the User-Agent request header:
SELECT current_setting('request.header.user-agent', 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`.
.. raw:: html
</details>
</p>
.. code-block:: postgresql
CREATE FUNCTION my_current_setting(text) RETURNS text
LANGUAGE SQL AS $$
SELECT nullif(current_setting($1, true), '');
$$;
.. _guc_req_path_method:
@@ -223,7 +227,7 @@ Notice that the ``response.headers`` should be set to an *array* of single-key o
.. 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``.
PostgREST provided headers such as ``Content-Type``, ``Location``, etc. can be overriden this way. Note that irrespective of overridden ``Content-Type`` response header, the content will still be converted to JSON, unless you use :ref:`custom_media`.
.. _guc_resp_status:
@@ -263,12 +267,62 @@ 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).
.. _impersonated_settings:
Impersonated Role Settings
~~~~~~~~~~~~~~~~~~~~~~~~~~
PostgreSQL applies the connection role (:ref:`authenticator <roles>`) settings. Additionally, PostgREST applies the :ref:`impersonated roles <user_impersonation>` settings as transaction-scoped settings.
This allows finer-grained control over actions made by a role.
For example, consider `statement_timeout <https://www.postgresql.org/docs/current/runtime-config-client.html#GUC-STATEMENT-TIMEOUT>`__. It allows you to abort any statement that takes more than a specified time. It is disabled by default.
.. code-block:: postgresql
ALTER ROLE authenticator SET statement_timeout TO '10s';
ALTER ROLE anonymous SET statement_timeout TO '1s';
With the above settings, all users get a global statement timeout of 10 seconds and :ref:`anonymous <roles>` users get a timeout of 1 second.
Settings with privileged context
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Settings that have a context which requires privileges won't be applied by default. This is so we don't cause permission errors.
For more details see `Understanding Postgres Parameter Context <https://www.enterprisedb.com/blog/understanding-postgres-parameter-context>`_.
However, starting from PostgreSQL 15, you can grant privileges for these settings with:
.. code-block:: postgresql
GRANT SET ON PARAMETER <setting> TO <authenticator>;
Function Settings
~~~~~~~~~~~~~~~~~
In addition to :ref:`impersonated_settings`, PostgREST will also apply function settings as transaction-scoped settings. This allows functions settings to override
the impersonated and connection role settings.
.. code-block:: postgresql
CREATE OR REPLACE FUNCTION myfunc()
RETURNS void as $$
SELECT pg_sleep(3); -- simulating some long-running process
$$
LANGUAGE SQL
SET statement_timeout TO '4s';
When calling the above function (see :ref:`s_procs`), the statement timeout will be 4 seconds.
.. note::
Currently, only ``statement_timeout`` is applied for functions.
.. _main_query:
Main query
----------
The main query is generated by requesting :ref:`tables_views` or :ref:`s_procs`. All generated queries use prepared statements(:ref:`db-prepared-statements`).
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
---------------
-130
View File
@@ -1,130 +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 <https://github.com/PostgREST/postgrest/releases/tag/v9.0.0>`_.
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 :ref:`old HTTP Context <guc_legacy_names>` to fail across all requests. Thankfully, `@robertsosinski <https://github.com/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 <https://www.postgresql.org/message-id/17045-6a4a9f0d1513f72b%40postgresql.org>`_.
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 <guc_req_headers_cookies_claims>`. 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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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 <embedding_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 <s_proc_single_unnamed>` 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 <operators>`.
* 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 :ref:`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 </ecosystem>`.
Bug fixes
---------
* Correct RPC return type handling for RETURNS TABLE with a single column (`#1930 <https://github.com/PostgREST/postgrest/issues/1930>`_).
* Schema Cache query failing with ``standard_conforming_strings = off`` (`#1992 <https://github.com/PostgREST/postgrest/issues/1992>`_).
* OpenAPI missing default values for String types (`#1871 <https://github.com/PostgREST/postgrest/issues/1871>`_).
Breaking changes
----------------
* Dropped support for PostgreSQL 9.5 as it already reached its end-of-life according to `PostgreSQL versioning policy <https://www.postgresql.org/support/versioning/>`_.
* Partitions of a `partitioned table <https://www.postgresql.org/docs/current/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE>`_ 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 <https://github.com/PostgREST/postgrest/releases/tag/v6.0.2>`_.
Thanks
------
Big thanks from the `PostgREST team <https://github.com/orgs/PostgREST/people>`_ 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 <https://www.igutech.at/>`_
* `Daniel Babiak <https://github.com/dbabiak>`_
* 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 <https://github.com/PostgREST/postgrest#user-content-supporting-development>`_.
-89
View File
@@ -1,89 +0,0 @@
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 <https://github.com/PostgREST/postgrest/releases/tag/v9.0.1>`_.
Bug Fixes
---------
* Keep working when ``EMFILE (Too many open files)`` is reached. (`#2042 <https://github.com/PostgREST/postgrest/issues/2042>`_)
* Disable parallel GC for better performance on higher core CPUs (`#2294 <https://github.com/PostgREST/postgrest/issues/2294>`_). Thanks to `NoRedInk for their blog post <https://blog.noredink.com/post/666654908557180928/tuning-haskell-rts-for-kubernetes-part-2>`_ that lead us to this fix.
* Fix using CPU while idle. (`#1076 <https://github.com/PostgREST/postgrest/issues/1076>`_)
* Fix reading database configuration properly when ``=`` is present in the value. (`#2120 <https://github.com/PostgREST/postgrest/issues/2120>`_)
* Fix ``is`` not working with upper or mixed case values like ``NULL``, ``TrUe``, ``FaLsE``. (`#2077 <https://github.com/PostgREST/postgrest/issues/2077>`_)
* Execute deferred constraint triggers when using ``Prefer: tx=rollback``. (`#2020 <https://github.com/PostgREST/postgrest/issues/2020>`_)
* Ignore ``Content-Type`` headers for ``GET`` requests when calling RPCs. (`#2147 <https://github.com/PostgREST/postgrest/issues/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 <https://github.com/PostgREST/postgrest/issues/1724>`_)
* Fix ``json`` and ``jsonb`` columns showing a type in OpenAPI spec. (`#2165 <https://github.com/PostgREST/postgrest/issues/2165>`_)
* Remove trigger functions from the schema cache and OpenAPI output, because they can't be called directly anyway. (`#2135 <https://github.com/PostgREST/postgrest/issues/2135>`_)
* Remove aggregates, procedures and window functions from the schema cache and OpenAPI output. (`#2101 <https://github.com/PostgREST/postgrest/issues/2101>`_)
* Fix schema cache loading when views with ``XMLTABLE`` and ``DEFAULT`` are present. (`#2024 <https://github.com/PostgREST/postgrest/issues/2024>`_)
* Fix ``--dump-schema`` running with a wrong PG version. (`#2153 <https://github.com/PostgREST/postgrest/issues/2153>`_)
* Fix misleading disambiguation error where the content of the ``relationship`` key looks like valid syntax. (`#2239 <https://github.com/PostgREST/postgrest/issues/2239>`_)
Thanks
------
Big thanks from the `PostgREST team <https://github.com/orgs/PostgREST/people>`_ 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 <https://www.igutech.at/>`_
* `Daniel Babiak <https://github.com/dbabiak>`_
* 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 <https://github.com/PostgREST/postgrest#user-content-supporting-development>`_.
-227
View File
@@ -1,227 +0,0 @@
10.0.0
======
Features
--------
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 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 <ww_postgis>` has an example to get you started.
Execution Plan
~~~~~~~~~~~~~~
The :ref:`execution plan <explain_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 <https://explain.depesz.com>`_ or `explain.dalibo.com <https://explain.dalibo.com>`_.
Resource Embedding
~~~~~~~~~~~~~~~~~~
- A :ref:`one-to-one relationship <one-to-one>` is now detected when a foreign key is unique.
- 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.
Horizontal/Vertical Filtering
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- :ref:`Accessing fields of a Composite type or elements of an Array type <composite_array_columns>` is now possible with the arrow operators(``->``, ``->>``) in the same way you would access a JSON type fields.
- :ref:`pattern_matching` operators for `POSIX regular expressions <https://www.postgresql.org/docs/current/functions-matching.html#FUNCTIONS-POSIX-REGEXP>`_ are now available: ``match`` and ``imatch``, equivalent in PostgreSQL to ``~`` and ``~*`` respectively.
Insertions/Updates
~~~~~~~~~~~~~~~~~~
- ``limit`` can now affect the number of updated/deleted rows. See :ref:`limited_update_delete`.
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.
Administration
~~~~~~~~~~~~~~
- Two :ref:`health check endpoints <health_check>` are now exposed in a secondary port.
- :ref:`pgrst_logging` now shows the database user.
- 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 <https://www.postgresql.org/docs/current/libpq-envars.html>`_ 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 <raise_error>` 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
~~~~~~~~~~~~~~~~~~~~~~~~~~
* 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 <config_full_list>`.
* Added the :ref:`file_descriptors` subsection.
* Added a reference page for :ref:`error_source`.
* Moved the :ref:`error_source` and the :ref:`status_codes` sections to the :ref:`errors reference page <error_source>`.
* 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 *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`.
* 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 <https://github.com/PostgREST/postgrest/issues/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 <https://github.com/PostgREST/postgrest/issues/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 <https://github.com/PostgREST/postgrest/issues/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 <https://github.com/PostgREST/postgrest/issues/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 <https://github.com/PostgREST/postgrest/issues/2001>`_).
* Using ``Prefer: return=representation`` no longer returns a ``Location`` header
- This reduces unnecessary computing for all insertions (`#2312 <https://github.com/PostgREST/postgrest/issues/2312>`_)
Bug fixes
---------
* Return ``204 No Content`` without ``Content-Type`` for ``PUT`` (`#2058 <https://github.com/PostgREST/postgrest/issues/2058>`_)
* Clarify error for failed schema cache load. (`#2107 <https://github.com/PostgREST/postgrest/issues/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 <https://github.com/PostgREST/postgrest/issues/1771>`_)
* Remove functions, which are not callable due to unnamed arguments, from schema cache and OpenAPI output. (`#2152 <https://github.com/PostgREST/postgrest/issues/2152>`_)
* Fix accessing JSON array fields with ``->`` and ``->>`` in ``?select=`` and ``?order=``. (`#2145 <https://github.com/PostgREST/postgrest/issues/2145>`_)
* Ignore ``max-rows`` on ``POST``, ``PATCH``, ``PUT`` and ``DELETE`` (`#2155 <https://github.com/PostgREST/postgrest/issues/2155>`_)
* Fix inferring a foreign key column as a primary key column on views (`#2254 <https://github.com/PostgREST/postgrest/issues/2254>`_)
* Restrict generated many-to-many relationships (`#2070 <https://github.com/PostgREST/postgrest/issues/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 <https://github.com/PostgREST/postgrest/issues/2278>`_)
* Prevent views from breaking one-to-many/many-to-one embeds when using column or foreign key as target (`#2277 <https://github.com/PostgREST/postgrest/issues/2277>`_, `#2238 <https://github.com/PostgREST/postgrest/issues/2238>`_, `#1643 <https://github.com/PostgREST/postgrest/issues/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 <https://github.com/PostgREST/postgrest/issues/2317>`_)
* The search path now correctly identifies schemas with uppercase and special characters in their names (regression) (`#2341 <https://github.com/PostgREST/postgrest/issues/2341>`_)
* "404 Not Found" on nested routes and "405 Method Not Allowed" errors no longer start an empty database transaction (`#2364 <https://github.com/PostgREST/postgrest/issues/2364>`_)
* Fix inaccurate result count when an inner embed was selected after a normal embed in the query string (`#2342 <https://github.com/PostgREST/postgrest/issues/2342>`_)
* ``OPTIONS`` requests no longer start an empty database transaction (`#2376 <https://github.com/PostgREST/postgrest/issues/2376>`_)
* Allow using columns with dollar sign ($) without double quoting in filters and ``select`` (`#2395 <https://github.com/PostgREST/postgrest/issues/2395>`_)
* Fix loop crash error on startup in PostgreSQL 15 beta 3. ``Log: "UNION types \"char\" and text cannot be matched."`` (`#2410 <https://github.com/PostgREST/postgrest/issues/2410>`_)
* Fix race conditions managing database connection helper (`#2397 <https://github.com/PostgREST/postgrest/issues/2397>`_)
* Allow ``limit=0`` in the request query to return an empty array (`#2269 <https://github.com/PostgREST/postgrest/issues/2269>`_)
Thanks
------
Big thanks from the `PostgREST team <https://github.com/orgs/PostgREST/people>`_ 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 <https://www.igutech.at/>`_
* `Daniel Babiak <https://github.com/dbabiak>`_
* 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 <https://github.com/PostgREST/postgrest#user-content-supporting-development>`_.
-153
View File
@@ -1,153 +0,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 <https://github.com/PostgREST/postgrest/releases/tag/v10.1.0>`_, `10.1.1 <https://github.com/PostgREST/postgrest/releases/tag/v10.1.1>`_ and `10.1.2 <https://github.com/PostgREST/postgrest/releases/tag/v10.1.2>`_. You can look at the detailed changelog and download the pre-compiled binaries on the `GitHub release page <https://github.com/PostgREST/postgrest/releases/tag/v10.2.0>`_.
Features
--------
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`.
Pool Connection Acquisition Timeout
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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
~~~~~~~~~~~~~~~~~~~~~~~~~~
* 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 <deploy_heroku>`.
Changes
-------
* Removed ``db-pool-timeout`` option because it was removed in the ``hasql-pool`` library that PostgREST uses for SQL connections. (`#2444 <https://github.com/PostgREST/postgrest/issues/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 <s_procs_array>` or JSON parameter for a better performance. (`#1385 <https://github.com/PostgREST/postgrest/issues/1385>`_)
Bug fixes
---------
* Reduce allocations communication with PostgreSQL, particularly for request bodies. (`#2261 <https://github.com/PostgREST/postgrest/issues/2261>`_, `#2349 <https://github.com/PostgREST/postgrest/issues/2349>`_, `#2467 <https://github.com/PostgREST/postgrest/issues/2467>`_)
* Fix ``SIGUSR1`` to fully flush the connection pool. (`#2401 <https://github.com/PostgREST/postgrest/issues/2401>`_, `#2444 <https://github.com/PostgREST/postgrest/issues/2444>`_)
* Fix opening an empty transaction on failed resource embedding. (`#2428 <https://github.com/PostgREST/postgrest/issues/2428>`_)
* Fix embedding the same table multiple times. (`#2455 <https://github.com/PostgREST/postgrest/issues/2455>`_)
* Fix a regression when embedding views where base tables have a different column order for foreign key columns (`#2518 <https://github.com/PostgREST/postgrest/issues/2518>`_)
* Fix a regression with the ``Location`` header when :ref:`inserting <insert>` into views with primary keys from multiple tables (`#2458 <https://github.com/PostgREST/postgrest/issues/2458>`_)
* Fix a regression in OpenAPI output with mode ``follow-privileges`` (`#2356 <https://github.com/PostgREST/postgrest/issues/2356>`_)
* Fix infinite recursion when loading schema cache with self-referencing view (`#2283 <https://github.com/PostgREST/postgrest/issues/2283>`_)
* Return status code ``200`` instead of ``404`` for ``PATCH`` requests which don't affect any rows (`#2343 <https://github.com/PostgREST/postgrest/issues/2343>`_)
* Treat the :ref:`computed relationships <computed_relationships>` that do not return ``SETOF`` as M2O/O2O relationship (`#2481 <https://github.com/PostgREST/postgrest/issues/2481>`_)
* Fix embedding a computed relationship with a normal relationship (`#2534 <https://github.com/PostgREST/postgrest/issues/2534>`_)
* Fix error message when ``[]`` is used inside ``select`` (`#2362 <https://github.com/PostgREST/postgrest/issues/2362>`_)
* Disallow ``!inner`` on computed columns (`#2475 <https://github.com/PostgREST/postgrest/issues/2475>`_)
* Ignore leading and trailing spaces in column names when parsing the query string (`#2285 <https://github.com/PostgREST/postgrest/issues/2285>`_)
* Fix ``UPSERT`` with PostgreSQL 15 (`#2545 <https://github.com/PostgREST/postgrest/issues/2545>`_)
* Fix embedding views with multiple references to the same base column (`#2459 <https://github.com/PostgREST/postgrest/issues/2459>`_)
* Fix regression when embedding views with partial references to multi column foreign keys (`#2548 <https://github.com/PostgREST/postgrest/issues/2548>`_)
* Fix regression when requesting ``limit=0`` and ``db-max-row`` is set (`#2558 <https://github.com/PostgREST/postgrest/issues/2558>`_)
* Return a clear error without hitting the database when trying to update or insert an unknown column with ``?columns`` (`#2542 <https://github.com/PostgREST/postgrest/issues/2542>`_)
* Fix bad M2M embedding on RPC (`#2565 <https://github.com/PostgREST/postgrest/issues/2565>`_)
* Replace misleading error message when no function is found with a hint containing functions/parameters names suggestions (`#2575 <https://github.com/PostgREST/postgrest/issues/2575>`_)
* Move explanation about "single parameters" from the ``message`` to the ``details`` in the error output (`#2582 <https://github.com/PostgREST/postgrest/issues/2582>`_)
* Replace misleading error message when no relationship is found with a hint containing parent/child names suggestions (`#2569 <https://github.com/PostgREST/postgrest/issues/2569>`_)
* Add the required OpenAPI items object when the parameter is an array (`#1405 <https://github.com/PostgREST/postgrest/issues/1405>`_)
* Add upsert headers for ``POST`` requests to the OpenAPI output (`#2592 <https://github.com/PostgREST/postgrest/issues/2592>`_)
* Fix foreign keys pointing to ``VIEW`` instead of ``TABLE`` in OpenAPI output (`#2623 <https://github.com/PostgREST/postgrest/issues/2623>`_)
* Consider any PostgreSQL authentication failure as fatal and exit immediately (`#2622 <https://github.com/PostgREST/postgrest/issues/2622>`_)
* Fix ``NOTIFY pgrst`` not reloading the db connections catalog cache (`#2620 <https://github.com/PostgREST/postgrest/issues/2620>`_)
* Fix ``db-pool-acquisition-timeout`` not logging to stderr when the timeout is reached (`#2667 <https://github.com/PostgREST/postgrest/issues/2667>`_)
* Fix PostgreSQL resource leak with long-lived connections through the :ref:`db-pool-max-lifetime` configuration (`#2638 <https://github.com/PostgREST/postgrest/issues/2638>`_)
* There is now a stricter parsing of the query string. Instead of silently ignoring, the parser now returns a :ref:`PostgREST error <pgrst100>` on invalid syntax. (`#2537 <https://github.com/PostgREST/postgrest/issues/2537>`_)
Thanks
------
Big thanks from the `PostgREST team <https://github.com/orgs/PostgREST/people>`_ 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 <https://www.igutech.at/>`_
* `Daniel Babiak <https://github.com/dbabiak>`_
* 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 <https://github.com/PostgREST/postgrest#user-content-supporting-development>`_.
-123
View File
@@ -1,123 +0,0 @@
11.0.1
======
These release notes include the changes in versions `11.0.0 <https://github.com/PostgREST/postgrest/releases/tag/v11.0.0>`_ and `11.0.1 <https://github.com/PostgREST/postgrest/releases/tag/v11.0.1>`_. The detailed changelog and pre-compiled binaries are available on the `GitHub release pages <https://github.com/PostgREST/postgrest/releases>`_.
Features
--------
Transactions
~~~~~~~~~~~~
- New configurable :ref:`isolation_lvl`.
- New :ref:`impersonated_settings`.
Inserts
~~~~~~~
- New :ref:`bulk_insert_default`.
Functions
~~~~~~~~~
- Filters on Table-Valued Functions are now guaranteed to be inlined. See :ref:`function_inlining`.
Horizontal Filtering
~~~~~~~~~~~~~~~~~~~~
- New ``isdistinct`` :ref:`operator <operators>`. It is a direct translation of `IS DINTINCT FROM <https://www.postgresql.org/docs/current/functions-comparison.html#FUNCTIONS-COMPARISON-PRED-TABLE>`_.
- New ``and/all`` :ref:`modifiers`.
Resource Embedding
~~~~~~~~~~~~~~~~~~
- New :ref:`top_level_order`.
- New :ref:`spread_embed`.
- New :ref:`null_embed`.
- New :ref:`empty_embed`.
OpenAPI
~~~~~~~
- Allow :ref:`overriding the OpenAPI title <open-api>`.
- Allow :ref:`override_openapi`.
Admin
~~~~~
- New :ref:`trace_header`.
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`, :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
----------------
- Removed Bulk Call with ``Prefer: params=multiple-objects`` on Stored Procedures.
- To comply with `RFC 9110 <https://www.rfc-editor.org/rfc/rfc9110.html#name-range>`_, 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
------
Big thanks from the `PostgREST team <https://github.com/orgs/PostgREST/people>`_ 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 <https://github.com/roboflow>`_
* Evans Fernandes
* Jan Sommer
* `Franz Gusenbauer <https://www.igutech.at/>`_
* 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 <https://github.com/PostgREST/postgrest#user-content-supporting-development>`_.
-73
View File
@@ -1,73 +0,0 @@
11.1.0
======
Features
--------
Connection Pool
~~~~~~~~~~~~~~~
- New :ref:`db-pool-max-idletime`. It enables a :ref:`dyn_conn_pool`.
Configuration
~~~~~~~~~~~~~
- New :ref:`in_db_config`. It no longer requires high privileges and can be used on cloud-hosted databases.
Bug fixes
---------
- Fix dropping schema cache reload notifications.
- Stop automatic recovery when the error is "no password supplied".
Thanks
------
Big thanks from the `PostgREST team <https://github.com/orgs/PostgREST/people>`_ 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 <https://github.com/roboflow>`_
* Evans Fernandes
* Jan Sommer
* `Franz Gusenbauer <https://www.igutech.at/>`_
* 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 <https://github.com/PostgREST/postgrest#user-content-supporting-development>`_.
-4
View File
@@ -1,4 +0,0 @@
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.
-4
View File
@@ -1,4 +0,0 @@
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.
+4 -3
View File
@@ -1,6 +1,7 @@
docutils==0.20.1
sphinx>=4.3.0
sphinx>=5.0.2
sphinx-copybutton
sphinx-rtd-theme>=0.5.1
sphinx-tabs
urllib3==2.2.3
sphinx-tabs>=3.2.0
urllib3==2.2.1
sphinxext-opengraph==0.9.1
+46
View File
@@ -0,0 +1,46 @@
.. tabs::
.. group-tab:: macOS
You can install PostgREST from the `Homebrew official repo <https://formulae.brew.sh/formula/postgrest>`_.
.. code:: bash
brew install postgrest
.. group-tab:: FreeBSD
You can install PostgREST from the `official ports <https://www.freshports.org/www/hs-postgrest>`_.
.. code:: bash
pkg install hs-postgrest
.. group-tab:: Linux
.. tabs::
.. tab:: Arch Linux
You can install PostgREST from the `community repo <https://archlinux.org/packages/extra/x86_64/postgrest/>`_.
.. code:: bash
pacman -S postgrest
.. tab:: Nix
You can install PostgREST from nixpkgs.
.. code:: bash
nix-env -i haskellPackages.postgrest
.. group-tab:: Windows
You can install PostgREST using `Chocolatey <https://community.chocolatey.org/packages/postgrest>`_ or `Scoop <https://github.com/ScoopInstaller/Scoop>`_.
.. code:: bash
choco install postgrest
scoop install postgrest
+1
View File
@@ -13,6 +13,7 @@ pkgs.mkShell {
docs.spellcheck
docs.dictcheck
docs.linkcheck
docs.check
];
shellHook = ''
+33 -6
View File
@@ -15,7 +15,12 @@ To make an API we'll simply be building a database. All the endpoints and permis
By the end of this tutorial you'll have a working database, PostgREST server, and a simple single-user todo list API.
Step 1. Install PostgreSQL
Step 1. Relax, we'll help
-------------------------
As you begin the tutorial, pop open the project `chat room <https://gitter.im/begriffs/postgrest>`_ in another tab. There are a nice group of people active in the project and we'll help you out if you get stuck.
Step 2. Install PostgreSQL
--------------------------
If you're already familiar with using PostgreSQL and have it installed on your system you can use the existing installation (see :ref:`pg-dependency` for minimum requirements). For this tutorial we'll describe how to use the database in Docker because database configuration is otherwise too complicated for a simple tutorial.
@@ -30,10 +35,28 @@ If Docker is not installed, you can get it `here <https://www.docker.com/get-sta
This will run the Docker instance as a daemon and expose port 5433 to the host system so that it looks like an ordinary PostgreSQL server to the rest of the system.
Step 2. Install PostgREST
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 <https://github.com/PostgREST/postgrest/releases/latest>`_ for a list of downloads. In the event that your platform is not among those already pre-built, see :ref:`build_source` for instructions how to build it yourself. Also let us know to add your platform in the next release.
Using a Package Manager
~~~~~~~~~~~~~~~~~~~~~~~
You can use your OS package manager to install PostgREST.
.. include:: ../shared/installation.rst
Then, try running it with:
.. code-block:: bash
postgrest -h
It should print the help page with its version and the available options.
Downloading a Pre-Built Binary
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PostgREST is also distributed as a single binary, with versions compiled for major distributions of macOS, Windows, Linux and FreeBSD. Visit the `latest release <https://github.com/PostgREST/postgrest/releases/latest>`_ for a list of downloads. In the event that your platform is not among those already pre-built, see :ref:`build_source` for instructions how to build it yourself. Also let us know to add your platform in the next release.
The pre-built binaries for download are :code:`.tar.xz` compressed files (except Windows which is a zip file). To extract the binary, go into the terminal and run
@@ -71,7 +94,7 @@ If everything is working correctly it will print out its version and the availab
</div></div>
</details>
<details>
<summary>OS X</summary>
<summary>macOS</summary>
<div class="highlight-bash"><div class="highlight">
<pre>brew install postgresql</pre>
</div></div>
@@ -86,7 +109,7 @@ If everything is working correctly it will print out its version and the availab
</details>
</p>
Step 3. Create Database for API
Step 4. Create Database for API
-------------------------------
Connect to the SQL console (psql) inside the container. To do so, run this from your command line:
@@ -149,7 +172,7 @@ Now quit out of psql; it's time to start the API!
\q
Step 4. Run PostgREST
Step 5. Run PostgREST
---------------------
PostgREST can use a configuration file to tell it how to connect to the database. Create a file :code:`tutorial.conf` with this inside:
@@ -167,6 +190,10 @@ Now run the server:
.. code-block:: bash
# Running postgrest installed from a package manager
postgrest tutorial.conf
# Running postgrest binary
./postgrest tutorial.conf
You should see
+4 -5
View File
@@ -41,16 +41,15 @@ Let's create a password and provide it to PostgREST. Think of a nice long one, o
export LC_CTYPE=C
# read random bytes and keep only alphanumerics
< /dev/urandom tr -dc A-Za-z0-9 | head -c32
echo "jwt-secret = \"$(LC_ALL=C tr -dc 'A-Za-z0-9' </dev/urandom | head -c32)\"" >> tutorial.conf
Open the :code:`tutorial.conf` (created in the previous tutorial) and add a line with the password:
check the :code:`tutorial.conf` (created in the previous tutorial) the line with the password:
.. code-block:: ini
# PASSWORD MUST BE AT LEAST 32 CHARS LONG
# add this line to tutorial.conf:
jwt-secret = "<the password you made>"
cat tutorial.conf
If the PostgREST server is still running from the previous tutorial, restart it to load the updated configuration file.
+1 -22
View File
@@ -1,37 +1,16 @@
{-# LANGUAGE CPP #-}
module Main (main) where
import System.IO (BufferMode (..), hSetBuffering)
import qualified PostgREST.App as App
import qualified PostgREST.CLI as CLI
import Protolude
#ifndef mingw32_HOST_OS
import qualified PostgREST.Unix as Unix
#endif
main :: IO ()
main = do
setBuffering
opts <- CLI.readCLIShowHelp
CLI.main installSignalHandlers runAppInSocket opts
installSignalHandlers :: App.SignalHandlerInstaller
#ifndef mingw32_HOST_OS
installSignalHandlers = Unix.installSignalHandlers
#else
installSignalHandlers _ = pass
#endif
runAppInSocket :: Maybe App.SocketRunner
#ifndef mingw32_HOST_OS
runAppInSocket = Just Unix.runAppWithSocket
#else
runAppInSocket = Nothing
#endif
CLI.main opts
setBuffering :: IO ()
setBuffering = do
+34 -9
View File
@@ -22,6 +22,15 @@ build the `postgrestPackage` attribute from the Nix expression it finds in our
`default.nix` (see below for details). Nix will take care of getting the right
GHC version and all the build dependencies.
You can also build a statically linked binary with:
```bash
$ nix-build --attr postgrestStatic
$ ldd result/bin/postgrest
$ not a dynamic executable
```
## Binary cache
We recommend that you use the PostgREST binary cache on
@@ -82,7 +91,7 @@ Some additional modules like `memory`, `docker` and `release`
have large dependencies that would need to be built before the shell becomes
available, which could take an especially long time if the cachix binary cache
is not used. You can activate those by passing a flag to `nix-shell` with
`nix-shell --arg <module> true`. This will make the respective utilites available:
`nix-shell --arg <module> true`. This will make the respective utilities available:
```bash
$ nix-shell --arg memory true
@@ -104,7 +113,7 @@ postgrest-test-memory
Note that `postgrest-test-memory` is now also available.
To run one-off commands, you can also use `nix-shell --run <command>`, which
will lauch the Nix shell, run that one command and exit. Note that the tab
will launch the Nix shell, run that one command and exit. Note that the tab
completion will not work with `nix-shell --run`, as Nix has yet to evaluate
our Nix expressions to see which utilities are available.
@@ -212,15 +221,14 @@ doctests for some of our modules are also available:
## Code coverage
Code coverage is available under the `postgrest-coverage` command. This will produce a `./coverage` directory that can be visualized with a simple http server.
Code coverage is available under the `postgrest-coverage` command. This will produce a `./coverage` directory that can be visualized on a browser.
```bash
# Will run all the tests and produce a coverage dir
[nix-shell]$ postgrest-coverage
...
# Visualize the output
[nix-shell]$ cd coverage
[nix-shell]$ python -mSimpleHTTPServer 8080
postgrest-coverage: To see the results, visit file://$(pwd)/coverage/check/hpc_index.html
```
## Linting and styling code
@@ -238,11 +246,11 @@ $ nix-shell --run postgrest-style
```
There is also `postgrest-style-check` that exits with a non-zero exit code if
the check resulted in any uncommited changes. It's mostly useful for CI.
the check resulted in any uncommitted changes. It's mostly useful for CI.
## General development tools
Tools like `postgrest-build`, `postgrest-run` etc. are simple wrappers around
Tools like `postgrest-build`, `postgrest-run`, `postgrest-repl` etc. are simple wrappers around
`cabal` and should do what you expect. `postgrest-check` runs most checks that will
also run in CI, with the exception of the IO and Memory checks that need to be run
separately.
@@ -256,6 +264,23 @@ run against the latest PostgreSQL version by default.
file is changed. For example, `postgrest-watch postgrest-with-all postgrest-test-spec`
will re-run the full spec test suite against all PostgreSQL versions on every change.
## REPL
You can use `postgrest-repl` to manually inspect the PostgREST modules.
```bash
$ postgrest-repl
ghci> import PostgREST.<tab>
PostgREST.Admin PostgREST.Config.Database PostgREST.Plan.MutatePlan PostgREST.Response.OpenAPI
PostgREST.ApiRequest PostgREST.Config.JSPath PostgREST.Plan.ReadPlan PostgREST.SchemaCache
...
ghci> import PostgREST.MediaType
ghci> decodeMediaType "application/json"
MTApplicationJSON
```
## Tour
The following is not required for working on PostgREST with Nix, but it will
@@ -284,7 +309,7 @@ version.
### `shell.nix`
[`shell.nix`](../shell.nix) defines an environment in which PostgREST can be
built and developed. It extends the build enviroment from our `postgrest`
built and developed. It extends the build environment from our `postgrest`
attribute with useful utilities that will be put on the PATH in `nix-shell`.
### `nix/overlays`
+1 -1
View File
@@ -74,7 +74,7 @@ required to avoid build timeouts in CI.
You'll need to set the `CACHIX_SIGNING_KEY` before proceeding, e.g. by creating
a file containing `export CACHIX_SIGNING_KEY=...` and sourcing that file, which
avoids having the secret in you shell history.
avoids having the secret in your shell history.
To push all new artifacts to Cachix, run:
+13
View File
@@ -57,6 +57,18 @@ let
postgrest "''${_arg_leftovers[@]}"
'';
repl =
checkedShellScript
{
name = "postgrest-repl";
docs = "Interact with PostgREST modules using the cabal repl";
args = [ "ARG_LEFTOVERS([cabal v2-repl arguments])" ];
inRootDir = true;
withEnv = postgrest.env;
}
''
exec ${cabal-install}/bin/cabal v2-repl "''${_arg_leftovers[@]}"
'';
in
buildToolbox
{
@@ -65,5 +77,6 @@ buildToolbox
build
clean
run
repl
];
}
+11 -8
View File
@@ -76,13 +76,12 @@ let
inherit name;
docs =
''
Run the vegeta loadtest twice:
- once on the <target> branch
Run the vegeta loadtest against every target branch and HEAD:
- once on the every <target-#> branch
- once in the current worktree
'';
args = [
"ARG_POSITIONAL_SINGLE([target], [Commit-ish reference to compare with])"
"ARG_LEFTOVERS([additional vegeta arguments])"
"ARG_POSITIONAL_INF([target], [Commit-ish reference to compare with], 1)"
];
positionalCompletion =
''
@@ -93,9 +92,11 @@ let
inRootDir = true;
}
''
for tgt in "''${_arg_target[@]}"; do
cat << EOF
Running loadtest on "$_arg_target"...
Running loadtest on "$tgt"...
EOF
@@ -104,21 +105,23 @@ let
# Save the results in the current working tree, too,
# otherwise they'd be lost in the temporary working tree
# created by withTools.withGit.
${withTools.withGit} "$_arg_target" ${loadtest} --output "$PWD/loadtest/$_arg_target.bin" --testdir "$PWD/test/load" "''${_arg_leftovers[@]}"
${withTools.withGit} "$tgt" ${loadtest} --output "$PWD/loadtest/$tgt.bin" --testdir "$PWD/test/load"
cat << EOF
Done running on "$_arg_target".
Done running on "$tgt".
EOF
done
cat << EOF
Running loadtest on HEAD...
EOF
${loadtest} --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load" "''${_arg_leftovers[@]}"
${loadtest} --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load"
cat << EOF
+3 -3
View File
@@ -56,8 +56,8 @@ let
inRootDir = true;
}
''
trap "echo You need to be on the main branch to proceed. Exiting ..." ERR
[ "$(git rev-parse --abbrev-ref HEAD)" == "main" ]
trap "echo You need to be on the main branch or a release branch to proceed. Exiting ..." ERR
[[ "$(git rev-parse --abbrev-ref HEAD)" =~ ^main$|^v[0-9]+$ ]]
trap "" ERR
trap "echo You have uncommitted changes in postgrest.cabal. Exiting ..." ERR
@@ -111,7 +111,7 @@ let
remote="$(git remote -v | grep PostgREST/postgrest | grep push | cut -f1)"
trap "" ERR
push="git push --atomic $remote main v$new_version"
push="git push --atomic $remote $(git rev-parse --abbrev-ref HEAD) v$new_version"
echo "To push both the branch and the new tag, the following will be run:"
echo
@@ -1,6 +1,8 @@
# PostgREST
[![Join the chat at https://gitter.im/begriffs/postgrest](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/begriffs/postgrest)
[![Donate](https://img.shields.io/badge/Donate-Patreon-orange.svg?colorB=F96854)](https://www.patreon.com/postgrest)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/postgrest)
[![Docs](https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat)](http://postgrest.org)
[![Build Status](https://github.com/postgrest/postgrest/actions/workflows/ci.yaml/badge.svg?branch=main)](https://github.com/PostgREST/postgrest/actions?query=branch%3Amain)
@@ -10,30 +12,30 @@ write from scratch.
## Sponsors
<table>
<table align="center">
<tbody>
<tr>
<td align="center" valign="middle">
<a href="https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest" target="_blank">
<img width="222px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/cybertec-new.png">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/cybertec-new.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://www.2ndquadrant.com/en/?utm_campaign=External%20Websites&utm_source=PostgREST&utm_medium=Logo" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/2ndquadrant.png">
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/gnuhost.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://tryretool.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/retool.png">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/neon.jpg">
</a>
</td>
</tr>
<tr></tr>
<tr>
<td align="center" valign="middle">
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/gnuhost.png">
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/code-build.png">
</a>
</td>
<td align="center" valign="middle">
@@ -42,8 +44,8 @@ write from scratch.
</a>
</td>
<td align="center" valign="middle">
<a href="https://oblivious.ai/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/oblivious.jpg">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/tembo.png">
</a>
</td>
</tr>
@@ -56,13 +58,13 @@ To learn how to use this container, see the [PostgREST Docker
documentation](https://postgrest.org/en/stable/install.html#docker).
You can configure the PostgREST image by setting
[enviroment variables](https://postgrest.org/en/stable/configuration.html).
[environment variables](https://postgrest.org/en/stable/configuration.html).
# How this image is built
The image is built from scratch using
[Nix](https://nixos.org/nixpkgs/manual/#sec-pkgs-dockerTools) instead of a
`Dockerfile`, which yields a higly secure and optimized image. This is also why
`Dockerfile`, which yields a highly secure and optimized image. This is also why
no commands are listed in the image history. See the [PostgREST
respository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for
details on the build process and how to inspect the image.
-1
View File
@@ -68,7 +68,6 @@ let
echo "Linting bash scripts..."
${shellcheck}/bin/shellcheck \
.github/get_cirrusci_freebsd \
.github/release
echo "Linting workflows..."
+2 -2
View File
@@ -164,7 +164,7 @@ let
${ghc}/bin/hpc markup --highlight-covered --destdir=coverage/overlay "$tmpdir"/overlay.tix || true
${ghc}/bin/hpc markup --highlight-covered --destdir=coverage/check "$tmpdir"/check.tix || true
echo "ERROR: Something is covered by both the tests and the overlay:"
echo "file://$(pwd)/coverage/check/hpc_index.html"
echo "postgrest-coverage: To see the results, visit file://$(pwd)/coverage/check/hpc_index.html"
exit 1
else
# copy the result .tix file to the coverage/ dir to make it available to postgrest-coverage-draft-overlay, too
@@ -174,7 +174,7 @@ let
# create html and stdout reports
${ghc}/bin/hpc markup --destdir=coverage coverage/postgrest.tix
echo "file://$(pwd)/coverage/hpc_index.html"
echo "postgrest-coverage: To see the results, visit file://$(pwd)/coverage/hpc_index.html"
${ghc}/bin/hpc report coverage/postgrest.tix "''${_arg_leftovers[@]}"
fi
''
+1 -1
View File
@@ -72,7 +72,7 @@ let
# We try to make the database cluster as independent as possible from the host
# by specifying the timezone, locale and encoding.
# initdb -U creates a superuser(man initdb)
PGTZ=UTC initdb --no-locale --encoding=UTF8 --nosync -U "${superuserRole}" --auth=trust \
TZ=$PGTZ initdb --no-locale --encoding=UTF8 --nosync -U "${superuserRole}" --auth=trust \
>> "$setuplog"
log "Starting the database cluster..."
+16 -9
View File
@@ -1,5 +1,5 @@
name: postgrest
version: 11.2.2
version: 12.0.3
synopsis: REST API for any Postgres database
description: Reads the schema of a PostgreSQL database and creates RESTful routes
for tables, views, and functions, supporting all HTTP methods that security
@@ -64,6 +64,7 @@ library
PostgREST.Plan.ReadPlan
PostgREST.Plan.Types
PostgREST.RangeQuery
PostgREST.Unix
PostgREST.ApiRequest
PostgREST.ApiRequest.Preferences
PostgREST.ApiRequest.QueryParams
@@ -71,6 +72,7 @@ library
PostgREST.Response
PostgREST.Response.OpenAPI
PostgREST.Response.GucHeader
PostgREST.Response.Performance
PostgREST.Version
other-modules: Paths_postgrest
build-depends: base >= 4.9 && < 4.17
@@ -80,12 +82,15 @@ library
, auto-update >= 0.1.4 && < 0.2
, base64-bytestring >= 1 && < 1.3
, bytestring >= 0.10.8 && < 0.12
, cache >= 0.1.3 && < 0.2.0
, case-insensitive >= 1.2 && < 1.3
, cassava >= 0.4.5 && < 0.6
, clock >= 0.8.3 && < 0.9.0
, configurator-pg >= 0.2 && < 0.3
, containers >= 0.5.7 && < 0.7
, contravariant-extras >= 0.3.3 && < 0.4
, cookie >= 0.4.2 && < 0.5
, directory >= 1.2.6 && < 1.4
, either >= 4.4.1 && < 5.1
, extra >= 1.7.0 && < 2.0
, fuzzyset >= 0.2.3
@@ -111,10 +116,13 @@ library
, regex-tdfa >= 1.2.2 && < 1.4
, retry >= 0.7.4 && < 0.10
, scientific >= 0.3.4 && < 0.4
, streaming-commons >= 0.1.1 && < 0.3
, swagger2 >= 2.4 && < 2.9
, text >= 1.2.2 && < 1.3
, time >= 1.6 && < 1.12
, timeit >= 2.0 && < 2.1
, unordered-containers >= 0.2.8 && < 0.3
, unix-compat >= 0.5.4 && < 0.6
, vault >= 0.3.1.5 && < 0.4
, vector >= 0.11 && < 0.14
, wai >= 3.2.1 && < 3.3
@@ -144,9 +152,6 @@ library
if !os(windows)
build-depends:
unix
, directory >= 1.2.6 && < 1.4
exposed-modules:
PostgREST.Unix
executable postgrest
default-language: Haskell2010
@@ -188,7 +193,6 @@ test-suite spec
Feature.ConcurrentSpec
Feature.CorsSpec
Feature.ExtraSearchPathSpec
Feature.LegacyGucsSpec
Feature.NoSuperuserSpec
Feature.ObservabilitySpec
Feature.OpenApi.DisabledOpenApiSpec
@@ -198,27 +202,30 @@ test-suite spec
Feature.OpenApi.RootSpec
Feature.OpenApi.SecurityOpenApiSpec
Feature.OptionsSpec
Feature.Query.AggregateFunctionsSpec
Feature.Query.AndOrParamsSpec
Feature.Query.ComputedRelsSpec
Feature.Query.CustomMediaSpec
Feature.Query.DeleteSpec
Feature.Query.EmbedDisambiguationSpec
Feature.Query.EmbedInnerJoinSpec
Feature.Query.PlanSpec
Feature.Query.HtmlRawOutputSpec
Feature.Query.ErrorSpec
Feature.Query.InsertSpec
Feature.Query.JsonOperatorSpec
Feature.Query.MultipleSchemaSpec
Feature.Query.ErrorSpec
Feature.Query.NullsStripSpec
Feature.Query.PgSafeUpdateSpec
Feature.Query.PlanSpec
Feature.Query.PostGISSpec
Feature.Query.PreferencesSpec
Feature.Query.QueryLimitedSpec
Feature.Query.QuerySpec
Feature.Query.RangeSpec
Feature.Query.RawOutputTypesSpec
Feature.Query.RelatedQueriesSpec
Feature.Query.RpcSpec
Feature.Query.ServerTimingSpec
Feature.Query.SingularSpec
Feature.Query.NullsStrip
Feature.Query.SpreadQueriesSpec
Feature.Query.UnicodeSpec
Feature.Query.UpdateSpec
+17 -40
View File
@@ -1,11 +1,9 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE NamedFieldPuns #-}
module PostgREST.Admin
( runAdmin
) where
import qualified Data.Text as T
import qualified Hasql.Session as SQL
import qualified Network.HTTP.Types.Status as HTTP
import qualified Network.Wai as Wai
@@ -22,24 +20,25 @@ import PostgREST.Config (AppConfig (..))
import qualified PostgREST.AppState as AppState
import Protolude
import Protolude.Partial (fromJust)
runAdmin :: AppConfig -> AppState -> Warp.Settings -> IO ()
runAdmin conf@AppConfig{configAdminServerPort} appState settings =
whenJust configAdminServerPort $ \adminPort -> do
AppState.logWithZTime appState $ "Admin server listening on port " <> show adminPort
void . forkIO $ Warp.runSettings (settings & Warp.setPort adminPort) adminApp
whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do
AppState.logWithZTime appState $ "Admin server listening on port " <> show (fromIntegral (fromJust configAdminServerPort) :: Integer)
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
where
adminApp = admin appState conf
-- | PostgREST admin application
admin :: AppState.AppState -> AppConfig -> Wai.Application
admin appState appConfig req respond = do
isMainAppReachable <- any isRight <$> reachMainApp appConfig
isMainAppReachable <- isRight <$> reachMainApp (AppState.getSocketREST appState)
isSchemaCacheLoaded <- isJust <$> AppState.getSchemaCache appState
isConnectionUp <-
if configDbChannelEnabled appConfig
then AppState.getIsListenerOn appState
else isRight <$> AppState.usePool appState (SQL.sql "SELECT 1")
else isRight <$> AppState.usePool appState appConfig (SQL.sql "SELECT 1")
case Wai.pathInfo req of
["ready"] ->
@@ -51,37 +50,15 @@ admin appState appConfig req respond = do
-- Try to connect to the main app socket
-- Note that it doesn't even send a valid HTTP request, we just want to check that the main app is accepting connections
-- The code for resolving the "*4", "!4", "*6", "!6", "*" special values is taken from
-- https://hackage.haskell.org/package/streaming-commons-0.2.2.4/docs/src/Data.Streaming.Network.html#bindPortGenEx
reachMainApp :: AppConfig -> IO [Either IOException ()]
reachMainApp AppConfig{..} =
case configServerUnixSocket of
Just path -> do
sock <- socket AF_UNIX Stream 0
(:[]) <$> try (do
connect sock $ SockAddrUnix path
withSocketsDo $ bracket (pure sock) close sendEmpty)
Nothing -> do
let
host | configServerHost `elem` ["*4", "!4", "*6", "!6", "*"] = Nothing
| otherwise = Just configServerHost
filterAddrs xs =
case configServerHost of
"*4" -> ipv4Addrs xs ++ ipv6Addrs xs
"!4" -> ipv4Addrs xs
"*6" -> ipv6Addrs xs ++ ipv4Addrs xs
"!6" -> ipv6Addrs xs
_ -> xs
ipv4Addrs = filter ((/=) AF_INET6 . addrFamily)
ipv6Addrs = filter ((==) AF_INET6 . addrFamily)
addrs <- getAddrInfo (Just $ defaultHints { addrSocketType = Stream }) (T.unpack <$> host) (Just . show $ configServerPort)
tryAddr `traverse` filterAddrs addrs
reachMainApp :: Socket -> IO (Either IOException ())
reachMainApp appSock = do
sockAddr <- getSocketName appSock
sock <- socket (addrFamily sockAddr) Stream defaultProtocol
try $ do
connect sock sockAddr
withSocketsDo $ bracket (pure sock) close sendEmpty
where
sendEmpty sock = void $ send sock mempty
tryAddr :: AddrInfo -> IO (Either IOException ())
tryAddr addr = do
sock <- socket (addrFamily addr) (addrSocketType addr) (addrProtocol addr)
try $ do
connect sock $ addrAddress addr
withSocketsDo $ bracket (pure sock) close sendEmpty
addrFamily (SockAddrInet _ _) = AF_INET
addrFamily (SockAddrInet6 {}) = AF_INET6
addrFamily (SockAddrUnix _) = AF_UNIX
+12 -51
View File
@@ -26,7 +26,6 @@ import qualified Data.ByteString.Lazy as LBS
import qualified Data.CaseInsensitive as CI
import qualified Data.Csv as CSV
import qualified Data.HashMap.Strict as HM
import qualified Data.List as L
import qualified Data.List.NonEmpty as NonEmptyList
import qualified Data.Map.Strict as M
import qualified Data.Set as S
@@ -37,7 +36,7 @@ import Data.Either.Combinators (mapBoth)
import Control.Arrow ((***))
import Data.Aeson.Types (emptyArray, emptyObject)
import Data.List (lookup, union)
import Data.List (lookup)
import Data.Ranged.Ranges (emptyRange, rangeIntersection,
rangeIsEmpty)
import Network.HTTP.Types.Header (RequestHeaders, hCookie)
@@ -51,12 +50,12 @@ import PostgREST.ApiRequest.Types (ApiRequestError (..),
RangeError (..))
import PostgREST.Config (AppConfig (..),
OpenAPIMode (..))
import PostgREST.MediaType (MTPlanFormat (..),
MediaType (..))
import PostgREST.MediaType (MediaType (..))
import PostgREST.RangeQuery (NonnegRange, allRange,
convertToLimitZeroRange,
hasLimitZero,
rangeRequested)
import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier (..),
Schema)
@@ -131,17 +130,16 @@ data ApiRequest = ApiRequest {
, iMethod :: ByteString -- ^ Raw request method
, iSchema :: Schema -- ^ The request schema. Can vary depending on profile headers.
, iNegotiatedByProfile :: Bool -- ^ If schema was was chosen according to the profile spec https://www.w3.org/TR/dx-prof-conneg/
, iAcceptMediaType :: MediaType -- ^ The media type in the Accept header
, iAcceptMediaType :: [MediaType] -- ^ The resolved media types in the Accept, considering quality(q) factors
, iContentMediaType :: MediaType -- ^ The media type in the Content-Type header
}
-- | Examines HTTP request and translates it into user intent.
userApiRequest :: AppConfig -> Request -> RequestBody -> Either ApiRequestError ApiRequest
userApiRequest conf req reqBody = do
userApiRequest :: AppConfig -> Request -> RequestBody -> SchemaCache -> Either ApiRequestError ApiRequest
userApiRequest conf req reqBody sCache = do
pInfo@PathInfo{..} <- getPathInfo conf $ pathInfo req
act <- getAction pInfo method
qPrms <- first QueryParamError $ QueryParams.parse (pathIsProc && act `elem` [ActionInvoke InvGet, ActionInvoke InvHead]) $ rawQueryString req
(acceptMediaType, contentMediaType) <- getMediaTypes conf hdrs act pInfo
(schema, negotiatedByProfile) <- getSchema conf hdrs method
(topLevelRange, ranges) <- getRanges method qPrms hdrs
(payload, columns) <- getPayload reqBody contentMediaType qPrms act pInfo
@@ -153,7 +151,7 @@ userApiRequest conf req reqBody = do
, iRange = ranges
, iTopLevelRange = topLevelRange
, iPayload = payload
, iPreferences = Preferences.fromHeaders (configDbTxAllowOverride conf) hdrs
, iPreferences = Preferences.fromHeaders (configDbTxAllowOverride conf) (dbTimezones sCache) hdrs
, iQueryParams = qPrms
, iColumns = columns
, iHeaders = iHdrs
@@ -162,7 +160,7 @@ userApiRequest conf req reqBody = do
, iMethod = method
, iSchema = schema
, iNegotiatedByProfile = negotiatedByProfile
, iAcceptMediaType = acceptMediaType
, iAcceptMediaType = maybe [MTAny] (map MediaType.decodeMediaType . parseHttpAccept) $ lookupHeader "accept"
, iContentMediaType = contentMediaType
}
where
@@ -171,6 +169,7 @@ userApiRequest conf req reqBody = do
lookupHeader = flip lookup hdrs
iHdrs = [ (CI.foldedCase k, v) | (k,v) <- hdrs, k /= hCookie]
iCkies = maybe [] parseCookies $ lookupHeader "Cookie"
contentMediaType = maybe MTApplicationJSON MediaType.decodeMediaType $ lookupHeader "content-type"
getPathInfo :: AppConfig -> [Text] -> Either ApiRequestError PathInfo
getPathInfo AppConfig{configOpenApiMode, configDbRootSpec} path =
@@ -204,15 +203,6 @@ getAction PathInfo{pathIsProc, pathIsDefSpec} method =
"OPTIONS" -> Right ActionInfo
_ -> Left $ UnsupportedMethod method
getMediaTypes :: AppConfig -> RequestHeaders -> Action -> PathInfo -> Either ApiRequestError (MediaType, MediaType)
getMediaTypes conf hdrs action path = do
acceptMediaType <- negotiateContent conf action path accepts
pure (acceptMediaType, contentMediaType)
where
accepts = maybe [MTAny] (map MediaType.decodeMediaType . parseHttpAccept) $ lookupHeader "accept"
contentMediaType = maybe MTApplicationJSON MediaType.decodeMediaType $ lookupHeader "content-type"
lookupHeader = flip lookup hdrs
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
getSchema AppConfig{configDbSchemas} hdrs method = do
case profile of
@@ -269,7 +259,9 @@ getPayload reqBody contentMediaType QueryParams{qsColumns} action PathInfo{pathI
else note "All object keys must match" . payloadAttributes reqBody
=<< if LBS.null reqBody && pathIsProc
then Right emptyObject
else first BS.pack $ JSON.eitherDecode reqBody
else first BS.pack $
-- Drop parsing error message in favor of generic one (https://github.com/PostgREST/postgrest/issues/2344)
maybe (Left "Empty or invalid json") Right $ JSON.decode reqBody
(MTTextCSV, _) -> do
json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
@@ -346,34 +338,3 @@ payloadAttributes raw json =
_ -> Just emptyPJArray
where
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
-- | Do content negotiation. i.e. choose a media type based on the intersection of accepted/produced media types.
negotiateContent :: AppConfig -> Action -> PathInfo -> [MediaType] -> Either ApiRequestError MediaType
negotiateContent conf action path accepts =
case firstAcceptedPick of
Just MTAny -> Right MTApplicationJSON -- by default(for */*) we respond with json
Just mt -> Right mt
Nothing -> Left . MediaTypeError $ map MediaType.toMime accepts
where
-- if there are multiple accepted media types, pick the first
firstAcceptedPick = listToMaybe $ L.intersect accepts $ producedMediaTypes conf action path
producedMediaTypes :: AppConfig -> Action -> PathInfo -> [MediaType]
producedMediaTypes conf action path =
case action of
ActionRead _ -> defaultMediaTypes ++ rawMediaTypes
ActionInvoke _ -> invokeMediaTypes
ActionInfo -> defaultMediaTypes
ActionMutate _ -> defaultMediaTypes
ActionInspect _ -> inspectMediaTypes
where
inspectMediaTypes = [MTOpenAPI, MTApplicationJSON, MTArrayJSONStrip, MTAny]
invokeMediaTypes =
defaultMediaTypes
++ rawMediaTypes
++ [MTOpenAPI | pathIsRootSpec path]
defaultMediaTypes =
[MTApplicationJSON, MTArrayJSONStrip, MTSingularJSON True, MTSingularJSON False, MTGeoJSON, MTTextCSV] ++
[MTPlan MTApplicationJSON PlanText mempty | configDbPlanEnabled conf] ++ [MTAny]
rawMediaTypes = configRawMediaTypes conf `union` [MTOctetStream, MTTextPlain, MTTextXML]
+70 -15
View File
@@ -10,11 +10,13 @@
module PostgREST.ApiRequest.Preferences
( Preferences(..)
, PreferCount(..)
, PreferHandling(..)
, PreferMissing(..)
, PreferParameters(..)
, PreferRepresentation(..)
, PreferResolution(..)
, PreferTransaction(..)
, PreferTimezone(..)
, fromHeaders
, shouldCount
, prefAppliedHeader
@@ -22,10 +24,12 @@ module PostgREST.ApiRequest.Preferences
import qualified Data.ByteString.Char8 as BS
import qualified Data.Map as Map
import qualified Data.Set as S
import qualified Network.HTTP.Types.Header as HTTP
import Protolude
import PostgREST.Config.Database (TimezoneNames)
import Protolude
-- $setup
-- Setup for doctests
@@ -36,6 +40,8 @@ import Protolude
-- >>> deriving instance Show PreferCount
-- >>> deriving instance Show PreferTransaction
-- >>> deriving instance Show PreferMissing
-- >>> deriving instance Show PreferHandling
-- >>> deriving instance Show PreferTimezone
-- >>> deriving instance Show Preferences
-- | Preferences recognized by the application.
@@ -47,14 +53,18 @@ data Preferences
, preferCount :: Maybe PreferCount
, preferTransaction :: Maybe PreferTransaction
, preferMissing :: Maybe PreferMissing
, preferHandling :: Maybe PreferHandling
, preferTimezone :: Maybe PreferTimezone
, invalidPrefs :: [ByteString]
}
-- |
-- Parse HTTP headers based on RFC7240[1] to identify preferences.
--
-- One header with comma-separated values can be used to set multiple preferences:
-- >>> let sc = S.fromList ["America/Los_Angeles"]
--
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates, count=exact")]
-- One header with comma-separated values can be used to set multiple preferences:
-- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates, count=exact, timezone=America/Los_Angeles")]
-- Preferences
-- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing
@@ -62,11 +72,15 @@ data Preferences
-- , preferCount = Just ExactCount
-- , preferTransaction = Nothing
-- , preferMissing = Nothing
-- , preferHandling = Nothing
-- , preferTimezone = Just
-- ( PreferTimezone "America/Los_Angeles" )
-- , invalidPrefs = []
-- }
--
-- Multiple headers can also be used:
--
-- >>> pPrint $ fromHeaders True [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null")]
-- >>> pPrint $ fromHeaders True sc [("Prefer", "resolution=ignore-duplicates"), ("Prefer", "count=exact"), ("Prefer", "missing=null"), ("Prefer", "handling=lenient"), ("Prefer", "invalid")]
-- Preferences
-- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing
@@ -74,31 +88,30 @@ data Preferences
-- , preferCount = Just ExactCount
-- , preferTransaction = Nothing
-- , preferMissing = Just ApplyNulls
-- , preferHandling = Just Lenient
-- , preferTimezone = Nothing
-- , invalidPrefs = [ "invalid" ]
-- }
--
-- If a preference is set more than once, only the first is used:
--
-- >>> preferTransaction $ fromHeaders True [("Prefer", "tx=commit, tx=rollback")]
-- >>> preferTransaction $ fromHeaders True sc [("Prefer", "tx=commit, tx=rollback")]
-- Just Commit
--
-- This is also the case across multiple headers:
--
-- >>> :{
-- preferResolution . fromHeaders True $
-- preferResolution . fromHeaders True sc $
-- [ ("Prefer", "resolution=ignore-duplicates")
-- , ("Prefer", "resolution=merge-duplicates")
-- ]
-- :}
-- Just IgnoreDuplicates
--
-- Preferences not recognized by the application are ignored:
--
-- >>> preferResolution $ fromHeaders True [("Prefer", "resolution=foo")]
-- Nothing
--
-- Preferences can be separated by arbitrary amounts of space, lower-case header is also recognized:
--
-- >>> pPrint $ fromHeaders True [("prefer", "count=exact, tx=commit ,return=representation , missing=default")]
-- >>> pPrint $ fromHeaders True sc [("prefer", "count=exact, tx=commit ,return=representation , missing=default, handling=strict, anything")]
-- Preferences
-- { preferResolution = Nothing
-- , preferRepresentation = Just Full
@@ -106,22 +119,43 @@ data Preferences
-- , preferCount = Just ExactCount
-- , preferTransaction = Just Commit
-- , preferMissing = Just ApplyDefaults
-- , preferHandling = Just Strict
-- , preferTimezone = Nothing
-- , invalidPrefs = [ "anything" ]
-- }
--
fromHeaders :: Bool -> [HTTP.Header] -> Preferences
fromHeaders allowTxEndOverride headers =
fromHeaders :: Bool -> TimezoneNames -> [HTTP.Header] -> Preferences
fromHeaders allowTxDbOverride acceptedTzNames headers =
Preferences
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
, preferParameters = parsePrefs [SingleObject]
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
, preferTransaction = if allowTxEndOverride then parsePrefs [Commit, Rollback] else Nothing
, preferTransaction = if allowTxDbOverride then parsePrefs [Commit, Rollback] else Nothing
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
, preferHandling = parsePrefs [Strict, Lenient]
, preferTimezone = if isTimezonePrefAccepted then PreferTimezone <$> timezonePref else Nothing
, invalidPrefs = filter checkPrefs prefs
}
where
mapToHeadVal :: ToHeaderValue a => [a] -> [ByteString]
mapToHeadVal = map toHeaderValue
acceptedPrefs = mapToHeadVal [MergeDuplicates, IgnoreDuplicates] ++
mapToHeadVal [Full, None, HeadersOnly] ++
mapToHeadVal [SingleObject] ++
mapToHeadVal [ExactCount, PlannedCount, EstimatedCount] ++
mapToHeadVal [Commit, Rollback] ++
mapToHeadVal [ApplyDefaults, ApplyNulls] ++
mapToHeadVal [Strict, Lenient]
prefHeaders = filter ((==) HTTP.hPrefer . fst) headers
prefs = fmap BS.strip . concatMap (BS.split ',' . snd) $ prefHeaders
timezonePref = listToMaybe $ mapMaybe (BS.stripPrefix "timezone=") prefs
isTimezonePrefAccepted = (S.member <$> timezonePref <*> pure acceptedTzNames) == Just True
checkPrefs p = p `notElem` acceptedPrefs && not isTimezonePrefAccepted
parsePrefs :: ToHeaderValue a => [a] -> Maybe a
parsePrefs vals =
head $ mapMaybe (flip Map.lookup $ prefMap vals) prefs
@@ -130,7 +164,7 @@ fromHeaders allowTxEndOverride headers =
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
prefAppliedHeader :: Preferences -> Maybe HTTP.Header
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing } =
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone } =
if null prefsVals
then Nothing
else Just (HTTP.hPreferenceApplied, combined)
@@ -143,6 +177,8 @@ prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferPar
, toHeaderValue <$> preferParameters
, toHeaderValue <$> preferCount
, toHeaderValue <$> preferTransaction
, toHeaderValue <$> preferHandling
, toHeaderValue <$> preferTimezone
]
-- |
@@ -158,6 +194,7 @@ class ToHeaderValue a where
data PreferResolution
= MergeDuplicates
| IgnoreDuplicates
deriving Eq
instance ToHeaderValue PreferResolution where
toHeaderValue MergeDuplicates = "resolution=merge-duplicates"
@@ -223,3 +260,21 @@ data PreferMissing
instance ToHeaderValue PreferMissing where
toHeaderValue ApplyDefaults = "missing=default"
toHeaderValue ApplyNulls = "missing=null"
-- |
-- Handling of unrecognised preferences
data PreferHandling
= Strict -- ^ Throw error on unrecognised preferences
| Lenient -- ^ Ignore unrecognised preferences
deriving Eq
instance ToHeaderValue PreferHandling where
toHeaderValue Strict = "handling=strict"
toHeaderValue Lenient = "handling=lenient"
-- |
-- Change timezone
newtype PreferTimezone = PreferTimezone ByteString
instance ToHeaderValue PreferTimezone where
toHeaderValue (PreferTimezone tz) = "timezone=" <> tz
+74 -25
View File
@@ -31,8 +31,8 @@ import Data.Tree (Tree (..))
import Text.Parsec.Error (errorMessages,
showErrorMessages)
import Text.ParserCombinators.Parsec (GenParser, ParseError, Parser,
anyChar, between, char, digit,
eof, errorPos, letter,
anyChar, between, char, choice,
digit, eof, errorPos, letter,
lookAhead, many1, noneOf,
notFollowedBy, oneOf,
optionMaybe, sepBy, sepBy1,
@@ -43,7 +43,8 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
rangeOffset, restrictRange)
import PostgREST.SchemaCache.Identifiers (FieldName)
import PostgREST.ApiRequest.Types (EmbedParam (..), EmbedPath, Field,
import PostgREST.ApiRequest.Types (AggregateFunction (..),
EmbedParam (..), EmbedPath, Field,
Filter (..), FtsOperator (..),
Hint, JoinType (..),
JsonOperand (..),
@@ -58,7 +59,7 @@ import PostgREST.ApiRequest.Types (EmbedParam (..), EmbedPath, Field,
SimpleOperator (..), SingleVal,
TrileanVal (..))
import Protolude hiding (try)
import Protolude hiding (Sum, try)
data QueryParams =
QueryParams
@@ -99,7 +100,7 @@ data QueryParams =
-- 'select' is a reserved parameter that selects the fields to be returned:
--
-- >>> qsSelect <$> parse False "select=name,location"
-- Right [Node {rootLabel = SelectField {selField = ("name",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectField {selField = ("location",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]
-- Right [Node {rootLabel = SelectField {selField = ("name",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectField {selField = ("location",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]
--
-- Filters are parameters whose value contains an operator, separated by a '.' from its value:
--
@@ -282,16 +283,16 @@ pTreePath = do
-- Parse select= into a Forest of SelectItems
--
-- >>> P.parse pFieldForest "" "id"
-- Right [Node {rootLabel = SelectField {selField = ("id",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]
-- Right [Node {rootLabel = SelectField {selField = ("id",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]
--
-- >>> P.parse pFieldForest "" "client(id)"
-- Right [Node {rootLabel = SelectRelation {selRelation = "client", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("id",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
-- Right [Node {rootLabel = SelectRelation {selRelation = "client", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("id",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
--
-- >>> P.parse pFieldForest "" "*,client(*,nested(*))"
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "client", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "nested", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]}]
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "client", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SelectRelation {selRelation = "nested", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]}]}]
--
-- >>> P.parse pFieldForest "" "*,...client(*),other(*)"
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SpreadRelation {selRelation = "client", selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]},Node {rootLabel = SelectRelation {selRelation = "other", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
-- Right [Node {rootLabel = SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []},Node {rootLabel = SpreadRelation {selRelation = "client", selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]},Node {rootLabel = SelectRelation {selRelation = "other", selAlias = Nothing, selHint = Nothing, selJoinType = Nothing}, subForest = [Node {rootLabel = SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing}, subForest = []}]}]
--
-- >>> P.parse pFieldForest "" ""
-- Right []
@@ -299,7 +300,7 @@ pTreePath = do
-- >>> P.parse pFieldForest "" "id,clients(name[])"
-- Left (line 1, column 16):
-- unexpected '['
-- expecting letter, digit, "-", "->>", "->", "::", ")", "," or end of input
-- expecting letter, digit, "-", "->>", "->", "::", ".", ")", "," or end of input
--
-- >>> P.parse pFieldForest "" "data->>-78xy"
-- Left (line 1, column 11):
@@ -349,8 +350,12 @@ pFieldForest = pFieldTree `sepBy` lexeme (char ',')
pFieldName :: Parser Text
pFieldName =
pQuotedValue <|>
T.intercalate "-" . map toS <$> (pIdentifier `sepBy1` dash) <?>
sepByDash pIdentifier <?>
"field name (* or [a..z0..9_$])"
sepByDash :: Parser Text -> Parser Text
sepByDash fieldIdent =
T.intercalate "-" . map toS <$> (fieldIdent `sepBy1` dash)
where
isDash :: GenParser Char st ()
isDash = try ( char '-' >> notFollowedBy (char '>') )
@@ -363,12 +368,18 @@ pFieldName =
-- >>> P.parse pJsonPath "" "->text"
-- Right [JArrow {jOp = JKey {jVal = "text"}}]
--
-- >>> P.parse pJsonPath "" "->!@#$%^&*_a"
-- Right [JArrow {jOp = JKey {jVal = "!@#$%^&*_a"}}]
--
-- >>> P.parse pJsonPath "" "->1"
-- Right [JArrow {jOp = JIdx {jVal = "+1"}}]
--
-- >>> P.parse pJsonPath "" "->>text"
-- Right [J2Arrow {jOp = JKey {jVal = "text"}}]
--
-- >>> P.parse pJsonPath "" "->>!@#$%^&*_a"
-- Right [J2Arrow {jOp = JKey {jVal = "!@#$%^&*_a"}}]
--
-- >>> P.parse pJsonPath "" "->>1"
-- Right [J2Arrow {jOp = JIdx {jVal = "+1"}}]
--
@@ -405,7 +416,7 @@ pJsonPath = many pJsonOperation
try (string "->" $> JArrow)
pJsonOperand =
let pJKey = JKey . toS <$> pFieldName
let pJKey = JKey . toS <$> pJsonKeyName
pJIdx = JIdx . toS <$> ((:) <$> P.option '+' (char '-') <*> many1 digit) <* pEnd
pEnd = try (void $ lookAhead (string "->")) <|>
try (void $ lookAhead (string "::")) <|>
@@ -414,6 +425,15 @@ pJsonPath = many pJsonOperation
try eof in
try pJIdx <|> try pJKey
pJsonKeyName :: Parser Text
pJsonKeyName =
pQuotedValue <|>
sepByDash pJsonKeyIdentifier <?>
"any non reserved character different from: .,>()"
pJsonKeyIdentifier :: Parser Text
pJsonKeyIdentifier = T.strip . toS <$> many1 (noneOf "(-:.,>)")
pField :: Parser Field
pField = lexeme $ (,) <$> pFieldName <*> P.option [] pJsonPath
@@ -452,35 +472,40 @@ pRelationSelect :: Parser SelectItem
pRelationSelect = lexeme $ do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
name <- pFieldName
guard (name /= "count")
(hint, jType) <- pEmbedParams
try (void $ lookAhead (string "("))
return $ SelectRelation name alias hint jType
-- |
-- Parse regular fields in select
--
-- >>> P.parse pFieldSelect "" "name"
-- Right (SelectField {selField = ("name",[]), selCast = Nothing, selAlias = Nothing})
-- Right (SelectField {selField = ("name",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing})
--
-- >>> P.parse pFieldSelect "" "name->jsonpath"
-- Right (SelectField {selField = ("name",[JArrow {jOp = JKey {jVal = "jsonpath"}}]), selCast = Nothing, selAlias = Nothing})
-- Right (SelectField {selField = ("name",[JArrow {jOp = JKey {jVal = "jsonpath"}}]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing})
--
-- >>> P.parse pFieldSelect "" "name::cast"
-- Right (SelectField {selField = ("name",[]), selCast = Just "cast", selAlias = Nothing})
-- Right (SelectField {selField = ("name",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Just "cast", selAlias = Nothing})
--
-- >>> P.parse pFieldSelect "" "alias:name"
-- Right (SelectField {selField = ("name",[]), selCast = Nothing, selAlias = Just "alias"})
-- Right (SelectField {selField = ("name",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Just "alias"})
--
-- >>> P.parse pFieldSelect "" "alias:name->jsonpath::cast"
-- Right (SelectField {selField = ("name",[JArrow {jOp = JKey {jVal = "jsonpath"}}]), selCast = Just "cast", selAlias = Just "alias"})
-- Right (SelectField {selField = ("name",[JArrow {jOp = JKey {jVal = "jsonpath"}}]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Just "cast", selAlias = Just "alias"})
--
-- >>> P.parse pFieldSelect "" "alias:name->!@#$%^&*_a::cast"
-- Right (SelectField {selField = ("name",[JArrow {jOp = JKey {jVal = "!@#$%^&*_a"}}]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Just "cast", selAlias = Just "alias"})
--
-- >>> P.parse pFieldSelect "" "*"
-- Right (SelectField {selField = ("*",[]), selCast = Nothing, selAlias = Nothing})
-- Right (SelectField {selField = ("*",[]), selAggregateFunction = Nothing, selAggregateCast = Nothing, selCast = Nothing, selAlias = Nothing})
--
-- >>> P.parse pFieldSelect "" "name!hint"
-- Left (line 1, column 5):
-- unexpected '!'
-- expecting letter, digit, "-", "->>", "->", "::", ")", "," or end of input
-- expecting letter, digit, "-", "->>", "->", "::", ".", ")", "," or end of input
--
-- >>> P.parse pFieldSelect "" "*!hint"
-- Left (line 1, column 2):
@@ -495,18 +520,36 @@ pFieldSelect :: Parser SelectItem
pFieldSelect = lexeme $ try (do
s <- pStar
pEnd
return $ SelectField (s, []) Nothing Nothing)
<|> do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
fld <- pField
cast' <- optionMaybe (string "::" *> pIdentifier)
return $ SelectField (s, []) Nothing Nothing Nothing Nothing)
<|> try (do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
_ <- string "count()"
aggCast' <- optionMaybe (string "::" *> pIdentifier)
pEnd
return $ SelectField fld (toS <$> cast') alias
return $ SelectField ("*", []) (Just Count) (toS <$> aggCast') Nothing alias)
<|> do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
fld <- pField
cast' <- optionMaybe (string "::" *> pIdentifier)
agg <- optionMaybe (try (char '.' *> pAggregation <* string "()"))
aggCast' <- optionMaybe (string "::" *> pIdentifier)
pEnd
return $ SelectField fld agg (toS <$> aggCast') (toS <$> cast') alias
where
pEnd = try (void $ lookAhead (string ")")) <|>
try (void $ lookAhead (string ",")) <|>
try eof
pStar = string "*" $> "*"
pAggregation = choice
[ string "sum" $> Sum
, string "avg" $> Avg
, string "count" $> Count
-- Using 'try' for "min" and "max" to allow backtracking.
-- This is necessary because both start with the same character 'm',
-- and without 'try', a partial match on "max" would prevent "min" from being tried.
, try (string "max") $> Max
, try (string "min") $> Min
]
-- |
@@ -657,9 +700,15 @@ pDelimiter = char '.' <?> "delimiter (.)"
-- >>> P.parse pOrder "" "json_col->key.asc.nullslast"
-- Right [OrderTerm {otTerm = ("json_col",[JArrow {jOp = JKey {jVal = "key"}}]), otDirection = Just OrderAsc, otNullOrder = Just OrderNullsLast}]
--
-- >>> P.parse pOrder "" "json_col->!@#$%^&*_a.asc.nullslast"
-- Right [OrderTerm {otTerm = ("json_col",[JArrow {jOp = JKey {jVal = "!@#$%^&*_a"}}]), otDirection = Just OrderAsc, otNullOrder = Just OrderNullsLast}]
--
-- >>> P.parse pOrder "" "clients(json_col->key).desc.nullsfirst"
-- Right [OrderRelationTerm {otRelation = "clients", otRelTerm = ("json_col",[JArrow {jOp = JKey {jVal = "key"}}]), otDirection = Just OrderDesc, otNullOrder = Just OrderNullsFirst}]
--
-- >>> P.parse pOrder "" "clients(json_col->!@#$%^&*_a).desc.nullsfirst"
-- Right [OrderRelationTerm {otRelation = "clients", otRelTerm = ("json_col",[JArrow {jOp = JKey {jVal = "!@#$%^&*_a"}}]), otDirection = Just OrderDesc, otNullOrder = Just OrderNullsFirst}]
--
-- >>> P.parse pOrder "" "clients(name,id)"
-- Left (line 1, column 8):
-- unexpected '('
+20 -7
View File
@@ -1,6 +1,7 @@
{-# LANGUAGE DuplicateRecordFields #-}
module PostgREST.ApiRequest.Types
( Alias
( AggregateFunction(..)
, Alias
, Cast
, Depth
, EmbedParam(..)
@@ -42,12 +43,14 @@ import PostgREST.SchemaCache.Routine (Routine (..))
import Protolude
-- | The value in `/tbl?select=alias:field::cast`
-- | The value in `/tbl?select=alias:field.aggregateFunction()::cast`
data SelectItem
= SelectField
{ selField :: Field
, selCast :: Maybe Cast
, selAlias :: Maybe Alias
{ selField :: Field
, selAggregateFunction :: Maybe AggregateFunction
, selAggregateCast :: Maybe Cast
, selCast :: Maybe Cast
, selAlias :: Maybe Alias
}
-- | The value in `/tbl?select=alias:another_tbl(*)`
| SelectRelation
@@ -65,12 +68,13 @@ data SelectItem
deriving (Eq, Show)
data ApiRequestError
= AmbiguousRelBetween Text Text [Relationship]
= AggregatesNotAllowed
| AmbiguousRelBetween Text Text [Relationship]
| AmbiguousRpc [Routine]
| BinaryFieldError MediaType
| MediaTypeError [ByteString]
| InvalidBody ByteString
| InvalidFilters
| InvalidPreferences [ByteString]
| InvalidRange RangeError
| InvalidRpcMethod ByteString
| LimitNoOrderError
@@ -86,6 +90,12 @@ data ApiRequestError
| UnacceptableSchema [Text]
| UnsupportedMethod ByteString
| ColumnNotFound Text Text
| GucHeadersError
| GucStatusError
| OffLimitsChangesError Int64 Integer
| PutMatchingPkError
| SingularityError Integer
| PGRSTParseError
deriving Show
data QPError = QPError Text Text
@@ -128,6 +138,9 @@ type Cast = Text
type Alias = Text
type Hint = Text
data AggregateFunction = Sum | Avg | Max | Min | Count
deriving (Show, Eq)
data EmbedParam
-- | Disambiguates an embedding operation when there's multiple relationships
-- between two tables. Can be the name of a foreign key constraint, column
+119 -77
View File
@@ -9,11 +9,10 @@ Some of its functionality includes:
- Producing HTTP Headers according to RFCs.
- Content Negotiation
-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
module PostgREST.App
( SignalHandlerInstaller
, SocketRunner
, postgrest
( postgrest
, run
) where
@@ -24,7 +23,6 @@ import Data.Maybe (fromJust)
import Data.String (IsString (..))
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
setServerName)
import System.Posix.Types (FileMode)
import qualified Data.HashMap.Strict as HM
import qualified Data.Text.Encoding as T
@@ -43,32 +41,38 @@ import qualified PostgREST.Logger as Logger
import qualified PostgREST.Plan as Plan
import qualified PostgREST.Query as Query
import qualified PostgREST.Response as Response
import qualified PostgREST.Unix as Unix (installSignalHandlers)
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
Mutation (..), Target (..))
import PostgREST.AppState (AppState)
import PostgREST.Auth (AuthResult (..))
import PostgREST.Config (AppConfig (..))
import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.Error (Error)
import PostgREST.Query (DbHandler)
import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.SchemaCache.Routine (Routine (..))
import PostgREST.Version (docsVersion, prettyVersion)
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
Mutation (..), Target (..))
import PostgREST.AppState (AppState)
import PostgREST.Auth (AuthResult (..))
import PostgREST.Config (AppConfig (..))
import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.Error (Error)
import PostgREST.Query (DbHandler)
import PostgREST.Response.Performance (ServerTiming (..),
serverTimingHeader)
import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.SchemaCache.Routine (Routine (..))
import PostgREST.Version (docsVersion, prettyVersion)
import Protolude hiding (Handler)
import qualified Data.ByteString.Char8 as BS
import qualified Data.List as L
import qualified Network.HTTP.Types as HTTP
import qualified Network.Socket as NS
import Protolude hiding (Handler)
import System.TimeIt (timeItT)
type Handler = ExceptT Error
type SignalHandlerInstaller = AppState -> IO()
run :: AppState -> IO ()
run appState = do
AppState.logWithZTime appState $ "Starting PostgREST " <> T.decodeUtf8 prettyVersion <> "..."
type SocketRunner = Warp.Settings -> Wai.Application -> FileMode -> FilePath -> IO()
run :: SignalHandlerInstaller -> Maybe SocketRunner -> AppState -> IO ()
run installHandlers maybeRunWithSocket appState = do
conf@AppConfig{..} <- AppState.getConfig appState
AppState.connectionWorker appState -- Loads the initial SchemaCache
installHandlers appState
Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.connectionWorker appState) (AppState.reReadConfig False appState)
-- reload schema cache + config on NOTIFY
AppState.runListener conf appState
@@ -76,19 +80,14 @@ run installHandlers maybeRunWithSocket appState = do
let app = postgrest conf appState (AppState.connectionWorker appState)
case configServerUnixSocket of
Just socket ->
-- run the postgrest application with user defined socket. Only for UNIX systems
case maybeRunWithSocket of
Just runWithSocket -> do
AppState.logWithZTime appState $ "Listening on unix socket " <> show socket
runWithSocket (serverSettings conf) app configServerUnixSocketMode socket
Nothing ->
panic "Cannot run with unix socket on non-unix platforms."
Nothing ->
do
AppState.logWithZTime appState $ "Listening on port " <> show configServerPort
Warp.runSettings (serverSettings conf) app
what <- case configServerUnixSocket of
Just path -> pure $ "unix socket " <> show path
Nothing -> do
port <- NS.socketPort $ AppState.getSocketREST appState
pure $ "port " <> show port
AppState.logWithZTime appState $ "Listening on " <> what
Warp.runSettingsSocket (serverSettings conf) (AppState.getSocketREST appState) app
serverSettings :: AppConfig -> Warp.Settings
serverSettings AppConfig{..} =
@@ -100,8 +99,8 @@ serverSettings AppConfig{..} =
-- | PostgREST application
postgrest :: AppConfig -> AppState.AppState -> IO () -> Wai.Application
postgrest conf appState connWorker =
Response.traceHeaderMiddleware conf .
Cors.middleware .
traceHeaderMiddleware conf .
Cors.middleware (configServerCorsAllowedOrigins conf) .
Auth.middleware appState .
Logger.middleware (configLogLevel conf) $
-- fromJust can be used, because the auth middleware will **always** add
@@ -122,10 +121,10 @@ postgrest conf appState connWorker =
-- Launch the connWorker when the connection is down. The postgrest
-- function can respond successfully (with a stale schema cache) before
-- the connWorker is done.
when (Response.isServiceUnavailable response) connWorker
when (isServiceUnavailable response) connWorker
resp <- do
delay <- AppState.getRetryNextIn appState
return $ Response.addRetryHint delay response
return $ addRetryHint delay response
respond resp
postgrestResponse
@@ -146,17 +145,19 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@
body <- lift $ Wai.strictRequestBody req
apiRequest <-
liftEither . mapLeft Error.ApiRequestError $
ApiRequest.userApiRequest conf req body
(parseTime, apiRequest) <-
calcTiming configServerTimingEnabled $
liftEither . mapLeft Error.ApiRequestError $
ApiRequest.userApiRequest conf req body sCache
handleRequest authResult conf appState (Just authRole /= configDbAnonRole) configDbPreparedStatements pgVer apiRequest sCache
let jwtTime = if configServerTimingEnabled then Auth.getJwtDur req else Nothing
handleRequest authResult conf appState (Just authRole /= configDbAnonRole) configDbPreparedStatements pgVer apiRequest sCache jwtTime parseTime
runDbHandler :: AppState.AppState -> SQL.IsolationLevel -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
runDbHandler appState isoLvl mode authenticated prepared handler = do
runDbHandler :: AppState.AppState -> AppConfig -> SQL.IsolationLevel -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
runDbHandler appState config isoLvl mode authenticated prepared handler = do
dbResp <- lift $ do
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction
AppState.usePool appState . transaction isoLvl mode $ runExceptT handler
AppState.usePool appState config . transaction isoLvl mode $ runExceptT handler
resp <-
liftEither . mapLeft Error.PgErr $
@@ -164,52 +165,63 @@ runDbHandler appState isoLvl mode authenticated prepared handler = do
liftEither resp
handleRequest :: AuthResult -> AppConfig -> AppState.AppState -> Bool -> Bool -> PgVersion -> ApiRequest -> SchemaCache -> Handler IO Wai.Response
handleRequest AuthResult{..} conf appState authenticated prepared pgVer apiReq@ApiRequest{..} sCache =
handleRequest :: AuthResult -> AppConfig -> AppState.AppState -> Bool -> Bool -> PgVersion -> ApiRequest -> SchemaCache -> Maybe Double -> Maybe Double -> Handler IO Wai.Response
handleRequest AuthResult{..} conf appState authenticated prepared pgVer apiReq@ApiRequest{..} sCache jwtTime parseTime =
case (iAction, iTarget) of
(ActionRead headersOnly, TargetIdent identifier) -> do
wrPlan <- liftEither $ Plan.wrappedReadPlan identifier conf sCache apiReq
resultSet <- runQuery roleIsoLvl (Plan.wrTxMode wrPlan) $ Query.readQuery wrPlan conf apiReq
return $ Response.readResponse headersOnly identifier apiReq resultSet
(planTime', wrPlan) <- withTiming $ liftEither $ Plan.wrappedReadPlan identifier conf sCache apiReq
(txTime', resultSet) <- withTiming $ runQuery roleIsoLvl Nothing (Plan.wrTxMode wrPlan) $ Query.readQuery wrPlan conf apiReq
(respTime', pgrst) <- withTiming $ liftEither $ Response.readResponse wrPlan headersOnly identifier apiReq resultSet
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
(ActionMutate MutationCreate, TargetIdent identifier) -> do
mrPlan <- liftEither $ Plan.mutateReadPlan MutationCreate apiReq identifier conf sCache
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.createQuery mrPlan apiReq conf
return $ Response.createResponse identifier mrPlan apiReq resultSet
(planTime', mrPlan) <- withTiming $ liftEither $ Plan.mutateReadPlan MutationCreate apiReq identifier conf sCache
(txTime', resultSet) <- withTiming $ runQuery roleIsoLvl Nothing (Plan.mrTxMode mrPlan) $ Query.createQuery mrPlan apiReq conf
(respTime', pgrst) <- withTiming $ liftEither $ Response.createResponse identifier mrPlan apiReq resultSet
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
(ActionMutate MutationUpdate, TargetIdent identifier) -> do
mrPlan <- liftEither $ Plan.mutateReadPlan MutationUpdate apiReq identifier conf sCache
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.updateQuery mrPlan apiReq conf
return $ Response.updateResponse apiReq resultSet
(planTime', mrPlan) <- withTiming $ liftEither $ Plan.mutateReadPlan MutationUpdate apiReq identifier conf sCache
(txTime', resultSet) <- withTiming $ runQuery roleIsoLvl Nothing (Plan.mrTxMode mrPlan) $ Query.updateQuery mrPlan apiReq conf
(respTime', pgrst) <- withTiming $ liftEither $ Response.updateResponse mrPlan apiReq resultSet
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
(ActionMutate MutationSingleUpsert, TargetIdent identifier) -> do
mrPlan <- liftEither $ Plan.mutateReadPlan MutationSingleUpsert apiReq identifier conf sCache
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.singleUpsertQuery mrPlan apiReq conf
return $ Response.singleUpsertResponse apiReq resultSet
(planTime', mrPlan) <- withTiming $ liftEither $ Plan.mutateReadPlan MutationSingleUpsert apiReq identifier conf sCache
(txTime', resultSet) <- withTiming $ runQuery roleIsoLvl Nothing (Plan.mrTxMode mrPlan) $ Query.singleUpsertQuery mrPlan apiReq conf
(respTime', pgrst) <- withTiming $ liftEither $ Response.singleUpsertResponse mrPlan apiReq resultSet
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
(ActionMutate MutationDelete, TargetIdent identifier) -> do
mrPlan <- liftEither $ Plan.mutateReadPlan MutationDelete apiReq identifier conf sCache
resultSet <- runQuery roleIsoLvl (Plan.mrTxMode mrPlan) $ Query.deleteQuery mrPlan apiReq conf
return $ Response.deleteResponse apiReq resultSet
(planTime', mrPlan) <- withTiming $ liftEither $ Plan.mutateReadPlan MutationDelete apiReq identifier conf sCache
(txTime', resultSet) <- withTiming $ runQuery roleIsoLvl Nothing (Plan.mrTxMode mrPlan) $ Query.deleteQuery mrPlan apiReq conf
(respTime', pgrst) <- withTiming $ liftEither $ Response.deleteResponse mrPlan apiReq resultSet
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
(ActionInvoke invMethod, TargetProc identifier _) -> do
cPlan <- liftEither $ Plan.callReadPlan identifier conf sCache apiReq invMethod
resultSet <- runQuery (fromMaybe roleIsoLvl $ pdIsoLvl (Plan.crProc cPlan))(Plan.crTxMode cPlan) $ Query.invokeQuery (Plan.crProc cPlan) cPlan apiReq conf pgVer
return $ Response.invokeResponse invMethod (Plan.crProc cPlan) apiReq resultSet
(planTime', cPlan) <- withTiming $ liftEither $ Plan.callReadPlan identifier conf sCache apiReq invMethod
(txTime', resultSet) <- withTiming $ runQuery (fromMaybe roleIsoLvl $ pdIsoLvl (Plan.crProc cPlan)) (pdTimeout $ Plan.crProc cPlan) (Plan.crTxMode cPlan) $ Query.invokeQuery (Plan.crProc cPlan) cPlan apiReq conf pgVer
(respTime', pgrst) <- withTiming $ liftEither $ Response.invokeResponse cPlan invMethod (Plan.crProc cPlan) apiReq resultSet
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
(ActionInspect headersOnly, TargetDefaultSpec tSchema) -> do
oaiResult <- runQuery roleIsoLvl Plan.inspectPlanTxMode $ Query.openApiQuery sCache pgVer conf tSchema
return $ Response.openApiResponse (T.decodeUtf8 prettyVersion, docsVersion) headersOnly oaiResult conf sCache iSchema iNegotiatedByProfile
(planTime', iPlan) <- withTiming $ liftEither $ Plan.inspectPlan apiReq
(txTime', oaiResult) <- withTiming $ runQuery roleIsoLvl Nothing (Plan.ipTxmode iPlan) $ Query.openApiQuery sCache pgVer conf tSchema
(respTime', pgrst) <- withTiming $ liftEither $ Response.openApiResponse (T.decodeUtf8 prettyVersion, docsVersion) headersOnly oaiResult conf sCache iSchema iNegotiatedByProfile
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' txTime' respTime') pgrst
(ActionInfo, TargetIdent identifier) ->
return $ Response.infoIdentResponse identifier sCache
(ActionInfo, TargetIdent identifier) -> do
(respTime', pgrst) <- withTiming $ liftEither $ Response.infoIdentResponse identifier sCache
return $ pgrstResponse (ServerTiming jwtTime parseTime Nothing Nothing respTime') pgrst
(ActionInfo, TargetProc identifier _) -> do
cPlan <- liftEither $ Plan.callReadPlan identifier conf sCache apiReq ApiRequest.InvHead
return $ Response.infoProcResponse (Plan.crProc cPlan)
(planTime', cPlan) <- withTiming $ liftEither $ Plan.callReadPlan identifier conf sCache apiReq ApiRequest.InvHead
(respTime', pgrst) <- withTiming $ liftEither $ Response.infoProcResponse (Plan.crProc cPlan)
return $ pgrstResponse (ServerTiming jwtTime parseTime planTime' Nothing respTime') pgrst
(ActionInfo, TargetDefaultSpec _) ->
return Response.infoRootResponse
(ActionInfo, TargetDefaultSpec _) -> do
(respTime', pgrst) <- withTiming $ liftEither Response.infoRootResponse
return $ pgrstResponse (ServerTiming jwtTime parseTime Nothing Nothing respTime') pgrst
_ ->
-- This is unreachable as the ApiRequest.hs rejects it before
@@ -218,8 +230,38 @@ handleRequest AuthResult{..} conf appState authenticated prepared pgVer apiReq@A
where
roleSettings = fromMaybe mempty (HM.lookup authRole $ configRoleSettings conf)
roleIsoLvl = HM.findWithDefault SQL.ReadCommitted authRole $ configRoleIsoLvl conf
runQuery isoLvl mode query =
runDbHandler appState isoLvl mode authenticated prepared $ do
Query.setPgLocals conf authClaims authRole (HM.toList roleSettings) apiReq pgVer
runQuery isoLvl timeout mode query =
runDbHandler appState conf isoLvl mode authenticated prepared $ do
Query.setPgLocals conf authClaims authRole (HM.toList roleSettings) apiReq timeout
Query.runPreReq conf
query
pgrstResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response
pgrstResponse timing (Response.PgrstResponse st hdrs bod) = Wai.responseLBS st (hdrs ++ ([serverTimingHeader timing | configServerTimingEnabled conf])) bod
withTiming = calcTiming $ configServerTimingEnabled conf
calcTiming :: Bool -> Handler IO a -> Handler IO (Maybe Double, a)
calcTiming timingEnabled f = if timingEnabled
then do
(t, r) <- timeItT f
pure (Just t, r)
else do
r <- f
pure (Nothing, r)
traceHeaderMiddleware :: AppConfig -> Wai.Middleware
traceHeaderMiddleware AppConfig{configServerTraceHeader} app req respond =
case configServerTraceHeader of
Nothing -> app req respond
Just hdr ->
let hdrVal = L.lookup hdr $ Wai.requestHeaders req in
app req (respond . Wai.mapResponseHeaders ([(hdr, fromMaybe mempty hdrVal)] ++))
addRetryHint :: Int -> Wai.Response -> Wai.Response
addRetryHint delay response = do
let h = ("Retry-After", BS.pack $ show delay)
Wai.mapResponseHeaders (\hs -> if isServiceUnavailable response then h:hs else hs) response
isServiceUnavailable :: Wai.Response -> Bool
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
+107 -23
View File
@@ -4,6 +4,7 @@
module PostgREST.AppState
( AppState
, AuthResult(..)
, destroy
, getConfig
, getSchemaCache
@@ -12,7 +13,11 @@ module PostgREST.AppState
, getPgVersion
, getRetryNextIn
, getTime
, getJwtCache
, getSocketREST
, getSocketAdmin
, init
, initSockets
, initWithPool
, logWithZTime
, putSchemaCache
@@ -24,15 +29,21 @@ module PostgREST.AppState
, runListener
) where
import qualified Data.Aeson as JSON
import qualified Data.Aeson.KeyMap as KM
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Cache as C
import Data.Either.Combinators (whenLeft)
import qualified Data.Text as T (unpack)
import qualified Data.Text.Encoding as T
import Hasql.Connection (acquire)
import qualified Hasql.Notifications as SQL
import qualified Hasql.Pool as SQL
import qualified Hasql.Session as SQL
import qualified Hasql.Transaction.Sessions as SQL
import qualified Network.HTTP.Types.Status as HTTP
import qualified Network.Socket as NS
import qualified PostgREST.Error as Error
import PostgREST.Version (prettyVersion)
@@ -48,6 +59,7 @@ import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
import Data.Time.Clock (UTCTime, getCurrentTime)
import PostgREST.Config (AppConfig (..),
LogLevel (..),
addFallbackAppName,
readAppConfig)
import PostgREST.Config.Database (queryDbSettings,
@@ -58,9 +70,16 @@ import PostgREST.Config.PgVersion (PgVersion (..),
import PostgREST.SchemaCache (SchemaCache,
querySchemaCache)
import PostgREST.SchemaCache.Identifiers (dumpQi)
import PostgREST.Unix (createAndBindDomainSocket)
import Data.Streaming.Network (bindPortTCP, bindRandomPortTCP)
import Data.String (IsString (..))
import Protolude
data AuthResult = AuthResult
{ authClaims :: KM.KeyMap JSON.Value
, authRole :: BS.ByteString
}
data AppState = AppState
-- | Database connection pool
@@ -87,15 +106,25 @@ data AppState = AppState
, stateRetryNextIn :: IORef Int
-- | Logs a pool error with a debounce
, debounceLogAcquisitionTimeout :: IO ()
-- | JWT Cache
, jwtCache :: C.Cache ByteString AuthResult
-- | Network socket for REST API
, stateSocketREST :: NS.Socket
-- | Network socket for the admin UI
, stateSocketAdmin :: Maybe NS.Socket
}
type AppSockets = (NS.Socket, Maybe NS.Socket)
init :: AppConfig -> IO AppState
init conf = do
pool <- initPool conf
initWithPool pool conf
(sock, adminSock) <- initSockets conf
state' <- initWithPool (sock, adminSock) pool conf
pure state' { stateSocketREST = sock, stateSocketAdmin = adminSock }
initWithPool :: SQL.Pool -> AppConfig -> IO AppState
initWithPool pool conf = do
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> IO AppState
initWithPool (sock, adminSock) pool conf = do
appState <- AppState pool
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
<*> newIORef Nothing
@@ -108,6 +137,9 @@ initWithPool pool conf = do
<*> myThreadId
<*> newIORef 0
<*> pure (pure ())
<*> C.newCache Nothing
<*> pure sock
<*> pure adminSock
debLogTimeout <-
@@ -131,6 +163,39 @@ initWithPool pool conf = do
destroy :: AppState -> IO ()
destroy = destroyPool
initSockets :: AppConfig -> IO AppSockets
initSockets AppConfig{..} = do
let
cfg'usp = configServerUnixSocket
cfg'uspm = configServerUnixSocketMode
cfg'host = configServerHost
cfg'port = configServerPort
cfg'adminport = configAdminServerPort
sock <- case cfg'usp of
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
-- but we need to have runtime error if we try to use it in Windows, not compile time error
Just path -> createAndBindDomainSocket path cfg'uspm
Nothing -> do
(_, sock) <-
if cfg'port /= 0
then do
sock <- bindPortTCP cfg'port (fromString $ T.unpack cfg'host)
pure (cfg'port, sock)
else do
-- explicitly bind to a random port, returning bound port number
(num, sock) <- bindRandomPortTCP (fromString $ T.unpack cfg'host)
pure (num, sock)
pure sock
adminSock <- case cfg'adminport of
Just adminPort -> do
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'host)
pure $ Just adminSock
Nothing -> pure Nothing
pure (sock, adminSock)
initPool :: AppConfig -> IO SQL.Pool
initPool AppConfig{..} =
SQL.acquire
@@ -141,12 +206,18 @@ initPool AppConfig{..} =
(toUtf8 $ addFallbackAppName prettyVersion configDbUri)
-- | Run an action with a database connection.
usePool :: AppState -> SQL.Session a -> IO (Either SQL.UsageError a)
usePool AppState{..} x = do
usePool :: AppState -> AppConfig -> SQL.Session a -> IO (Either SQL.UsageError a)
usePool appState@AppState{..} AppConfig{configLogLevel} x = do
res <- SQL.use statePool x
whenLeft res (\case
SQL.AcquisitionTimeoutUsageError -> debounceLogAcquisitionTimeout -- this can happen rapidly for many requests, so we debounce
_ -> pure ())
when (configLogLevel > LogCrit) $ do
whenLeft res (\case
SQL.AcquisitionTimeoutUsageError -> debounceLogAcquisitionTimeout -- this can happen rapidly for many requests, so we debounce
error
-- TODO We're using the 500 HTTP status for getting all internal db errors but there's no response here. We need a new intermediate type to not rely on the HTTP status.
| Error.status (Error.PgError False error) >= HTTP.status500 -> logPgrstError appState error
| otherwise -> pure ())
return res
-- | Flush the connection pool so that any future use of the pool will
@@ -188,6 +259,15 @@ putConfig = atomicWriteIORef . stateConf
getTime :: AppState -> IO UTCTime
getTime = stateGetTime
getJwtCache :: AppState -> C.Cache ByteString AuthResult
getJwtCache = jwtCache
getSocketREST :: AppState -> NS.Socket
getSocketREST = stateSocketREST
getSocketAdmin :: AppState -> Maybe NS.Socket
getSocketAdmin = stateSocketAdmin
-- | Log to stderr with local time
logWithZTime :: AppState -> Text -> IO ()
logWithZTime appState txt = do
@@ -229,7 +309,7 @@ loadSchemaCache appState = do
conf@AppConfig{..} <- getConfig appState
result <-
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
usePool appState . transaction SQL.ReadCommitted SQL.Read $
usePool appState conf . transaction SQL.ReadCommitted SQL.Read $
querySchemaCache conf
case result of
Left e -> do
@@ -272,17 +352,17 @@ internalConnectionWorker :: AppState -> IO ()
internalConnectionWorker appState = work
where
work = do
AppConfig{..} <- getConfig appState
logWithZTime appState $ "Starting PostgREST " <> T.decodeUtf8 prettyVersion <> "..."
config@AppConfig{..} <- getConfig appState
logWithZTime appState "Attempting to connect to the database..."
connected <- establishConnection appState
connected <- establishConnection appState config
case connected of
FatalConnectionError reason ->
-- Fatal error when connecting
logWithZTime appState reason >> killThread (getMainThreadId appState)
NotConnected ->
-- Unreachable because establishConnection will keep trying to connect
return ()
-- Unreachable because establishConnection will keep trying to connect, unless disable-recovery is turned on
unless configDbPoolAutomaticRecovery
$ logWithZTime appState "Automatic recovery disabled, exiting." >> killThread (getMainThreadId appState)
Connected actualPgVersion -> do
-- Procede with initialization
putPgVersion appState actualPgVersion
@@ -314,8 +394,8 @@ internalConnectionWorker appState = work
--
-- The connection tries are capped, but if the connection times out no error is
-- thrown, just 'False' is returned.
establishConnection :: AppState -> IO ConnectionStatus
establishConnection appState =
establishConnection :: AppState -> AppConfig -> IO ConnectionStatus
establishConnection appState config =
retrying retrySettings shouldRetry $
const $ flushPool appState >> getConnectionStatus
where
@@ -325,7 +405,7 @@ establishConnection appState =
getConnectionStatus :: IO ConnectionStatus
getConnectionStatus = do
pgVersion <- usePool appState $ queryPgVersion False -- No need to prepare the query here, as the connection might not be established
pgVersion <- usePool appState config $ queryPgVersion False -- No need to prepare the query here, as the connection might not be established
case pgVersion of
Left e -> do
logPgrstError appState e
@@ -344,9 +424,10 @@ establishConnection appState =
shouldRetry :: RetryStatus -> ConnectionStatus -> IO Bool
shouldRetry rs isConnSucc = do
AppConfig{..} <- getConfig appState
let
delay = fromMaybe 0 (rsPreviousDelay rs) `div` backoffMicroseconds
itShould = NotConnected == isConnSucc
itShould = NotConnected == isConnSucc && configDbPoolAutomaticRecovery
when itShould . logWithZTime appState $
"Attempting to reconnect to the database in "
<> (show delay::Text)
@@ -357,10 +438,11 @@ establishConnection appState =
-- | Re-reads the config plus config options from the db
reReadConfig :: Bool -> AppState -> IO ()
reReadConfig startingUp appState = do
AppConfig{..} <- getConfig appState
config@AppConfig{..} <- getConfig appState
pgVer <- getPgVersion appState
dbSettings <-
if configDbConfig then do
qDbSettings <- usePool appState $ queryDbSettings (dumpQi <$> configDbPreConfig) configDbPreparedStatements
qDbSettings <- usePool appState config $ queryDbSettings (dumpQi <$> configDbPreConfig) configDbPreparedStatements
case qDbSettings of
Left e -> do
logWithZTime appState
@@ -378,7 +460,7 @@ reReadConfig startingUp appState = do
pure mempty
(roleSettings, roleIsolationLvl) <-
if configDbConfig then do
rSettings <- usePool appState $ queryRoleSettings configDbPreparedStatements
rSettings <- usePool appState config $ queryRoleSettings pgVer configDbPreparedStatements
case rSettings of
Left e -> do
logWithZTime appState "An error ocurred when trying to query the role settings"
@@ -420,7 +502,7 @@ listener appState = do
waitListener appState
-- forkFinally allows to detect if the thread dies
void . flip forkFinally (handleFinally dbChannel) $ do
void . flip forkFinally (handleFinally dbChannel configDbPoolAutomaticRecovery) $ do
dbOrError <- acquire $ toUtf8 (addFallbackAppName prettyVersion configDbUri)
case dbOrError of
Right db -> do
@@ -431,7 +513,9 @@ listener appState = do
_ ->
die $ "Could not listen for notifications on the " <> dbChannel <> " channel"
where
handleFinally dbChannel _ = do
handleFinally _ False _ =
logWithZTime appState "Automatic recovery disabled, exiting." >> killThread (getMainThreadId appState)
handleFinally dbChannel True _ = do
-- if the thread dies, we try to recover
logWithZTime appState $ "Retrying listening for notifications on the " <> dbChannel <> " channel.."
putIsListenerOn appState False
+59 -12
View File
@@ -14,6 +14,7 @@ very simple authentication system inside the PostgreSQL database.
module PostgREST.Auth
( AuthResult (..)
, getResult
, getJwtDur
, getRole
, middleware
) where
@@ -25,6 +26,8 @@ import qualified Data.Aeson.KeyMap as KM
import qualified Data.Aeson.Types as JSON
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
import qualified Data.Cache as C
import qualified Data.Scientific as Sci
import qualified Data.Vault.Lazy as Vault
import qualified Data.Vector as V
import qualified Network.HTTP.Types.Header as HTTP
@@ -35,21 +38,20 @@ import Control.Lens (set)
import Control.Monad.Except (liftEither)
import Data.Either.Combinators (mapLeft)
import Data.List (lookup)
import Data.Time.Clock (UTCTime)
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import System.Clock (TimeSpec (..))
import System.IO.Unsafe (unsafePerformIO)
import System.TimeIt (timeItT)
import PostgREST.AppState (AppState, getConfig, getTime)
import PostgREST.AppState (AppState, AuthResult (..), getConfig,
getJwtCache, getTime)
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
import PostgREST.Error (Error (..))
import Protolude
data AuthResult = AuthResult
{ authClaims :: KM.KeyMap JSON.Value
, authRole :: BS.ByteString
}
-- | Receives the JWT secret and audience (from config) and a JWT and returns a
-- JSON object of JWT claims.
parseToken :: Monad m =>
@@ -102,14 +104,52 @@ middleware appState app req respond = do
conf <- getConfig appState
time <- getTime appState
let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
authResult <- runExceptT $
parseToken conf (LBS.fromStrict token) time >>=
parseClaims conf
let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
parseJwt = runExceptT $ parseToken conf (LBS.fromStrict token) time >>= parseClaims conf
-- If DbPlanEnabled -> calculate JWT validation time
-- If JwtCacheMaxLifetime -> cache JWT validation result
req' <- case (configServerTimingEnabled conf, configJwtCacheMaxLifetime conf) of
(True, 0) -> do
(dur, authResult) <- timeItT parseJwt
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
(True, maxLifetime) -> do
(dur, authResult) <- timeItT $ getJWTFromCache appState token maxLifetime parseJwt time
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
(False, 0) -> do
authResult <- parseJwt
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
(False, maxLifetime) -> do
authResult <- getJWTFromCache appState token maxLifetime parseJwt time
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
let req' = req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
app req' respond
-- | Used to retrieve and insert JWT to JWT Cache
getJWTFromCache :: AppState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
getJWTFromCache appState token maxLifetime parseJwt utc = do
checkCache <- C.lookup (getJwtCache appState) token
authResult <- maybe parseJwt (pure . Right) checkCache
case (authResult,checkCache) of
(Right res, Nothing) -> C.insert' (getJwtCache appState) (getTimeSpec res maxLifetime utc) token res
_ -> pure ()
return authResult
-- Used to extract JWT exp claim and add to JWT Cache
getTimeSpec :: AuthResult -> Int -> UTCTime -> Maybe TimeSpec
getTimeSpec res maxLifetime utc = do
let expireJSON = KM.lookup "exp" (authClaims res)
utcToSecs = floor . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds
sciToInt = fromMaybe 0 . Sci.toBoundedInteger
case expireJSON of
Just (JSON.Number seconds) -> Just $ TimeSpec (sciToInt seconds - utcToSecs utc) 0
_ -> Just $ TimeSpec (fromIntegral maxLifetime :: Int64) 0
authResultKey :: Vault.Key (Either Error AuthResult)
authResultKey = unsafePerformIO Vault.newKey
{-# NOINLINE authResultKey #-}
@@ -117,5 +157,12 @@ authResultKey = unsafePerformIO Vault.newKey
getResult :: Wai.Request -> Maybe (Either Error AuthResult)
getResult = Vault.lookup authResultKey . Wai.vault
jwtDurKey :: Vault.Key Double
jwtDurKey = unsafePerformIO Vault.newKey
{-# NOINLINE jwtDurKey #-}
getJwtDur :: Wai.Request -> Maybe Double
getJwtDur = Vault.lookup jwtDurKey . Wai.vault
getRole :: Wai.Request -> Maybe BS.ByteString
getRole req = authRole <$> (rightToMaybe =<< getResult req)
+15 -10
View File
@@ -29,8 +29,8 @@ import qualified PostgREST.Config as Config
import Protolude hiding (hPutStrLn)
main :: App.SignalHandlerInstaller -> Maybe App.SocketRunner -> CLI -> IO ()
main installSignalHandlers runAppWithSocket CLI{cliCommand, cliPath} = do
main :: CLI -> IO ()
main CLI{cliCommand, cliPath} = do
conf@AppConfig{..} <-
either panic identity <$> Config.readAppConfig mempty cliPath Nothing mempty mempty
@@ -45,7 +45,7 @@ main installSignalHandlers runAppWithSocket CLI{cliCommand, cliPath} = do
when configDbConfig $ AppState.reReadConfig True appState
putStr . Config.toText =<< AppState.getConfig appState
CmdDumpSchema -> putStrLn =<< dumpSchema appState
CmdRun -> App.run installSignalHandlers runAppWithSocket appState)
CmdRun -> App.run appState)
-- | Dump SchemaCache schema to JSON
dumpSchema :: AppState -> IO LBS.ByteString
@@ -53,7 +53,7 @@ dumpSchema appState = do
conf@AppConfig{..} <- AppState.getConfig appState
result <-
let transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction in
AppState.usePool appState $
AppState.usePool appState conf $
transaction SQL.ReadCommitted SQL.Read $
querySchemaCache conf
case result of
@@ -162,6 +162,9 @@ exampleConfigFile =
|## Time in seconds after which to recycle unused pool connections
|# db-pool-max-idletime = 30
|
|## Allow automatic database connection retrying
|# db-pool-automatic-recovery = true
|
|## Stored proc to exec immediately after auth
|# db-pre-request = "stored_proc_name"
|
@@ -188,10 +191,6 @@ exampleConfigFile =
|## https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
|db-uri = "postgresql://"
|
|## 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 new names (string without dashes, valid on all PostgreSQL versions) with json values.
|## For PostgreSQL v14 and up, this setting will be ignored.
|db-use-legacy-gucs = true
|
|# jwt-aud = "your_audience_claim"
|
|## Jspath to the role claim key
@@ -202,6 +201,9 @@ exampleConfigFile =
|# jwt-secret = "secret_with_at_least_32_characters"
|jwt-secret-is-base64 = false
|
|## Enables and set JWT Cache max lifetime, disables caching with 0
|# jwt-cache-max-lifetime = 0
|
|## Logging level, the admitted values are: crit, error, warn and info.
|log-level = "error"
|
@@ -212,12 +214,15 @@ exampleConfigFile =
|## Base url for the OpenAPI output
|openapi-server-proxy-uri = ""
|
|## Content types to produce raw output
|# raw-media-types="image/png, image/jpg"
|## Configurable CORS origins
|# server-cors-allowed-origins = ""
|
|server-host = "!4"
|server-port = 3000
|
|## Allow getting the request-response timing information through the `Server-Timing` header
|server-timing-enabled = false
|
|## Unix socket location
|## if specified it takes precedence over server-port
|# server-unix-socket = "/tmp/pgrst.sock"
+22 -8
View File
@@ -61,7 +61,6 @@ import PostgREST.Config.JSPath (JSPath, JSPathExp (..),
dumpJSPath, pRoleClaimKey)
import PostgREST.Config.Proxy (Proxy (..),
isMalformedProxyUri, toURI)
import PostgREST.MediaType (MediaType (..), toMime)
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi,
toQi)
@@ -70,6 +69,7 @@ import Protolude hiding (Proxy, toList)
data AppConfig = AppConfig
{ configAppSettings :: [(Text, Text)]
, configDbAggregates :: Bool
, configDbAnonRole :: Maybe BS.ByteString
, configDbChannel :: Text
, configDbChannelEnabled :: Bool
@@ -80,6 +80,7 @@ data AppConfig = AppConfig
, configDbPoolAcquisitionTimeout :: Int
, configDbPoolMaxLifetime :: Int
, configDbPoolMaxIdletime :: Int
, configDbPoolAutomaticRecovery :: Bool
, configDbPreRequest :: Maybe QualifiedIdentifier
, configDbPreparedStatements :: Bool
, configDbRootSpec :: Maybe QualifiedIdentifier
@@ -89,21 +90,22 @@ data AppConfig = AppConfig
, configDbTxAllowOverride :: Bool
, configDbTxRollbackAll :: Bool
, configDbUri :: Text
, configDbUseLegacyGucs :: Bool
, configFilePath :: Maybe FilePath
, configJWKS :: Maybe JWKSet
, configJwtAudience :: Maybe StringOrURI
, configJwtRoleClaimKey :: JSPath
, configJwtSecret :: Maybe BS.ByteString
, configJwtSecretIsBase64 :: Bool
, configJwtCacheMaxLifetime :: Int
, configLogLevel :: LogLevel
, configOpenApiMode :: OpenAPIMode
, configOpenApiSecurityActive :: Bool
, configOpenApiServerProxyUri :: Maybe Text
, configRawMediaTypes :: [MediaType]
, configServerCorsAllowedOrigins :: Maybe [Text]
, configServerHost :: Text
, configServerPort :: Int
, configServerTraceHeader :: Maybe (CI.CI BS.ByteString)
, configServerTimingEnabled :: Bool
, configServerUnixSocket :: Maybe FilePath
, configServerUnixSocketMode :: FileMode
, configAdminServerPort :: Maybe Int
@@ -113,6 +115,7 @@ data AppConfig = AppConfig
}
data LogLevel = LogCrit | LogError | LogWarn | LogInfo
deriving (Eq, Ord)
dumpLogLevel :: LogLevel -> Text
dumpLogLevel = \case
@@ -137,7 +140,8 @@ toText conf =
where
-- apply conf to all pgrst settings
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
[("db-anon-role", q . T.decodeUtf8 . fromMaybe "" . configDbAnonRole)
[("db-aggregates-enabled", T.toLower . show . configDbAggregates)
,("db-anon-role", q . T.decodeUtf8 . fromMaybe "" . configDbAnonRole)
,("db-channel", q . configDbChannel)
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
,("db-extra-search-path", q . T.intercalate "," . configDbExtraSearchPath)
@@ -147,6 +151,7 @@ toText conf =
,("db-pool-acquisition-timeout", show . configDbPoolAcquisitionTimeout)
,("db-pool-max-lifetime", show . configDbPoolMaxLifetime)
,("db-pool-max-idletime", show . configDbPoolMaxIdletime)
,("db-pool-automatic-recovery", T.toLower . show . configDbPoolAutomaticRecovery)
,("db-pre-request", q . maybe mempty dumpQi . configDbPreRequest)
,("db-prepared-statements", T.toLower . show . configDbPreparedStatements)
,("db-root-spec", q . maybe mempty dumpQi . configDbRootSpec)
@@ -155,19 +160,20 @@ toText conf =
,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig)
,("db-tx-end", q . showTxEnd)
,("db-uri", q . configDbUri)
,("db-use-legacy-gucs", T.toLower . show . configDbUseLegacyGucs)
,("jwt-aud", T.decodeUtf8 . LBS.toStrict . JSON.encode . maybe "" toJSON . configJwtAudience)
,("jwt-role-claim-key", q . T.intercalate mempty . fmap dumpJSPath . configJwtRoleClaimKey)
,("jwt-secret", q . T.decodeUtf8 . showJwtSecret)
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
,("jwt-cache-max-lifetime", show . configJwtCacheMaxLifetime)
,("log-level", q . dumpLogLevel . configLogLevel)
,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode)
,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive)
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
,("raw-media-types", q . T.decodeUtf8 . BS.intercalate "," . fmap toMime . configRawMediaTypes)
,("server-cors-allowed-origins", q . maybe "" (T.intercalate ",") . configServerCorsAllowedOrigins)
,("server-host", q . configServerHost)
,("server-port", show . configServerPort)
,("server-trace-header", q . T.decodeUtf8 . maybe mempty CI.original . configServerTraceHeader)
,("server-timing-enabled", T.toLower . show . configServerTimingEnabled)
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
,("server-unix-socket-mode", q . T.pack . showSocketMode)
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
@@ -229,6 +235,7 @@ parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> Rol
parser optPath env dbSettings roleSettings roleIsolationLvl =
AppConfig
<$> parseAppSettings "app.settings"
<*> (fromMaybe False <$> optBool "db-aggregates-enabled")
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
<*> (fromMaybe "pgrst" <$> optString "db-channel")
<*> (fromMaybe True <$> optBool "db-channel-enabled")
@@ -241,6 +248,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (fromMaybe 1800 <$> optInt "db-pool-max-lifetime")
<*> (fromMaybe 30 <$> optWithAlias (optInt "db-pool-timeout")
(optInt "db-pool-max-idletime"))
<*> (fromMaybe True <$> optBool "db-pool-automatic-recovery")
<*> (fmap toQi <$> optWithAlias (optString "db-pre-request")
(optString "pre-request"))
<*> (fromMaybe True <$> optBool "db-prepared-statements")
@@ -253,7 +261,6 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> parseTxEnd "db-tx-end" snd
<*> parseTxEnd "db-tx-end" fst
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
<*> (fromMaybe True <$> optBool "db-use-legacy-gucs")
<*> pure optPath
<*> pure Nothing
<*> parseJwtAudience "jwt-aud"
@@ -262,14 +269,16 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (fromMaybe False <$> optWithAlias
(optBool "jwt-secret-is-base64")
(optBool "secret-is-base64"))
<*> (fromMaybe 0 <$> optInt "jwt-cache-max-lifetime")
<*> parseLogLevel "log-level"
<*> parseOpenAPIMode "openapi-mode"
<*> (fromMaybe False <$> optBool "openapi-security-active")
<*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
<*> (maybe [] (fmap (MTOther . encodeUtf8) . splitOnCommas) <$> optValue "raw-media-types")
<*> parseCORSAllowedOrigins "server-cors-allowed-origins"
<*> (fromMaybe "!4" <$> optString "server-host")
<*> (fromMaybe 3000 <$> optInt "server-port")
<*> (fmap (CI.mk . encodeUtf8) <$> optString "server-trace-header")
<*> (fromMaybe False <$> optBool "server-timing-enabled")
<*> (fmap T.unpack <$> optString "server-unix-socket")
<*> parseSocketFileMode "server-unix-socket-mode"
<*> optInt "admin-server-port"
@@ -348,6 +357,11 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
Nothing -> pure [JSPKey "role"]
Just rck -> either (fail . show) pure $ pRoleClaimKey rck
parseCORSAllowedOrigins k =
optString k >>= \case
Nothing -> pure Nothing
Just orig -> pure $ Just (T.strip <$> T.splitOn "," orig)
optWithAlias :: C.Parser C.Config (Maybe a) -> C.Parser C.Config (Maybe a) -> C.Parser C.Config (Maybe a)
optWithAlias orig alias =
orig >>= \case
+14 -7
View File
@@ -3,16 +3,17 @@
module PostgREST.Config.Database
( pgVersionStatement
, queryDbSettings
, queryRoleSettings
, queryPgVersion
, queryRoleSettings
, RoleSettings
, RoleIsolationLvl
, TimezoneNames
, toIsolationLevel
) where
import Control.Arrow ((***))
import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.Config.PgVersion (PgVersion (..), pgVersion150)
import qualified Data.HashMap.Strict as HM
@@ -29,6 +30,7 @@ import Protolude
type RoleSettings = (HM.HashMap ByteString (HM.HashMap ByteString ByteString))
type RoleIsolationLvl = HM.HashMap ByteString SQL.IsolationLevel
type TimezoneNames = Set ByteString -- cache timezone names for prefer timezone=
toIsolationLevel :: (Eq a, IsString a) => a -> SQL.IsolationLevel
toIsolationLevel a = case a of
@@ -43,7 +45,8 @@ prefix = "pgrst."
dbSettingsNames :: [Text]
dbSettingsNames =
(prefix <>) <$>
["db_anon_role"
["db_aggregates_enabled"
,"db_anon_role"
,"db_pre_config"
,"db_extra_search_path"
,"db_max_rows"
@@ -53,16 +56,17 @@ dbSettingsNames =
,"db_root_spec"
,"db_schemas"
,"db_tx_end"
,"db_use_legacy_gucs"
,"jwt_aud"
,"jwt_role_claim_key"
,"jwt_secret"
,"jwt_secret_is_base64"
,"jwt_cache_max_lifetime"
,"openapi_mode"
,"openapi_security_active"
,"openapi_server_proxy_uri"
,"raw_media_types"
,"server_trace_header"
,"server_timing_enabled"
]
queryPgVersion :: Bool -> Session PgVersion
@@ -126,8 +130,8 @@ queryDbSettings preConfFunc prepared =
|]::Text
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
queryRoleSettings :: Bool -> Session (RoleSettings, RoleIsolationLvl)
queryRoleSettings prepared =
queryRoleSettings :: PgVersion -> Bool -> Session (RoleSettings, RoleIsolationLvl)
queryRoleSettings pgVer prepared =
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
transaction SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) prepared
where
@@ -156,7 +160,10 @@ queryRoleSettings prepared =
i.value as iso_lvl,
coalesce(array_agg(row(kv.key, kv.value)) filter (where key <> 'default_transaction_isolation'), '{}') as role_settings
from kv_settings kv
join pg_settings ps on ps.name = kv.key and ps.context = 'user'
join pg_settings ps on ps.name = kv.key |] <>
(if pgVer >= pgVersion150
then "and (ps.context = 'user' or has_parameter_privilege(current_user::regrole::oid, ps.name, 'set')) "
else "and ps.context = 'user' ") <> [q|
left join iso_setting i on i.rolname = kv.rolname
group by kv.rolname, i.value;
|]
+4
View File
@@ -13,6 +13,7 @@ module PostgREST.Config.PgVersion
, pgVersion121
, pgVersion130
, pgVersion140
, pgVersion150
) where
import qualified Data.Aeson as JSON
@@ -62,3 +63,6 @@ pgVersion130 = PgVersion 130000 "13.0"
pgVersion140 :: PgVersion
pgVersion140 = PgVersion 140000 "14.0"
pgVersion150 :: PgVersion
pgVersion150 = PgVersion 150000 "15.0"
+10 -6
View File
@@ -2,10 +2,14 @@
Module : PostgREST.Cors
Description : Wai Middleware to set cors policy.
-}
{-# LANGUAGE TupleSections #-}
module PostgREST.Cors (middleware) where
import qualified Data.ByteString.Char8 as BS
import qualified Data.CaseInsensitive as CI
import qualified Data.Text.Encoding as T
import qualified Network.Wai as Wai
import qualified Network.Wai.Middleware.Cors as Wai
@@ -13,15 +17,15 @@ import Data.List (lookup)
import Protolude
middleware :: Wai.Middleware
middleware = Wai.cors corsPolicy
middleware :: Maybe [Text] -> Wai.Middleware
middleware corsAllowedOrigins = Wai.cors $ corsPolicy corsAllowedOrigins
-- | CORS policy to be used in by Wai Cors middleware
corsPolicy :: Wai.Request -> Maybe Wai.CorsResourcePolicy
corsPolicy req = case lookup "origin" headers of
Just origin ->
corsPolicy :: Maybe [Text] -> Wai.Request -> Maybe Wai.CorsResourcePolicy
corsPolicy corsAllowedOrigins req = case lookup "origin" headers of
Just _ ->
Just Wai.CorsResourcePolicy
{ Wai.corsOrigins = Just ([origin], True)
{ Wai.corsOrigins = (, True) . map T.encodeUtf8 <$> corsAllowedOrigins
, Wai.corsMethods = ["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"]
, Wai.corsRequestHeaders = "Authorization" : accHeaders
, Wai.corsExposedHeaders = Just
+248 -212
View File
@@ -11,13 +11,15 @@ module PostgREST.Error
, PgError(..)
, Error(..)
, errorPayload
, singularityError
, status
) where
import qualified Data.Aeson as JSON
import qualified Data.ByteString.Char8 as BS
import qualified Data.CaseInsensitive as CI
import qualified Data.FuzzySet as Fuzzy
import qualified Data.HashMap.Strict as HM
import qualified Data.Map.Internal as M
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Text.Encoding.Error as T
@@ -25,7 +27,7 @@ import qualified Hasql.Pool as SQL
import qualified Hasql.Session as SQL
import qualified Network.HTTP.Types.Status as HTTP
import Data.Aeson ((.=))
import Data.Aeson ((.:), (.:?), (.=))
import Network.Wai (Response, responseLBS)
import Network.HTTP.Types.Header (Header)
@@ -55,15 +57,18 @@ class (JSON.ToJSON a) => PgrstError a where
errorPayload = JSON.encode
errorResponseFor :: a -> Response
errorResponseFor err = responseLBS (status err) (headers err) $ errorPayload err
errorResponseFor err =
let baseHeader = MediaType.toContentType MTApplicationJSON in
responseLBS (status err) (baseHeader : headers err) $ errorPayload err
instance PgrstError ApiRequestError where
status AggregatesNotAllowed{} = HTTP.status400
status AmbiguousRelBetween{} = HTTP.status300
status AmbiguousRpc{} = HTTP.status300
status BinaryFieldError{} = HTTP.status406
status MediaTypeError{} = HTTP.status415
status MediaTypeError{} = HTTP.status406
status InvalidBody{} = HTTP.status400
status InvalidFilters = HTTP.status405
status InvalidPreferences{} = HTTP.status400
status InvalidRpcMethod{} = HTTP.status405
status InvalidRange{} = HTTP.status416
status NotFound = HTTP.status404
@@ -80,108 +85,131 @@ instance PgrstError ApiRequestError where
status UnsupportedMethod{} = HTTP.status405
status LimitNoOrderError = HTTP.status400
status ColumnNotFound{} = HTTP.status400
status GucHeadersError = HTTP.status500
status GucStatusError = HTTP.status500
status OffLimitsChangesError{} = HTTP.status400
status PutMatchingPkError = HTTP.status400
status SingularityError{} = HTTP.status406
status PGRSTParseError = HTTP.status500
headers _ = [MediaType.toContentType MTApplicationJSON]
headers _ = mempty
toJsonPgrstError :: ErrorCode -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value
toJsonPgrstError code msg details hint = JSON.object [
"code" .= code
, "message" .= msg
, "details" .= details
, "hint" .= hint
]
instance JSON.ToJSON ApiRequestError where
toJSON (QueryParamError (QPError message details)) = JSON.object [
"code" .= ApiRequestErrorCode00,
"message" .= message,
"details" .= details,
"hint" .= JSON.Null]
toJSON (InvalidRpcMethod method) = JSON.object [
"code" .= ApiRequestErrorCode01,
"message" .= ("Cannot use the " <> T.decodeUtf8 method <> " method on RPC"),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (InvalidBody errorMessage) = JSON.object [
"code" .= ApiRequestErrorCode02,
"message" .= T.decodeUtf8 errorMessage,
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (InvalidRange rangeError) = JSON.object [
"code" .= ApiRequestErrorCode03,
"message" .= ("Requested range not satisfiable" :: Text),
"details" .= (case rangeError of
NegativeLimit -> "Limit should be greater than or equal to zero."
LowerGTUpper -> "The lower boundary must be lower than or equal to the upper boundary in the Range header."
OutOfBounds lower total -> "An offset of " <> lower <> " was requested, but there are only " <> total <> " rows."),
"hint" .= JSON.Null]
toJSON InvalidFilters = JSON.object [
"code" .= ApiRequestErrorCode05,
"message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (UnacceptableSchema schemas) = JSON.object [
"code" .= ApiRequestErrorCode06,
"message" .= ("The schema must be one of the following: " <> T.intercalate ", " schemas),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (MediaTypeError cts) = JSON.object [
"code" .= ApiRequestErrorCode07,
"message" .= ("None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (QueryParamError (QPError message details)) = toJsonPgrstError
ApiRequestErrorCode00 message (Just (JSON.String details)) Nothing
toJSON (InvalidRpcMethod method) = toJsonPgrstError
ApiRequestErrorCode01 ("Cannot use the " <> T.decodeUtf8 method <> " method on RPC") Nothing Nothing
toJSON (InvalidBody errorMessage) = toJsonPgrstError
ApiRequestErrorCode02 (T.decodeUtf8 errorMessage) Nothing Nothing
toJSON (InvalidRange rangeError) = toJsonPgrstError
ApiRequestErrorCode03
"Requested range not satisfiable"
(Just $ case rangeError of
NegativeLimit -> "Limit should be greater than or equal to zero."
LowerGTUpper -> "The lower boundary must be lower than or equal to the upper boundary in the Range header."
OutOfBounds lower total -> JSON.String $ "An offset of " <> lower <> " was requested, but there are only " <> total <> " rows.")
Nothing
toJSON InvalidFilters = toJsonPgrstError
ApiRequestErrorCode05 "Filters must include all and only primary key columns with 'eq' operators" Nothing Nothing
toJSON (UnacceptableSchema schemas) = toJsonPgrstError
ApiRequestErrorCode06 ("The schema must be one of the following: " <> T.intercalate ", " schemas) Nothing Nothing
toJSON (MediaTypeError cts) = toJsonPgrstError
ApiRequestErrorCode07 ("None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)) Nothing Nothing
toJSON NotFound = JSON.object []
toJSON (NotEmbedded resource) = JSON.object [
"code" .= ApiRequestErrorCode08,
"message" .= ("'" <> resource <> "' is not an embedded resource in this request" :: Text),
"details" .= JSON.Null,
"hint" .= ("Verify that '" <> resource <> "' is included in the 'select' query parameter." :: Text)]
toJSON LimitNoOrderError = JSON.object [
"code" .= ApiRequestErrorCode09,
"message" .= ("A 'limit' was applied without an explicit 'order'":: Text),
"details" .= JSON.Null,
"hint" .= ("Apply an 'order' using unique column(s)" :: Text)]
toJSON (NotEmbedded resource) = toJsonPgrstError
ApiRequestErrorCode08
("'" <> resource <> "' is not an embedded resource in this request")
Nothing
(Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter.")
toJSON (BinaryFieldError ct) = JSON.object [
"code" .= ApiRequestErrorCode13,
"message" .= ((T.decodeUtf8 (MediaType.toMime ct) <> " requested but more than one column was selected") :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON LimitNoOrderError = toJsonPgrstError
ApiRequestErrorCode09 "A 'limit' was applied without an explicit 'order'" Nothing (Just "Apply an 'order' using unique column(s)")
toJSON PutLimitNotAllowedError = JSON.object [
"code" .= ApiRequestErrorCode14,
"message" .= ("limit/offset querystring parameters are not allowed for PUT" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (OffLimitsChangesError n maxs) = toJsonPgrstError
ApiRequestErrorCode10
"The maximum number of rows allowed to change was surpassed"
(Just $ JSON.String $ T.unwords ["Results contain", show n, "rows changed but the maximum number allowed is", show maxs])
Nothing
toJSON (UnsupportedMethod method) = JSON.object [
"code" .= ApiRequestErrorCode17,
"message" .= ("Unsupported HTTP method: " <> T.decodeUtf8 method),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON GucHeadersError = toJsonPgrstError
ApiRequestErrorCode11 "response.headers guc must be a JSON array composed of objects with a single key and a string value" Nothing Nothing
toJSON (RelatedOrderNotToOne origin target) = JSON.object [
"code" .= ApiRequestErrorCode18,
"message" .= ("A related order on '" <> target <> "' is not possible" :: Text),
"details" .= ("'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship" :: Text),
"hint" .= JSON.Null]
toJSON GucStatusError = toJsonPgrstError
ApiRequestErrorCode12 "response.status guc must be a valid status code" Nothing Nothing
toJSON (SpreadNotToOne origin target) = JSON.object [
"code" .= ApiRequestErrorCode19,
"message" .= ("A spread operation on '" <> target <> "' is not possible" :: Text),
"details" .= ("'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship" :: Text),
"hint" .= JSON.Null]
toJSON PutLimitNotAllowedError = toJsonPgrstError
ApiRequestErrorCode14 "limit/offset querystring parameters are not allowed for PUT" Nothing Nothing
toJSON (UnacceptableFilter target) = JSON.object [
"code" .= ApiRequestErrorCode20,
"message" .= ("Bad operator on the '" <> target <> "' embedded resource":: Text),
"details" .= ("Only is null or not is null filters are allowed on embedded resources":: Text),
"hint" .= JSON.Null]
toJSON PutMatchingPkError = toJsonPgrstError
ApiRequestErrorCode15 "Payload values do not match URL in primary key column(s)" Nothing Nothing
toJSON (NoRelBetween parent child embedHint schema allRels) = JSON.object [
"code" .= SchemaCacheErrorCode00,
"message" .= ("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache" :: Text),
"details" .= ("Searched for a foreign key relationship between '" <> parent <> "' and '" <> child <> maybe mempty ("' using the hint '" <>) embedHint <> "' in the schema '" <> schema <> "', but no matches were found."),
"hint" .= noRelBetweenHint parent child schema allRels]
toJSON (SingularityError n) = toJsonPgrstError
ApiRequestErrorCode16
"JSON object requested, multiple (or no) rows returned"
(Just $ JSON.String $ T.unwords ["The result contains", show n, "rows"])
Nothing
toJSON (UnsupportedMethod method) = toJsonPgrstError
ApiRequestErrorCode17 ("Unsupported HTTP method: " <> T.decodeUtf8 method) Nothing Nothing
toJSON (RelatedOrderNotToOne origin target) = toJsonPgrstError
ApiRequestErrorCode18
("A related order on '" <> target <> "' is not possible")
(Just $ JSON.String $ "'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship")
Nothing
toJSON (SpreadNotToOne origin target) = toJsonPgrstError
ApiRequestErrorCode19
("A spread operation on '" <> target <> "' is not possible")
(Just $ JSON.String $ "'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship")
Nothing
toJSON (UnacceptableFilter target) = toJsonPgrstError
ApiRequestErrorCode20
("Bad operator on the '" <> target <> "' embedded resource")
(Just "Only is null or not is null filters are allowed on embedded resources")
Nothing
toJSON PGRSTParseError = toJsonPgrstError
ApiRequestErrorCode21 "The message and detail field of RAISE 'PGRST' error expects JSON" Nothing Nothing
toJSON (InvalidPreferences prefs) = toJsonPgrstError
ApiRequestErrorCode22
"Invalid preferences given with handling=strict"
(Just $ JSON.String $ T.decodeUtf8 ("Invalid preferences: " <> BS.intercalate ", " prefs))
Nothing
toJSON AggregatesNotAllowed = toJsonPgrstError
ApiRequestErrorCode23 "Use of aggregate functions is not allowed" Nothing Nothing
toJSON (NoRelBetween parent child embedHint schema allRels) = toJsonPgrstError
SchemaCacheErrorCode00
("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache")
(Just $ JSON.String $ "Searched for a foreign key relationship between '" <> parent <> "' and '" <> child <> maybe mempty ("' using the hint '" <>) embedHint <> "' in the schema '" <> schema <> "', but no matches were found.")
(JSON.String <$> noRelBetweenHint parent child schema allRels)
toJSON (AmbiguousRelBetween parent child rels) = toJsonPgrstError
SchemaCacheErrorCode01
("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'")
(Just $ JSON.toJSONList (compressedRel <$> rels))
(Just $ JSON.String $ "Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key.")
toJSON (AmbiguousRelBetween parent child rels) = JSON.object [
"code" .= SchemaCacheErrorCode01,
"message" .= ("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'" :: Text),
"details" .= (compressedRel <$> rels),
"hint" .= ("Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key." :: Text)]
toJSON (NoRpc schema procName argumentKeys hasPreferSingleObject contentType isInvPost allProcs overloadedProcs) =
let func = schema <> "." <> procName
prms = T.intercalate ", " argumentKeys
@@ -189,10 +217,10 @@ instance JSON.ToJSON ApiRequestError where
prmsDet = " with parameter" <> (if length argumentKeys > 1 then "s " else " ") <> prms
fmtPrms p = if null argumentKeys then " without parameters" else p
onlySingleParams = hasPreferSingleObject || (isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream])
in JSON.object [
"code" .= SchemaCacheErrorCode02,
"message" .= ("Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache"),
"details" .= ("Searched for the function " <> func <>
in toJsonPgrstError
SchemaCacheErrorCode02
("Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache")
(Just $ JSON.String $ "Searched for the function " <> func <>
(case (hasPreferSingleObject, isInvPost, contentType) of
(True, _, _) -> " with a single json/jsonb parameter"
(_, True, MTTextPlain) -> " with a single unnamed text parameter"
@@ -200,21 +228,20 @@ instance JSON.ToJSON ApiRequestError where
(_, True, MTOctetStream) -> " with a single unnamed bytea parameter"
(_, True, MTApplicationJSON) -> fmtPrms prmsDet <> " or with a single unnamed json/jsonb parameter"
_ -> fmtPrms prmsDet) <>
", but no matches were found in the schema cache."),
", but no matches were found in the schema cache.")
-- The hint will be null in the case of single unnamed parameter functions
"hint" .= if onlySingleParams
then Nothing
else noRpcHint schema procName argumentKeys allProcs overloadedProcs ]
toJSON (AmbiguousRpc procs) = JSON.object [
"code" .= SchemaCacheErrorCode03,
"message" .= ("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs]),
"details" .= JSON.Null,
"hint" .= ("Try renaming the parameters or the function itself in the database so function overloading can be resolved" :: Text)]
toJSON (ColumnNotFound relName colName) = JSON.object [
"code" .= SchemaCacheErrorCode04,
"message" .= ("Column '" <> colName <> "' of relation '" <> relName <> "' does not exist" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
(if onlySingleParams
then Nothing
else JSON.String <$> noRpcHint schema procName argumentKeys allProcs overloadedProcs)
toJSON (AmbiguousRpc procs) = toJsonPgrstError
SchemaCacheErrorCode03
("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs])
Nothing
(Just "Try renaming the parameters or the function itself in the database so function overloading can be resolved")
toJSON (ColumnNotFound relName colName) = toJsonPgrstError
SchemaCacheErrorCode04 ("Could not find the '" <> colName <> "' column of '" <> relName <> "' in the schema cache") Nothing Nothing
-- |
-- If no relationship is found then:
@@ -359,49 +386,60 @@ type Authenticated = Bool
instance PgrstError PgError where
status (PgError authed usageError) = pgErrorStatus authed usageError
headers (PgError _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p))))) =
case (parseMessage m, parseDetails d) of
(Just _, Just r) -> headers PGRSTParseError ++ map intoHeader (M.toList $ getHeaders r)
_ -> headers PGRSTParseError
where
intoHeader (k,v) = (CI.mk $ T.encodeUtf8 k, T.encodeUtf8 v)
headers err =
if status err == HTTP.status401
then [MediaType.toContentType MTApplicationJSON, ("WWW-Authenticate", "Bearer") :: Header]
else [MediaType.toContentType MTApplicationJSON]
then [("WWW-Authenticate", "Bearer") :: Header]
else mempty
instance JSON.ToJSON PgError where
toJSON (PgError _ usageError) = JSON.toJSON usageError
instance JSON.ToJSON SQL.UsageError where
toJSON (SQL.ConnectionUsageError e) = JSON.object [
"code" .= ConnectionErrorCode00,
"message" .= ("Database connection error. Retrying the connection." :: Text),
"details" .= (T.decodeUtf8With T.lenientDecode $ fromMaybe "" e :: Text),
"hint" .= JSON.Null]
toJSON (SQL.ConnectionUsageError e) = toJsonPgrstError
ConnectionErrorCode00
"Database connection error. Retrying the connection."
(Just $ JSON.String $ T.decodeUtf8With T.lenientDecode $ fromMaybe "" e)
Nothing
toJSON (SQL.SessionUsageError e) = JSON.toJSON e -- SQL.Error
toJSON SQL.AcquisitionTimeoutUsageError = JSON.object [
"code" .= ConnectionErrorCode03,
"message" .= ("Timed out acquiring connection from connection pool." :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON SQL.AcquisitionTimeoutUsageError = toJsonPgrstError
ConnectionErrorCode03 "Timed out acquiring connection from connection pool." Nothing Nothing
instance JSON.ToJSON SQL.QueryError where
toJSON (SQL.QueryError _ _ e) = JSON.toJSON e
instance JSON.ToJSON SQL.CommandError where
-- Special error raised with code PGRST, to allow full response control
toJSON (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p)) =
case (parseMessage m, parseDetails d) of
(Just r, Just _) -> JSON.object [
"code" .= getCode r,
"message" .= getMessage r,
"details" .= checkMaybe (getDetails r),
"hint" .= checkMaybe (getHint r)]
_ -> JSON.toJSON PGRSTParseError
where
checkMaybe = maybe JSON.Null JSON.String
toJSON (SQL.ResultError (SQL.ServerError c m d h _p)) = JSON.object [
"code" .= (T.decodeUtf8 c :: Text),
"message" .= (T.decodeUtf8 m :: Text),
"details" .= (fmap T.decodeUtf8 d :: Maybe Text),
"hint" .= (fmap T.decodeUtf8 h :: Maybe Text)]
toJSON (SQL.ResultError resultError) = JSON.object [
"code" .= InternalErrorCode00,
"message" .= (show resultError :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (SQL.ClientError d) = JSON.object [
"code" .= ConnectionErrorCode01,
"message" .= ("Database client error. Retrying the connection." :: Text),
"details" .= (fmap T.decodeUtf8 d :: Maybe Text),
"hint" .= JSON.Null]
toJSON (SQL.ResultError resultError) = toJsonPgrstError
InternalErrorCode00 (show resultError) Nothing Nothing
toJSON (SQL.ClientError d) = toJsonPgrstError
ConnectionErrorCode01 "Database client error. Retrying the connection." (JSON.String <$> fmap T.decodeUtf8 d) Nothing
pgErrorStatus :: Bool -> SQL.UsageError -> HTTP.Status
pgErrorStatus _ (SQL.ConnectionUsageError _) = HTTP.status503
@@ -409,7 +447,7 @@ pgErrorStatus _ SQL.AcquisitionTimeoutUsageError = HTTP.status504
pgErrorStatus _ (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ClientError _))) = HTTP.status503
pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError rError))) =
case rError of
(SQL.ServerError c m _ _ _) ->
(SQL.ServerError c m d _ _) ->
case BS.unpack c of
'0':'8':_ -> HTTP.status503 -- pg connection err
'0':'9':_ -> HTTP.status500 -- triggered action exception
@@ -418,6 +456,10 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
"23503" -> HTTP.status409 -- foreign_key_violation
"23505" -> HTTP.status409 -- unique_violation
"25006" -> HTTP.status405 -- read_only_sql_transaction
"21000" -> -- cardinality_violation
if BS.isSuffixOf "requires a WHERE clause" m
then HTTP.status400 -- special case for pg-safeupdate, which we consider as client error
else HTTP.status500 -- generic function or view server error, e.g. "more than one row returned by a subquery used as an expression"
'2':'5':_ -> HTTP.status500 -- invalid tx state
'2':'8':_ -> HTTP.status403 -- invalid auth specification
'2':'D':_ -> HTTP.status500 -- invalid tx termination
@@ -440,98 +482,52 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
then HTTP.status406
else HTTP.status404 -- undefined function
"42P01" -> HTTP.status404 -- undefined table
"42P17" -> HTTP.status500 -- infinite recursion
"42501" -> if authed then HTTP.status403 else HTTP.status401 -- insufficient privilege
'P':'T':n -> fromMaybe HTTP.status500 (HTTP.mkStatus <$> readMaybe n <*> pure m)
"PGRST" ->
case (parseMessage m, parseDetails d) of
(Just _, Just r) -> maybe (toEnum $ getStatus r) (HTTP.mkStatus (getStatus r) . T.encodeUtf8) (getStatusText r)
_ -> status PGRSTParseError
_ -> HTTP.status400
_ -> HTTP.status500
data Error
= ApiRequestError ApiRequestError
| GucHeadersError
| GucStatusError
| JwtTokenInvalid Text
| JwtTokenMissing
| JwtTokenRequired
| NoSchemaCacheError
| OffLimitsChangesError Int64 Integer
| PgErr PgError
| PutMatchingPkError
| SingularityError Integer
instance PgrstError Error where
status (ApiRequestError err) = status err
status GucHeadersError = HTTP.status500
status GucStatusError = HTTP.status500
status JwtTokenInvalid{} = HTTP.unauthorized401
status JwtTokenMissing = HTTP.status500
status JwtTokenRequired = HTTP.unauthorized401
status NoSchemaCacheError = HTTP.status503
status OffLimitsChangesError{} = HTTP.status400
status (PgErr err) = status err
status PutMatchingPkError = HTTP.status400
status SingularityError{} = HTTP.status406
status (ApiRequestError err) = status err
status JwtTokenInvalid{} = HTTP.unauthorized401
status JwtTokenMissing = HTTP.status500
status JwtTokenRequired = HTTP.unauthorized401
status NoSchemaCacheError = HTTP.status503
status (PgErr err) = status err
headers (ApiRequestError err) = headers err
headers (JwtTokenInvalid m) = [MediaType.toContentType MTApplicationJSON, invalidTokenHeader m]
headers JwtTokenRequired = [MediaType.toContentType MTApplicationJSON, requiredTokenHeader]
headers (PgErr err) = headers err
headers SingularityError{} = [MediaType.toContentType (MTSingularJSON False)]
headers _ = [MediaType.toContentType MTApplicationJSON]
headers (ApiRequestError err) = headers err
headers (JwtTokenInvalid m) = [invalidTokenHeader m]
headers JwtTokenRequired = [requiredTokenHeader]
headers (PgErr err) = headers err
headers _ = mempty
instance JSON.ToJSON Error where
toJSON NoSchemaCacheError = JSON.object [
"code" .= ConnectionErrorCode02,
"message" .= ("Could not query the database for the schema cache. Retrying." :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON NoSchemaCacheError = toJsonPgrstError
ConnectionErrorCode02 "Could not query the database for the schema cache. Retrying." Nothing Nothing
toJSON JwtTokenMissing = JSON.object [
"code" .= JWTErrorCode00,
"message" .= ("Server lacks JWT secret" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (JwtTokenInvalid message) = JSON.object [
"code" .= JWTErrorCode01,
"message" .= (message :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON JwtTokenRequired = JSON.object [
"code" .= JWTErrorCode02,
"message" .= ("Anonymous access is disabled" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON JwtTokenMissing = toJsonPgrstError
JWTErrorCode00 "Server lacks JWT secret" Nothing Nothing
toJSON (OffLimitsChangesError n maxs) = JSON.object [
"code" .= ApiRequestErrorCode10,
"message" .= ("The maximum number of rows allowed to change was surpassed" :: Text),
"details" .= T.unwords ["Results contain", show n, "rows changed but the maximum number allowed is", show maxs],
"hint" .= JSON.Null]
toJSON (JwtTokenInvalid message) = toJsonPgrstError
JWTErrorCode01 message Nothing Nothing
toJSON GucHeadersError = JSON.object [
"code" .= ApiRequestErrorCode11,
"message" .= ("response.headers guc must be a JSON array composed of objects with a single key and a string value" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON GucStatusError = JSON.object [
"code" .= ApiRequestErrorCode12,
"message" .= ("response.status guc must be a valid status code" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON PutMatchingPkError = JSON.object [
"code" .= ApiRequestErrorCode15,
"message" .= ("Payload values do not match URL in primary key column(s)" :: Text),
"details" .= JSON.Null,
"hint" .= JSON.Null]
toJSON (SingularityError n) = JSON.object [
"code" .= ApiRequestErrorCode16,
"message" .= ("JSON object requested, multiple (or no) rows returned" :: Text),
"details" .= T.unwords ["The result contains", show n, "rows"],
"hint" .= JSON.Null]
toJSON JwtTokenRequired = toJsonPgrstError
JWTErrorCode02 "Anonymous access is disabled" Nothing Nothing
toJSON (PgErr err) = JSON.toJSON err
toJSON (ApiRequestError err) = JSON.toJSON err
@@ -543,8 +539,44 @@ invalidTokenHeader m =
requiredTokenHeader :: Header
requiredTokenHeader = ("WWW-Authenticate", "Bearer")
singularityError :: (Integral a) => a -> Error
singularityError = SingularityError . toInteger
-- For parsing byteString to JSON Object, used for allowing full response control
data PgRaiseErrMessage = PgRaiseErrMessage {
getCode :: Text,
getMessage :: Text,
getDetails :: Maybe Text,
getHint :: Maybe Text
}
data PgRaiseErrDetails = PgRaiseErrDetails {
getStatus :: Int,
getStatusText :: Maybe Text,
getHeaders :: Map Text Text
}
instance JSON.FromJSON PgRaiseErrMessage where
parseJSON (JSON.Object m) =
PgRaiseErrMessage
<$> m .: "code"
<*> m .: "message"
<*> m .:? "details"
<*> m .:? "hint"
parseJSON _ = mzero
instance JSON.FromJSON PgRaiseErrDetails where
parseJSON (JSON.Object d) =
PgRaiseErrDetails
<$> d .: "status"
<*> d .:? "status_text"
<*> d .: "headers"
parseJSON _ = mzero
parseMessage :: ByteString -> Maybe PgRaiseErrMessage
parseMessage = JSON.decodeStrict
parseDetails :: Maybe ByteString -> Maybe PgRaiseErrDetails
parseDetails d = JSON.decodeStrict =<< d
-- Error codes are grouped by common modules or characteristics
data ErrorCode
@@ -558,7 +590,7 @@ data ErrorCode
| ApiRequestErrorCode01
| ApiRequestErrorCode02
| ApiRequestErrorCode03
| ApiRequestErrorCode04 -- no longer used (used to be mapped to ParseRequestError)
-- | ApiRequestErrorCode04 -- no longer used (used to be mapped to ParseRequestError)
| ApiRequestErrorCode05
| ApiRequestErrorCode06
| ApiRequestErrorCode07
@@ -566,8 +598,8 @@ data ErrorCode
| ApiRequestErrorCode09
| ApiRequestErrorCode10
| ApiRequestErrorCode11
-- | ApiRequestErrorCode13 -- no longer used (used to be mapped to BinaryFieldError)
| ApiRequestErrorCode12
| ApiRequestErrorCode13
| ApiRequestErrorCode14
| ApiRequestErrorCode15
| ApiRequestErrorCode16
@@ -575,6 +607,9 @@ data ErrorCode
| ApiRequestErrorCode18
| ApiRequestErrorCode19
| ApiRequestErrorCode20
| ApiRequestErrorCode21
| ApiRequestErrorCode22
| ApiRequestErrorCode23
-- Schema Cache errors
| SchemaCacheErrorCode00
| SchemaCacheErrorCode01
@@ -604,7 +639,6 @@ buildErrorCode code = "PGRST" <> case code of
ApiRequestErrorCode01 -> "101"
ApiRequestErrorCode02 -> "102"
ApiRequestErrorCode03 -> "103"
ApiRequestErrorCode04 -> "104"
ApiRequestErrorCode05 -> "105"
ApiRequestErrorCode06 -> "106"
ApiRequestErrorCode07 -> "107"
@@ -613,7 +647,6 @@ buildErrorCode code = "PGRST" <> case code of
ApiRequestErrorCode10 -> "110"
ApiRequestErrorCode11 -> "111"
ApiRequestErrorCode12 -> "112"
ApiRequestErrorCode13 -> "113"
ApiRequestErrorCode14 -> "114"
ApiRequestErrorCode15 -> "115"
ApiRequestErrorCode16 -> "116"
@@ -621,6 +654,9 @@ buildErrorCode code = "PGRST" <> case code of
ApiRequestErrorCode18 -> "118"
ApiRequestErrorCode19 -> "119"
ApiRequestErrorCode20 -> "120"
ApiRequestErrorCode21 -> "121"
ApiRequestErrorCode22 -> "122"
ApiRequestErrorCode23 -> "123"
SchemaCacheErrorCode00 -> "200"
SchemaCacheErrorCode01 -> "201"
+32 -42
View File
@@ -1,9 +1,10 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
module PostgREST.MediaType
( MediaType(..)
, MTPlanOption (..)
, MTPlanFormat (..)
, MTVndPlanOption (..)
, MTVndPlanFormat (..)
, toContentType
, toMime
, decodeMediaType
@@ -19,8 +20,6 @@ import Protolude
-- | Enumeration of currently supported media types
data MediaType
= MTApplicationJSON
| MTArrayJSONStrip
| MTSingularJSON Bool
| MTGeoJSON
| MTTextCSV
| MTTextPlain
@@ -30,32 +29,23 @@ data MediaType
| MTOctetStream
| MTAny
| MTOther ByteString
-- TODO MTPlan should only have its options as [Text]. Its ResultAggregate should have the typed attributes.
| MTPlan MediaType MTPlanFormat [MTPlanOption]
deriving Show
instance Eq MediaType where
MTApplicationJSON == MTApplicationJSON = True
MTArrayJSONStrip == MTArrayJSONStrip = True
MTSingularJSON x == MTSingularJSON y = x == y
MTGeoJSON == MTGeoJSON = True
MTTextCSV == MTTextCSV = True
MTTextPlain == MTTextPlain = True
MTTextXML == MTTextXML = True
MTOpenAPI == MTOpenAPI = True
MTUrlEncoded == MTUrlEncoded = True
MTOctetStream == MTOctetStream = True
MTAny == MTAny = True
MTOther x == MTOther y = x == y
MTPlan{} == MTPlan{} = True
_ == _ = False
-- vendored media types
| MTVndArrayJSONStrip
| MTVndSingularJSON Bool
-- TODO MTVndPlan should only have its options as [Text]. Its ResultAggregate should have the typed attributes.
| MTVndPlan MediaType MTVndPlanFormat [MTVndPlanOption]
deriving (Eq, Show, Generic)
instance Hashable MediaType
data MTPlanOption
data MTVndPlanOption
= PlanAnalyze | PlanVerbose | PlanSettings | PlanBuffers | PlanWAL
deriving (Eq, Show)
deriving (Eq, Show, Generic)
instance Hashable MTVndPlanOption
data MTPlanFormat
data MTVndPlanFormat
= PlanJSON | PlanText
deriving (Eq, Show)
deriving (Eq, Show, Generic)
instance Hashable MTVndPlanFormat
-- | Convert MediaType to a Content-Type HTTP Header
toContentType :: MediaType -> Header
@@ -69,31 +59,31 @@ toContentType ct = (hContentType, toMime ct <> charset)
-- | Convert from MediaType to a ByteString representing the mime type
toMime :: MediaType -> ByteString
toMime MTApplicationJSON = "application/json"
toMime MTArrayJSONStrip = "application/vnd.pgrst.array+json;nulls=stripped"
toMime MTVndArrayJSONStrip = "application/vnd.pgrst.array+json;nulls=stripped"
toMime MTGeoJSON = "application/geo+json"
toMime MTTextCSV = "text/csv"
toMime MTTextPlain = "text/plain"
toMime MTTextXML = "text/xml"
toMime MTOpenAPI = "application/openapi+json"
toMime (MTSingularJSON True) = "application/vnd.pgrst.object+json;nulls=stripped"
toMime (MTSingularJSON False) = "application/vnd.pgrst.object+json"
toMime (MTVndSingularJSON True) = "application/vnd.pgrst.object+json;nulls=stripped"
toMime (MTVndSingularJSON False) = "application/vnd.pgrst.object+json"
toMime MTUrlEncoded = "application/x-www-form-urlencoded"
toMime MTOctetStream = "application/octet-stream"
toMime MTAny = "*/*"
toMime (MTOther ct) = ct
toMime (MTPlan mt fmt opts) =
toMime (MTVndPlan mt fmt opts) =
"application/vnd.pgrst.plan+" <> toMimePlanFormat fmt <>
("; for=\"" <> toMime mt <> "\"") <>
(if null opts then mempty else "; options=" <> BS.intercalate "|" (toMimePlanOption <$> opts))
toMimePlanOption :: MTPlanOption -> ByteString
toMimePlanOption :: MTVndPlanOption -> ByteString
toMimePlanOption PlanAnalyze = "analyze"
toMimePlanOption PlanVerbose = "verbose"
toMimePlanOption PlanSettings = "settings"
toMimePlanOption PlanBuffers = "buffers"
toMimePlanOption PlanWAL = "wal"
toMimePlanFormat :: MTPlanFormat -> ByteString
toMimePlanFormat :: MTVndPlanFormat -> ByteString
toMimePlanFormat PlanJSON = "json"
toMimePlanFormat PlanText = "text"
@@ -103,25 +93,25 @@ toMimePlanFormat PlanText = "text"
-- MTApplicationJSON
--
-- >>> decodeMediaType "application/vnd.pgrst.plan;"
-- MTPlan MTApplicationJSON PlanText []
-- MTVndPlan MTApplicationJSON PlanText []
--
-- >>> decodeMediaType "application/vnd.pgrst.plan;for=\"application/json\""
-- MTPlan MTApplicationJSON PlanText []
-- MTVndPlan MTApplicationJSON PlanText []
--
-- >>> decodeMediaType "application/vnd.pgrst.plan+json;for=\"text/csv\""
-- MTPlan MTTextCSV PlanJSON []
-- MTVndPlan MTTextCSV PlanJSON []
--
-- >>> decodeMediaType "application/vnd.pgrst.array+json;nulls=stripped"
-- MTArrayJSONStrip
-- MTVndArrayJSONStrip
--
-- >>> decodeMediaType "application/vnd.pgrst.array+json"
-- MTApplicationJSON
--
-- >>> decodeMediaType "application/vnd.pgrst.object+json;nulls=stripped"
-- MTSingularJSON True
-- MTVndSingularJSON True
--
-- >>> decodeMediaType "application/vnd.pgrst.object+json"
-- MTSingularJSON False
-- MTVndSingularJSON False
decodeMediaType :: BS.ByteString -> MediaType
decodeMediaType mt =
@@ -145,11 +135,11 @@ decodeMediaType mt =
other:_ -> MTOther other
_ -> MTAny
where
checkArrayNullStrip ["nulls=stripped"] = MTArrayJSONStrip
checkArrayNullStrip ["nulls=stripped"] = MTVndArrayJSONStrip
checkArrayNullStrip _ = MTApplicationJSON
checkSingularNullStrip ["nulls=stripped"] = MTSingularJSON True
checkSingularNullStrip _ = MTSingularJSON False
checkSingularNullStrip ["nulls=stripped"] = MTVndSingularJSON True
checkSingularNullStrip _ = MTVndSingularJSON False
getPlan fmt rest =
let
@@ -161,7 +151,7 @@ decodeMediaType mt =
strippedFor <- BS.stripPrefix "for=" foundFor
pure . decodeMediaType $ dropAround (== BS.c2w '"') strippedFor
in
MTPlan mtFor fmt $
MTVndPlan mtFor fmt $
[PlanAnalyze | inOpts "analyze" ] ++
[PlanVerbose | inOpts "verbose" ] ++
[PlanSettings | inOpts "settings"] ++
+271 -117
View File
@@ -19,10 +19,11 @@ module PostgREST.Plan
( wrappedReadPlan
, mutateReadPlan
, callReadPlan
, inspectPlan
, WrappedReadPlan(..)
, MutateReadPlan(..)
, CallReadPlan(..)
, inspectPlanTxMode
, InspectPlan(..)
) where
import qualified Data.ByteString.Lazy as LBS
@@ -33,7 +34,7 @@ import qualified Data.Set as S
import qualified PostgREST.SchemaCache.Routine as Routine
import Data.Either.Combinators (mapLeft, mapRight)
import Data.List (delete)
import Data.List (delete, lookup)
import Data.Tree (Tree (..))
import PostgREST.ApiRequest (Action (..),
@@ -51,6 +52,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange,
import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier (..),
RelIdentifier (..),
Schema)
import PostgREST.SchemaCache.Relationship (Cardinality (..),
Junction (..),
@@ -59,7 +61,9 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
relIsToOne)
import PostgREST.SchemaCache.Representations (DataRepresentation (..),
RepresentationsMap)
import PostgREST.SchemaCache.Routine (ResultAggregate (..),
import PostgREST.SchemaCache.Routine (MediaHandler (..),
MediaHandlerMap,
ResolvedHandler,
Routine (..),
RoutineMap,
RoutineParam (..),
@@ -80,6 +84,7 @@ import PostgREST.Plan.Types
import qualified Hasql.Transaction.Sessions as SQL
import qualified PostgREST.ApiRequest.QueryParams as QueryParams
import qualified PostgREST.MediaType as MediaType
import Protolude hiding (from)
@@ -90,14 +95,16 @@ import Protolude hiding (from)
data WrappedReadPlan = WrappedReadPlan {
wrReadPlan :: ReadPlanTree
, wrTxMode :: SQL.Mode
, wrResAgg :: ResultAggregate
, wrHandler :: MediaHandler
, wrMedia :: MediaType
}
data MutateReadPlan = MutateReadPlan {
mrReadPlan :: ReadPlanTree
, mrMutatePlan :: MutatePlan
, mrTxMode :: SQL.Mode
, mrResAgg :: ResultAggregate
, mrHandler :: MediaHandler
, mrMedia :: MediaType
}
data CallReadPlan = CallReadPlan {
@@ -105,37 +112,45 @@ data CallReadPlan = CallReadPlan {
, crCallPlan :: CallPlan
, crTxMode :: SQL.Mode
, crProc :: Routine
, crResAgg :: ResultAggregate
, crHandler :: MediaHandler
, crMedia :: MediaType
}
data InspectPlan = InspectPlan {
ipMedia :: MediaType
, ipTxmode :: SQL.Mode
}
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error WrappedReadPlan
wrappedReadPlan identifier conf sCache apiRequest = do
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} = do
rPlan <- readPlan identifier conf sCache apiRequest
binField <- mapLeft ApiRequestError $ binaryField conf (iAcceptMediaType apiRequest) Nothing rPlan
return $ WrappedReadPlan rPlan SQL.Read $ mediaToAggregate (iAcceptMediaType apiRequest) binField apiRequest
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
return $ WrappedReadPlan rPlan SQL.Read handler mediaType
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error MutateReadPlan
mutateReadPlan mutation apiRequest identifier conf sCache = do
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
rPlan <- readPlan identifier conf sCache apiRequest
binField <- mapLeft ApiRequestError $ binaryField conf (iAcceptMediaType apiRequest) Nothing rPlan
mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan
return $ MutateReadPlan rPlan mPlan SQL.Write $ mediaToAggregate (iAcceptMediaType apiRequest) binField apiRequest
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
callReadPlan identifier conf sCache apiRequest invMethod = do
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} invMethod = do
let paramKeys = case invMethod of
InvGet -> S.fromList $ fst <$> qsParams'
InvHead -> S.fromList $ fst <$> qsParams'
InvPost -> iColumns apiRequest
InvPost -> iColumns
proc@Function{..} <- mapLeft ApiRequestError $
findProc identifier paramKeys (preferParameters == Just SingleObject) (dbRoutines sCache) (iContentMediaType apiRequest) (invMethod == InvPost)
findProc identifier paramKeys (preferParameters == Just SingleObject) (dbRoutines sCache) iContentMediaType (invMethod == InvPost)
let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations
rPlan <- readPlan relIdentifier conf sCache apiRequest
let args = case (invMethod, iContentMediaType apiRequest) of
let args = case (invMethod, iContentMediaType) of
(InvGet, _) -> jsonRpcParams proc qsParams'
(InvHead, _) -> jsonRpcParams proc qsParams'
(InvPost, MTUrlEncoded) -> maybe mempty (jsonRpcParams proc . payArray) $ iPayload apiRequest
(InvPost, _) -> maybe mempty payRaw $ iPayload apiRequest
(InvPost, MTUrlEncoded) -> maybe mempty (jsonRpcParams proc . payArray) iPayload
(InvPost, _) -> maybe mempty payRaw iPayload
txMode = case (invMethod, pdVolatility) of
(InvGet, _) -> SQL.Read
(InvHead, _) -> SQL.Read
@@ -143,11 +158,24 @@ callReadPlan identifier conf sCache apiRequest invMethod = do
(InvPost, Routine.Immutable) -> SQL.Read
(InvPost, Routine.Volatile) -> SQL.Write
cPlan = callPlan proc apiRequest paramKeys args rPlan
binField <- mapLeft ApiRequestError $ binaryField conf (iAcceptMediaType apiRequest) (Just proc) rPlan
return $ CallReadPlan rPlan cPlan txMode proc $ mediaToAggregate (iAcceptMediaType apiRequest) binField apiRequest
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
return $ CallReadPlan rPlan cPlan txMode proc handler mediaType
where
Preferences{..} = iPreferences apiRequest
qsParams' = QueryParams.qsParams (iQueryParams apiRequest)
qsParams' = QueryParams.qsParams iQueryParams
hasDefaultSelect :: ReadPlanTree -> Bool
hasDefaultSelect (Node ReadPlan{select=[CoercibleSelectField{csField=CoercibleField{cfName}}]} []) = cfName == "*"
hasDefaultSelect _ = False
inspectPlan :: ApiRequest -> Either Error InspectPlan
inspectPlan apiRequest = do
let producedMTs = [MTOpenAPI, MTApplicationJSON, MTAny]
accepts = iAcceptMediaType apiRequest
mediaType <- if not . null $ L.intersect accepts producedMTs
then Right MTOpenAPI
else Left . ApiRequestError . MediaTypeError $ MediaType.toMime <$> accepts
return $ InspectPlan mediaType SQL.Read
{-|
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
@@ -206,9 +234,6 @@ findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPo
-- and can match any or none of the default parameters.
(reqParams, optParams) -> argumentsKeys `S.difference` S.fromList (ppName <$> optParams) == S.fromList (ppName <$> reqParams)
inspectPlanTxMode :: SQL.Mode
inspectPlanTxMode = SQL.Read
-- | During planning we need to resolve Field -> CoercibleField (finding the context specific target type and map function).
-- | ResolverContext facilitates this without the need to pass around a laundry list of parameters.
data ResolverContext = ResolverContext
@@ -274,18 +299,21 @@ resolveQueryInputField ctx field = withTextParse ctx $ resolveTypeOrUnknown ctx
-- | Adds filters, order, limits on its respective nodes.
-- | Adds joins conditions obtained from resource embedding.
readPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Either Error ReadPlanTree
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows} SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest =
readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregates} SchemaCache{dbTables, dbRelationships, dbRepresentations} apiRequest =
let
-- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV.
ctx = ResolverContext dbTables dbRepresentations qi "json"
in
mapLeft ApiRequestError $
treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
validateAggFunctions configDbAggregates =<<
hoistSpreadAggFunctions =<<
addRelSelects =<<
addNullEmbedFilters =<<
validateSpreadEmbeds =<<
addRelatedOrders =<<
addDataRepresentationAliases =<<
expandStarsForDataRepresentations ctx =<<
addAliases =<<
expandStars ctx =<<
addRels qiSchema (iAction apiRequest) dbRelationships Nothing =<<
addLogicTrees ctx apiRequest =<<
addRanges apiRequest =<<
@@ -298,7 +326,7 @@ initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi ctx, relName=qiName, depth=rootDepth} []
where
rootDepth = 0
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing False rootDepth
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing False [] rootDepth
treeEntry :: Depth -> Tree SelectItem -> ReadPlanTree -> ReadPlanTree
treeEntry depth (Node si fldForest) (Node q rForest) =
let nxtDepth = succ depth in
@@ -314,49 +342,86 @@ initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
(Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth, relIsSpread=True} [])
fldForest:rForest
SelectField{..} ->
Node q{select=(resolveOutputField ctx{qi=from q} selField, selCast, selAlias):select q} rForest
Node q{select=CoercibleSelectField (resolveOutputField ctx{qi=from q} selField) selAggregateFunction selAggregateCast selCast selAlias:select q} rForest
-- | Preserve the original field name if data representation is used to coerce the value.
addDataRepresentationAliases :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
addDataRepresentationAliases rPlanTree = Right $ fmap (\rPlan@ReadPlan{select=sel} -> rPlan{select=map aliasSelectItem sel}) rPlanTree
-- If an alias is explicitly specified, it is always respected. However, an alias may be
-- determined automatically in the case of a select term with a JSON path, or in the case
-- of domain representations.
addAliases :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
addAliases = Right . fmap addAliasToPlan
where
aliasSelectItem :: (CoercibleField, Maybe Cast, Maybe Alias) -> (CoercibleField, Maybe Cast, Maybe Alias)
-- If there already is an alias, don't overwrite it.
aliasSelectItem (fld@(CoercibleField{cfName=fieldName, cfTransform=(Just _)}), Nothing, Nothing) = (fld, Nothing, Just fieldName)
aliasSelectItem fld = fld
addAliasToPlan rp@ReadPlan{select=sel} = rp{select=map aliasSelectField sel}
aliasSelectField :: CoercibleSelectField -> CoercibleSelectField
aliasSelectField field@CoercibleSelectField{csField=fieldDetails, csAggFunction=aggFun, csAlias=alias}
| isJust alias || isJust aggFun = field
| isJsonKeyPath fieldDetails, Just key <- lastJsonKey fieldDetails = field { csAlias = Just key }
| isTransformPath fieldDetails = field { csAlias = Just (cfName fieldDetails) }
| otherwise = field
isJsonKeyPath CoercibleField{cfJsonPath=(_: _)} = True
isJsonKeyPath _ = False
isTransformPath CoercibleField{cfTransform=(Just _), cfName=_} = True
isTransformPath _ = False
lastJsonKey CoercibleField{cfName=fieldName, cfJsonPath=jsonPath} =
case jOp <$> lastMay jsonPath of
Just (JKey key) -> Just key
Just (JIdx _) -> Just $ fromMaybe fieldName lastKey
-- We get the lastKey because on:
-- `select=data->1->mycol->>2`, we need to show the result as [ {"mycol": ..}, {"mycol": ..} ]
-- `select=data->3`, we need to show the result as [ {"data": ..}, {"data": ..} ]
where lastKey = jVal <$> find (\case JKey{} -> True; _ -> False) (jOp <$> reverse jsonPath)
Nothing -> Nothing
knownColumnsInContext :: ResolverContext -> [Column]
knownColumnsInContext ResolverContext{..} =
fromMaybe [] $ HM.lookup qi tables >>=
Just . tableColumnsList
-- | Expand "select *" into explicit field names of the table, if necessary to apply data representations.
expandStarsForDataRepresentations :: ResolverContext -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
expandStarsForDataRepresentations ctx@ResolverContext{qi} rPlanTree = Right $ fmap expandStars rPlanTree
-- | Expand "select *" into explicit field names of the table in the following situations:
-- * When there are data representations present.
-- * When there is an aggregate function in a given ReadPlan or its parent.
expandStars :: ResolverContext -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree
where
expandStars :: ReadPlan -> ReadPlan
expandStarsForReadPlan :: Bool -> ReadPlanTree -> ReadPlanTree
expandStarsForReadPlan hasAgg (Node rp@ReadPlan{select, from=fromQI, fromAlias=alias} children) =
let
newHasAgg = hasAgg || any (isJust . csAggFunction) select
newCtx = adjustContext ctx fromQI alias
newRPlan = expandStarsForTable newCtx newHasAgg rp
in Node newRPlan (map (expandStarsForReadPlan newHasAgg) children)
-- Choose the appropriate context based on whether we're dealing with "pgrst_source"
adjustContext :: ResolverContext -> QualifiedIdentifier -> Maybe Text -> ResolverContext
-- When the schema is "" and the table is the source CTE, we assume the true source table is given in the from
-- alias and belongs to the request schema. See the bit in `addRels` with `newFrom = ...`.
expandStars rPlan@ReadPlan{from=(QualifiedIdentifier "" "pgrst_source"), fromAlias=(Just tblAlias)} =
expandStarsForTable ctx{qi=qi{qiName=tblAlias}} rPlan
expandStars rPlan@ReadPlan{from=fromTable} =
expandStarsForTable ctx{qi=fromTable} rPlan
adjustContext context@ResolverContext{qi=ctxQI} (QualifiedIdentifier "" "pgrst_source") (Just a) = context{qi=ctxQI{qiName=a}}
adjustContext context fromQI _ = context{qi=fromQI}
expandStarsForTable :: ResolverContext -> ReadPlan -> ReadPlan
expandStarsForTable ctx@ResolverContext{representations, outputType} rplan@ReadPlan{select=selectItems} =
-- If we have a '*' select AND the target table has at least one data representation, expand.
if ("*" `elem` map (\(field, _, _) -> cfName field) selectItems) && any hasOutputRep knownColumns
then rplan{select=concatMap (expandStarSelectItem knownColumns) selectItems}
else rplan
expandStarsForTable :: ResolverContext -> Bool -> ReadPlan -> ReadPlan
expandStarsForTable ctx@ResolverContext{representations, outputType} hasAgg rp@ReadPlan{select=selectFields}
-- We expand if either of the below are true:
-- * We have a '*' select AND there is an aggregate function in this ReadPlan's sub-tree.
-- * We have a '*' select AND the target table has at least one data representation.
-- We ignore any '*' selects that have an aggregate function attached (i.e for COUNT(*)).
| hasStarSelect && (hasAgg || hasDataRepresentation) = rp{select = concatMap (expandStarSelectField knownColumns) selectFields}
| otherwise = rp
where
hasStarSelect = "*" `elem` map (cfName . csField) filteredSelectFields
filteredSelectFields = filter (isNothing . csAggFunction) selectFields
hasDataRepresentation = any hasOutputRep knownColumns
knownColumns = knownColumnsInContext ctx
hasOutputRep :: Column -> Bool
hasOutputRep col = HM.member (colNominalType col, outputType) representations
expandStarSelectItem :: [Column] -> (CoercibleField, Maybe Cast, Maybe Alias) -> [(CoercibleField, Maybe Cast, Maybe Alias)]
expandStarSelectItem columns (CoercibleField{cfName="*", cfJsonPath=[]}, b, c) = map (\col -> (withOutputFormat ctx $ resolveColumnField col, b, c)) columns
expandStarSelectItem _ selectItem = [selectItem]
expandStarSelectField :: [Column] -> CoercibleSelectField -> [CoercibleSelectField]
expandStarSelectField columns sel@CoercibleSelectField{csField=CoercibleField{cfName="*", cfJsonPath=[]}, csAggFunction=Nothing} =
map (\col -> sel { csField = withOutputFormat ctx $ resolveColumnField col }) columns
expandStarSelectField _ selectField = [selectField]
-- | Enforces the `max-rows` config on the result
treeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
@@ -513,6 +578,123 @@ findRel schema allRels origin target hint =
)
) $ fromMaybe mempty $ HM.lookup (QualifiedIdentifier schema origin, schema) allRels
addRelSelects :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
addRelSelects node@(Node rp forest)
| null forest = Right node
| otherwise =
let newForest = rights $ addRelSelects <$> forest
newRelSelects = mapMaybe generateRelSelectField newForest
in Right $ Node rp { relSelect = newRelSelects } newForest
generateRelSelectField :: ReadPlanTree -> Maybe RelSelectField
generateRelSelectField (Node rp@ReadPlan{relToParent=Just _, relAggAlias, relIsSpread = True} _) =
Just $ Spread { rsSpreadSel = generateSpreadSelectFields rp, rsAggAlias = relAggAlias }
generateRelSelectField (Node ReadPlan{relToParent=Just rel, select, relName, relAlias, relAggAlias, relIsSpread = False} forest) =
Just $ JsonEmbed { rsEmbedMode, rsSelName, rsAggAlias = relAggAlias, rsEmptyEmbed }
where
rsSelName = fromMaybe relName relAlias
rsEmbedMode = if relIsToOne rel then JsonObject else JsonArray
rsEmptyEmbed = null select && null forest
generateRelSelectField _ = Nothing
generateSpreadSelectFields :: ReadPlan -> [SpreadSelectField]
generateSpreadSelectFields ReadPlan{select, relSelect} =
-- We combine the select and relSelect fields into a single list of SpreadSelectField.
selectSpread ++ relSelectSpread
where
selectSpread = map selectToSpread select
selectToSpread :: CoercibleSelectField -> SpreadSelectField
selectToSpread CoercibleSelectField{csField = CoercibleField{cfName}, csAlias} =
SpreadSelectField { ssSelName = fromMaybe cfName csAlias, ssSelAggFunction = Nothing, ssSelAggCast = Nothing, ssSelAlias = Nothing }
relSelectSpread = concatMap relSelectToSpread relSelect
relSelectToSpread :: RelSelectField -> [SpreadSelectField]
relSelectToSpread (JsonEmbed{rsSelName}) =
[SpreadSelectField { ssSelName = rsSelName, ssSelAggFunction = Nothing, ssSelAggCast = Nothing, ssSelAlias = Nothing }]
relSelectToSpread (Spread{rsSpreadSel}) =
rsSpreadSel
-- When aggregates are present in a ReadPlan that will be spread, we "hoist"
-- to the highest level possible so that their semantics make sense. For instance,
-- imagine the user performs the following request:
-- `GET /projects?select=client_id,...project_invoices(invoice_total.sum())`
--
-- In this case, it is sensible that we would expect to receive the sum of the
-- `invoice_total`, grouped by the `client_id`. Without hoisting, the sum would
-- be performed in the sub-query for the joined table `project_invoices`, thus
-- making it essentially a no-op. With hoisting, we hoist the aggregate function
-- so that the aggregate function is performed in a more sensible context.
--
-- We will try to hoist the aggregate function to the highest possible level,
-- which means that we hoist until we reach the root node, or until we reach a
-- ReadPlan that will be embedded a JSON object or JSON array.
-- This type alias represents an aggregate that is to be hoisted to the next
-- level up. The first tuple of `Alias` and `FieldName` contain the alias for
-- the joined table and the original field name for the hoisted field.
--
-- The second tuple contains the aggregate function to be applied, the cast, and
-- the alias, if it was supplied by the user or otherwise determined.
type HoistedAgg = ((Alias, FieldName), (AggregateFunction, Maybe Cast, Maybe Alias))
hoistSpreadAggFunctions :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
hoistSpreadAggFunctions tree = Right $ fst $ applySpreadAggHoistingToNode tree
applySpreadAggHoistingToNode :: ReadPlanTree -> (ReadPlanTree, [HoistedAgg])
applySpreadAggHoistingToNode (Node rp@ReadPlan{relAggAlias, relToParent, relIsSpread} children) =
let (newChildren, childAggLists) = unzip $ map applySpreadAggHoistingToNode children
allChildAggLists = concat childAggLists
(newSelects, aggList) = if depth rp == 0 || (isJust relToParent && not relIsSpread)
then (select rp, [])
else hoistFromSelectFields relAggAlias (select rp)
newRelSelects = if null children
then relSelect rp
else map (hoistIntoRelSelectFields allChildAggLists) $ relSelect rp
in (Node rp { select = newSelects, relSelect = newRelSelects } newChildren, aggList)
-- Hoist aggregate functions from the select list of a ReadPlan, and return the
-- updated select list and the list of hoisted aggregates.
hoistFromSelectFields :: Alias -> [CoercibleSelectField] -> ([CoercibleSelectField], [HoistedAgg])
hoistFromSelectFields aggAlias fields =
let (newFields, maybeAggs) = foldr processField ([], []) fields
in (newFields, catMaybes maybeAggs)
where
processField field (newFields, aggList) =
let (modifiedField, maybeAgg) = modifyField field
in (modifiedField : newFields, maybeAgg : aggList)
modifyField field =
case csAggFunction field of
Just aggFunc ->
( field { csAggFunction = Nothing, csAggCast = Nothing },
Just ((aggAlias, determineFieldName field), (aggFunc, csAggCast field, csAlias field)))
Nothing -> (field, Nothing)
determineFieldName field = fromMaybe (cfName $ csField field) (csAlias field)
-- Taking the hoisted aggregates, modify the rel selects to apply the aggregates,
-- and any applicable casts or aliases.
hoistIntoRelSelectFields :: [HoistedAgg] -> RelSelectField -> RelSelectField
hoistIntoRelSelectFields aggList r@(Spread {rsSpreadSel = spreadSelects, rsAggAlias = aggAlias}) =
r { rsSpreadSel = map updateSelect spreadSelects }
where
updateSelect s =
case lookup (aggAlias, ssSelName s) aggList of
Just (aggFunc, aggCast, fldAlias) ->
s { ssSelAggFunction = Just aggFunc,
ssSelAggCast = aggCast,
ssSelAlias = fldAlias }
Nothing -> s
hoistIntoRelSelectFields _ r = r
validateAggFunctions :: Bool -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
| aggFunctionsAllowed = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
| any (isJust . csAggFunction) select = Left AggregatesNotAllowed
| otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
addFilters ctx ApiRequest{..} rReq =
foldr addFilterToNode (Right rReq) flts
@@ -586,7 +768,8 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
-- relName = "projects",
-- relToParent = Nothing,
-- relJoinConds = [],
-- relAlias = Nothing, relAggAlias = "clients_projects_1", relHint = Nothing, relJoinType = Nothing, relIsSpread = False, depth = 1
-- relAlias = Nothing, relAggAlias = "clients_projects_1", relHint = Nothing, relJoinType = Nothing, relIsSpread = False, depth = 1,
-- relSelect = []
-- },
-- subForest = []
-- }
@@ -611,7 +794,8 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
-- )
-- ],
-- order = [], range_ = fullRange, relName = "clients", relToParent = Nothing, relJoinConds = [], relAlias = Nothing, relAggAlias = "", relHint = Nothing,
-- relJoinType = Nothing, relIsSpread = False, depth = 0
-- relJoinType = Nothing, relIsSpread = False, depth = 0,
-- relSelect = []
-- },
-- subForest = subForst
-- }
@@ -767,7 +951,7 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
| "*" `elem` fldNames = ["*"]
| otherwise = returnings
where
fldNames = cfName . (\(f, _, _) -> f) <$> select
fldNames = cfName . csField <$> select
-- Without fkCols, when a mutatePlan to
-- /projects?select=name,clients(name) occurs, the RETURNING SQL part would
-- be `RETURNING name`(see QueryBuilder). This would make the embedding
@@ -809,64 +993,34 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
addFilterToLogicForest :: CoercibleFilter -> [CoercibleLogicTree] -> [CoercibleLogicTree]
addFilterToLogicForest flt lf = CoercibleStmnt flt : lf
-- | If raw(binary) output is requested, check that MediaType is one of the
-- admitted rawMediaTypes and that`?select=...` contains only one field other
-- than `*`
binaryField :: AppConfig -> MediaType -> Maybe Routine -> ReadPlanTree -> Either ApiRequestError (Maybe FieldName)
binaryField AppConfig{configRawMediaTypes} acceptMediaType proc rpTree
| isRawMediaType =
if (funcReturnsScalar <$> proc) == Just True ||
(funcReturnsSetOfScalar <$> proc) == Just True
then Right $ Just "pgrst_scalar"
else
let
fieldName = fstFieldName rpTree
in
case fieldName of
Just fld -> Right $ Just fld
Nothing -> Left $ BinaryFieldError acceptMediaType
| otherwise =
Right Nothing
-- | Do content negotiation. i.e. choose a media type based on the intersection of accepted/produced media types.
negotiateContent :: AppConfig -> ApiRequest -> QualifiedIdentifier -> [MediaType] -> MediaHandlerMap -> Bool -> Either ApiRequestError ResolvedHandler
negotiateContent conf ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} identifier accepts produces defaultSelect =
case (act, firstAcceptedPick) of
(_, Nothing) -> Left . MediaTypeError $ map MediaType.toMime accepts
(ActionMutate _, Just (x, mt)) -> Right (if rep == Just Full then x else NoAgg, mt)
-- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849
-- TODO: despite no aggregate, these are responding with a Content-Type, which is not correct.
(ActionRead True, Just (_, mt)) -> Right (NoAgg, mt)
(ActionInvoke InvHead, Just (_, mt)) -> Right (NoAgg, mt)
(_, Just (x, mt)) -> Right (x, mt)
where
isRawMediaType = acceptMediaType `elem` configRawMediaTypes `L.union` [MTOctetStream, MTTextPlain, MTTextXML] || isRawPlan acceptMediaType
isRawPlan mt = case mt of
MTPlan MTOctetStream _ _ -> True
MTPlan MTTextPlain _ _ -> True
MTPlan MTTextXML _ _ -> True
_ -> False
fstFieldName :: ReadPlanTree -> Maybe FieldName
fstFieldName (Node ReadPlan{select=(CoercibleField{cfName="*", cfJsonPath=[]}, _, _):_} []) = Nothing
fstFieldName (Node ReadPlan{select=[(CoercibleField{cfName=fld, cfJsonPath=[]}, _, _)]} []) = Just fld
fstFieldName _ = Nothing
mediaToAggregate :: MediaType -> Maybe FieldName -> ApiRequest -> ResultAggregate
mediaToAggregate mt binField apiReq@ApiRequest{iAction=act, iPreferences=Preferences{preferRepresentation=rep}} =
if noAgg then NoAgg
else case mt of
MTApplicationJSON -> BuiltinAggJson
MTSingularJSON strip -> BuiltinAggSingleJson strip
MTArrayJSONStrip -> BuiltinAggArrayJsonStrip
MTGeoJSON -> BuiltinAggGeoJson
MTTextCSV -> BuiltinAggCsv
MTAny -> BuiltinAggJson
MTOpenAPI -> BuiltinAggJson
MTUrlEncoded -> NoAgg -- TODO: unreachable since a previous step (producedMediaTypes) whitelists the media types that can become aggregates.
-- binary types
MTTextPlain -> BuiltinAggBinary binField
MTTextXML -> BuiltinAggXml binField
MTOctetStream -> BuiltinAggBinary binField
MTOther _ -> BuiltinAggBinary binField
-- Doing `Accept: application/vnd.pgrst.plan; for="application/vnd.pgrst.plan"` doesn't make sense, so we just empty the body.
-- TODO: fail instead to be more strict
MTPlan (MTPlan{}) _ _ -> NoAgg
MTPlan media _ _ -> mediaToAggregate media binField apiReq
where
noAgg = case act of
ActionMutate _ -> rep == Just HeadersOnly || rep == Just None || isNothing rep
ActionRead _isHead -> _isHead -- no need for an aggregate on HEAD https://github.com/PostgREST/postgrest/issues/2849
ActionInvoke invMethod -> invMethod == InvHead
_ -> False
firstAcceptedPick = listToMaybe $ mapMaybe matchMT accepts -- If there are multiple accepted media types, pick the first. This is usual in content negotiation.
matchMT mt = case mt of
-- all the vendored media types have special handling as they have media type parameters, they cannot be overridden
m@(MTVndSingularJSON strip) -> Just (BuiltinAggSingleJson strip, m)
m@MTVndArrayJSONStrip -> Just (BuiltinAggArrayJsonStrip, m)
m@(MTVndPlan (MTVndSingularJSON strip) _ _) -> mtPlanToNothing $ Just (BuiltinAggSingleJson strip, m)
m@(MTVndPlan MTVndArrayJSONStrip _ _) -> mtPlanToNothing $ Just (BuiltinAggArrayJsonStrip, m)
-- TODO the plan should have its own MediaHandler instead of relying on MediaType
m@(MTVndPlan mType _ _) -> mtPlanToNothing $ (,) <$> (fst <$> lookupHandler mType) <*> pure m
-- all the other media types can be overridden
x -> lookupHandler x
mtPlanToNothing x = if configDbPlanEnabled conf then x else Nothing -- don't find anything if the plan media type is not allowed
lookupHandler mt =
when' defaultSelect (HM.lookup (RelId identifier, MTAny) produces) <|> -- lookup for identifier and `*/*`
when' defaultSelect (HM.lookup (RelId identifier, mt) produces) <|> -- lookup for identifier and a particular media type
HM.lookup (RelAnyElement, mt) produces -- lookup for anyelement and a particular media type
when' :: Bool -> Maybe a -> Maybe a
when' True (Just a) = Just a
when' _ _ = Nothing
+7 -5
View File
@@ -6,11 +6,12 @@ module PostgREST.Plan.ReadPlan
import Data.Tree (Tree (..))
import PostgREST.ApiRequest.Types (Alias, Cast, Depth, Hint,
import PostgREST.ApiRequest.Types (Alias, Depth, Hint,
JoinType, NodeName)
import PostgREST.Plan.Types (CoercibleField (..),
CoercibleLogicTree,
CoercibleOrderTerm)
import PostgREST.Plan.Types (CoercibleLogicTree,
CoercibleOrderTerm,
CoercibleSelectField (..),
RelSelectField (..))
import PostgREST.RangeQuery (NonnegRange)
import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier)
@@ -28,7 +29,7 @@ data JoinCondition =
deriving (Eq, Show)
data ReadPlan = ReadPlan
{ select :: [(CoercibleField, Maybe Cast, Maybe Alias)]
{ select :: [CoercibleSelectField]
, from :: QualifiedIdentifier
, fromAlias :: Maybe Alias
, where_ :: [CoercibleLogicTree]
@@ -42,6 +43,7 @@ data ReadPlan = ReadPlan
, relHint :: Maybe Hint
, relJoinType :: Maybe JoinType
, relIsSpread :: Bool
, relSelect :: [RelSelectField]
, depth :: Depth
-- ^ used for aliasing
}

Some files were not shown because too many files have changed in this diff Show More