Compare commits

..
163 Commits
Author SHA1 Message Date
renovate[bot]andWolfgang Walther 4a58fd463b chore(deps): update actions/checkout action to v7.0.1 2026-07-23 20:11:40 +00:00
renovate[bot]andWolfgang Walther a03e008960 chore(deps): update all dependencies 2026-06-19 19:07:35 +00:00
renovate[bot]andWolfgang Walther 8c77da4ab2 chore(deps): update all dependencies to v6.0.3 2026-06-03 07:40:19 +00:00
renovate[bot]andWolfgang Walther c2043df96c chore(deps): update cachix/cachix-action digest to 5f2d7c5 2026-05-12 13:10:53 +00:00
Wolfgang Walther bebe2fbc97 docs: fix broken link 2026-04-15 09:30:25 +02:00
renovate[bot]andWolfgang Walther 63f3c7e687 chore(deps): update cachix/cachix-action action to v17 2026-03-18 21:19:00 +00:00
Wolfgang Walther 3764a68d05 ci: remove unused Dockerfile
This was used by CI, which is disabled on this branch.

Removing to prevent useless auto-updates by renovate.
2026-03-17 11:08:14 +01:00
renovate[bot]andWolfgang Walther 0866b83732 chore(deps): update all dependencies 2026-03-13 14:02:38 +00:00
Wolfgang Walther b8951ea79f ci: remove unused actions
These trigger dependency updates otherwise, which we don't need here.
2026-03-13 14:59:35 +01:00
Wolfgang Walther 7ce205286e docs: fix prometheus text format link
Reported by linkcheck.
2026-03-11 09:45:53 +01:00
dshukertjrandWolfgang Walther 63031a1726 docs: Update the Supabase logo to a correct one 2026-02-19 10:53:31 +01:00
Wolfgang Walther 79126ced77 docs: fix links
datrium.com doesn't exist anymore, while euronodes.com seems to only
fail SSL in CI.
2026-01-28 09:57:45 +01:00
renovate[bot]andWolfgang Walther 8987582428 chore(deps): update actions/upload-artifact action to v6 2025-12-13 17:12:38 +00:00
renovate[bot]andWolfgang Walther 5ff6c10a2d chore(deps): update actions/cache action to v5.0.1 2025-12-13 11:31:26 +00:00
renovate[bot]andWolfgang Walther b65358933a chore(deps): update actions/cache action to v5 2025-12-12 08:29:26 +00:00
renovate[bot]andWolfgang Walther 32e1b1ccbd chore(deps): update actions/checkout action to v6 2025-12-03 13:52:43 +00:00
renovate[bot]andWolfgang Walther 966129f29b chore(deps): update actions/checkout action to v5.0.1 2025-11-18 09:38:33 +00:00
renovate[bot]andWolfgang Walther 163e6b31e0 chore(deps): update ubuntu:noble docker digest to c35e29c 2025-11-15 13:52:18 +00:00
renovate[bot]andWolfgang Walther 4470346049 chore(deps): update actions/upload-artifact action to v5 2025-10-25 08:12:49 +00:00
renovate[bot]andWolfgang Walther 38bdb55d06 chore(deps): update ubuntu:noble docker digest to 66460d5 2025-10-13 10:21:37 +00:00
renovate[bot]andWolfgang Walther 1295552a66 chore(deps): update ubuntu:noble docker digest to 59a458b 2025-10-10 07:33:53 +00:00
renovate[bot]andWolfgang Walther 0a30718f51 chore(deps): update ubuntu:noble docker digest to 728785b 2025-10-03 13:34:20 +00:00
renovate[bot]andWolfgang Walther fee33d4cdf chore(deps): update ubuntu:noble docker digest to fdb6c9c 2025-10-02 10:04:05 +00:00
renovate[bot]andWolfgang Walther 88e57f7849 chore(deps): update nixbuild/nix-quick-install-action action to v34 2025-09-25 08:22:29 +00:00
renovate[bot]andWolfgang Walther 22e54d52bf chore(deps): update actions/cache action to v4.3.0 2025-09-25 08:22:04 +00:00
renovate[bot]andWolfgang Walther d07aba9745 chore(deps): update ubuntu:noble docker digest to 353675e 2025-09-17 10:32:26 +00:00
renovate[bot]andWolfgang Walther e0aa59163c chore(deps): update ubuntu:noble docker digest to 590e57a 2025-09-16 09:01:42 +00:00
renovate[bot]andWolfgang Walther f66f8a15cd chore(deps): update ubuntu:noble docker digest to 9cbed75 2025-09-03 13:53:30 +00:00
renovate[bot]andWolfgang Walther b970c78a43 chore(deps): update ubuntu:noble docker digest to f3b7f1b 2025-09-02 08:16:47 +00:00
renovate[bot]andWolfgang Walther d13a39655d chore(deps): update nixbuild/nix-quick-install-action action to v33 2025-08-25 13:55:39 +00:00
renovate[bot]andWolfgang Walther 2e341ac825 chore(deps): update ubuntu:noble docker digest to 7c06e91 2025-08-13 07:19:13 +00:00
renovate[bot]andWolfgang Walther fcb163198d chore(deps): update actions/checkout action to v5 2025-08-11 15:31:59 +00:00
renovate[bot]andWolfgang Walther 68cea0750b chore(deps): update actions/cache action to v4.2.4 2025-08-07 14:24:02 +00:00
renovate[bot]andWolfgang Walther c5a984b874 chore(deps): update ubuntu:noble docker digest to a08e551 2025-07-19 12:16:50 +00:00
Wolfgang Walther 930d0ae6d7 docs: adjust some redirects
Those were reported in the weekly linkcheck.
2025-07-16 11:04:42 +02:00
renovate[bot]andWolfgang Walther 830edaeedf chore(deps): update ubuntu:noble docker digest to c4570d2 2025-07-16 09:00:05 +00:00
renovate[bot]andWolfgang Walther e925665df6 chore(deps): update ubuntu:noble docker digest to e356c06 2025-07-16 08:51:14 +00:00
renovate[bot]andWolfgang Walther 9625b39873 chore(deps): update ubuntu:noble docker digest to 440dcf6 2025-07-09 07:08:50 +00:00
steve-chavezandWolfgang Walther 5bcdeba526 docs: clarify health checks empty response body 2025-07-08 20:18:39 +02:00
Wolfgang Walther 8f8efbaa12 ci: disable all CI except docs
IO tests fail randomly and we don't need to run everything if we only
ever update the docs on this branch from now on.
2025-07-05 22:18:49 +02:00
Taimoor ZaeemandWolfgang Walther 9675ab239b docs: horizontal filtering on table-valued functions 2025-07-05 21:10:05 +02:00
Wolfgang Walther 4a16dd924b ci: fix loadtest job on stable branches
Previously, the loadtest would always compare against main and the
latest tag. This meant a loadtest running on the v13 release branch,
would compare to a *future* version of both. This is not useful, and
also not supported by `postgrest-loadtest-against`, which recently
introduced a change on `main`, that now breaks the tests on the back
branches.

With this change, the loadtest will always run against the current
target branch of a PR, so against the v13 branch for a PR against v13,
for example. Also, it will compare against the latest released tag *for
that branch*.

Thus, when running this for v12, it will test against the v12 branch and
the v12.2.12 tag.
2025-06-26 10:51:12 +00:00
Wolfgang Walther 17fcc5f47b ci: update Nix to 2.29.1
Related:
https://discourse.nixos.org/t/security-advisory-privilege-escalations-in-nix-lix-and-guix/66017
2025-06-25 12:59:01 +02:00
Thilo HohltandWolfgang Walther 38f1b06122 Update ecosystem.rst
The repository has been moved from a dedicated organisation to my personal profile, so this old link will no longer work after 90 days.
2025-05-31 13:50:17 +02:00
steve-chavezandWolfgang Walther 0031ec3ed0 docs: remove Greenplum integration
They're not really a sponsor, so it's not fair to include them.
2025-05-30 14:36:37 +02:00
Wolfgang Walther ec2f2e7f33 docs: apply redirects
Those redirect, so we might as well hardcode the them.
2025-05-28 09:16:36 +02:00
Wolfgang Walther 7538e4640f docs: remove outdated "in production" links
Moat has been bought by Oracle. The advertising subpage redirects to
oracle.com, but pointing at that would be a bit misleading.

No need to keep failing links either.
2025-05-28 09:16:11 +02:00
steve-chavez cd3cf9ed97 bump version to 12.2.12 2025-05-01 20:24:30 -05:00
steve-chavez 1f28efa9bd fix: don't enable admin server /config by default
This now requires setting `admin-server-config-enabled`.
2025-05-01 20:24:08 -05:00
steve-chavez 36eb72c2a0 bump version to 12.2.11 2025-04-21 17:08:00 -05:00
Taimoor ZaeemandSteve Chavez 38c596800a fix: regression with parameter charset=utf-8 in mediatype 2025-04-21 17:06:26 -05:00
steve-chavez a7f9181462 bump version to 12.2.10 2025-04-18 21:28:00 -05:00
Michal KleczekandSteve Chavez f68d5944e6 fix: purge JWT cache asynchronously in a separate thread
Otherwise performance was reduced unnecessarily.
2025-04-18 21:27:36 -05:00
Wolfgang Walther b454f29b2c bump version to 12.2.9 2025-04-16 20:39:19 +02:00
Taimoor ZaeemandWolfgang Walther a3cc762f17 fix: valid JWTs after jwt-secret is changed in a config reload 2025-04-16 18:26:42 +00:00
Taimoor ZaeemandWolfgang Walther 66cd4ca596 fix: regression that replaces an unknown media type with */* (#4013) 2025-04-14 18:00:21 +02:00
Taimoor ZaeemandWolfgang Walther 513704dc2b fix: parsing of the for parameter of plan media type (#4005) 2025-04-12 15:37:37 +00:00
Thilo HohltandWolfgang Walther 026d84a093 docs: add archtika to example apps section on ecosystem page (#4007) 2025-04-11 14:34:56 +02:00
Taimoor ZaeemandWolfgang Walther 106f193a56 docs: add note that ordering of columns is not enforced (#3999) 2025-04-10 14:44:03 +02:00
steve-chavezandWolfgang Walther 79fc43ec39 docs: redirect from broken #bulk-insert-default 2025-04-04 18:24:26 +02:00
Taimoor ZaeemandWolfgang Walther b187f09574 docs: mention that updates also supports specifying columns and missing pref 2025-04-04 18:24:25 +02:00
Taimoor ZaeemandWolfgang Walther 4f1dc72e40 docs: explain missing preference header 2025-04-04 18:24:23 +02:00
renovate[bot]andWolfgang Walther d70d5cb19d chore(deps): update peter-evans/dockerhub-description action to v4.0.2 2025-04-03 12:19:48 +00:00
renovate[bot]andWolfgang Walther 6e851cce59 chore(deps): update peter-evans/dockerhub-description action to v4.0.1 2025-04-01 20:25:14 +00:00
renovate[bot]andWolfgang Walther accf46d2ee chore(deps): update all dependencies 2025-03-27 19:51:24 +00:00
renovate[bot]andWolfgang Walther 852f833d56 chore(deps): update all dependencies 2025-03-27 19:32:19 +00:00
Wolfgang Walther 48e6267dde docs: Remove broken link 2025-03-26 17:00:31 +01:00
Wolfgang Walther ed86bbf3de docs: remove broken link 2025-03-14 23:21:41 +01:00
Wolfgang Walther 3ba4290ef0 chore: Fix style check 2025-02-22 17:01:27 +01:00
Wolfgang Walther 0be047a52f docs: Fix outdated links 2025-02-22 16:10:50 +01:00
Wolfgang Walther d2d3367046 ci: Fix cirrus FreeBSD builds
Apparently Cirrus removed the 14-1 image. When I firsted looked into
this some days ago, when the job started failing, the docs were not
updated, yet - so it wasn't clear. Now the docs mention freebsd-14-2
explicitly...
2025-02-22 14:22:29 +01:00
Laurence IslaandWolfgang Walther ba6f985dac docs: make the aggregate functions docs less verbose 2025-02-22 13:19:10 +01:00
steve-chavezandWolfgang Walther 7cb007567b docs: reduce verbosity of aggregate functions 2025-02-22 13:19:10 +01:00
Taimoor ZaeemandWolfgang Walther acb72b7c67 docs: add missing jwt claims and clock skew (#3908) 2025-02-22 13:19:10 +01:00
steve-chavezandWolfgang Walther 8649d27709 docs: correct package for installation under Nix 2025-02-22 13:19:10 +01:00
steve-chavez 4a3936f71c bump version to 12.2.8 2025-02-10 13:25:22 -05:00
M. Taimoor Zaeemandsteve-chavez 9b6b5d06ea fix: log 503 client error to stderr 2025-02-10 13:24:52 -05:00
Wolfgang Walther ec83d08be6 bump version to 12.2.7 2025-02-03 18:38:41 +01:00
Diogo BiazusandWolfgang Walther c026d0752f fix: Fix regression for schema cache reloading via NOTIFY on Windows
Upstream accidentally removed the fix, which was introduced for #2524. Fixed again.

(cherry picked from commit b285f5fba6)
2025-02-03 18:35:24 +01:00
steve-chavez dc96cdb0c3 bump version to 12.2.6 2025-01-29 15:22:22 -05:00
Taimoor Zaeemandsteve-chavez 89be285e6a fix: jwt cache is not purged (#3801) 2025-01-29 15:14:05 -05:00
renovate[bot]andWolfgang Walther c6b4fca160 chore(deps): update codecov/codecov-action action to v5.3.1 2025-01-25 12:01:21 +01:00
renovate[bot]andWolfgang Walther 31cccd3264 chore(deps): update codecov/codecov-action action to v5.3.0 2025-01-25 11:12:46 +01:00
Wolfgang Walther 0bec15fb71 bump version to 12.2.5 2025-01-20 18:24:51 +01:00
Wolfgang Walther cc22da5a02 fix: Make postgrest binary in arm64 docker image executable
This happened in 06aebfaa and caused the arm64 docker image to not start
up properly.

Resolves #3867
2025-01-20 18:24:06 +01:00
Wolfgang Walther 4d477d6e12 bump version to 12.2.4 2025-01-18 19:01:16 +01:00
Wolfgang Walther aff2a78aac Revert "ci: Remove brew install libpq for macos-14 stack build"
This partially reverts commit 53164453d8.
2025-01-18 19:01:16 +01:00
Wolfgang Walther 868f4a066b ci: Fix stack cache on Windows
Apparently the STACK_ROOT has been moved to C:\sr - for unknown reasons,
at least to me.

This should enable caching again and make the stack on windows builds
much faster than recently.
2025-01-18 17:54:42 +01:00
Wolfgang Walther 2c04b98dd9 ci: Split ci into ci and release workflows
This is now possible, after we moved to the ARM build to the GitHub
runners.
2025-01-18 17:54:42 +01:00
Wolfgang Walther 71c73b20ca ci: Remove left-over permissions setting from tag job
This has been replaced by using the SSH key.
2025-01-18 17:54:42 +01:00
Wolfgang Walther 0160b6f370 ci: Display loadtest results in step summary
Much easier to implement and should be easier to find, too.
2025-01-18 17:54:42 +01:00
Wolfgang Walther 038c05447a ci: Build the ubuntu-aarch64 binary with new ARM runners
The new GitHub arm runners are available, so we can use them to build
the ubuntu aarch64 binary instead of our custom machine.
2025-01-18 17:54:42 +01:00
Wolfgang Walther 5d5ee71dce ci: Skip cachix push when no cachix token is set
This happens in forks.
2025-01-18 17:54:42 +01:00
Wolfgang Walther f09b4f4b7d ci: Remove brew install libpq for macos-14 stack build
When macos-14 was rolled out libpq was not installed, but by now it is
by default. Thus, we don't need to do that, it only creates a warning
annotation right now.
2025-01-18 17:54:42 +01:00
Wolfgang Walther d3f9de528c ci: Fix release name of x86-64 binaries
This should have been x86-64, only x64 is not a thing.
2025-01-18 17:54:42 +01:00
Wolfgang Walther c3f88d7a1d ci: Fix release name of macos binary
This is built on macos-14, which is running on new arm based hardware,
not the old x86_64 ones.
2025-01-18 17:54:42 +01:00
Wolfgang Walther 164e6a601f ci: Update stack builder to ubuntu 24.04
Renovate doesn't seem to pick this up, because it's in a matrix
specification.
2025-01-18 17:54:42 +01:00
Andrei DziahelandWolfgang Walther c135a30515 ci: drop directories from windows release
Puts windows release in line with others which have the executable on the top level
2025-01-18 17:54:32 +01:00
renovate[bot]andWolfgang Walther 7515eb5490 chore(deps): update all dependencies 2025-01-17 20:59:24 +01:00
renovate[bot]andWolfgang Walther 629b57337c chore(deps): update haskell-actions/setup action to v2.7.8 2024-12-30 16:31:53 +01:00
M. Taimoor ZaeemandWolfgang Walther 9fcbf994cb fix: insert with missing=default uses column default before using domain default 2024-12-25 11:43:34 +01:00
renovate[bot]andWolfgang Walther 7b9027ea0a chore(deps): update all dependencies 2024-12-20 20:00:36 +01:00
renovate[bot]andWolfgang Walther 0a7ae1815c chore(deps): update dependency macos to v14 2024-12-18 13:52:31 +01:00
renovate[bot]andWolfgang Walther 06014047cb chore(deps): update actions/upload-artifact action to v4.5.0 2024-12-18 12:56:42 +01:00
renovate[bot]andWolfgang Walther 7feb755521 chore(deps): update all dependencies 2024-12-06 09:48:27 +01:00
renovate[bot]andWolfgang Walther a0e7c4f591 chore(deps): update ubuntu:noble docker digest to 80dd3c3 2024-12-04 08:45:16 +01:00
renovate[bot]andWolfgang Walther 572f24901e chore(deps): update codecov/codecov-action action to v5.0.7 2024-11-21 21:26:15 +01:00
renovate[bot]andWolfgang Walther 1e599a2319 chore(deps): update codecov/codecov-action action to v5.0.5 2024-11-20 19:36:28 +01:00
steve-chavezandWolfgang Walther 4e9601d155 fix: always show schema cache load time
It used to be that this was only enabled with log-level=debug.
But the default log-level is misleading, for example:

```
$ PGRST_DB_SCHEMAS="apflora" postgrest-with-postgresql-16  -f test/io/big_schema.sql postgrest-run

...
13/Nov/2024:22:08:20 -0500: Config reloaded
13/Nov/2024:22:08:20 -0500: Schema cache queried in 36.3 milliseconds
13/Nov/2024:22:08:20 -0500: Schema cache loaded 326 Relations, 305 Relationships, 7 Functions, 0 Domain Representations, 4 Media Type Handlers, 1194 Timezones
```

The "Schema cache loaded" can take a while to appear, yet the 22:08:20
time is the same. If we reveal the load time this is clarified:

```
13/Nov/2024:22:08:37 -0500: Schema cache loaded in 16770.1 milliseconds
```
2024-11-16 22:41:44 +01:00
renovate[bot]andWolfgang Walther 53ce2df33a chore(deps): update ubuntu:noble docker digest to 278628f 2024-11-16 22:41:14 +01:00
Laurence IslaandWolfgang Walther c7152787b9 docs: clarify usage of upsert with surrogate primary keys 2024-11-16 13:16:35 +01:00
renovate[bot]andWolfgang Walther 4d4f93da40 chore(deps): update codecov/codecov-action action to v5.0.2 2024-11-15 18:54:10 +01:00
renovate[bot]andWolfgang Walther cb22931015 chore(deps): update codecov/codecov-action action to v5 2024-11-14 19:39:58 +01:00
renovate[bot]andWolfgang Walther 4cc09c6b42 chore(deps): update all dependencies 2024-11-06 19:43:33 +01:00
Wolfgang WaltherandWolfgang Walther 3379ee3483 chore(deps): Update nixpkgs to unstable 2024-05-29
This makes actionlint happy about the new ubuntu runners.
2024-11-06 17:35:55 +01:00
Wolfgang Walther fc7f715aeb ci: Update cirrus' freebsd image to 14.1
This should fix CI which is failing lately like this:
https://cirrus-ci.com/task/4665005218463744

ld-elf.so.1: /lib/libc.so.7: version FBSD_1.8 required by
/usr/local/bin/stack not found
2024-10-26 14:52:13 +02:00
renovate[bot]andWolfgang Walther ec568efd06 chore(deps): update all dependencies 2024-10-24 05:10:55 +02:00
Wolfgang Walther 5b755b2877 docs: Remove broken link
https://github.com/PostgREST/postgrest/actions/runs/11136795565/job/30949162312
2024-10-04 16:02:33 +02:00
renovate[bot]andWolfgang Walther f1453f3c37 chore(deps): update ubuntu:noble docker digest to dfc1087 2024-09-18 20:01:57 +02:00
renovate[bot]andWolfgang Walther 9100ba3b70 chore(deps): update dependency urllib3 to v2.2.3 2024-09-12 21:01:08 +02:00
renovate[bot]andWolfgang Walther 6fe82d9e94 chore(deps): update actions/upload-artifact action to v4.4.0 2024-09-04 13:29:55 +02:00
Jason CloseandGitHub 1fff248c78 docs: rpc example for array of json objects
This change adds an explanation of how to handle an array of JSON objects within an RPC call.  To pass multiple objects, an array of JSON objects must be the JSON value, with the key being the json or jsonb variable name of the Postgres function.

For people who want to perform multiple tasks/inserts/updates within a single API call, this is a needed explanation for that use-case.
2024-08-23 13:00:19 -05:00
renovate[bot]andWolfgang Walther f5264c5aa3 chore(deps): update ubuntu:noble docker digest to 8a37d68 2024-08-18 14:04:26 +02:00
renovate[bot]andLaurence Isla 5de55ea141 chore(deps): update all dependencies 2024-08-13 13:25:12 -05:00
renovate[bot]andWolfgang Walther 1ef83d3732 chore(deps): update actions/upload-artifact action to v4.3.5 2024-08-02 21:07:44 +02:00
Laurence Isla 519615d5fb bump version to 12.2.3 2024-08-01 11:46:52 -05:00
Laurence Isla de5a8954b2 changelog: add missing entry for 3670 2024-08-01 11:18:15 -05:00
64b5a59410 fix: schema cache loading before the in-db config (#3670)
Fixes #3660. Load the config after getting the pg version but before loading the schema.

The regression happened on f09655b.

Also remove schema cache load wrapper and separate db queries in different functions.

Co-authored-by: Laurence Isla <lau.isla.c@gmail.com>
2024-08-01 17:41:07 +02:00
Dan KurinandWolfgang Walther 417a96d137 fix: Remove OpenAPI format for rowFilter params (#3661) 2024-08-01 17:41:07 +02:00
Wolfgang Walther 53593824a8 fix: Embed One-to-One relationship with different column order properly 2024-07-13 22:27:04 +02:00
Wolfgang Walther e642086d27 chore: Fix changelog after 13868983 2024-07-13 17:16:06 +02:00
steve-chavezandWolfgang Walther e451700d5d chore: remove links to gitter
We'll now use github discussions for support.
2024-07-13 17:11:06 +02:00
Salim BandWolfgang Walther 13868983af fix: update OpenAPI externalDocs URL
fixes https://github.com/PostgREST/postgrest/issues/3091
2024-07-13 17:11:06 +02:00
steve-chavez db9da0b1ca bump version to 12.2.2 2024-07-10 23:07:23 -05:00
steve-chavez f09655b7a6 fix: schema cache retrying without backoff
Fixes https://github.com/PostgREST/postgrest/issues/3523.

Now if there's a failure when obtaining the pg version OR schema cache,
we do the same retrying process. This way we don't add two retries.

Refactors and renames the "connectionWorker" to "schemaCacheLoader".
This makes more sense since what we really want is the schema cache,
the version is the pre-requisite for ensuring our
schema cache queries work.

Additionally, we no longer log ` Attempting to connect to the database...`
at startup unnecessarily. This is only logged whenever there's a retry attempt.
2024-07-10 21:11:20 -05:00
steve-chavezandWolfgang Walther 8715e426c0 chore: remove paypal links
It was tied to a personal account and donations there have been too rare.
2024-07-10 21:13:23 +02:00
Wolfgang WaltherandWolfgang Walther 9a40bc266e fix: List correct enum options when multiple types with same name are present
The schema cache and OpenAPI output would currently list the first found
enum with the same name instead of the correct type. One other case
where this comes up is when a regular type and an enum type have the
same name. For example in the spec fixtures, we have an enum called
"bit". Every "bit" type, no matter whether it's that enum or the
built-in bit type, will show those enum options in the OpenApi output.

Not adding a test, because OpenAPI is supposed to go away in the future
anyway.
2024-07-09 09:09:22 +02:00
Wolfgang WaltherandWolfgang Walther e67461b991 fix: Show number of loaded timezones in log output
There is no reason to hide those, right?
2024-07-09 09:09:22 +02:00
Wolfgang WaltherandWolfgang Walther 420933ceba fix: Make --dump-schema work with in-database pgrst.db_schemas setting
This needs to be loaded from in-database configuration first, otherwise
the dump-schema output will be for the default (public) schema.
2024-07-09 09:09:22 +02:00
Laurence IslaandWolfgang Walther 8f8dbd3425 fix: nested empty embeds no longer return empty values and are correctly omitted 2024-07-09 09:09:22 +02:00
renovate[bot]andWolfgang Walther 8d7d7e4112 chore(deps): update all dependencies 2024-07-06 11:35:11 +02:00
Sandro BauerandWolfgang Walther 521acf61cb docs: fix rendering for inline code block in operator list 2024-07-02 12:28:24 +02:00
steve-chavez d3f7cba92e bump version to 12.2.1 2024-06-27 14:59:35 -05:00
Laurence IslaandSteve Chavez a71d9ea50f changelog: add missing entries for #3592 and #3616 2024-06-27 14:08:16 -05:00
Laurence IslaandSteve Chavez 5a1e9b01e4 nix: remove texlive dependencies from postgrest-docs-render 2024-06-27 14:08:16 -05:00
Laurence IslaandSteve Chavez d3292d42de docs: use PlantUML instead of Latex to generate Schema Isolation image 2024-06-27 14:08:16 -05:00
Laurence IslaandSteve Chavez 572581cbb6 chore: organize diagrams in different folders 2024-06-27 14:08:16 -05:00
steve-chavez 9daddb549c docs: add listener recovery 2024-06-27 14:08:16 -05:00
steve-chavez 716fcbf147 fix: don't reload cache on every listener fail
Revert "prevent GSSAPI error between Listener and pool"

This reverts commit 4beac10d3d.
2024-06-27 14:08:16 -05:00
Laurence IslaandSteve Chavez 5b62501ce7 docs: improve architecture diagram
- SVG format instead of PNG
- The components now have links to their reference in the Docs
- Supports dark mode
2024-06-27 14:08:16 -05:00
Wolfgang WaltherandSteve Chavez 7860e7287f docs: Fix punctuation in install.rst 2024-06-27 14:08:16 -05:00
Michal KleczekandSteve Chavez c958f94753 docs: add pg-notify-stdout to ecosystem 2024-06-27 14:08:16 -05:00
renovate[bot]andWolfgang Walther 94f0548344 chore(deps): update ubuntu:noble docker digest to 2e863c4 2024-06-19 19:22:47 +02:00
Wolfgang Walther 16d0e1ca8c fix: Treat pre-release and docs versions correctly for new release workflow
Since we changed our release workflow, we have adjusted:
- the docs to use postgrest.org/en/v12/ -style URLs, i.e. only using the
major component.
- the pre-release / devel versions to contain only two instead of four
version parts, i.e. currently 12.3.
2024-06-19 08:42:34 +02:00
Wolfgang Walther 9657d46c42 docs: Fix linkcheck
Some URLs are still forbidden for our linkcheck tool, so disabling them
again.

Others are permanently redirected, so adjusting them.
2024-06-19 08:40:58 +02:00
renovate[bot]andWolfgang Walther 70e07ccbc6 chore(deps): update dependency urllib3 to v2.2.2 2024-06-17 21:04:03 +02:00
renovate[bot]andWolfgang Walther cc5c4ae5ef chore(deps): update codecov/codecov-action action to v4.5.0 2024-06-13 19:51:15 +02:00
renovate[bot]andWolfgang Walther af8b62ba10 chore(deps): update all dependencies 2024-06-13 08:46:23 +02:00
Laurence Isla d4e7d23268 docs: fix example of listener failure on read replicas 2024-06-12 19:46:23 -05:00
Laurence Isla 4e084b664c docs: clarify what is logged when "log-level=debug" 2024-06-12 18:30:19 -05:00
Laurence Isla 91024b6eab docs: add missing logs to stderr
- Schema cache stats are now logged to stderr
- Log when the LISTEN channel gets a notification
2024-06-12 18:30:19 -05:00
Laurence IslaandSteve Chavez 0e4fcd7387 docs: add "Listener" page
Co-authored-by: Steve Chavez <stevechavezast@gmail.com>
2024-06-12 18:30:19 -05:00
Wolfgang Walther 49a7aa9933 bump docs version to 12.2 2024-06-12 07:47:17 +02:00
195 changed files with 4485 additions and 7799 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
freebsd_instance: freebsd_instance:
image_family: freebsd-14-3 image_family: freebsd-14-2
build_task: build_task:
# Don't change this name without adjusting .github/workflows/build.yaml # Don't change this name without adjusting .github/workflows/build.yaml
@@ -35,7 +35,7 @@ build_task:
- find main src -type f -iname '*.hs' -exec md5sum "{}" + - find main src -type f -iname '*.hs' -exec md5sum "{}" +
build_script: | build_script: |
stack build -j 1 --local-bin-path . --copy-bins stack build -j 1 --local-bin-path . --copy-bins --stack-yaml stack-21.7.yaml
strip postgrest strip postgrest
bin_artifacts: bin_artifacts:
-9
View File
@@ -1,9 +0,0 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 2
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
+17
View File
@@ -0,0 +1,17 @@
<!--
Before reporting a bug:
If your database schema has changed while the PostgREST server is running,
send the server a SIGUSR1 signal or restart it(http://postgrest.org/en/stable/admin.html#schema-reloading)
to ensure the schema cache is not stale. This sometimes fixes apparent bugs.
-->
### Environment
* PostgreSQL version: (if using docker, specify the image)
* PostgREST version: (if using docker, specify the image)
* Operating system:
### Description of issue
(Expected behavior vs actual behavior)
(Steps to reproduce: Include a minimal SQL definition plus how you make the request to PostgREST and the response body)
-28
View File
@@ -1,28 +0,0 @@
---
name: Bug report
about: Create a bug report to help us improve
type: Bug
title: ''
labels: ''
assignees: ''
---
<!--
Before reporting a bug:
If your database schema has changed while the PostgREST server is running,
send the server a SIGUSR1 signal or restart it (http://postgrest.org/en/stable/admin.html#schema-reloading) to ensure the schema cache is not stale. This sometimes fixes apparent bugs.
-->
### Environment
* PostgreSQL version: (if using docker, specify the image)
* PostgREST version: (if using docker, specify the image)
* Operating system:
### Description of issue
Describe the behavior you expected vs the actual behavior. Include:
- A minimal SQL definition.
- How you make the request to PostgREST (curl command preferred).
- The PostgREST response.
-1
View File
@@ -1 +0,0 @@
blank_issues_enabled: false
-17
View File
@@ -1,17 +0,0 @@
---
name: Feature request
about: Suggest an enhancement for this project
type: Feature
title: ''
labels: ''
assignees: ''
---
## Problem
A clear and concise description of what the problem is.
## Solution
A clear and concise description of what you want to happen.
@@ -1,119 +0,0 @@
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ inputs.upload }}
path: ${{ steps.download.outputs.artifacts }}
if-no-files-found: error
-35
View File
@@ -1,35 +0,0 @@
name: Cache on main
description: Stores caches on main and release branches only, but restores them on all branches.
inputs:
path:
description: Path(s) to cache
required: true
save-prs:
description: Whether to additionally store the cache in a pull request, too. Should only be used for very small caches.
type: boolean
prefix:
description: Cache key prefix to be used in both primary key and restore-keys.
required: true
suffix:
description: Cache key suffix to be used only in primary key.
required: true
runs:
using: composite
steps:
- uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
restore-keys: |
${{ runner.os }}-${{ inputs.prefix }}-
- uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
with:
path: ${{ inputs.path }}
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
restore-keys: |
${{ runner.os }}-${{ inputs.prefix }}-
+2 -2
View File
@@ -11,12 +11,12 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- uses: nixbuild/nix-quick-install-action@1f095fee853b33114486cfdeae62fa099cda35a9 # v33 - uses: nixbuild/nix-quick-install-action@9f63be77f412a248c9d9a65a4c82cf066cdf8f0c # v35
with: with:
nix_conf: |- nix_conf: |-
always-allow-substitutes = true always-allow-substitutes = true
max-jobs = auto max-jobs = auto
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@5f2d7c5294214f71b873db4b969586b980625e71 # v17
with: with:
name: postgrest name: postgrest
authToken: ${{ inputs.authToken }} authToken: ${{ inputs.authToken }}
+4 -19
View File
@@ -8,33 +8,18 @@
"/^v[0-9]+/" "/^v[0-9]+/"
], ],
"rebaseWhen": "conflicted", "rebaseWhen": "conflicted",
"pip_requirements": {
"enabled": false
},
"packageRules": [ "packageRules": [
{
"matchBaseBranches": [ "/^v[0-9]+/" ],
"matchManagers": ["haskell-cabal"],
"enabled": false
},
{ {
"matchBaseBranches": [ "/^v[0-9]+/" ], "matchBaseBranches": [ "/^v[0-9]+/" ],
"groupName": "all dependencies" "groupName": "all dependencies"
}, },
{ {
"matchManagers": ["haskell-cabal"], "matchPackageNames": ["docutils"],
"matchPackageNames": ["base", "bytestring", "containers", "directory", "mtl", "parsec", "process", "text"], "allowedVersions": "<0.21.0"
"groupName": "GHC dependencies"
}, },
{ {
"matchManagers": ["haskell-cabal"], "matchPackageNames": ["macos"],
"matchPackageNames": ["hasql", "hasql-dynamic-statements", "hasql-notifications", "hasql-transaction", "hasql-pool"], "allowedVersions": "<13"
"groupName": "hasql"
},
{
"matchManagers": ["haskell-cabal"],
"matchPackageNames": ["fuzzyset"],
"allowedVersions": "<0.3"
} }
] ]
} }
-210
View File
@@ -1,210 +0,0 @@
name: Build
on:
workflow_call:
secrets:
CACHIX_AUTH_TOKEN:
required: false
pull_request:
branches:
- main
- v[0-9]+
paths:
- .github/workflows/build.yaml
- .github/actions/**
- .github/scripts/**
- .github/*
- '*.nix'
- nix/**
- .cirrus.yml
- cabal.project*
- postgrest.cabal
- stack.yaml*
- '**.hs'
- '!**.md'
concurrency:
# Terminate all previous runs of the same workflow for pull requests
group: build-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
static:
name: Nix - Linux x86-64 static
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build static executable
run: nix-build -A postgrestStatic
- name: Save built executable as artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: postgrest-linux-static-x86-64
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: postgrest-docker-x86-64
path: postgrest-docker.tar.gz
if-no-files-found: error
macos:
name: Nix - MacOS
runs-on: macos-14
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Install gnu sed
run: brew install gnu-sed
- name: Build everything
run: |
# The --dry-run will give us a list of derivations to download from cachix and
# derivations to build. We only take those that would have to be built and then build
# those explicitly. This has the advantage that pure verification will not include
# a download anymore, making it much faster. If something needs to be built, only
# the dependencies required to do so will be downloaded, but not everything.
nix-build --dry-run 2>&1 \
| gsed -e '1,/derivations will be built:$/d' -e '/paths will be fetched/Q' \
| xargs nix-build
stack:
strategy:
fail-fast: false
matrix:
include:
- name: Linux aarch64
runs-on: ubuntu-24.04-arm
cache: |
~/.stack/pantry
~/.stack/snapshots
~/.stack/stack.sqlite3
artifact: postgrest-ubuntu-aarch64
deps: sudo apt-get update && sudo apt-get install libpq-dev
- name: MacOS aarch64
runs-on: macos-14
cache: |
~/.stack/pantry
~/.stack/snapshots
~/.stack/stack.sqlite3
artifact: postgrest-macos-aarch64
deps: brew link --force libpq
- name: MacOS x86-64
runs-on: macos-13
cache: |
~/.stack/pantry
~/.stack/snapshots
~/.stack/stack.sqlite3
artifact: postgrest-macos-x86-64
deps: brew link --force libpq
- name: Windows
runs-on: windows-2022
cache: |
C:\sr\pantry
C:\sr\snapshots
C:\sr\stack.sqlite3
deps: Add-Content $env:GITHUB_PATH $env:PGBIN
artifact: postgrest-windows-x86-64
name: Stack - ${{ matrix.name }}
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: haskell-actions/setup@7909071ceec0344debcc968c6c7a96a52e8dd0d7 # v2.8.1
with:
# This must match the version in stack.yaml's resolver
ghc-version: 9.6.7
enable-stack: true
stack-no-global: true
stack-setup-ghc: true
- name: Cache ~/.stack
uses: ./.github/actions/cache-on-main
with:
path: ${{ matrix.cache }}
prefix: stack
suffix: ${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
- name: Cache .stack-work
uses: ./.github/actions/cache-on-main
with:
path: .stack-work
save-prs: true
prefix: stack-work-${{ hashFiles('postgrest.cabal', 'stack.yaml.lock') }}
suffix: ${{ hashFiles('main/**/*.hs', 'src/**/*.hs') }}
- name: Install dependencies
if: matrix.deps
run: ${{ matrix.deps }}
- name: Build with Stack
run: stack build --lock-file error-on-write --local-bin-path result --copy-bins
- name: Strip Executable
run: strip result/postgrest*
- name: Save built executable as artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ matrix.artifact }}
path: |
result/postgrest
result/postgrest.exe
if-no-files-found: error
freebsd:
name: Stack - FreeBSD from CirrusCI
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: ./.github/actions/artifact-from-cirrus
with:
token: ${{ github.token }}
task: Build FreeBSD (Stack)
download: bin
upload: postgrest-freebsd-x86-64
cabal:
strategy:
matrix:
ghc: ['9.6.7', '9.8.4']
fail-fast: false
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: haskell-actions/setup@7909071ceec0344debcc968c6c7a96a52e8dd0d7 # v2.8.1
with:
ghc-version: ${{ matrix.ghc }}
- name: Cache .cabal
uses: ./.github/actions/cache-on-main
with:
path: |
~/.cabal/packages
~/.cabal/store
prefix: cabal-${{ matrix.ghc }}-${{ hashFiles('cabal.project.freeze') }}
suffix: ${{ hashFiles('postgrest.cabal', 'cabal.project') }}
- name: Cache dist-newstyle
uses: ./.github/actions/cache-on-main
with:
path: dist-newstyle
save-prs: true
prefix: cabal-${{ matrix.ghc }}-dist-newstyle-${{ hashFiles('postgrest.cabal', 'cabal.project', 'cabal.project.freeze') }}
suffix: ${{ hashFiles('**/*.hs') }}
- name: Install dependencies
run: cabal build --only-dependencies --enable-tests --enable-benchmarks
- name: Build
run: cabal build --enable-tests --enable-benchmarks all
-32
View File
@@ -1,32 +0,0 @@
name: Check
on:
workflow_call:
secrets:
CACHIX_AUTH_TOKEN:
required: false
pull_request:
branches:
- main
- v[0-9]+
concurrency:
# Terminate all previous runs of the same workflow for pull requests
group: style-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
lint-style:
name: Lint & Style
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: style.lint.bin style.styleCheck.bin
- 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
-70
View File
@@ -1,70 +0,0 @@
name: CI
on:
push:
branches:
- main
- v[0-9]+
jobs:
check:
name: Check
uses: ./.github/workflows/check.yaml
secrets:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
docs:
name: Docs
uses: ./.github/workflows/docs.yaml
secrets:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
test:
name: Test
uses: ./.github/workflows/test.yaml
secrets:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
build:
name: Build
uses: ./.github/workflows/build.yaml
secrets:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
tag:
name: Tag
concurrency:
# Never tag outdated commits on the main branch by skipping superseded commits
group: ci-tag-${{ (github.ref == 'refs/heads/main' && github.ref) || github.run_id }}
# TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved
cancel-in-progress: false
if: vars.RELEASE_ENABLED
runs-on: ubuntu-24.04
needs:
- docs
- test
- build
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
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
+20 -6
View File
@@ -1,10 +1,10 @@
name: Docs name: Docs
on: on:
workflow_call: push:
secrets: branches:
CACHIX_AUTH_TOKEN: - main
required: false - v[0-9]+
pull_request: pull_request:
branches: branches:
- main - main
@@ -27,7 +27,7 @@ jobs:
name: Build name: Build
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
@@ -41,7 +41,7 @@ jobs:
name: Spellcheck name: Spellcheck
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
@@ -51,3 +51,17 @@ jobs:
run: postgrest-docs-spellcheck run: postgrest-docs-spellcheck
- name: Run dictcheck - name: Run dictcheck
run: postgrest-docs-dictcheck run: postgrest-docs-dictcheck
linkcheck:
name: Linkcheck
if: github.base_ref == 'main'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: docs.linkcheck.bin
- run: postgrest-docs-linkcheck
-17
View File
@@ -1,17 +0,0 @@
name: Linkcheck
on:
schedule:
- cron: '1 2 * * 3'
jobs:
linkcheck:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: docs.linkcheck.bin
- run: postgrest-docs-linkcheck
-205
View File
@@ -1,205 +0,0 @@
name: Release
on:
push:
tags:
- devel
- v*
concurrency:
# Terminate all previous runs of the same workflow for the same tag.
group: release-${{ github.ref }}
# TODO: Enable this once https://github.com/orgs/community/discussions/13015 is solved
cancel-in-progress: false
jobs:
build:
name: Build
uses: ./.github/workflows/build.yaml
secrets:
CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }}
prepare:
name: Prepare
runs-on: ubuntu-24.04
needs:
- build
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: release-changes
path: CHANGES.md
if-no-files-found: error
github:
name: GitHub
permissions:
contents: write
runs-on: ubuntu-24.04
needs:
- prepare
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download all artifacts
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
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-x86-64.tar.xz" \
-C artifacts/postgrest-linux-static-x86-64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-aarch64.tar.xz" \
-C artifacts/postgrest-macos-aarch64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-x86-64.tar.xz" \
-C artifacts/postgrest-macos-x86-64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \
-C artifacts/postgrest-freebsd-x86-64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-ubuntu-aarch64.tar.xz" \
-C artifacts/postgrest-ubuntu-aarch64 postgrest
zip --junk-paths "release-bundle/postgrest-${GITHUB_REF_NAME}-windows-x86-64.zip" \
artifacts/postgrest-windows-x86-64/postgrest.exe
- name: Save release bundle
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
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
docker:
name: Docker Hub
runs-on: ubuntu-24.04-arm
needs:
- prepare
if: |
vars.DOCKER_REPO && vars.DOCKER_USER
env:
DOCKER_REPO: ${{ vars.DOCKER_REPO }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download x86-64 Docker image
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: postgrest-docker-x86-64
- name: Download aarch64 binary
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with:
name: postgrest-ubuntu-aarch64
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0
with:
username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Build aarch64 Docker image
run: |
# This only pushes the image via digest, not a tag. This will not appear
# in the image list on Docker Hub, yet. It will be later added to the main
# tag's manifest.
docker buildx build \
-t "$DOCKER_REPO/postgrest" \
--platform linux/arm64 \
--output push-by-digest=true,type=image,push=true \
--metadata-file metadata.json \
.
echo "SHA256_ARM=$(jq -r '."containerimage.digest"' metadata.json)" >> "$GITHUB_ENV"
- name: Publish images on Docker Hub
run: |
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}"
docker buildx imagetools create --append \
-t "$DOCKER_REPO/postgrest:${GITHUB_REF_NAME}" \
"$DOCKER_REPO/postgrest@$SHA256_ARM"
# 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
docker buildx imagetools create --append \
-t "$DOCKER_REPO/postgrest:latest" \
"$DOCKER_REPO/postgrest@$SHA256_ARM"
else
echo "Skipping push to 'latest' tag for pre-release..."
fi
docker-description:
name: Docker Hub Description
runs-on: ubuntu-24.04
if: |
vars.DOCKER_REPO && vars.DOCKER_USER &&
github.ref == 'refs/tags/devel'
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2
with:
username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
repository: ${{ vars.DOCKER_REPO }}/postgrest
short-description: ${{ github.event.repository.description }}
readme-filepath: ./docker-hub-readme.md
-161
View File
@@ -1,161 +0,0 @@
name: Test
on:
workflow_call:
secrets:
CACHIX_AUTH_TOKEN:
required: false
CODECOV_TOKEN:
required: false
pull_request:
branches:
- main
- v[0-9]+
paths:
- .github/workflows/test.yaml
- .github/workflows/report.yaml
- .github/actions/setup-nix/**
- default.nix
- nix/**
- .stylish-haskell.yaml
- cabal.project
- postgrest.cabal
- '**.hs'
- test/**
- '!**.md'
concurrency:
# Terminate all previous runs of the same workflow for pull requests
group: test-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
coverage:
name: Coverage
runs-on: ubuntu-24.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: tests.coverage.bin tests.testDoctests.bin tests.testSpecIdempotence.bin
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
run: postgrest-coverage
- name: Upload coverage to codecov
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
files: ./coverage/codecov.json
token: ${{ secrets.CODECOV_TOKEN }}
- name: Run doctests
if: always()
run: postgrest-test-doctests
- name: Check the spec tests for idempotence
if: always()
run: postgrest-test-spec-idempotence
postgres:
strategy:
fail-fast: false
matrix:
pgVersion: [12, 13, 14, 15, 16, 17]
name: PG ${{ matrix.pgVersion }}
runs-on: ubuntu-24.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@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: tests.testSpec.bin tests.testIO.bin tests.testBigSchema.bin withTools.postgresql-${{ matrix.pgVersion }}.bin
- 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 }} postgrest-test-io -vv
- name: Run IO tests on a big schema
if: always()
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-big-schema -vv
memory:
name: Memory
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: tests.testMemory.bin
- name: Run memory tests
run: postgrest-test-memory
loadtest:
strategy:
matrix:
kind: ['mixed', 'jwt']
name: Loadtest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: loadtest.loadtestAgainst.bin loadtest.report.bin
- name: Run loadtest
env:
TARGET_BRANCH: ${{ github.base_ref || github.ref_name }}
run: |
if [ "$TARGET_BRANCH" = "main" ]; then
latest_tag=$(git tag --sort=-creatordate --list "v*" | head -n1)
else
latest_tag=$(git tag --merged HEAD --sort=-creatordate "v*" | head -n1)
fi
postgrest-loadtest-against -k ${{ matrix.kind }} "$TARGET_BRANCH" "$latest_tag"
postgrest-loadtest-report >> "$GITHUB_STEP_SUMMARY"
flake:
strategy:
fail-fast: false
matrix:
runs-on:
- macos-13 # x86_64-darwin
- macos-14 # aarch64-darwin
- ubuntu-24.04 # x86_64-linux
- ubuntu-24.04-arm # aarch64-linux
name: Flake Check
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Run flake check
run: |
nix flake check
+1 -2
View File
@@ -14,7 +14,7 @@ site
.#* .#*
*.swp *.swp
result* result*
dist-* dist-newstyle
postgrest.hp postgrest.hp
postgrest.prof postgrest.prof
__pycache__ __pycache__
@@ -24,4 +24,3 @@ coverage
loadtest loadtest
.history .history
.docs-build .docs-build
gen_targets.http
+1 -1
View File
@@ -5,6 +5,6 @@ python:
install: install:
- requirements: docs/requirements.txt - requirements: docs/requirements.txt
build: build:
os: ubuntu-24.04 os: ubuntu-22.04
tools: tools:
python: "3.11" python: "3.11"
+12 -12
View File
@@ -13,26 +13,31 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/neon.jpg"> <img width="296px" src="static/gnuhost.png">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png"> <img width="296px" src="static/neon.jpg">
</a> </a>
</td> </td>
</tr> </tr>
<tr></tr> <tr></tr>
<tr> <tr>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://www.euronodes.com/postgrest" target="_blank"> <a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg"> <img width="296px" src="static/code-build.png">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <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"> <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"> <img width="296px" src="static/supabase.svg">
</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> </a>
</td> </td>
</tr> </tr>
@@ -88,11 +93,6 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
<img width="222px" src="static/oblivious.jpg"> <img width="222px" src="static/oblivious.jpg">
</a> </a>
</td> </td>
<td align="center" valign="middle">
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="222px" src="static/code-build.png">
</a>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
+2 -116
View File
@@ -5,120 +5,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## Unreleased
## [13.0.7] - 2025-09-14
### Added
- Improve the `PGRST106` error when the requested schema is invalid by @laurenceisla in #4089
+ It now shows the invalid schema in the `message` field.
+ The exposed schemas are now listed in the `hint` instead of the `message` field.
- Improve error details of `PGRST301` error by @taimoorzaeem in #4051
## [13.0.6] - 2025-08-30
### Fixed
- Fix logging the Haskell type instead of the listener error message directly by @laurenceisla in #3588
- Fix format of `IPv6` address logged at PostgREST startup by @taimoorzaeem in #4291
- Fix empty enum in `preferParams` OpenAPI parameter by @laurenceisla in #4292
## [13.0.5] - 2025-08-24
### Fixed
- Fix OpenAPI broken docs link by @taimoorzaeem in #4048
- Fix OpenAPI specification incorrectly exposing GET methods for volatile functions by @joelonsql in #4174
- Fix empty spread embeddings return unexpected SQL error by @taimoorzaeem in #3887
- Fix `/metrics` endpoint not responding with `Content-Type` header by @taimoorzaeem in #4271
## [13.0.4] - 2025-06-17
### Fixed
- Fix regression that makes full-text search not work on domain types based on `tsvector` by @laurenceisla in #4135
- Fix `jwt-aud` config not failing when set to an invalid URI by @taimoorzaeem in #4132
## [13.0.3] - 2025-06-16
### Fixed
- Fix `max-affected` preference not failing with RPC when `handling=strict` by @taimoorzaeem in #4100
- Fix a property definition's type in OpenAPI not showing the correct base type of a recursive domain by @laurenceisla in #4136
## [13.0.2] - 2025-06-02
### Fixed
- Fix regression that makes `ORDER BY` with nulls-order not work alongside limits by @laurenceisla in #4109
## [13.0.1] - 2025-06-01
### Fixed
- Fix jwt error returning HTTP status `400` for invalid role by @taimoorzaeem in #3601
- Fix `db-extra-search-path` cannot be set to nothing by @taimoorzaeem in #4074
+ It can now be disabled by setting it to empty string.
+ Schema Cache load error is now logged including `db-schemas` and `db-extra-search-path` config values.
## [13.0.0] - 2025-05-08
### Added
- #3558, Add the `admin-server-host` config to set the host for the admin server - @develop7
- #3607, Log to stderr when the JWT secret is less than 32 characters long - @laurenceisla
- #2858, Performance improvements when calling RPCs via GET using indexes in more cases - @wolfgangwalther
- #3560, Log resolved host in "Listening on ..." messages - @develop7
- #3727, Log maximum pool size - @steve-chavez
- #1536, Add string comparison feature for jwt-role-claim-key - @taimoorzaeem
- #3747, Allow `not_null` value for the `is` operator - @taimoorzaeem
- #2255, Apply `to_tsvector()` explicitly to the full-text search filtered column (excluding `tsvector` types) - @laurenceisla
- #1578, Log the main SQL query to stderr at the current `log-level` when `log-query=main-query` - @laurenceisla
- #3903, Log connection pool borrows on `log-level=debug` - @taimoorzaeem
- #3041, Allow spreading one-to-many and many-to-many embedded resources - @laurenceisla
+ The selected columns in the embedded resources are aggregated into arrays
+ Aggregates are not supported
- #2967, Add `Proxy-Status` header for better error response - @taimoorzaeem
- #4016, Add `Content-Length` response header - @laurenceisla
### Fixed
- #3693, Prevent spread embedding to allow aggregates when they are disabled - @laurenceisla
- #3693, A nested spread embedding now correctly groups by the fields of its top parent relationship - @laurenceisla
- #3693, Fix spread embedding errors when using the `count()` aggregate without a field - @laurenceisla
+ Fixed `"column reference <col> is ambiguous"` error when selecting `?select=...table(col,count())`
+ Fixed `"column <json_aggregate>.<alias> does not exist"` error when selecting `?select=...table(aias:count())`
- #3727, Clarify "listening" logs - @steve-chavez
- #3795, Clarify `Accept: vnd.pgrst.object` error message - @steve-chavez
- #3697, #3602, Handle queries on non-existing table gracefully - @taimoorzaeem
- #3600, #3926, Improve JWT errors - @taimoorzaeem
- #3013, Fix `order=` with POST, PATCH, PUT and DELETE requests - @taimoorzaeem
- #3965, Fix filter on unselected columns in a table-valued function - @taimoorzaeem
- #4052, Fix schema cache load duplicate objects with different object type but same oid - @taimoorzaeem
### Changed
- #2052, Dropped support for PostgreSQL 9.6 - @wolfgangwalther
- #2052, Dropped support for PostgreSQL 10 - @wolfgangwalther
- #2052, Dropped support for PostgreSQL 11 - @wolfgangwalther
- #3508, PostgREST now fails to start when `server-port` and `admin-server-port` config options are the same - @develop7
- #3607, PostgREST now fails to start when the JWT secret is less than 32 characters long - @laurenceisla
- #3644, Fail schema cache lookup with invalid `db-schemas` or `db-extra-search-path` config - @wolfgangwalther
- Previously, this would silently return 200 - OK on the root endpoint, but don't provide any usable endpoints.
- Note: This also applies when deleting the `public` schema - both config options default to that.
- #3757, Remove support for `Prefer: params=single-object` - @joelonsql
+ This preference was deprecated in favor of Functions with an array of JSON objects
- #3013, Drop support for Limited updates/deletes
+ The feature was complicated and largely unused.
- #3956, Drop `/config` endpoint of admin server - @steve-chavez
+ The endpoint was at risk of being left unprotected when exposing it.
+ The accompanying `admin-server-config-enabled` config was also dropped.
- #3697, #3602, Querying non-existent table now returns `PGRST205` error instead of empty json - @taimoorzaeem
- #3600, #3926, Improve JWT errors - @taimoorzaeem
+ Return `PGRST301` error when `Bearer` in auth header is sent empty
+ Diagnostic error messages instead of exposed internals
+ Return new `PGRST303` error when jwt claims decoding fails
- #3906, Return `PGRST125` and `PGRST126` errors instead of empty json - @taimoorzaeem
## [12.2.12] - 2025-05-01 ## [12.2.12] - 2025-05-01
### Fixed ### Fixed
@@ -126,11 +12,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #3956, Fix exposing admin server `/config` by default - @steve-chavez - #3956, Fix exposing admin server `/config` by default - @steve-chavez
+ The above endpoint is now disabled unless the `admin-server-config-enabled` config is set to `true` + The above endpoint is now disabled unless the `admin-server-config-enabled` config is set to `true`
## [12.2.11] - 2025-04-22 ## [12.2.11] - 2025-04-21
### Fixed ### Fixed
- #4030, Fix regression with parameter `charset=utf-8` in mediatype - @taimoorzaeem - #4030, Fix regression with parameter `charset=utf-8` in mediatype - @taimoorzaeem
## [12.2.10] - 2025-04-18 ## [12.2.10] - 2025-04-18
+3
View File
@@ -0,0 +1,3 @@
This repository follows the same contribution guidelines as the main PostgREST repository contribution guidelines:
https://github.com/PostgREST/postgrest/blob/main/.github/CONTRIBUTING.md
-21
View File
@@ -1,21 +0,0 @@
# PostgREST Docker Hub image for aarch64.
# The x86-64 is a single-static-binary image built via Nix, see:
# nix/tools/docker/README.md
FROM ubuntu:noble@sha256:9cbed754112939e914291337b5e554b07ad7c392491dba6daf25eef1332a22e8 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
RUN chmod +x /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"]
+12 -7
View File
@@ -22,26 +22,31 @@ API than you are likely to write from scratch.
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/neon.jpg"> <img width="296px" src="static/gnuhost.png">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png"> <img width="296px" src="static/neon.jpg">
</a> </a>
</td> </td>
</tr> </tr>
<tr></tr> <tr></tr>
<tr> <tr>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://www.euronodes.com/postgrest" target="_blank"> <a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg"> <img width="296px" src="static/code-build.png">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <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"> <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"> <img width="296px" src="static/supabase.svg">
</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> </a>
</td> </td>
</tr> </tr>
+1 -1
View File
@@ -1 +1 @@
index-state: hackage.haskell.org 2025-07-08T15:50:10Z index-state: hackage.haskell.org 2025-02-01T14:59:33Z
+35 -25
View File
@@ -3,15 +3,7 @@
, compiler ? "ghc948" , compiler ? "ghc948"
, # Commit of the Nixpkgs repository that we want to use. , # Commit of the Nixpkgs repository that we want to use.
# It defaults to reading the inputs from flake.lock, which serves nixpkgsVersion ? import nix/nixpkgs-version.nix
# as a compatibility layer for non-flake builds / default.nix / shell.nix.
nixpkgsVersion ? let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
in
{
inherit (lock.nodes.nixpkgs.locked) owner repo rev;
tarballHash = lock.nodes.nixpkgs.locked.narHash;
}
, # Nix files that describe the Nixpkgs repository. We evaluate the expression , # Nix files that describe the Nixpkgs repository. We evaluate the expression
# using `import` below. # using `import` below.
@@ -27,7 +19,7 @@ let
"postgrest"; "postgrest";
# PostgREST source files, filtered based on the rules in the .gitignore files # PostgREST source files, filtered based on the rules in the .gitignore files
# and file extensions. We want to include as little as possible, as the files # and file extensions. We want to include as litte as possible, as the files
# added here will increase the space used in the Nix store and trigger the # added here will increase the space used in the Nix store and trigger the
# build of new Nix derivations when changed. # build of new Nix derivations when changed.
src = src =
@@ -44,6 +36,9 @@ let
allOverlays.checked-shell-script allOverlays.checked-shell-script
allOverlays.gitignore allOverlays.gitignore
allOverlays.postgresql-libpq allOverlays.postgresql-libpq
allOverlays.postgresql-legacy
allOverlays.postgresql-future
allOverlays.postgis
(allOverlays.haskell-packages { inherit compiler; }) (allOverlays.haskell-packages { inherit compiler; })
allOverlays.slocat allOverlays.slocat
]; ];
@@ -54,28 +49,34 @@ let
postgresqlVersions = postgresqlVersions =
[ [
{ name = "postgresql-17"; postgresql = pkgs.postgresql_17.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "postgresql-16"; postgresql = pkgs.postgresql_16.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-16"; postgresql = pkgs.postgresql_16.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "postgresql-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "postgresql-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "postgresql-12"; postgresql = pkgs.postgresql_12.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "postgresql-12"; postgresql = pkgs.postgresql_12.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "postgresql-11"; postgresql = pkgs.postgresql_11.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "postgresql-10"; postgresql = pkgs.postgresql_10.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
{ name = "postgresql-9_6"; postgresql = pkgs.postgresql_9_6.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
]; ];
# Dynamic derivation for PostgREST # Dynamic derivation for PostgREST
postgrest = pkgs.lib.pipe (pkgs.haskell.packages."${compiler}".callCabal2nix name src { }) [ postgrest =
# To allow ghc-datasize to be used. pkgs.haskell.packages."${compiler}".callCabal2nix name src { };
lib.disableLibraryProfiling
# We are never going to use dynamic haskell libraries anyway. "Dynamic" refers to how
# non-haskell deps are linked. All haskell dependencies are always statically linked.
lib.disableSharedLibraries
];
staticHaskellPackage = import nix/static.nix { inherit compiler name pkgs src; }; staticHaskellPackage = import nix/static.nix { inherit compiler name pkgs src; };
# Options passed to cabal in dev tools and tests # Options passed to cabal in dev tools and tests
devCabalOptions = devCabalOptions =
"-f dev --test-show-detail=direct --disable-shared"; "-f dev --test-show-detail=direct";
profiledHaskellPackages =
pkgs.haskell.packages."${compiler}".extend (_: super:
{
mkDerivation =
args:
super.mkDerivation (args // { enableLibraryProfiling = true; });
}
);
inherit (pkgs.haskell) lib; inherit (pkgs.haskell) lib;
in in
@@ -89,11 +90,12 @@ rec {
lib.dontCheck postgrest; lib.dontCheck postgrest;
# Profiled dynamic executable. # Profiled dynamic executable.
postgrestProfiled = pkgs.lib.pipe postgrestPackage [ postgrestProfiled =
lib.enableExecutableProfiling lib.enableExecutableProfiling (
lib.enableLibraryProfiling lib.dontHaddock (
lib.dontHaddock lib.dontCheck (profiledHaskellPackages.callCabal2nix name src { })
]; )
);
inherit (postgrest) env; inherit (postgrest) env;
@@ -123,9 +125,17 @@ rec {
loadtest = loadtest =
pkgs.callPackage nix/tools/loadtest.nix { inherit withTools; }; pkgs.callPackage nix/tools/loadtest.nix { inherit withTools; };
# Script for running memory tests.
memory =
pkgs.callPackage nix/tools/memory.nix { inherit postgrestProfiled withTools; };
# Utility for updating the pinned version of Nixpkgs.
nixpkgsTools =
pkgs.callPackage nix/tools/nixpkgsTools.nix { };
# Scripts for publishing new releases. # Scripts for publishing new releases.
release = release =
pkgs.callPackage nix/tools/release.nix { }; pkgs.callPackage nix/tools/release { };
# Linting and styling tools. # Linting and styling tools.
style = style =
+13 -8
View File
@@ -19,26 +19,31 @@ write from scratch.
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <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/neon.jpg"> <img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/gnuhost.png">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <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/tembo.png"> <img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/neon.jpg">
</a> </a>
</td> </td>
</tr> </tr>
<tr></tr> <tr></tr>
<tr> <tr>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://www.euronodes.com/postgrest" target="_blank"> <a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/euronodes.svg"> <img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/code-build.png">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <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"> <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"> <img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.svg">
</a>
</td>
<td align="center" valign="middle">
<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> </a>
</td> </td>
</tr> </tr>
@@ -59,7 +64,7 @@ The image is built from scratch using
[Nix](https://nixos.org/nixpkgs/manual/#sec-pkgs-dockerTools) instead of a [Nix](https://nixos.org/nixpkgs/manual/#sec-pkgs-dockerTools) instead of a
`Dockerfile`, which yields a highly 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 no commands are listed in the image history. See the [PostgREST
repository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for respository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for
details on the build process and how to inspect the image. details on the build process and how to inspect the image.
This does not apply to the arm64 variant, which is based on Ubuntu. This does not apply to the arm64 variant, which is based on Ubuntu.
+2 -9
View File
@@ -60,10 +60,6 @@ note bottom of ApiRequest
Parses the URL syntax Parses the URL syntax
end note end note
note bottom of Plan
Generates internal AST
end note
note bottom of Query note bottom of Query
Generates the SQL Generates the SQL
end note end note
@@ -75,15 +71,12 @@ end note
url of Admin is [[../references/admin_server.html#admin-server]] url of Admin is [[../references/admin_server.html#admin-server]]
url of API is [[../explanations/schema_isolation.html]] url of API is [[../explanations/schema_isolation.html]]
url of Auth is [[../references/auth.html#authn]] url of Auth is [[../references/auth.html#authn]]
url of ApiRequest is [[../explanations/architecture.html#api-request]]
url of Plan is [[../explanations/architecture.html#plan]]
url of Query is [[../explanations/architecture.html#query]]
url of Authorization is [[../explanations/db_authz.html]] url of Authorization is [[../explanations/db_authz.html]]
url of CLI is [[../references/cli.html#cli]] url of CLI is [[../references/cli.html#cli]]
url of "Connection Pool" is [[../references/connection_pool.html]] url of "Connection Pool" is [[../references/connection_pool.html]]
url of Config is [[../references/configuration.html#configuration]] url of Config is [[../references/configuration.html#configuration]]
url of HTTPADMIN is [[../explanations/architecture.html#http]] url of HTTPADMIN is [[https://aosabook.org/en/posa/warp.html]]
url of HTTPAPI is [[../explanations/architecture.html#http]] url of HTTPAPI is [[https://aosabook.org/en/posa/warp.html]]
url of Listener is [[../references/listener.html#listener]] url of Listener is [[../references/listener.html#listener]]
url of Proxy is [[../explanations/nginx.html]] url of Proxy is [[../explanations/nginx.html]]
url of "Schema Cache" is [[../references/schema_cache.html#schema-cache]] url of "Schema Cache" is [[../references/schema_cache.html#schema-cache]]
+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 23 KiB

+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 24 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 B

After

Width:  |  Height:  |  Size: 468 B

+1 -1
View File
@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="391px" preserveAspectRatio="none" style="width:328px;height:391px;" version="1.1" viewBox="0 0 328 391" width="328px" zoomAndPan="magnify"><defs/><g><!--cluster PostgreSQL--><g id="cluster_PostgreSQL"><path d="M6,16 C6,6 158.5,6 158.5,6 C158.5,6 311,6 311,16 L311,293.59 C311,303.59 158.5,303.59 158.5,303.59 C158.5,303.59 6,303.59 6,293.59 L6,16 " fill="none" style="stroke:#E7E7E7;stroke-width:1.0;"/><path d="M6,16 C6,26 158.5,26 158.5,26 C158.5,26 311,26 311,16 " fill="none" style="stroke:#E7E7E7;stroke-width:1.0;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="92.2305" x="112.3848" y="40.9951">PostgreSQL</text></g><!--cluster public--><g id="cluster_public"><polygon fill="none" points="30,74,40,64,153,64,153,146.29,143,156.29,30,156.29,30,74" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="143" x2="153" y1="74" y2="64"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="30" x2="143" y1="74" y2="74"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="143" x2="143" y1="74" y2="156.29"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="47.9063" x="63.5469" y="89.9951">public</text></g><!--cluster API--><g id="cluster_API"><polygon fill="none" points="70,190.29,80,180.29,246,180.29,246,269.59,236,279.59,70,279.59,70,190.29" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="236" x2="246" y1="190.29" y2="180.29"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="70" x2="236" y1="190.29" y2="190.29"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="236" x2="236" y1="190.29" y2="279.59"/><text fill="#FFFFFF" font-family="sans-serif" font-size="20" font-weight="bold" lengthAdjust="spacing" textLength="34.668" x="136.666" y="211.8545">api</text></g><!--entity tables_public--><g id="elem_tables_public"><rect fill="#313139" height="36.2969" rx="2.5" ry="2.5" style="stroke:#E7E7E7;stroke-width:0.5;" width="62.752" x="71.62" y="104"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="42.752" x="81.62" y="126.9951">tables</text></g><!--entity extensions--><g id="elem_extensions"><polygon fill="none" points="169.14,109,179.14,99,294.8695,99,294.8695,135.2969,284.8695,145.2969,169.14,145.2969,169.14,109" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="284.8695" x2="294.8695" y1="109" y2="99"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="169.14" x2="284.8695" y1="109" y2="109"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="284.8695" x2="284.8695" y1="109" y2="145.2969"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="85.7295" x="184.14" y="131.9951">extensions</text></g><!--entity vf_api--><g id="elem_vf_api"><rect fill="#313139" height="36.2969" rx="2.5" ry="2.5" style="stroke:#E7E7E7;stroke-width:0.5;" width="144.6465" x="85.68" y="227.29"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="124.6465" x="95.68" y="250.2851">views + functions</text></g><!--entity PostgREST--><g id="elem_PostgREST"><ellipse cx="158" cy="352.59" fill="#313139" rx="8" ry="8" style="stroke:#E7E7E7;stroke-width:0.5;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="74.6895" x="120.6553" y="382.5851">PostgREST</text></g><!--reverse link tables_public to vf_api--><g id="link_tables_public_vf_api"><path d="M110.03,146.6 C110.03,169.85 110.03,203.55 110.03,226.86 " fill="none" id="tables_public-backto-vf_api" style="stroke:#E7E7E7;stroke-width:1.0;"/><polygon fill="#E7E7E7" points="110.03,140.6,106.03,149.6,110.03,145.6,114.03,149.6,110.03,140.6" style="stroke:#E7E7E7;stroke-width:1.0;"/></g><!--reverse link extensions to vf_api--><g id="link_extensions_vf_api"><path d="M199.73,151.63 C199.73,175.24 199.73,205.18 199.73,226.88 " fill="none" id="extensions-backto-vf_api" style="stroke:#E7E7E7;stroke-width:1.0;"/><polygon fill="#E7E7E7" points="199.73,145.63,195.73,154.63,199.73,150.63,203.73,154.63,199.73,145.63" style="stroke:#E7E7E7;stroke-width:1.0;"/></g><!--link vf_api to PostgREST--><g id="link_vf_api_PostgREST"><path d="M158,269.62 C158,292.9 158,320.34 158,337.81 " fill="none" id="vf_api-PostgREST" style="stroke:#E7E7E7;stroke-width:3.0;"/><polygon fill="#E7E7E7" points="158,263.62,154,272.62,158,268.62,162,272.62,158,263.62" style="stroke:#E7E7E7;stroke-width:3.0;"/><polygon fill="#E7E7E7" points="158,343.81,162,334.81,158,338.81,154,334.81,158,343.81" style="stroke:#E7E7E7;stroke-width:3.0;"/></g><!--SRC=[KypCIyufJKbLqDFJBqxEqCqipjShpSq10000]--></g></svg> <?xml version="1.0" encoding="us-ascii" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="411px" preserveAspectRatio="none" style="width:315px;height:411px;" version="1.1" viewBox="0 0 315 411" width="315px" zoomAndPan="magnify"><defs/><g><!--cluster PostgreSQL--><g id="cluster_PostgreSQL"><path d="M6,16 C6,6 152,6 152,6 C152,6 298,6 298,16 L298,309.14 C298,319.14 152,319.14 152,319.14 C152,319.14 6,319.14 6,309.14 L6,16 " fill="none" style="stroke:#E7E7E7;stroke-width:1.0;"/><path d="M6,16 C6,26 152,26 152,26 C152,26 298,26 298,16 " fill="none" style="stroke:#E7E7E7;stroke-width:1.0;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="81.375" x="111.3125" y="42.9659">PostgreSQL</text></g><!--cluster public--><g id="cluster_public"><polygon fill="none" points="30,77,40,67,150,67,150,155.07,140,165.07,30,165.07,30,77" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="140" x2="150" y1="77" y2="67"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="30" x2="140" y1="77" y2="77"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="140" x2="140" y1="77" y2="165.07"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="44.625" x="63.6875" y="94.9659">public</text></g><!--cluster API--><g id="cluster_API"><polygon fill="none" points="68,199.07,78,189.07,236,189.07,236,285.14,226,295.14,68,295.14,68,199.07" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="226" x2="236" y1="199.07" y2="189.07"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="68" x2="226" y1="199.07" y2="199.07"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="226" x2="226" y1="199.07" y2="295.14"/><text fill="#FFFFFF" font-family="sans-serif" font-size="20" font-weight="bold" lengthAdjust="spacing" textLength="29.875" x="133.0625" y="223.45">api</text></g><!--entity tables_public--><g id="elem_tables_public"><rect fill="#313139" height="39.0679" rx="2.5" ry="2.5" style="stroke:#E7E7E7;stroke-width:0.5;" width="61" x="69.5" y="110"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="41" x="79.5" y="134.9659">tables</text></g><!--entity extensions--><g id="elem_extensions"><polygon fill="none" points="165.81,115,175.81,105,282.185,105,282.185,144.0679,272.185,154.0679,165.81,154.0679,165.81,115" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="272.185" x2="282.185" y1="115" y2="105"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="165.81" x2="272.185" y1="115" y2="115"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="272.185" x2="272.185" y1="115" y2="154.0679"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="76.375" x="180.81" y="139.9659">extensions</text></g><!--entity vf_api--><g id="elem_vf_api"><rect fill="#313139" height="39.0679" rx="2.5" ry="2.5" style="stroke:#E7E7E7;stroke-width:0.5;" width="136" x="84" y="240.07"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="116" x="94" y="265.0359">views + functions</text></g><!--entity PostgREST--><g id="elem_PostgREST"><ellipse cx="152" cy="371.14" fill="#313139" rx="8" ry="8" style="stroke:#E7E7E7;stroke-width:0.5;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="70" x="117" y="403.1059">PostgREST</text></g><!--reverse link tables_public to vf_api--><g id="link_tables_public_vf_api"><path d="M107.25,155.29 C107.25,179.74 107.25,215.13 107.25,239.67 " fill="none" id="tables_public-backto-vf_api" style="stroke:#E7E7E7;stroke-width:1.0;"/><polygon fill="#E7E7E7" points="107.25,149.29,103.25,158.29,107.25,154.29,111.25,158.29,107.25,149.29" style="stroke:#E7E7E7;stroke-width:1.0;"/></g><!--reverse link extensions to vf_api--><g id="link_extensions_vf_api"><path d="M192.91,160.31 C192.91,185.07 192.91,216.68 192.91,239.61 " fill="none" id="extensions-backto-vf_api" style="stroke:#E7E7E7;stroke-width:1.0;"/><polygon fill="#E7E7E7" points="192.91,154.31,188.91,163.31,192.91,159.31,196.91,163.31,192.91,154.31" style="stroke:#E7E7E7;stroke-width:1.0;"/></g><!--link vf_api to PostgREST--><g id="link_vf_api_PostgREST"><path d="M152,285.55 C152,309.98 152,338.34 152,356.18 " fill="none" id="vf_api-PostgREST" style="stroke:#E7E7E7;stroke-width:3.0;"/><polygon fill="#E7E7E7" points="152,279.55,148,288.55,152,284.55,156,288.55,152,279.55" style="stroke:#E7E7E7;stroke-width:3.0;"/><polygon fill="#E7E7E7" points="152,362.18,156,353.18,152,357.18,148,353.18,152,362.18" style="stroke:#E7E7E7;stroke-width:3.0;"/></g><!--SRC=[KypCIyufJKbLqDFJBqxEqCqipjShpSq10000]--></g></svg>

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

+1 -1
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

+3 -5
View File
@@ -50,7 +50,7 @@ source_suffix = ".rst"
master_doc = "index" master_doc = "index"
# This is overriden by readthedocs with the version tag anyway # This is overriden by readthedocs with the version tag anyway
version = "13.0" version = "12.2"
# To avoid repetition in <title> we set this to an empty string. # To avoid repetition in <title> we set this to an empty string.
release = "" release = ""
@@ -114,7 +114,7 @@ html_theme = "sphinx_rtd_theme"
# Theme options are theme-specific and customize the look and feel of a theme # Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the # further. For a list of options available for each theme, see the
# documentation. # documentation.
html_theme_options = {} html_theme_options = {"display_version": False}
# Add any paths that contain custom themes here, relative to this directory. # Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = [] # html_theme_path = []
@@ -297,11 +297,9 @@ user_agent = (
) )
linkcheck_ignore = [ linkcheck_ignore = [
# 403 only in CI / GitHub Actions
r"https://www.patreon.com/postgrest",
r"https://blog.frankel.ch/poor-man-api",
# Odd SSL error # Odd SSL error
r"https://www.dripdepot.com", r"https://www.dripdepot.com",
r"https://www.euronodes.com",
# New GitHub UI delays comment load, so anchor fails # New GitHub UI delays comment load, so anchor fails
r"https://github.com/.*#issuecomment", r"https://github.com/.*#issuecomment",
# Random 500 Internal Server Error # Random 500 Internal Server Error
+1 -1
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>`_ - * `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. In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.
* `PostgREST + Auth0: Create REST API in mintutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service. * `PostgREST + 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.
* `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ - * `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ -
A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service). A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service).
+1 -1
View File
@@ -58,7 +58,7 @@ A request might be rejected at this level if it's invalid. For example when prov
Plan Plan
---- ----
Using the Schema Cache, `Plan.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Plan.hs>`_ generates an internal AST, filling out-of-band SQL details (like an ``ON CONFLICT (pk)`` clause) required to complete the user request. Using the Schema Cache, `Plan.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Plan.hs>`_ fills in out-of-band SQL details (like an ``ON CONFLICT (pk)`` clause) required to complete the user request.
A request might be rejected at this level if it's invalid. For example when doing resource embedding on a nonexistent resource. A request might be rejected at this level if it's invalid. For example when doing resource embedding on a nonexistent resource.
+7 -2
View File
@@ -16,11 +16,16 @@ Supported PostgreSQL versions
============================= =============================
=============== ================================= =============== =================================
**Supported** PostgreSQL >= 12 **Supported** PostgreSQL >= 9.6
=============== ================================= =============== =================================
PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_. PostgREST works with all PostgreSQL versions starting from 9.6.
.. note::
Support for PostgreSQL versions 9.6, 10 and 11 is deprecated. From this on version onwards, PostgREST will only support non-end-of-life PostgreSQL versions.
See https://www.postgresql.org/support/versioning/.
Running PostgREST Running PostgREST
================= =================
+14 -12
View File
@@ -38,6 +38,9 @@ Sponsors
.. image:: ../static/cybertec.svg .. image:: ../static/cybertec.svg
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest :target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
.. image:: ../static/gnuhost.png
:target: https://euronodes.com/?utm_source=sponsor&utm_campaign=postgrest
.. container:: img-dark .. container:: img-dark
.. image:: ../static/neon-dark.jpg .. image:: ../static/neon-dark.jpg
@@ -48,36 +51,36 @@ Sponsors
.. image:: ../static/neon.jpg .. image:: ../static/neon.jpg
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest :target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
.. image:: ../static/tembo.png
:target: https://www.tembo.io/?utm_source=sponsor&utm_campaign=postgrest
| |
.. container:: img-dark .. container:: img-dark
.. image:: ../static/euronodes.svg .. image:: ../static/code-build-dark.png
:target: https://www.euronodes.com/postgrest :target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
.. container:: img-light .. container:: img-light
.. image:: ../static/euronodes.svg .. image:: ../static/code-build.png
:target: https://www.euronodes.com/postgrest :target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
.. container:: img-dark .. container:: img-dark
.. image:: ../static/supabase-dark.png .. image:: ../static/supabase-dark.svg
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage :target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
.. container:: img-light .. container:: img-light
.. image:: ../static/supabase.png .. image:: ../static/supabase.svg
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage :target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
.. image:: ../static/tembo.png
:target: https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround .. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
to create space and center the logos. It's not easy to layout with restructuredText. to create space and center the logos. It's not easy to layout with restructuredText.
.. image:: _static/empty.png .. .. image:: _static/empty.png
:target: #sponsors :target: #sponsors
| |
@@ -206,7 +209,6 @@ In Production
Here are some companies that use PostgREST in production. Here are some companies that use PostgREST in production.
* `Catarse <https://www.catarse.me>`_ * `Catarse <https://www.catarse.me>`_
* `Datrium <https://www.datrium.com>`_
* `Drip Depot <https://www.dripdepot.com>`_ * `Drip Depot <https://www.dripdepot.com>`_
* `Image-charts <https://www.image-charts.com>`_ * `Image-charts <https://www.image-charts.com>`_
* `Netwo <https://www.netwo.io>`_ * `Netwo <https://www.netwo.io>`_
@@ -1,6 +1,6 @@
.. _external_auth: .. _external_jwt:
External Authentication External JWT Generation
----------------------- -----------------------
JWT from Auth0 JWT from Auth0
+1 -7
View File
@@ -3,7 +3,6 @@ api
API's API's
APIs APIs
APISIX APISIX
AST
aud aud
Auth Auth
auth auth
@@ -28,12 +27,10 @@ CSV
durations durations
DDL DDL
DOM DOM
DSL
DevOps DevOps
dockerize dockerize
enum enum
Enums Enums
Entra
eq eq
ETH ETH
Ethereum Ethereum
@@ -70,11 +67,9 @@ isdistinct
JS JS
js js
JSON JSON
JSPath
JWK JWK
JWT JWT
jwt jwt
Keycloak
Kubernetes Kubernetes
localhost localhost
login login
@@ -98,11 +93,10 @@ npm
nxl nxl
nxr nxr
OAuth OAuth
ORM
Observability Observability
Okta
OpenAPI OpenAPI
openapi openapi
ORM
ov ov
parametrized parametrized
passphrase passphrase
+21
View File
@@ -55,6 +55,27 @@ Metrics
Provides :ref:`metrics`. Provides :ref:`metrics`.
.. _runtime_config:
Runtime Configuration
=====================
Provides a ``config`` endpoint that returns the runtime :ref:`configuration`. This requires setting :ref:`admin-server-config-enabled`.
.. code-block:: bash
curl "http://localhost:3001/config"
.. code-block::
db-aggregates-enabled = false
db-anon-role = "web_anon"
db-channel = "pgrst"
db-channel-enabled = false
...
.. _runtime_schema_cache:
Runtime Schema Cache Runtime Schema Cache
==================== ====================
+3 -7
View File
@@ -225,10 +225,10 @@ If we also want to get the total ``amount`` grouped by the ``order_date`` of the
Note that the aggregate is done within the embedded resource ``orders``. Note that the aggregate is done within the embedded resource ``orders``.
It is not affected by any of the columns from the top-level relationship ``customers``. It is not affected by any of the columns from the top-level relationship ``customers``.
Aggregates in To-One Spreads Using Aggregates in Spreads
~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
All the aggregates inside a :ref:`one-to-one or many-to-one spread embedded resource <spread_to_one_embed>` will be hoisted to the top-level relationship. All the aggregates inside a :ref:`spread embedded resource <spread_embed>` will be hoisted to the top-level relationship.
In other words, it will behave as if the aggregate was done in the top-level relationship itself. For example: In other words, it will behave as if the aggregate was done in the top-level relationship itself. For example:
.. code-block:: bash .. code-block:: bash
@@ -251,7 +251,3 @@ This will take the ``max`` and ``min`` subscription date of every customer and g
"min": "2016-02-11" "min": "2016-02-11"
} }
] ]
.. note::
Aggregates inside to-many spreads are not supported
+4
View File
@@ -131,6 +131,10 @@ For this the ``Content-Type: application/json`` header must be included in the r
If an overloaded function has a single ``json`` or ``jsonb`` unnamed parameter, PostgREST will call this function as a fallback provided that no other overloaded function is found with the parameters sent in the POST request. If an overloaded function has a single ``json`` or ``jsonb`` unnamed parameter, PostgREST will call this function as a fallback provided that no other overloaded function is found with the parameters sent in the POST request.
.. warning::
Sending the JSON request body as a single argument is also possible with :ref:`Prefer: params=single-object <prefer_params>` but this method is **deprecated**.
.. _function_single_unnamed: .. _function_single_unnamed:
Functions with a single unnamed parameter Functions with a single unnamed parameter
-3
View File
@@ -19,9 +19,6 @@ For extra customization, the OpenAPI output contains a "description" field for e
COMMENT ON TABLE monotremes IS COMMENT ON TABLE monotremes IS
'Freakish mammals lay the best eggs for breakfast'; 'Freakish mammals lay the best eggs for breakfast';
COMMENT ON VIEW monotremes_v IS
'Only the platypus is publicly visible';
COMMENT ON COLUMN monotremes.has_venomous_claw IS COMMENT ON COLUMN monotremes.has_venomous_claw IS
'Sometimes breakfast is not worth it'; 'Sometimes breakfast is not worth it';
+19 -22
View File
@@ -15,6 +15,7 @@ The following preferences are supported.
- ``Prefer: missing``. See :ref:`prefer_missing`. - ``Prefer: missing``. See :ref:`prefer_missing`.
- ``Prefer: max-affected``, See :ref:`prefer_max_affected`. - ``Prefer: max-affected``, See :ref:`prefer_max_affected`.
- ``Prefer: tx``. See :ref:`prefer_tx`. - ``Prefer: tx``. See :ref:`prefer_tx`.
- ``Prefer: params``. See :ref:`prefer_params`.
.. _prefer_handling: .. _prefer_handling:
@@ -242,7 +243,7 @@ Will result in:
Max Affected Max Affected
============ ============
You can set a limit to the amount of resources affected in a request by sending ``max-affected`` preference. This feature works in combination with ``handling=strict`` preference. ``max-affected`` would be ignored with lenient handling. The "affected resources" are the number of rows returned by ``DELETE`` and ``PATCH`` requests. You can set a limit to the amount of resources affected in a request by sending ``max-affected`` preference. This feature works in combination with ``handling=strict`` preference. ``max-affected`` would be ignored with lenient handling. The "affected resources" are the number of rows returned by ``DELETE`` and ``PATCH`` requests. This is also supported through ``RPC`` calls.
To illustrate the use of this preference, consider the following scenario where the ``items`` table contains 14 rows. To illustrate the use of this preference, consider the following scenario where the ``items`` table contains 14 rows.
@@ -265,34 +266,30 @@ To illustrate the use of this preference, consider the following scenario where
"hint": null "hint": null
} }
With :ref:`RPC <functions>`, the preference is honored completely on the basis of the number of rows returned in the result set of the function. This can be useful for complex mutation queries using `data-modifying statements <https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-MODIFYING>`_. A simple example: .. _prefer_params:
Single JSON object as Function Parameter
----------------------------------------
.. warning::
Using this preference is **deprecated** in favor of :ref:`function_single_json`.
:code:`Prefer: params=single-object` allows sending the JSON request body as the single argument of a :ref:`function <functions>`.
.. code-block:: postgres .. code-block:: postgres
CREATE FUNCTION test.delete_items() CREATE FUNCTION mult_them(param json) RETURNS int AS $$
RETURNS SETOF items AS $$ SELECT (param->>'x')::int * (param->>'y')::int
DELETE FROM items WHERE id < 15 RETURNING *;
$$ LANGUAGE SQL; $$ LANGUAGE SQL;
.. code-block:: bash .. code-block:: bash
curl -i "http://localhost:3000/rpc/delete_items" \ curl "http://localhost:3000/rpc/mult_them" \
-H "Content-Type: application/json" \ -X POST -H "Content-Type: application/json" \
-H "Prefer: handling=strict, max-affected=10" -H "Prefer: params=single-object" \
-d '{ "x": 4, "y": 2 }'
.. code-block:: http
HTTP/1.1 400 Bad Request
.. code-block:: json .. code-block:: json
{ 8
"code": "PGRST124",
"message": "Query result exceeds max-affected preference constraint",
"details": "The query affects 14 rows",
"hint": null
}
.. note::
It is important for functions to return ``SETOF`` or ``TABLE`` when called with ``max-affected`` preference. A violation of this would cause a :ref:`PGRST128 <pgrst128>` error.
+17 -163
View File
@@ -209,25 +209,18 @@ The join table is also detected if the composite key has additional columns.
One-to-one relationships One-to-one relationships
------------------------ ------------------------
One-to-one relationships are detected in two ways. (We'll use the ``films`` and ``technical_specs`` tables from the :ref:`sample film database <erd_film>` as an example). One-to-one relationships are detected in two ways.
- When the foreign key is also a primary key. - When the foreign key is a primary key as specified in the :ref:`sample film database <erd_film>`.
- When the foreign key has a unique constraint.
.. code-block:: postgres .. code-block:: postgres
create table technical_specs( create table technical_specs(
film_id int references films(id) primary key film_id int references films(id) unique,
-- ... runtime time,
); camera text,
sound text
- Or when the foreign key has a unique constraint.
.. code-block:: postgres
create table technical_specs(
id int primary key
, film_id int references films(id) unique
-- ...
); );
.. code-block:: bash .. code-block:: bash
@@ -934,12 +927,7 @@ Filters can also be applied on nested embedded resources:
.. code-block:: bash .. code-block:: bash
# curl "http://localhost:3000/films?select=*,roles(*,actors(*))&roles.actors.order=last_name&roles.actors.first_name=like.*Tom*" curl "http://localhost:3000/films?select=*,roles(*,actors(*))&roles.actors.order=last_name&roles.actors.first_name=like.*Tom*"
curl --get "http://localhost:3000/films" \
-d "select=*,roles(*,actors(*))" \
-d "roles.actors.order=last_name" \
-d "roles.actors.first_name=like.*Tom*"
The result will show the nested actors named Tom and order them by last name. Aliases can also be used instead of the resource names to filter the nested tables. The result will show the nested actors named Tom and order them by last name. Aliases can also be used instead of the resource names to filter the nested tables.
@@ -1150,19 +1138,14 @@ For example, to arrange the films in descending order using the director's last
Spread embedded resource Spread embedded resource
======================== ========================
You can modify the shape of the embedded resources by using the spread syntax (``...``). On many-to-one and one-to-one relationships, you can "spread" the embedded resource. That is, remove the surrounding JSON object for the embedded resource columns.
.. _spread_to_one_embed:
Spread To-One relationships
---------------------------
Spread on resources forming :ref:`one-to-one <one-to-one>` and :ref:`many-to-one <many-to-one>` relationships, will lift the embedded columns to the top object.
.. code-block:: bash .. code-block:: bash
# curl "http://localhost:3000/films?select=title,...directors(director_last_name:last_name)&title=like.*Workers*"
curl --get "http://localhost:3000/films" \ curl --get "http://localhost:3000/films" \
-d "select=title,...directors(director_first_name:first_name, director_last_name:last_name)" \ -d "select=title,...directors(director_last_name:last_name)" \
-d "title=like.*Workers*" -d "title=like.*Workers*"
.. code-block:: json .. code-block:: json
@@ -1170,148 +1153,17 @@ Spread on resources forming :ref:`one-to-one <one-to-one>` and :ref:`many-to-one
[ [
{ {
"title": "Workers Leaving The Lumière Factory In Lyon", "title": "Workers Leaving The Lumière Factory In Lyon",
"director_first_name": "Louis",
"director_last_name": "Lumière" "director_last_name": "Lumière"
} }
] ]
Note that there is no wrapping ``"directors"`` object, unlike regularly embedding :ref:`many-to-one <many-to-one>` relationships. Also note that embedded columns can be aliased normally. Note that there is no ``"directors"`` object. Also the embed columns can be aliased normally.
.. _spread_to_many_embed: You can use this to get the columns of a join table in a many-to-many relationship. For instance, to get films and its actors, but including the ``character`` column from the roles table:
Spread To-Many relationships
----------------------------
Spread on resources forming :ref:`one-to-many <one-to-many>` and :ref:`many-to-many <many-to-many>` relationships, will convert the embedded columns into correlated arrays.
.. code-block:: bash .. code-block:: bash
curl --get "http://localhost:3000/directors" \ # curl "http://localhost:3000/films?select=title,actors:roles(character,...actors(first_name,last_name))&title=like.*Lighthouse*"
-d "select=first_name,...films(film_titles:title,film_years:year)" \
-d "first_name=like.Quentin*"
.. code-block:: json
[
{
"first_name": "Quentin",
"film_titles": [
"Pulp Fiction",
"Reservoir Dogs"
],
"film_years": [
1994,
1992
]
}
]
Note that ``films`` is no longer an array of objects, unlike regularly embedding :ref:`one-to-many`. The embedded columns become arrays and they're correlated—in the above result, we can say that "Pulp Fiction" premiered in 1994 and "Reservoir Dogs" in 1992.
Order in spread to-many
~~~~~~~~~~~~~~~~~~~~~~~
In the above example, the order of the values inside the correlated arrays is unspecified, but all the values are guaranteed to be in the same unspecified order.
You can order the correlated arrays explicitly. For example, to order by the film year:
.. code-block:: bash
curl --get "http://localhost:3000/directors" \
-d "select=first_name,...films(film_titles:title,film_years:year)" \
-d "first_name=like.Quentin*" \
-d "films.order=year"
.. code-block:: json
[
{
"first_name": "Quentin",
"film_titles": [
"Reservoir Dogs",
"Pulp Fiction"
],
"film_years": [
1992,
1994
]
}
]
.. warning::
Aliasing spreaded columns is recommended since JSON allows duplicate keys. Example:
.. code-block:: bash
curl --get "localhost:3000/projects" \
-d "select=id,name,...clients(id,name)"
.. code-block:: json
[{"id":1,"name":"Windows 7","id":1,"name":"Microsoft"},
{"id":2,"name":"Windows 10","id":1,"name":"Microsoft"},
{"id":3,"name":"IOS","id":2,"name":"Apple"},
{"id":4,"name":"OSX","id":2,"name":"Apple"},
{"id":5,"name":"Orphan","id":null,"name":null}]
This can be a problem in Javascript objects, since only the last duplicated key will be considered. To solve it do:
.. code-block:: bash
curl --get "localhost:3000/projects" \
-d "select=id,name,...clients(client_id:id,client_name:name)"
Multiple Spreads
----------------
You can use multiple spreads at any level. For example, let's spread ``technical_specs`` and ``roles`` into ``films`` and then spread ``films`` into ``directors``:
.. code-block:: bash
curl --get "http://localhost:3000/directors" \
-d "select=first_name,...films(film_titles:title,film_years:year,...technical_specs(film_runtimes:runtime),...roles(film_characters:character))" \
-d "first_name=like.Quentin*" \
-d "films.order=year" \
-d "films.roles.order=character"
.. code-block:: json
[
{
"first_name": "Quentin",
"film_titles": [
"Reservoir Dogs",
"Pulp Fiction"
],
"film_years": [
1992,
1994
],
"film_runtimes": [
"01:39:00",
"02:29:00"
]
"film_characters": [
[ "Mr. Pink", "Mr. White" ],
[ "Mia Wallace", "Vincent Vega" ]
]
}
]
Note that:
- All the ``film_*`` arrays are correlated—"Reservoir Dogs" premiered in 1992, its runtime is 1:39:00 and it has the following characters: ``[ "Mr. Pink", "Mr. White" ]``.
- The ``film_*`` arrays are ordered by ``year`` (due to ``films.order=year``).
- The bottom level array ``film_characters`` is ordered (due to ``films.roles.order=character``).
Spread a join table
-------------------
Spread can be used to move the columns of a join table in a :ref:`many-to-many <many-to-many>` to the top object. For instance, to get the ``character`` column of the ``roles`` join table into ``actors``:
.. code-block:: bash
curl --get "http://localhost:3000/films" \ curl --get "http://localhost:3000/films" \
-d "select=title,actors:roles(character,...actors(first_name,last_name))" \ -d "select=title,actors:roles(character,...actors(first_name,last_name))" \
@@ -1332,4 +1184,6 @@ Spread can be used to move the columns of a join table in a :ref:`many-to-many <
} }
] ]
.. note::
The spread operator ``...`` is borrowed from the Javascript `spread syntax <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax>`_.
@@ -100,15 +100,17 @@ This returns
{ "id": 1 } { "id": 1 }
with a :code:`Content-Type: application/vnd.pgrst.object+json`.
When a singular response is requested but no entries are found, the server responds with an error message and 406 Not Acceptable status code rather than the usual empty array and 200 status: When a singular response is requested but no entries are found, the server responds with an error message and 406 Not Acceptable status code rather than the usual empty array and 200 status:
.. code-block:: json .. code-block:: json
{ {
"code": "PGRST116", "message": "JSON object requested, multiple (or no) rows returned",
"message": "Cannot coerce the result to a single JSON object", "details": "Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row",
"details": "The result contains 0 rows", "hint": null,
"hint": null "code": "PGRST505"
} }
.. note:: .. note::
+32 -29
View File
@@ -72,7 +72,7 @@ imatch :code:`~*` ~* operator, see :ref:`pattern_matching`
in :code:`IN` one of a list of values, e.g. :code:`?a=in.(1,2,3)` in :code:`IN` one of a list of values, e.g. :code:`?a=in.(1,2,3)`
also supports commas in quoted strings like also supports commas in quoted strings like
:code:`?a=in.("hi,there","yes,you")` :code:`?a=in.("hi,there","yes,you")`
is :code:`IS` checking for exact equality (null,not_null,true,false,unknown) is :code:`IS` checking for exact equality (null,true,false,unknown)
isdistinct :code:`IS DISTINCT FROM` not equal, treating :code:`NULL` as a comparable value isdistinct :code:`IS DISTINCT FROM` not equal, treating :code:`NULL` as a comparable value
fts :code:`@@` :ref:`fts` using to_tsquery fts :code:`@@` :ref:`fts` using to_tsquery
plfts :code:`@@` :ref:`fts` using plainto_tsquery plfts :code:`@@` :ref:`fts` using plainto_tsquery
@@ -175,47 +175,25 @@ To ensure best performance on larger data sets, an `appropriate index <https://w
Full-Text Search Full-Text Search
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
The :code:`fts` operator has a number of options to support flexible textual queries, namely the choice of plain vs phrase search and the language used for stemming. The :code:`fts` filter mentioned above has a number of options to support flexible textual queries, namely the choice of plain vs phrase search and the language used for stemming. Suppose that :code:`tsearch` is a table with column :code:`my_tsv`, of type `tsvector <https://www.postgresql.org/docs/current/datatype-textsearch.html>`_. The following examples illustrate the possibilities.
The following examples illustrate the possibilities, assuming column :code:`my_tsv` is of type `tsvector <https://www.postgresql.org/docs/current/datatype-textsearch.html>`_.
.. code-block:: bash .. code-block:: bash
curl --get "http://localhost:3000/people" \ curl "http://localhost:3000/tsearch?my_tsv=fts(french).amusant"
-d "my_tsv=fts(french).amusant"
.. code-block:: bash .. code-block:: bash
curl --get "http://localhost:3000/people" \ curl "http://localhost:3000/tsearch?my_tsv=plfts.The%20Fat%20Cats"
-d "my_tsv=plfts.The%20Fat%20Cats"
.. code-block:: bash .. code-block:: bash
curl --get "http://localhost:3000/people" \ curl "http://localhost:3000/tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats"
-d "my_tsv=not.phfts(english).The%20Fat%20Cats"
.. code-block:: bash .. code-block:: bash
curl --get "http://localhost:3000/people" \ curl "http://localhost:3000/tsearch?my_tsv=not.wfts(french).amusant"
-d "my_tsv=not.wfts(french).amusant"
.. _fts_to_tsvector: Using `websearch_to_tsquery` requires PostgreSQL of version at least 11.0 and will raise an error in earlier versions of the database.
Automatic ``tsvector`` conversion
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
If the filtered column is not of type ``tsvector``, then it will be automatically converted using `to_tsvector() <https://www.postgresql.org/docs/current/functions-textsearch.html#TEXTSEARCH-FUNCTIONS-TABLE>`_.
This allows using the ``fts`` operator on ``text`` and ``json`` types out of the box.
.. code-block:: bash
curl --get "http://localhost:3000/people" \
-d "my_text_column=fts(french).amusant"
.. code-block:: bash
curl --get "http://localhost:3000/people" \
-d "my_json_column=not.phfts(english).The%20Fat%20Cats"
.. _v_filter: .. _v_filter:
@@ -705,6 +683,31 @@ Deletions also support :ref:`prefer_return`, :ref:`resource_embedding` and :ref:
Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`. Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`.
.. _limited_update_delete:
Limited Update/Delete
=====================
You can limit the amount of affected rows by :ref:`update` or :ref:`delete` with the ``limit`` query parameter. For this, you must add an explicit ``order`` on a unique column(s).
.. code-block:: bash
curl -X PATCH "/users?limit=10&order=id&last_login=lt.2020-01-01" \
-H "Content-Type: application/json" \
-d '{ "status": "inactive" }'
.. code-block:: bash
curl -X DELETE "http://localhost:3000/users?limit=10&order=id&status=eq.inactive"
If your table has no unique columns, you can use the `ctid <https://www.postgresql.org/docs/current/ddl-system-columns.html>`_ system column.
Using ``offset`` to target a different subset of rows is also possible.
.. note::
There is no native ``UPDATE...LIMIT`` or ``DELETE...LIMIT`` support in PostgreSQL; the generated query simulates that behavior and is based on `this Crunchy Data blog post <https://www.crunchydata.com/blog/simulating-update-or-delete-with-limit-in-postgres-ctes-to-the-rescue>`_.
.. raw:: html .. raw:: html
<script type="text/javascript"> <script type="text/javascript">
+3 -75
View File
@@ -77,7 +77,7 @@ If the client included no JWT (or one without a role claim) then PostgREST switc
JWT Generation JWT Generation
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
You can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_auth`). You can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_jwt`).
.. _client_auth: .. _client_auth:
@@ -156,21 +156,6 @@ You can specify the literal value as we saw earlier, or reference a filename to
jwt-secret = "@rsa.jwk.pub" jwt-secret = "@rsa.jwk.pub"
JWK ``kid`` validation
^^^^^^^^^^^^^^^^^^^^^^
PostgREST has built-in validation of the `key ID parameter <https://www.rfc-editor.org/rfc/rfc7517#section-4.5>`_, useful when working with a JWK Set.
It goes as follows:
- If the JWT contains a ``kid`` parameter, then PostgREST will look for the JWK in the :ref:`jwt-secret`.
+ If no JWK matches the same ``kid`` value (or if they do not have a ``kid``), then the token will be rejected with a :ref:`401 Unauthorized <pgrst301>` error.
+ If a JWK matches the ``kid`` value then it will validate the token against that JWK accordingly.
- If the JWT does not have a ``kid`` parameter, then PostgREST will validate the token against each JWK in the :ref:`jwt-secret`.
.. _jwt_claims_validation:
JWT Claims Validation JWT Claims Validation
~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
@@ -179,67 +164,10 @@ PostgREST honors the following `JWT claims <https://datatracker.ietf.org/doc/htm
- ``exp`` Expiration Time - ``exp`` Expiration Time
- ``iat`` Issued At - ``iat`` Issued At
- ``nbf`` Not Before - ``nbf`` Not Before
- ``aud`` :ref:`Audience <jwt_aud_validation>` - ``aud`` Audience, see :ref:`jwt-aud`
.. note:: .. note::
PostgREST allows for a 30-second clock skew when validating the ``exp``, ``iat`` and ``nbf`` claims. 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.
In other words, it gives an extra 30 seconds before the token is rejected if there is a slight discrepancy in the timestamps.
.. _jwt_aud_validation:
JWT ``aud`` Claim Validation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
PostgREST has built-in validation of the `JWT audience claim <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3>`_.
It works this way:
- If :ref:`jwt-aud` is not set (the default), PostgREST identifies with all audiences and allows the JWT for any ``aud`` claim.
- If :ref:`jwt-aud` is set to a specific audience, PostgREST will check if this audience is present in the ``aud`` claim:
+ If the ``aud`` value is a JSON string, it will match it to the :ref:`jwt-aud`.
+ If the ``aud`` value is a JSON array of strings, it will search every element for a match.
+ If the match fails or if the ``aud`` value is not a string or array of strings, then the token will be rejected with a :ref:`401 Unauthorized <pgrst303>` error.
+ If the ``aud`` key **is not present** or if its value is ``null`` or ``[]``, PostgREST will interpret this token as allowed for all audiences and will complete the request.
.. _jwt_role_extract:
JWT Role Extraction
~~~~~~~~~~~~~~~~~~~
A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. It's configured by :ref:`jwt-role-claim-key`. This can be used to consume a JWT provided by a third party service like Auth0, Okta, Microsoft Entra or Keycloak.
The DSL follows the `JSONPath <https://goessner.net/articles/JsonPath/>`_ expression grammar with extended string comparison operators. Supported operators are:
- ``==`` selects the first array element that exactly matches the right operand
- ``!=`` selects the first array element that does not match the right operand
- ``^==`` selects the first array element that starts with the right operand
- ``==^`` selects the first array element that ends with the right operand
- ``*==`` selects the first array element that contains the right operand
Usage examples:
.. code:: bash
# {"postgrest":{"roles": ["other", "author"]}}
# the DSL accepts characters that are alphanumerical or one of "_$@" as keys
jwt-role-claim-key = ".postgrest.roles[1]"
# {"https://www.example.com/role": { "key": "author" }}
# non-alphanumerical characters can go inside quotes(escaped in the config value)
jwt-role-claim-key = ".\"https://www.example.com/role\".key"
# {"postgrest":{"roles": ["other", "author"]}}
# `@` represents the current element in the array
# all the these match the string "author"
jwt-role-claim-key = ".postgrest.roles[?(@ == \"author\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ != \"other\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ ^== \"aut\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ *== \"utho\")]"
.. note::
The string comparison operators are implemented as a custom extension to the JSPath and does not strictly follow the `RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_.
JWT Security JWT Security
~~~~~~~~~~~~ ~~~~~~~~~~~~
+41 -56
View File
@@ -146,21 +146,6 @@ To reload the configuration from within the database, you can use the ``NOTIFY``
List of parameters List of parameters
================== ==================
.. _admin-server-host:
admin-server-host
-----------------
=============== =======================
**Type** String
**Default** `server-host` value
**Reloadable** N
**Environment** PGRST_ADMIN_SERVER_HOST
**In-Database** `n/a`
=============== =======================
Specifies the host for the :ref:`admin_server`. Defaults to :ref:`server-host` value.
.. _admin-server-port: .. _admin-server-port:
admin-server-port admin-server-port
@@ -174,7 +159,31 @@ admin-server-port
**In-Database** `n/a` **In-Database** `n/a`
=============== ======================= =============== =======================
Specifies the port for the :ref:`admin_server`. Cannot be equal to :ref:`server-port`. Specifies the port for the :ref:`admin_server`.
.. _admin-server-config-enabled:
admin-server-config-enabled
---------------------------
.. danger::
The ``/config`` endpoint contains sensitive information, don't enable this if you're exposing the Admin Server publicly.
To safely enable this you can use a proxy like :ref:`nginx` to:
- Ensure ``/config`` are only available to local networks.
- Only expose ``/live`` and ``/ready`` to public networks.
=============== =================================
**Type** Boolean
**Default** False
**Reloadable** N
**Environment** PGRST_ADMIN_SERVER_CONFIG_ENABLED
**In-Database** `n/a`
=============== =================================
Enables the admin server :ref:`runtime_config` and :ref:`runtime_schema_cache` endpoints.
.. _app.settings.*: .. _app.settings.*:
@@ -191,10 +200,6 @@ 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')`. 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')`.
When using the environment variable `PGRST_APP_SETTINGS_*` form, the remainder of the variable is used as the new name. Case is not important : :code:`PGRST_APP_SETTINGS_MY_ENV_VARIABLE=some_value` can be accessed in postgres as :code:`current_setting('app.settings.my_env_variable')`.
The :code:`current_setting` function has `an optional boolean second <https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET>`_ argument to avoid it from raising an error if the value was not defined. Default values to :code:`app.settings` can then be given by combining this argument with :code:`coalesce` and :code:`nullif` : :code:`coalesce(nullif(current_setting('app.settings.my_custom_variable', true), ''), 'default value')`. The use of :code:`nullif` is necessary because if set in a transaction, the setting is sometimes not "rolled back" to :code:`null`. See also :ref:`this section <guc_req_headers_cookies_claims>` for more information on this behaviour.
.. _db-aggregates-enabled: .. _db-aggregates-enabled:
db-aggregates-enabled db-aggregates-enabled
@@ -315,10 +320,6 @@ db-extra-search-path
Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``. Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``.
.. important::
We default this config to ``public`` because it is the most common schema used to install PostgreSQL extensions such as :ref:`PostGIS <ww_postgis>`. You can disable this by setting this config to ``""``.
.. _db-hoisted-tx-settings: .. _db-hoisted-tx-settings:
db-hoisted-tx-settings db-hoisted-tx-settings
@@ -603,7 +604,11 @@ jwt-aud
**In-Database** pgrst.jwt_aud **In-Database** pgrst.jwt_aud
=============== ================================= =============== =================================
Specifies an audience for the JWT ``aud`` claim. See :ref:`jwt_aud_validation`. Specifies the `JWT audience claim <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3>`_. If this claim is present in the client provided JWT then you must set this to the same value as in the JWT, otherwise verifying the JWT will fail.
.. warning::
Using this setting will only reject tokens with a different audience claim. Tokens **without** audience claim will still be accepted.
.. _jwt-role-claim-key: .. _jwt-role-claim-key:
@@ -620,7 +625,17 @@ jwt-role-claim-key
*For backwards compatibility, this config parameter is also available without prefix as "role-claim-key".* *For backwards compatibility, this config parameter is also available without prefix as "role-claim-key".*
See :ref:`jwt_role_extract` on how to specify key paths and usage examples. A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. This can be used to consume a JWT provided by a third party service like Auth0, Okta or Keycloak. Usage examples:
.. code:: bash
# {"postgrest":{"roles": ["other", "author"]}}
# the DSL accepts characters that are alphanumerical or one of "_$@" as keys
jwt-role-claim-key = ".postgrest.roles[1]"
# {"https://www.example.com/role": { "key": "author }}
# non-alphanumerical characters can go inside quotes(escaped in the config value)
jwt-role-claim-key = ".\"https://www.example.com/role\".key"
.. _jwt-secret: .. _jwt-secret:
@@ -708,30 +723,6 @@ log-level
Because currently there's no buffering for logging, the levels with minimal logging(``crit/error``) will increase throughput. Because currently there's no buffering for logging, the levels with minimal logging(``crit/error``) will increase throughput.
.. _log-query:
log-query
---------
=============== =================================
**Type** String
**Default** "disabled"
**Reloadable** Y
**Environment** PGRST_LOG_QUERY
**In-Database** `n/a`
=============== =================================
Logs the SQL query for the corresponding request at the current :ref:`log-level`.
See :ref:`sql_query_logs`.
.. code:: bash
# Logs the main SQL query
log-query = "main-query"
# Disables logging the SQL query
log-query = "disabled"
.. _openapi-mode: .. _openapi-mode:
openapi-mode openapi-mode
@@ -845,12 +836,6 @@ server-host
* :code:`*6` - any IPv4 or IPv6 hostname, IPv6 preferred * :code:`*6` - any IPv4 or IPv6 hostname, IPv6 preferred
* :code:`!6` - any IPv6 hostname * :code:`!6` - any IPv6 hostname
Examples:
.. code:: bash
server-host = "127.0.0.1"
.. _server-port: .. _server-port:
server-port server-port
+14 -60
View File
@@ -5,8 +5,6 @@ Errors
PostgREST error messages follow the PostgreSQL error structure. It includes ``MESSAGE``, ``DETAIL``, ``HINT``, ``ERRCODE`` and will add an HTTP status code to the response. PostgREST error messages follow the PostgreSQL error structure. It includes ``MESSAGE``, ``DETAIL``, ``HINT``, ``ERRCODE`` and will add an HTTP status code to the response.
.. _postgresql_errors:
Errors from PostgreSQL Errors from PostgreSQL
====================== ======================
@@ -207,6 +205,14 @@ Related to the HTTP request elements.
| | | specified in the ``select`` part of the query string. | | | | specified in the ``select`` part of the query string. |
| PGRST108 | | See :ref:`embed_filters`. | | PGRST108 | | See :ref:`embed_filters`. |
+---------------+-------------+-------------------------------------------------------------+ +---------------+-------------+-------------------------------------------------------------+
| .. _pgrst109: | 400 | Restricting a Deletion or an Update using limits must |
| | | include the ordering of a unique column. |
| PGRST109 | | See :ref:`limited_update_delete`. |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst110: | 400 | When restricting a Deletion or an Update using limits |
| | | modifies more rows than the maximum specified in the limit. |
| PGRST110 | | See :ref:`limited_update_delete`. |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst111: | 500 | An invalid ``response.headers`` was set. | | .. _pgrst111: | 500 | An invalid ``response.headers`` was set. |
| | | See :ref:`guc_resp_hdrs`. | | | | See :ref:`guc_resp_hdrs`. |
| PGRST111 | | | | PGRST111 | | |
@@ -235,6 +241,10 @@ Related to the HTTP request elements.
| | | there is no many-to-one or one-to-one relationship between | | | | there is no many-to-one or one-to-one relationship between |
| PGRST118 | | them. | | PGRST118 | | them. |
+---------------+-------------+-------------------------------------------------------------+ +---------------+-------------+-------------------------------------------------------------+
| .. _pgrst119: | 400 | Could not use the spread operator on the related table |
| | | because there is no many-to-one or one-to-one relationship |
| PGRST119 | | between them. |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst120: | 400 | An embedded resource can only be filtered using the | | .. _pgrst120: | 400 | An embedded resource can only be filtered using the |
| | | ``is.null`` or ``not.is.null`` :ref:`operators <operators>`.| | | | ``is.null`` or ``not.is.null`` :ref:`operators <operators>`.|
| PGRST120 | | | | PGRST120 | | |
@@ -247,31 +257,6 @@ Related to the HTTP request elements.
| | | ``Prefer: handling=strict``. See :ref:`prefer_handling`. | | | | ``Prefer: handling=strict``. See :ref:`prefer_handling`. |
| PGRST122 | | | | PGRST122 | | |
+---------------+-------------+-------------------------------------------------------------+ +---------------+-------------+-------------------------------------------------------------+
| .. _pgrst123: | 400 | Aggregate functions are disabled. |
| | | See :ref:`db-aggregates-enabled`. |
| PGRST123 | | |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst124: | 400 | ``max-affected`` preference is violated. |
| | | See :ref:`prefer_max_affected`. |
| PGRST124 | | |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst125: | 404 | Invalid path is specified in request URL. |
| | | |
| PGRST125 | | |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst126: | 404 | Open API config is disabled but API root path is |
| | | accessed. See :ref:`openapi-mode`. |
| PGRST126 | | |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst127: | 400 | The feature specified in the ``details`` field is not |
| | | implemented. |
| PGRST127 | | |
+---------------+-------------+-------------------------------------------------------------+
| .. _pgrst128: | 400 | ``max-affected`` preference is violated with ``RPC`` call. |
| | | See :ref:`prefer_max_affected`. |
| PGRST128 | | |
+---------------+-------------+-------------------------------------------------------------+
.. _pgrst2**: .. _pgrst2**:
@@ -305,10 +290,6 @@ Related to a :ref:`schema_cache`. Most of the time, these errors are solved by :
| | | in the ``columns`` query parameter is not found. | | | | in the ``columns`` query parameter is not found. |
| PGRST204 | | | | PGRST204 | | |
+---------------+-------------+-------------------------------------------------------------+ +---------------+-------------+-------------------------------------------------------------+
| .. _pgrst205: | 404 | Caused when the :ref:`table specified <tables_views>` in |
| | | the URI is not found. |
| PGRST205 | | |
+---------------+-------------+-------------------------------------------------------------+
.. _pgrst3**: .. _pgrst3**:
@@ -324,18 +305,14 @@ Related to the authentication process using JWT. You can follow the :ref:`tut1`
| | | configuration. | | | | configuration. |
| PGRST300 | | | | PGRST300 | | |
+---------------+-------------+-------------------------------------------------------------+ +---------------+-------------+-------------------------------------------------------------+
| .. _pgrst301: | 401 | Provided JWT couldn't be decoded or it is invalid. | | .. _pgrst301: | 401 | Any error related to the verification of the JWT, |
| | | | | | | which means that the JWT provided is invalid in some way. |
| PGRST301 | | | | PGRST301 | | |
+---------------+-------------+-------------------------------------------------------------+ +---------------+-------------+-------------------------------------------------------------+
| .. _pgrst302: | 401 | Attempted to do a request without | | .. _pgrst302: | 401 | Attempted to do a request without |
| | | :ref:`authentication <client_auth>` when the anonymous role | | | | :ref:`authentication <client_auth>` when the anonymous role |
| PGRST302 | | is disabled by not setting it in :ref:`db-anon-role`. | | PGRST302 | | is disabled by not setting it in :ref:`db-anon-role`. |
+---------------+-------------+-------------------------------------------------------------+ +---------------+-------------+-------------------------------------------------------------+
| .. _pgrst303: | 401 | :ref:`JWT claims validation <jwt_claims_validation>` |
| | | or parsing failed. |
| PGRST303 | | |
+---------------+-------------+-------------------------------------------------------------+
.. The Internal Errors Group X** is always at the end .. The Internal Errors Group X** is always at the end
@@ -354,8 +331,6 @@ Internal errors. If you encounter any of these, you may have stumbled on a Postg
| PGRSTX00 | | | | PGRSTX00 | | |
+---------------+-------------+-------------------------------------------------------------+ +---------------+-------------+-------------------------------------------------------------+
.. _custom_errors:
Custom Errors Custom Errors
============= =============
@@ -452,24 +427,3 @@ For non standard HTTP status, you can optionally add ``status_text`` to describe
detail = '{"status":419,"status_text":"Page Expired","headers":{"X-Powered-By":"Nerd Rage"}}'; 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**>`. If PostgREST can't parse the JSON objects ``message`` and ``detail``, it will throw a ``PGRST121`` error. See :ref:`Errors from PostgREST<pgrst1**>`.
.. _proxy-status_header:
Proxy-Status Header
===================
For error cases, the standard `Proxy-Status <https://www.rfc-editor.org/rfc/rfc9209.html#name-the-proxy-status-http-field>`_ header is returned with the error code. The error code comes from either :ref:`PostgREST <pgrst_errors>`, :ref:`PostgreSQL <postgresql_errors>` or :ref:`Custom <custom_errors>` errors. This is useful when doing ``HEAD`` requests where the HTTP status is not descriptive enough.
For example, doing a request on a table with high count (say 30_000_000), we get:
.. code-block:: http
HEAD /table HTTP/1.1
Prefer: count=exact
.. code-block:: http
HTTP/1.1 500 Internal Server Error
Proxy-Status: PostgREST; error=57014
The PostgreSQL error code ``57014`` (`ref <https://www.postgresql.org/docs/current/errcodes-appendix.html>`_) reveals that the error is due to a short ``statement_timeout`` value.
+10 -73
View File
@@ -15,14 +15,12 @@ Observability allows measuring a system's current state based on the data it gen
Logs Logs
==== ====
PostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, the HTTP response status and the response body size in bytes if available. PostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, and HTTP response status.
With :ref:`log-level` set to ``info``, we get:
.. code:: .. code::
127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 56 "" "curl/7.64.0" 127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 - "" "curl/7.64.0"
127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 162 "" "curl/7.64.0" 127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 - "" "curl/7.64.0"
For diagnostic information about the server itself, PostgREST logs to ``stderr``: For diagnostic information about the server itself, PostgREST logs to ``stderr``:
@@ -33,52 +31,22 @@ For diagnostic information about the server itself, PostgREST logs to ``stderr``
.. code:: .. code::
06/May/2024:08:16:11 -0500: Starting PostgREST 12.1... 06/May/2024:08:16:11 -0500: Starting PostgREST 12.1...
06/May/2024:08:16:11 -0500: Attempting to connect to the database...
06/May/2024:08:16:11 -0500: Successfully connected to PostgreSQL 14.10 (Ubuntu 14.10-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit 06/May/2024:08:16:11 -0500: Successfully connected to PostgreSQL 14.10 (Ubuntu 14.10-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit
06/May/2024:08:16:11 -0500: Connection Pool initialized with a maximum size of 10 connections 06/May/2024:08:16:11 -0500: Listening on port 3000
06/May/2024:08:16:11 -0500: API server listening on port 3000 06/May/2024:08:16:11 -0500: Listening for notifications on the "pgrst" channel
06/May/2024:08:16:11 -0500: Listening for database notifications on the "pgrst" channel
06/May/2024:08:16:11 -0500: Config reloaded 06/May/2024:08:16:11 -0500: Config reloaded
06/May/2024:08:16:11 -0500: Schema cache queried in 3.8 milliseconds 06/May/2024:08:16:11 -0500: Schema cache queried in 3.8 milliseconds
06/May/2024:08:16:11 -0500: Schema cache loaded 15 Relations, 8 Relationships, 8 Functions, 0 Domain Representations, 4 Media Type Handlers 06/May/2024:08:16:11 -0500: Schema cache loaded 15 Relations, 8 Relationships, 8 Functions, 0 Domain Representations, 4 Media Type Handlers
06/May/2024:14:11:27 -0500: Received a config reload message on the "pgrst" channel 06/May/2024:14:11:27 -0500: Received a config reload message on the "pgrst" channel
06/May/2024:14:11:27 -0500: Config reloaded 06/May/2024:14:11:27 -0500: Config reloaded
.. note::
Logs are based on the ``log-level`` setting. See :ref:`log-level`.
.. _sql_query_logs:
SQL Query Logs
--------------
To log the :ref:`main SQL query <main_query>` executed for a request, set the :ref:`log-query` to ``main-query``.
It will be logged based on the current :ref:`log-level` setting.
For example, with this configuration:
.. code-block:: bash
log-level = "warn"
log-query = "main-query"
The SQL queries will only be logged on ``400`` HTTP errors and up.
So, if the user requests a resource without sufficient privileges:
.. code-block:: bash
curl "localhost:3000/protected_table"
This will be logged by PostgREST:
.. code::
17/Feb/2025:17:28:15 -0500: WITH pgrst_source AS ( SELECT "public"."protected_table".* FROM "public"."protected_table" ) SELECT null::bigint AS total_result_set, pg_catalog.count(_postgrest_t) AS page_total, coalesce(json_agg(_postgrest_t), '[]') AS body, nullif(current_setting('response.headers', true), '') AS response_headers, nullif(current_setting('response.status', true), '') AS response_status, '' AS response_inserted FROM ( SELECT * FROM pgrst_source ) _postgrest_t
127.0.0.1 - web_anon [17/Feb/2025:17:28:15 -0500] "GET /protected_table HTTP/1.1" 401 99 "" "curl/8.7.1"
Database Logs Database Logs
------------- -------------
Additionally, to find all the SQL operations, you can watch the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below. Currently PostgREST doesn't log the SQL commands executed against the underlying database.
To find the SQL operations, you can watch the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below.
Find :code:`postgresql.conf` inside your PostgreSQL data directory (to find that, issue the command :code:`show data_directory;`). Either find the settings scattered throughout the file and change them to the following values, or append this block of code to the end of the configuration file. Find :code:`postgresql.conf` inside your PostgreSQL data directory (to find that, issue the command :code:`show data_directory;`). Either find the settings scattered throughout the file and change them to the following values, or append this block of code to the end of the configuration file.
@@ -122,17 +90,12 @@ Restart the database and watch the log file in real-time to understand how HTTP
Metrics Metrics
======= =======
The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in `Prometheus text format <https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format>`_. The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in `Prometheus text format <https://prometheus.io/docs/instrumenting/exposition_formats/#prometheus-text-format>`_.
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3001/metrics" curl "http://localhost:3001/metrics"
.. code-block:: http
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
# HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load # HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load
# TYPE pgrst_schema_cache_query_time_seconds gauge # TYPE pgrst_schema_cache_query_time_seconds gauge
pgrst_schema_cache_query_time_seconds 1.5937927e-2 pgrst_schema_cache_query_time_seconds 1.5937927e-2
@@ -241,11 +204,6 @@ You can enable tracing HTTP requests by setting :ref:`server-trace-header`. Spec
HTTP/1.1 200 OK HTTP/1.1 200 OK
X-Request-Id: 123 X-Request-Id: 123
Proxy-Status Header
-------------------
See :ref:`proxy-status_header`.
.. _server-timing_header: .. _server-timing_header:
Server-Timing Header Server-Timing Header
@@ -275,27 +233,6 @@ This header communicates metrics of the different phases in the request-response
We're working on lowering the duration of the ``parse`` and ``plan`` stages on https://github.com/PostgREST/postgrest/issues/2816. We're working on lowering the duration of the ``parse`` and ``plan`` stages on https://github.com/PostgREST/postgrest/issues/2816.
.. _content-length_header:
Content-Length Header
---------------------
You can verify the response body size in bytes in the `Content-Length header <https://httpwg.org/specs/rfc9110.html#field.content-length>`_.
.. code-block:: bash
curl -i 'localhost:3000/users'
.. code-block:: http
HTTP/1.1 200 OK
Content-Length: 104
Note that this header won't be returned on ``HEAD`` requests for optimization purposes (see :ref:`head_req`).
This is in line with `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.content-length>`_.
The body size is also present in the :ref:`PostgREST logs <pgrst_logging>`.
.. _explain_plan: .. _explain_plan:
Execution plan Execution plan
+5 -4
View File
@@ -284,10 +284,11 @@ However, starting from PostgreSQL 15, you can grant privileges for these setting
GRANT SET ON PARAMETER <setting> TO <authenticator>; GRANT SET ON PARAMETER <setting> TO <authenticator>;
Hoisted Function Settings Function Settings
~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
PostgREST can "hoist" function settings to transaction-scoped settings. This allows functions settings to override the impersonated and connection role 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:: postgres .. code-block:: postgres
@@ -302,7 +303,7 @@ When calling the above function (see :ref:`functions`), the statement timeout wi
.. note:: .. note::
Only the settings in :ref:`db-hoisted-tx-settings` will be hoisted. Only the transactions that are hoisted by config :ref:`db-hoisted-tx-settings` will be applied.
.. _main_query: .. _main_query:
+8 -6
View File
@@ -1,7 +1,9 @@
# This file is auto-generated by postgrest-nixpkgs-upgrade docutils==0.20.1
sphinx==7.4.7 sphinx-copybutton
sphinx-copybutton==0.5.2 sphinx-intl
sphinx-rtd-dark-mode==1.3.0 sphinx-rtd-theme>=0.5.1
sphinx-rtd-theme==3.0.2 sphinx-rtd-dark-mode>=1.3.0
sphinx-tabs==3.4.7 sphinx-tabs>=3.2.0
sphinx>=5.0.2
sphinxext-opengraph==0.9.1 sphinxext-opengraph==0.9.1
urllib3==2.2.3
+1 -12
View File
@@ -28,7 +28,7 @@
pacman -S postgrest pacman -S postgrest
.. tab:: Nix via nixpkgs .. tab:: Nix
You can install PostgREST from nixpkgs. You can install PostgREST from nixpkgs.
@@ -36,17 +36,6 @@
nix-env -i postgrest nix-env -i postgrest
.. tab:: Nix via flake
You can install PostgREST via flake.
.. code:: nix
{
inputs.postgrest.url = "github:postgrest/postgrest";
# ...
}
.. group-tab:: Windows .. group-tab:: Windows
You can install PostgREST using `Chocolatey <https://community.chocolatey.org/packages/postgrest>`_ or `Scoop <https://github.com/ScoopInstaller/Scoop>`_. You can install PostgREST using `Chocolatey <https://community.chocolatey.org/packages/postgrest>`_ or `Scoop <https://github.com/ScoopInstaller/Scoop>`_.
+6 -2
View File
@@ -213,8 +213,12 @@ You should see something similar to:
.. code-block:: text .. code-block:: text
Starting PostgREST 12.0.2... Starting PostgREST 12.0.2...
Successfully connected to PostgreSQL 14.10 (Ubuntu 14.10-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit Attempting to connect to the database...
API server listening on port 3000 Connection successful
Listening on port 3000
Config reloaded
Listening for notifications on the pgrst channel
Schema cache loaded
It's now ready to serve web requests. There are many nice graphical API exploration tools you can use, but for this tutorial we'll use :code:`curl` because it's likely to be installed on your system already. Open a new terminal (leaving the one open that PostgREST is running inside). Try doing an HTTP request for the todos. It's now ready to serve web requests. There are many nice graphical API exploration tools you can use, but for this tutorial we'll use :code:`curl` because it's likely to be installed on your system already. Open a new terminal (leaving the one open that PostgREST is running inside). Try doing an HTTP request for the todos.
+11 -33
View File
@@ -52,31 +52,17 @@ Check that the :code:`tutorial.conf` (created in the previous tutorial) has the
If the PostgREST server is still running from the previous tutorial, restart it to load the updated configuration file. If the PostgREST server is still running from the previous tutorial, restart it to load the updated configuration file.
.. _tut1_step3:
Step 3. Sign a Token Step 3. Sign a Token
-------------------- --------------------
Ordinarily your own code in the database or in another server will create and sign authentication tokens, but for this tutorial we will make one "by hand" using ``bash`` and ``openssl``. Ordinarily your own code in the database or in another server will create and sign authentication tokens, but for this tutorial we will make one "by hand." Go to `jwt.io <https://jwt.io/#debugger-io>`_ and fill in the fields like this:
.. code:: bash .. figure:: ../_static/tuts/tut1-jwt-io.png
:alt: jwt.io interface
#!/bin/bash How to create a token at https://jwt.io
set -e
JWT_SECRET='test_secret_that_is_at_least_32_characters_long' **Remember to fill in the secret you generated rather than the word "secret".** After you have filled in the secret and payload, the encoded data on the left will update. Copy the encoded token.
_base64 () { openssl base64 -e -A | tr '+/' '-_' | tr -d '='; }
header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | _base64)
payload=$(echo -n "{\"role\":\"todo_user\"}" | _base64)
signature=$(echo -n "$header.$payload" | openssl dgst -sha256 -hmac "$JWT_SECRET" -binary | _base64)
echo -n "$header.$payload.$signature"
**Remember to fill in the secret you generated rather than keeping the "test_secret_that_is_at_least_32_characters_long".** After you have filled in the secret and payload, the encoded data on the left will update. Copy the encoded token.
.. note:: .. note::
@@ -159,22 +145,14 @@ To observe expiration in action, we'll add an :code:`exp` claim of five minutes
select extract(epoch from now() + '5 minutes'::interval) :: integer; select extract(epoch from now() + '5 minutes'::interval) :: integer;
Or in ``bash``: Go back to jwt.io and change the payload to
.. code-block:: json
.. code-block:: bash {
"role": "todo_user",
exp=$(( EPOCHSECONDS + 5*60 )) # five minutes "exp": 123456789
}
echo $exp
Go back to :ref:`tut1_step3` and change the payload to
.. code-block:: bash
payload=$(echo -n "{\"role\":\"todo_user\",\"exp\":\"123456789\"}" | _base64)
echo -n "$header.$payload.$signature"
**NOTE**: Don't forget to change the dummy epoch value :code:`123456789` in the snippet above to the epoch value returned by the :code:`psql` command. **NOTE**: Don't forget to change the dummy epoch value :code:`123456789` in the snippet above to the epoch value returned by the :code:`psql` command.
Generated
-27
View File
@@ -1,27 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1731165248,
"narHash": "sha256-DiHFKIdBmMx5/DUARhVqaxvEIiy4EE6Eqs9Qs4oxme8=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "a90280100f41a10914edfe729a4053e60c92b8e3",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
-50
View File
@@ -1,50 +0,0 @@
{
description = "REST API for any Postgres database";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
};
nixConfig = {
extra-substituters = "https://postgrest.cachix.org";
extra-trusted-public-keys = "postgrest.cachix.org-1:icgW4R15fz1+LqvhPjt4EnX/r19AaqxiVV+1olwlZtI=";
};
outputs = { nixpkgs, ... }:
let
systems = [
"aarch64-darwin"
"aarch64-linux"
"x86_64-darwin"
"x86_64-linux"
];
pgrstFor = system: import ./default.nix {
inherit system;
nixpkgsVersion = {
owner = "nixos";
repo = "nixpkgs";
inherit (nixpkgs) rev;
tarballHash = nixpkgs.narHash;
};
};
genSystems = f: nixpkgs.lib.genAttrs systems (system: f (pgrstFor system));
in
{
packages = genSystems (attrs: {
default = attrs.postgrestPackage;
profiled = attrs.postgrestProfiled;
} // nixpkgs.lib.optionalAttrs (attrs ? postgrestStatic) {
static = attrs.postgrestStatic;
});
apps = genSystems (attrs: {
default = {
type = "app";
program = "${attrs.postgrestStatic or attrs.postgrestPackage}/bin/postgrest";
meta.description = "REST API for any Postgres database";
};
});
};
}
+30 -36
View File
@@ -72,53 +72,45 @@ The PostgREST utilities available in `nix-shell` all have names that begin with
```bash ```bash
# Note: The utilities listed here might not be up to date. # Note: The utilities listed here might not be up to date.
[nix-shell]$ postgrest-<tab> [nix-shell]$ postgrest-<tab>
postgrest-build postgrest-profiled-run postgrest-build postgrest-test-spec
postgrest-check postgrest-push-cachix postgrest-check postgrest-watch
postgrest-clean postgrest-release postgrest-clean postgrest-with-all
postgrest-coverage postgrest-repl postgrest-coverage postgrest-with-postgresql-10
postgrest-coverage-draft-overlay postgrest-run postgrest-lint postgrest-with-postgresql-11
postgrest-docs-build postgrest-style postgrest-run postgrest-with-postgresql-12
postgrest-docs-check postgrest-style-check postgrest-style postgrest-with-postgresql-13
postgrest-docs-dictcheck postgrest-test-big-schema postgrest-style-check postgrest-with-postgresql-9.6
postgrest-docs-linkcheck postgrest-test-doctests postgrest-test-io
postgrest-docs-render postgrest-test-io
postgrest-docs-serve postgrest-test-memory
postgrest-docs-spellcheck postgrest-test-replica
postgrest-dump-minimal-imports postgrest-test-spec
postgrest-dump-schema postgrest-test-spec-idempotence
postgrest-gen-ctags postgrest-watch
postgrest-gen-jwt postgrest-with-all
postgrest-gen-secret postgrest-with-git
postgrest-git-hooks postgrest-with-pgrst
postgrest-hsie-graph-modules postgrest-with-postgresql-12
postgrest-hsie-graph-symbols postgrest-with-postgresql-13
postgrest-hsie-minimal-imports postgrest-with-postgresql-14
postgrest-lint postgrest-with-postgresql-15
postgrest-loadtest postgrest-with-postgresql-16
postgrest-loadtest-against postgrest-with-postgresql-17
postgrest-loadtest-report postgrest-with-slow-pg
postgrest-nixpkgs-upgrade postgrest-with-slow-postgrest
postgrest-parallel-curl
... ...
[nix-shell]$ [nix-shell]$
``` ```
The `docker` module has large dependencies to be build before the shell becomes 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 available, which could take an especially long time if the cachix binary cache
is not used. You can activate it by passing a flag to `nix-shell` with is not used. You can activate those by passing a flag to `nix-shell` with
`nix-shell --arg docker true`. This will make the respective utilities available: `nix-shell --arg <module> true`. This will make the respective utilities available:
```bash ```bash
$ nix-shell --arg docker true $ nix-shell --arg memory true
[nix-shell]$ postgrest-docker-<tab> [nix-shell]$ postgrest-<tab>
postgrest-docker-load postgrest-build postgrest-test-spec
postgrest-check postgrest-watch
postgrest-clean postgrest-with-all
postgrest-coverage postgrest-with-postgresql-10
postgrest-lint postgrest-with-postgresql-11
postgrest-run postgrest-with-postgresql-12
postgrest-style postgrest-with-postgresql-13
postgrest-style-check postgrest-with-postgresql-9.6
postgrest-test-io
postgrest-test-memory
... ...
``` ```
Note that `postgrest-docker-load` is now also available. Note that `postgrest-test-memory` is now also available.
To run one-off commands, you can also use `nix-shell --run <command>`, which To run one-off commands, you can also use `nix-shell --run <command>`, which
will launch 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
@@ -379,8 +371,10 @@ that).
We also use `default.nix` to load our pinned version of the `nixpkgs` We also use `default.nix` to load our pinned version of the `nixpkgs`
repository. This set of packages will always be the same, independently from repository. This set of packages will always be the same, independently from
where or when you use it. The pinned version is taken from `flake.lock` and where or when you use it. The pinned version can be upgraded with the small
can be updated with `nix flake update`. `nixpkgs-upgrade` utility. Running `nixpkgs-upgrade > nix/nixpkgs-version.nix`
in `nix-shell` will upgrade the pinned version to the latest `nixpkgs-unstable`
version.
### `shell.nix` ### `shell.nix`
@@ -0,0 +1,46 @@
{ buildPythonPackage
, fetchFromGitHub
, lib
, nose
, sphinx
, sphinx-rtd-theme
}:
buildPythonPackage rec {
pname = "sphinx-rtd-dark-mode";
version = "1.3.0";
format = "setuptools";
src = fetchFromGitHub {
owner = "MrDogeBro";
repo = "sphinx_rtd_dark_mode";
rev = "refs/tags/v${version}";
hash = "sha256-N5KG2Wqn9wfGNY3VH4FnBce1aZUbnvVmwD10Loe0Qn4=";
};
propagatedBuildInputs = [
sphinx-rtd-theme
];
nativeCheckInputs = [
nose
sphinx
];
checkPhase = ''
runHook preCheck
nosetests tests
runHook postCheck
'';
pythonImportsCheck = [
"sphinx_rtd_dark_mode"
];
meta = with lib; {
description = "Adds a toggleable dark mode to the Read the Docs theme for Sphinx.";
homepage = "https://github.com/MrDogeBro/sphinx_rtd_dark_mode";
changelog = "https://github.com/MrDogeBro/sphinx_rtd_dark_mode/releases/tag/v${version}";
license = licenses.mit;
};
}
-4
View File
@@ -19,10 +19,6 @@ stdenv.mkDerivation {
pname = "libpq"; pname = "libpq";
inherit (postgresql) src version patches; inherit (postgresql) src version patches;
__structuredAttrs = true;
env.CFLAGS = "-fdata-sections -ffunction-sections"
+ (if stdenv.cc.isClang then " -flto" else " -fmerge-constants -Wl,--gc-sections");
configureFlags = [ configureFlags = [
"--without-gssapi" "--without-gssapi"
"--without-icu" "--without-icu"
+8
View File
@@ -0,0 +1,8 @@
# Pinned version of Nixpkgs, generated with postgrest-nixpkgs-upgrade.
{
owner = "NixOS";
repo = "nixpkgs";
date = "2024-05-29";
rev = "a15e8d1b3d9e6496c4a3214e2104f6d28dfa7df7";
tarballHash = "sha256:0r4a4165f4n1zlpnyjrdrwrg86n1b6g3axsdh3j5iizpmjrlxmd7";
}
+3
View File
@@ -3,6 +3,9 @@
checked-shell-script = import ./checked-shell-script; checked-shell-script = import ./checked-shell-script;
gitignore = import ./gitignore.nix; gitignore = import ./gitignore.nix;
haskell-packages = import ./haskell-packages.nix; haskell-packages = import ./haskell-packages.nix;
postgis = import ./postgis.nix;
postgresql-libpq = import ./postgresql-libpq.nix; postgresql-libpq = import ./postgresql-libpq.nix;
postgresql-legacy = import ./postgresql-legacy.nix;
postgresql-future = import ./postgresql-future.nix;
slocat = import ./slocat.nix; slocat = import ./slocat.nix;
} }
+17 -15
View File
@@ -48,7 +48,14 @@ let
# Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329 # Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329
# jailbreak, because hspec limit for tests # jailbreak, because hspec limit for tests
fuzzyset = prev.fuzzyset_0_2_4; fuzzyset = lib.doJailbreak
(prev.callHackageDirect
{
pkg = "fuzzyset";
ver = "0.2.4";
sha256 = "sha256-lpkrTFcR0B4rT/P6x7ui31Twgq7BBj6KIvjKyqXKdpc=";
}
{ });
hasql-pool = lib.dontCheck (prev.callHackageDirect hasql-pool = lib.dontCheck (prev.callHackageDirect
{ {
@@ -56,7 +63,13 @@ let
ver = "1.0.1"; ver = "1.0.1";
sha256 = "sha256-Hf1f7lX0LWkjrb25SDBovCYPRdmUP1H6pAxzi7kT4Gg="; sha256 = "sha256-Hf1f7lX0LWkjrb25SDBovCYPRdmUP1H6pAxzi7kT4Gg=";
} }
{ }); { }
);
postgresql-libpq = lib.dontCheck
(prev.postgresql-libpq.override {
postgresql = super.libpq;
});
hasql-notifications = lib.dontCheck (prev.callHackageDirect hasql-notifications = lib.dontCheck (prev.callHackageDirect
{ {
@@ -64,20 +77,9 @@ let
ver = "0.2.2.2"; ver = "0.2.2.2";
sha256 = "sha256-myKwlug7OgTa/qP6mHfCD+5Q8IhM17JvpJBfSo+M01k="; sha256 = "sha256-myKwlug7OgTa/qP6mHfCD+5Q8IhM17JvpJBfSo+M01k=";
} }
{ }); { }
);
# newer nixpkgs already has 0.10., so we fallback to default for forward compat
jose-jwt = prev.jose-jwt_0_10_0 or prev.jose-jwt;
postgresql-libpq = lib.dontCheck (prev.callHackageDirect
{
pkg = "postgresql-libpq";
ver = "0.10.1.0";
sha256 = "sha256-tXOMqCO8opMilI9rx0D+njqjIjbZsH168Bzb8Aq8Ff4=";
}
{
postgresql = super.libpq;
});
}; };
in in
{ {
+28
View File
@@ -0,0 +1,28 @@
final: prev:
let
postgis_3_2_3 = rec {
version = "3.2.3";
src = final.fetchurl {
url = "https://download.osgeo.org/postgis/source/postgis-${version}.tar.gz";
sha256 = "sha256-G02LXHVuWrpZ77wYM7Iu/k1lYneO7KVvpJf+susTZow=";
};
meta.broken = false;
};
in
{
postgresql_11 = prev.postgresql_11.override { this = final.postgresql_11; } // {
pkgs = prev.postgresql_11.pkgs // {
postgis = prev.postgresql_11.pkgs.postgis.overrideAttrs (_: postgis_3_2_3);
};
};
postgresql_10 = prev.postgresql_10.override { this = final.postgresql_11; } // {
pkgs = prev.postgresql_10.pkgs // {
postgis = prev.postgresql_10.pkgs.postgis.overrideAttrs (_: postgis_3_2_3);
};
};
postgresql_9_6 = prev.postgresql_9_6.override { this = final.postgresql_11; } // {
pkgs = prev.postgresql_9_6.pkgs // {
postgis = prev.postgresql_9_6.pkgs.postgis.overrideAttrs (_: postgis_3_2_3);
};
};
}
+19
View File
@@ -0,0 +1,19 @@
_: _:
# Overlay that adds future versions of PostgreSQL that are supported by
# PostgREST.
{
## Example for including a postgresql version from a specific nixpks commit:
##
# postgresql_16 =
# let
# rev = "5148520bfab61f99fd25fb9ff7bfbb50dad3c9db";
# tarballHash = "1dfjmz65h8z4lk845724vypzmf3dbgsdndjpj8ydlhx6c7rpcq3p";
#
# pinnedPkgs =
# builtins.fetchTarball {
# url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
# sha256 = tarballHash;
# };
# in
# (import pinnedPkgs { }).pkgs.postgresql_16;
}
+49
View File
@@ -0,0 +1,49 @@
_: _:
# Overlay that adds legacy versions of PostgreSQL that are supported by
# PostgREST.
{
# PostgreSQL 9.6 was removed from Nixpkgs with
# https://github.com/NixOS/nixpkgs/commit/757dd008b2f2926fc0f7688fa8189f930ea47521
# We pin its parent commit to get the last version that was available.
postgresql_9_6 =
let
rev = "571cbf3d1db477058303cef8754fb85a14e90eb7";
tarballHash = "0q74wn418i1bn5sssacmw8ykpmqvzr0s93sj6pbs3rf6bf134fkz";
pinnedPkgs =
builtins.fetchTarball {
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
sha256 = tarballHash;
};
in
(import pinnedPkgs { }).pkgs.postgresql_9_6;
# PostgreSQL 10 was removed from Nixpkgs with
# https://github.com/NixOS/nixpkgs/commit/aa1483114bb329fee7e1266100b8d8921ed4723f
# We pin its parent commit to get the last version that was available.
postgresql_10 =
let
rev = "79661ba7e2fb96ebefbb537458a5bbae9dc5bd1a";
tarballHash = "0rn796pfn4sg90ai9fdnwmr10a2s835p1arazzgz46h6s5cxvq97";
pinnedPkgs =
builtins.fetchTarball {
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
sha256 = tarballHash;
};
in
(import pinnedPkgs { }).pkgs.postgresql_10;
# PostgreSQL 11 was removed from Nixpkgs with
# https://github.com/NixOS/nixpkgs/commit/1220a4d4dd1a4590780a5e1c18d1333a121be366
# We pin its parent commit to get the last version that was available.
postgresql_11 =
let
rev = "f5458516e42cc5cb4123cc2d93f45c240548aa18";
tarballHash = "1h03621sxfhw4z6ya74k6c2lyx3z7pvf2jcg4vs7i01yz2m6w3cv";
pinnedPkgs =
builtins.fetchTarball {
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
sha256 = tarballHash;
};
in
(import pinnedPkgs { }).pkgs.postgresql_11;
}
+2 -3
View File
@@ -1,7 +1,6 @@
_: super: _: super:
{ {
# Depending on which nixpkgs version is pinned, libpq might either be available already - or not. libpq = super.callPackage ../libpq.nix {
libpq = super.libpq or (super.callPackage ../libpq.nix {
postgresql = super.postgresql_16; postgresql = super.postgresql_16;
}); };
} }
+7 -10
View File
@@ -4,8 +4,12 @@
, src , src
}: }:
let let
# This builds a static PostgREST executable based on pkgsStatic. # This builds a static PostgREST exectuable based on pkgsStatic.
inherit (pkgs) pkgsStatic; # pkgsStatic is based on musl, so is a kind of cross-compilation.
# We still make this explicit here via pkgsCross, because we need
# to get postgresql/libpq for musl, too.
pkgsCross = pkgs.pkgsCross.musl64;
inherit (pkgsCross) pkgsStatic;
inherit (pkgsStatic.haskell) lib; inherit (pkgsStatic.haskell) lib;
packagesStatic = packagesStatic =
@@ -21,16 +25,9 @@ let
overrides = pkgs.lib.composeExtensions old.overrides (_: prev: { overrides = pkgs.lib.composeExtensions old.overrides (_: prev: {
postgresql-libpq = (lib.overrideCabal prev.postgresql-libpq { postgresql-libpq = (lib.overrideCabal prev.postgresql-libpq {
# TODO: This section can be simplified when this PR has made it's way to us:
# https://github.com/NixOS/nixpkgs/pull/286370
# Additionally, we need to use the default version in nixpkgs, otherwise the
# override will not be active as well.
# Using use-pkg-config flag, because pg_config won't work when cross-compiling
configureFlags = [ "-fuse-pkg-config" ];
# postgresql doesn't build in the fully static overlay - but the default # postgresql doesn't build in the fully static overlay - but the default
# derivation is built with static libraries anyway. # derivation is built with static libraries anyway.
libraryPkgconfigDepends = [ pkgsStatic.libpq ]; libraryPkgconfigDepends = [ pkgsCross.libpq ];
librarySystemDepends = [ ];
}).overrideAttrs (_: prevAttrs: { }).overrideAttrs (_: prevAttrs: {
buildInputs = prevAttrs.buildInputs ++ [ pkgsStatic.openssl ]; buildInputs = prevAttrs.buildInputs ++ [ pkgsStatic.openssl ];
}); });
-31
View File
@@ -15,7 +15,6 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
exec ${cabal-install}/bin/cabal v2-build ${devCabalOptions} "''${_arg_leftovers[@]}" exec ${cabal-install}/bin/cabal v2-build ${devCabalOptions} "''${_arg_leftovers[@]}"
''; '';
@@ -60,35 +59,6 @@ let
postgrest "''${_arg_leftovers[@]}" postgrest "''${_arg_leftovers[@]}"
''; '';
runProfiled =
checkedShellScript
{
name = "postgrest-profiled-run";
docs = "Run a profiled build of postgREST. This will generate a postgrest.prof file that can be used to do optimization.";
args =
[
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [PostgREST anonymous role])"
"ARG_USE_ENV([PGRST_DB_POOL], [1], [PostgREST pool size])"
"ARG_USE_ENV([PGRST_DB_POOL_ACQUISITION_TIMEOUT], [1], [PostgREST pool timeout])"
"ARG_USE_ENV([PGRST_JWT_SECRET], [reallyreallyreallyreallyverysafe], [PostgREST JWT secret])"
"ARG_LEFTOVERS([PostgREST arguments])"
];
workingDir = "/";
withEnv = postgrest.env;
}
''
export PGRST_DB_ANON_ROLE
export PGRST_DB_POOL
export PGRST_DB_POOL_ACQUISITION_TIMEOUT
export PGRST_JWT_SECRET
${cabal-install}/bin/cabal v2-update
${cabal-install}/bin/cabal --builddir="dist-prof" v2-build --enable-profiling --disable-shared exe:postgrest
${cabal-install}/bin/cabal --builddir="dist-prof" v2-run -- \
postgrest +RTS -p -h -RTS "''${_arg_leftovers[@]}"
'';
repl = repl =
checkedShellScript checkedShellScript
{ {
@@ -110,7 +80,6 @@ buildToolbox
build build
clean clean
run run
runProfiled
repl; repl;
}; };
} }
+3 -13
View File
@@ -246,7 +246,6 @@ let
} }
'' ''
mkdir -p "$_arg_dumpdir" mkdir -p "$_arg_dumpdir"
${cabal-install}/bin/cabal v2-update
${cabal-install}/bin/cabal v2-build ${devCabalOptions} \ ${cabal-install}/bin/cabal v2-build ${devCabalOptions} \
--builddir="$tmpdir" \ --builddir="$tmpdir" \
--ghc-option=-ddump-minimal-imports \ --ghc-option=-ddump-minimal-imports \
@@ -334,29 +333,20 @@ let
checkedShellScript checkedShellScript
{ {
name = "postgrest-gen-jwt"; name = "postgrest-gen-jwt";
docs = '' docs = "Generate a JWT";
Generate a JWT. Example: postgrest-gen-jwt --exp 10 postgrest_test_author
# This can be used to quickly prove a JWT expiry
$ curl localhost:3000/authors_only -H "Authorization: Bearer \$(postgrest-gen-jwt --exp -31 postgrest_test_author)"
'';
args = [ args = [
"ARG_POSITIONAL_SINGLE([role], [role for the jwt payload])" "ARG_POSITIONAL_SINGLE([role], [role for the jwt payload])"
"ARG_OPTIONAL_SINGLE([secret],, [secret used to sign the JWT], [reallyreallyreallyreallyverysafe])" "ARG_OPTIONAL_SINGLE([secret],, [secret used to sign the JWT], [reallyreallyreallyreallyverysafe])"
"ARG_OPTIONAL_SINGLE([exp],, [seconds for JWT expiry, it accepts negative values], [3600])"
]; ];
} }
'' ''
# Based on https://stackoverflow.com/questions/59002949/how-to-create-a-json-web-token-jwt-using-openssl-shell-commands # From https://stackoverflow.com/questions/59002949/how-to-create-a-json-web-token-jwt-using-openssl-shell-commands
# Construct the header # Construct the header
jwt_header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | base64 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//) jwt_header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | base64 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//)
# Construct the exp value
expiry=$((EPOCHSECONDS + _arg_exp))
# Construct the payload # Construct the payload
payload=$(echo -n "{\"role\": \"$_arg_role\", \"exp\": $expiry}" | base64 | sed s/\+/-/g |sed 's/\//_/g' | sed -E s/=+$//) payload=$(echo -n "{\"role\":\"$_arg_role\"}" | base64 | sed s/\+/-/g |sed 's/\//_/g' | sed -E s/=+$//)
# Convert secret to hex # Convert secret to hex
hexsecret=$(echo -n "$_arg_secret" | xxd -p | paste -sd "") hexsecret=$(echo -n "$_arg_secret" | xxd -p | paste -sd "")
+13 -31
View File
@@ -2,40 +2,23 @@
, aspellDicts , aspellDicts
, buildToolbox , buildToolbox
, checkedShellScript , checkedShellScript
, fetchPypi
, lib
, plantuml
, python3 , python3
, python3Packages , python3Packages
, writeTextFile
, writers , writers
, plantuml
}: }:
let let
selectPythonPackages = ps: selectPythonPackages = ps: [
let ps.sphinx
# TODO: Remove with next nixpkgs update ps.sphinx_rtd_theme
sphinx-rtd-theme = assert ps.sphinx-rtd-theme.version == "2.0.0"; ps.sphinx-rtd-theme.overrideAttrs rec { ps.livereload
version = "3.0.2"; ps.sphinx-tabs
src = fetchPypi { ps.sphinx-copybutton
pname = "sphinx_rtd_theme"; ps.sphinxext-opengraph
inherit version; (ps.callPackage ../docs-extensions/sphinx-rtd-dark-mode.nix { })
hash = "sha256-t0V7wl3acjsgsIamcLmVPIWeq2CioD7o6yuyPhduX4U="; # TODO: Remove override once new sphinx-intl version (> 2.1.0) is released and available in nixpkgs
}; (ps.sphinx-intl.overrideAttrs (drv: { nativeBuildInputs = drv.nativeBuildInputs ++ [ ps.six ]; }))
}; ];
in
[
ps.sphinx
ps.sphinx-copybutton
(ps.sphinx-rtd-dark-mode.override { inherit sphinx-rtd-theme; })
sphinx-rtd-theme
ps.sphinx-tabs
ps.sphinxext-opengraph
];
requirements = writeTextFile {
name = "requirements.txt";
text = lib.concatMapStringsSep "\n" (pkg: "${pkg.pname}==${pkg.version}") (selectPythonPackages python3Packages);
};
python = python3.withPackages selectPythonPackages; python = python3.withPackages selectPythonPackages;
@@ -90,7 +73,7 @@ let
server = server =
writers.writePython3 writers.writePython3
"postgrest-docs-server" "postgrest-docs-server"
{ libraries = selectPythonPackages python3Packages ++ [ python3Packages.livereload ]; } { libraries = selectPythonPackages python3Packages; }
'' ''
import sys import sys
from livereload import Server, shell from livereload import Server, shell
@@ -200,5 +183,4 @@ buildToolbox
serve serve
spellcheck; spellcheck;
}; };
extra = { inherit requirements; };
} }
-94
View File
@@ -1,94 +0,0 @@
# generates a file to be used by the vegeta load testing tool
import time
import hmac
import hashlib
import base64
import json
import argparse
import sys
import random
SECRET = b"reallyreallyreallyreallyverysafe"
URL = "http://postgrest"
JWT_DURATION = 120
TOTAL_TARGETS = 50000 # tuned by hand to reduce result variance
def base64url_encode(data: bytes) -> str:
"""URL-safe Base64 encode without padding."""
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def generate_jwt(exp_inc: int) -> str:
"""Generate an HS256 JWT"""
# Header & payload
header = {"alg": "HS256", "typ": "JWT"}
now = int(time.time())
payload = {
"sub": f"user_{random.getrandbits(32)}",
"iat": now,
"exp": now + exp_inc,
"role": "postgrest_test_author",
}
# Encode to JSON and then to Base64URL
header_b = json.dumps(header, separators=(",", ":")).encode()
payload_b = json.dumps(payload, separators=(",", ":")).encode()
header_b64 = base64url_encode(header_b)
payload_b64 = base64url_encode(payload_b)
# Sign (HMACSHA256) the "<header>.<payload>" string
signing_input = f"{header_b64}.{payload_b64}".encode()
signature = hmac.new(SECRET, signing_input, hashlib.sha256).digest()
signature_b64 = base64url_encode(signature)
return f"{header_b64}.{payload_b64}.{signature_b64}"
# We want to ensure 401 Unauthorized responses don't happen during
# JWT validation, this can happen when the jwt `exp` is too short.
# At the same time, we want to ensure the `exp` is not too big,
# so expires will occur and postgREST will have to clean cached expired JWTs.
def estimate_adequate_jwt_exp_increase(iteration: int) -> int:
# estimated time takes to build and run postgrest itself
build_run_postgrest_time = 2
# estimated time it takes to generate the targets file
file_generation_time = TOTAL_TARGETS // (10**-5)
# estimated exp time so some JWTs will expire
dynamic_exp_inc = iteration // 1000
return build_run_postgrest_time + file_generation_time + dynamic_exp_inc
def main():
parser = argparse.ArgumentParser(
description="Generate Vegeta targets with unique JWTs"
)
parser.add_argument(
"output",
help="Path to write the generated targets file",
)
args = parser.parse_args()
lines = []
start_time = time.time()
for i in range(TOTAL_TARGETS):
token = generate_jwt(estimate_adequate_jwt_exp_increase(i))
lines.append(f"OPTIONS {URL}/authors_only")
lines.append(f"Authorization: Bearer {token}")
lines.append("") # blank line to separate requests
try:
with open(args.output, "w") as f:
f.write("\n".join(lines))
except IOError as e:
print(f"Error writing to {args.output}: {e}", file=sys.stderr)
sys.exit(1)
elapsed = time.time() - start_time
print(f"Created {TOTAL_TARGETS} targets in {args.output} ({elapsed:.2f}s)")
if __name__ == "__main__":
main()
+9 -32
View File
@@ -41,8 +41,6 @@ let
args = [ args = [
"ARG_OPTIONAL_SINGLE([output], [o], [Filename to dump json output to], [./loadtest/result.bin])" "ARG_OPTIONAL_SINGLE([output], [o], [Filename to dump json output to], [./loadtest/result.bin])"
"ARG_OPTIONAL_SINGLE([testdir], [t], [Directory to load tests and fixtures from], [./test/load])" "ARG_OPTIONAL_SINGLE([testdir], [t], [Directory to load tests and fixtures from], [./test/load])"
"ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest (mixed: repeat mixed requests, jwt: run once over many requests with unique jwts)], [mixed])"
"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,jwt])"
"ARG_LEFTOVERS([additional vegeta arguments])" "ARG_LEFTOVERS([additional vegeta arguments])"
]; ];
workingDir = "/"; workingDir = "/";
@@ -57,36 +55,17 @@ let
export PGRST_DB_POOL="1" export PGRST_DB_POOL="1"
export PGRST_DB_TX_END="rollback-allow-override" export PGRST_DB_TX_END="rollback-allow-override"
export PGRST_LOG_LEVEL="crit" export PGRST_LOG_LEVEL="crit"
export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe"
export PGRST_JWT_CACHE_MAX_LIFETIME="86400"
mkdir -p "$(dirname "$_arg_output")" mkdir -p "$(dirname "$_arg_output")"
abs_output="$(realpath "$_arg_output")" abs_output="$(realpath "$_arg_output")"
case "$_arg_kind" in # shellcheck disable=SC2145
jwt) ${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withSlowPg} \
${genTargets} "$_arg_testdir"/gen_targets.http ${withTools.withPgrst} \
${withTools.withSlowPgrst} \
# shellcheck disable=SC2145 sh -c "cd \"$_arg_testdir\" && ${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \ ${vegeta}/bin/vegeta report -type=text "$_arg_output"
${withTools.withPgrst} \
sh -c "cd \"$_arg_testdir\" && ${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
${vegeta}/bin/vegeta report -type=text "$_arg_output"
;;
*)
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withSlowPg} \
${withTools.withPgrst} \
${withTools.withSlowPgrst} \
sh -c "cd \"$_arg_testdir\" && ${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
${vegeta}/bin/vegeta report -type=text "$_arg_output"
;;
esac
''; '';
loadtestAgainst = loadtestAgainst =
@@ -104,7 +83,6 @@ let
''; '';
args = [ args = [
"ARG_POSITIONAL_INF([target], [Commit-ish reference to compare with], 1)" "ARG_POSITIONAL_INF([target], [Commit-ish reference to compare with], 1)"
"ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest], [mixed])"
]; ];
positionalCompletion = positionalCompletion =
'' ''
@@ -128,7 +106,7 @@ let
# Save the results in the current working tree, too, # Save the results in the current working tree, too,
# otherwise they'd be lost in the temporary working tree # otherwise they'd be lost in the temporary working tree
# created by withTools.withGit. # created by withTools.withGit.
${withTools.withGit} "$tgt" ${loadtest} -k "$_arg_kind" --output "$PWD/loadtest/$tgt.bin" --testdir "$PWD/test/load" ${withTools.withGit} "$tgt" ${loadtest} --output "$PWD/loadtest/$tgt.bin" --testdir "$PWD/test/load"
cat << EOF cat << EOF
@@ -144,7 +122,7 @@ let
EOF EOF
${loadtest} -k "$_arg_kind" --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load" ${loadtest} --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load"
cat << EOF cat << EOF
@@ -200,7 +178,6 @@ let
| ${toMarkdown} | ${toMarkdown}
''; '';
genTargets = writers.writePython3 "postgrest-gen-loadtest-targets" { } (builtins.readFile ./generate_targets.py);
in in
buildToolbox { buildToolbox {
name = "postgrest-loadtest"; name = "postgrest-loadtest";
+51
View File
@@ -0,0 +1,51 @@
# The memory tests have large dependencies (a profiled build of PostgREST)
# and are run less often than the spec tests, so we don't include them in
# the default test environment. We make them available through a separate module.
# TODO both of these require reentering the nix-shell if you make a change to the code
{ buildToolbox
, checkedShellScript
, curl
, postgrestProfiled
, withTools
}:
let
test =
checkedShellScript
{
name = "postgrest-test-memory";
docs = "Run the memory tests.";
workingDir = "/";
withPath = [ postgrestProfiled curl ];
}
''
${withTools.withPg} -f test/spec/fixtures/load.sql test/memory/memory-tests.sh
'';
runProfiled =
checkedShellScript
{
name = "postgrest-profiled-run";
docs = "Run a profiled build of postgREST. This will generate a postgrest.prof file that can be used to do optimization. Note: if you make a change to the code, you must reenter the nix-shell for an updated profiled build.";
args =
[
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [PostgREST anonymous role])"
"ARG_USE_ENV([PGRST_DB_POOL], [1], [PostgREST pool size])"
"ARG_USE_ENV([PGRST_DB_POOL_ACQUISITION_TIMEOUT], [1], [PostgREST pool timeout])"
"ARG_LEFTOVERS([PostgREST arguments])"
];
workingDir = "/";
withPath = [ postgrestProfiled ];
}
''
export PGRST_DB_ANON_ROLE
export PGRST_DB_POOL
export PGRST_DB_POOL_ACQUISITION_TIMEOUT
postgrest +RTS -p -h -RTS "''${_arg_leftovers[@]}"
'';
in
buildToolbox
{
name = "postgrest-memory";
tools = { inherit test runProfiled; };
}
+57
View File
@@ -0,0 +1,57 @@
{ buildToolbox
, checkedShellScript
, coreutils
, curl
, jq
, nix
}:
# Utility script for pinning the latest stable version of Nixpkgs.
# Instead of pinning Nixpkgs based on the huge Git repository, we reference a
# specific tarball that only contains the source of the revision that we want
# to pin.
let
name =
"postgrest-nixpkgs-upgrade";
refUrl =
"https://api.github.com/repos/nixos/nixpkgs/git/matching-refs/heads/nixpkgs-";
githubV3Header =
"Accept: application/vnd.github.v3+json";
tarballUrlBase =
"https://github.com/nixos/nixpkgs/archive/";
upgrade =
checkedShellScript
{
inherit name;
docs = "Pin the newest stable version of Nixpkgs.";
workingDir = "/";
}
''
# The list of refs is sorted. The first result will be nixpkgs-unstable, the second the latest stable branch.
commitHash="$(${curl}/bin/curl "${refUrl}" -H "${githubV3Header}" | ${jq}/bin/jq -r 'sort_by(.ref) | reverse | .[1].object.sha')"
tarballUrl="${tarballUrlBase}$commitHash.tar.gz"
tarballHash="$(${nix}/bin/nix-prefetch-url --unpack "$tarballUrl")"
currentDate="$(${coreutils}/bin/date --iso)"
cat > nix/nixpkgs-version.nix << EOF
# Pinned version of Nixpkgs, generated with ${name}.
{
owner = "NixOS";
repo = "nixpkgs";
date = "$currentDate";
rev = "$commitHash";
tarballHash = "$tarballHash";
}
EOF
'';
in
buildToolbox
{
name = "postgrest-nixpkgs";
tools = { inherit upgrade; };
}
@@ -29,28 +29,22 @@ let
case "$1" in case "$1" in
major) major)
new_version="$((major+1)).0.0" new_version="$((major+1)).0.0"
new_docs_version="$((major+1)).0"
;; ;;
minor) minor)
new_version="$major.$((minor+1)).0" new_version="$major.$((minor+1)).0"
new_docs_version="$major.$((minor+1))"
;; ;;
patch) patch)
new_version="$major.$minor.$((patch+1))" new_version="$major.$minor.$((patch+1))"
new_docs_version="$major.$minor"
;; ;;
devel) devel)
new_version="$major.$((minor+1))" new_version="$major.$((minor+1))"
new_docs_version="devel"
;; ;;
esac esac
echo "Updating postgrest.cabal ..." echo "Updating postgrest.cabal ..."
sed -i -E "s/^(version:\s+).*$/\1$new_version/" postgrest.cabal > /dev/null sed -i -E "s/^(version:\s+).*$/\1$new_version/" postgrest.cabal > /dev/null
echo "Updating docs/conf.py ..."
sed -i -E "s/^(version = ).*$/\1\"$new_docs_version\"/" docs/conf.py > /dev/null
git add postgrest.cabal docs/conf.py > /dev/null git add postgrest.cabal > /dev/null
} }
today_date_for_changelog="$(date '+%Y-%m-%d')" today_date_for_changelog="$(date '+%Y-%m-%d')"
+4 -30
View File
@@ -1,7 +1,6 @@
{ buildToolbox { buildToolbox
, cabal-install , cabal-install
, checkedShellScript , checkedShellScript
, curl
, devCabalOptions , devCabalOptions
, ghc , ghc
, glibcLocales ? null , glibcLocales ? null
@@ -28,7 +27,6 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
${withTools.withPg} -f test/spec/fixtures/load.sql \ ${withTools.withPg} -f test/spec/fixtures/load.sql \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec -- "''${_arg_leftovers[@]}" ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec -- "''${_arg_leftovers[@]}"
''; '';
@@ -42,7 +40,6 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
# This makes nix-env -iA tests.doctests.bin work. # This makes nix-env -iA tests.doctests.bin work.
export NIX_GHC=${postgrest.env.NIX_GHC} export NIX_GHC=${postgrest.env.NIX_GHC}
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:doctests ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:doctests
@@ -57,7 +54,6 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
${withTools.withPg} -f test/spec/fixtures/load.sql \ ${withTools.withPg} -f test/spec/fixtures/load.sql \
${runtimeShell} -c " \ ${runtimeShell} -c " \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec && \ ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec && \
@@ -85,8 +81,7 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update ${cabal-install}/bin/cabal v2-build ${devCabalOptions}
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/fixtures.sql \ ${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/fixtures.sql \
${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io "''${_arg_leftovers[@]}" ${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io "''${_arg_leftovers[@]}"
''; '';
@@ -101,8 +96,7 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update ${cabal-install}/bin/cabal v2-build ${devCabalOptions}
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/big_schema.sql \ ${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/big_schema.sql \
${ioTestPython}/bin/pytest -v test/io/test_big_schema.py "''${_arg_leftovers[@]}" ${ioTestPython}/bin/pytest -v test/io/test_big_schema.py "''${_arg_leftovers[@]}"
''; '';
@@ -117,8 +111,7 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update ${cabal-install}/bin/cabal v2-build ${devCabalOptions}
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} --replica -f test/io/replica.sql \ ${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} --replica -f test/io/replica.sql \
${ioTestPython}/bin/pytest -v test/io/test_replica.py "''${_arg_leftovers[@]}" ${ioTestPython}/bin/pytest -v test/io/test_replica.py "''${_arg_leftovers[@]}"
''; '';
@@ -133,7 +126,6 @@ let
withPath = [ jq ]; withPath = [ jq ];
} }
'' ''
${cabal-install}/bin/cabal v2-update
${withTools.withPg} -f test/spec/fixtures/load.sql \ ${withTools.withPg} -f test/spec/fixtures/load.sql \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \ ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
postgrest --dump-schema postgrest --dump-schema
@@ -162,7 +154,6 @@ let
rm -rf coverage/* rm -rf coverage/*
# build once before running all the tests # build once before running all the tests
${cabal-install}/bin/cabal v2-update
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec ${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec
( (
@@ -236,22 +227,6 @@ let
sed -i 's|^module \(.*\):|module \1/|g' test/coverage.overlay sed -i 's|^module \(.*\):|module \1/|g' test/coverage.overlay
''; '';
testMemory =
checkedShellScript
{
name = "postgrest-test-memory";
docs = "Run the memory tests.";
workingDir = "/";
withEnv = postgrest.env;
withPath = [ curl ];
}
''
${cabal-install}/bin/cabal v2-update
${cabal-install}/bin/cabal --builddir="dist-prof" v2-build --enable-profiling --disable-shared exe:postgrest
${cabal-install}/bin/cabal --builddir="dist-prof" v2-exec -- ${withTools.withPg} -f test/spec/fixtures/load.sql \
test/memory/memory-tests.sh
'';
in in
buildToolbox buildToolbox
{ {
@@ -266,7 +241,6 @@ buildToolbox
testReplica testReplica
dumpSchema dumpSchema
coverage coverage
coverageDraftOverlay coverageDraftOverlay;
testMemory;
}; };
} }
+23 -23
View File
@@ -1,5 +1,5 @@
name: postgrest name: postgrest
version: 13.0.7 version: 12.2.12
synopsis: REST API for any Postgres database synopsis: REST API for any Postgres database
description: Reads the schema of a PostgreSQL database and creates RESTful routes description: Reads the schema of a PostgreSQL database and creates RESTful routes
for tables, views, and functions, supporting all HTTP methods that security for tables, views, and functions, supporting all HTTP methods that security
@@ -16,13 +16,14 @@ extra-source-files: CHANGELOG.md
cabal-version: >= 1.10 cabal-version: >= 1.10
tested-with: tested-with:
-- nix
GHC == 9.4.8
-- cabal on Ubuntu
-- stack on FreeBSD, MacOS, Ubuntu, Windows -- stack on FreeBSD, MacOS, Ubuntu, Windows
, GHC == 9.6.7 GHC == 9.4.5
-- nix, cabal on Ubuntu (arm)
, GHC == 9.4.8
-- cabal on Ubuntu -- cabal on Ubuntu
, GHC == 9.8.4 , GHC == 9.6.4
-- cabal on Ubuntu
, GHC == 9.8.2
source-repository head source-repository head
type: git type: git
@@ -47,8 +48,6 @@ library
PostgREST.App PostgREST.App
PostgREST.AppState PostgREST.AppState
PostgREST.Auth PostgREST.Auth
PostgREST.Auth.JwtCache
PostgREST.Auth.Types
PostgREST.CLI PostgREST.CLI
PostgREST.Config PostgREST.Config
PostgREST.Config.Database PostgREST.Config.Database
@@ -67,7 +66,6 @@ library
PostgREST.Logger PostgREST.Logger
PostgREST.MediaType PostgREST.MediaType
PostgREST.Metrics PostgREST.Metrics
PostgREST.Network
PostgREST.Observation PostgREST.Observation
PostgREST.Query PostgREST.Query
PostgREST.Query.QueryBuilder PostgREST.Query.QueryBuilder
@@ -89,11 +87,12 @@ library
PostgREST.Response.GucHeader PostgREST.Response.GucHeader
PostgREST.Response.Performance PostgREST.Response.Performance
PostgREST.Version PostgREST.Version
other-modules: Paths_postgrest
build-depends: base >= 4.9 && < 4.20 build-depends: base >= 4.9 && < 4.20
, HTTP >= 4000.3.7 && < 4000.5 , HTTP >= 4000.3.7 && < 4000.5
, Ranged-sets >= 0.3 && < 0.5 , Ranged-sets >= 0.3 && < 0.5
, aeson >= 2.0.3 && < 2.3 , aeson >= 2.0.3 && < 2.3
, auto-update >= 0.1.4 && < 0.3 , auto-update >= 0.1.4 && < 0.2
, base64-bytestring >= 1 && < 1.3 , base64-bytestring >= 1 && < 1.3
, bytestring >= 0.10.8 && < 0.13 , bytestring >= 0.10.8 && < 0.13
, cache >= 0.1.3 && < 0.2.0 , cache >= 0.1.3 && < 0.2.0
@@ -102,30 +101,30 @@ library
, clock >= 0.8.3 && < 0.9.0 , clock >= 0.8.3 && < 0.9.0
, configurator-pg >= 0.2 && < 0.3 , configurator-pg >= 0.2 && < 0.3
, containers >= 0.5.7 && < 0.7 , containers >= 0.5.7 && < 0.7
, cookie >= 0.4.2 && < 0.6 , contravariant-extras >= 0.3.3 && < 0.4
, cookie >= 0.4.2 && < 0.5
, directory >= 1.2.6 && < 1.4 , directory >= 1.2.6 && < 1.4
, either >= 4.4.1 && < 5.1 , either >= 4.4.1 && < 5.1
, extra >= 1.7.0 && < 2.0 , extra >= 1.7.0 && < 2.0
, fuzzyset >= 0.2.4 && < 0.3 , fuzzyset >= 0.2.4 && < 0.3
, gitrev >= 1.2 && < 1.4
, hasql >= 1.6.1.1 && < 1.7 , hasql >= 1.6.1.1 && < 1.7
, hasql-dynamic-statements >= 0.3.1 && < 0.4 , hasql-dynamic-statements >= 0.3.1 && < 0.4
, hasql-notifications >= 0.2.2.2 && < 0.2.3 , hasql-notifications >= 0.2.2.2 && < 0.2.3
, hasql-pool >= 1.0.1 && < 1.1 , hasql-pool >= 1.0.1 && < 1.1
, hasql-transaction >= 1.0.1 && < 1.2 , hasql-transaction >= 1.0.1 && < 1.1
, heredoc >= 0.2 && < 0.3 , heredoc >= 0.2 && < 0.3
, http-types >= 0.12.2 && < 0.13 , http-types >= 0.12.2 && < 0.13
, insert-ordered-containers >= 0.2.2 && < 0.3 , insert-ordered-containers >= 0.2.2 && < 0.3
, iproute >= 1.7.0 && < 1.8 , interpolatedstring-perl6 >= 1 && < 1.1
, jose-jwt >= 0.9.6 && < 0.11 , jose >= 0.8.5.1 && < 0.12
, lens >= 4.14 && < 5.4 , lens >= 4.14 && < 5.3
, lens-aeson >= 1.0.1 && < 1.3 , lens-aeson >= 1.0.1 && < 1.3
, mtl >= 2.2.2 && < 2.4 , mtl >= 2.2.2 && < 2.4
, neat-interpolation >= 0.5 && < 0.6 , network >= 2.6 && < 3.2
, network >= 2.6 && < 3.3
, network-uri >= 2.6.1 && < 2.8 , network-uri >= 2.6.1 && < 2.8
, optparse-applicative >= 0.13 && < 0.19 , optparse-applicative >= 0.13 && < 0.19
, parsec >= 3.1.11 && < 3.2 , parsec >= 3.1.11 && < 3.2
-- Technically unused, can be removed after updating to hasql >= 1.7
, postgresql-libpq >= 0.10 , postgresql-libpq >= 0.10
, prometheus-client >= 1.1.1 && < 1.2.0 , prometheus-client >= 1.1.1 && < 1.2.0
, protolude >= 0.3.1 && < 0.4 , protolude >= 0.3.1 && < 0.4
@@ -149,7 +148,7 @@ library
-- for unix sockets; this is tested in test/io/test_io.py. See -- for unix sockets; this is tested in test/io/test_io.py. See
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3 -- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
, wai-logger >= 2.4.0 , wai-logger >= 2.4.0
, warp >= 3.3.19 && < 3.5 , warp >= 3.3.19 && < 3.4
-- -fno-spec-constr may help keep compile time memory use in check, -- -fno-spec-constr may help keep compile time memory use in check,
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304 -- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
-- -optP-Wno-nonportable-include-path -- -optP-Wno-nonportable-include-path
@@ -205,7 +204,7 @@ test-suite spec
Feature.Auth.AuthSpec Feature.Auth.AuthSpec
Feature.Auth.BinaryJwtSecretSpec Feature.Auth.BinaryJwtSecretSpec
Feature.Auth.NoAnonSpec Feature.Auth.NoAnonSpec
Feature.Auth.NoJwtSecretSpec Feature.Auth.NoJwtSpec
Feature.ConcurrentSpec Feature.ConcurrentSpec
Feature.CorsSpec Feature.CorsSpec
Feature.ExtraSearchPathSpec Feature.ExtraSearchPathSpec
@@ -228,6 +227,7 @@ test-suite spec
Feature.Query.ErrorSpec Feature.Query.ErrorSpec
Feature.Query.InsertSpec Feature.Query.InsertSpec
Feature.Query.JsonOperatorSpec Feature.Query.JsonOperatorSpec
Feature.Query.LimitedMutationSpec
Feature.Query.MultipleSchemaSpec Feature.Query.MultipleSchemaSpec
Feature.Query.NullsStripSpec Feature.Query.NullsStripSpec
Feature.Query.PgSafeUpdateSpec Feature.Query.PgSafeUpdateSpec
@@ -253,19 +253,19 @@ test-suite spec
, aeson >= 2.0.3 && < 2.3 , aeson >= 2.0.3 && < 2.3
, aeson-qq >= 0.8.1 && < 0.9 , aeson-qq >= 0.8.1 && < 0.9
, async >= 2.1.1 && < 2.3 , async >= 2.1.1 && < 2.3
, auto-update >= 0.1.4 && < 0.2
, base64-bytestring >= 1 && < 1.3 , base64-bytestring >= 1 && < 1.3
, bytestring >= 0.10.8 && < 0.13 , bytestring >= 0.10.8 && < 0.13
, case-insensitive >= 1.2 && < 1.3 , case-insensitive >= 1.2 && < 1.3
, containers >= 0.5.7 && < 0.7 , containers >= 0.5.7 && < 0.7
, hasql-pool >= 1.0.1 && < 1.1 , hasql-pool >= 1.0.1 && < 1.1
, hasql-transaction >= 1.0.1 && < 1.2 , hasql-transaction >= 1.0.1 && < 1.1
, heredoc >= 0.2 && < 0.3 , heredoc >= 0.2 && < 0.3
, hspec >= 2.3 && < 2.12 , hspec >= 2.3 && < 2.12
, hspec-wai >= 0.10 && < 0.12 , hspec-wai >= 0.10 && < 0.12
, hspec-wai-json >= 0.10 && < 0.12 , hspec-wai-json >= 0.10 && < 0.12
, http-types >= 0.12.3 && < 0.13 , http-types >= 0.12.3 && < 0.13
, jose-jwt >= 0.9.6 && < 0.11 , lens >= 4.14 && < 5.3
, lens >= 4.14 && < 5.4
, lens-aeson >= 1.0.1 && < 1.3 , lens-aeson >= 1.0.1 && < 1.3
, monad-control >= 1.0.1 && < 1.1 , monad-control >= 1.0.1 && < 1.1
, postgrest , postgrest
+5 -2
View File
@@ -7,6 +7,7 @@
# We highly recommend that use the PostgREST binary cache by installing cachix # We highly recommend that use the PostgREST binary cache by installing cachix
# (https://app.cachix.org/) and running `cachix use postgrest`. # (https://app.cachix.org/) and running `cachix use postgrest`.
{ docker ? false { docker ? false
, memory ? false
}: }:
let let
postgrest = postgrest =
@@ -22,12 +23,14 @@ let
postgrest.devTools postgrest.devTools
postgrest.docs postgrest.docs
postgrest.loadtest postgrest.loadtest
postgrest.release postgrest.nixpkgsTools
postgrest.style postgrest.style
postgrest.tests postgrest.tests
postgrest.withTools postgrest.withTools
postgrest.release
] ]
++ lib.optional docker postgrest.docker; ++ lib.optional docker postgrest.docker
++ lib.optional memory postgrest.memory;
in in
lib.overrideDerivation postgrest.env ( lib.overrideDerivation postgrest.env (
+12 -5
View File
@@ -11,17 +11,19 @@ import qualified Network.Wai.Handler.Warp as Warp
import Control.Monad.Extra (whenJust) import Control.Monad.Extra (whenJust)
import qualified Data.ByteString.Lazy as LBS
import Network.Socket import Network.Socket
import Network.Socket.ByteString import Network.Socket.ByteString
import PostgREST.AppState (AppState) import PostgREST.AppState (AppState)
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.MediaType (MediaType (..), toContentType)
import PostgREST.Metrics (metricsToText) import PostgREST.Metrics (metricsToText)
import PostgREST.Network (resolveHost)
import PostgREST.Observation (Observation (..)) import PostgREST.Observation (Observation (..))
import qualified PostgREST.AppState as AppState import qualified PostgREST.AppState as AppState
import qualified PostgREST.Config as Config
import Protolude import Protolude
@@ -29,8 +31,7 @@ runAdmin :: AppState -> Warp.Settings -> IO ()
runAdmin appState settings = do runAdmin appState settings = do
AppConfig{configAdminServerPort} <- AppState.getConfig appState AppConfig{configAdminServerPort} <- AppState.getConfig appState
whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do
host <- resolveHost adminSocket observer $ AdminStartObs configAdminServerPort
observer $ AdminStartObs host configAdminServerPort
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
where where
adminApp = admin appState adminApp = admin appState
@@ -54,12 +55,18 @@ admin appState req respond = do
| otherwise = HTTP.status500 | otherwise = HTTP.status500
in in
respond $ Wai.responseLBS status [] mempty respond $ Wai.responseLBS status [] mempty
["config"] -> do
config@Config.AppConfig{configAdminServerConfigEnabled} <- AppState.getConfig appState
if configAdminServerConfigEnabled then
respond $ Wai.responseLBS HTTP.status200 [] (LBS.fromStrict $ encodeUtf8 $ Config.toText config)
else
respond $ Wai.responseLBS HTTP.status404 [] mempty
["schema_cache"] -> do ["schema_cache"] -> do
sCache <- AppState.getSchemaCache appState sCache <- AppState.getSchemaCache appState
respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache) respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache)
["metrics"] -> do ["metrics"] -> do
mets <- metricsToText mets <- metricsToText
respond $ Wai.responseLBS HTTP.status200 [toContentType MTTextPlain] mets -- Content-Type is required for prometheus compliance respond $ Wai.responseLBS HTTP.status200 [] mets
_ -> _ ->
respond $ Wai.responseLBS HTTP.status404 [] mempty respond $ Wai.responseLBS HTTP.status404 [] mempty
+15 -20
View File
@@ -4,6 +4,7 @@ Description : PostgREST functions to translate HTTP request to a domain type cal
-} -}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
-- TODO: This module shouldn't depend on SchemaCache
module PostgREST.ApiRequest module PostgREST.ApiRequest
( ApiRequest(..) ( ApiRequest(..)
, InvokeMethod(..) , InvokeMethod(..)
@@ -13,7 +14,6 @@ module PostgREST.ApiRequest
, DbAction(..) , DbAction(..)
, Payload(..) , Payload(..)
, userApiRequest , userApiRequest
, userPreferences
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
@@ -44,16 +44,16 @@ import Network.Wai.Parse (parseHttpAccept)
import Web.Cookie (parseCookies) import Web.Cookie (parseCookies)
import PostgREST.ApiRequest.QueryParams (QueryParams (..)) import PostgREST.ApiRequest.QueryParams (QueryParams (..))
import PostgREST.ApiRequest.Types (ApiRequestError (..),
RangeError (..))
import PostgREST.Config (AppConfig (..), import PostgREST.Config (AppConfig (..),
OpenAPIMode (..)) OpenAPIMode (..))
import PostgREST.Config.Database (TimezoneNames)
import PostgREST.Error (ApiRequestError (..),
RangeError (..))
import PostgREST.MediaType (MediaType (..)) import PostgREST.MediaType (MediaType (..))
import PostgREST.RangeQuery (NonnegRange, allRange, import PostgREST.RangeQuery (NonnegRange, allRange,
convertToLimitZeroRange, convertToLimitZeroRange,
hasLimitZero, hasLimitZero,
rangeRequested) rangeRequested)
import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.SchemaCache.Identifiers (FieldName, import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier (..), QualifiedIdentifier (..),
Schema) Schema)
@@ -128,8 +128,8 @@ data ApiRequest = ApiRequest {
} }
-- | Examines HTTP request and translates it into user intent. -- | Examines HTTP request and translates it into user intent.
userApiRequest :: AppConfig -> Preferences.Preferences -> Request -> RequestBody -> Either ApiRequestError ApiRequest userApiRequest :: AppConfig -> Request -> RequestBody -> SchemaCache -> Either ApiRequestError ApiRequest
userApiRequest conf prefs req reqBody = do userApiRequest conf req reqBody sCache = do
resource <- getResource conf $ pathInfo req resource <- getResource conf $ pathInfo req
(schema, negotiatedByProfile) <- getSchema conf hdrs method (schema, negotiatedByProfile) <- getSchema conf hdrs method
act <- getAction resource schema method act <- getAction resource schema method
@@ -141,7 +141,7 @@ userApiRequest conf prefs req reqBody = do
, iRange = ranges , iRange = ranges
, iTopLevelRange = topLevelRange , iTopLevelRange = topLevelRange
, iPayload = payload , iPayload = payload
, iPreferences = prefs , iPreferences = Preferences.fromHeaders (configDbTxAllowOverride conf) (dbTimezones sCache) hdrs
, iQueryParams = qPrms , iQueryParams = qPrms
, iColumns = columns , iColumns = columns
, iHeaders = iHdrs , iHeaders = iHdrs
@@ -162,21 +162,15 @@ userApiRequest conf prefs req reqBody = do
contentMediaType = maybe MTApplicationJSON MediaType.decodeMediaType $ lookupHeader "content-type" contentMediaType = maybe MTApplicationJSON MediaType.decodeMediaType $ lookupHeader "content-type"
actIsInvokeSafe x = case x of {ActDb (ActRoutine _ (InvRead _)) -> True; _ -> False} actIsInvokeSafe x = case x of {ActDb (ActRoutine _ (InvRead _)) -> True; _ -> False}
-- | Parses the Prefer header
userPreferences :: AppConfig -> Request -> TimezoneNames -> Preferences.Preferences
userPreferences conf req timezones = Preferences.fromHeaders (configDbTxAllowOverride conf) timezones $ requestHeaders req
getResource :: AppConfig -> [Text] -> Either ApiRequestError Resource getResource :: AppConfig -> [Text] -> Either ApiRequestError Resource
getResource AppConfig{configOpenApiMode, configDbRootSpec} = \case getResource AppConfig{configOpenApiMode, configDbRootSpec} = \case
[] -> [] -> case configDbRootSpec of
case (configOpenApiMode,configDbRootSpec) of Just (QualifiedIdentifier _ pathName) -> Right $ ResourceRoutine pathName
(OADisabled,_) -> Left OpenAPIDisabled Nothing | configOpenApiMode == OADisabled -> Left NotFound
(_, Just qi) -> Right $ ResourceRoutine (qiName qi) | otherwise -> Right ResourceSchema
(_, Nothing) -> Right ResourceSchema
[table] -> Right $ ResourceRelation table [table] -> Right $ ResourceRelation table
["rpc", pName] -> Right $ ResourceRoutine pName ["rpc", pName] -> Right $ ResourceRoutine pName
_ -> Left InvalidResourcePath _ -> Left NotFound
getAction :: Resource -> Schema -> ByteString -> Either ApiRequestError Action getAction :: Resource -> Schema -> ByteString -> Either ApiRequestError Action
getAction resource schema method = getAction resource schema method =
@@ -207,7 +201,7 @@ getAction resource schema method =
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool) getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
getSchema AppConfig{configDbSchemas} hdrs method = do getSchema AppConfig{configDbSchemas} hdrs method = do
case profile of case profile of
Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema p $ toList configDbSchemas Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema $ toList configDbSchemas
| otherwise -> Right (p, True) | otherwise -> Right (p, True)
Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated
where where
@@ -224,8 +218,9 @@ getSchema AppConfig{configDbSchemas} hdrs method = do
lookupHeader = flip lookup hdrs lookupHeader = flip lookup hdrs
getRanges :: ByteString -> QueryParams -> RequestHeaders -> Either ApiRequestError (NonnegRange, HM.HashMap Text NonnegRange) getRanges :: ByteString -> QueryParams -> RequestHeaders -> Either ApiRequestError (NonnegRange, HM.HashMap Text NonnegRange)
getRanges method QueryParams{qsRanges} hdrs getRanges method QueryParams{qsOrder,qsRanges} hdrs
| isInvalidRange = Left $ InvalidRange (if rangeIsEmpty headerRange then LowerGTUpper else NegativeLimit) | isInvalidRange = Left $ InvalidRange (if rangeIsEmpty headerRange then LowerGTUpper else NegativeLimit)
| method `elem` ["PATCH", "DELETE"] && not (null qsRanges) && null qsOrder = Left LimitNoOrderError
| method == "PUT" && topLevelRange /= allRange = Left PutLimitNotAllowedError | method == "PUT" && topLevelRange /= allRange = Left PutLimitNotAllowedError
| otherwise = Right (topLevelRange, ranges) | otherwise = Right (topLevelRange, ranges)
where where
+20 -2
View File
@@ -2,7 +2,7 @@
-- Module: PostgREST.ApiRequest.Preferences -- Module: PostgREST.ApiRequest.Preferences
-- Description: Track client preferences to be employed when processing requests -- Description: Track client preferences to be employed when processing requests
-- --
-- Track client preferences set in HTTP 'Prefer' headers according to RFC7240[1]. -- Track client prefences set in HTTP 'Prefer' headers according to RFC7240[1].
-- --
-- [1] https://datatracker.ietf.org/doc/html/rfc7240 -- [1] https://datatracker.ietf.org/doc/html/rfc7240
-- --
@@ -12,6 +12,7 @@ module PostgREST.ApiRequest.Preferences
, PreferCount(..) , PreferCount(..)
, PreferHandling(..) , PreferHandling(..)
, PreferMissing(..) , PreferMissing(..)
, PreferParameters(..)
, PreferRepresentation(..) , PreferRepresentation(..)
, PreferResolution(..) , PreferResolution(..)
, PreferTransaction(..) , PreferTransaction(..)
@@ -36,6 +37,7 @@ import Protolude
-- >>> import Text.Pretty.Simple (pPrint) -- >>> import Text.Pretty.Simple (pPrint)
-- >>> deriving instance Show PreferResolution -- >>> deriving instance Show PreferResolution
-- >>> deriving instance Show PreferRepresentation -- >>> deriving instance Show PreferRepresentation
-- >>> deriving instance Show PreferParameters
-- >>> deriving instance Show PreferCount -- >>> deriving instance Show PreferCount
-- >>> deriving instance Show PreferTransaction -- >>> deriving instance Show PreferTransaction
-- >>> deriving instance Show PreferMissing -- >>> deriving instance Show PreferMissing
@@ -49,6 +51,7 @@ data Preferences
= Preferences = Preferences
{ preferResolution :: Maybe PreferResolution { preferResolution :: Maybe PreferResolution
, preferRepresentation :: Maybe PreferRepresentation , preferRepresentation :: Maybe PreferRepresentation
, preferParameters :: Maybe PreferParameters
, preferCount :: Maybe PreferCount , preferCount :: Maybe PreferCount
, preferTransaction :: Maybe PreferTransaction , preferTransaction :: Maybe PreferTransaction
, preferMissing :: Maybe PreferMissing , preferMissing :: Maybe PreferMissing
@@ -68,6 +71,7 @@ data Preferences
-- Preferences -- Preferences
-- { preferResolution = Just IgnoreDuplicates -- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing -- , preferRepresentation = Nothing
-- , preferParameters = Nothing
-- , preferCount = Just ExactCount -- , preferCount = Just ExactCount
-- , preferTransaction = Nothing -- , preferTransaction = Nothing
-- , preferMissing = Nothing -- , preferMissing = Nothing
@@ -85,6 +89,7 @@ data Preferences
-- Preferences -- Preferences
-- { preferResolution = Just IgnoreDuplicates -- { preferResolution = Just IgnoreDuplicates
-- , preferRepresentation = Nothing -- , preferRepresentation = Nothing
-- , preferParameters = Nothing
-- , preferCount = Just ExactCount -- , preferCount = Just ExactCount
-- , preferTransaction = Nothing -- , preferTransaction = Nothing
-- , preferMissing = Just ApplyNulls -- , preferMissing = Just ApplyNulls
@@ -117,6 +122,7 @@ data Preferences
-- Preferences -- Preferences
-- { preferResolution = Nothing -- { preferResolution = Nothing
-- , preferRepresentation = Just Full -- , preferRepresentation = Just Full
-- , preferParameters = Nothing
-- , preferCount = Just ExactCount -- , preferCount = Just ExactCount
-- , preferTransaction = Just Commit -- , preferTransaction = Just Commit
-- , preferMissing = Just ApplyDefaults -- , preferMissing = Just ApplyDefaults
@@ -131,6 +137,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
Preferences Preferences
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates] { preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
, preferRepresentation = parsePrefs [Full, None, HeadersOnly] , preferRepresentation = parsePrefs [Full, None, HeadersOnly]
, preferParameters = parsePrefs [SingleObject]
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount] , preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
, preferTransaction = if allowTxDbOverride then parsePrefs [Commit, Rollback] else Nothing , preferTransaction = if allowTxDbOverride then parsePrefs [Commit, Rollback] else Nothing
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls] , preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
@@ -144,6 +151,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
mapToHeadVal = map toHeaderValue mapToHeadVal = map toHeaderValue
acceptedPrefs = mapToHeadVal [MergeDuplicates, IgnoreDuplicates] ++ acceptedPrefs = mapToHeadVal [MergeDuplicates, IgnoreDuplicates] ++
mapToHeadVal [Full, None, HeadersOnly] ++ mapToHeadVal [Full, None, HeadersOnly] ++
mapToHeadVal [SingleObject] ++
mapToHeadVal [ExactCount, PlannedCount, EstimatedCount] ++ mapToHeadVal [ExactCount, PlannedCount, EstimatedCount] ++
mapToHeadVal [Commit, Rollback] ++ mapToHeadVal [Commit, Rollback] ++
mapToHeadVal [ApplyDefaults, ApplyNulls] ++ mapToHeadVal [ApplyDefaults, ApplyNulls] ++
@@ -171,7 +179,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref)) prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
prefAppliedHeader :: Preferences -> Maybe HTTP.Header prefAppliedHeader :: Preferences -> Maybe HTTP.Header
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone, preferMaxAffected } = prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone, preferMaxAffected } =
if null prefsVals if null prefsVals
then Nothing then Nothing
else Just (HTTP.hPreferenceApplied, combined) else Just (HTTP.hPreferenceApplied, combined)
@@ -181,6 +189,7 @@ prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferCou
toHeaderValue <$> preferResolution toHeaderValue <$> preferResolution
, toHeaderValue <$> preferMissing , toHeaderValue <$> preferMissing
, toHeaderValue <$> preferRepresentation , toHeaderValue <$> preferRepresentation
, toHeaderValue <$> preferParameters
, toHeaderValue <$> preferCount , toHeaderValue <$> preferCount
, toHeaderValue <$> preferTransaction , toHeaderValue <$> preferTransaction
, toHeaderValue <$> preferHandling , toHeaderValue <$> preferHandling
@@ -222,6 +231,15 @@ instance ToHeaderValue PreferRepresentation where
toHeaderValue None = "return=minimal" toHeaderValue None = "return=minimal"
toHeaderValue HeadersOnly = "return=headers-only" toHeaderValue HeadersOnly = "return=headers-only"
-- | How to pass parameters to stored procedures.
-- TODO: deprecated. Remove on next major version.
data PreferParameters
= SingleObject -- ^ Pass all parameters as a single json object to a stored procedure.
deriving Eq
instance ToHeaderValue PreferParameters where
toHeaderValue SingleObject = "params=single-object"
-- | How to determine the count of (expected) results -- | How to determine the count of (expected) results
data PreferCount data PreferCount
= ExactCount -- ^ Exact count (slower). = ExactCount -- ^ Exact count (slower).
+10 -12
View File
@@ -46,7 +46,7 @@ import PostgREST.SchemaCache.Identifiers (FieldName)
import PostgREST.ApiRequest.Types (AggregateFunction (..), import PostgREST.ApiRequest.Types (AggregateFunction (..),
EmbedParam (..), EmbedPath, Field, EmbedParam (..), EmbedPath, Field,
Filter (..), FtsOperator (..), Filter (..), FtsOperator (..),
Hint, IsVal (..), JoinType (..), Hint, JoinType (..),
JsonOperand (..), JsonOperand (..),
JsonOperation (..), JsonPath, JsonOperation (..), JsonPath,
ListVal, LogicOperator (..), ListVal, LogicOperator (..),
@@ -54,11 +54,10 @@ import PostgREST.ApiRequest.Types (AggregateFunction (..),
OpQuantifier (..), Operation (..), OpQuantifier (..), Operation (..),
OrderDirection (..), OrderDirection (..),
OrderNulls (..), OrderTerm (..), OrderNulls (..), OrderTerm (..),
QuantOperator (..), QPError (..), QuantOperator (..),
SelectItem (..), SelectItem (..),
SimpleOperator (..), SingleVal) SimpleOperator (..), SingleVal,
TrileanVal (..))
import PostgREST.Error (QPError (..))
import Protolude hiding (Sum, try) import Protolude hiding (Sum, try)
@@ -641,7 +640,7 @@ pOpExpr pSVal = do
pOperation = pIn <|> pIs <|> pIsDist <|> try pFts <|> try pSimpleOp <|> try pQuantOp <?> "operator (eq, gt, ...)" pOperation = pIn <|> pIs <|> pIsDist <|> try pFts <|> try pSimpleOp <|> try pQuantOp <?> "operator (eq, gt, ...)"
pIn = In <$> (try (string "in" *> pDelimiter) *> pListVal) pIn = In <$> (try (string "in" *> pDelimiter) *> pListVal)
pIs = Is <$> (try (string "is" *> pDelimiter) *> pIsVal) pIs = Is <$> (try (string "is" *> pDelimiter) *> pTriVal)
pIsDist = IsDistinctFrom <$> (try (string "isdistinct" *> pDelimiter) *> pSVal) pIsDist = IsDistinctFrom <$> (try (string "isdistinct" *> pDelimiter) *> pSVal)
@@ -654,12 +653,11 @@ pOpExpr pSVal = do
quant <- optionMaybe $ try (between (char '(') (char ')') (try (string "any" $> QuantAny) <|> string "all" $> QuantAll)) quant <- optionMaybe $ try (between (char '(') (char ')') (try (string "any" $> QuantAny) <|> string "all" $> QuantAll))
pDelimiter *> (OpQuant op quant <$> pSVal) pDelimiter *> (OpQuant op quant <$> pSVal)
pIsVal = try (ciString "null" $> IsNull) pTriVal = try (ciString "null" $> TriNull)
<|> try (ciString "not_null" $> IsNotNull) <|> try (ciString "unknown" $> TriUnknown)
<|> try (ciString "true" $> IsTriTrue) <|> try (ciString "true" $> TriTrue)
<|> try (ciString "false" $> IsTriFalse) <|> try (ciString "false" $> TriFalse)
<|> try (ciString "unknown" $> IsTriUnknown) <?> "null or trilean value (unknown, true, false)"
<?> "isVal: (null, not_null, true, false, unknown)"
pFts = do pFts = do
op <- try (string "fts" $> FilterFts) op <- try (string "fts" $> FilterFts)
+64 -12
View File
@@ -5,6 +5,7 @@ module PostgREST.ApiRequest.Types
, Cast , Cast
, Depth , Depth
, EmbedParam(..) , EmbedParam(..)
, ApiRequestError(..)
, EmbedPath , EmbedPath
, Field , Field
, Filter(..) , Filter(..)
@@ -13,7 +14,6 @@ module PostgREST.ApiRequest.Types
, JsonOperand(..) , JsonOperand(..)
, JsonOperation(..) , JsonOperation(..)
, JsonPath , JsonPath
, Language
, ListVal , ListVal
, LogicOperator(..) , LogicOperator(..)
, LogicTree(..) , LogicTree(..)
@@ -24,15 +24,23 @@ module PostgREST.ApiRequest.Types
, OrderDirection(..) , OrderDirection(..)
, OrderNulls(..) , OrderNulls(..)
, OrderTerm(..) , OrderTerm(..)
, QPError(..)
, RaiseError(..)
, RangeError(..)
, SingleVal , SingleVal
, IsVal(..) , TrileanVal(..)
, SimpleOperator(..) , SimpleOperator(..)
, QuantOperator(..) , QuantOperator(..)
, FtsOperator(..) , FtsOperator(..)
, SelectItem(..) , SelectItem(..)
) where ) where
import PostgREST.SchemaCache.Identifiers (FieldName) import PostgREST.MediaType (MediaType (..))
import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier)
import PostgREST.SchemaCache.Relationship (Relationship,
RelationshipsMap)
import PostgREST.SchemaCache.Routine (Routine (..))
import Protolude import Protolude
@@ -60,6 +68,51 @@ data SelectItem
} }
deriving (Eq, Show) deriving (Eq, Show)
data ApiRequestError
= AggregatesNotAllowed
| AmbiguousRelBetween Text Text [Relationship]
| AmbiguousRpc [Routine]
| MediaTypeError [ByteString]
| InvalidBody ByteString
| InvalidFilters
| InvalidPreferences [ByteString]
| InvalidRange RangeError
| InvalidRpcMethod ByteString
| LimitNoOrderError
| NotFound
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
| NoRpc Text Text [Text] Bool MediaType Bool [QualifiedIdentifier] [Routine]
| NotEmbedded Text
| PutLimitNotAllowedError
| QueryParamError QPError
| RelatedOrderNotToOne Text Text
| SpreadNotToOne Text Text
| UnacceptableFilter Text
| UnacceptableSchema [Text]
| UnsupportedMethod ByteString
| ColumnNotFound Text Text
| GucHeadersError
| GucStatusError
| OffLimitsChangesError Int64 Integer
| PutMatchingPkError
| SingularityError Integer
| PGRSTParseError RaiseError
| MaxAffectedViolationError Integer
deriving Show
data QPError = QPError Text Text
deriving Show
data RaiseError
= MsgParseError ByteString
| DetParseError ByteString
| NoDetail
deriving Show
data RangeError
= NegativeLimit
| LowerGTUpper
| OutOfBounds Text Text
deriving Show
type NodeName = Text type NodeName = Text
type Depth = Integer type Depth = Integer
@@ -122,7 +175,7 @@ data JsonOperation
deriving (Eq, Show, Ord) deriving (Eq, Show, Ord)
-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text -- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text
-- because we reuse our escaping functions and let pg do the casting with -- because we reuse our escaping functons and let pg do the casting with
-- '1'::int -- '1'::int
data JsonOperand data JsonOperand
= JKey { jVal :: Text } = JKey { jVal :: Text }
@@ -165,7 +218,7 @@ data Operation
= Op SimpleOperator SingleVal = Op SimpleOperator SingleVal
| OpQuant QuantOperator (Maybe OpQuantifier) SingleVal | OpQuant QuantOperator (Maybe OpQuantifier) SingleVal
| In ListVal | In ListVal
| Is IsVal | Is TrileanVal
| IsDistinctFrom SingleVal | IsDistinctFrom SingleVal
| Fts FtsOperator (Maybe Language) SingleVal | Fts FtsOperator (Maybe Language) SingleVal
deriving (Eq, Show) deriving (Eq, Show)
@@ -178,13 +231,12 @@ type SingleVal = Text
-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3) -- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)
type ListVal = [Text] type ListVal = [Text]
data IsVal -- | Three-valued logic values
= IsNull data TrileanVal
| IsNotNull = TriTrue
-- Trilean values | TriFalse
| IsTriTrue | TriNull
| IsTriFalse | TriUnknown
| IsTriUnknown
deriving (Eq, Show) deriving (Eq, Show)
-- Operators that are quantifiable, i.e. they can be used with the any/all modifiers -- Operators that are quantifiable, i.e. they can be used with the any/all modifiers
+9 -27
View File
@@ -17,7 +17,7 @@ module PostgREST.App
import Control.Monad.Except (liftEither) import Control.Monad.Except (liftEither)
import Data.Either.Combinators (mapLeft, whenLeft) import Data.Either.Combinators (mapLeft)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Data.String (IsString (..)) import Data.String (IsString (..))
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort, import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
@@ -42,12 +42,10 @@ import qualified PostgREST.Unix as Unix (installSignalHandlers)
import PostgREST.ApiRequest (ApiRequest (..)) import PostgREST.ApiRequest (ApiRequest (..))
import PostgREST.AppState (AppState) import PostgREST.AppState (AppState)
import PostgREST.Auth.Types (AuthResult (..)) import PostgREST.Auth (AuthResult (..))
import PostgREST.Config (AppConfig (..), LogLevel (..), import PostgREST.Config (AppConfig (..), LogLevel (..))
LogQuery (..))
import PostgREST.Config.PgVersion (PgVersion (..)) import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.Error (Error) import PostgREST.Error (Error)
import PostgREST.Network (resolveHost)
import PostgREST.Observation (Observation (..)) import PostgREST.Observation (Observation (..))
import PostgREST.Response.Performance (ServerTiming (..), import PostgREST.Response.Performance (ServerTiming (..),
serverTimingHeader) serverTimingHeader)
@@ -68,6 +66,8 @@ run appState = do
let observer = AppState.getObserver appState let observer = AppState.getObserver appState
conf@AppConfig{..} <- AppState.getConfig appState conf@AppConfig{..} <- AppState.getConfig appState
observer $ AppStartObs prettyVersion
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState) Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
@@ -82,8 +82,7 @@ run appState = do
observer $ AppServerUnixObs path observer $ AppServerUnixObs path
Nothing -> do Nothing -> do
port <- NS.socketPort $ AppState.getSocketREST appState port <- NS.socketPort $ AppState.getSocketREST appState
host <- resolveHost $ AppState.getSocketREST appState observer $ AppServerPortObs port
observer $ AppServerPortObs (fromJust host) port
Warp.runSettingsSocket (serverSettings conf) (AppState.getSocketREST appState) app Warp.runSettingsSocket (serverSettings conf) (AppState.getSocketREST appState) app
@@ -144,28 +143,11 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@
body <- lift $ Wai.strictRequestBody req body <- lift $ Wai.strictRequestBody req
let jwtTime = if configServerTimingEnabled then Auth.getJwtDur req else Nothing let jwtTime = if configServerTimingEnabled then Auth.getJwtDur req else Nothing
timezones = dbTimezones sCache
prefs = ApiRequest.userPreferences conf req timezones
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestError $ ApiRequest.userApiRequest conf prefs req body (parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestError $ ApiRequest.userApiRequest conf req body sCache
(planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache (planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
(queryTime, queryResult) <- withTiming $ Query.runQuery appState conf authResult apiReq plan sCache pgVer (Just authRole /= configDbAnonRole)
let query = Query.query conf authResult apiReq plan sCache pgVer (respTime, resp) <- withTiming $ liftEither $ Response.actionResponse queryResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
logSQL = lift . AppState.getObserver appState . DBQuery (Query.getSQLQuery query)
(queryTime, queryResult) <- withTiming $ do
case query of
Query.NoDbQuery r -> pure r
Query.DbQuery{..} -> do
dbRes <- lift $ AppState.usePool appState (dqTransaction dqIsoLevel dqTxMode $ runExceptT dqDbHandler)
let eitherResp = mapLeft Error.PgErr . mapLeft (Error.PgError (Just authRole /= configDbAnonRole)) $ dbRes
when (configLogQuery /= LogQueryDisabled) $ whenLeft eitherResp $ logSQL . Error.status
liftEither eitherResp >>= liftEither
(respTime, resp) <- withTiming $ do
let response = Response.actionResponse queryResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
when (configLogQuery /= LogQueryDisabled) $ logSQL $ either Error.status Response.pgrstStatus response
liftEither response
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime queryTime respTime) resp return $ toWaiResponse (ServerTiming jwtTime parseTime planTime queryTime respTime) resp
+40 -21
View File
@@ -4,6 +4,8 @@
module PostgREST.AppState module PostgREST.AppState
( AppState ( AppState
, AuthResult(..)
, JwtCacheState(..)
, destroy , destroy
, getConfig , getConfig
, getSchemaCache , getSchemaCache
@@ -30,7 +32,10 @@ module PostgREST.AppState
, isPending , isPending
) where ) 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.Char8 as BS
import qualified Data.Cache as C
import Data.Either.Combinators (whenLeft) import Data.Either.Combinators (whenLeft)
import qualified Data.Text as T (unpack) import qualified Data.Text as T (unpack)
import qualified Hasql.Pool as SQL import qualified Hasql.Pool as SQL
@@ -39,7 +44,6 @@ import qualified Hasql.Session as SQL
import qualified Hasql.Transaction.Sessions as SQL import qualified Hasql.Transaction.Sessions as SQL
import qualified Network.HTTP.Types.Status as HTTP import qualified Network.HTTP.Types.Status as HTTP
import qualified Network.Socket as NS import qualified Network.Socket as NS
import qualified PostgREST.Auth.JwtCache as JwtCache
import qualified PostgREST.Error as Error import qualified PostgREST.Error as Error
import qualified PostgREST.Logger as Logger import qualified PostgREST.Logger as Logger
import qualified PostgREST.Metrics as Metrics import qualified PostgREST.Metrics as Metrics
@@ -57,7 +61,6 @@ import Data.IORef (IORef, atomicWriteIORef, newIORef,
readIORef) readIORef)
import Data.Time.Clock (UTCTime, getCurrentTime) import Data.Time.Clock (UTCTime, getCurrentTime)
import PostgREST.Auth.JwtCache (JwtCacheState)
import PostgREST.Config (AppConfig (..), import PostgREST.Config (AppConfig (..),
addFallbackAppName, addFallbackAppName,
readAppConfig) readAppConfig)
@@ -76,6 +79,17 @@ import Data.Streaming.Network (bindPortTCP, bindRandomPortTCP)
import Data.String (IsString (..)) import Data.String (IsString (..))
import Protolude import Protolude
data AuthResult = AuthResult
{ authClaims :: KM.KeyMap JSON.Value
, authRole :: BS.ByteString
}
-- | JWT Cache and IO action that triggers purging old entries from the cache
data JwtCacheState = JwtCacheState
{ jwtCache :: C.Cache ByteString AuthResult
, purgeCache :: IO ()
}
data AppState = AppState data AppState = AppState
-- | Database connection pool -- | Database connection pool
{ statePool :: SQL.Pool { statePool :: SQL.Pool
@@ -99,14 +113,14 @@ data AppState = AppState
, stateNextDelay :: IORef Int , stateNextDelay :: IORef Int
-- | Keeps track of the next delay for the listener -- | Keeps track of the next delay for the listener
, stateNextListenerDelay :: IORef Int , stateNextListenerDelay :: IORef Int
-- | JWT Cache
, jwtCacheState :: JwtCacheState
-- | Network socket for REST API -- | Network socket for REST API
, stateSocketREST :: NS.Socket , stateSocketREST :: NS.Socket
-- | Network socket for the admin UI -- | Network socket for the admin UI
, stateSocketAdmin :: Maybe NS.Socket , stateSocketAdmin :: Maybe NS.Socket
-- | Observation handler -- | Observation handler
, stateObserver :: ObservationHandler , stateObserver :: ObservationHandler
-- | JWT Cache
, stateJwtCache :: JwtCache.JwtCacheState
, stateLogger :: Logger.LoggerState , stateLogger :: Logger.LoggerState
, stateMetrics :: Metrics.MetricsState , stateMetrics :: Metrics.MetricsState
} }
@@ -125,16 +139,23 @@ init conf@AppConfig{configLogLevel, configDbPoolSize} = do
metricsState <- Metrics.init configDbPoolSize metricsState <- Metrics.init configDbPoolSize
let observer = liftA2 (>>) (Logger.observationLogger loggerState configLogLevel) (Metrics.observationMetrics metricsState) let observer = liftA2 (>>) (Logger.observationLogger loggerState configLogLevel) (Metrics.observationMetrics metricsState)
observer $ AppStartObs prettyVersion
jwtCacheState <- JwtCache.init
pool <- initPool conf observer pool <- initPool conf observer
(sock, adminSock) <- initSockets conf (sock, adminSock) <- initSockets conf
state' <- initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState observer state' <- initWithPool (sock, adminSock) pool conf loggerState metricsState observer
pure state' { stateSocketREST = sock, stateSocketAdmin = adminSock} pure state' { stateSocketREST = sock, stateSocketAdmin = adminSock}
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> JwtCache.JwtCacheState -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState observer = do initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
cache <- C.newCache Nothing
-- purgeExpired has O(n^2) complexity
-- so we wrap it in debounce to make sure it:
-- 1) is executed asynchronously
-- 2) only a single purge operation is running at a time
debounce <- mkDebounce defaultDebounceSettings
-- debounceFreq is set to default 1 second
{ debounceAction = C.purgeExpired cache
, debounceEdge = leadingEdge
}
appState <- AppState pool appState <- AppState pool
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step <$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
@@ -147,10 +168,10 @@ initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState
<*> myThreadId <*> myThreadId
<*> newIORef 0 <*> newIORef 0
<*> newIORef 1 <*> newIORef 1
<*> pure (JwtCacheState cache debounce)
<*> pure sock <*> pure sock
<*> pure adminSock <*> pure adminSock
<*> pure observer <*> pure observer
<*> pure jwtCacheState
<*> pure loggerState <*> pure loggerState
<*> pure metricsState <*> pure metricsState
@@ -174,8 +195,7 @@ initSockets AppConfig{..} = do
cfg'uspm = configServerUnixSocketMode cfg'uspm = configServerUnixSocketMode
cfg'host = configServerHost cfg'host = configServerHost
cfg'port = configServerPort cfg'port = configServerPort
cfg'adminHost = configAdminServerHost cfg'adminport = configAdminServerPort
cfg'adminPort = configAdminServerPort
sock <- case cfg'usp of sock <- case cfg'usp of
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows, -- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
@@ -193,16 +213,16 @@ initSockets AppConfig{..} = do
pure (num, sock) pure (num, sock)
pure sock pure sock
adminSock <- case cfg'adminPort of adminSock <- case cfg'adminport of
Just adminPort -> do Just adminPort -> do
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'adminHost) adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'host)
pure $ Just adminSock pure $ Just adminSock
Nothing -> pure Nothing Nothing -> pure Nothing
pure (sock, adminSock) pure (sock, adminSock)
initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool
initPool AppConfig{..} observer = do initPool AppConfig{..} observer =
SQL.acquire $ SQL.settings SQL.acquire $ SQL.settings
[ SQL.size configDbPoolSize [ SQL.size configDbPoolSize
, SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout , SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout
@@ -312,7 +332,7 @@ getTime :: AppState -> IO UTCTime
getTime = stateGetTime getTime = stateGetTime
getJwtCacheState :: AppState -> JwtCacheState getJwtCacheState :: AppState -> JwtCacheState
getJwtCacheState = stateJwtCache getJwtCacheState = jwtCacheState
getSocketREST :: AppState -> NS.Socket getSocketREST :: AppState -> NS.Socket
getSocketREST = stateSocketREST getSocketREST = stateSocketREST
@@ -388,7 +408,6 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion
killThread mainThreadId killThread mainThreadId
observer $ DBConnectedObs $ pgvFullName actualPgVersion observer $ DBConnectedObs $ pgvFullName actualPgVersion
observer $ PoolInit configDbPoolSize
putPgVersion appState actualPgVersion putPgVersion appState actualPgVersion
return $ Just actualPgVersion return $ Just actualPgVersion
@@ -407,7 +426,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
Left e -> do Left e -> do
putSCacheStatus appState SCPending putSCacheStatus appState SCPending
putSchemaCache appState Nothing putSchemaCache appState Nothing
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e observer $ SchemaCacheErrorObs e
return Nothing return Nothing
Right sCache -> do Right sCache -> do
@@ -437,7 +456,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
-- | Reads the in-db config and reads the config file again -- | Reads the in-db config and reads the config file again
-- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue. -- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue.
readInDbConfig :: Bool -> AppState -> IO () readInDbConfig :: Bool -> AppState -> IO ()
readInDbConfig startingUp appState@AppState{stateObserver=observer} = do readInDbConfig startingUp appState@AppState{stateObserver=observer, jwtCacheState=JwtCacheState{jwtCache}} = do
conf <- getConfig appState conf <- getConfig appState
pgVer <- getPgVersion appState pgVer <- getPgVersion appState
dbSettings <- dbSettings <-
@@ -474,7 +493,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
if configJwtSecret conf == configJwtSecret newConf then if configJwtSecret conf == configJwtSecret newConf then
pass pass
else else
JwtCache.emptyCache (getJwtCacheState appState) -- atomic O(1) operation C.purge jwtCache -- atomic O(1) operation
if startingUp then if startingUp then
pass pass
+80 -99
View File
@@ -10,124 +10,76 @@ Authentication should always be implemented in an external service.
In the test suite there is an example of simple login function that can be used for a In the test suite there is an example of simple login function that can be used for a
very simple authentication system inside the PostgreSQL database. very simple authentication system inside the PostgreSQL database.
-} -}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
module PostgREST.Auth module PostgREST.Auth
( getResult ( AuthResult (..)
, getResult
, getJwtDur , getJwtDur
, getRole , getRole
, middleware , middleware
) where ) where
import qualified Crypto.JWT as JWT
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.Aeson.Key as K import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM import qualified Data.Aeson.KeyMap as KM
import qualified Data.Aeson.Types as JSON import qualified Data.Aeson.Types as JSON
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS
import qualified Data.ByteString.Lazy.Char8 as LBS import qualified Data.ByteString.Lazy.Char8 as LBS
import qualified Data.Cache as C
import qualified Data.Scientific as Sci import qualified Data.Scientific as Sci
import qualified Data.Text as T
import qualified Data.Vault.Lazy as Vault import qualified Data.Vault.Lazy as Vault
import qualified Data.Vector as V import qualified Data.Vector as V
import qualified Jose.Jwk as JWT
import qualified Jose.Jwt as JWT
import qualified Network.HTTP.Types.Header as HTTP import qualified Network.HTTP.Types.Header as HTTP
import qualified Network.Wai as Wai import qualified Network.Wai as Wai
import qualified Network.Wai.Middleware.HttpAuth as Wai import qualified Network.Wai.Middleware.HttpAuth as Wai
import Control.Lens (set)
import Control.Monad.Except (liftEither) import Control.Monad.Except (liftEither)
import Data.Either.Combinators (mapLeft) import Data.Either.Combinators (mapLeft)
import Data.List (lookup) import Data.List (lookup)
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds) import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds) import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import System.Clock (TimeSpec (..))
import System.IO.Unsafe (unsafePerformIO) import System.IO.Unsafe (unsafePerformIO)
import System.TimeIt (timeItT) import System.TimeIt (timeItT)
import PostgREST.AppState (AppState, getConfig, getJwtCacheState, import PostgREST.AppState (AppState, AuthResult (..),
getTime) JwtCacheState (..), getConfig,
import PostgREST.Auth.JwtCache (lookupJwtCache) getJwtCacheState, getTime)
import PostgREST.Auth.Types (AuthResult (..)) import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
import PostgREST.Config (AppConfig (..), FilterExp (..), import PostgREST.Error (Error (..))
JSPath, JSPathExp (..))
import PostgREST.Error (Error (..), JwtClaimsError (..),
JwtDecodeError (..), JwtError (..))
import Protolude import Protolude
-- | Receives the JWT secret and audience (from config) and a JWT and returns a -- | Receives the JWT secret and audience (from config) and a JWT and returns a
-- JSON object of JWT claims. -- JSON object of JWT claims.
parseToken :: AppConfig -> Maybe ByteString -> UTCTime -> ExceptT Error IO JSON.Value parseToken :: Monad m =>
parseToken _ Nothing _ = return JSON.emptyObject AppConfig -> LByteString -> UTCTime -> ExceptT Error m JSON.Value
parseToken _ (Just "") _ = throwE . JwtErr $ JwtDecodeErr EmptyAuthHeader parseToken _ "" _ = return JSON.emptyObject
parseToken AppConfig{..} (Just tkn) time = do parseToken AppConfig{..} token time = do
secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS secret <- liftEither . maybeToRight JwtTokenMissing $ configJWKS
tknWith3Parts <- liftEither $ hasThreeParts tkn eitherClaims <-
eitherContent <- liftIO $ JWT.decode (JWT.keys secret) Nothing tknWith3Parts lift . runExceptT $
content <- liftEither . mapLeft (JwtErr . jwtDecodeError) $ eitherContent JWT.verifyClaimsAt validation secret time =<< JWT.decodeCompact token
liftEither $ mapLeft JwtErr $ verifyClaims content liftEither . mapLeft jwtClaimsError $ JSON.toJSON <$> eitherClaims
where where
hasThreeParts :: ByteString -> Either Error ByteString validation =
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of JWT.defaultJWTValidationSettings audienceCheck & set JWT.allowedSkew 30
3 -> Right token
n -> Left $ JwtErr $ JwtDecodeErr $ UnexpectedParts n
jwtDecodeError :: JWT.JwtError -> JwtError
-- The only errors we can get from JWT.decode function are:
-- BadAlgorithm
-- KeyError
-- BadCrypto
jwtDecodeError (JWT.KeyError m) = JwtDecodeErr $ KeyError m
jwtDecodeError (JWT.BadAlgorithm m) = JwtDecodeErr $ BadAlgorithm m
jwtDecodeError JWT.BadCrypto = JwtDecodeErr BadCrypto
-- Control never reaches here, the decode function only returns the above three
jwtDecodeError _ = JwtDecodeErr UnreachableDecodeError
verifyClaims :: JWT.JwtContent -> Either JwtError JSON.Value audienceCheck :: JWT.StringOrURI -> Bool
verifyClaims (JWT.Jws (_, claims)) = case JSON.decodeStrict claims of audienceCheck = maybe (const True) (==) configJwtAudience
Just jclaims@(JSON.Object mclaims) ->
verifyClaim mclaims "exp" isValidExpClaim JWTExpired >>
verifyClaim mclaims "nbf" isValidNbfClaim JWTNotYetValid >>
verifyClaim mclaims "iat" isValidIatClaim JWTIssuedAtFuture >>
verifyClaim mclaims "aud" isValidAudClaim JWTNotInAudience >>
return jclaims
_ -> Left $ JwtClaimsErr ParsingClaimsFailed
-- TODO: We could enable JWE support here (encrypted tokens)
verifyClaims _ = Left $ JwtDecodeErr UnsupportedTokenType
verifyClaim mclaims claim func err = do jwtClaimsError :: JWT.JWTError -> Error
isValid <- maybe (Right True) func (KM.lookup claim mclaims) jwtClaimsError JWT.JWTExpired = JwtTokenInvalid "JWT expired"
unless isValid $ Left $ JwtClaimsErr err jwtClaimsError e = JwtTokenInvalid $ show e
allowedSkewSeconds = 30 :: Int64
now = floor . nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds time
sciToInt = fromMaybe 0 . Sci.toBoundedInteger
allStrings = all (\case (JSON.String _) -> True; _ -> False)
isValidExpClaim :: JSON.Value -> Either JwtError Bool
isValidExpClaim (JSON.Number secs) = Right $ now <= (sciToInt secs + allowedSkewSeconds)
isValidExpClaim _ = Left $ JwtClaimsErr ExpClaimNotNumber
isValidNbfClaim :: JSON.Value -> Either JwtError Bool
isValidNbfClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
isValidNbfClaim _ = Left $ JwtClaimsErr NbfClaimNotNumber
isValidIatClaim :: JSON.Value -> Either JwtError Bool
isValidIatClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
isValidIatClaim _ = Left $ JwtClaimsErr IatClaimNotNumber
isValidAudClaim :: JSON.Value -> Either JwtError Bool
isValidAudClaim JSON.Null = Right True -- {"aud": null} is valid for all audiences
isValidAudClaim (JSON.String str) = Right $ maybe (const True) (==) configJwtAudience str
isValidAudClaim (JSON.Array arr)
| null arr = Right True -- {"aud": []} is valid for all audiences
| allStrings arr = Right $ maybe True (\a -> JSON.String a `elem` arr) configJwtAudience
isValidAudClaim _ = Left $ JwtClaimsErr AudClaimNotStringOrArray
parseClaims :: Monad m => parseClaims :: Monad m =>
AppConfig -> JSON.Value -> ExceptT Error m AuthResult AppConfig -> JSON.Value -> ExceptT Error m AuthResult
parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
-- role defaults to anon if not specified in jwt -- role defaults to anon if not specified in jwt
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $ role <- liftEither . maybeToRight JwtTokenRequired $
unquoted <$> walkJSPath (Just jclaims) configJwtRoleClaimKey <|> configDbAnonRole unquoted <$> walkJSPath (Just jclaims) configJwtRoleClaimKey <|> configDbAnonRole
return AuthResult return AuthResult
{ authClaims = mclaims & KM.insert "role" (JSON.toJSON $ decodeUtf8 role) { authClaims = mclaims & KM.insert "role" (JSON.toJSON $ decodeUtf8 role)
@@ -138,20 +90,8 @@ parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
walkJSPath x [] = x walkJSPath x [] = x
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
walkJSPath (Just (JSON.Array ar)) [JSPFilter (EqualsCond txt)] = findFirstMatch (==) txt ar
walkJSPath (Just (JSON.Array ar)) [JSPFilter (NotEqualsCond txt)] = findFirstMatch (/=) txt ar
walkJSPath (Just (JSON.Array ar)) [JSPFilter (StartsWithCond txt)] = findFirstMatch T.isPrefixOf txt ar
walkJSPath (Just (JSON.Array ar)) [JSPFilter (EndsWithCond txt)] = findFirstMatch T.isSuffixOf txt ar
walkJSPath (Just (JSON.Array ar)) [JSPFilter (ContainsCond txt)] = findFirstMatch T.isInfixOf txt ar
walkJSPath _ _ = Nothing walkJSPath _ _ = Nothing
findFirstMatch matchWith pattern = foldr checkMatch Nothing
where
checkMatch (JSON.String txt) acc
| pattern `matchWith` txt = Just $ JSON.String txt
| otherwise = acc
checkMatch _ acc = acc
unquoted :: JSON.Value -> BS.ByteString unquoted :: JSON.Value -> BS.ByteString
unquoted (JSON.String t) = encodeUtf8 t unquoted (JSON.String t) = encodeUtf8 t
unquoted v = LBS.toStrict $ JSON.encode v unquoted v = LBS.toStrict $ JSON.encode v
@@ -165,11 +105,10 @@ middleware appState app req respond = do
conf <- getConfig appState conf <- getConfig appState
time <- getTime appState time <- getTime appState
let token = Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req) let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
parseJwt = runExceptT $ parseToken conf token time >>= parseClaims conf parseJwt = runExceptT $ parseToken conf (LBS.fromStrict token) time >>= parseClaims conf
jwtCacheState = getJwtCacheState appState
-- If ServerTimingEnabled -> calculate JWT validation time -- If DbPlanEnabled -> calculate JWT validation time
-- If JwtCacheMaxLifetime -> cache JWT validation result -- If JwtCacheMaxLifetime -> cache JWT validation result
req' <- case (configServerTimingEnabled conf, configJwtCacheMaxLifetime conf) of req' <- case (configServerTimingEnabled conf, configJwtCacheMaxLifetime conf) of
(True, 0) -> do (True, 0) -> do
@@ -177,9 +116,7 @@ middleware appState app req respond = do
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur } return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
(True, maxLifetime) -> do (True, maxLifetime) -> do
(dur, authResult) <- timeItT $ case token of (dur, authResult) <- timeItT $ getJWTFromCache appState token maxLifetime parseJwt time
Just tkn -> lookupJwtCache jwtCacheState tkn maxLifetime parseJwt time
Nothing -> parseJwt
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur } return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
(False, 0) -> do (False, 0) -> do
@@ -187,13 +124,57 @@ middleware appState app req respond = do
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult } return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
(False, maxLifetime) -> do (False, maxLifetime) -> do
authResult <- case token of authResult <- getJWTFromCache appState token maxLifetime parseJwt time
Just tkn -> lookupJwtCache jwtCacheState tkn maxLifetime parseJwt time
Nothing -> parseJwt
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult } return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
app req' respond 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
let JwtCacheState{..} = getJwtCacheState appState
checkCache <- C.lookup jwtCache token
authResult <- maybe parseJwt (pure . Right) checkCache
case (authResult,checkCache) of
-- From comment:
-- https://github.com/PostgREST/postgrest/pull/3801#discussion_r1857987914
--
-- We purge expired cache entries on a cache miss
-- The reasoning is that:
--
-- 1. We expect it to be rare (otherwise there is no point of the cache)
-- 2. It makes sure the cache is not growing (as inserting new entries
-- does garbage collection)
-- 3. Since this is time expiration based cache there is no real risk of
-- starvation - sooner or later we are going to have a cache miss.
(Right res, Nothing) -> do -- cache miss
let timeSpec = getTimeSpec res maxLifetime utc
-- insert new cache entry
C.insert' jwtCache timeSpec token res
-- Execute IO action to purge the cache
-- It is assumed this action returns immidiately
-- so that request processing is not blocked.
purgeCache
_ -> 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 :: Vault.Key (Either Error AuthResult)
authResultKey = unsafePerformIO Vault.newKey authResultKey = unsafePerformIO Vault.newKey
{-# NOINLINE authResultKey #-} {-# NOINLINE authResultKey #-}
-99
View File
@@ -1,99 +0,0 @@
{-|
Module : PostgREST.Auth.JwtCache
Description : PostgREST Jwt Authentication Result Cache.
This module provides functions to deal with the JWT cache
-}
{-# LANGUAGE NamedFieldPuns #-}
module PostgREST.Auth.JwtCache
( init
, JwtCacheState
, lookupJwtCache
, emptyCache
) where
import qualified Data.Aeson as JSON
import qualified Data.Aeson.KeyMap as KM
import qualified Data.Cache as C
import qualified Data.Scientific as Sci
import Control.Debounce
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import System.Clock (TimeSpec (..))
import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Error (Error (..))
import Protolude
-- | JWT Cache and IO action that triggers purging old entries from the cache
data JwtCacheState = JwtCacheState
{ jwtCache :: C.Cache ByteString AuthResult
, purgeCache :: IO ()
}
-- | Initialize JwtCacheState
init :: IO JwtCacheState
init = do
cache <- C.newCache Nothing -- no default expiration
-- purgeExpired has O(n^2) complexity
-- so we wrap it in debounce to make sure it:
-- 1) is executed asynchronously
-- 2) only a single purge operation is running at a time
debounce <- mkDebounce defaultDebounceSettings
-- debounceFreq is set to default 1 second
{ debounceAction = C.purgeExpired cache
, debounceEdge = leadingEdge
}
pure $ JwtCacheState cache debounce
-- | Used to retrieve and insert JWT to JWT Cache
lookupJwtCache :: JwtCacheState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
lookupJwtCache JwtCacheState{jwtCache, purgeCache} token maxLifetime parseJwt utc = do
checkCache <- C.lookup jwtCache token
authResult <- maybe parseJwt (pure . Right) checkCache
case (authResult,checkCache) of
-- From comment:
-- https://github.com/PostgREST/postgrest/pull/3801#discussion_r1857987914
--
-- We purge expired cache entries on a cache miss
-- The reasoning is that:
--
-- 1. We expect it to be rare (otherwise there is no point of the cache)
-- 2. It makes sure the cache is not growing (as inserting new entries
-- does garbage collection)
-- 3. Since this is time expiration based cache there is no real risk of
-- starvation - sooner or later we are going to have a cache miss.
(Right res, Nothing) -> do -- cache miss
let timeSpec = getTimeSpec res maxLifetime utc
-- insert new cache entry
C.insert' jwtCache (Just timeSpec) token res
-- Execute IO action to purge the cache
-- It is assumed this action returns immidiately
-- so that request processing is not blocked.
purgeCache
_ -> pure ()
return authResult
-- Used to extract JWT exp claim and add to JWT Cache
getTimeSpec :: AuthResult -> Int -> UTCTime -> 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) -> TimeSpec (sciToInt seconds - utcToSecs utc) 0
_ -> TimeSpec (fromIntegral maxLifetime :: Int64) 0
-- | Empty the cache (done when the config is reloaded)
emptyCache :: JwtCacheState -> IO ()
emptyCache JwtCacheState{jwtCache} = C.purge jwtCache
-13
View File
@@ -1,13 +0,0 @@
module PostgREST.Auth.Types
( AuthResult (..) )
where
import qualified Data.Aeson as JSON
import qualified Data.Aeson.KeyMap as KM
import qualified Data.ByteString as BS
-- | Parse result for JWT Claims
data AuthResult = AuthResult
{ authClaims :: KM.KeyMap JSON.Value
, authRole :: BS.ByteString
}
+4 -4
View File
@@ -60,7 +60,7 @@ dumpSchema appState = do
case result of case result of
Left e -> do Left e -> do
let observer = AppState.getObserver appState let observer = AppState.getObserver appState
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e observer $ SchemaCacheErrorObs e
exitFailure exitFailure
Right sCache -> return $ JSON.encode sCache Right sCache -> return $ JSON.encode sCache
@@ -128,6 +128,9 @@ exampleConfigFile =
[str|## Admin server used for checks. It's disabled by default unless a port is specified. [str|## Admin server used for checks. It's disabled by default unless a port is specified.
|# admin-server-port = 3001 |# admin-server-port = 3001
| |
|## Whether to enable the /config endpoint of the admin server
|# admin-server-config-enabled = false
|
|## The database role to use when no client authentication is provided |## The database role to use when no client authentication is provided
|# db-anon-role = "anon" |# db-anon-role = "anon"
| |
@@ -209,9 +212,6 @@ exampleConfigFile =
|## Logging level, the admitted values are: crit, error, warn, info and debug. |## Logging level, the admitted values are: crit, error, warn, info and debug.
|log-level = "error" |log-level = "error"
| |
|## Log the requested SQL query at the current log-level.
|log-query = "disabled"
|
|## Determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely. |## Determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely.
|## Admitted values: follow-privileges, ignore-privileges, disabled |## Admitted values: follow-privileges, ignore-privileges, disabled
|openapi-mode = "follow-privileges" |openapi-mode = "follow-privileges"
+53 -104
View File
@@ -15,9 +15,7 @@ module PostgREST.Config
, Environment , Environment
, JSPath , JSPath
, JSPathExp(..) , JSPathExp(..)
, FilterExp(..)
, LogLevel(..) , LogLevel(..)
, LogQuery(..)
, OpenAPIMode(..) , OpenAPIMode(..)
, Proxy(..) , Proxy(..)
, toText , toText
@@ -30,25 +28,28 @@ module PostgREST.Config
, addTargetSessionAttrs , addTargetSessionAttrs
) where ) where
import qualified Crypto.JOSE.Types as JOSE
import qualified Crypto.JWT as JWT
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Base64 as B64
import qualified Data.ByteString.Lazy as LBS
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.Configurator as C import qualified Data.Configurator as C
import qualified Data.Map.Strict as M import qualified Data.Map.Strict as M
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Encoding as T import qualified Data.Text.Encoding as T
import qualified Jose.Jwa as JWT
import qualified Jose.Jwk as JWT
import Control.Lens (preview)
import Control.Monad (fail) import Control.Monad (fail)
import Crypto.JWT (JWK, JWKSet, StringOrURI, stringOrUri)
import Data.Aeson (toJSON)
import Data.Either.Combinators (mapLeft) import Data.Either.Combinators (mapLeft)
import Data.List (lookup) import Data.List (lookup)
import Data.List.NonEmpty (fromList, toList) import Data.List.NonEmpty (fromList, toList)
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Data.Scientific (floatingOrInteger) import Data.Scientific (floatingOrInteger)
import Jose.Jwk (Jwk, JwkSet) import Network.URI (escapeURIString,
import Network.URI (escapeURIString, isURI,
isUnescapedInURIComponent) isUnescapedInURIComponent)
import Numeric (readOct, showOct) import Numeric (readOct, showOct)
import System.Environment (getEnvironment) import System.Environment (getEnvironment)
@@ -56,9 +57,8 @@ import System.Posix.Types (FileMode)
import PostgREST.Config.Database (RoleIsolationLvl, import PostgREST.Config.Database (RoleIsolationLvl,
RoleSettings) RoleSettings)
import PostgREST.Config.JSPath (FilterExp (..), JSPath, import PostgREST.Config.JSPath (JSPath, JSPathExp (..),
JSPathExp (..), dumpJSPath, dumpJSPath, pRoleClaimKey)
pRoleClaimKey)
import PostgREST.Config.Proxy (Proxy (..), import PostgREST.Config.Proxy (Proxy (..),
isMalformedProxyUri, toURI) isMalformedProxyUri, toURI)
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi, import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi,
@@ -92,14 +92,13 @@ data AppConfig = AppConfig
, configDbTxRollbackAll :: Bool , configDbTxRollbackAll :: Bool
, configDbUri :: Text , configDbUri :: Text
, configFilePath :: Maybe FilePath , configFilePath :: Maybe FilePath
, configJWKS :: Maybe JwkSet , configJWKS :: Maybe JWKSet
, configJwtAudience :: Maybe Text , configJwtAudience :: Maybe StringOrURI
, configJwtRoleClaimKey :: JSPath , configJwtRoleClaimKey :: JSPath
, configJwtSecret :: Maybe BS.ByteString , configJwtSecret :: Maybe BS.ByteString
, configJwtSecretIsBase64 :: Bool , configJwtSecretIsBase64 :: Bool
, configJwtCacheMaxLifetime :: Int , configJwtCacheMaxLifetime :: Int
, configLogLevel :: LogLevel , configLogLevel :: LogLevel
, configLogQuery :: LogQuery
, configOpenApiMode :: OpenAPIMode , configOpenApiMode :: OpenAPIMode
, configOpenApiSecurityActive :: Bool , configOpenApiSecurityActive :: Bool
, configOpenApiServerProxyUri :: Maybe Text , configOpenApiServerProxyUri :: Maybe Text
@@ -110,8 +109,8 @@ data AppConfig = AppConfig
, configServerTimingEnabled :: Bool , configServerTimingEnabled :: Bool
, configServerUnixSocket :: Maybe FilePath , configServerUnixSocket :: Maybe FilePath
, configServerUnixSocketMode :: FileMode , configServerUnixSocketMode :: FileMode
, configAdminServerHost :: Text
, configAdminServerPort :: Maybe Int , configAdminServerPort :: Maybe Int
, configAdminServerConfigEnabled :: Bool
, configRoleSettings :: RoleSettings , configRoleSettings :: RoleSettings
, configRoleIsoLvl :: RoleIsolationLvl , configRoleIsoLvl :: RoleIsolationLvl
, configInternalSCSleep :: Maybe Int32 , configInternalSCSleep :: Maybe Int32
@@ -128,14 +127,6 @@ dumpLogLevel = \case
LogInfo -> "info" LogInfo -> "info"
LogDebug -> "debug" LogDebug -> "debug"
data LogQuery = LogQueryMain | LogQueryDisabled
deriving (Eq)
dumpLogQuery :: LogQuery -> Text
dumpLogQuery = \case
LogQueryMain -> "main-query"
LogQueryDisabled -> "disabled"
data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
deriving Eq deriving Eq
@@ -173,13 +164,12 @@ toText conf =
,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig) ,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig)
,("db-tx-end", q . showTxEnd) ,("db-tx-end", q . showTxEnd)
,("db-uri", q . configDbUri) ,("db-uri", q . configDbUri)
,("jwt-aud", q . fromMaybe mempty . configJwtAudience) ,("jwt-aud", T.decodeUtf8 . LBS.toStrict . JSON.encode . maybe "" toJSON . configJwtAudience)
,("jwt-role-claim-key", q . T.intercalate mempty . fmap dumpJSPath . configJwtRoleClaimKey) ,("jwt-role-claim-key", q . T.intercalate mempty . fmap dumpJSPath . configJwtRoleClaimKey)
,("jwt-secret", q . T.decodeUtf8 . showJwtSecret) ,("jwt-secret", q . T.decodeUtf8 . showJwtSecret)
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64) ,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
,("jwt-cache-max-lifetime", show . configJwtCacheMaxLifetime) ,("jwt-cache-max-lifetime", show . configJwtCacheMaxLifetime)
,("log-level", q . dumpLogLevel . configLogLevel) ,("log-level", q . dumpLogLevel . configLogLevel)
,("log-query", q . dumpLogQuery . configLogQuery)
,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode) ,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode)
,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive) ,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive)
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri) ,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
@@ -190,8 +180,8 @@ toText conf =
,("server-timing-enabled", T.toLower . show . configServerTimingEnabled) ,("server-timing-enabled", T.toLower . show . configServerTimingEnabled)
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket) ,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
,("server-unix-socket-mode", q . T.pack . showSocketMode) ,("server-unix-socket-mode", q . T.pack . showSocketMode)
,("admin-server-host", q . configAdminServerHost)
,("admin-server-port", maybe "\"\"" show . configAdminServerPort) ,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
,("admin-server-config-enabled", T.toLower . show . configAdminServerConfigEnabled)
] ]
-- quote all app.settings -- quote all app.settings
@@ -235,18 +225,16 @@ readAppConfig dbSettings optPath prevDbUri roleSettings roleIsolationLvl = do
Left err -> Left err ->
return . Left $ "Error in config " <> err return . Left $ "Error in config " <> err
Right parsedConfig -> Right parsedConfig ->
mapLeft show <$> decodeLoadFiles parsedConfig Right <$> decodeLoadFiles parsedConfig
where where
-- Both C.ParseError and IOError are shown here -- Both C.ParseError and IOError are shown here
loadConfig :: FilePath -> IO (Either SomeException C.Config) loadConfig :: FilePath -> IO (Either SomeException C.Config)
loadConfig = try . C.load loadConfig = try . C.load
decodeLoadFiles :: AppConfig -> IO (Either IOException AppConfig) decodeLoadFiles :: AppConfig -> IO AppConfig
decodeLoadFiles parsedConfig = try $ decodeLoadFiles parsedConfig =
decodeJWKS =<< decodeJWKS <$>
decodeSecret =<< (decodeSecret =<< readSecretFile =<< readDbUriFile prevDbUri parsedConfig)
readSecretFile =<<
readDbUriFile prevDbUri parsedConfig
parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> RoleIsolationLvl -> C.Parser C.Config AppConfig parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> RoleIsolationLvl -> C.Parser C.Config AppConfig
parser optPath env dbSettings roleSettings roleIsolationLvl = parser optPath env dbSettings roleSettings roleIsolationLvl =
@@ -256,8 +244,8 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (fmap encodeUtf8 <$> optString "db-anon-role") <*> (fmap encodeUtf8 <$> optString "db-anon-role")
<*> (fromMaybe "pgrst" <$> optString "db-channel") <*> (fromMaybe "pgrst" <$> optString "db-channel")
<*> (fromMaybe True <$> optBool "db-channel-enabled") <*> (fromMaybe True <$> optBool "db-channel-enabled")
<*> (maybe ["public"] splitOnCommasEmptyable <$> optStringEmptyable "db-extra-search-path") <*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
<*> (maybe defaultHoistedAllowList splitOnCommas <$> optString "db-hoisted-tx-settings") <*> (maybe defaultHoistedAllowList splitOnCommas <$> optValue "db-hoisted-tx-settings")
<*> optWithAlias (optInt "db-max-rows") <*> optWithAlias (optInt "db-max-rows")
(optInt "max-rows") (optInt "max-rows")
<*> (fromMaybe False <$> optBool "db-plan-enabled") <*> (fromMaybe False <$> optBool "db-plan-enabled")
@@ -272,8 +260,8 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (fromMaybe True <$> optBool "db-prepared-statements") <*> (fromMaybe True <$> optBool "db-prepared-statements")
<*> (fmap toQi <$> optWithAlias (optString "db-root-spec") <*> (fmap toQi <$> optWithAlias (optString "db-root-spec")
(optString "root-spec")) (optString "root-spec"))
<*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optString "db-schemas") <*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optValue "db-schemas")
(optString "db-schema")) (optValue "db-schema"))
<*> (fromMaybe True <$> optBool "db-config") <*> (fromMaybe True <$> optBool "db-config")
<*> (fmap toQi <$> optString "db-pre-config") <*> (fmap toQi <$> optString "db-pre-config")
<*> parseTxEnd "db-tx-end" snd <*> parseTxEnd "db-tx-end" snd
@@ -281,7 +269,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (fromMaybe "postgresql://" <$> optString "db-uri") <*> (fromMaybe "postgresql://" <$> optString "db-uri")
<*> pure optPath <*> pure optPath
<*> pure Nothing <*> pure Nothing
<*> optStringOrURI "jwt-aud" <*> parseJwtAudience "jwt-aud"
<*> parseRoleClaimKey "jwt-role-claim-key" "role-claim-key" <*> parseRoleClaimKey "jwt-role-claim-key" "role-claim-key"
<*> (fmap encodeUtf8 <$> optString "jwt-secret") <*> (fmap encodeUtf8 <$> optString "jwt-secret")
<*> (fromMaybe False <$> optWithAlias <*> (fromMaybe False <$> optWithAlias
@@ -289,20 +277,18 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
(optBool "secret-is-base64")) (optBool "secret-is-base64"))
<*> (fromMaybe 0 <$> optInt "jwt-cache-max-lifetime") <*> (fromMaybe 0 <$> optInt "jwt-cache-max-lifetime")
<*> parseLogLevel "log-level" <*> parseLogLevel "log-level"
<*> parseLogQuery "log-query"
<*> parseOpenAPIMode "openapi-mode" <*> parseOpenAPIMode "openapi-mode"
<*> (fromMaybe False <$> optBool "openapi-security-active") <*> (fromMaybe False <$> optBool "openapi-security-active")
<*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri" <*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
<*> parseCORSAllowedOrigins "server-cors-allowed-origins" <*> parseCORSAllowedOrigins "server-cors-allowed-origins"
<*> (defaultServerHost <$> optString "server-host") <*> (fromMaybe "!4" <$> optString "server-host")
<*> parseServerPort "server-port" <*> (fromMaybe 3000 <$> optInt "server-port")
<*> (fmap (CI.mk . encodeUtf8) <$> optString "server-trace-header") <*> (fmap (CI.mk . encodeUtf8) <$> optString "server-trace-header")
<*> (fromMaybe False <$> optBool "server-timing-enabled") <*> (fromMaybe False <$> optBool "server-timing-enabled")
<*> (fmap T.unpack <$> optString "server-unix-socket") <*> (fmap T.unpack <$> optString "server-unix-socket")
<*> parseSocketFileMode "server-unix-socket-mode" <*> parseSocketFileMode "server-unix-socket-mode"
<*> (defaultServerHost <$> optWithAlias (optString "admin-server-host") <*> optInt "admin-server-port"
(optString "server-host")) <*> (fromMaybe False <$> optBool "admin-server-config-enabled")
<*> parseAdminServerPort "admin-server-port"
<*> pure roleSettings <*> pure roleSettings
<*> pure roleIsolationLvl <*> pure roleIsolationLvl
<*> optInt "internal-schema-cache-sleep" <*> optInt "internal-schema-cache-sleep"
@@ -314,17 +300,6 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
fromEnv = M.mapKeys fromJust $ M.filterWithKey (\k _ -> isJust k) $ M.mapKeys normalize env fromEnv = M.mapKeys fromJust $ M.filterWithKey (\k _ -> isJust k) $ M.mapKeys normalize env
normalize k = ("app.settings." <>) <$> T.stripPrefix "PGRST_APP_SETTINGS_" (toS k) normalize k = ("app.settings." <>) <$> T.stripPrefix "PGRST_APP_SETTINGS_" (toS k)
parseServerPort :: C.Key -> C.Parser C.Config Int
parseServerPort k = fromMaybe 3000 <$> optInt k
parseAdminServerPort :: C.Key -> C.Parser C.Config (Maybe Int)
parseAdminServerPort k = do
serverPort <- parseServerPort "server-port"
optInt k >>= \case
Nothing -> pure Nothing
Just asp | asp == serverPort -> fail "admin-server-port cannot be the same as server-port"
| otherwise -> pure $ Just asp
parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
parseSocketFileMode k = parseSocketFileMode k =
optString k >>= \case optString k >>= \case
@@ -354,6 +329,14 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
Just val | isMalformedProxyUri val -> fail "Malformed proxy uri, a correct example: https://example.com:8443/basePath" Just val | isMalformedProxyUri val -> fail "Malformed proxy uri, a correct example: https://example.com:8443/basePath"
| otherwise -> pure $ Just val | otherwise -> pure $ Just val
parseJwtAudience :: C.Key -> C.Parser C.Config (Maybe StringOrURI)
parseJwtAudience k =
optString k >>= \case
Nothing -> pure Nothing -- no audience in config file
Just aud -> case preview stringOrUri (T.unpack aud) of
Nothing -> fail "Invalid Jwt audience. Check your configuration."
aud' -> pure aud'
parseLogLevel :: C.Key -> C.Parser C.Config LogLevel parseLogLevel :: C.Key -> C.Parser C.Config LogLevel
parseLogLevel k = parseLogLevel k =
optString k >>= \case optString k >>= \case
@@ -365,14 +348,6 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
Just "debug" -> pure LogDebug Just "debug" -> pure LogDebug
Just _ -> fail "Invalid logging level. Check your configuration." Just _ -> fail "Invalid logging level. Check your configuration."
parseLogQuery :: C.Key -> C.Parser C.Config LogQuery
parseLogQuery k =
optString k >>= \case
Nothing -> pure LogQueryDisabled
Just "disabled" -> pure LogQueryDisabled
Just "main-query" -> pure LogQueryMain
Just _ -> fail "Invalid SQL logging value. Check your configuration."
parseTxEnd :: C.Key -> ((Bool, Bool) -> Bool) -> C.Parser C.Config Bool parseTxEnd :: C.Key -> ((Bool, Bool) -> Bool) -> C.Parser C.Config Bool
parseTxEnd k f = parseTxEnd k f =
optString k >>= \case optString k >>= \case
@@ -404,22 +379,8 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
optString :: C.Key -> C.Parser C.Config (Maybe Text) optString :: C.Key -> C.Parser C.Config (Maybe Text)
optString k = mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText optString k = mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
optStringEmptyable :: C.Key -> C.Parser C.Config (Maybe Text) optValue :: C.Key -> C.Parser C.Config (Maybe C.Value)
optStringEmptyable k = overrideFromDbOrEnvironment C.optional k coerceText optValue k = overrideFromDbOrEnvironment C.optional k identity
optStringOrURI :: C.Key -> C.Parser C.Config (Maybe Text)
optStringOrURI k = do
stringOrURI <- mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
-- If the string contains ':' then it should
-- be a valid URI according to RFC 3986
case stringOrURI of
Just s -> if T.isInfixOf ":" s then validateURI s else return (Just s)
Nothing -> return Nothing
where
validateURI :: Text -> C.Parser C.Config (Maybe Text)
validateURI s = if isURI (T.unpack s)
then return $ Just s
else fail "jwt-aud should be a string or a valid URI"
optInt :: (Read i, Integral i) => C.Key -> C.Parser C.Config (Maybe i) optInt :: (Read i, Integral i) => C.Key -> C.Parser C.Config (Maybe i)
optInt k = join <$> overrideFromDbOrEnvironment C.optional k coerceInt optInt k = join <$> overrideFromDbOrEnvironment C.optional k coerceInt
@@ -459,18 +420,12 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
Nothing -> (> 0) <$> (readMaybe s :: Maybe Integer) Nothing -> (> 0) <$> (readMaybe s :: Maybe Integer)
coerceBool _ = Nothing coerceBool _ = Nothing
splitOnCommas :: Text -> [Text] splitOnCommas :: C.Value -> [Text]
splitOnCommas s = T.strip <$> T.splitOn "," s splitOnCommas (C.String s) = T.strip <$> T.splitOn "," s
splitOnCommas _ = []
splitOnCommasEmptyable :: Text -> [Text]
splitOnCommasEmptyable "" = []
splitOnCommasEmptyable s = T.strip <$> T.splitOn "," s
defaultHoistedAllowList = ["statement_timeout","plan_filter.statement_cost_limit","default_transaction_isolation"] defaultHoistedAllowList = ["statement_timeout","plan_filter.statement_cost_limit","default_transaction_isolation"]
defaultServerHost :: Maybe Text -> Text
defaultServerHost = fromMaybe "!4"
-- | Read the JWT secret from a file if configJwtSecret is actually a -- | Read the JWT secret from a file if configJwtSecret is actually a
-- filepath(has @ as its prefix). To check if the JWT secret is provided is -- filepath(has @ as its prefix). To check if the JWT secret is provided is
-- in fact a file path, it must be decoded as 'Text' to be processed. -- in fact a file path, it must be decoded as 'Text' to be processed.
@@ -495,30 +450,24 @@ decodeSecret conf@AppConfig{..} =
decodeB64 = B64.decode . encodeUtf8 . T.strip . replaceUrlChars . decodeUtf8 decodeB64 = B64.decode . encodeUtf8 . T.strip . replaceUrlChars . decodeUtf8
replaceUrlChars = T.replace "_" "/" . T.replace "-" "+" . T.replace "." "=" replaceUrlChars = T.replace "_" "/" . T.replace "-" "+" . T.replace "." "="
-- | Parse `jwt-secret` configuration option and turn into a JWKS. -- | Parse `jwt-secret` configuration option and turn into a JWKSet.
-- --
-- There are three ways to specify `jwt-secret`: text secret, JSON Web Key -- There are three ways to specify `jwt-secret`: text secret, JSON Web Key
-- (JWK), or JSON Web Key Set (JWKS). The first two are converted into a JwkSet -- (JWK), or JSON Web Key Set (JWKS). The first two are converted into a JWKSet
-- with one key and the last is converted as is. -- with one key and the last is converted as is.
decodeJWKS :: AppConfig -> IO AppConfig decodeJWKS :: AppConfig -> AppConfig
decodeJWKS conf = do decodeJWKS conf =
jwks <- case configJwtSecret conf of conf { configJWKS = parseSecret <$> configJwtSecret conf }
Just s -> either fail (pure . Just) $ parseSecret s
Nothing -> pure Nothing
return $ conf { configJWKS = jwks }
parseSecret :: ByteString -> Either [Char] JwkSet parseSecret :: ByteString -> JWKSet
parseSecret bytes = parseSecret bytes =
case maybeJWKSet of fromMaybe (maybe secret (\jwk' -> JWT.JWKSet [jwk']) maybeJWK)
Just jwk -> Right jwk maybeJWKSet
Nothing -> maybe validateSecret (\jwk' -> Right $ JWT.JwkSet [jwk']) maybeJWK
where where
maybeJWKSet = JSON.decodeStrict bytes :: Maybe JwkSet maybeJWKSet = JSON.decodeStrict bytes :: Maybe JWKSet
maybeJWK = JSON.decodeStrict bytes :: Maybe Jwk maybeJWK = JSON.decodeStrict bytes :: Maybe JWK
secret = JWT.JwkSet [JWT.SymmetricJwk bytes Nothing (Just JWT.Sig) (Just $ JWT.Signed JWT.HS256)] secret = JWT.JWKSet [JWT.fromKeyMaterial keyMaterial]
validateSecret keyMaterial = JWT.OctKeyMaterial . JWT.OctKeyParameters $ JOSE.Base64Octets bytes
| BS.length bytes < 32 = Left "The JWT secret must be at least 32 characters long."
| otherwise = Right secret
-- | Read database uri from a separate file if `db-uri` is a filepath. -- | Read database uri from a separate file if `db-uri` is a filepath.
readDbUriFile :: Maybe Text -> AppConfig -> IO AppConfig readDbUriFile :: Maybe Text -> AppConfig -> IO AppConfig
+13 -14
View File
@@ -24,7 +24,7 @@ import qualified Hasql.Statement as SQL
import qualified Hasql.Transaction as SQL import qualified Hasql.Transaction as SQL
import qualified Hasql.Transaction.Sessions as SQL import qualified Hasql.Transaction.Sessions as SQL
import NeatInterpolation (trimming) import Text.InterpolatedString.Perl6 (q, qc)
import Protolude import Protolude
@@ -95,7 +95,7 @@ queryDbSettings preConfFunc prepared =
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
transaction SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings prepared transaction SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings prepared
where where
sql = encodeUtf8 [trimming| sql = [qc|
WITH WITH
role_setting AS ( role_setting AS (
SELECT setdatabase as database, SELECT setdatabase as database,
@@ -109,25 +109,25 @@ queryDbSettings preConfFunc prepared =
substr(setting, 1, strpos(setting, '=') - 1) as k, substr(setting, 1, strpos(setting, '=') - 1) as k,
substr(setting, strpos(setting, '=') + 1) as v substr(setting, strpos(setting, '=') + 1) as v
FROM role_setting FROM role_setting
${preConfigF} {preConfigF}
) )
SELECT DISTINCT ON (key) SELECT DISTINCT ON (key)
replace(k, '${prefix}', '') AS key, replace(k, '{prefix}', '') AS key,
v AS value v AS value
FROM kv_settings FROM kv_settings
WHERE k = ANY($$1) AND v IS NOT NULL WHERE k = ANY($1) AND v IS NOT NULL
ORDER BY key, database DESC NULLS LAST; ORDER BY key, database DESC NULLS LAST;
|] |]
preConfigF = case preConfFunc of preConfigF = case preConfFunc of
Nothing -> mempty Nothing -> mempty
Just func -> [trimming| Just func -> [qc|
UNION UNION
SELECT SELECT
null as database, null as database,
x as k, x as k,
current_setting(x, true) as v current_setting(x, true) as v
FROM unnest($$1) x FROM unnest($1) x
JOIN ${func}() _ ON TRUE JOIN {func}() _ ON TRUE
|]::Text |]::Text
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
@@ -136,7 +136,7 @@ queryRoleSettings pgVer prepared =
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in 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 transaction SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) prepared
where where
sql = encodeUtf8 [trimming| sql = [q|
with with
role_setting as ( role_setting as (
select r.rolname, unnest(r.rolconfig) as setting select r.rolname, unnest(r.rolconfig) as setting
@@ -161,15 +161,14 @@ queryRoleSettings pgVer prepared =
i.value as iso_lvl, i.value as iso_lvl,
coalesce(array_agg(row(kv.key, kv.value)) filter (where key <> 'default_transaction_isolation'), '{}') as role_settings coalesce(array_agg(row(kv.key, kv.value)) filter (where key <> 'default_transaction_isolation'), '{}') as role_settings
from kv_settings kv from kv_settings kv
join pg_settings ps on ps.name = kv.key and (ps.context = 'user' ${hasParameterPrivilege}) 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 left join iso_setting i on i.rolname = kv.rolname
group by kv.rolname, i.value; group by kv.rolname, i.value;
|] |]
hasParameterPrivilege
| pgVer >= pgVersion150 = "or has_parameter_privilege(current_user::regrole::oid, ps.name, 'set')"
| otherwise = ""
processRows :: [(Text, Maybe Text, [(Text, Text)])] -> (RoleSettings, RoleIsolationLvl) processRows :: [(Text, Maybe Text, [(Text, Text)])] -> (RoleSettings, RoleIsolationLvl)
processRows rs = processRows rs =
let let
+15 -64
View File
@@ -1,8 +1,6 @@
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
module PostgREST.Config.JSPath module PostgREST.Config.JSPath
( JSPath ( JSPath
, JSPathExp(..) , JSPathExp(..)
, FilterExp(..)
, dumpJSPath , dumpJSPath
, pRoleClaimKey , pRoleClaimKey
) where ) where
@@ -16,37 +14,18 @@ import Text.Read (read)
import Protolude import Protolude
-- | full jspath, e.g. .property[0].attr.detail[?(@ == "role1")] -- | full jspath, e.g. .property[0].attr.detail
type JSPath = [JSPathExp] type JSPath = [JSPathExp]
-- NOTE: We only accept one JSPFilter expr (at the end of input) -- | jspath expression, e.g. .property, .property[0] or ."property-dash"
-- | jspath expression
data JSPathExp data JSPathExp
= JSPKey Text -- .property or ."property-dash" = JSPKey Text
| JSPIdx Int -- [0] | JSPIdx Int
| JSPFilter FilterExp -- [?(@ == "match")]
data FilterExp
= EqualsCond Text
| NotEqualsCond Text
| StartsWithCond Text
| EndsWithCond Text
| ContainsCond Text
dumpJSPath :: JSPathExp -> Text dumpJSPath :: JSPathExp -> Text
-- TODO: this needs to be quoted properly for special chars -- TODO: this needs to be quoted properly for special chars
dumpJSPath (JSPKey k) = "." <> show k dumpJSPath (JSPKey k) = "." <> show k
dumpJSPath (JSPIdx i) = "[" <> show i <> "]" dumpJSPath (JSPIdx i) = "[" <> show i <> "]"
dumpJSPath (JSPFilter cond) = "[?(@" <> expr <> ")]"
where
expr =
case cond of
EqualsCond text -> " == " <> show text
NotEqualsCond text -> " != " <> show text
StartsWithCond text -> " ^== " <> show text
EndsWithCond text -> " ==^ " <> show text
ContainsCond text -> " *== " <> show text
-- Used for the config value "role-claim-key" -- Used for the config value "role-claim-key"
pRoleClaimKey :: Text -> Either Text JSPath pRoleClaimKey :: Text -> Either Text JSPath
@@ -54,47 +33,19 @@ pRoleClaimKey selStr =
mapLeft show $ P.parse pJSPath ("failed to parse role-claim-key value (" <> toS selStr <> ")") (toS selStr) mapLeft show $ P.parse pJSPath ("failed to parse role-claim-key value (" <> toS selStr <> ")") (toS selStr)
pJSPath :: P.Parser JSPath pJSPath :: P.Parser JSPath
pJSPath = P.many1 pJSPathExp <* P.eof pJSPath = toJSPath <$> (period *> pPath `P.sepBy` period <* P.eof)
where
toJSPath :: [(Text, Maybe Int)] -> JSPath
toJSPath = concatMap (\(key, idx) -> JSPKey key : maybeToList (JSPIdx <$> idx))
period = P.char '.' <?> "period (.)"
pPath :: P.Parser (Text, Maybe Int)
pPath = (,) <$> pJSPKey <*> P.optionMaybe pJSPIdx
pJSPathExp :: P.Parser JSPathExp pJSPKey :: P.Parser Text
pJSPathExp = pJSPKey <|> pJSPFilter <|> pJSPIdx pJSPKey = toS <$> P.many1 (P.alphaNum <|> P.oneOf "_$@") <|> pQuotedValue <?> "attribute name [a..z0..9_$@])"
pJSPKey :: P.Parser JSPathExp pJSPIdx :: P.Parser Int
pJSPKey = do pJSPIdx = P.char '[' *> (read <$> P.many1 P.digit) <* P.char ']' <?> "array index [0..n]"
P.char '.'
val <- toS <$> P.many1 (P.alphaNum <|> P.oneOf "_$@") <|> pQuotedValue
return (JSPKey val) <?> "pJSPKey: JSPath attribute key"
pJSPIdx :: P.Parser JSPathExp
pJSPIdx = do
P.char '['
num <- read <$> P.many1 P.digit
P.char ']'
return (JSPIdx num) <?> "pJSPIdx: JSPath array index"
pJSPFilter :: P.Parser JSPathExp
pJSPFilter = do
P.try $ P.string "[?("
condition <- pFilterConditionParser
P.char ')'
P.char ']'
P.eof -- this should be the last jspath expression
return (JSPFilter condition) <?> "pJSPFilter: JSPath filter exp"
pFilterConditionParser :: P.Parser FilterExp
pFilterConditionParser = do
P.char '@'
P.spaces
filt <- matchOperator
P.spaces
filt <$> pQuotedValue
where
matchOperator =
P.try (P.string "==^" $> EndsWithCond)
<|> P.try (P.string "==" $> EqualsCond)
<|> P.try (P.string "!=" $> NotEqualsCond)
<|> P.try (P.string "^==" $> StartsWithCond)
<|> P.try (P.string "*==" $> ContainsCond)
pQuotedValue :: P.Parser Text pQuotedValue :: P.Parser Text
pQuotedValue = toS <$> (P.char '"' *> P.many (P.noneOf "\"") <* P.char '"') pQuotedValue = toS <$> (P.char '"' *> P.many (P.noneOf "\"") <* P.char '"')
+30 -5
View File
@@ -3,10 +3,17 @@
module PostgREST.Config.PgVersion module PostgREST.Config.PgVersion
( PgVersion(..) ( PgVersion(..)
, minimumPgVersion , minimumPgVersion
, pgVersion96
, pgVersion100
, pgVersion109
, pgVersion110
, pgVersion112
, pgVersion114
, pgVersion120
, pgVersion121
, pgVersion130 , pgVersion130
, pgVersion140 , pgVersion140
, pgVersion150 , pgVersion150
, pgVersion170
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
@@ -26,7 +33,28 @@ instance Ord PgVersion where
-- | Tells the minimum PostgreSQL version required by this version of PostgREST -- | Tells the minimum PostgreSQL version required by this version of PostgREST
minimumPgVersion :: PgVersion minimumPgVersion :: PgVersion
minimumPgVersion = pgVersion121 minimumPgVersion = pgVersion96
pgVersion96 :: PgVersion
pgVersion96 = PgVersion 90600 "9.6" "9.6"
pgVersion100 :: PgVersion
pgVersion100 = PgVersion 100000 "10" "10"
pgVersion109 :: PgVersion
pgVersion109 = PgVersion 100009 "10.9" "10.9"
pgVersion110 :: PgVersion
pgVersion110 = PgVersion 110000 "11.0" "11.0"
pgVersion112 :: PgVersion
pgVersion112 = PgVersion 110002 "11.2" "11.2"
pgVersion114 :: PgVersion
pgVersion114 = PgVersion 110004 "11.4" "11.4"
pgVersion120 :: PgVersion
pgVersion120 = PgVersion 120000 "12.0" "12.0"
pgVersion121 :: PgVersion pgVersion121 :: PgVersion
pgVersion121 = PgVersion 120001 "12.1" "12.1" pgVersion121 = PgVersion 120001 "12.1" "12.1"
@@ -39,6 +67,3 @@ pgVersion140 = PgVersion 140000 "14.0" "14.0"
pgVersion150 :: PgVersion pgVersion150 :: PgVersion
pgVersion150 = PgVersion 150000 "15.0" "15.0" pgVersion150 = PgVersion 150000 "15.0" "15.0"
pgVersion170 :: PgVersion
pgVersion170 = PgVersion 170000 "17.0" "17.0"
+307 -427
View File
@@ -8,27 +8,21 @@ Description : PostgREST error HTTP responses
module PostgREST.Error module PostgREST.Error
( errorResponseFor ( errorResponseFor
, ApiRequestError(..) , ApiRequestError(..)
, QPError(..)
, RangeError(..)
, SchemaCacheError(..)
, PgError(..) , PgError(..)
, Error(..) , Error(..)
, JwtError (..)
, JwtDecodeError(..)
, JwtClaimsError(..)
, errorPayload , errorPayload
, status , status
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.CaseInsensitive as CI import qualified Data.CaseInsensitive as CI
import qualified Data.FuzzySet as Fuzzy import qualified Data.FuzzySet as Fuzzy
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import qualified Data.Map.Internal as M import qualified Data.Map.Internal as M
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.Text.Encoding as T import qualified Data.Text.Encoding as T
import qualified Data.Text.Encoding.Error as T
import qualified Hasql.Pool as SQL import qualified Hasql.Pool as SQL
import qualified Hasql.Session as SQL import qualified Hasql.Session as SQL
import qualified Network.HTTP.Types.Status as HTTP import qualified Network.HTTP.Types.Status as HTTP
@@ -38,8 +32,12 @@ import Network.Wai (Response, responseLBS)
import Network.HTTP.Types.Header (Header) import Network.HTTP.Types.Header (Header)
import PostgREST.MediaType (MediaType (..)) import PostgREST.ApiRequest.Types (ApiRequestError (..),
import qualified PostgREST.MediaType as MediaType QPError (..),
RaiseError (..),
RangeError (..))
import PostgREST.MediaType (MediaType (..))
import qualified PostgREST.MediaType as MediaType
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..), import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
Schema) Schema)
@@ -49,11 +47,10 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
RelationshipsMap) RelationshipsMap)
import PostgREST.SchemaCache.Routine (Routine (..), import PostgREST.SchemaCache.Routine (Routine (..),
RoutineParam (..)) RoutineParam (..))
import PostgREST.SchemaCache.Table (Table (..))
import Protolude import Protolude
class (ErrorBody a, JSON.ToJSON a) => PgrstError a where class (JSON.ToJSON a) => PgrstError a where
status :: a -> HTTP.Status status :: a -> HTTP.Status
headers :: a -> [Header] headers :: a -> [Header]
@@ -62,272 +59,200 @@ class (ErrorBody a, JSON.ToJSON a) => PgrstError a where
errorResponseFor :: a -> Response errorResponseFor :: a -> Response
errorResponseFor err = errorResponseFor err =
let let baseHeader = MediaType.toContentType MTApplicationJSON in
baseHeader = MediaType.toContentType MTApplicationJSON responseLBS (status err) (baseHeader : headers err) $ errorPayload err
cLHeader body = (,) "Content-Length" (show $ LBS.length body) :: Header
in
responseLBS (status err) (baseHeader : cLHeader (errorPayload err) : headers err) $ errorPayload err
class ErrorBody a where
code :: a -> Text
message :: a -> Text
details :: a -> Maybe JSON.Value
hint :: a -> Maybe JSON.Value
data ApiRequestError
= AggregatesNotAllowed
| MediaTypeError [ByteString]
| InvalidBody ByteString
| InvalidFilters
| InvalidPreferences [ByteString]
| InvalidRange RangeError
| InvalidRpcMethod ByteString
| NotEmbedded Text
| NotImplemented Text
| PutLimitNotAllowedError
| QueryParamError QPError
| RelatedOrderNotToOne Text Text
| UnacceptableFilter Text
| UnacceptableSchema Text [Text]
| UnsupportedMethod ByteString
| GucHeadersError
| GucStatusError
| PutMatchingPkError
| SingularityError Integer
| PGRSTParseError RaiseError
| MaxAffectedViolationError Integer
| InvalidResourcePath
| OpenAPIDisabled
| MaxAffectedRpcViolation
deriving Show
data QPError = QPError Text Text
deriving Show
data RaiseError
= MsgParseError ByteString
| DetParseError ByteString
| NoDetail
deriving Show
data RangeError
= NegativeLimit
| LowerGTUpper
| OutOfBounds Text Text
deriving Show
instance PgrstError ApiRequestError where instance PgrstError ApiRequestError where
status AggregatesNotAllowed{} = HTTP.status400 status AggregatesNotAllowed{} = HTTP.status400
status AmbiguousRelBetween{} = HTTP.status300
status AmbiguousRpc{} = HTTP.status300
status MediaTypeError{} = HTTP.status406 status MediaTypeError{} = HTTP.status406
status InvalidBody{} = HTTP.status400 status InvalidBody{} = HTTP.status400
status InvalidFilters = HTTP.status405 status InvalidFilters = HTTP.status405
status InvalidPreferences{} = HTTP.status400 status InvalidPreferences{} = HTTP.status400
status InvalidRpcMethod{} = HTTP.status405 status InvalidRpcMethod{} = HTTP.status405
status InvalidRange{} = HTTP.status416 status InvalidRange{} = HTTP.status416
status NotFound = HTTP.status404
status NoRelBetween{} = HTTP.status400
status NoRpc{} = HTTP.status404
status NotEmbedded{} = HTTP.status400 status NotEmbedded{} = HTTP.status400
status NotImplemented{} = HTTP.status400
status PutLimitNotAllowedError = HTTP.status400 status PutLimitNotAllowedError = HTTP.status400
status QueryParamError{} = HTTP.status400 status QueryParamError{} = HTTP.status400
status RelatedOrderNotToOne{} = HTTP.status400 status RelatedOrderNotToOne{} = HTTP.status400
status SpreadNotToOne{} = HTTP.status400
status UnacceptableFilter{} = HTTP.status400 status UnacceptableFilter{} = HTTP.status400
status UnacceptableSchema{} = HTTP.status406 status UnacceptableSchema{} = HTTP.status406
status UnsupportedMethod{} = HTTP.status405 status UnsupportedMethod{} = HTTP.status405
status LimitNoOrderError = HTTP.status400
status ColumnNotFound{} = HTTP.status400
status GucHeadersError = HTTP.status500 status GucHeadersError = HTTP.status500
status GucStatusError = HTTP.status500 status GucStatusError = HTTP.status500
status OffLimitsChangesError{} = HTTP.status400
status PutMatchingPkError = HTTP.status400 status PutMatchingPkError = HTTP.status400
status SingularityError{} = HTTP.status406 status SingularityError{} = HTTP.status406
status PGRSTParseError{} = HTTP.status500 status PGRSTParseError{} = HTTP.status500
status MaxAffectedViolationError{} = HTTP.status400 status MaxAffectedViolationError{} = HTTP.status400
status InvalidResourcePath = HTTP.status404
status OpenAPIDisabled = HTTP.status404
status MaxAffectedRpcViolation = HTTP.status400
headers _ = mempty headers _ = mempty
-- Error codes: toJsonPgrstError :: ErrorCode -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value
-- toJsonPgrstError code msg details hint = JSON.object [
-- Error codes are grouped by common modules or characteristics "code" .= code
-- New group of errors will be added at the end of all the groups and will have the next prefix in the sequence , "message" .= msg
-- Keep the "PGRST" prefix in every code for an easier search/grep , "details" .= details
-- They are grouped as following: , "hint" .= hint
-- ]
-- PGRST0xx -> Connection Error
-- PGRST1xx -> ApiRequest Error
-- PGRST2xx -> SchemaCache Error
-- PGRST3xx -> JWT authentication Error
-- PGRSTXxx -> Internal Hasql Error
instance ErrorBody ApiRequestError where
-- CODE: Text
code QueryParamError{} = "PGRST100"
code InvalidRpcMethod{} = "PGRST101"
code InvalidBody{} = "PGRST102"
code InvalidRange{} = "PGRST103"
-- code ParseRequestError = "PGRST104" -- no longer used
code InvalidFilters = "PGRST105"
code UnacceptableSchema{} = "PGRST106"
code MediaTypeError{} = "PGRST107"
code NotEmbedded{} = "PGRST108"
-- code LimitNoOrderError = "PGRST109" -- no longer used
-- code OffLimitsChangesError = "PGRST110" -- no longer used
code GucHeadersError = "PGRST111"
code GucStatusError = "PGRST112"
-- code BinaryFieldError = "PGRST113" -- no longer used
code PutLimitNotAllowedError = "PGRST114"
code PutMatchingPkError = "PGRST115"
code SingularityError{} = "PGRST116"
code UnsupportedMethod{} = "PGRST117"
code RelatedOrderNotToOne{} = "PGRST118"
-- code SpreadNotToOne = "PGRST109" -- no longer used
code UnacceptableFilter{} = "PGRST120"
code PGRSTParseError{} = "PGRST121"
code InvalidPreferences{} = "PGRST122"
code AggregatesNotAllowed = "PGRST123"
code MaxAffectedViolationError{} = "PGRST124"
code InvalidResourcePath = "PGRST125"
code OpenAPIDisabled = "PGRST126"
code NotImplemented{} = "PGRST127"
code MaxAffectedRpcViolation = "PGRST128"
-- MESSAGE: Text
message (QueryParamError (QPError msg _)) = msg
message (InvalidRpcMethod method) = "Cannot use the " <> T.decodeUtf8 method <> " method on RPC"
message (InvalidBody errorMessage) = T.decodeUtf8 errorMessage
message (InvalidRange _) = "Requested range not satisfiable"
message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators"
message (UnacceptableSchema sch _) = "Invalid schema: " <> sch
message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)
message (NotEmbedded resource) = "'" <> resource <> "' is not an embedded resource in this request"
message GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value"
message GucStatusError = "response.status guc must be a valid status code"
message PutLimitNotAllowedError = "limit/offset querystring parameters are not allowed for PUT"
message PutMatchingPkError = "Payload values do not match URL in primary key column(s)"
message (SingularityError _) = "Cannot coerce the result to a single JSON object"
message (UnsupportedMethod method) = "Unsupported HTTP method: " <> T.decodeUtf8 method
message (RelatedOrderNotToOne _ target) = "A related order on '" <> target <> "' is not possible"
message (UnacceptableFilter target) = "Bad operator on the '" <> target <> "' embedded resource"
message (PGRSTParseError _) = "Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error"
message (InvalidPreferences _) = "Invalid preferences given with handling=strict"
message AggregatesNotAllowed = "Use of aggregate functions is not allowed"
message (MaxAffectedViolationError _) = "Query result exceeds max-affected preference constraint"
message InvalidResourcePath = "Invalid path specified in request URL"
message OpenAPIDisabled = "Root endpoint metadata is disabled"
message (NotImplemented _) = "Feature not implemented"
message MaxAffectedRpcViolation = "Function must return SETOF or TABLE when max-affected preference is used with handling=strict"
-- DETAILS: Maybe JSON.Value
details (QueryParamError (QPError _ dets)) = Just $ JSON.String dets
details (InvalidRange rangeError) = 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."
details (SingularityError n) = Just $ JSON.String $ T.unwords ["The result contains", show n, "rows"]
details (RelatedOrderNotToOne origin target) = Just $ JSON.String $ "'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship"
details (UnacceptableFilter _) = Just "Only is null or not is null filters are allowed on embedded resources"
details (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorDetails raiseErr
details (InvalidPreferences prefs) = Just $ JSON.String $ T.decodeUtf8 ("Invalid preferences: " <> BS.intercalate ", " prefs)
details (MaxAffectedViolationError n) = Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"]
details (NotImplemented details') = Just $ JSON.String details'
details _ = Nothing
-- HINT: Maybe JSON.Value
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
hint (UnacceptableSchema _ schemas) = Just $ JSON.String $ "Only the following schemas are exposed: " <> T.intercalate ", " schemas
hint _ = Nothing
instance JSON.ToJSON ApiRequestError where instance JSON.ToJSON ApiRequestError where
toJSON err = toJsonPgrstError toJSON (QueryParamError (QPError message details)) = toJsonPgrstError
(code err) (message err) (details err) (hint err) ApiRequestErrorCode00 message (Just (JSON.String details)) Nothing
data SchemaCacheError toJSON (InvalidRpcMethod method) = toJsonPgrstError
= AmbiguousRelBetween Text Text [Relationship] ApiRequestErrorCode01 ("Cannot use the " <> T.decodeUtf8 method <> " method on RPC") Nothing Nothing
| AmbiguousRpc [Routine]
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
| ColumnNotFound Text Text
| TableNotFound Text Text [Table]
deriving Show
instance PgrstError SchemaCacheError where toJSON (InvalidBody errorMessage) = toJsonPgrstError
status AmbiguousRelBetween{} = HTTP.status300 ApiRequestErrorCode02 (T.decodeUtf8 errorMessage) Nothing Nothing
status AmbiguousRpc{} = HTTP.status300
status NoRelBetween{} = HTTP.status400
status NoRpc{} = HTTP.status404
status ColumnNotFound{} = HTTP.status400
status TableNotFound{} = HTTP.status404
headers _ = mempty 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
instance ErrorBody SchemaCacheError where toJSON InvalidFilters = toJsonPgrstError
code NoRelBetween{} = "PGRST200" ApiRequestErrorCode05 "Filters must include all and only primary key columns with 'eq' operators" Nothing Nothing
code AmbiguousRelBetween{} = "PGRST201"
code NoRpc{} = "PGRST202"
code AmbiguousRpc{} = "PGRST203"
code ColumnNotFound{} = "PGRST204"
code TableNotFound{} = "PGRST205"
message (NoRelBetween parent child _ _ _) = "Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache" toJSON (UnacceptableSchema schemas) = toJsonPgrstError
message (AmbiguousRelBetween parent child _) = "Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'" ApiRequestErrorCode06 ("The schema must be one of the following: " <> T.intercalate ", " schemas) Nothing Nothing
message (NoRpc schema procName argumentKeys contentType isInvPost _ _) = "Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache"
where toJSON (MediaTypeError cts) = toJsonPgrstError
onlySingleParams = isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream] ApiRequestErrorCode07 ("None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)) Nothing Nothing
func = schema <> "." <> procName
toJSON NotFound = JSON.object []
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 LimitNoOrderError = toJsonPgrstError
ApiRequestErrorCode09 "A 'limit' was applied without an explicit 'order'" Nothing (Just "Apply an 'order' using unique column(s)")
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 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 GucStatusError = toJsonPgrstError
ApiRequestErrorCode12 "response.status guc must be a valid status code" Nothing Nothing
toJSON PutLimitNotAllowedError = toJsonPgrstError
ApiRequestErrorCode14 "limit/offset querystring parameters are not allowed for PUT" Nothing Nothing
toJSON PutMatchingPkError = toJsonPgrstError
ApiRequestErrorCode15 "Payload values do not match URL in primary key column(s)" Nothing Nothing
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 raiseErr) = toJsonPgrstError
ApiRequestErrorCode21
"Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error"
(Just $ JSON.String $ pgrstParseErrorDetails raiseErr)
(Just $ JSON.String $ pgrstParseErrorHint raiseErr)
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 (MaxAffectedViolationError n) = toJsonPgrstError
ApiRequestErrorCode24
"Query result exceeds max-affected preference constraint"
(Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"])
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 (NoRpc schema procName argumentKeys hasPreferSingleObject contentType isInvPost allProcs overloadedProcs) =
let func = schema <> "." <> procName
prms = T.intercalate ", " argumentKeys prms = T.intercalate ", " argumentKeys
prmsMsg = "(" <> prms <> ")" prmsMsg = "(" <> prms <> ")"
fmtPrms p = if null argumentKeys then " without parameters" else p
message (AmbiguousRpc procs) = "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]
message (ColumnNotFound rel col) = "Could not find the '" <> col <> "' column of '" <> rel <> "' in the schema cache"
message (TableNotFound schemaName relName _) = "Could not find the table '" <> schemaName <> "." <> relName <> "' in the schema cache"
details (NoRelBetween parent child embedHint schema _) = 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."
details (AmbiguousRelBetween _ _ rels) = Just $ JSON.toJSONList (compressedRel <$> rels)
details (NoRpc schema procName argumentKeys contentType isInvPost _ _) =
Just $ JSON.String $ "Searched for the function " <> func <>
(case (isInvPost, contentType) of
(True, MTTextPlain) -> " with a single unnamed text parameter"
(True, MTTextXML) -> " with a single unnamed xml parameter"
(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."
where
func = schema <> "." <> procName
prms = T.intercalate ", " argumentKeys
prmsDet = " with parameter" <> (if length argumentKeys > 1 then "s " else " ") <> prms prmsDet = " with parameter" <> (if length argumentKeys > 1 then "s " else " ") <> prms
fmtPrms p = if null argumentKeys then " without parameters" else p fmtPrms p = if null argumentKeys then " without parameters" else p
onlySingleParams = hasPreferSingleObject || (isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream])
details _ = Nothing in toJsonPgrstError
SchemaCacheErrorCode02
hint (NoRelBetween parent child _ schema allRels) = JSON.String <$> noRelBetweenHint parent child schema allRels ("Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache")
hint (AmbiguousRelBetween _ child rels) = Just $ JSON.String $ "Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key." (Just $ JSON.String $ "Searched for the function " <> func <>
-- The hint will be null in the case of single unnamed parameter functions (case (hasPreferSingleObject, isInvPost, contentType) of
hint (NoRpc schema procName argumentKeys contentType isInvPost allProcs overloadedProcs) = (True, _, _) -> " with a single json/jsonb parameter"
if onlySingleParams (_, True, MTTextPlain) -> " with a single unnamed text parameter"
(_, True, MTTextXML) -> " with a single unnamed xml parameter"
(_, 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.")
-- The hint will be null in the case of single unnamed parameter functions
(if onlySingleParams
then Nothing then Nothing
else JSON.String <$> noRpcHint schema procName argumentKeys allProcs overloadedProcs else JSON.String <$> noRpcHint schema procName argumentKeys allProcs overloadedProcs)
where
onlySingleParams = isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream]
hint (AmbiguousRpc _) = Just "Try renaming the parameters or the function itself in the database so function overloading can be resolved"
hint (TableNotFound schemaName relName tbls) = JSON.String <$> tableNotFoundHint schemaName relName tbls
hint _ = Nothing 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")
instance JSON.ToJSON SchemaCacheError where toJSON (ColumnNotFound relName colName) = toJsonPgrstError
toJSON err = toJsonPgrstError SchemaCacheErrorCode04 ("Could not find the '" <> colName <> "' column of '" <> relName <> "' in the schema cache") Nothing Nothing
(code err) (message err) (details err) (hint err)
toJsonPgrstError :: Text -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value
toJsonPgrstError code' message' details' hint' = JSON.object [
"code" .= code'
, "message" .= message'
, "details" .= details'
, "hint" .= hint'
]
-- | -- |
-- If no relationship is found then: -- If no relationship is found then:
@@ -426,16 +351,6 @@ noRpcHint schema procName params allProcs overloadedProcs =
| null overloadedProcs = Fuzzy.getOne fuzzySetOfProcs procName | null overloadedProcs = Fuzzy.getOne fuzzySetOfProcs procName
| otherwise = (procName <>) <$> Fuzzy.getOne fuzzySetOfParams (listToText params) | otherwise = (procName <>) <$> Fuzzy.getOne fuzzySetOfParams (listToText params)
-- |
-- Do a fuzzy search in all tables in the same schema and return closest result
tableNotFoundHint :: Text -> Text -> [Table] -> Maybe Text
tableNotFoundHint schema tblName tblList
= fmap (\tbl -> "Perhaps you meant the table '" <> schema <> "." <> tbl <> "'") perhapsTable
where
perhapsTable = Fuzzy.getOne fuzzyTableSet tblName
fuzzyTableSet = Fuzzy.fromList [ tableName tbl | tbl <- tblList, tableSchema tbl == schema]
compressedRel :: Relationship -> JSON.Value compressedRel :: Relationship -> JSON.Value
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed -- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
compressedRel ComputedRelationship{} = JSON.object mempty compressedRel ComputedRelationship{} = JSON.object mempty
@@ -488,8 +403,6 @@ pgrstParseErrorHint err = case err of
_ -> "DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'." _ -> "DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'."
data PgError = PgError Authenticated SQL.UsageError data PgError = PgError Authenticated SQL.UsageError
deriving Show
type Authenticated = Bool type Authenticated = Bool
instance PgrstError PgError where instance PgrstError PgError where
@@ -507,81 +420,48 @@ instance PgrstError PgError where
then [("WWW-Authenticate", "Bearer") :: Header] then [("WWW-Authenticate", "Bearer") :: Header]
else mempty else mempty
proxyStatusHeader :: Text -> Header
proxyStatusHeader code' = ("Proxy-Status", "PostgREST; error=" <> T.encodeUtf8 code')
instance JSON.ToJSON PgError where instance JSON.ToJSON PgError where
toJSON (PgError _ usageError) = toJsonPgrstError toJSON (PgError _ usageError) = JSON.toJSON usageError
(code usageError) (message usageError) (details usageError) (hint usageError)
instance ErrorBody PgError where
code (PgError _ usageError) = code usageError
message (PgError _ usageError) = message usageError
details (PgError _ usageError) = details usageError
hint (PgError _ usageError) = hint usageError
instance JSON.ToJSON SQL.UsageError where instance JSON.ToJSON SQL.UsageError where
toJSON err = toJsonPgrstError toJSON (SQL.ConnectionUsageError e) = toJsonPgrstError
(code err) (message err) (details err) (hint err) ConnectionErrorCode00
"Database connection error. Retrying the connection."
(Just $ JSON.String $ T.decodeUtf8With T.lenientDecode $ fromMaybe "" e)
Nothing
instance ErrorBody SQL.UsageError where toJSON (SQL.SessionUsageError e) = JSON.toJSON e -- SQL.Error
code (SQL.ConnectionUsageError _) = "PGRST000"
code (SQL.SessionUsageError (SQL.QueryError _ _ e)) = code e
code SQL.AcquisitionTimeoutUsageError = "PGRST003"
message (SQL.ConnectionUsageError _) = "Database connection error. Retrying the connection." toJSON SQL.AcquisitionTimeoutUsageError = toJsonPgrstError
message (SQL.SessionUsageError (SQL.QueryError _ _ e)) = message e ConnectionErrorCode03 "Timed out acquiring connection from connection pool." Nothing Nothing
message SQL.AcquisitionTimeoutUsageError = "Timed out acquiring connection from connection pool."
details (SQL.ConnectionUsageError e) = JSON.String . T.decodeUtf8 <$> e instance JSON.ToJSON SQL.QueryError where
details (SQL.SessionUsageError (SQL.QueryError _ _ e)) = details e toJSON (SQL.QueryError _ _ e) = JSON.toJSON e
details SQL.AcquisitionTimeoutUsageError = Nothing
hint (SQL.ConnectionUsageError _) = Nothing
hint (SQL.SessionUsageError (SQL.QueryError _ _ e)) = hint e
hint SQL.AcquisitionTimeoutUsageError = Nothing
instance JSON.ToJSON SQL.CommandError where instance JSON.ToJSON SQL.CommandError where
toJSON err = toJsonPgrstError
(code err) (message err) (details err) (hint err)
instance ErrorBody SQL.CommandError where
-- Special error raised with code PGRST, to allow full response control -- Special error raised with code PGRST, to allow full response control
code (SQL.ResultError (SQL.ServerError "PGRST" m d _ _)) = toJSON (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p)) =
case parseRaisePGRST m d of case parseRaisePGRST m d of
Right (r, _) -> getCode r Right (r, _) -> JSON.object [
Left e -> code e "code" .= getCode r,
code (SQL.ResultError (SQL.ServerError c _ _ _ _)) = T.decodeUtf8 c "message" .= getMessage r,
"details" .= checkMaybe (getDetails r),
"hint" .= checkMaybe (getHint r)]
Left e -> JSON.toJSON e
where
checkMaybe = maybe JSON.Null JSON.String
code (SQL.ResultError _) = "PGRSTX00" -- Internal Error 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)]
code (SQL.ClientError _) = "PGRST001" toJSON (SQL.ResultError resultError) = toJsonPgrstError
InternalErrorCode00 (show resultError) Nothing Nothing
message (SQL.ResultError (SQL.ServerError "PGRST" m d _ _)) =
case parseRaisePGRST m d of
Right (r, _) -> getMessage r
Left e -> message e
message (SQL.ResultError (SQL.ServerError _ m _ _ _)) = T.decodeUtf8 m
message (SQL.ResultError resultError) = show resultError -- We never really return this error, because we kill pgrst thread early in App.hs
message (SQL.ClientError _) = "Database client error. Retrying the connection."
details (SQL.ResultError (SQL.ServerError "PGRST" m d _ _)) =
case parseRaisePGRST m d of
Right (r, _) -> JSON.String <$> getDetails r
Left e -> details e
details (SQL.ResultError (SQL.ServerError _ _ d _ _)) = JSON.String . T.decodeUtf8 <$> d
details (SQL.ClientError d) = JSON.String . T.decodeUtf8 <$> d
details _ = Nothing
hint (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p)) =
case parseRaisePGRST m d of
Right (r, _) -> JSON.String <$> getHint r
Left e -> hint e
hint (SQL.ResultError (SQL.ServerError _ _ _ h _)) = JSON.String . T.decodeUtf8 <$> h
hint _ = 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 :: Bool -> SQL.UsageError -> HTTP.Status
pgErrorStatus _ (SQL.ConnectionUsageError _) = HTTP.status503 pgErrorStatus _ (SQL.ConnectionUsageError _) = HTTP.status503
@@ -602,10 +482,6 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
if BS.isSuffixOf "requires a WHERE clause" m if BS.isSuffixOf "requires a WHERE clause" m
then HTTP.status400 -- special case for pg-safeupdate, which we consider as client error 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" else HTTP.status500 -- generic function or view server error, e.g. "more than one row returned by a subquery used as an expression"
"22023" -> -- invalid_parameter_value. Catch nonexistent role error, see https://github.com/PostgREST/postgrest/issues/3601
if BS.isPrefixOf "role" m && BS.isSuffixOf "does not exist" m
then HTTP.status401 -- role in jwt does not exist
else HTTP.status400
'2':'5':_ -> HTTP.status500 -- invalid tx state '2':'5':_ -> HTTP.status500 -- invalid tx state
'2':'8':_ -> HTTP.status403 -- invalid auth specification '2':'8':_ -> HTTP.status403 -- invalid auth specification
'2':'D':_ -> HTTP.status500 -- invalid tx termination '2':'D':_ -> HTTP.status500 -- invalid tx termination
@@ -643,132 +519,41 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
data Error data Error
= ApiRequestError ApiRequestError = ApiRequestError ApiRequestError
| SchemaCacheErr SchemaCacheError | JwtTokenInvalid Text
| JwtErr JwtError | JwtTokenMissing
| JwtTokenRequired
| NoSchemaCacheError | NoSchemaCacheError
| PgErr PgError | PgErr PgError
deriving Show
data JwtError
= JwtDecodeErr JwtDecodeError
| JwtSecretMissing
| JwtTokenRequired
| JwtClaimsErr JwtClaimsError
deriving Show
data JwtDecodeError
= EmptyAuthHeader
| UnexpectedParts Int
| KeyError Text
| BadAlgorithm Text
| BadCrypto
| UnsupportedTokenType
| UnreachableDecodeError
deriving Show
data JwtClaimsError
= JWTExpired
| JWTNotYetValid
| JWTIssuedAtFuture
| JWTNotInAudience
| ParsingClaimsFailed
| ExpClaimNotNumber
| NbfClaimNotNumber
| IatClaimNotNumber
| AudClaimNotStringOrArray
deriving Show
instance PgrstError Error where instance PgrstError Error where
status (ApiRequestError err) = status err status (ApiRequestError err) = status err
status (SchemaCacheErr err) = status err status JwtTokenInvalid{} = HTTP.unauthorized401
status (JwtErr err) = status err status JwtTokenMissing = HTTP.status500
status JwtTokenRequired = HTTP.unauthorized401
status NoSchemaCacheError = HTTP.status503 status NoSchemaCacheError = HTTP.status503
status (PgErr err) = status err status (PgErr err) = status err
headers (ApiRequestError err) = proxyStatusHeader (code err) : headers err headers (ApiRequestError err) = headers err
headers (SchemaCacheErr err) = proxyStatusHeader (code err) : headers err headers (JwtTokenInvalid m) = [invalidTokenHeader m]
headers (JwtErr err) = proxyStatusHeader (code err) : headers err headers JwtTokenRequired = [requiredTokenHeader]
headers (PgErr err) = proxyStatusHeader (code err) : headers err headers (PgErr err) = headers err
headers err@NoSchemaCacheError = proxyStatusHeader (code err) : mempty headers _ = mempty
instance JSON.ToJSON Error where instance JSON.ToJSON Error where
toJSON err = toJsonPgrstError toJSON NoSchemaCacheError = toJsonPgrstError
(code err) (message err) (details err) (hint err) ConnectionErrorCode02 "Could not query the database for the schema cache. Retrying." Nothing Nothing
instance ErrorBody Error where toJSON JwtTokenMissing = toJsonPgrstError
code (ApiRequestError err) = code err JWTErrorCode00 "Server lacks JWT secret" Nothing Nothing
code (SchemaCacheErr err) = code err
code (JwtErr err) = code err
code NoSchemaCacheError = "PGRST002"
code (PgErr err) = code err
message (ApiRequestError err) = message err toJSON (JwtTokenInvalid message) = toJsonPgrstError
message (SchemaCacheErr err) = message err JWTErrorCode01 message Nothing Nothing
message (JwtErr err) = message err
message NoSchemaCacheError = "Could not query the database for the schema cache. Retrying."
message (PgErr err) = message err
details (ApiRequestError err) = details err toJSON JwtTokenRequired = toJsonPgrstError
details (SchemaCacheErr err) = details err JWTErrorCode02 "Anonymous access is disabled" Nothing Nothing
details (JwtErr err) = details err
details NoSchemaCacheError = Nothing
details (PgErr err) = details err
hint (ApiRequestError err) = hint err toJSON (PgErr err) = JSON.toJSON err
hint (SchemaCacheErr err) = hint err toJSON (ApiRequestError err) = JSON.toJSON err
hint (JwtErr err) = hint err
hint NoSchemaCacheError = Nothing
hint (PgErr err) = hint err
instance PgrstError JwtError where
status JwtDecodeErr{} = HTTP.unauthorized401
status JwtSecretMissing = HTTP.status500
status JwtTokenRequired = HTTP.unauthorized401
status JwtClaimsErr{} = HTTP.unauthorized401
headers e@(JwtDecodeErr _) = [invalidTokenHeader $ message e]
headers JwtTokenRequired = [requiredTokenHeader]
headers e@(JwtClaimsErr _) = [invalidTokenHeader $ message e]
headers _ = mempty
instance JSON.ToJSON JwtError where
toJSON err = toJsonPgrstError
(code err) (message err) (details err) (hint err)
instance ErrorBody JwtError where
code JwtSecretMissing = "PGRST300"
code (JwtDecodeErr _) = "PGRST301"
code JwtTokenRequired = "PGRST302"
code (JwtClaimsErr _) = "PGRST303"
message JwtSecretMissing = "Server lacks JWT secret"
message (JwtDecodeErr e) = case e of
EmptyAuthHeader -> "Empty JWT is sent in Authorization header"
UnexpectedParts n -> "Expected 3 parts in JWT; got " <> show n
KeyError _ -> "No suitable key or wrong key type"
BadAlgorithm _ -> "Wrong or unsupported encoding algorithm"
BadCrypto -> "JWT cryptographic operation failed"
UnsupportedTokenType -> "Unsupported token type"
UnreachableDecodeError -> "JWT couldn't be decoded"
message JwtTokenRequired = "Anonymous access is disabled"
message (JwtClaimsErr e) = case e of
JWTExpired -> "JWT expired"
JWTNotYetValid -> "JWT not yet valid"
JWTIssuedAtFuture -> "JWT issued at future"
JWTNotInAudience -> "JWT not in audience"
ParsingClaimsFailed -> "Parsing claims failed"
ExpClaimNotNumber -> "The JWT 'exp' claim must be a number"
NbfClaimNotNumber -> "The JWT 'nbf' claim must be a number"
IatClaimNotNumber -> "The JWT 'iat' claim must be a number"
AudClaimNotStringOrArray -> "The JWT 'aud' claim must be a string or an array of strings"
details (JwtDecodeErr jde) = case jde of
KeyError dets -> Just $ JSON.String dets
BadAlgorithm dets -> Just $ JSON.String dets
_ -> Nothing
details _ = Nothing
hint _ = Nothing
invalidTokenHeader :: Text -> Header invalidTokenHeader :: Text -> Header
invalidTokenHeader m = invalidTokenHeader m =
@@ -816,3 +601,98 @@ parseRaisePGRST m d = do
det <- maybeToRight (PGRSTParseError NoDetail) d det <- maybeToRight (PGRSTParseError NoDetail) d
detJson <- maybeToRight (PGRSTParseError $ DetParseError det) (JSON.decodeStrict det) detJson <- maybeToRight (PGRSTParseError $ DetParseError det) (JSON.decodeStrict det)
return (msgJson, detJson) return (msgJson, detJson)
-- Error codes are grouped by common modules or characteristics
data ErrorCode
-- PostgreSQL connection errors
= ConnectionErrorCode00
| ConnectionErrorCode01
| ConnectionErrorCode02
| ConnectionErrorCode03
-- API Request errors
| ApiRequestErrorCode00
| ApiRequestErrorCode01
| ApiRequestErrorCode02
| ApiRequestErrorCode03
-- | ApiRequestErrorCode04 -- no longer used (used to be mapped to ParseRequestError)
| ApiRequestErrorCode05
| ApiRequestErrorCode06
| ApiRequestErrorCode07
| ApiRequestErrorCode08
| ApiRequestErrorCode09
| ApiRequestErrorCode10
| ApiRequestErrorCode11
-- | ApiRequestErrorCode13 -- no longer used (used to be mapped to BinaryFieldError)
| ApiRequestErrorCode12
| ApiRequestErrorCode14
| ApiRequestErrorCode15
| ApiRequestErrorCode16
| ApiRequestErrorCode17
| ApiRequestErrorCode18
| ApiRequestErrorCode19
| ApiRequestErrorCode20
| ApiRequestErrorCode21
| ApiRequestErrorCode22
| ApiRequestErrorCode23
| ApiRequestErrorCode24
-- Schema Cache errors
| SchemaCacheErrorCode00
| SchemaCacheErrorCode01
| SchemaCacheErrorCode02
| SchemaCacheErrorCode03
| SchemaCacheErrorCode04
-- JWT authentication errors
| JWTErrorCode00
| JWTErrorCode01
| JWTErrorCode02
-- Internal errors related to the Hasql library
| InternalErrorCode00
instance JSON.ToJSON ErrorCode where
toJSON e = JSON.toJSON (buildErrorCode e)
-- New group of errors will be added at the end of all the groups and will have the next prefix in the sequence
-- New errors are added at the end of the group they belong to and will have the next code in the sequence
buildErrorCode :: ErrorCode -> Text
buildErrorCode code = case code of
-- Keep the "PGRST" prefix in every code for an easier search/grep
ConnectionErrorCode00 -> "PGRST000"
ConnectionErrorCode01 -> "PGRST001"
ConnectionErrorCode02 -> "PGRST002"
ConnectionErrorCode03 -> "PGRST003"
ApiRequestErrorCode00 -> "PGRST100"
ApiRequestErrorCode01 -> "PGRST101"
ApiRequestErrorCode02 -> "PGRST102"
ApiRequestErrorCode03 -> "PGRST103"
ApiRequestErrorCode05 -> "PGRST105"
ApiRequestErrorCode06 -> "PGRST106"
ApiRequestErrorCode07 -> "PGRST107"
ApiRequestErrorCode08 -> "PGRST108"
ApiRequestErrorCode09 -> "PGRST109"
ApiRequestErrorCode10 -> "PGRST110"
ApiRequestErrorCode11 -> "PGRST111"
ApiRequestErrorCode12 -> "PGRST112"
ApiRequestErrorCode14 -> "PGRST114"
ApiRequestErrorCode15 -> "PGRST115"
ApiRequestErrorCode16 -> "PGRST116"
ApiRequestErrorCode17 -> "PGRST117"
ApiRequestErrorCode18 -> "PGRST118"
ApiRequestErrorCode19 -> "PGRST119"
ApiRequestErrorCode20 -> "PGRST120"
ApiRequestErrorCode21 -> "PGRST121"
ApiRequestErrorCode22 -> "PGRST122"
ApiRequestErrorCode23 -> "PGRST123"
ApiRequestErrorCode24 -> "PGRST124"
SchemaCacheErrorCode00 -> "PGRST200"
SchemaCacheErrorCode01 -> "PGRST201"
SchemaCacheErrorCode02 -> "PGRST202"
SchemaCacheErrorCode03 -> "PGRST203"
SchemaCacheErrorCode04 -> "PGRST204"
JWTErrorCode00 -> "PGRST300"
JWTErrorCode01 -> "PGRST301"
JWTErrorCode02 -> "PGRST302"
InternalErrorCode00 -> "PGRSTX00"
+14 -23
View File
@@ -21,7 +21,7 @@ import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
import qualified Network.Wai as Wai import qualified Network.Wai as Wai
import qualified Network.Wai.Middleware.RequestLogger as Wai import qualified Network.Wai.Middleware.RequestLogger as Wai
import Network.HTTP.Types.Status (Status, status400, status500) import Network.HTTP.Types.Status (status400, status500)
import System.IO.Unsafe (unsafePerformIO) import System.IO.Unsafe (unsafePerformIO)
import PostgREST.Config (LogLevel (..)) import PostgREST.Config (LogLevel (..))
@@ -57,27 +57,24 @@ logWithDebounce loggerState action = do
-- TODO stop using this middleware to reuse the same "observer" pattern for all our logs -- TODO stop using this middleware to reuse the same "observer" pattern for all our logs
middleware :: LogLevel -> (Wai.Request -> Maybe BS.ByteString) -> Wai.Middleware middleware :: LogLevel -> (Wai.Request -> Maybe BS.ByteString) -> Wai.Middleware
middleware logLevel getAuthRole = middleware logLevel getAuthRole = case logLevel of
unsafePerformIO $ LogCrit -> requestLogger (const False)
LogError -> requestLogger (>= status500)
LogWarn -> requestLogger (>= status400)
LogInfo -> requestLogger (const True)
LogDebug -> requestLogger (const True)
where
requestLogger filterStatus = unsafePerformIO $
Wai.mkRequestLogger Wai.defaultRequestLoggerSettings Wai.mkRequestLogger Wai.defaultRequestLoggerSettings
{ Wai.outputFormat = { Wai.outputFormat =
Wai.ApacheWithSettings $ Wai.ApacheWithSettings $
Wai.defaultApacheSettings & Wai.defaultApacheSettings &
Wai.setApacheRequestFilter (\_ res -> shouldLogResponse logLevel $ Wai.responseStatus res) & Wai.setApacheRequestFilter (\_ res -> filterStatus $ Wai.responseStatus res) &
Wai.setApacheUserGetter getAuthRole Wai.setApacheUserGetter getAuthRole
, Wai.autoFlush = True , Wai.autoFlush = True
, Wai.destination = Wai.Handle stdout , Wai.destination = Wai.Handle stdout
} }
shouldLogResponse :: LogLevel -> Status -> Bool
shouldLogResponse logLevel = case logLevel of
LogCrit -> const False
LogError -> (>= status500)
LogWarn -> (>= status400)
LogInfo -> const True
LogDebug -> const True
-- All observations are logged except some that depend on the log-level
observationLogger :: LoggerState -> LogLevel -> ObservationHandler observationLogger :: LoggerState -> LogLevel -> ObservationHandler
observationLogger loggerState logLevel obs = case obs of observationLogger loggerState logLevel obs = case obs of
o@(PoolAcqTimeoutObs _) -> do o@(PoolAcqTimeoutObs _) -> do
@@ -90,16 +87,10 @@ observationLogger loggerState logLevel obs = case obs of
o@(HasqlPoolObs _) -> do o@(HasqlPoolObs _) -> do
when (logLevel >= LogDebug) $ do when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o logWithZTime loggerState $ observationMessage o
o@(DBQuery sql status) -> do PoolRequest ->
-- Does not log SQL when it's empty (for OPTIONS requests or for the default OpenAPI output) pure ()
when (sql /= mempty && shouldLogResponse logLevel status) $ do PoolRequestFullfilled ->
logWithZTime loggerState $ observationMessage o pure ()
o@PoolRequest ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o
o@PoolRequestFullfilled ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o
o -> o ->
logWithZTime loggerState $ observationMessage o logWithZTime loggerState $ observationMessage o
+1 -6
View File
@@ -1,7 +1,3 @@
{-|
Module : PostgREST.Logger
Description : Metrics based on the Observation module. See Observation.hs.
-}
module PostgREST.Metrics module PostgREST.Metrics
( init ( init
, MetricsState (..) , MetricsState (..)
@@ -32,7 +28,6 @@ init configDbPoolSize = do
setGauge poolMaxSize (fromIntegral configDbPoolSize) setGauge poolMaxSize (fromIntegral configDbPoolSize)
pure $ MetricsState poolTimeouts poolAvailable poolWaiting poolMaxSize schemaCacheLoads schemaCacheQueryTime pure $ MetricsState poolTimeouts poolAvailable poolWaiting poolMaxSize schemaCacheLoads schemaCacheQueryTime
-- Only some observations are used as metrics
observationMetrics :: MetricsState -> ObservationHandler observationMetrics :: MetricsState -> ObservationHandler
observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schemaCacheLoads schemaCacheQueryTime) obs = case obs of observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schemaCacheLoads schemaCacheQueryTime) obs = case obs of
(PoolAcqTimeoutObs _) -> do (PoolAcqTimeoutObs _) -> do
@@ -52,7 +47,7 @@ observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schema
SchemaCacheLoadedObs resTime -> do SchemaCacheLoadedObs resTime -> do
withLabel schemaCacheLoads "SUCCESS" incCounter withLabel schemaCacheLoads "SUCCESS" incCounter
setGauge schemaCacheQueryTime resTime setGauge schemaCacheQueryTime resTime
SchemaCacheErrorObs{} -> do SchemaCacheErrorObs _ -> do
withLabel schemaCacheLoads "FAIL" incCounter withLabel schemaCacheLoads "FAIL" incCounter
_ -> _ ->
pure () pure ()
-21
View File
@@ -1,21 +0,0 @@
module PostgREST.Network
( resolveHost
) where
import Data.IP (fromHostAddress, fromHostAddress6)
import Data.String (IsString (..))
import qualified Network.Socket as NS
import Protolude
resolveHost :: NS.Socket -> IO (Maybe Text)
resolveHost sock = do
sn <- NS.getSocketName sock
case sn of
NS.SockAddrInet _ hostAddr -> pure $ Just $ fromString $ show $ fromHostAddress hostAddr
-- The IPv6 addresses are wrapped in [] brackets. This is done in accordance
-- to RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).
-- In short, we did this to have a clear separation between the port and host
-- because the components of an IPv6 are separated with the ':' character.
NS.SockAddrInet6 _ _ hostAddr6 _ -> pure $ Just $ fromString $ "[" ++ show (fromHostAddress6 hostAddr6) ++ "]"
_ -> pure Nothing

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