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
182 changed files with 4426 additions and 6982 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.
+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"
} }
] ]
} }
+18 -4
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
@@ -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
+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"
+11 -11
View File
@@ -13,21 +13,21 @@ 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">
@@ -35,6 +35,11 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
<img width="296px" src="static/supabase.svg"> <img width="296px" src="static/supabase.svg">
</a> </a>
</td> </td>
<td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png">
</a>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -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 -122
View File
@@ -5,126 +5,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## Unreleased
## [13.0.8] - 2025-10-24
### Fixed
- Fix loading utf-8 config files with `ASCII` locale set by @taimoorzaeem in #4386
## [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
@@ -132,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
+11 -6
View File
@@ -22,21 +22,21 @@ 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">
@@ -44,6 +44,11 @@ API than you are likely to write from scratch.
<img width="296px" src="static/supabase.svg"> <img width="296px" src="static/supabase.svg">
</a> </a>
</td> </td>
<td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png">
</a>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
+1 -1
View File
@@ -1 +1 @@
index-state: hackage.haskell.org 2025-10-13T04:53:27Z 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 =
+12 -7
View File
@@ -19,21 +19,21 @@ 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">
@@ -41,6 +41,11 @@ write from scratch.
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.svg"> <img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.svg">
</a> </a>
</td> </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>
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
@@ -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

+2 -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,9 +297,6 @@ 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", r"https://www.euronodes.com",
+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
================= =================
+12 -9
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,20 +51,17 @@ 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
@@ -73,11 +73,14 @@ Sponsors
.. image:: ../static/supabase.svg .. 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
| |
@@ -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.
+9 -72
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.
@@ -128,11 +96,6 @@ The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in
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;
} }
+15 -23
View File
@@ -48,17 +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
# TODO: Remove once available in nixpkgs haskellPackages
configurator-pg =
prev.callHackageDirect
{ {
pkg = "configurator-pg"; pkg = "fuzzyset";
ver = "0.2.11"; ver = "0.2.4";
sha256 = "sha256-mtGtNawDJgz2ZIEVca+IYXVu4oNw9xsfJiYWAqAbbgc="; sha256 = "sha256-lpkrTFcR0B4rT/P6x7ui31Twgq7BBj6KIvjKyqXKdpc=";
} }
{ }; { });
hasql-pool = lib.dontCheck (prev.callHackageDirect hasql-pool = lib.dontCheck (prev.callHackageDirect
{ {
@@ -66,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
{ {
@@ -74,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;
}; };
} }
+24 -24
View File
@@ -1,5 +1,5 @@
name: postgrest name: postgrest
version: 13.0.8 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,43 +87,44 @@ 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
, case-insensitive >= 1.2 && < 1.3 , case-insensitive >= 1.2 && < 1.3
, cassava >= 0.4.5 && < 0.6 , cassava >= 0.4.5 && < 0.6
, clock >= 0.8.3 && < 0.9.0 , clock >= 0.8.3 && < 0.9.0
, configurator-pg >= 0.2.11 && < 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
+25 -43
View File
@@ -1,10 +1,7 @@
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-| {-|
Module : PostgREST.Observation Module : PostgREST.Observation
Description : This module holds an Observation type which is the core of Observability for PostgREST. Description : Observations that can be used for Logging and Metrics
The Observation and ObservationHandler (the observer) are abstractions that allow centralizing logging and metrics concerns,
only observer calls with an Observation constructor are applied at different parts in the codebase.
The Logger and Metrics modules then decide which observations to expose. Not all observations need to be logged nor all correspond to a metric.
-} -}
module PostgREST.Observation module PostgREST.Observation
( Observation(..) ( Observation(..)
@@ -14,31 +11,29 @@ module PostgREST.Observation
) where ) where
import qualified Data.ByteString.Lazy as LBS import qualified Data.ByteString.Lazy as LBS
import Data.List.NonEmpty (toList)
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 Hasql.Connection as SQL import qualified Hasql.Connection as SQL
import qualified Hasql.Pool as SQL import qualified Hasql.Pool as SQL
import qualified Hasql.Pool.Observation as SQL import qualified Hasql.Pool.Observation as SQL
import Network.HTTP.Types.Status (Status)
import qualified Network.Socket as NS import qualified Network.Socket as NS
import Numeric (showFFloat) import Numeric (showFFloat)
import PostgREST.Config.PgVersion import PostgREST.Config.PgVersion
import qualified PostgREST.Error as Error import qualified PostgREST.Error as Error
import Protolude hiding (toList) import Protolude
import Protolude.Partial (fromJust) import Protolude.Partial (fromJust)
data Observation data Observation
= AdminStartObs (Maybe Text) (Maybe Int) = AdminStartObs (Maybe Int)
| AppStartObs ByteString | AppStartObs ByteString
| AppServerPortObs Text NS.PortNumber | AppServerPortObs NS.PortNumber
| AppServerUnixObs FilePath | AppServerUnixObs FilePath
| ExitUnsupportedPgVersion PgVersion PgVersion | ExitUnsupportedPgVersion PgVersion PgVersion
| ExitDBNoRecoveryObs | ExitDBNoRecoveryObs
| ExitDBFatalError ObsFatalError SQL.UsageError | ExitDBFatalError ObsFatalError SQL.UsageError
| DBConnectedObs Text | DBConnectedObs Text
| SchemaCacheErrorObs (NonEmpty Text) [Text] SQL.UsageError | SchemaCacheErrorObs SQL.UsageError
| SchemaCacheQueriedObs Double | SchemaCacheQueriedObs Double
| SchemaCacheSummaryObs Text | SchemaCacheSummaryObs Text
| SchemaCacheLoadedObs Double | SchemaCacheLoadedObs Double
@@ -48,14 +43,12 @@ data Observation
| DBListenRetry Int | DBListenRetry Int
| DBListenerGotSCacheMsg ByteString | DBListenerGotSCacheMsg ByteString
| DBListenerGotConfigMsg ByteString | DBListenerGotConfigMsg ByteString
| DBQuery ByteString Status
| ConfigReadErrorObs SQL.UsageError | ConfigReadErrorObs SQL.UsageError
| ConfigInvalidObs Text | ConfigInvalidObs Text
| ConfigSucceededObs | ConfigSucceededObs
| QueryRoleSettingsErrorObs SQL.UsageError | QueryRoleSettingsErrorObs SQL.UsageError
| QueryErrorCodeHighObs SQL.UsageError | QueryErrorCodeHighObs SQL.UsageError
| QueryPgVersionError SQL.UsageError | QueryPgVersionError SQL.UsageError
| PoolInit Int
| PoolAcqTimeoutObs SQL.UsageError | PoolAcqTimeoutObs SQL.UsageError
| HasqlPoolObs SQL.Observation | HasqlPoolObs SQL.Observation
| PoolRequest | PoolRequest
@@ -67,14 +60,14 @@ type ObservationHandler = Observation -> IO ()
observationMessage :: Observation -> Text observationMessage :: Observation -> Text
observationMessage = \case observationMessage = \case
AdminStartObs host port -> AdminStartObs port ->
"Admin server listening on " <> fromJust host <> ":" <> show (fromIntegral (fromJust port) :: Integer) "Admin server listening on port " <> show (fromIntegral (fromJust port) :: Integer)
AppStartObs ver -> AppStartObs ver ->
"Starting PostgREST " <> T.decodeUtf8 ver <> "..." "Starting PostgREST " <> T.decodeUtf8 ver <> "..."
AppServerPortObs host port -> AppServerPortObs port ->
"API server listening on " <> host <> ":" <> show port "Listening on port " <> show port
AppServerUnixObs sock -> AppServerUnixObs sock ->
"API server listening on unix socket " <> show sock "Listening on unix socket " <> show sock
DBConnectedObs ver -> DBConnectedObs ver ->
"Successfully connected to " <> ver "Successfully connected to " <> ver
ExitUnsupportedPgVersion pgVer minPgVer -> ExitUnsupportedPgVersion pgVer minPgVer ->
@@ -89,12 +82,8 @@ observationMessage = \case
"If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr "If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
ExitDBFatalError ServerError08P01 usageErr -> ExitDBFatalError ServerError08P01 usageErr ->
"Connection poolers in statement mode are not supported." <> jsonMessage usageErr "Connection poolers in statement mode are not supported." <> jsonMessage usageErr
SchemaCacheErrorObs dbSchemas extraPaths usageErr -> SchemaCacheErrorObs usageErr ->
"Failed to load the schema cache using " "Failed to load the schema cache. " <> jsonMessage usageErr
<> "db-schemas=" <> T.intercalate "," (toList dbSchemas)
<> " and "
<> "db-extra-search-path=" <> T.intercalate "," extraPaths
<> ". " <> jsonMessage usageErr
SchemaCacheQueriedObs resultTime -> SchemaCacheQueriedObs resultTime ->
"Schema cache queried in " <> showMillis resultTime <> " milliseconds" "Schema cache queried in " <> showMillis resultTime <> " milliseconds"
SchemaCacheSummaryObs summary -> SchemaCacheSummaryObs summary ->
@@ -106,18 +95,19 @@ observationMessage = \case
QueryPgVersionError usageErr -> QueryPgVersionError usageErr ->
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr "Failed to query the PostgreSQL version. " <> jsonMessage usageErr
DBListenStart channel -> do DBListenStart channel -> do
"Listening for database notifications on the " <> show channel <> " channel" "Listening for notifications on the " <> show channel <> " channel"
DBListenFail channel listenErr -> DBListenFail channel listenErr ->
"Failed listening for database notifications on the " <> show channel <> " channel. " <> "Failed listening for notifications on the " <> show channel <> " channel. " <> (
either showListenerConnError showListenerException listenErr case listenErr of
Left err -> show err
Right err -> showListenerError err
)
DBListenRetry delay -> DBListenRetry delay ->
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..." "Retrying listening for notifications in " <> (show delay::Text) <> " seconds..."
DBListenerGotSCacheMsg channel -> DBListenerGotSCacheMsg channel ->
"Received a schema cache reload message on the " <> show channel <> " channel" "Received a schema cache reload message on the " <> show channel <> " channel"
DBListenerGotConfigMsg channel -> DBListenerGotConfigMsg channel ->
"Received a config reload message on the " <> show channel <> " channel" "Received a config reload message on the " <> show channel <> " channel"
DBQuery sql _ ->
T.decodeUtf8 sql
ConfigReadErrorObs usageErr -> ConfigReadErrorObs usageErr ->
"Failed to query database settings for the config parameters." <> jsonMessage usageErr "Failed to query database settings for the config parameters." <> jsonMessage usageErr
QueryRoleSettingsErrorObs usageErr -> QueryRoleSettingsErrorObs usageErr ->
@@ -128,8 +118,6 @@ observationMessage = \case
"Failed reloading config: " <> err "Failed reloading config: " <> err
ConfigSucceededObs -> ConfigSucceededObs ->
"Config reloaded" "Config reloaded"
PoolInit poolSize ->
"Connection Pool initialized with a maximum size of " <> show poolSize <> " connections"
PoolAcqTimeoutObs usageErr -> PoolAcqTimeoutObs usageErr ->
jsonMessage usageErr jsonMessage usageErr
HasqlPoolObs (SQL.ConnectionObservation uuid status) -> HasqlPoolObs (SQL.ConnectionObservation uuid status) ->
@@ -144,21 +132,15 @@ observationMessage = \case
SQL.ReleaseConnectionTerminationReason -> "release" SQL.ReleaseConnectionTerminationReason -> "release"
SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message. SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message.
) )
PoolRequest -> _ -> mempty
"Trying to borrow a connection from pool"
PoolRequestFullfilled ->
"Borrowed a connection from the pool"
where where
showMillis :: Double -> Text showMillis :: Double -> Text
showMillis x = toS $ showFFloat (Just 1) (x * 1000) "" showMillis x = toS $ showFFloat (Just 1) (x * 1000) ""
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n" showListenerError :: Either SomeException () -> Text
showListenerError (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
showListenerConnError :: SQL.ConnectionError -> Text showListenerError (Left e) =
showListenerConnError = maybe "Connection error" (showOnSingleLine . T.decodeUtf8) let showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt in -- the errors from hasql-notifications come intercalated with "\t\n"
showOnSingleLine $ show e
showListenerException :: Either SomeException () -> Text
showListenerException (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
showListenerException (Left e) = showOnSingleLine $ show e
+161 -238
View File
@@ -25,16 +25,15 @@ module PostgREST.Plan
, CallReadPlan(..) , CallReadPlan(..)
) where ) where
import qualified Data.ByteString.Lazy as LBS
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import qualified Data.HashMap.Strict.InsOrd as HMI import qualified Data.HashMap.Strict.InsOrd as HMI
import qualified Data.List as L import qualified Data.List as L
import qualified Data.Set as S import qualified Data.Set as S
import qualified Data.Text as T
import qualified PostgREST.SchemaCache.Routine as Routine import qualified PostgREST.SchemaCache.Routine as Routine
import Data.Either.Combinators (mapLeft, mapRight) import Data.Either.Combinators (mapLeft, mapRight)
import Data.List (delete, lookup) import Data.List (delete, lookup)
import Data.Maybe (fromJust)
import Data.Tree (Tree (..)) import Data.Tree (Tree (..))
import PostgREST.ApiRequest (Action (..), import PostgREST.ApiRequest (Action (..),
@@ -44,9 +43,7 @@ import PostgREST.ApiRequest (Action (..),
Mutation (..), Mutation (..),
Payload (..)) Payload (..))
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.Error (ApiRequestError (..), import PostgREST.Error (Error (..))
Error (..),
SchemaCacheError (..))
import PostgREST.MediaType (MediaType (..)) import PostgREST.MediaType (MediaType (..))
import PostgREST.Query.SqlFragment (sourceCTEName) import PostgREST.Query.SqlFragment (sourceCTEName)
import PostgREST.RangeQuery (NonnegRange, allRange, import PostgREST.RangeQuery (NonnegRange, allRange,
@@ -72,8 +69,7 @@ import PostgREST.SchemaCache.Routine (MediaHandler (..),
RoutineParam (..), RoutineParam (..),
funcReturnsCompositeAlias, funcReturnsCompositeAlias,
funcReturnsScalar, funcReturnsScalar,
funcReturnsSetOfScalar, funcReturnsSetOfScalar)
funcReturnsSingle)
import PostgREST.SchemaCache.Table (Column (..), Table (..), import PostgREST.SchemaCache.Table (Column (..), Table (..),
TablesMap, TablesMap,
tableColumnsList, tableColumnsList,
@@ -157,34 +153,32 @@ dbActionPlan dbAct conf apiReq sCache = case dbAct of
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Bool -> Either Error CrudPlan wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Bool -> Either Error CrudPlan
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do
qi <- findTable identifier (dbTables sCache) rPlan <- readPlan identifier conf sCache apiRequest
rPlan <- readPlan qi conf sCache apiRequest (handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right () if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly qi return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly identifier
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
qi <- findTable identifier (dbTables sCache) rPlan <- readPlan identifier conf sCache apiRequest
rPlan <- readPlan qi conf sCache apiRequest mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan
mPlan <- mutatePlan mutation qi apiRequest sCache rPlan
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right () if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan) (handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation identifier
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{preferHandling, invalidPrefs, preferMaxAffected},..} invMethod = do callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} invMethod = do
let paramKeys = case invMethod of let paramKeys = case invMethod of
InvRead _ -> S.fromList $ fst <$> qsParams' InvRead _ -> S.fromList $ fst <$> qsParams'
Inv -> iColumns Inv -> iColumns
proc@Function{..} <- mapLeft SchemaCacheErr $ proc@Function{..} <- mapLeft ApiRequestError $
findProc identifier paramKeys (dbRoutines sCache) iContentMediaType (invMethod == Inv) findProc identifier paramKeys (preferParameters == Just SingleObject) (dbRoutines sCache) iContentMediaType (invMethod == Inv)
let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations
rPlan <- readPlan relIdentifier conf sCache apiRequest rPlan <- readPlan relIdentifier conf sCache apiRequest
let args = case (invMethod, iContentMediaType) of let args = case (invMethod, iContentMediaType) of
(InvRead _, _) -> DirectArgs $ toRpcParams proc qsParams' (InvRead _, _) -> jsonRpcParams proc qsParams'
(Inv, MTUrlEncoded) -> DirectArgs $ maybe mempty (toRpcParams proc . payArray) iPayload (Inv, MTUrlEncoded) -> maybe mempty (jsonRpcParams proc . payArray) iPayload
(Inv, _) -> JsonArgs $ payRaw <$> iPayload (Inv, _) -> maybe mempty payRaw iPayload
txMode = case (invMethod, pdVolatility) of txMode = case (invMethod, pdVolatility) of
(InvRead _, _) -> SQL.Read (InvRead _, _) -> SQL.Read
(Inv, Routine.Stable) -> SQL.Read (Inv, Routine.Stable) -> SQL.Read
@@ -193,15 +187,10 @@ callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferenc
cPlan = callPlan proc apiRequest paramKeys args rPlan cPlan = callPlan proc apiRequest paramKeys args rPlan
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan) (handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right () if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
failMaxAffectedRpcReturnsSingle (preferMaxAffected, preferHandling) proc
return $ CallReadPlan rPlan cPlan txMode proc handler mediaType invMethod identifier return $ CallReadPlan rPlan cPlan txMode proc handler mediaType invMethod identifier
where where
qsParams' = QueryParams.qsParams iQueryParams qsParams' = QueryParams.qsParams iQueryParams
failMaxAffectedRpcReturnsSingle :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> Routine -> Either Error ()
failMaxAffectedRpcReturnsSingle (Just (PreferMaxAffected _), Just Strict) rout = if funcReturnsSingle rout then Left $ ApiRequestError MaxAffectedRpcViolation else Right ()
failMaxAffectedRpcReturnsSingle _ _ = Right ()
hasDefaultSelect :: ReadPlanTree -> Bool hasDefaultSelect :: ReadPlanTree -> Bool
hasDefaultSelect (Node ReadPlan{select=[CoercibleSelectField{csField=CoercibleField{cfName}}]} []) = cfName == "*" hasDefaultSelect (Node ReadPlan{select=[CoercibleSelectField{csField=CoercibleField{cfName}}]} []) = cfName == "*"
hasDefaultSelect _ = False hasDefaultSelect _ = False
@@ -219,10 +208,10 @@ inspectPlan apiRequest headersOnly schema = do
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded, Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
the name is not enough to find it. An overloaded function can have a different volatility or even a different return type. the name is not enough to find it. An overloaded function can have a different volatility or even a different return type.
-} -}
findProc :: QualifiedIdentifier -> S.Set Text -> RoutineMap -> MediaType -> Bool -> Either SchemaCacheError Routine findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> RoutineMap -> MediaType -> Bool -> Either ApiRequestError Routine
findProc qi argumentsKeys allProcs contentMediaType isInvPost = findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPost =
case matchProc of case matchProc of
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) contentMediaType isInvPost (HM.keys allProcs) lookupProcName ([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentMediaType isInvPost (HM.keys allProcs) lookupProcName
-- If there are no functions with named arguments, fallback to the single unnamed argument function -- If there are no functions with named arguments, fallback to the single unnamed argument function
([], [proc]) -> Right proc ([], [proc]) -> Right proc
([], procs) -> Left $ AmbiguousRpc (toList procs) ([], procs) -> Left $ AmbiguousRpc (toList procs)
@@ -253,9 +242,13 @@ findProc qi argumentsKeys allProcs contentMediaType isInvPost =
matchesParams proc = matchesParams proc =
let let
params = pdParams proc params = pdParams proc
firstType = (ppType <$> headMay params)
in in
-- exceptional case for Prefer: params=single-object
if paramsAsSingleObject
then length params == 1 && (firstType == Just "json" || firstType == Just "jsonb")
-- If the function has no parameters, the arguments keys must be empty as well -- If the function has no parameters, the arguments keys must be empty as well
if null params else if null params
then null argumentsKeys && not (isInvPost && contentMediaType `elem` [MTOctetStream, MTTextPlain, MTTextXML]) then null argumentsKeys && not (isInvPost && contentMediaType `elem` [MTOctetStream, MTTextPlain, MTTextXML])
-- A function has optional and required parameters. Optional parameters have a default value and -- A function has optional and required parameters. Optional parameters have a default value and
-- don't require arguments for the function to be executed, required parameters must have an argument present. -- don't require arguments for the function to be executed, required parameters must have an argument present.
@@ -277,29 +270,27 @@ data ResolverContext = ResolverContext
, outputType :: Text -- ^ The output type for the response payload; e.g. "csv", "json", "binary". , outputType :: Text -- ^ The output type for the response payload; e.g. "csv", "json", "binary".
} }
resolveColumnField :: Column -> Maybe ToTsVector -> CoercibleField resolveColumnField :: Column -> CoercibleField
resolveColumnField col toTsV = CoercibleField (colName col) mempty False toTsV (colNominalType col) (colType col) Nothing (colDefault col) False resolveColumnField col = CoercibleField (colName col) mempty False (colNominalType col) Nothing (colDefault col)
resolveTableFieldName :: Table -> FieldName -> Maybe ToTsVector -> CoercibleField resolveTableFieldName :: Table -> FieldName -> CoercibleField
resolveTableFieldName table fieldName toTsV= resolveTableFieldName table fieldName =
fromMaybe (unknownField fieldName []) $ HMI.lookup fieldName (tableColumns table) >>= fromMaybe (unknownField fieldName []) $ HMI.lookup fieldName (tableColumns table) >>=
Just . flip resolveColumnField toTsV Just . resolveColumnField
-- | Resolve a type within the context based on the given field name and JSON path. Although there are situations where failure to resolve a field is considered an error (see `resolveOrError`), there are also situations where we allow it (RPC calls). If it should be an error and `resolveOrError` doesn't fit, ensure to check the `cfIRType` isn't empty. -- | Resolve a type within the context based on the given field name and JSON path. Although there are situations where failure to resolve a field is considered an error (see `resolveOrError`), there are also situations where we allow it (RPC calls). If it should be an error and `resolveOrError` doesn't fit, ensure to check the `cfIRType` isn't empty.
resolveTypeOrUnknown :: ResolverContext -> Field -> Maybe ToTsVector -> CoercibleField resolveTypeOrUnknown :: ResolverContext -> Field -> CoercibleField
resolveTypeOrUnknown ResolverContext{..} (fn, jp) toTsV = resolveTypeOrUnknown ResolverContext{..} (fn, jp) =
case res of case res of
-- types that are already json/jsonb don't need to be converted with `to_jsonb` for using arrow operators `data->attr` -- types that are already json/jsonb don't need to be converted with `to_jsonb` for using arrow operators `data->attr`
-- this prevents indexes not applying https://github.com/PostgREST/postgrest/issues/2594 -- this prevents indexes not applying https://github.com/PostgREST/postgrest/issues/2594
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp, cfToJson=False} cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp, cfToJson=False}
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp, cfToJson=False} cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp, cfToJson=False}
-- Do not apply to_tsvector to tsvector types
cf@CoercibleField{cfBaseType="tsvector"} -> cf{cfJsonPath=jp, cfToJson=True, cfToTsVector=Nothing}
-- other types will get converted `to_jsonb(col)->attr`, even unknown types -- other types will get converted `to_jsonb(col)->attr`, even unknown types
cf -> cf{cfJsonPath=jp, cfToJson=True} cf -> cf{cfJsonPath=jp, cfToJson=True}
where where
res = fromMaybe (unknownField fn jp) $ HM.lookup qi tables >>= res = fromMaybe (unknownField fn jp) $ HM.lookup qi tables >>=
Just . (\t -> resolveTableFieldName t fn toTsV) Just . flip resolveTableFieldName fn
-- | Install any pre-defined data representation from source to target to coerce this reference. -- | Install any pre-defined data representation from source to target to coerce this reference.
-- --
@@ -325,15 +316,11 @@ withJsonParse ctx field@CoercibleField{cfIRType} = withTransformer ctx "json" cf
-- | Map the intermediate representation type to the output type defined by the resolver context (normally json), if available. -- | Map the intermediate representation type to the output type defined by the resolver context (normally json), if available.
resolveOutputField :: ResolverContext -> Field -> CoercibleField resolveOutputField :: ResolverContext -> Field -> CoercibleField
resolveOutputField ctx field = withOutputFormat ctx $ resolveTypeOrUnknown ctx field Nothing resolveOutputField ctx field = withOutputFormat ctx $ resolveTypeOrUnknown ctx field
-- | Map the query string format of a value (text) into the intermediate representation type, if available. -- | Map the query string format of a value (text) into the intermediate representation type, if available.
resolveQueryInputField :: ResolverContext -> Field -> OpExpr -> CoercibleField resolveQueryInputField :: ResolverContext -> Field -> CoercibleField
resolveQueryInputField ctx field opExpr = withTextParse ctx $ resolveTypeOrUnknown ctx field toTsVector resolveQueryInputField ctx field = withTextParse ctx $ resolveTypeOrUnknown ctx field
where
toTsVector = case opExpr of
OpExpr _ (Fts _ lang _) -> Just $ ToTsVector lang
_ -> Nothing
-- | Builds the ReadPlan tree on a number of stages. -- | Builds the ReadPlan tree on a number of stages.
-- | Adds filters, order, limits on its respective nodes. -- | Adds filters, order, limits on its respective nodes.
@@ -344,12 +331,13 @@ readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregate
-- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV. -- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV.
ctx = ResolverContext dbTables dbRepresentations qi "json" ctx = ResolverContext dbTables dbRepresentations qi "json"
in in
mapLeft ApiRequestError $
treeRestrictRange configDbMaxRows (iAction apiRequest) =<< treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
addToManyOrderSelects =<<
hoistSpreadAggFunctions =<<
validateAggFunctions configDbAggregates =<< validateAggFunctions configDbAggregates =<<
hoistSpreadAggFunctions =<<
addRelSelects =<< addRelSelects =<<
addNullEmbedFilters =<< addNullEmbedFilters =<<
validateSpreadEmbeds =<<
addRelatedOrders =<< addRelatedOrders =<<
addAliases =<< addAliases =<<
expandStars ctx =<< expandStars ctx =<<
@@ -365,7 +353,7 @@ initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi ctx, relName=qiName, depth=rootDepth} [] foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi ctx, relName=qiName, depth=rootDepth} []
where where
rootDepth = 0 rootDepth = 0
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing Nothing [] rootDepth defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing False [] rootDepth
treeEntry :: Depth -> Tree SelectItem -> ReadPlanTree -> ReadPlanTree treeEntry :: Depth -> Tree SelectItem -> ReadPlanTree -> ReadPlanTree
treeEntry depth (Node si fldForest) (Node q rForest) = treeEntry depth (Node si fldForest) (Node q rForest) =
let nxtDepth = succ depth in let nxtDepth = succ depth in
@@ -378,42 +366,26 @@ initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
SpreadRelation{..} -> SpreadRelation{..} ->
Node q $ Node q $
foldr (treeEntry nxtDepth) foldr (treeEntry nxtDepth)
(Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth, relSpread=Just ToOneSpread} []) (Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth, relIsSpread=True} [])
fldForest:rForest fldForest:rForest
SelectField{..} -> SelectField{..} ->
Node q{select=CoercibleSelectField (resolveOutputField ctx{qi=from q} selField) selAggregateFunction selAggregateCast selCast selAlias:select q} rForest Node q{select=CoercibleSelectField (resolveOutputField ctx{qi=from q} selField) selAggregateFunction selAggregateCast selCast selAlias:select q} rForest
-- If an alias is explicitly specified, it is always respected. However, an alias may be -- If an alias is explicitly specified, it is always respected. However, an alias may be
-- determined automatically in these cases: -- determined automatically in the case of a select term with a JSON path, or in the case
-- * A select term with a JSON path -- of domain representations.
-- * Domain representations addAliases :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
-- * Aggregates in spread relationships
addAliases :: ReadPlanTree -> Either Error ReadPlanTree
addAliases = Right . fmap addAliasToPlan addAliases = Right . fmap addAliasToPlan
where where
addAliasToPlan rp@ReadPlan{select=sel, relSpread=spr} = rp{select=map (aliasSelectField $ isJust spr) sel} addAliasToPlan rp@ReadPlan{select=sel} = rp{select=map aliasSelectField sel}
aliasSelectField :: Bool -> CoercibleSelectField -> CoercibleSelectField aliasSelectField :: CoercibleSelectField -> CoercibleSelectField
aliasSelectField isSpread field@CoercibleSelectField{csField=fieldDetails, csAggFunction=aggFun, csAlias=alias} aliasSelectField field@CoercibleSelectField{csField=fieldDetails, csAggFunction=aggFun, csAlias=alias}
| isJust alias = field | isJust alias || isJust aggFun = field
| isJust aggFun = fieldAliasForSpreadAgg isSpread field
| isJsonKeyPath fieldDetails, Just key <- lastJsonKey fieldDetails = field { csAlias = Just key } | isJsonKeyPath fieldDetails, Just key <- lastJsonKey fieldDetails = field { csAlias = Just key }
| isTransformPath fieldDetails = field { csAlias = Just (cfName fieldDetails) } | isTransformPath fieldDetails = field { csAlias = Just (cfName fieldDetails) }
| otherwise = field | otherwise = field
-- Spread relationships with non-aliased aggregates can cause problems when selecting the fields in the top level resource.
-- The top level won't know the name of the field in this case:
-- A nested to-one spread like `/top_table?select=...middle_table(...nested_table(count()))`
-- will do a `SELECT nested_table` instead of `SELECT *`, because doing a `COUNT(*)` in `top_table`
-- would not return the desired results.
--
-- That's why we need to use the aggregate name as an alias (e.g. COUNT(...) AS "count").
-- Since PostgreSQL labels the columns with the aggregate name, it shouldn't be a problem to
-- apply the aliases to all the aggregates regardless if the previous conditions are met.
fieldAliasForSpreadAgg True field@CoercibleSelectField{csAggFunction=Just agg} =
field { csAlias = Just (T.toLower $ show agg) }
fieldAliasForSpreadAgg _ field = field
isJsonKeyPath CoercibleField{cfJsonPath=(_: _)} = True isJsonKeyPath CoercibleField{cfJsonPath=(_: _)} = True
isJsonKeyPath _ = False isJsonKeyPath _ = False
@@ -438,14 +410,13 @@ knownColumnsInContext ResolverContext{..} =
-- | Expand "select *" into explicit field names of the table in the following situations: -- | Expand "select *" into explicit field names of the table in the following situations:
-- * When there are data representations present. -- * When there are data representations present.
-- * When there is an aggregate function in a given ReadPlan or its parent. -- * When there is an aggregate function in a given ReadPlan or its parent.
-- * When the ReadPlan is a to-many spread relationship expandStars :: ResolverContext -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
expandStars :: ResolverContext -> ReadPlanTree -> Either Error ReadPlanTree
expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree
where where
expandStarsForReadPlan :: Bool -> ReadPlanTree -> ReadPlanTree expandStarsForReadPlan :: Bool -> ReadPlanTree -> ReadPlanTree
expandStarsForReadPlan hasAgg (Node rp@ReadPlan{select, from=fromQI, fromAlias=alias, relSpread=spread} children) = expandStarsForReadPlan hasAgg (Node rp@ReadPlan{select, from=fromQI, fromAlias=alias} children) =
let let
newHasAgg = hasAgg || any (isJust . csAggFunction) select || case spread of Just ToManySpread{} -> True; _ -> False newHasAgg = hasAgg || any (isJust . csAggFunction) select
newCtx = adjustContext ctx fromQI alias newCtx = adjustContext ctx fromQI alias
newRPlan = expandStarsForTable newCtx newHasAgg rp newRPlan = expandStarsForTable newCtx newHasAgg rp
in Node newRPlan (map (expandStarsForReadPlan newHasAgg) children) in Node newRPlan (map (expandStarsForReadPlan newHasAgg) children)
@@ -458,33 +429,29 @@ expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree
adjustContext context fromQI _ = context{qi=fromQI} adjustContext context fromQI _ = context{qi=fromQI}
expandStarsForTable :: ResolverContext -> Bool -> ReadPlan -> ReadPlan expandStarsForTable :: ResolverContext -> Bool -> ReadPlan -> ReadPlan
expandStarsForTable ctx@ResolverContext{representations, outputType} hasAgg rp@ReadPlan{select=selectFields, relSpread=spread} expandStarsForTable ctx@ResolverContext{representations, outputType} hasAgg rp@ReadPlan{select=selectFields}
-- We expand if either of the below are true: -- We expand if either of the below are true:
-- * We have a '*' select AND there is an aggregate function in this ReadPlan's sub-tree. -- * We have a '*' select AND there is an aggregate function in this ReadPlan's sub-tree.
-- * We have a '*' select AND the target table has at least one data representation. -- * We have a '*' select AND the target table has at least one data representation.
-- We ignore '*' selects that have an aggregate function attached, unless it's a `COUNT(*)` for a Spread Embed, -- We ignore any '*' selects that have an aggregate function attached (i.e for COUNT(*)).
-- we tag it as "full row" in that case. | hasStarSelect && (hasAgg || hasDataRepresentation) = rp{select = concatMap (expandStarSelectField knownColumns) selectFields}
| hasStarSelect && (hasAgg || hasDataRepresentation) = rp{select = concatMap (expandStarSelectField (isJust spread) knownColumns) selectFields}
| otherwise = rp | otherwise = rp
where where
hasStarSelect = "*" `elem` map (cfName . csField) filteredSelectFields hasStarSelect = "*" `elem` map (cfName . csField) filteredSelectFields
filteredSelectFields = filter (shouldExpandOrTag . csAggFunction) selectFields filteredSelectFields = filter (isNothing . csAggFunction) selectFields
shouldExpandOrTag aggFunc = isNothing aggFunc || (isJust spread && aggFunc == Just Count)
hasDataRepresentation = any hasOutputRep knownColumns hasDataRepresentation = any hasOutputRep knownColumns
knownColumns = knownColumnsInContext ctx knownColumns = knownColumnsInContext ctx
hasOutputRep :: Column -> Bool hasOutputRep :: Column -> Bool
hasOutputRep col = HM.member (colNominalType col, outputType) representations hasOutputRep col = HM.member (colNominalType col, outputType) representations
expandStarSelectField :: Bool -> [Column] -> CoercibleSelectField -> [CoercibleSelectField] expandStarSelectField :: [Column] -> CoercibleSelectField -> [CoercibleSelectField]
expandStarSelectField _ columns sel@CoercibleSelectField{csField=CoercibleField{cfName="*", cfJsonPath=[]}, csAggFunction=Nothing} = expandStarSelectField columns sel@CoercibleSelectField{csField=CoercibleField{cfName="*", cfJsonPath=[]}, csAggFunction=Nothing} =
map (\col -> sel { csField = withOutputFormat ctx $ resolveColumnField col Nothing }) columns map (\col -> sel { csField = withOutputFormat ctx $ resolveColumnField col }) columns
expandStarSelectField True _ sel@CoercibleSelectField{csField=fld@CoercibleField{cfName="*", cfJsonPath=[]}, csAggFunction=Just Count} = expandStarSelectField _ selectField = [selectField]
[sel { csField = fld { cfFullRow = True } }]
expandStarSelectField _ _ selectField = [selectField]
-- | Enforces the `max-rows` config on the result -- | Enforces the `max-rows` config on the result
treeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either Error ReadPlanTree treeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
treeRestrictRange _ (ActDb (ActRelationMut _ _)) request = Right request treeRestrictRange _ (ActDb (ActRelationMut _ _)) request = Right request
treeRestrictRange maxRows _ request = pure $ nodeRestrictRange maxRows <$> request treeRestrictRange maxRows _ request = pure $ nodeRestrictRange maxRows <$> request
where where
@@ -493,22 +460,21 @@ treeRestrictRange maxRows _ request = pure $ nodeRestrictRange maxRows <$> reque
-- add relationships to the nodes of the tree by traversing the forest while keeping track of the parentNode(https://stackoverflow.com/questions/22721064/get-the-parent-of-a-node-in-data-tree-haskell#comment34627048_22721064) -- add relationships to the nodes of the tree by traversing the forest while keeping track of the parentNode(https://stackoverflow.com/questions/22721064/get-the-parent-of-a-node-in-data-tree-haskell#comment34627048_22721064)
-- also adds aliasing -- also adds aliasing
addRels :: Schema -> Action -> RelationshipsMap -> Maybe ReadPlanTree -> ReadPlanTree -> Either Error ReadPlanTree addRels :: Schema -> Action -> RelationshipsMap -> Maybe ReadPlanTree -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
addRels schema action allRels parentNode (Node rPlan@ReadPlan{relName,relHint,relAlias,relSpread,depth} forest) = addRels schema action allRels parentNode (Node rPlan@ReadPlan{relName,relHint,relAlias,depth} forest) =
case parentNode of case parentNode of
Just (Node ReadPlan{from=parentNodeQi, fromAlias=parentAlias} _) -> Just (Node ReadPlan{from=parentNodeQi, fromAlias=parentAlias} _) ->
let let
newReadPlan = (\r -> newReadPlan = (\r ->
let newAlias = Just (qiName (relForeignTable r) <> "_" <> show depth) let newAlias = Just (qiName (relForeignTable r) <> "_" <> show depth)
aggAlias = qiName (relTable r) <> "_" <> fromMaybe relName relAlias <> "_" <> show depth aggAlias = qiName (relTable r) <> "_" <> fromMaybe relName relAlias <> "_" <> show depth in
updSpread = if isJust relSpread && not (relIsToOne r) then Just $ ToManySpread [] [] else relSpread in
case r of case r of
Relationship{relCardinality=M2M _} -> -- m2m does internal implicit joins that don't need aliasing Relationship{relCardinality=M2M _} -> -- m2m does internal implicit joins that don't need aliasing
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, relJoinConds=getJoinConditions Nothing parentAlias r, relSpread=updSpread} rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, relJoinConds=getJoinConditions Nothing parentAlias r}
ComputedRelationship{} -> ComputedRelationship{} ->
rPlan{from=relForeignTable r, relToParent=Just r{relTableAlias=maybe (relTable r) (QualifiedIdentifier mempty) parentAlias}, relAggAlias=aggAlias, fromAlias=newAlias, relSpread=updSpread} rPlan{from=relForeignTable r, relToParent=Just r{relTableAlias=maybe (relTable r) (QualifiedIdentifier mempty) parentAlias}, relAggAlias=aggAlias, fromAlias=newAlias}
_ -> _ ->
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, fromAlias=newAlias, relJoinConds=getJoinConditions newAlias parentAlias r, relSpread=updSpread} rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, fromAlias=newAlias, relJoinConds=getJoinConditions newAlias parentAlias r}
) <$> rel ) <$> rel
origin = if depth == 1 -- Only on depth 1 we check if the root(depth 0) has an alias so the sourceCTEName alias can be found as a relationship origin = if depth == 1 -- Only on depth 1 we check if the root(depth 0) has an alias so the sourceCTEName alias can be found as a relationship
then fromMaybe (qiName parentNodeQi) parentAlias then fromMaybe (qiName parentNodeQi) parentAlias
@@ -529,7 +495,7 @@ addRels schema action allRels parentNode (Node rPlan@ReadPlan{relName,relHint,re
in in
Node newReadPlan <$> updateForest (Just $ Node newReadPlan forest) Node newReadPlan <$> updateForest (Just $ Node newReadPlan forest)
where where
updateForest :: Maybe ReadPlanTree -> Either Error [ReadPlanTree] updateForest :: Maybe ReadPlanTree -> Either ApiRequestError [ReadPlanTree]
updateForest rq = addRels schema action allRels rq `traverse` forest updateForest rq = addRels schema action allRels rq `traverse` forest
getJoinConditions :: Maybe Alias -> Maybe Alias -> Relationship -> [JoinCondition] getJoinConditions :: Maybe Alias -> Maybe Alias -> Relationship -> [JoinCondition]
@@ -559,12 +525,12 @@ getJoinConditions tblAlias parentAlias Relationship{relTable=qi,relForeignTable=
-- request is ambiguous and we return an error. In that case the request can -- request is ambiguous and we return an error. In that case the request can
-- be disambiguated by adding precision to the target or by using a hint: -- be disambiguated by adding precision to the target or by using a hint:
-- /origin?select=target!hint(*). The origin can be a table or view. -- /origin?select=target!hint(*). The origin can be a table or view.
findRel :: Schema -> RelationshipsMap -> NodeName -> NodeName -> Maybe Hint -> Either Error Relationship findRel :: Schema -> RelationshipsMap -> NodeName -> NodeName -> Maybe Hint -> Either ApiRequestError Relationship
findRel schema allRels origin target hint = findRel schema allRels origin target hint =
case rels of case rels of
[] -> Left $ SchemaCacheErr $ NoRelBetween origin target hint schema allRels [] -> Left $ NoRelBetween origin target hint schema allRels
[r] -> Right r [r] -> Right r
rs -> Left $ SchemaCacheErr $ AmbiguousRelBetween origin target rs rs -> Left $ AmbiguousRelBetween origin target rs
where where
matchFKSingleCol hint_ card = case card of matchFKSingleCol hint_ card = case card of
O2M{relColumns=[(col, _)]} -> hint_ == col O2M{relColumns=[(col, _)]} -> hint_ == col
@@ -640,7 +606,7 @@ findRel schema allRels origin target hint =
) $ fromMaybe mempty $ HM.lookup (QualifiedIdentifier schema origin, schema) allRels ) $ fromMaybe mempty $ HM.lookup (QualifiedIdentifier schema origin, schema) allRels
addRelSelects :: ReadPlanTree -> Either Error ReadPlanTree addRelSelects :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
addRelSelects node@(Node rp forest) addRelSelects node@(Node rp forest)
| null forest = Right node | null forest = Right node
| otherwise = | otherwise =
@@ -649,9 +615,9 @@ addRelSelects node@(Node rp forest)
in Right $ Node rp { relSelect = newRelSelects } newForest in Right $ Node rp { relSelect = newRelSelects } newForest
generateRelSelectField :: ReadPlanTree -> Maybe RelSelectField generateRelSelectField :: ReadPlanTree -> Maybe RelSelectField
generateRelSelectField (Node rp@ReadPlan{relToParent=Just _, relAggAlias, relSpread = Just _} _) = generateRelSelectField (Node rp@ReadPlan{relToParent=Just _, relAggAlias, relIsSpread = True} _) =
Just $ Spread { rsSpreadSel = generateSpreadSelectFields rp, rsAggAlias = relAggAlias } Just $ Spread { rsSpreadSel = generateSpreadSelectFields rp, rsAggAlias = relAggAlias }
generateRelSelectField (Node ReadPlan{relToParent=Just rel, select, relName, relAlias, relAggAlias, relSpread = Nothing} forest) = generateRelSelectField (Node ReadPlan{relToParent=Just rel, select, relName, relAlias, relAggAlias, relIsSpread = False} forest) =
Just $ JsonEmbed { rsEmbedMode, rsSelName, rsAggAlias = relAggAlias, rsEmptyEmbed } Just $ JsonEmbed { rsEmbedMode, rsSelName, rsAggAlias = relAggAlias, rsEmptyEmbed }
where where
rsSelName = fromMaybe relName relAlias rsSelName = fromMaybe relName relAlias
@@ -680,7 +646,7 @@ generateSpreadSelectFields ReadPlan{select, relSelect} =
relSelectToSpread (Spread{rsSpreadSel}) = relSelectToSpread (Spread{rsSpreadSel}) =
rsSpreadSel rsSpreadSel
-- When aggregates are present in a ReadPlan with a to-one spread, we "hoist" -- When aggregates are present in a ReadPlan that will be spread, we "hoist"
-- to the highest level possible so that their semantics make sense. For instance, -- to the highest level possible so that their semantics make sense. For instance,
-- imagine the user performs the following request: -- imagine the user performs the following request:
-- `GET /projects?select=client_id,...project_invoices(invoice_total.sum())` -- `GET /projects?select=client_id,...project_invoices(invoice_total.sum())`
@@ -701,39 +667,28 @@ generateSpreadSelectFields ReadPlan{select, relSelect} =
-- --
-- The second tuple contains the aggregate function to be applied, the cast, and -- The second tuple contains the aggregate function to be applied, the cast, and
-- the alias, if it was supplied by the user or otherwise determined. -- the alias, if it was supplied by the user or otherwise determined.
--
-- No hoisting is done for to-many spreads
type HoistedAgg = ((Alias, FieldName), (AggregateFunction, Maybe Cast, Maybe Alias)) type HoistedAgg = ((Alias, FieldName), (AggregateFunction, Maybe Cast, Maybe Alias))
hoistSpreadAggFunctions :: ReadPlanTree -> Either Error ReadPlanTree hoistSpreadAggFunctions :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
hoistSpreadAggFunctions tree = Right $ fst $ applySpreadAggHoistingToNode tree hoistSpreadAggFunctions tree = Right $ fst $ applySpreadAggHoistingToNode tree
applySpreadAggHoistingToNode :: ReadPlanTree -> (ReadPlanTree, [HoistedAgg]) applySpreadAggHoistingToNode :: ReadPlanTree -> (ReadPlanTree, [HoistedAgg])
applySpreadAggHoistingToNode (Node rp@ReadPlan{relAggAlias, relToParent, relSpread} children) = applySpreadAggHoistingToNode (Node rp@ReadPlan{relAggAlias, relToParent, relIsSpread} children) =
let (newChildren, childAggLists) = unzip $ map applySpreadAggHoistingToNode children let (newChildren, childAggLists) = unzip $ map applySpreadAggHoistingToNode children
allChildAggLists = concat childAggLists allChildAggLists = concat childAggLists
isToOneSpread = relSpread == Just ToOneSpread (newSelects, aggList) = if depth rp == 0 || (isJust relToParent && not relIsSpread)
(newSelects, aggList) = if depth rp == 0 || (isJust relToParent && not isToOneSpread)
then (select rp, []) then (select rp, [])
else hoistFromSelectFields relAggAlias (select rp) else hoistFromSelectFields relAggAlias (select rp)
-- If the current `ReadPlan` is a to-one spread rel and it has aggregates hoisted from newRelSelects = if null children
-- child relationships, then it must hoist those aggregates to its parent rel.
-- So we update them with the current `relAggAlias`.
hoistAgg ((_, fieldName), hoistFunc) = ((relAggAlias, fieldName), hoistFunc)
hoistedAggList = if isToOneSpread
then aggList ++ map hoistAgg allChildAggLists
else aggList
newRelSelects = if null children || isToOneSpread
then relSelect rp then relSelect rp
else map (hoistIntoRelSelectFields allChildAggLists) $ relSelect rp else map (hoistIntoRelSelectFields allChildAggLists) $ relSelect rp
in (Node rp { select = newSelects, relSelect = newRelSelects } newChildren, hoistedAggList) in (Node rp { select = newSelects, relSelect = newRelSelects } newChildren, aggList)
-- Hoist aggregate functions from the select list of a ReadPlan, and return the -- Hoist aggregate functions from the select list of a ReadPlan, and return the
-- updated select list and the list of hoisted aggregates. -- updated select list and the list of hoisted aggregates.
hoistFromSelectFields :: Alias -> [CoercibleSelectField] -> ([CoercibleSelectField], [HoistedAgg]) hoistFromSelectFields :: Alias -> [CoercibleSelectField] -> ([CoercibleSelectField], [HoistedAgg])
hoistFromSelectFields relAggAlias fields = hoistFromSelectFields aggAlias fields =
let (newFields, maybeAggs) = foldr processField ([], []) fields let (newFields, maybeAggs) = foldr processField ([], []) fields
in (newFields, catMaybes maybeAggs) in (newFields, catMaybes maybeAggs)
where where
@@ -741,21 +696,23 @@ hoistFromSelectFields relAggAlias fields =
let (modifiedField, maybeAgg) = modifyField field let (modifiedField, maybeAgg) = modifyField field
in (modifiedField : newFields, maybeAgg : aggList) in (modifiedField : newFields, maybeAgg : aggList)
modifyField field@CoercibleSelectField{csAggFunction=Just aggFunc, csField, csAggCast, csAlias} = modifyField field =
let determineFieldName = fromMaybe (cfName csField) csAlias case csAggFunction field of
updatedField = field {csAggFunction = Nothing, csAggCast = Nothing} Just aggFunc ->
hoistedField = Just ((relAggAlias, determineFieldName), (aggFunc, csAggCast, csAlias)) ( field { csAggFunction = Nothing, csAggCast = Nothing },
in (updatedField, hoistedField) Just ((aggAlias, determineFieldName field), (aggFunc, csAggCast field, csAlias field)))
modifyField field = (field, Nothing) Nothing -> (field, Nothing)
determineFieldName field = fromMaybe (cfName $ csField field) (csAlias field)
-- Taking the hoisted aggregates, modify the rel selects to apply the aggregates, -- Taking the hoisted aggregates, modify the rel selects to apply the aggregates,
-- and any applicable casts or aliases. -- and any applicable casts or aliases.
hoistIntoRelSelectFields :: [HoistedAgg] -> RelSelectField -> RelSelectField hoistIntoRelSelectFields :: [HoistedAgg] -> RelSelectField -> RelSelectField
hoistIntoRelSelectFields aggList r@(Spread {rsSpreadSel = spreadSelects, rsAggAlias = relAggAlias}) = hoistIntoRelSelectFields aggList r@(Spread {rsSpreadSel = spreadSelects, rsAggAlias = aggAlias}) =
r { rsSpreadSel = map updateSelect spreadSelects } r { rsSpreadSel = map updateSelect spreadSelects }
where where
updateSelect s = updateSelect s =
case lookup (relAggAlias, ssSelName s) aggList of case lookup (aggAlias, ssSelName s) aggList of
Just (aggFunc, aggCast, fldAlias) -> Just (aggFunc, aggCast, fldAlias) ->
s { ssSelAggFunction = Just aggFunc, s { ssSelAggFunction = Just aggFunc,
ssSelAggCast = aggCast, ssSelAggCast = aggCast,
@@ -763,45 +720,13 @@ hoistIntoRelSelectFields aggList r@(Spread {rsSpreadSel = spreadSelects, rsAggAl
Nothing -> s Nothing -> s
hoistIntoRelSelectFields _ r = r hoistIntoRelSelectFields _ r = r
-- | Handle ordering in a To-Many Spread Relationship validateAggFunctions :: Bool -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
-- * It removes the ordering done in the ReadPlan and moves it to the SpreadType.
-- We also select the ordering columns and alias them to avoid collisions. This is because it would be impossible
-- to order once it's aggregated if it's not selected in the inner query beforehand.
addToManyOrderSelects :: ReadPlanTree -> Either Error ReadPlanTree
addToManyOrderSelects (Node rp@ReadPlan{order, select, relAggAlias, relSelect, relSpread = Just ToManySpread {}} forest)
| anyAggSel || anyAggRelSel = Left $ ApiRequestError $ NotImplemented "Aggregates are not implemented for one-to-many or many-to-many spreads."
| otherwise = Node rp { order = [], relSpread = newRelSpread } <$> addToManyOrderSelects `traverse` forest
where
newRelSpread = Just ToManySpread { stExtraSelect = addSprExtraSelects, stOrder = addSprOrder}
anyAggSel = any (isJust . csAggFunction) select
anyAggRelSel = any (\case Spread sels _ -> any (isJust . ssSelAggFunction) sels; _ -> False) relSelect
(addSprExtraSelects, addSprOrder) = unzip $ zipWith ordToExtraSelsAndSprOrds [1..] order
ordToExtraSelsAndSprOrds i = \case
CoercibleOrderTerm fld dir ordr -> (
(Nothing, CoercibleSelectField fld Nothing Nothing Nothing (Just $ selOrdAlias (cfName fld) i)),
CoercibleOrderTerm (unknownField (selOrdAlias (cfName fld) i) []) dir ordr
)
CoercibleOrderRelationTerm rel (fld,jp) dir ordr -> (
(Just rel, CoercibleSelectField (unknownField fld jp) Nothing Nothing Nothing (Just $ selOrdAlias fld i)),
CoercibleOrderTerm (unknownField (selOrdAlias fld i) []) dir ordr
)
selOrdAlias :: Alias -> Integer -> Alias
selOrdAlias name i = relAggAlias <> "_" <> name <> "_" <> show i -- add index to avoid collisions in aliases
addToManyOrderSelects (Node rp forest) = Node rp <$> addToManyOrderSelects `traverse` forest
validateAggFunctions :: Bool -> ReadPlanTree -> Either Error ReadPlanTree
validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest) validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
| not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left $ ApiRequestError AggregatesNotAllowed | aggFunctionsAllowed = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
| any (isJust . csAggFunction) select = Left AggregatesNotAllowed
| otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest | otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
-- | Lookup table in the schema cache before creating read plan addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
findTable :: QualifiedIdentifier -> TablesMap -> Either Error QualifiedIdentifier
findTable qi@QualifiedIdentifier{..} tableMap =
case HM.lookup qi tableMap of
Nothing -> Left $ SchemaCacheErr $ TableNotFound qiSchema qiName (HM.elems tableMap)
Just _ -> Right qi
addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
addFilters ctx ApiRequest{..} rReq = addFilters ctx ApiRequest{..} rReq =
foldr addFilterToNode (Right rReq) flts foldr addFilterToNode (Right rReq) flts
where where
@@ -812,26 +737,29 @@ addFilters ctx ApiRequest{..} rReq =
ActDb (ActRoutine _ _) -> qsFilters ActDb (ActRoutine _ _) -> qsFilters
_ -> qsFiltersNotRoot _ -> qsFiltersNotRoot
addFilterToNode :: (EmbedPath, Filter) -> Either Error ReadPlanTree -> Either Error ReadPlanTree addFilterToNode :: (EmbedPath, Filter) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
addFilterToNode = addFilterToNode =
updateNode (\flt (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest (resolveFilter ctx{qi=fromTable} flt) lf} f) updateNode (\flt (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest (resolveFilter ctx{qi=fromTable} flt) lf} f)
addOrders :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree addOrders :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
addOrders ctx ApiRequest{..} rReq = foldr addOrderToNode (Right rReq) qsOrder addOrders ctx ApiRequest{..} rReq =
case iAction of
ActDb (ActRelationMut _ _) -> Right rReq
_ -> foldr addOrderToNode (Right rReq) qsOrder
where where
QueryParams.QueryParams{..} = iQueryParams QueryParams.QueryParams{..} = iQueryParams
addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either Error ReadPlanTree -> Either Error ReadPlanTree addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
addOrderToNode = updateNode (\o (Node q f) -> Node q{order=resolveOrder ctx <$> o} f) addOrderToNode = updateNode (\o (Node q f) -> Node q{order=resolveOrder ctx <$> o} f)
resolveOrder :: ResolverContext -> OrderTerm -> CoercibleOrderTerm resolveOrder :: ResolverContext -> OrderTerm -> CoercibleOrderTerm
resolveOrder _ (OrderRelationTerm a b c d) = CoercibleOrderRelationTerm a b c d resolveOrder _ (OrderRelationTerm a b c d) = CoercibleOrderRelationTerm a b c d
resolveOrder ctx (OrderTerm fld dir nulls) = CoercibleOrderTerm (resolveTypeOrUnknown ctx fld Nothing) dir nulls resolveOrder ctx (OrderTerm fld dir nulls) = CoercibleOrderTerm (resolveTypeOrUnknown ctx fld) dir nulls
-- Validates that the related resource on the order is an embedded resource, -- Validates that the related resource on the order is an embedded resource,
-- e.g. if `clients` is inside the `select` in /projects?order=clients(id)&select=*,clients(*), -- e.g. if `clients` is inside the `select` in /projects?order=clients(id)&select=*,clients(*),
-- and if it's a to-one relationship, it adds the right alias to the OrderRelationTerm so the generated query can succeed. -- and if it's a to-one relationship, it adds the right alias to the OrderRelationTerm so the generated query can succeed.
addRelatedOrders :: ReadPlanTree -> Either Error ReadPlanTree addRelatedOrders :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
newOrder <- newRelOrder `traverse` order newOrder <- newRelOrder `traverse` order
Node rp{order=newOrder} <$> addRelatedOrders `traverse` forest Node rp{order=newOrder} <$> addRelatedOrders `traverse` forest
@@ -845,9 +773,9 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
name = fromMaybe relName relAlias in name = fromMaybe relName relAlias in
if isToOne == Just True if isToOne == Just True
then Right $ cot{coRelation=relAggAlias} then Right $ cot{coRelation=relAggAlias}
else Left $ ApiRequestError $ RelatedOrderNotToOne (qiName from) name else Left $ RelatedOrderNotToOne (qiName from) name
Nothing -> Nothing ->
Left $ ApiRequestError $ NotEmbedded coRelation Left $ NotEmbedded coRelation
-- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null` -- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null`
-- --
@@ -855,8 +783,8 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
-- --
-- Setup: -- Setup:
-- --
-- >>> let nullOp = OpExpr True (Is IsNull) -- >>> let nullOp = OpExpr True (Is TriNull)
-- >>> let nonNullOp = OpExpr False (Is IsNull) -- >>> let nonNullOp = OpExpr False (Is TriNull)
-- >>> let notEqOp = OpExpr True (Op OpNotEqual "val") -- >>> let notEqOp = OpExpr True (Op OpNotEqual "val")
-- >>> :{ -- >>> :{
-- -- this represents the `projects(*)` part on `/clients?select=*,projects(*)` -- -- this represents the `projects(*)` part on `/clients?select=*,projects(*)`
@@ -871,7 +799,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
-- relName = "projects", -- relName = "projects",
-- relToParent = Nothing, -- relToParent = Nothing,
-- relJoinConds = [], -- relJoinConds = [],
-- relAlias = Nothing, relAggAlias = "clients_projects_1", relHint = Nothing, relJoinType = Nothing, relSpread = Nothing, depth = 1, -- relAlias = Nothing, relAggAlias = "clients_projects_1", relHint = Nothing, relJoinType = Nothing, relIsSpread = False, depth = 1,
-- relSelect = [] -- relSelect = []
-- }, -- },
-- subForest = [] -- subForest = []
@@ -891,13 +819,13 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
-- where_ = [ -- where_ = [
-- CoercibleStmnt ( -- CoercibleStmnt (
-- CoercibleFilter { -- CoercibleFilter {
-- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson=False, cfToTsVector = Nothing, cfIRType = "", cfBaseType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, -- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson=False, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing},
-- opExpr = op -- opExpr = op
-- } -- }
-- ) -- )
-- ], -- ],
-- order = [], range_ = fullRange, relName = "clients", relToParent = Nothing, relJoinConds = [], relAlias = Nothing, relAggAlias = "", relHint = Nothing, -- order = [], range_ = fullRange, relName = "clients", relToParent = Nothing, relJoinConds = [], relAlias = Nothing, relAggAlias = "", relHint = Nothing,
-- relJoinType = Nothing, relSpread = Nothing, depth = 0, -- relJoinType = Nothing, relIsSpread = False, depth = 0,
-- relSelect = [] -- relSelect = []
-- }, -- },
-- subForest = subForst -- subForest = subForst
@@ -907,7 +835,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter. -- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
-- --
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp []) -- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfToTsVector = Nothing, cfIRType = "", cfBaseType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is IsNull)})] -- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing}, opExpr = OpExpr True (Is TriNull)})]
-- --
-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later. -- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.
-- --
@@ -916,43 +844,43 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
-- --
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nonNullOp subForestPlan) -- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nonNullOp subForestPlan)
-- Right [CoercibleStmnt (CoercibleFilterNullEmbed False "clients_projects_1")] -- Right [CoercibleStmnt (CoercibleFilterNullEmbed False "clients_projects_1")]
addNullEmbedFilters :: ReadPlanTree -> Either Error ReadPlanTree addNullEmbedFilters :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
let forestReadPlans = rootLabel <$> forest let forestReadPlans = rootLabel <$> forest
newLogic <- newNullFilters forestReadPlans `traverse` curLogic newLogic <- newNullFilters forestReadPlans `traverse` curLogic
Node rp{ReadPlan.where_= newLogic} <$> (addNullEmbedFilters `traverse` forest) Node rp{ReadPlan.where_= newLogic} <$> (addNullEmbedFilters `traverse` forest)
where where
newNullFilters :: [ReadPlan] -> CoercibleLogicTree -> Either Error CoercibleLogicTree newNullFilters :: [ReadPlan] -> CoercibleLogicTree -> Either ApiRequestError CoercibleLogicTree
newNullFilters rPlans = \case newNullFilters rPlans = \case
(CoercibleExpr b lOp trees) -> (CoercibleExpr b lOp trees) ->
CoercibleExpr b lOp <$> (newNullFilters rPlans `traverse` trees) CoercibleExpr b lOp <$> (newNullFilters rPlans `traverse` trees)
flt@(CoercibleStmnt (CoercibleFilter CoercibleField{cfName=fld, cfJsonPath=[]} opExpr)) -> flt@(CoercibleStmnt (CoercibleFilter (CoercibleField fld [] _ _ _ _) opExpr)) ->
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
case (foundRP, opExpr) of case (foundRP, opExpr) of
(Just ReadPlan{relAggAlias}, OpExpr b (Is IsNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias (Just ReadPlan{relAggAlias}, OpExpr b (Is TriNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
_ -> Right flt _ -> Right flt
flt@(CoercibleStmnt _) -> flt@(CoercibleStmnt _) ->
Right flt Right flt
addRanges :: ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree addRanges :: ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
addRanges ApiRequest{..} rReq = addRanges ApiRequest{..} rReq =
case iAction of case iAction of
ActDb (ActRelationMut _ _) -> Right rReq ActDb (ActRelationMut _ _) -> Right rReq
_ -> foldr addRangeToNode (Right rReq) =<< ranges _ -> foldr addRangeToNode (Right rReq) =<< ranges
where where
ranges :: Either Error [(EmbedPath, NonnegRange)] ranges :: Either ApiRequestError [(EmbedPath, NonnegRange)]
ranges = first (ApiRequestError . QueryParamError) $ QueryParams.pRequestRange `traverse` HM.toList iRange ranges = first QueryParamError $ QueryParams.pRequestRange `traverse` HM.toList iRange
addRangeToNode :: (EmbedPath, NonnegRange) -> Either Error ReadPlanTree -> Either Error ReadPlanTree addRangeToNode :: (EmbedPath, NonnegRange) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
addRangeToNode = updateNode (\r (Node q f) -> Node q{range_=r} f) addRangeToNode = updateNode (\r (Node q f) -> Node q{range_=r} f)
addLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree addLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
addLogicTrees ctx ApiRequest{..} rReq = addLogicTrees ctx ApiRequest{..} rReq =
foldr addLogicTreeToNode (Right rReq) qsLogic foldr addLogicTreeToNode (Right rReq) qsLogic
where where
QueryParams.QueryParams{..} = iQueryParams QueryParams.QueryParams{..} = iQueryParams
addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either Error ReadPlanTree -> Either Error ReadPlanTree addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
addLogicTreeToNode = updateNode (\t (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=resolveLogicTree ctx{qi=fromTable} t:lf} f) addLogicTreeToNode = updateNode (\t (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=resolveLogicTree ctx{qi=fromTable} t:lf} f)
resolveLogicTree :: ResolverContext -> LogicTree -> CoercibleLogicTree resolveLogicTree :: ResolverContext -> LogicTree -> CoercibleLogicTree
@@ -960,15 +888,24 @@ resolveLogicTree ctx (Stmnt flt) = CoercibleStmnt $ resolveFilter ctx flt
resolveLogicTree ctx (Expr b op lts) = CoercibleExpr b op (map (resolveLogicTree ctx) lts) resolveLogicTree ctx (Expr b op lts) = CoercibleExpr b op (map (resolveLogicTree ctx) lts)
resolveFilter :: ResolverContext -> Filter -> CoercibleFilter resolveFilter :: ResolverContext -> Filter -> CoercibleFilter
resolveFilter ctx (Filter fld opExpr) = CoercibleFilter{field=resolveQueryInputField ctx fld opExpr, opExpr=opExpr} resolveFilter ctx (Filter fld opExpr) = CoercibleFilter{field=resolveQueryInputField ctx fld, opExpr=opExpr}
-- Validates that spread embeds are only done on to-one relationships
validateSpreadEmbeds :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
validateSpreadEmbeds (Node rp@ReadPlan{relToParent=Nothing} forest) = Node rp <$> validateSpreadEmbeds `traverse` forest
validateSpreadEmbeds (Node rp@ReadPlan{relIsSpread,relToParent=Just rel,relName} forest) = do
validRP <- if relIsSpread && not (relIsToOne rel)
then Left $ SpreadNotToOne (qiName $ relTable rel) relName -- TODO using relTable is not entirely right because ReadPlan might have an alias, need to store the parent alias on ReadPlan
else Right rp
Node validRP <$> validateSpreadEmbeds `traverse` forest
-- Find a Node of the Tree and apply a function to it -- Find a Node of the Tree and apply a function to it
updateNode :: (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either Error ReadPlanTree -> Either Error ReadPlanTree updateNode :: (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
updateNode f ([], a) rr = f a <$> rr updateNode f ([], a) rr = f a <$> rr
updateNode _ _ (Left e) = Left e updateNode _ _ (Left e) = Left e
updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) = updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
case findNode of case findNode of
Nothing -> Left $ ApiRequestError $ NotEmbedded targetNodeName Nothing -> Left $ NotEmbedded targetNodeName
Just target -> Just target ->
(\node -> Node rootNode $ node : delete target forest) <$> (\node -> Node rootNode $ node : delete target forest) <$>
updateNode f (remainingPath, a) (Right target) updateNode f (remainingPath, a) (Right target)
@@ -977,12 +914,12 @@ updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> relName == targetNodeName || relAlias == Just targetNodeName) forest findNode = find (\(Node ReadPlan{relName, relAlias} _) -> relName == targetNodeName || relAlias == Just targetNodeName) forest
mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan
mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq = mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq = mapLeft ApiRequestError $
case mutation of case mutation of
MutationCreate -> MutationCreate ->
mapRight (\typedColumns -> Insert qi typedColumns body ((,) <$> preferResolution <*> Just confCols) [] returnings pkCols applyDefaults) typedColumnsOrError mapRight (\typedColumns -> Insert qi typedColumns body ((,) <$> preferResolution <*> Just confCols) [] returnings pkCols applyDefaults) typedColumnsOrError
MutationUpdate -> MutationUpdate ->
mapRight (\typedColumns -> Update qi typedColumns body combinedLogic returnings applyDefaults) typedColumnsOrError mapRight (\typedColumns -> Update qi typedColumns body combinedLogic iTopLevelRange rootOrder returnings applyDefaults) typedColumnsOrError
MutationSingleUpsert -> MutationSingleUpsert ->
if null qsLogic && if null qsLogic &&
qsFilterFields == S.fromList pkCols && qsFilterFields == S.fromList pkCols &&
@@ -992,8 +929,8 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
_ -> False) qsFiltersRoot _ -> False) qsFiltersRoot
then mapRight (\typedColumns -> Insert qi typedColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty False) typedColumnsOrError then mapRight (\typedColumns -> Insert qi typedColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty False) typedColumnsOrError
else else
Left $ ApiRequestError InvalidFilters Left InvalidFilters
MutationDelete -> Right $ Delete qi combinedLogic returnings MutationDelete -> Right $ Delete qi combinedLogic iTopLevelRange rootOrder returnings
where where
ctx = ResolverContext dbTables dbRepresentations qi "json" ctx = ResolverContext dbTables dbRepresentations qi "json"
confCols = fromMaybe pkCols qsOnConflict confCols = fromMaybe pkCols qsOnConflict
@@ -1001,72 +938,58 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
returnings = returnings =
if preferRepresentation == Just None || isNothing preferRepresentation if preferRepresentation == Just None || isNothing preferRepresentation
then [] then []
else S.toList $ inferColsEmbedNeeds readReq pkCols else inferColsEmbedNeeds readReq pkCols
-- TODO: remove fromJust by refactoring later tbl = HM.lookup qi dbTables
-- we can use fromJust, we have already looked up the table before building mutatePlan pkCols = maybe mempty tablePKCols tbl
tbl = fromJust $ HM.lookup qi dbTables
pkCols = maybe mempty tablePKCols (Just tbl)
logic = map (resolveLogicTree ctx . snd) qsLogic logic = map (resolveLogicTree ctx . snd) qsLogic
rootOrder = resolveOrder ctx <$> maybe [] snd (find (\(x, _) -> null x) qsOrder)
combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates) body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
applyDefaults = preferMissing == Just ApplyDefaults applyDefaults = preferMissing == Just ApplyDefaults
typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList iColumns typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList iColumns
resolveOrError :: ResolverContext -> Table -> FieldName -> Either Error CoercibleField resolveOrError :: ResolverContext -> Maybe Table -> FieldName -> Either ApiRequestError CoercibleField
resolveOrError ctx table field = case resolveTableFieldName table field Nothing of resolveOrError _ Nothing _ = Left NotFound
CoercibleField{cfIRType=""} -> Left $ SchemaCacheErr $ ColumnNotFound (tableName table) field resolveOrError ctx (Just table) field =
case resolveTableFieldName table field of
CoercibleField{cfIRType=""} -> Left $ ColumnNotFound (tableName table) field
cf -> Right $ withJsonParse ctx cf cf -> Right $ withJsonParse ctx cf
callPlan :: Routine -> ApiRequest -> S.Set FieldName -> CallArgs -> ReadPlanTree -> CallPlan callPlan :: Routine -> ApiRequest -> S.Set FieldName -> LBS.ByteString -> ReadPlanTree -> CallPlan
callPlan proc ApiRequest{} paramKeys args readReq = FunctionCall { callPlan proc ApiRequest{iPreferences=Preferences{..}} paramKeys args readReq = FunctionCall {
funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc) funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc)
, funCParams = callParams , funCParams = callParams
, funCArgs = args , funCArgs = Just args
, funCScalar = funcReturnsScalar proc , funCScalar = funcReturnsScalar proc
, funCSetOfScalar = funcReturnsSetOfScalar proc , funCSetOfScalar = funcReturnsSetOfScalar proc
, funCRetCompositeAlias = funcReturnsCompositeAlias proc , funCRetCompositeAlias = funcReturnsCompositeAlias proc
, funCFilterFields = getFilterFieldNames readReq
, funCReturning = inferColsEmbedNeeds readReq [] , funCReturning = inferColsEmbedNeeds readReq []
} }
where where
paramsAsSingleObject = preferParameters == Just SingleObject
specifiedParams = filter (\x -> ppName x `S.member` paramKeys) specifiedParams = filter (\x -> ppName x `S.member` paramKeys)
callParams = case pdParams proc of callParams = case pdParams proc of
[prm] | ppName prm == mempty -> OnePosParam prm [prm] | paramsAsSingleObject -> OnePosParam prm
| ppName prm == mempty -> OnePosParam prm
| otherwise -> KeyParams $ specifiedParams [prm] | otherwise -> KeyParams $ specifiedParams [prm]
prms -> KeyParams $ specifiedParams prms prms -> KeyParams $ specifiedParams prms
-- | Get filter fields/column names from read plan
getFilterFieldNames :: ReadPlanTree -> Set FieldName
getFilterFieldNames rpt = S.fromList $ foldr (\rp names -> names <> rpToFieldNames rp) [] rpt
where
rpToFieldNames :: ReadPlan -> [FieldName]
rpToFieldNames = logicTreesToFieldName . ReadPlan.where_
logicTreesToFieldName :: [CoercibleLogicTree] -> [FieldName]
logicTreesToFieldName = concatMap coLogicTreeToFieldNames
coLogicTreeToFieldNames :: CoercibleLogicTree -> [FieldName]
coLogicTreeToFieldNames = \case
CoercibleStmnt (CoercibleFilter{field=CoercibleField{cfName}}) -> [cfName]
CoercibleStmnt (CoercibleFilterNullEmbed _ cfName) -> [cfName] -- needs test coverage
CoercibleExpr _ _ clts -> concatMap coLogicTreeToFieldNames clts
-- | Infers the columns needed for an embed to be successful after a mutation or a function call. -- | Infers the columns needed for an embed to be successful after a mutation or a function call.
inferColsEmbedNeeds :: ReadPlanTree -> [FieldName] -> S.Set FieldName inferColsEmbedNeeds :: ReadPlanTree -> [FieldName] -> [FieldName]
inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
-- if * is part of the select, we must not add pk or fk columns manually - -- if * is part of the select, we must not add pk or fk columns manually -
-- otherwise those would be selected and output twice -- otherwise those would be selected and output twice
| "*" `S.member` fldNames = S.singleton "*" | "*" `elem` fldNames = ["*"]
| otherwise = returnings | otherwise = returnings
where where
fldNames = S.fromList $ cfName . csField <$> select fldNames = cfName . csField <$> select
-- Without fkCols, when a mutatePlan to -- Without fkCols, when a mutatePlan to
-- /projects?select=name,clients(name) occurs, the RETURNING SQL part would -- /projects?select=name,clients(name) occurs, the RETURNING SQL part would
-- be `RETURNING name`(see QueryBuilder). This would make the embedding -- be `RETURNING name`(see QueryBuilder). This would make the embedding
-- fail because the following JOIN would need the "client_id" column from -- fail because the following JOIN would need the "client_id" column from
-- projects. So this adds the foreign key columns to ensure the embedding -- projects. So this adds the foreign key columns to ensure the embedding
-- succeeds, result would be `RETURNING name, client_id`. -- succeeds, result would be `RETURNING name, client_id`.
fkCols = S.fromList $ concat $ mapMaybe (\case fkCols = concat $ mapMaybe (\case
Node ReadPlan{relToParent=Just Relationship{relCardinality=O2M _ cols}} _ -> Node ReadPlan{relToParent=Just Relationship{relCardinality=O2M _ cols}} _ ->
Just $ fst <$> cols Just $ fst <$> cols
Node ReadPlan{relToParent=Just Relationship{relCardinality=M2O _ cols}} _ -> Node ReadPlan{relToParent=Just Relationship{relCardinality=M2O _ cols}} _ ->
@@ -1093,8 +1016,8 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
-- INSERT/POST -- INSERT/POST
returnings = returnings =
if not hasComputedRel if not hasComputedRel
then fldNames <> fkCols <> S.fromList pkCols then S.toList . S.fromList $ fldNames ++ fkCols ++ pkCols
else S.singleton "*" -- on computed relationships we cannot know the required columns for an embedding to succeed, so we just return all else ["*"] -- on computed relationships we cannot know the required columns for an embedding to succeed, so we just return all
-- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree -- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree
-- they are later concatenated with AND in the QueryBuilder -- they are later concatenated with AND in the QueryBuilder
+16 -24
View File
@@ -2,9 +2,7 @@
module PostgREST.Plan.CallPlan module PostgREST.Plan.CallPlan
( CallPlan(..) ( CallPlan(..)
, CallParams(..) , CallParams(..)
, CallArgs(..) , jsonRpcParams
, RpcParamValue(..)
, toRpcParams
) )
where where
@@ -21,38 +19,25 @@ import Protolude
data CallPlan = FunctionCall data CallPlan = FunctionCall
{ funCQi :: QualifiedIdentifier { funCQi :: QualifiedIdentifier
, funCParams :: CallParams , funCParams :: CallParams
, funCArgs :: CallArgs , funCArgs :: Maybe LBS.ByteString
, funCScalar :: Bool , funCScalar :: Bool
, funCSetOfScalar :: Bool , funCSetOfScalar :: Bool
, funCRetCompositeAlias :: Bool , funCRetCompositeAlias :: Bool
, funCFilterFields :: Set FieldName , funCReturning :: [FieldName]
, funCReturning :: Set FieldName
} }
data CallParams data CallParams
= KeyParams [RoutineParam] -- ^ Call with key params: func(a := val1, b:= val2) = KeyParams [RoutineParam] -- ^ Call with key params: func(a := val1, b:= val2)
| OnePosParam RoutineParam -- ^ Call with positional params(only one supported): func(val) | OnePosParam RoutineParam -- ^ Call with positional params(only one supported): func(val)
data CallArgs
= DirectArgs (HM.HashMap Text RpcParamValue)
| JsonArgs (Maybe LBS.ByteString)
-- | RPC query param value `/rpc/func?v=<value>`, used for VARIADIC functions on form-urlencoded POST and GETs
-- | It can be fixed `?v=1` or repeated `?v=1&v=2&v=3.
data RpcParamValue = Fixed Text | Variadic [Text]
instance JSON.ToJSON RpcParamValue where
toJSON (Fixed v) = JSON.toJSON v
-- Not possible to get here anymore. Variadic arguments are only supported for
-- true variadic arguments, but the toJSON instance is only used for the "single unnamed json argument" case.
toJSON (Variadic v) = JSON.toJSON v
-- | Convert rpc params `/rpc/func?a=val1&b=val2` to json `{"a": "val1", "b": "val2"} -- | Convert rpc params `/rpc/func?a=val1&b=val2` to json `{"a": "val1", "b": "val2"}
toRpcParams :: Routine -> [(Text, Text)] -> HM.HashMap Text RpcParamValue jsonRpcParams :: Routine -> [(Text, Text)] -> LBS.ByteString
toRpcParams proc prms = jsonRpcParams proc prms =
if not $ pdHasVariadic proc then -- if proc has no variadic param, save steps and directly convert to map if not $ pdHasVariadic proc then -- if proc has no variadic param, save steps and directly convert to json
HM.fromList $ second Fixed <$> prms JSON.encode $ HM.fromList $ second JSON.toJSON <$> prms
else else
HM.fromListWith mergeParams $ toRpcParamValue proc <$> prms let paramsMap = HM.fromListWith mergeParams $ toRpcParamValue proc <$> prms in
JSON.encode paramsMap
where where
mergeParams :: RpcParamValue -> RpcParamValue -> RpcParamValue mergeParams :: RpcParamValue -> RpcParamValue -> RpcParamValue
mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a
@@ -63,3 +48,10 @@ toRpcParamValue proc (k, v) | prmIsVariadic k = (k, Variadic [v])
| otherwise = (k, Fixed v) | otherwise = (k, Fixed v)
where where
prmIsVariadic prm = isJust $ find (\RoutineParam{ppName, ppVar} -> ppName == prm && ppVar) $ pdParams proc prmIsVariadic prm = isJust $ find (\RoutineParam{ppName, ppVar} -> ppName == prm && ppVar) $ pdParams proc
-- | RPC query param value `/rpc/func?v=<value>`, used for VARIADIC functions on form-urlencoded POST and GETs
-- | It can be fixed `?v=1` or repeated `?v=1&v=2&v=3.
data RpcParamValue = Fixed Text | Variadic [Text]
instance JSON.ToJSON RpcParamValue where
toJSON (Fixed v) = JSON.toJSON v
toJSON (Variadic v) = JSON.toJSON v
+7 -1
View File
@@ -7,7 +7,9 @@ import qualified Data.ByteString.Lazy as LBS
import PostgREST.ApiRequest.Preferences (PreferResolution) import PostgREST.ApiRequest.Preferences (PreferResolution)
import PostgREST.Plan.Types (CoercibleField, import PostgREST.Plan.Types (CoercibleField,
CoercibleLogicTree) CoercibleLogicTree,
CoercibleOrderTerm)
import PostgREST.RangeQuery (NonnegRange)
import PostgREST.SchemaCache.Identifiers (FieldName, import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier) QualifiedIdentifier)
@@ -30,11 +32,15 @@ data MutatePlan
, updCols :: [CoercibleField] , updCols :: [CoercibleField]
, updBody :: Maybe LBS.ByteString , updBody :: Maybe LBS.ByteString
, where_ :: [CoercibleLogicTree] , where_ :: [CoercibleLogicTree]
, mutRange :: NonnegRange
, mutOrder :: [CoercibleOrderTerm]
, returning :: [FieldName] , returning :: [FieldName]
, applyDefs :: Bool , applyDefs :: Bool
} }
| Delete | Delete
{ in_ :: QualifiedIdentifier { in_ :: QualifiedIdentifier
, where_ :: [CoercibleLogicTree] , where_ :: [CoercibleLogicTree]
, mutRange :: NonnegRange
, mutOrder :: [CoercibleOrderTerm]
, returning :: [FieldName] , returning :: [FieldName]
} }
+2 -5
View File
@@ -2,7 +2,6 @@ module PostgREST.Plan.ReadPlan
( ReadPlanTree ( ReadPlanTree
, ReadPlan(..) , ReadPlan(..)
, JoinCondition(..) , JoinCondition(..)
, SpreadType(..)
) where ) where
import Data.Tree (Tree (..)) import Data.Tree (Tree (..))
@@ -12,8 +11,7 @@ import PostgREST.ApiRequest.Types (Alias, Depth, Hint,
import PostgREST.Plan.Types (CoercibleLogicTree, import PostgREST.Plan.Types (CoercibleLogicTree,
CoercibleOrderTerm, CoercibleOrderTerm,
CoercibleSelectField (..), CoercibleSelectField (..),
RelSelectField (..), RelSelectField (..))
SpreadType (..))
import PostgREST.RangeQuery (NonnegRange) import PostgREST.RangeQuery (NonnegRange)
import PostgREST.SchemaCache.Identifiers (FieldName, import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier) QualifiedIdentifier)
@@ -30,7 +28,6 @@ data JoinCondition =
(QualifiedIdentifier, FieldName) (QualifiedIdentifier, FieldName)
deriving (Eq, Show) deriving (Eq, Show)
-- TODO: Enforce uniqueness of columns by changing to a Set instead of a List where applicable
data ReadPlan = ReadPlan data ReadPlan = ReadPlan
{ select :: [CoercibleSelectField] { select :: [CoercibleSelectField]
, from :: QualifiedIdentifier , from :: QualifiedIdentifier
@@ -45,7 +42,7 @@ data ReadPlan = ReadPlan
, relAggAlias :: Alias , relAggAlias :: Alias
, relHint :: Maybe Hint , relHint :: Maybe Hint
, relJoinType :: Maybe JoinType , relJoinType :: Maybe JoinType
, relSpread :: Maybe SpreadType , relIsSpread :: Bool
, relSelect :: [RelSelectField] , relSelect :: [RelSelectField]
, depth :: Depth , depth :: Depth
-- ^ used for aliasing -- ^ used for aliasing
+9 -26
View File
@@ -5,18 +5,15 @@ module PostgREST.Plan.Types
, CoercibleLogicTree(..) , CoercibleLogicTree(..)
, CoercibleFilter(..) , CoercibleFilter(..)
, TransformerProc , TransformerProc
, ToTsVector(..)
, CoercibleOrderTerm(..) , CoercibleOrderTerm(..)
, RelSelectField(..) , RelSelectField(..)
, RelJsonEmbedMode(..) , RelJsonEmbedMode(..)
, SpreadSelectField(..) , SpreadSelectField(..)
, SpreadType(..)
) where ) where
import PostgREST.ApiRequest.Types (AggregateFunction, Alias, Cast, import PostgREST.ApiRequest.Types (AggregateFunction, Alias, Cast,
Field, JsonPath, Language, Field, JsonPath, LogicOperator,
LogicOperator, OpExpr, OpExpr, OrderDirection, OrderNulls)
OrderDirection, OrderNulls)
import PostgREST.SchemaCache.Identifiers (FieldName) import PostgREST.SchemaCache.Identifiers (FieldName)
@@ -24,9 +21,6 @@ import Protolude
type TransformerProc = Text type TransformerProc = Text
newtype ToTsVector = ToTsVector (Maybe Language)
deriving (Eq, Show)
-- | A CoercibleField pairs the name of a query element with any type coercion information we need for some specific use case. -- | A CoercibleField pairs the name of a query element with any type coercion information we need for some specific use case.
-- | -- |
-- | As suggested by the name, it's often a reference to a field in a table but really it can be any nameable element (function parameter, calculation with an alias, etc) with a knowable type. -- | As suggested by the name, it's often a reference to a field in a table but really it can be any nameable element (function parameter, calculation with an alias, etc) with a knowable type.
@@ -39,19 +33,16 @@ newtype ToTsVector = ToTsVector (Maybe Language)
-- | -- |
-- | The type value is allowed to be the empty string. The analog here is soft type checking in programming languages: sometimes we don't need a variable to have a specified type and things will work anyhow. So the empty type variant is valid when we don't know and *don't need to know* about the specific type in some context. Note that this variation should not be used if it guarantees failure: in that case you should instead raise an error at the planning stage and bail out. For example, we can't parse JSON with `json_to_recordset` without knowing the types of each recipient field, and so error out. Using the empty string for the type would be incorrect and futile. On the other hand we use the empty type for RPC calls since type resolution isn't implemented for RPC, but it's fine because the query still works with Postgres' implicit coercion. In the future, hopefully we will support data representations across the board and then the empty type may be permanently retired. -- | The type value is allowed to be the empty string. The analog here is soft type checking in programming languages: sometimes we don't need a variable to have a specified type and things will work anyhow. So the empty type variant is valid when we don't know and *don't need to know* about the specific type in some context. Note that this variation should not be used if it guarantees failure: in that case you should instead raise an error at the planning stage and bail out. For example, we can't parse JSON with `json_to_recordset` without knowing the types of each recipient field, and so error out. Using the empty string for the type would be incorrect and futile. On the other hand we use the empty type for RPC calls since type resolution isn't implemented for RPC, but it's fine because the query still works with Postgres' implicit coercion. In the future, hopefully we will support data representations across the board and then the empty type may be permanently retired.
data CoercibleField = CoercibleField data CoercibleField = CoercibleField
{ cfName :: FieldName { cfName :: FieldName
, cfJsonPath :: JsonPath , cfJsonPath :: JsonPath
, cfToJson :: Bool , cfToJson :: Bool
, cfToTsVector :: Maybe ToTsVector -- ^ If the field should be converted using to_tsvector(<language>, <field>) , cfIRType :: Text -- ^ The native Postgres type of the field, the intermediate (IR) type before mapping.
, cfIRType :: Text -- ^ The native Postgres type of the field, the intermediate (IR) type before mapping. , cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
, cfBaseType :: Text -- ^ The base type of the field in case of domains, or just the type otherwise (without modifiers in case of pg_catalog types) , cfDefault :: Maybe Text
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
, cfDefault :: Maybe Text
, cfFullRow :: Bool -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spreaded resources.
} deriving (Eq, Show) } deriving (Eq, Show)
unknownField :: FieldName -> JsonPath -> CoercibleField unknownField :: FieldName -> JsonPath -> CoercibleField
unknownField name path = CoercibleField name path False Nothing "" "" Nothing Nothing False unknownField name path = CoercibleField name path False "" Nothing Nothing
-- | Like an API request LogicTree, but with coercible field information. -- | Like an API request LogicTree, but with coercible field information.
data CoercibleLogicTree data CoercibleLogicTree
@@ -113,11 +104,3 @@ data SpreadSelectField =
, ssSelAlias :: Maybe Alias , ssSelAlias :: Maybe Alias
} }
deriving (Eq, Show) deriving (Eq, Show)
data SpreadType
= ToOneSpread
| ToManySpread
{ stExtraSelect :: [(Maybe FieldName, CoercibleSelectField)]
, stOrder :: [CoercibleOrderTerm]
}
deriving (Eq, Show)
+133 -121
View File
@@ -2,31 +2,32 @@
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
-- TODO: This module shouldn't depend on SchemaCache -- TODO: This module shouldn't depend on SchemaCache
module PostgREST.Query module PostgREST.Query
( Query (..) ( QueryResult (..)
, QueryResult (..) , runQuery
, query
, getSQLQuery
) where ) where
import Control.Monad.Except (liftEither)
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
import qualified Data.Aeson.KeyMap as KM import qualified Data.Aeson.KeyMap as KM
import qualified Data.ByteString as BS import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy.Char8 as LBS import qualified Data.ByteString.Lazy.Char8 as LBS
import Data.Either.Combinators (mapLeft)
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import qualified Data.Set as S import qualified Data.Set as S
import qualified Hasql.Decoders as HD import qualified Hasql.Decoders as HD
import qualified Hasql.DynamicStatements.Snippet as SQL (Snippet) import qualified Hasql.DynamicStatements.Snippet as SQL (Snippet)
import qualified Hasql.DynamicStatements.Statement as SQL import qualified Hasql.DynamicStatements.Statement as SQL
import qualified Hasql.Session as SQL (Session)
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 qualified PostgREST.ApiRequest.Types as ApiRequestTypes
import qualified PostgREST.AppState as AppState
import qualified PostgREST.Error as Error import qualified PostgREST.Error as Error
import qualified PostgREST.Query.QueryBuilder as QueryBuilder import qualified PostgREST.Query.QueryBuilder as QueryBuilder
import qualified PostgREST.Query.Statements as Statements import qualified PostgREST.Query.Statements as Statements
import qualified PostgREST.RangeQuery as RangeQuery
import qualified PostgREST.SchemaCache as SchemaCache import qualified PostgREST.SchemaCache as SchemaCache
import PostgREST.ApiRequest (ApiRequest (..), import PostgREST.ApiRequest (ApiRequest (..),
Mutation (..)) Mutation (..))
import PostgREST.ApiRequest.Preferences (PreferCount (..), import PostgREST.ApiRequest.Preferences (PreferCount (..),
@@ -36,7 +37,7 @@ import PostgREST.ApiRequest.Preferences (PreferCount (..),
PreferTransaction (..), PreferTransaction (..),
Preferences (..), Preferences (..),
shouldCount) shouldCount)
import PostgREST.Auth.Types (AuthResult (..)) import PostgREST.Auth (AuthResult (..))
import PostgREST.Config (AppConfig (..), import PostgREST.Config (AppConfig (..),
OpenAPIMode (..)) OpenAPIMode (..))
import PostgREST.Config.PgVersion (PgVersion (..)) import PostgREST.Config.PgVersion (PgVersion (..))
@@ -49,6 +50,7 @@ import PostgREST.Plan (ActionPlan (..),
InfoPlan (..), InfoPlan (..),
InspectPlan (..)) InspectPlan (..))
import PostgREST.Plan.MutatePlan (MutatePlan (..)) import PostgREST.Plan.MutatePlan (MutatePlan (..))
import PostgREST.Plan.ReadPlan (ReadPlanTree)
import PostgREST.Query.SqlFragment (escapeIdentList, fromQi, import PostgREST.Query.SqlFragment (escapeIdentList, fromQi,
intercalateSnippet, intercalateSnippet,
setConfigWithConstantName, setConfigWithConstantName,
@@ -57,43 +59,41 @@ import PostgREST.Query.SqlFragment (escapeIdentList, fromQi,
import PostgREST.Query.Statements (ResultSet (..)) import PostgREST.Query.Statements (ResultSet (..))
import PostgREST.SchemaCache (SchemaCache (..)) import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..)) import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
import PostgREST.SchemaCache.Routine (Routine (..), RoutineMap) import PostgREST.SchemaCache.Routine (MediaHandler, Routine (..),
RoutineMap)
import PostgREST.SchemaCache.Table (TablesMap) import PostgREST.SchemaCache.Table (TablesMap)
import Protolude hiding (Handler) import Protolude hiding (Handler)
type DbHandler = ExceptT Error SQL.Transaction type DbHandler = ExceptT Error SQL.Transaction
data Query
= DbQuery {
dqIsoLevel :: SQL.IsolationLevel
, dqTxMode :: SQL.Mode
, dqDbHandler :: DbHandler QueryResult
, dqTransaction :: SQL.IsolationLevel -> SQL.Mode -> SQL.Transaction (Either Error QueryResult) -> SQL.Session (Either Error QueryResult)
, dqSQL :: ByteString
}
| NoDbQuery QueryResult
data QueryResult data QueryResult
= DbCrudResult CrudPlan ResultSet = DbCrudResult CrudPlan ResultSet
| DbCallResult CallReadPlan ResultSet | DbCallResult CallReadPlan ResultSet
| MaybeDbResult InspectPlan (Maybe (TablesMap, RoutineMap, Maybe Text)) | MaybeDbResult InspectPlan (Maybe (TablesMap, RoutineMap, Maybe Text))
| NoDbResult InfoPlan | NoDbResult InfoPlan
query :: AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> PgVersion -> Query -- TODO This function needs to be free from IO, only App.hs should do IO
query _ _ _ (NoDb x) _ _ = NoDbQuery $ NoDbResult x runQuery :: AppState.AppState -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> PgVersion -> Bool -> ExceptT Error IO QueryResult
query config AuthResult{..} apiReq (Db plan) sCache pgVer = runQuery _ _ _ _ (NoDb x) _ _ _ = pure $ NoDbResult x
DbQuery isoLvl txMode dbHandler transaction mainSQLQuery runQuery appState config AuthResult{..} apiReq (Db plan) sCache pgVer authenticated = do
dbResp <- lift $ do
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction
AppState.usePool appState (transaction isoLvl txMode $ runExceptT dbHandler)
resp <-
liftEither . mapLeft Error.PgErr $
mapLeft (Error.PgError authenticated) dbResp
liftEither resp
where where
transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction
prepared = configDbPreparedStatements config prepared = configDbPreparedStatements config
isoLvl = planIsoLvl config authRole plan isoLvl = planIsoLvl config authRole plan
txMode = planTxMode plan txMode = planTxMode plan
(mainActionQuery, mainSQLQuery) = actionQuery plan config apiReq pgVer sCache
dbHandler = do dbHandler = do
setPgLocals plan config authClaims authRole apiReq setPgLocals plan config authClaims authRole apiReq
runPreReq config runPreReq config
mainActionQuery actionQuery plan config apiReq pgVer sCache
planTxMode :: DbActionPlan -> SQL.Mode planTxMode :: DbActionPlan -> SQL.Mode
planTxMode (DbCrud x) = pTxMode x planTxMode (DbCrud x) = pTxMode x
@@ -107,99 +107,107 @@ planIsoLvl AppConfig{configRoleIsoLvl} role actPlan = case actPlan of
where where
roleIsoLvl = HM.findWithDefault SQL.ReadCommitted role configRoleIsoLvl roleIsoLvl = HM.findWithDefault SQL.ReadCommitted role configRoleIsoLvl
-- TODO: Generate the Hasql Statement in a diferent module after the OpenAPI functionality is removed actionQuery :: DbActionPlan -> AppConfig -> ApiRequest -> PgVersion -> SchemaCache -> DbHandler QueryResult
actionQuery :: DbActionPlan -> AppConfig -> ApiRequest -> PgVersion -> SchemaCache -> (DbHandler QueryResult, ByteString)
actionQuery (DbCrud plan@WrappedReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ _ =
(mainActionQuery, mainSQLQuery)
where
countQuery = QueryBuilder.readPlanToCountQuery wrReadPlan
(result, mainSQLQuery) = Statements.prepareRead
(QueryBuilder.readPlanToQuery wrReadPlan)
(if preferCount == Just EstimatedCount then
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
QueryBuilder.limitedQuery countQuery ((+ 1) <$> configDbMaxRows)
else
countQuery
)
(shouldCount preferCount)
wrMedia
wrHandler
configDbPreparedStatements
mainActionQuery = do
resultSet <- lift $ SQL.statement mempty result
failNotSingular wrMedia resultSet
optionalRollback conf apiReq
DbCrudResult plan <$> resultSetWTotal conf apiReq resultSet countQuery
actionQuery (DbCrud plan@MutateReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ _ = actionQuery (DbCrud plan@WrappedReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ _ = do
(mainActionQuery, mainSQLQuery) let countQuery = QueryBuilder.readPlanToCountQuery wrReadPlan
where resultSet <-
(isPut, isInsert, pkCols) = case mrMutatePlan of {Insert{where_,insPkCols} -> ((not . null) where_, True, insPkCols); _ -> (False,False, mempty);} lift . SQL.statement mempty $
(result, mainSQLQuery) = Statements.prepareWrite Statements.prepareRead
(QueryBuilder.readPlanToQuery mrReadPlan) (QueryBuilder.readPlanToQuery wrReadPlan)
(QueryBuilder.mutatePlanToQuery mrMutatePlan) (if preferCount == Just EstimatedCount then
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
QueryBuilder.limitedQuery countQuery ((+ 1) <$> configDbMaxRows)
else
countQuery
)
(shouldCount preferCount)
wrMedia
wrHandler
configDbPreparedStatements
failNotSingular wrMedia resultSet
optionalRollback conf apiReq
DbCrudResult plan <$> resultSetWTotal conf apiReq resultSet countQuery
actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationCreate, ..}) conf apiReq _ _ = do
resultSet <- writeQuery mrReadPlan mrMutatePlan mrMedia mrHandler apiReq conf
failNotSingular mrMedia resultSet
optionalRollback conf apiReq
pure $ DbCrudResult plan resultSet
actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationUpdate, ..}) conf apiReq@ApiRequest{iPreferences=Preferences{..}, ..} _ _ = do
resultSet <- writeQuery mrReadPlan mrMutatePlan mrMedia mrHandler apiReq conf
failNotSingular mrMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
optionalRollback conf apiReq
pure $ DbCrudResult plan resultSet
actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationSingleUpsert, ..}) conf apiReq _ _ = do
resultSet <- writeQuery mrReadPlan mrMutatePlan mrMedia mrHandler apiReq conf
failPut resultSet
optionalRollback conf apiReq
pure $ DbCrudResult plan resultSet
actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationDelete, ..}) conf apiReq@ApiRequest{iPreferences=Preferences{..}, ..} _ _ = do
resultSet <- writeQuery mrReadPlan mrMutatePlan mrMedia mrHandler apiReq conf
failNotSingular mrMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
optionalRollback conf apiReq
pure $ DbCrudResult plan resultSet
actionQuery (DbCall plan@CallReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} pgVer _ = do
resultSet <-
lift . SQL.statement mempty $
Statements.prepareCall
crProc
(QueryBuilder.callPlanToQuery crCallPlan pgVer)
(QueryBuilder.readPlanToQuery crReadPlan)
(QueryBuilder.readPlanToCountQuery crReadPlan)
(shouldCount preferCount)
crMedia
crHandler
configDbPreparedStatements
optionalRollback conf apiReq
failNotSingular crMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
pure $ DbCallResult plan resultSet
actionQuery (MaybeDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ pgVer sCache =
lift $ case configOpenApiMode of
OAFollowPriv -> do
tableAccess <- SQL.statement [tSchema] (SchemaCache.accessibleTables pgVer configDbPreparedStatements)
MaybeDbResult plan . Just <$> ((,,)
(HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache)
<$> SQL.statement (tSchema, configDbHoistedTxSettings) (SchemaCache.accessibleFuncs pgVer configDbPreparedStatements)
<*> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
OAIgnorePriv ->
MaybeDbResult plan . Just <$> ((,,)
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbTables sCache)
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbRoutines sCache)
<$> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
OADisabled ->
pure $ MaybeDbResult plan Nothing
writeQuery :: ReadPlanTree -> MutatePlan -> MediaType -> MediaHandler -> ApiRequest -> AppConfig -> DbHandler ResultSet
writeQuery readPlan mutatePlan mType mHandler ApiRequest{iPreferences=Preferences{..}} conf =
let
(isPut, isInsert, pkCols) = case mutatePlan of {Insert{where_,insPkCols} -> ((not . null) where_, True, insPkCols); _ -> (False,False, mempty);}
in
lift . SQL.statement mempty $
Statements.prepareWrite
(QueryBuilder.readPlanToQuery readPlan)
(QueryBuilder.mutatePlanToQuery mutatePlan)
isInsert isInsert
isPut isPut
mrMedia mType
mrHandler mHandler
preferRepresentation preferRepresentation
preferResolution preferResolution
pkCols pkCols
configDbPreparedStatements (configDbPreparedStatements conf)
failMutation resultSet = case mrMutation of
MutationCreate -> do
failNotSingular mrMedia resultSet
MutationUpdate -> do
failNotSingular mrMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
MutationSingleUpsert -> do
failPut resultSet
MutationDelete -> do
failNotSingular mrMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
mainActionQuery = do
resultSet <- lift $ SQL.statement mempty result
failMutation resultSet
optionalRollback conf apiReq
pure $ DbCrudResult plan resultSet
actionQuery (DbCall plan@CallReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} pgVer _ =
(mainActionQuery, mainSQLQuery)
where
(result, mainSQLQuery) = Statements.prepareCall
crProc
(QueryBuilder.callPlanToQuery crCallPlan pgVer)
(QueryBuilder.readPlanToQuery crReadPlan)
(QueryBuilder.readPlanToCountQuery crReadPlan)
(shouldCount preferCount)
crMedia
crHandler
configDbPreparedStatements
mainActionQuery = do
resultSet <- lift $ SQL.statement mempty result
optionalRollback conf apiReq
failNotSingular crMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
pure $ DbCallResult plan resultSet
actionQuery (MaybeDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ _ sCache =
(mainActionQuery, mempty)
where
mainActionQuery = lift $
case configOpenApiMode of
OAFollowPriv -> do
tableAccess <- SQL.statement [tSchema] (SchemaCache.accessibleTables configDbPreparedStatements)
MaybeDbResult plan . Just <$> ((,,)
(HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache)
<$> SQL.statement ([tSchema], configDbHoistedTxSettings) (SchemaCache.accessibleFuncs configDbPreparedStatements)
<*> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
OAIgnorePriv ->
MaybeDbResult plan . Just <$> ((,,)
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbTables sCache)
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbRoutines sCache)
<$> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
OADisabled ->
pure $ MaybeDbResult plan Nothing
-- Makes sure the querystring pk matches the payload pk -- Makes sure the querystring pk matches the payload pk
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, -- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted,
@@ -211,7 +219,7 @@ failPut RSPlan{} = pure ()
failPut RSStandard{rsQueryTotal=queryTotal} = failPut RSStandard{rsQueryTotal=queryTotal} =
when (queryTotal /= 1) $ do when (queryTotal /= 1) $ do
lift SQL.condemn lift SQL.condemn
throwError $ Error.ApiRequestError Error.PutMatchingPkError throwError $ Error.ApiRequestError ApiRequestTypes.PutMatchingPkError
resultSetWTotal :: AppConfig -> ApiRequest -> ResultSet -> SQL.Snippet -> DbHandler ResultSet resultSetWTotal :: AppConfig -> ApiRequest -> ResultSet -> SQL.Snippet -> DbHandler ResultSet
resultSetWTotal _ _ rs@RSPlan{} _ = return rs resultSetWTotal _ _ rs@RSPlan{} _ = return rs
@@ -243,14 +251,22 @@ failNotSingular _ RSPlan{} = pure ()
failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} = failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =
when (elem mediaType [MTVndSingularJSON True, MTVndSingularJSON False] && queryTotal /= 1) $ do when (elem mediaType [MTVndSingularJSON True, MTVndSingularJSON False] && queryTotal /= 1) $ do
lift SQL.condemn lift SQL.condemn
throwError $ Error.ApiRequestError . Error.SingularityError $ toInteger queryTotal throwError $ Error.ApiRequestError . ApiRequestTypes.SingularityError $ toInteger queryTotal
failExceedsMaxAffectedPref :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> ResultSet -> DbHandler () failExceedsMaxAffectedPref :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> ResultSet -> DbHandler ()
failExceedsMaxAffectedPref (Nothing,_) _ = pure () failExceedsMaxAffectedPref (Nothing,_) _ = pure ()
failExceedsMaxAffectedPref _ RSPlan{} = pure () failExceedsMaxAffectedPref _ RSPlan{} = pure ()
failExceedsMaxAffectedPref (Just (PreferMaxAffected n), handling) RSStandard{rsQueryTotal=queryTotal} = when ((queryTotal > n) && (handling == Just Strict)) $ do failExceedsMaxAffectedPref (Just (PreferMaxAffected n), handling) RSStandard{rsQueryTotal=queryTotal} = when ((queryTotal > n) && (handling == Just Strict)) $ do
lift SQL.condemn lift SQL.condemn
throwError $ Error.ApiRequestError . Error.MaxAffectedViolationError $ toInteger queryTotal throwError $ Error.ApiRequestError . ApiRequestTypes.MaxAffectedViolationError $ toInteger queryTotal
failsChangesOffLimits :: Maybe Integer -> ResultSet -> DbHandler ()
failsChangesOffLimits _ RSPlan{} = pure ()
failsChangesOffLimits Nothing _ = pure ()
failsChangesOffLimits (Just maxChanges) RSStandard{rsQueryTotal=queryTotal} =
when (queryTotal > fromIntegral maxChanges) $ do
lift SQL.condemn
throwError $ Error.ApiRequestError $ ApiRequestTypes.OffLimitsChangesError queryTotal maxChanges
-- | Set a transaction to roll back if requested -- | Set a transaction to roll back if requested
optionalRollback :: AppConfig -> ApiRequest -> DbHandler () optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
@@ -298,7 +314,3 @@ runPreReq conf = lift $ traverse_ (SQL.statement mempty . stmt) (configDbPreRequ
("select " <> fromQi req <> "()") ("select " <> fromQi req <> "()")
HD.noResult HD.noResult
(configDbPreparedStatements conf) (configDbPreparedStatements conf)
getSQLQuery :: Query -> ByteString
getSQLQuery DbQuery{dqSQL} = dqSQL
getSQLQuery _ = mempty
+76 -83
View File
@@ -1,6 +1,5 @@
{-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-| {-|
Module : PostgREST.Query.QueryBuilder Module : PostgREST.Query.QueryBuilder
Description : PostgREST SQL queries generating functions. Description : PostgREST SQL queries generating functions.
@@ -17,18 +16,15 @@ module PostgREST.Query.QueryBuilder
, limitedQuery , limitedQuery
) where ) where
import qualified Data.Aeson as JSON
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Data.HashMap.Strict as HM
import qualified Data.Set as S
import qualified Hasql.DynamicStatements.Snippet as SQL import qualified Hasql.DynamicStatements.Snippet as SQL
import qualified Hasql.Encoders as HE
import Data.Maybe (fromJust) import Data.Maybe (fromJust)
import Data.Tree (Tree (..)) import Data.Tree (Tree (..))
import PostgREST.ApiRequest.Preferences (PreferResolution (..)) import PostgREST.ApiRequest.Preferences (PreferResolution (..))
import PostgREST.Config.PgVersion (PgVersion, pgVersion130) import PostgREST.Config.PgVersion (PgVersion, pgVersion110,
pgVersion130)
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..)) import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
import PostgREST.SchemaCache.Relationship (Cardinality (..), import PostgREST.SchemaCache.Relationship (Cardinality (..),
Junction (..), Junction (..),
@@ -41,18 +37,19 @@ import PostgREST.Plan.MutatePlan
import PostgREST.Plan.ReadPlan import PostgREST.Plan.ReadPlan
import PostgREST.Plan.Types import PostgREST.Plan.Types
import PostgREST.Query.SqlFragment import PostgREST.Query.SqlFragment
import PostgREST.RangeQuery (allRange)
import Protolude import Protolude
readPlanToQuery :: ReadPlanTree -> SQL.Snippet readPlanToQuery :: ReadPlanTree -> SQL.Snippet
readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds, relSelect, relSpread} forest) = readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds, relSelect} forest) =
"SELECT " <> "SELECT " <>
intercalateSnippet ", " (selects ++ sprExtraSelects ++ joinsSelects) <> intercalateSnippet ", " ((pgFmtSelectItem qi <$> (if null select && null forest then defSelect else select)) ++ joinsSelects) <> " " <>
fromFrag <> fromFrag <> " " <>
intercalateSnippet " " joins <> intercalateSnippet " " joins <> " " <>
(if null logicForest && null relJoinConds (if null logicForest && null relJoinConds
then mempty then mempty
else " WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition relJoinConds)) <> " " <> else "WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition relJoinConds)) <> " " <>
groupF qi select relSelect <> " " <> groupF qi select relSelect <> " " <>
orderF qi order <> " " <> orderF qi order <> " " <>
limitOffsetF readRange limitOffsetF readRange
@@ -62,29 +59,25 @@ readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicFor
-- gets all the columns in case of an empty select, ignoring/obtaining these columns is done at the aggregation stage -- gets all the columns in case of an empty select, ignoring/obtaining these columns is done at the aggregation stage
defSelect = [CoercibleSelectField (unknownField "*" []) Nothing Nothing Nothing Nothing] defSelect = [CoercibleSelectField (unknownField "*" []) Nothing Nothing Nothing Nothing]
joins = getJoins node joins = getJoins node
selects = pgFmtSelectItem qi <$> (if null select && null forest then defSelect else select)
joinsSelects = getJoinSelects node joinsSelects = getJoinSelects node
sprExtraSelects = case relSpread of
Just (ToManySpread sels _) -> (\s -> pgFmtSelectItem (maybe qi (QualifiedIdentifier "") $ fst s) $ snd s) <$> sels
_ -> mempty
getJoinSelects :: ReadPlanTree -> [SQL.Snippet] getJoinSelects :: ReadPlanTree -> [SQL.Snippet]
getJoinSelects (Node ReadPlan{relSelect} _) = getJoinSelects (Node ReadPlan{relSelect} _) =
join $ map relSelectToSnippet relSelect mapMaybe relSelectToSnippet relSelect
where where
relSelectToSnippet :: RelSelectField -> [SQL.Snippet] relSelectToSnippet :: RelSelectField -> Maybe SQL.Snippet
relSelectToSnippet fld = relSelectToSnippet fld =
let aggAlias = pgFmtIdent $ rsAggAlias fld let aggAlias = pgFmtIdent $ rsAggAlias fld
in in
case fld of case fld of
JsonEmbed{rsEmptyEmbed = True} -> JsonEmbed{rsEmptyEmbed = True} ->
[] Nothing
JsonEmbed{rsSelName, rsEmbedMode = JsonObject} -> JsonEmbed{rsSelName, rsEmbedMode = JsonObject} ->
["row_to_json(" <> aggAlias <> ".*)::jsonb AS " <> pgFmtIdent rsSelName] Just $ "row_to_json(" <> aggAlias <> ".*)::jsonb AS " <> pgFmtIdent rsSelName
JsonEmbed{rsSelName, rsEmbedMode = JsonArray} -> JsonEmbed{rsSelName, rsEmbedMode = JsonArray} ->
["COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> pgFmtIdent rsSelName] Just $ "COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> pgFmtIdent rsSelName
Spread{rsSpreadSel, rsAggAlias} -> Spread{rsSpreadSel, rsAggAlias} ->
pgFmtSpreadSelectItem rsAggAlias <$> rsSpreadSel Just $ intercalateSnippet ", " (pgFmtSpreadSelectItem rsAggAlias <$> rsSpreadSel)
getJoins :: ReadPlanTree -> [SQL.Snippet] getJoins :: ReadPlanTree -> [SQL.Snippet]
getJoins (Node _ []) = [] getJoins (Node _ []) = []
@@ -96,31 +89,26 @@ getJoins (Node ReadPlan{relSelect} forest) =
) relSelect ) relSelect
getJoin :: RelSelectField -> ReadPlanTree -> SQL.Snippet getJoin :: RelSelectField -> ReadPlanTree -> SQL.Snippet
getJoin fld node@(Node ReadPlan{relJoinType, relSpread} _) = getJoin fld node@(Node ReadPlan{relJoinType} _) =
let let
correlatedSubquery sub al cond = correlatedSubquery sub al cond =
" " <> (if relJoinType == Just JTInner then "INNER" else "LEFT") <> " JOIN LATERAL ( " <> sub <> " ) AS " <> al <> " ON " <> cond (if relJoinType == Just JTInner then "INNER" else "LEFT") <> " JOIN LATERAL ( " <> sub <> " ) AS " <> al <> " ON " <> cond
subquery = readPlanToQuery node subquery = readPlanToQuery node
aggAlias = pgFmtIdent $ rsAggAlias fld aggAlias = pgFmtIdent $ rsAggAlias fld
selectSubqAgg = "SELECT json_agg(" <> aggAlias <> ")::jsonb AS " <> aggAlias
fromSubqAgg = " FROM (" <> subquery <> " ) AS " <> aggAlias
joinCondition = if relJoinType == Just JTInner then aggAlias <> " IS NOT NULL" else "TRUE"
in in
case fld of case fld of
JsonEmbed{rsEmbedMode = JsonObject} -> JsonEmbed{rsEmbedMode = JsonObject} ->
correlatedSubquery subquery aggAlias "TRUE" correlatedSubquery subquery aggAlias "TRUE"
Spread{rsSpreadSel, rsAggAlias} -> Spread{} ->
case relSpread of correlatedSubquery subquery aggAlias "TRUE"
Just (ToManySpread _ sprOrder) ->
let selSpread = selectSubqAgg <> (if null rsSpreadSel then mempty else ", ") <> intercalateSnippet ", " (pgFmtSpreadJoinSelectItem rsAggAlias sprOrder <$> rsSpreadSel)
in correlatedSubquery (selSpread <> fromSubqAgg) aggAlias joinCondition
_ ->
correlatedSubquery subquery aggAlias "TRUE"
JsonEmbed{rsEmbedMode = JsonArray} -> JsonEmbed{rsEmbedMode = JsonArray} ->
correlatedSubquery (selectSubqAgg <> fromSubqAgg) aggAlias joinCondition let
subq = "SELECT json_agg(" <> aggAlias <> ")::jsonb AS " <> aggAlias <> " FROM (" <> subquery <> " ) AS " <> aggAlias
condition = if relJoinType == Just JTInner then aggAlias <> " IS NOT NULL" else "TRUE"
in correlatedSubquery subq aggAlias condition
mutatePlanToQuery :: MutatePlan -> SQL.Snippet mutatePlanToQuery :: MutatePlan -> SQL.Snippet
mutatePlanToQuery (Insert mainQi iCols body onConflict putConditions returnings _ applyDefaults) = mutatePlanToQuery (Insert mainQi iCols body onConflct putConditions returnings _ applyDefaults) =
"INSERT INTO " <> fromQi mainQi <> (if null iCols then " " else "(" <> cols <> ") ") <> "INSERT INTO " <> fromQi mainQi <> (if null iCols then " " else "(" <> cols <> ") ") <>
fromJsonBodyF body iCols True False applyDefaults <> fromJsonBodyF body iCols True False applyDefaults <>
-- Only used for PUT -- Only used for PUT
@@ -137,90 +125,95 @@ mutatePlanToQuery (Insert mainQi iCols body onConflict putConditions returnings
if null iCols if null iCols
then "DO NOTHING" then "DO NOTHING"
else "DO UPDATE SET " <> intercalateSnippet ", " ((pgFmtIdent . cfName) <> const " = EXCLUDED." <> (pgFmtIdent . cfName) <$> iCols) <> (if null putConditions && not mergeDups then mempty else "WHERE " <> addConfigPgrstInserted False) else "DO UPDATE SET " <> intercalateSnippet ", " ((pgFmtIdent . cfName) <> const " = EXCLUDED." <> (pgFmtIdent . cfName) <$> iCols) <> (if null putConditions && not mergeDups then mempty else "WHERE " <> addConfigPgrstInserted False)
) onConflict <> " " <> ) onConflct <> " " <>
returningF mainQi returnings returningF mainQi returnings
where where
cols = intercalateSnippet ", " $ pgFmtIdent . cfName <$> iCols cols = intercalateSnippet ", " $ pgFmtIdent . cfName <$> iCols
mergeDups = case onConflict of {Just (MergeDuplicates,_) -> True; _ -> False;} mergeDups = case onConflct of {Just (MergeDuplicates,_) -> True; _ -> False;}
mutatePlanToQuery (Update mainQi uCols body logicForest returnings applyDefaults) -- An update without a limit is always filtered with a WHERE
mutatePlanToQuery (Update mainQi uCols body logicForest range ordts returnings applyDefaults)
| null uCols = | null uCols =
-- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax -- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax
-- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select= -- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select=
-- the select has to be based on "returnings" to make computed overloaded functions not throw -- the select has to be based on "returnings" to make computed overloaded functions not throw
"SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false" "SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false"
| otherwise = | range == allRange =
"UPDATE " <> mainTbl <> " SET " <> cols <> " " <> "UPDATE " <> mainTbl <> " SET " <> nonRangeCols <> " " <>
fromJsonBodyF body uCols False False applyDefaults <> fromJsonBodyF body uCols False False applyDefaults <>
whereLogic <> " " <> whereLogic <> " " <>
returningF mainQi returnings returningF mainQi returnings
| otherwise =
"WITH " <>
"pgrst_update_body AS (" <> fromJsonBodyF body uCols True True applyDefaults <> "), " <>
"pgrst_affected_rows AS (" <>
"SELECT " <> rangeIdF <> " FROM " <> mainTbl <>
whereLogic <> " " <>
orderF mainQi ordts <> " " <>
limitOffsetF range <>
") " <>
"UPDATE " <> mainTbl <> " SET " <> rangeCols <>
"FROM pgrst_affected_rows " <>
"WHERE " <> whereRangeIdF <> " " <>
returningF mainQi returnings
where where
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest) whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
mainTbl = fromQi mainQi mainTbl = fromQi mainQi
emptyBodyReturnedColumns = if null returnings then "NULL" else intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings) emptyBodyReturnedColumns = if null returnings then "NULL" else intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)
cols = intercalateSnippet ", " (pgFmtIdent . cfName <> const " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_body") . cfName <$> uCols) nonRangeCols = intercalateSnippet ", " (pgFmtIdent . cfName <> const " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_body") . cfName <$> uCols)
rangeCols = intercalateSnippet ", " ((\col -> pgFmtIdent (cfName col) <> " = (SELECT " <> pgFmtIdent (cfName col) <> " FROM pgrst_update_body) ") <$> uCols)
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (cfName . coField <$> ordts)
mutatePlanToQuery (Delete mainQi logicForest range ordts returnings)
| range == allRange =
"DELETE FROM " <> fromQi mainQi <> " " <>
whereLogic <> " " <>
returningF mainQi returnings
| otherwise =
"WITH " <>
"pgrst_affected_rows AS (" <>
"SELECT " <> rangeIdF <> " FROM " <> fromQi mainQi <>
whereLogic <> " " <>
orderF mainQi ordts <> " " <>
limitOffsetF range <>
") " <>
"DELETE FROM " <> fromQi mainQi <> " " <>
"USING pgrst_affected_rows " <>
"WHERE " <> whereRangeIdF <> " " <>
returningF mainQi returnings
mutatePlanToQuery (Delete mainQi logicForest returnings) =
"DELETE FROM " <> fromQi mainQi <> " " <>
whereLogic <> " " <>
returningF mainQi returnings
where where
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest) whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (cfName . coField <$> ordts)
callPlanToQuery :: CallPlan -> PgVersion -> SQL.Snippet callPlanToQuery :: CallPlan -> PgVersion -> SQL.Snippet
callPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScalar returnsCompositeAlias filterFields returnings) pgVer = callPlanToQuery (FunctionCall qi params args returnsScalar returnsSetOfScalar returnsCompositeAlias returnings) pgVer =
"SELECT " <> (if returnsScalar || returnsSetOfScalar then "pgrst_call.pgrst_scalar" else returnedColumns) <> " " <> "SELECT " <> (if returnsScalar || returnsSetOfScalar then "pgrst_call.pgrst_scalar" else returnedColumns) <> " " <>
fromCall fromCall
where where
jsonArgs = case arguments of
DirectArgs args -> Just $ JSON.encode args
JsonArgs json -> json
fromCall = case params of fromCall = case params of
OnePosParam prm -> "FROM " <> callIt (singleParameter jsonArgs $ encodeUtf8 $ ppType prm) OnePosParam prm -> "FROM " <> callIt (singleParameter args $ encodeUtf8 $ ppType prm)
KeyParams [] -> "FROM " <> callIt mempty KeyParams [] -> "FROM " <> callIt mempty
KeyParams prms -> case arguments of KeyParams prms -> fromJsonBodyF args ((\p -> CoercibleField (ppName p) mempty False (ppTypeMaxLength p) Nothing Nothing) <$> prms) False True False <> ", " <>
DirectArgs args -> "FROM " <> callIt (fmtArgs prms args)
JsonArgs json -> fromJsonBodyF json ((\p -> CoercibleField (ppName p) mempty False Nothing (ppTypeMaxLength p) mempty Nothing Nothing False) <$> prms) False True False <> ", " <>
"LATERAL " <> callIt (fmtParams prms) "LATERAL " <> callIt (fmtParams prms)
callIt :: SQL.Snippet -> SQL.Snippet callIt :: SQL.Snippet -> SQL.Snippet
callIt argument | pgVer < pgVersion130 && returnsCompositeAlias = "(SELECT (" <> fromQi qi <> "(" <> argument <> ")).*) pgrst_call" callIt argument | pgVer < pgVersion130 && pgVer >= pgVersion110 && returnsCompositeAlias = "(SELECT (" <> fromQi qi <> "(" <> argument <> ")).*) pgrst_call"
| returnsScalar || returnsSetOfScalar = "(SELECT " <> fromQi qi <> "(" <> argument <> ") pgrst_scalar) pgrst_call" | returnsScalar || returnsSetOfScalar = "(SELECT " <> fromQi qi <> "(" <> argument <> ") pgrst_scalar) pgrst_call"
| otherwise = fromQi qi <> "(" <> argument <> ") pgrst_call" | otherwise = fromQi qi <> "(" <> argument <> ") pgrst_call"
fmtParams :: [RoutineParam] -> SQL.Snippet fmtParams :: [RoutineParam] -> SQL.Snippet
fmtParams prms = intercalateSnippet ", " fmtParams prms = intercalateSnippet ", "
((\a -> (if ppVar a then "VARIADIC " else mempty) <> pgFmtIdent (ppName a) <> " := pgrst_body." <> pgFmtIdent (ppName a)) <$> prms) ((\a -> (if ppVar a then "VARIADIC " else mempty) <> pgFmtIdent (ppName a) <> " := pgrst_body." <> pgFmtIdent (ppName a)) <$> prms)
fmtArgs :: [RoutineParam] -> HM.HashMap Text RpcParamValue -> SQL.Snippet
fmtArgs prms args = intercalateSnippet ", " $ fmtArg <$> prms
where
fmtArg RoutineParam{..} =
(if ppVar then "VARIADIC " else mempty) <>
pgFmtIdent ppName <>
" := " <>
encodeArg (HM.lookup ppName args) <>
"::" <>
SQL.sql (encodeUtf8 ppTypeMaxLength)
encodeArg :: Maybe RpcParamValue -> SQL.Snippet
encodeArg (Just (Variadic v)) = SQL.encoderAndParam (HE.nonNullable $ HE.foldableArray $ HE.nonNullable HE.text) v
encodeArg (Just (Fixed v)) = SQL.encoderAndParam (HE.nonNullable HE.unknown) $ encodeUtf8 v
-- Currently not supported: Calling functions without some of their arguments without DEFAULT.
-- We could fallback to providing this NULL value in those cases.
encodeArg Nothing = "NULL"
-- the columns here would be the returnings + the columns that would later
-- be used by a where clause filter, if they intersect, we remove the duplicates
-- and if * is returned then no need to explicitly add filter columns
returnedColumns :: SQL.Snippet returnedColumns :: SQL.Snippet
returnedColumns = case S.toList returnings of returnedColumns
[] -> "*" | null returnings = "*"
["*"] -> pgFmtColumn (QualifiedIdentifier mempty "pgrst_call") "*" | otherwise = intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty "pgrst_call") <$> returnings)
_ -> intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty "pgrst_call") <$> returnedColumns')
where
returnedColumns' = S.toList $ returnings <> filterFields
-- | SQL query meant for COUNTing the root node of the Tree. -- | SQL query meant for COUNTing the root node of the Tree.
-- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT. -- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT.
@@ -274,7 +267,7 @@ getQualifiedIdentifier rel mainQi tblAlias = case rel of
-- FROM clause plus implicit joins -- FROM clause plus implicit joins
fromF :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> SQL.Snippet fromF :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> SQL.Snippet
fromF rel mainQi tblAlias = " FROM " <> fromF rel mainQi tblAlias = "FROM " <>
(case rel of (case rel of
-- Due to the use of CTEs on RPC, we need to cast the parameter to the table name in case of function overloading. -- Due to the use of CTEs on RPC, we need to cast the parameter to the table name in case of function overloading.
-- See https://github.com/PostgREST/postgrest/issues/2963#issuecomment-1736557386 -- See https://github.com/PostgREST/postgrest/issues/2963#issuecomment-1736557386
+35 -48
View File
@@ -13,6 +13,7 @@ module PostgREST.Query.SqlFragment
, fromQi , fromQi
, limitOffsetF , limitOffsetF
, locationF , locationF
, mutRangeF
, orderF , orderF
, pgFmtColumn , pgFmtColumn
, pgFmtFilter , pgFmtFilter
@@ -22,7 +23,6 @@ module PostgREST.Query.SqlFragment
, pgFmtOrderTerm , pgFmtOrderTerm
, pgFmtSelectItem , pgFmtSelectItem
, pgFmtSpreadSelectItem , pgFmtSpreadSelectItem
, pgFmtSpreadJoinSelectItem
, fromJsonBodyF , fromJsonBodyF
, responseHeadersF , responseHeadersF
, responseStatusF , responseStatusF
@@ -53,13 +53,12 @@ import qualified Hasql.Encoders as HE
import Control.Arrow ((***)) import Control.Arrow ((***))
import Data.Foldable (foldr1) import Data.Foldable (foldr1)
import NeatInterpolation (trimming) import Text.InterpolatedString.Perl6 (qc)
import PostgREST.ApiRequest.Types (AggregateFunction (..), import PostgREST.ApiRequest.Types (AggregateFunction (..),
Alias, Cast, Alias, Cast,
FtsOperator (..), FtsOperator (..),
IsVal (..),
JsonOperand (..), JsonOperand (..),
JsonOperation (..), JsonOperation (..),
JsonPath, JsonPath,
@@ -70,7 +69,8 @@ import PostgREST.ApiRequest.Types (AggregateFunction (..),
OrderDirection (..), OrderDirection (..),
OrderNulls (..), OrderNulls (..),
QuantOperator (..), QuantOperator (..),
SimpleOperator (..)) SimpleOperator (..),
TrileanVal (..))
import PostgREST.MediaType (MTVndPlanFormat (..), import PostgREST.MediaType (MTVndPlanFormat (..),
MTVndPlanOption (..)) MTVndPlanOption (..))
import PostgREST.Plan.ReadPlan (JoinCondition (..)) import PostgREST.Plan.ReadPlan (JoinCondition (..))
@@ -81,7 +81,6 @@ import PostgREST.Plan.Types (CoercibleField (..),
CoercibleSelectField (..), CoercibleSelectField (..),
RelSelectField (..), RelSelectField (..),
SpreadSelectField (..), SpreadSelectField (..),
ToTsVector (..),
unknownField) unknownField)
import PostgREST.RangeQuery (NonnegRange, allRange, import PostgREST.RangeQuery (NonnegRange, allRange,
rangeLimit, rangeOffset) rangeLimit, rangeOffset)
@@ -158,10 +157,10 @@ pgBuildArrayLiteral vals =
-- TODO: refactor by following https://github.com/PostgREST/postgrest/pull/1631#issuecomment-711070833 -- TODO: refactor by following https://github.com/PostgREST/postgrest/pull/1631#issuecomment-711070833
pgFmtIdent :: Text -> SQL.Snippet pgFmtIdent :: Text -> SQL.Snippet
pgFmtIdent x = SQL.sql . encodeUtf8 $ escapeIdent x pgFmtIdent x = SQL.sql $ escapeIdent x
escapeIdent :: Text -> Text escapeIdent :: Text -> ByteString
escapeIdent x = "\"" <> T.replace "\"" "\"\"" (trimNullChars x) <> "\"" escapeIdent x = encodeUtf8 $ "\"" <> T.replace "\"" "\"\"" (trimNullChars x) <> "\""
-- Only use it if the input comes from the database itself, like on `jsonb_build_object('column_from_a_table', val)..` -- Only use it if the input comes from the database itself, like on `jsonb_build_object('column_from_a_table', val)..`
pgFmtLit :: Text -> Text pgFmtLit :: Text -> Text
@@ -182,7 +181,7 @@ trimNullChars = T.takeWhile (/= '\x0')
-- >>> escapeIdentList ["schema_1", "schema_2", "SPECIAL \"@/\\#~_-"] -- >>> escapeIdentList ["schema_1", "schema_2", "SPECIAL \"@/\\#~_-"]
-- "\"schema_1\", \"schema_2\", \"SPECIAL \"\"@/\\#~_-\"" -- "\"schema_1\", \"schema_2\", \"SPECIAL \"\"@/\\#~_-\""
escapeIdentList :: [Text] -> ByteString escapeIdentList :: [Text] -> ByteString
escapeIdentList schemas = BS.intercalate ", " $ encodeUtf8 . escapeIdent <$> schemas escapeIdentList schemas = BS.intercalate ", " $ escapeIdent <$> schemas
asCsvF :: SQL.Snippet asCsvF :: SQL.Snippet
asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
@@ -230,11 +229,11 @@ customFuncF _ funcQi RelAnyElement = fromQi funcQi <> "(_postgrest_t)
customFuncF _ funcQi (RelId target) = fromQi funcQi <> "(_postgrest_t::" <> fromQi target <> ")" customFuncF _ funcQi (RelId target) = fromQi funcQi <> "(_postgrest_t::" <> fromQi target <> ")"
locationF :: [Text] -> SQL.Snippet locationF :: [Text] -> SQL.Snippet
locationF pKeys = SQL.sql $ encodeUtf8 [trimming|( locationF pKeys = [qc|(
WITH data AS (SELECT row_to_json(_) AS row FROM ${sourceCTEName} AS _ LIMIT 1) WITH data AS (SELECT row_to_json(_) AS row FROM {sourceCTEName} AS _ LIMIT 1)
SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null')) SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null'))
FROM data CROSS JOIN json_each_text(data.row) AS json_data FROM data CROSS JOIN json_each_text(data.row) AS json_data
WHERE json_data.key IN ('${fmtPKeys}') WHERE json_data.key IN ('{fmtPKeys}')
)|] )|]
where where
fmtPKeys = T.intercalate "','" pKeys fmtPKeys = T.intercalate "','" pKeys
@@ -253,15 +252,9 @@ pgFmtCallUnary :: Text -> SQL.Snippet -> SQL.Snippet
pgFmtCallUnary f x = SQL.sql (encodeUtf8 f) <> "(" <> x <> ")" pgFmtCallUnary f x = SQL.sql (encodeUtf8 f) <> "(" <> x <> ")"
pgFmtField :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet pgFmtField :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
pgFmtField table cf = case cfToTsVector cf of pgFmtField table CoercibleField{cfName=fn, cfJsonPath=[]} = pgFmtColumn table fn
Just (ToTsVector lang) -> "to_tsvector(" <> pgFmtFtsLang lang <> fmtFld <> ")" pgFmtField table CoercibleField{cfName=fn, cfToJson=doToJson, cfJsonPath=jp} | doToJson = "to_jsonb(" <> pgFmtColumn table fn <> ")" <> pgFmtJsonPath jp
_ -> fmtFld | otherwise = pgFmtColumn table fn <> pgFmtJsonPath jp
where
fmtFld = case cf of
CoercibleField{cfFullRow=True} -> pgFmtIdent (qiName table)
CoercibleField{cfName=fn, cfJsonPath=[]} -> pgFmtColumn table fn
CoercibleField{cfName=fn, cfToJson=doToJson, cfJsonPath=jp} | doToJson -> "to_jsonb(" <> pgFmtColumn table fn <> ")" <> pgFmtJsonPath jp
| otherwise -> pgFmtColumn table fn <> pgFmtJsonPath jp
-- Select the value of a named element from a table, applying its optional coercion mapping if any. -- Select the value of a named element from a table, applying its optional coercion mapping if any.
pgFmtTableCoerce :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet pgFmtTableCoerce :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
@@ -279,7 +272,11 @@ pgFmtSelectItem table CoercibleSelectField{csField=fld, csAggFunction=agg, csAgg
pgFmtSpreadSelectItem :: Alias -> SpreadSelectField -> SQL.Snippet pgFmtSpreadSelectItem :: Alias -> SpreadSelectField -> SQL.Snippet
pgFmtSpreadSelectItem aggAlias SpreadSelectField{ssSelName, ssSelAggFunction, ssSelAggCast, ssSelAlias} = pgFmtSpreadSelectItem aggAlias SpreadSelectField{ssSelName, ssSelAggFunction, ssSelAggCast, ssSelAlias} =
pgFmtApplyAggregate ssSelAggFunction ssSelAggCast (pgFmtFullSelName aggAlias ssSelName) <> pgFmtAs ssSelAlias pgFmtApplyAggregate ssSelAggFunction ssSelAggCast fullSelName <> pgFmtAs ssSelAlias
where
fullSelName = case ssSelName of
"*" -> pgFmtIdent aggAlias <> ".*"
_ -> pgFmtIdent aggAlias <> "." <> pgFmtIdent ssSelName
pgFmtApplyAggregate :: Maybe AggregateFunction -> Maybe Cast -> SQL.Snippet -> SQL.Snippet pgFmtApplyAggregate :: Maybe AggregateFunction -> Maybe Cast -> SQL.Snippet -> SQL.Snippet
pgFmtApplyAggregate Nothing _ snippet = snippet pgFmtApplyAggregate Nothing _ snippet = snippet
@@ -291,14 +288,6 @@ pgFmtApplyAggregate (Just agg) aggCast snippet =
convertAggFunction = SQL.sql . BS.map toUpper . BS.pack . show convertAggFunction = SQL.sql . BS.map toUpper . BS.pack . show
aggregatedSnippet = convertAggFunction agg <> "(" <> snippet <> ")" aggregatedSnippet = convertAggFunction agg <> "(" <> snippet <> ")"
pgFmtSpreadJoinSelectItem :: Alias -> [CoercibleOrderTerm] -> SpreadSelectField -> SQL.Snippet
pgFmtSpreadJoinSelectItem aggAlias order SpreadSelectField{ssSelName, ssSelAlias} =
"COALESCE(json_agg(" <> fmtField <> " " <> fmtOrder <> "),'[]')::jsonb" <> " AS " <> fmtAlias
where
fmtField = pgFmtFullSelName aggAlias ssSelName
fmtOrder = orderF (QualifiedIdentifier "" aggAlias) order
fmtAlias = pgFmtIdent (fromMaybe ssSelName ssSelAlias)
pgFmtApplyCast :: Maybe Cast -> SQL.Snippet -> SQL.Snippet pgFmtApplyCast :: Maybe Cast -> SQL.Snippet -> SQL.Snippet
pgFmtApplyCast Nothing snippet = snippet pgFmtApplyCast Nothing snippet = snippet
-- Ideally we'd quote the cast with "pgFmtIdent cast". However, that would invalidate common casts such as "int", "bigint", etc. -- Ideally we'd quote the cast with "pgFmtIdent cast". However, that would invalidate common casts such as "int", "bigint", etc.
@@ -306,11 +295,6 @@ pgFmtApplyCast Nothing snippet = snippet
-- Not quoting should be fine, we validate the input on Parsers. -- Not quoting should be fine, we validate the input on Parsers.
pgFmtApplyCast (Just cast) snippet = "CAST( " <> snippet <> " AS " <> SQL.sql (encodeUtf8 cast) <> " )" pgFmtApplyCast (Just cast) snippet = "CAST( " <> snippet <> " AS " <> SQL.sql (encodeUtf8 cast) <> " )"
pgFmtFullSelName :: Alias -> FieldName -> SQL.Snippet
pgFmtFullSelName aggAlias fieldName = case fieldName of
"*" -> pgFmtIdent aggAlias <> ".*"
_ -> pgFmtIdent aggAlias <> "." <> pgFmtIdent fieldName
-- TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body -- TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body
fromJsonBodyF :: Maybe LBS.ByteString -> [CoercibleField] -> Bool -> Bool -> Bool -> SQL.Snippet fromJsonBodyF :: Maybe LBS.ByteString -> [CoercibleField] -> Bool -> Bool -> Bool -> SQL.Snippet
fromJsonBodyF body fields includeSelect includeLimitOne includeDefaults = fromJsonBodyF body fields includeSelect includeLimitOne includeDefaults =
@@ -395,15 +379,13 @@ pgFmtFilter table (CoercibleFilter fld (OpExpr hasNot oper)) = notOp <> " " <> p
-- IS cannot be prepared. `PREPARE boolplan AS SELECT * FROM projects where id IS $1` will give a syntax error. -- IS cannot be prepared. `PREPARE boolplan AS SELECT * FROM projects where id IS $1` will give a syntax error.
-- The above can be fixed by using `PREPARE boolplan AS SELECT * FROM projects where id IS NOT DISTINCT FROM $1;` -- The above can be fixed by using `PREPARE boolplan AS SELECT * FROM projects where id IS NOT DISTINCT FROM $1;`
-- However that would not accept the TRUE/FALSE/NULL/"NOT NULL"/UNKNOWN keywords. See: https://stackoverflow.com/questions/6133525/proper-way-to-set-preparedstatement-parameter-to-null-under-postgres. -- However that would not accept the TRUE/FALSE/NULL/UNKNOWN keywords. See: https://stackoverflow.com/questions/6133525/proper-way-to-set-preparedstatement-parameter-to-null-under-postgres.
-- This is why `IS` operands are whitelisted at the Parsers.hs level -- This is why `IS` operands are whitelisted at the Parsers.hs level
Is isVal -> " IS " <> Is triVal -> " IS " <> case triVal of
case isVal of TriTrue -> "TRUE"
IsNull -> "NULL" TriFalse -> "FALSE"
IsNotNull -> "NOT NULL" TriNull -> "NULL"
IsTriTrue -> "TRUE" TriUnknown -> "UNKNOWN"
IsTriFalse -> "FALSE"
IsTriUnknown -> "UNKNOWN"
IsDistinctFrom val -> " IS DISTINCT FROM " <> unknownLiteral val IsDistinctFrom val -> " IS DISTINCT FROM " <> unknownLiteral val
@@ -414,8 +396,9 @@ pgFmtFilter table (CoercibleFilter fld (OpExpr hasNot oper)) = notOp <> " " <> p
[""] -> "= ANY('{}') " [""] -> "= ANY('{}') "
_ -> "= ANY (" <> pgFmtArrayLiteralForField vals fld <> ") " _ -> "= ANY (" <> pgFmtArrayLiteralForField vals fld <> ") "
Fts op lang val -> " " <> ftsOperator op <> "(" <> pgFmtFtsLang lang <> unknownLiteral val <> ") " Fts op lang val -> " " <> ftsOperator op <> "(" <> ftsLang lang <> unknownLiteral val <> ") "
where where
ftsLang = maybe mempty (\l -> unknownLiteral l <> ", ")
notOp = if hasNot then "NOT" else mempty notOp = if hasNot then "NOT" else mempty
star c = if c == '*' then '%' else c star c = if c == '*' then '%' else c
fmtQuant q val = case q of fmtQuant q val = case q of
@@ -423,9 +406,6 @@ pgFmtFilter table (CoercibleFilter fld (OpExpr hasNot oper)) = notOp <> " " <> p
Just QuantAll -> "ALL(" <> val <> ")" Just QuantAll -> "ALL(" <> val <> ")"
Nothing -> val Nothing -> val
pgFmtFtsLang :: Maybe Text -> SQL.Snippet
pgFmtFtsLang = maybe mempty (\l -> unknownLiteral l <> ", ")
pgFmtJoinCondition :: JoinCondition -> SQL.Snippet pgFmtJoinCondition :: JoinCondition -> SQL.Snippet
pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) = pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2 pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2
@@ -522,6 +502,13 @@ currentSettingF setting =
-- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15 -- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
"nullif(current_setting('" <> setting <> "', true), '')" "nullif(current_setting('" <> setting <> "', true), '')"
mutRangeF :: QualifiedIdentifier -> [FieldName] -> (SQL.Snippet, SQL.Snippet)
mutRangeF mainQi rangeId =
(
intercalateSnippet " AND " $ (\col -> pgFmtColumn mainQi col <> " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_affected_rows") col) <$> rangeId
, intercalateSnippet ", " (pgFmtColumn mainQi <$> rangeId)
)
orderF :: QualifiedIdentifier -> [CoercibleOrderTerm] -> SQL.Snippet orderF :: QualifiedIdentifier -> [CoercibleOrderTerm] -> SQL.Snippet
orderF _ [] = mempty orderF _ [] = mempty
orderF qi ordts = "ORDER BY " <> intercalateSnippet ", " (pgFmtOrderTerm qi <$> ordts) orderF qi ordts = "ORDER BY " <> intercalateSnippet ", " (pgFmtOrderTerm qi <$> ordts)
+9 -12
View File
@@ -56,11 +56,10 @@ data ResultSet
prepareWrite :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> MediaType -> MediaHandler -> prepareWrite :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> MediaType -> MediaHandler ->
Maybe PreferRepresentation -> Maybe PreferResolution -> [Text] -> Bool -> (SQL.Statement () ResultSet, ByteString) Maybe PreferRepresentation -> Maybe PreferResolution -> [Text] -> Bool -> SQL.Statement () ResultSet
prepareWrite selectQuery mutateQuery isInsert isPut mt handler rep resolution pKeys prepared = prepareWrite selectQuery mutateQuery isInsert isPut mt handler rep resolution pKeys =
(result, sql) SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
where where
result@(SQL.Statement sql _ _ _) = SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt prepared
checkUpsert snip = if isInsert && (isPut || resolution == Just MergeDuplicates) then snip else "''" checkUpsert snip = if isInsert && (isPut || resolution == Just MergeDuplicates) then snip else "''"
pgrstInsertedF = checkUpsert "nullif(current_setting('pgrst.inserted', true),'')::int" pgrstInsertedF = checkUpsert "nullif(current_setting('pgrst.inserted', true),'')::int"
snippet = snippet =
@@ -94,11 +93,10 @@ prepareWrite selectQuery mutateQuery isInsert isPut mt handler rep resolution pK
MTVndPlan{} -> planRow MTVndPlan{} -> planRow
_ -> fromMaybe (RSStandard Nothing 0 mempty mempty Nothing Nothing Nothing) <$> HD.rowMaybe (standardRow False) _ -> fromMaybe (RSStandard Nothing 0 mempty mempty Nothing Nothing Nothing) <$> HD.rowMaybe (standardRow False)
prepareRead :: SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler -> Bool -> (SQL.Statement () ResultSet, ByteString) prepareRead :: SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler -> Bool -> SQL.Statement () ResultSet
prepareRead selectQuery countQuery countTotal mt handler prepared = prepareRead selectQuery countQuery countTotal mt handler =
(result, sql) SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
where where
result@(SQL.Statement sql _ _ _) = SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt prepared
snippet = snippet =
"WITH " <> sourceCTE <> " AS ( " <> selectQuery <> " ) " <> "WITH " <> sourceCTE <> " AS ( " <> selectQuery <> " ) " <>
countCTEF <> " " <> countCTEF <> " " <>
@@ -120,11 +118,10 @@ prepareRead selectQuery countQuery countTotal mt handler prepared =
prepareCall :: Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool -> prepareCall :: Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
MediaType -> MediaHandler -> Bool -> MediaType -> MediaHandler -> Bool ->
(SQL.Statement () ResultSet, ByteString) SQL.Statement () ResultSet
prepareCall rout callProcQuery selectQuery countQuery countTotal mt handler prepared = prepareCall rout callProcQuery selectQuery countQuery countTotal mt handler =
(result, sql) SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
where where
result@(SQL.Statement sql _ _ _) = SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt prepared
snippet = snippet =
"WITH " <> sourceCTE <> " AS (" <> callProcQuery <> ") " <> "WITH " <> sourceCTE <> " AS (" <> callProcQuery <> ") " <>
countCTEF <> countCTEF <>
+31 -47
View File
@@ -50,6 +50,7 @@ import PostgREST.SchemaCache.Routine (FuncVolatility (..),
Routine (..)) Routine (..))
import PostgREST.SchemaCache.Table (Table (..)) import PostgREST.SchemaCache.Table (Table (..))
import qualified PostgREST.ApiRequest.Types as ApiRequestTypes
import qualified PostgREST.SchemaCache.Routine as Routine import qualified PostgREST.SchemaCache.Routine as Routine
import Protolude hiding (Handler, toS) import Protolude hiding (Handler, toS)
@@ -68,8 +69,7 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
RSStandard{..} -> do RSStandard{..} -> do
let let
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal (status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
cLHeader = if headersOnly then mempty else [contentLengthHeaderStrict rsBody] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
headers = headers =
[ contentRange [ contentRange
, ( "Content-Location" , ( "Content-Location"
@@ -78,21 +78,20 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
<> if BS.null (qsCanonical iQueryParams) then mempty else "?" <> qsCanonical iQueryParams <> if BS.null (qsCanonical iQueryParams) then mempty else "?" <> qsCanonical iQueryParams
) )
] ]
++ cLHeader
++ contentTypeHeaders wrMedia ctxApiRequest ++ contentTypeHeaders wrMedia ctxApiRequest
++ prefHeader ++ prefHeader
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers (ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers
let bod | status == HTTP.status416 = Error.errorPayload $ Error.ApiRequestError $ Error.InvalidRange $ let bod | status == HTTP.status416 = Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange $
Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal) ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
| headersOnly = mempty | headersOnly = mempty
| otherwise = LBS.fromStrict rsBody | otherwise = LBS.fromStrict rsBody
Right $ PgrstResponse ovStatus ovHeaders bod Right $ PgrstResponse ovStatus ovHeaders bod
RSPlan plan -> RSPlan plan ->
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders wrMedia ctxApiRequest) $ LBS.fromStrict plan Right $ PgrstResponse HTTP.status200 (contentTypeHeaders wrMedia ctxApiRequest) $ LBS.fromStrict plan
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutatePlan, mrMedia, crudQi=QualifiedIdentifier{..}} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}, ..} _ _ _ _ _ = case resultSet of actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutatePlan, mrMedia, crudQi=QualifiedIdentifier{..}} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}, ..} _ _ _ _ _ = case resultSet of
RSStandard{..} -> do RSStandard{..} -> do
@@ -100,7 +99,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;} pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;}
prefHeader = prefAppliedHeader $ prefHeader = prefAppliedHeader $
Preferences (if null pkCols && isNothing (qsOnConflict iQueryParams) then Nothing else preferResolution) Preferences (if null pkCols && isNothing (qsOnConflict iQueryParams) then Nothing else preferResolution)
preferRepresentation preferCount preferTransaction preferMissing preferHandling preferTimezone Nothing [] preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone Nothing []
headers = headers =
catMaybes catMaybes
[ if null rsLocation then [ if null rsLocation then
@@ -114,7 +113,6 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
) )
, Just . RangeQuery.contentRangeH 1 0 $ , Just . RangeQuery.contentRangeH 1 0 $
if shouldCount preferCount then Just rsQueryTotal else Nothing if shouldCount preferCount then Just rsQueryTotal else Nothing
, Just $ contentLengthHeaderStrict rsBody
, prefHeader ] , prefHeader ]
let isInsertIfGTZero i = let isInsertIfGTZero i =
@@ -133,7 +131,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
Right $ PgrstResponse ovStatus ovHeaders bod Right $ PgrstResponse ovStatus ovHeaders bod
RSPlan plan -> RSPlan plan ->
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
RSStandard{..} -> do RSStandard{..} -> do
@@ -141,12 +139,12 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia}
contentRangeHeader = contentRangeHeader =
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $ Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
if shouldCount preferCount then Just rsQueryTotal else Nothing if shouldCount preferCount then Just rsQueryTotal else Nothing
prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction preferMissing preferHandling preferTimezone preferMaxAffected [] prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone preferMaxAffected []
headers = catMaybes [contentRangeHeader, prefHeader] headers = catMaybes [contentRangeHeader, prefHeader]
let (status, headers', body) = let (status, headers', body) =
case preferRepresentation of case preferRepresentation of
Just Full -> (HTTP.status200, headers ++ [contentLengthHeaderStrict rsBody] ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody) Just Full -> (HTTP.status200, headers ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
Just None -> (HTTP.status204, headers, mempty) Just None -> (HTTP.status204, headers, mempty)
_ -> (HTTP.status204, headers, mempty) _ -> (HTTP.status204, headers, mempty)
@@ -155,20 +153,19 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia}
Right $ PgrstResponse ovStatus ovHeaders body Right $ PgrstResponse ovStatus ovHeaders body
RSPlan plan -> RSPlan plan ->
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
RSStandard {..} -> do RSStandard {..} -> do
let let
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction Nothing preferHandling preferTimezone Nothing [] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
cLHeader = [contentLengthHeaderStrict rsBody]
cTHeader = contentTypeHeaders mrMedia ctxApiRequest cTHeader = contentTypeHeaders mrMedia ctxApiRequest
let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200 let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200
upsertStatus = isInsertIfGTZero $ fromJust rsInserted upsertStatus = isInsertIfGTZero $ fromJust rsInserted
(status, headers, body) = (status, headers, body) =
case preferRepresentation of case preferRepresentation of
Just Full -> (upsertStatus, cLHeader ++ cTHeader ++ prefHeader, LBS.fromStrict rsBody) Just Full -> (upsertStatus, cTHeader ++ prefHeader, LBS.fromStrict rsBody)
Just None -> (HTTP.status204, prefHeader, mempty) Just None -> (HTTP.status204, prefHeader, mempty)
_ -> (HTTP.status204, prefHeader, mempty) _ -> (HTTP.status204, prefHeader, mempty)
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers (ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers
@@ -176,7 +173,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrM
Right $ PgrstResponse ovStatus ovHeaders body Right $ PgrstResponse ovStatus ovHeaders body
RSPlan plan -> RSPlan plan ->
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
RSStandard {..} -> do RSStandard {..} -> do
@@ -184,12 +181,12 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia}
contentRangeHeader = contentRangeHeader =
RangeQuery.contentRangeH 1 0 $ RangeQuery.contentRangeH 1 0 $
if shouldCount preferCount then Just rsQueryTotal else Nothing if shouldCount preferCount then Just rsQueryTotal else Nothing
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected [] prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
headers = contentRangeHeader : prefHeader headers = contentRangeHeader : prefHeader
let (status, headers', body) = let (status, headers', body) =
case preferRepresentation of case preferRepresentation of
Just Full -> (HTTP.status200, headers ++ [contentLengthHeaderStrict rsBody] ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody) Just Full -> (HTTP.status200, headers ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
Just None -> (HTTP.status204, headers, mempty) Just None -> (HTTP.status204, headers, mempty)
_ -> (HTTP.status204, headers, mempty) _ -> (HTTP.status204, headers, mempty)
@@ -198,7 +195,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia}
Right $ PgrstResponse ovStatus ovHeaders body Right $ PgrstResponse ovStatus ovHeaders body
RSPlan plan -> RSPlan plan ->
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=proc} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..},..} _ _ _ _ _ = case resultSet of actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=proc} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..},..} _ _ _ _ _ = case resultSet of
RSStandard {..} -> do RSStandard {..} -> do
@@ -206,12 +203,10 @@ actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=p
(status, contentRange) = (status, contentRange) =
RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
rsOrErrBody = if status == HTTP.status416 rsOrErrBody = if status == HTTP.status416
then Error.errorPayload $ Error.ApiRequestError $ Error.InvalidRange then Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange
$ Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal) $ ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
else LBS.fromStrict rsBody else LBS.fromStrict rsBody
isHeadMethod = invMethod == InvRead True prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferParameters preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
cLHeader = if isHeadMethod then mempty else [contentLengthHeaderLazy rsOrErrBody]
headers = contentRange : prefHeader headers = contentRange : prefHeader
let (status', headers', body) = let (status', headers', body) =
@@ -219,27 +214,25 @@ actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=p
(HTTP.status204, headers, mempty) (HTTP.status204, headers, mempty)
else else
(status, (status,
headers ++ cLHeader ++ contentTypeHeaders crMedia ctxApiRequest, headers ++ contentTypeHeaders crMedia ctxApiRequest,
if isHeadMethod then mempty else rsOrErrBody) if invMethod == InvRead True then mempty else rsOrErrBody)
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status' headers' (ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status' headers'
Right $ PgrstResponse ovStatus ovHeaders body Right $ PgrstResponse ovStatus ovHeaders body
RSPlan plan -> RSPlan plan ->
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders crMedia ctxApiRequest) $ LBS.fromStrict plan Right $ PgrstResponse HTTP.status200 (contentTypeHeaders crMedia ctxApiRequest) $ LBS.fromStrict plan
actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versions conf sCache schema negotiatedByProfile = actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versions conf sCache schema negotiatedByProfile =
let Right $ PgrstResponse HTTP.status200
rsBody = maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body (MediaType.toContentType MTOpenAPI : maybeToList (profileHeader schema negotiatedByProfile))
cLHeader = if headersOnly then mempty else [contentLengthHeaderLazy rsBody] (maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body)
in
Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader schema negotiatedByProfile)) rsBody
actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ SchemaCache{dbTables} _ _ = actionResponse (NoDbResult (RelInfoPlan identifier)) _ _ _ sCache _ _ =
case HM.lookup qi dbTables of case HM.lookup identifier (dbTables sCache) of
Just tbl -> respondInfo $ allowH tbl Just tbl -> respondInfo $ allowH tbl
Nothing -> Left $ Error.SchemaCacheErr $ Error.TableNotFound qiSchema qiName (HM.elems dbTables) Nothing -> Left $ Error.ApiRequestError ApiRequestTypes.NotFound
where where
allowH table = allowH table =
let hasPK = not . null $ tablePKCols table in let hasPK = not . null $ tablePKCols table in
@@ -259,7 +252,7 @@ actionResponse (NoDbResult SchemaInfoPlan) _ _ _ _ _ _ = respondInfo "OPTIONS,GE
respondInfo :: ByteString -> Either Error.Error PgrstResponse respondInfo :: ByteString -> Either Error.Error PgrstResponse
respondInfo allowHeader = respondInfo allowHeader =
let allOrigins = ("Access-Control-Allow-Origin", "*") in let allOrigins = ("Access-Control-Allow-Origin", "*") in
Right $ PgrstResponse HTTP.status200 [contentLengthHeaderStrict mempty, allOrigins, (HTTP.hAllow, allowHeader)] mempty Right $ PgrstResponse HTTP.status200 [allOrigins, (HTTP.hAllow, allowHeader)] mempty
-- Status and headers can be overridden as per https://postgrest.org/en/stable/references/transactions.html#response-headers -- Status and headers can be overridden as per https://postgrest.org/en/stable/references/transactions.html#response-headers
overrideStatusHeaders :: Maybe Text -> Maybe BS.ByteString -> HTTP.Status -> [HTTP.Header]-> Either Error.Error (HTTP.Status, [HTTP.Header]) overrideStatusHeaders :: Maybe Text -> Maybe BS.ByteString -> HTTP.Status -> [HTTP.Header]-> Either Error.Error (HTTP.Status, [HTTP.Header])
@@ -270,20 +263,11 @@ overrideStatusHeaders rsGucStatus rsGucHeaders pgrstStatus pgrstHeaders = do
decodeGucHeaders :: Maybe BS.ByteString -> Either Error.Error [GucHeader] decodeGucHeaders :: Maybe BS.ByteString -> Either Error.Error [GucHeader]
decodeGucHeaders = decodeGucHeaders =
maybe (Right []) $ first (const . Error.ApiRequestError $ Error.GucHeadersError) . JSON.eitherDecode . LBS.fromStrict maybe (Right []) $ first (const . Error.ApiRequestError $ ApiRequestTypes.GucHeadersError) . JSON.eitherDecode . LBS.fromStrict
decodeGucStatus :: Maybe Text -> Either Error.Error (Maybe HTTP.Status) decodeGucStatus :: Maybe Text -> Either Error.Error (Maybe HTTP.Status)
decodeGucStatus = decodeGucStatus =
maybe (Right Nothing) $ first (const . Error.ApiRequestError $ Error.GucStatusError) . fmap (Just . toEnum . fst) . decimal maybe (Right Nothing) $ first (const . Error.ApiRequestError $ ApiRequestTypes.GucStatusError) . fmap (Just . toEnum . fst) . decimal
contentLengthHeader :: Show b => (a -> b) -> a -> HTTP.Header
contentLengthHeader lenFn body = ("Content-Length", show (lenFn body))
contentLengthHeaderStrict :: BS.ByteString -> HTTP.Header
contentLengthHeaderStrict = contentLengthHeader BS.length
contentLengthHeaderLazy :: LBS.ByteString -> HTTP.Header
contentLengthHeaderLazy = contentLengthHeader LBS.length
contentTypeHeaders :: MediaType -> ApiRequest -> [HTTP.Header] contentTypeHeaders :: MediaType -> ApiRequest -> [HTTP.Header]
contentTypeHeaders mediaType ApiRequest{..} = contentTypeHeaders mediaType ApiRequest{..} =

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