Compare commits

...
323 Commits
Author SHA1 Message Date
steve-chavez 83e1bebcdc bump version to 14.10 2026-04-16 13:54:44 -05:00
renovate[bot]andWolfgang Walther 89132e886a chore(deps): update ubuntu:noble docker digest to c4a8d55 2026-04-16 10:44:03 +00:00
Michal KleczekandTaimoor Zaeem abd76ca8ac fix: Flush pool as late as possible during schema cache reloading (#4645)
retryingSchemaCacheLoad flushes the pool upon every retry before it starts reloading the schema. This is too early as schema reloading might take some time during which new connections might be acquired. The consequence is that:
* upon successful schema cache reload we might have some connections created with the old schema cache
* we close connections upon each retry and under load we will keep closing and re-opening connections until schema cache load succeeds

This change is to make sure we flush the pool only after successful schema cache querying but before loading (so that connections acquired during loading wait for it and do not interfere with timing the loading process).
2026-04-16 11:55:43 +05:00
Michał KłeczekandTaimoor Zaeem df87ce46ed add: log pool flushes
Emit a dedicated PoolFlushed observation when the DB pool is released during schema cache reload.
2026-04-16 11:18:44 +05:00
renovate[bot]andWolfgang Walther d369d2c41e chore(deps): update haskell-actions/setup action to v2.11.0 2026-04-15 18:05:21 +00:00
Wolfgang Walther 9ee366a903 docs: fix broken link 2026-04-15 09:30:05 +02:00
Michał KłeczekandTaimoor Zaeem 01f7f13389 test(refactor): make debug output testing more robust
This change introduces a match_log function, that implements lenient matching of log output lines. Tests of log output are retrofited to use the function.
2026-04-15 07:40:16 +05:00
steve-chavez 4922a9feaf docs: target_session_attrs=read-only in libpq>=14 2026-04-14 11:11:22 -05:00
renovate[bot]andWolfgang Walther 679b957355 chore(deps): update actions/cache action to v5.0.5 2026-04-13 21:04:21 +00:00
renovate[bot]andWolfgang Walther 05df2c5692 chore(deps): update all dependencies 2026-04-12 15:25:03 +00:00
steve-chavez 16e2c73a95 bump version to 14.9 2026-04-10 18:25:04 -05:00
steve-chavez fb31654277 fix: remove red herring warp logs on default log-level
The logs added on e95e815483 are red
herrings under normal operation.

This moves them to `log-level=debug` and removes "error" from the
message prefix.

Fixes https://github.com/PostgREST/postgrest/issues/4799
2026-04-10 18:16:19 -05:00
steve-chavez 9230f3f49a Revert "refactor: Simplify schema cache loading triggering logic"
This reverts commit 810023a47c.
2026-04-10 18:16:19 -05:00
steve-chavez 96bbc86756 Revert "refactor: clarify debouncer function"
This reverts commit 577ed4dd4c.
2026-04-10 18:16:19 -05:00
renovate[bot]andWolfgang Walther dd394d6f81 chore(deps): update haskell-actions/setup action to v2.10.4 2026-04-09 08:48:45 +00:00
Steve ChavezandTaimoor Zaeem 577ed4dd4c refactor: clarify debouncer function
(cherry picked from commit 3d98f8d65b)
2026-04-08 10:46:38 +05:00
Michał KłeczekandTaimoor Zaeem 810023a47c refactor: Simplify schema cache loading triggering logic
DISCLAIMER:
This commit was authored entirely by a human without the assistance of LLMs.

Using debouncer to trigger schema cache loading makes it difficult to understand when exactly it is triggered.

(cherry picked from commit a4c1d945ee)
2026-04-08 10:46:38 +05:00
renovate[bot]andWolfgang Walther 687ebf0850 chore(deps): update ubuntu:noble docker digest to 84e77de 2026-04-07 08:10:14 +00:00
Michał KłeczekandSteve Chavez b9c8562641 add: Log pg version details of listener connection
Follow-up to #4617 adding more information to log entry produced upon successful listener connection establishement.
2026-04-06 11:11:16 -05:00
Michał KłeczekandSteve Chavez 34a767a5cc add: Log actual host and port of listener connection
Diagnosing problems with listener channel notifications not being handled properly by PostgREST connected to read replicas is difficult. Issues might be related to lost connections and listener not being connected to the right host after failover or database server restarts.
This patch adds logging of actual host:port used by libpq connection opened by the listener. It should make it easier to find out if PostgREST is connected to the right host.
2026-04-06 11:11:16 -05:00
steve-chavezandTaimoor Zaeem 5eac8bd203 docs: clarify set operators need views/functions
Closes https://github.com/PostgREST/postgrest/issues/4780.
2026-04-06 11:18:42 +05:00
steve-chavez 9f722e0799 bump version to 14.8 2026-04-03 16:51:55 -05:00
Artur Bento de CarvalhoandSteve Chavez 0d4d1dca51 fix: use int32/int64 formats for integer types
Fixed integer type mapping in OpenAPI 2.0: replaced the invalid integer format with int32/int64 and added the toSwaggerFormat function to map PostgreSQL types to valid OpenAPI 2.0 formats:

smallint -> int32
integer -> int32
bigint -> int64
2026-04-03 16:36:55 -05:00
Laurence IslaandSteve Chavez 250747aadc add(logs): Include a HINT when the LISTEN channel breaks due to a Postgres bug
The HINT shows a SQL command that solves the issue.
2026-04-03 16:36:55 -05:00
Michał KłeczekandTaimoor Zaeem aae929a718 test: Schema cache load debouncing
test: adjust replicateM to 100
(cherry picked from commit 328598eaed)
2026-04-03 08:00:57 +05:00
renovate[bot]andWolfgang Walther 77dde73057 chore(deps): update docker/login-action action to v4.1.0 2026-04-02 20:06:52 +00:00
Michal KleczekandSteve Chavez afb95a5268 refactor(test): provide means to validate metrics and observations
Some helpers are provided for introspecting metrics already (used in JWT cache tests). This change provides facilities to additionally validate emited Observation events.
A new Spec module is also implemented, adding basic tests of schema cache reloading - their main goal is to excercise the new infrastructure.
2026-04-02 13:34:38 -05:00
Michał KłeczekandTaimoor Zaeem 8262faa235 refactor: move socket creation and management to App module
Right now listening sockets initialization, management and usage is split between App, AppState and Admin modules: they are created in AppState.init and remembered in AppState but used only in App and Admin.

It has several negative consequences:
- sockets are initialized even if not needed (eg. command line invocations like dump-config or dump-schema)
- it is impossible to start listening on a socket after initial schema cache load because it requires AppState

This change decouples listen socket management from AppState. Sockets are created only when needed (ie. not in command line tools invocation) and passed to admin application and to Warp by the App module.
2026-04-02 12:27:31 +05:00
Michał KłeczekandTaimoor Zaeem 1d40fe5d93 test: Fix flakiness of test_second_request_for_non_existent_table_should_be_quick
Changed divider in assertion (response.elapsed.total_seconds() < first_duration / divider) to 2 (from 10).

(cherry picked from commit 886df84e87)
2026-04-01 15:14:45 +05:00
renovate[bot]andWolfgang Walther 3a4dc5eff3 chore(deps): update codecov/codecov-action action to v6 2026-03-27 11:02:38 +00:00
renovate[bot]andWolfgang Walther 55912515c0 chore(deps): update all dependencies 2026-03-27 08:45:13 +00:00
postgrest-ci[bot]andGitHub 863d1c9e9b docs: explain schema cache reload behavior with NOTIFY debouncing 2026-03-26 11:00:40 +05:00
steve-chavez 806579e659 bump version to 14.7 2026-03-20 14:22:31 -05:00
steve-chavez 1856434a74 fix: not logging termination unix signals
Under container environments like ECS, it's hard to know when PostgREST
is being terminated.
2026-03-20 13:38:40 -05:00
Taimoor ZaeemandSteve Chavez dade15acc4 nix(test): add test suite for observability tests
- Create separate test suite for observability tests

- Create wrapper script `postgrest-test-observability`

- Add to CI and `postgrest-check`

- Move JWT cache tests under observability tests

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-03-20 09:13:47 -05:00
Michał KłeczekandSteve Chavez 79531a612d test(spec): Move metrics state helpers from JwtCacheSpec to SpecHelpers
Refactoring: State validation helpers used in JwtCacheSpec moved to SpecHelper
to make them available in other Spec modules.

(cherry picked from commit 85a313a8cc)
2026-03-19 14:30:18 -05:00
renovate[bot]andWolfgang Walther 9d0e13d961 chore(deps): update ubuntu:noble docker digest to 186072b 2026-03-19 09:15:59 +00:00
renovate[bot]andWolfgang Walther 3e61dc024e chore(deps): update cachix/cachix-action action to v17 2026-03-18 21:24:50 +00:00
renovate[bot]andWolfgang Walther 1bdf773417 chore(deps): update all dependencies 2026-03-18 20:01:41 +00:00
renovate[bot]andWolfgang Walther 55868351f3 chore(deps): update actions/cache action to v5.0.4 2026-03-18 19:09:06 +00:00
renovate[bot]andWolfgang Walther 8c3cb76fbd chore(deps): update ubuntu:noble docker digest to 0d39fcc 2026-03-17 10:22:17 +00:00
renovate[bot]andWolfgang Walther 3bdb69bae7 chore(deps): update actions/create-github-app-token action to v3 2026-03-14 18:06:45 +00:00
renovate[bot]andWolfgang Walther 1ed3c2c197 chore(deps): update all dependencies 2026-03-13 17:23:12 +00:00
renovate[bot]andWolfgang Walther dde145b685 chore(deps): update all dependencies 2026-03-13 17:19:11 +00:00
Wolfgang Walther 720705e32b docs: fix prometheus text format link
Reported by linkcheck.
2026-03-11 09:45:21 +01:00
steve-chavez 6813ceaf31 bump version to 14.6 2026-03-06 16:30:04 -05:00
Taimoor Zaeemandsteve-chavez 73c8ae0bbc fix(error): leaking table and function names when calculating hint
Increase similarity score to 0.75 from 0.33 for table and functions
error hint.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-03-06 16:16:05 -05:00
Laurence Isla 6f356e32b6 chore: update sponsors
* The list of sponsors is now ordered by total contribution value (highest first)
* Removed former backers from sponsor pages
2026-03-03 23:11:22 -05:00
Laurence Isla c4772184e4 chore: update sponsor 2026-03-03 23:11:22 -05:00
dshukertjrandWolfgang Walther 0602080acb docs: Update the Supabase logo to a correct one 2026-02-19 10:54:29 +01:00
steve-chavez dc67e4d3f5 chore: fix changelog entries 2026-02-13 13:40:38 -05:00
steve-chavez d89826b982 bump version to 14.5 2026-02-12 20:12:52 -05:00
steve-chavez 94350fdde6 fix: don't hide async exceptions in logs
Fixes #4646. Using the repro on #4646, this now produces the log:

```
11/Feb/2026:09:40:08 -0500: Warp server error: stack overflow
```

When:
```
$ curl localhost:3000/
curl: (52) Empty reply from server
```

(cherry picked from commit e95e815483)
2026-02-12 20:11:44 -05:00
Taimoor ZaeemandWolfgang Walther 230eb3630c docs(install): update postgresql minimum supported version
PostgREST dropped support for PostgreSQL version 12 however,
it was not reflected in the docs.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-02-12 10:23:29 +01:00
steve-chavez 5485b8ca9a bump version to 14.4 2026-01-29 14:13:49 -05:00
steve-chavez 895e9c536c chore: remove wrong entry in CHANGELOG 2026-01-29 13:24:20 -05:00
Michal Kleczekandsteve-chavez 16c767134c fix: listener running with exception masked after first failure 2026-01-29 13:22:46 -05:00
Laurence IslaandSteve Chavez 0a8b836435 fix: filtering the returned representation whenn using or/and filters on mutations
(cherry picked from commit 1682677297)
2026-01-29 09:16:08 -05:00
Michal KleczekandSteve Chavez 5796f86100 fix: ensure Listener connections are released
retryingListen function potentially leaks database connections. This patch ensures the connections are released in case of listen/notify errors.

(cherry picked from commit 00c7cb1a22)
2026-01-28 18:26:15 -05:00
Wolfgang Walther 101eac1cce docs: fix links
datrium.com doesn't exist anymore, while euronodes.com seems to only
fail SSL in CI.
2026-01-28 09:57:23 +01:00
renovate[bot]andWolfgang Walther 1ae14afdf2 chore(deps): update haskell-actions/setup action to v2.10.2 2026-01-11 17:36:28 +00:00
Wolfgang Walther 69090bd224 ci: pin backport action to version instead of default branch 2026-01-11 18:34:43 +01:00
renovate[bot]andWolfgang Walther 5d5160fbd7 chore(deps): update haskell-actions/setup action to v2.10.1 2026-01-05 19:01:32 +00:00
steve-chavez 545f45d9de bump version to 14.3 2026-01-03 16:45:56 +08:00
Taimoor Zaeemandsteve-chavez eb55e73645 chore: move changelog entry to unreleased section
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2026-01-03 16:44:10 +08:00
Michał KłeczekandSteve Chavez e252a4900c fix: Performance and high memory usage of relation hint calculation
* Calculation of hint message when requested relation is not present in schema cache requires creation of a FuzzySet (to use fuzzy search to find candidate tables). For schemas with many tables it is costly.
This patch introduces dbTablesFuzzyIndex in SchemaCache to memoize the FuzzySet creation.

* Additionally, because of FuzzySet large memory requirements, this patch introduces a limit of 500 relations per schema, above which FuzzySet is not created and hint calculation disabled.

(cherry picked from commit e592d568c6)
2026-01-03 15:18:37 +08:00
Taimoor ZaeemandSteve Chavez 01bdb05c89 nix: add config file for hlint
Adds a config file for hlint containing arguments and
custom warnings.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-12-23 11:51:05 -05:00
122ed4d02e refactor: fix definition of Ord instance for Routine type (#4577)
The `Ord` instance definition for type `Routine` had a logical
error when comparing two routines. The error did not affect any
end users. However, for correctness and completeness reasons, this
commit fixes the error.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
Co-authored-by: Joel Jacobson <joel@compiler.org>
2025-12-23 11:51:05 -05:00
renovate[bot]andWolfgang Walther 7ff6755af7 chore(deps): update docker/setup-buildx-action action to v3.12.0 2025-12-20 20:24:23 +00:00
steve-chavez 29d6d35020 bump version to 14.2 2025-12-18 21:30:38 -05:00
steve-chavez 4d35eb02e7 fix: inaccurate Server-Timing durations
The transaction duration was notably off, doing:

```
curl localhost:3000/rpc/sleep?seconds=5 -i
```

Shows `46.1` for the `transaction;dur`, with this fix we obtain
`5007.3`.

Fixes https://github.com/PostgREST/postgrest/issues/4522

This also fixes inaccurate "schema cache queried" logs,
see https://github.com/PostgREST/postgrest/issues/4551.

(cherry picked from commit 013f078bc4)
2025-12-18 18:18:09 -05:00
renovate[bot]andWolfgang Walther e126956c1b chore(deps): update haskell-actions/setup action to v2.9.1 2025-12-17 18:01:25 +00:00
Joel JakobssonandSteve Chavez ce7871c047 fix: hasSingleUnnamedParam incorrectly matching named parameters
The hasSingleUnnamedParam function was only checking the parameter type
but not whether the parameter actually had no name. This caused functions
with a single NAMED parameter (e.g., `foo(data json)`) to incorrectly
match the single-param fallback mode.

The result was a confusing PostgreSQL error 42883 "function does not exist"
instead of a clean PGRST202 error explaining that no matching function
was found.

Added ppName == mempty check so functions with named parameters don't
incorrectly match the single-param fallback.

(cherry picked from commit fd6a3bdccf)
2025-12-16 17:24:20 -05:00
Taimoor ZaeemandSteve Chavez a59e6d97c5 refactor: create function to handle response preferences
Centralizes handling of preferences that are used to
create `Preference-Applied` header which is returned on
responses.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 29c2aa015f)
2025-12-16 11:20:33 -05:00
renovate[bot]andWolfgang Walther 40fec696a6 chore(deps): update korthout/backport-action action to v4.0.1 2025-12-15 20:17:46 +00:00
renovate[bot]andWolfgang Walther 9eb36f1244 chore(deps): update all dependencies 2025-12-13 11:31:10 +00:00
Laurence IslaandSteve Chavez 720eb8e528 chore(changelog): move incorrectly placed fixes 2025-12-12 14:47:26 -05:00
Laurence IslaandSteve Chavez aec95f7944 refactor: use only Lazy.ByteString to calculate the response body length 2025-12-12 14:47:26 -05:00
Laurence IslaandSteve Chavez 1c33d2dd38 fix: add missing Content-Length to empty HTTP 201 responses
For when a preference other than return=representation is requested.
2025-12-12 14:47:26 -05:00
Laurence IslaandSteve Chavez 7be638f0f2 fix: regression that truncates error message when offset is out of bounds
- Happens when offset > the number of rows and when "Prefer: count=exact" header is sent
- Regression introduced in commit 57ef998
2025-12-12 14:47:26 -05:00
Taimoor ZaeemandSteve Chavez d4b8109522 fix: misleading logs on unsupported postgresql versions
Postgrest fails on unsupported pg versions. However before killing
the thread, it continues to print a few more log messages which
were misleading. This commit fixes this by making sure that the
no log message should be printed after the unsupported pg version
observation and kill the thread immediately.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 0fa2f569a7)
2025-12-10 20:24:14 -05:00
renovate[bot]andWolfgang Walther f2b96b1d41 chore(deps): update codecov/codecov-action action to v5.5.2 2025-12-10 12:24:00 +00:00
renovate[bot]andWolfgang Walther cc43d356c1 chore(deps): update actions/create-github-app-token action to v2.2.1 2025-12-06 11:00:16 +00:00
renovate[bot]andWolfgang Walther 8bcbbd1bfd chore(deps): update all dependencies 2025-12-03 13:52:35 +00:00
renovate[bot]andWolfgang Walther ec4d2e2af3 chore(deps): update actions/checkout action to v6 2025-12-03 13:52:23 +00:00
Taimoor ZaeemandSteve Chavez e8cb0e33eb test(io): fix freeport function to prevent failures
Sometimes, a healthcheck related test fails as occurred in
https://github.com/PostgREST/postgrest/actions/runs/19771357953/job/56655949002.
This happens due to freeport function accidently picking up a used port.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 50eec773bf)
2025-11-30 13:38:55 -05:00
Taimoor ZaeemandSteve Chavez 05074f41c2 test(io): move fixtures to fixtures/ directory
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 35de13eebd)
2025-11-28 13:23:20 -05:00
Taimoor ZaeemandSteve Chavez c9c617cce1 test(io): rename fixtures.sql to load.sql
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 3baa6b0063)
2025-11-28 13:23:20 -05:00
Taimoor ZaeemandWolfgang Walther e429974f31 nix(shell): remove postgrest/ directory prefix when running pg (#4502)
When running postgres from nix-shell, nix creates a directory
structure like `postgrest/postgrest-with-pg-17-XXX` in the `/tmp`
directory. This commit removes the extra `postgrest/` prefix to
shorten length of absolute path length of filenames.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 802cce9a28)
2025-11-26 08:33:23 +00:00
Taimoor ZaeemandSteve Chavez 88538c1357 test(io): move authentication related tests to test_auth.py
The `test_io.py` module is too bloated (2100+ lines). To
logically group related tests, as a first step, this commit
separates authentication related IO tests into `test_auth.py`
module.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit c561a3749c)
2025-11-22 12:45:09 -05:00
Taimoor ZaeemandSteve Chavez 1cbe6b7c5f test(io): remove stale jwt cache test
Removes a test related to jwt cache which is stale
since #4084.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 379eaec8e0)
2025-11-19 16:48:25 -05:00
Taimoor ZaeemandWolfgang Walther f97200948f nix: shorten postgrest-with-postgresql-xx scripts
Renames these scripts to `postgrest-with-pg-xx`. The renaming
helps reduce the length of temporary filenames. This is needed
to ensure that socket file names remain under the maximum
allowed length of 107 chars.

Closes #4461.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 462798dc2d)
2025-11-18 18:27:17 +00:00
Taimoor ZaeemandWolfgang Walther 0bacce6909 docs: update server-host config in docker-compose example
The docker-compose example did not work with multiple containers
when PGRST_SERVER_HOST is set to `localhost`. This updates the
value to `0.0.0.0` allowing other containers to connect.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 08c6deba12)
2025-11-18 18:13:26 +00:00
renovate[bot]andWolfgang Walther 4c3ac2f087 chore(deps): update actions/checkout action to v5.0.1 2025-11-18 10:06:11 +00:00
Wolfgang Walther 1c6f215bd7 ci/test: remove macos x86 flake check
See previous commit.
2025-11-18 11:05:43 +01:00
Wolfgang Walther 94fb0a489a ci: remove macos x86 builds
The x86 GitHub runner will not be available anymore, soon.

We might be able to re-introduce this, once we can build a static
executable via Nix on darwin, too.
2025-11-18 11:01:41 +01:00
Taimoor ZaeemandSteve Chavez e4e1b626a6 test(io): move resource embedding tests to test_io.py
- Adds fixtures to `test/io/fixtures.sql` to test resource
  embedding related queries.

- Moves the resource embedding related tests that no longer
  require big schema from `test_big_schema.py` to `test_io.py`.

Closes #4417.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 91abcd49e1)
2025-11-17 13:24:49 -05:00
renovate[bot]andWolfgang Walther 810341f635 chore(deps): update ubuntu:noble docker digest to c35e29c 2025-11-15 13:52:10 +00:00
Taimoor ZaeemandWolfgang Walther 4a6f0b4a18 chore(cabal): remove unused haskell dependencies
This should reduce setup time for build process.

- cache: introduced in #2928, defunct since #4084
- clock: introduced in #2928, defunct since #4084
- heredoc: introduced in #714, defunct since #4390
- iproute: introduced in #3560, defunct since #4288

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 99217433b1)
2025-11-10 14:24:48 +00:00
steve-chavez b3b4e5ff35 bump version to 14.1 2025-11-05 09:09:22 -05:00
Taimoor ZaeemandWolfgang Walther 84f437b6c9 chore(changelog): update versioning scheme description
The changelog description mentioned that we follow semantic
versioning but from now on we don't. Hence updated the description
to reflect new versioning policy.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit eb908c696f)
2025-11-04 07:25:35 +00:00
renovate[bot]andWolfgang Walther a3c8065e58 chore(deps): update actions/checkout digest to 71cf226 2025-11-03 20:47:35 +00:00
Taimoor ZaeemandWolfgang Walther c797c09e22 fix: server-host !6 incorrectly binds to IPv4 address
Updates streaming-commons to version 0.2.3.1. This resolves #3202.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 58efc2680e)
2025-11-03 11:40:09 +00:00
Taimoor ZaeemandWolfgang Walther fc6fbe9748 chore(changelog): fix typo in changelog entry
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 8180905890)
2025-11-01 09:18:20 +00:00
Taimoor ZaeemandSteve Chavez 4aa712b9d8 fix: db-pre-config function failing with pg reserved words
When db-pre-config is accidentally set to a pg reserved word
like "true", it fails with a confusing error. The function
names should be properly quoted to avoid such errors. This commit
resolves this by quoting the pre-config function name.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit a688878236)
2025-10-30 14:32:37 -05:00
Taimoor ZaeemandSteve Chavez 939061baff refactor: move escapeIdent function to Identifiers.hs
Moves the functions `escapeIdent` and `trimNullChars` to
SchemaCache/Identifiers.hs module.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 66f84c5903)
2025-10-30 12:24:49 -05:00
Taimoor ZaeemandSteve Chavez 6150d53592 refactor: sort exports of Identifiers.hs and SqlFragments.hs
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
(cherry picked from commit 5d9b169380)
2025-10-30 12:24:49 -05:00
Wolfgang Walther d245e07df5 ci: fix tag job with new release workflow
A single component version is the development version, everything with
more components is not. Thus, we only need to check for a single dot.
2025-10-25 10:21:51 +02:00
renovate[bot]andWolfgang Walther e913efb8cb chore(deps): update all dependencies 2025-10-25 08:12:30 +00:00
steve-chavez 23750e644f bump version to 14.0 2025-10-24 17:39:04 -05:00
steve-chavez 4dff3550b3 docs: new two-part versioning in releases section
Also add entry to the changelog
2025-10-24 17:23:57 -05:00
steve-chavez 3085cc81ec docs: link external auth on architecture page
Partly addresses https://github.com/PostgREST/postgrest/issues/4110
2025-10-24 14:56:24 -05:00
steve-chavez 4ea983163a chore: add changelog for v13.0.8 2025-10-24 14:12:02 -05:00
steve-chavez 1d2350188c chore(changelog): put additions at the top 2025-10-24 13:19:21 -05:00
steve-chavez b20d369176 docs: elaborate on Docker tag
Also link to installation from README.md

Closes https://github.com/PostgREST/postgrest/issues/4385.
2025-10-23 18:57:41 -05:00
steve-chavez 7968b918ec nix: fix postgrest-loadtest command
Uses python for sending SIGNIT cross-platform
2025-10-23 16:05:15 -05:00
renovate[bot]andWolfgang Walther 82981a1a29 chore(deps): update actions/checkout digest to ff7abcd 2025-10-23 10:57:37 +00:00
Wolfgang Walther 41def2f0eb ci: fix vars reference 2025-10-23 12:56:28 +02:00
Taimoor ZaeemandWolfgang Walther 6aa5a09c18 ci: automate backports using backport action
Creates a workflow using https://github.com/korthout/backport-action.
This allows creating a backport PR by labeling the merged PR with a
`backport <version>` label.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-10-23 10:54:03 +00:00
Michał KłeczekandLaurence Isla 66161104bf refactor: Remove redundant VANull constructor in Auth.JWT module 2025-10-22 00:40:30 +00:00
Michał KłeczekandLaurence Isla 5a4e2e4dec refactor: Encapsulate aud config
This change is an initial step to change JWT aud configuration to regular expression.
Exporting function
audMatchesCfg :: AppConfig -> Text -> Bool
from Config module allows changing the way how JWT aud is configured to be isolated and not affect code in Auth.JWT
2025-10-22 00:40:30 +00:00
steve-chavez 0f7ac1bc39 test: schema cache load max duration
Test to ensure no regression happens after https://github.com/PostgREST/postgrest/pull/4396
2025-10-21 14:10:14 -05:00
Laurence Isla 1ada6e6f84 chore(changelog): add missing devs from changelog entries 2025-10-21 16:16:42 +00:00
Michal KleczekandGitHub be9a0ab467 perf: Index various lists in SchemaCache to change complexity from O(n*n) to O(n) (#4396) 2025-10-20 21:17:50 -05:00
Laurence Isla ea5db2a09a fix: reloading the Schema Cache unnecessarily on a PGRST002 error
When 503 errors happen if the Schema Cache is empty,
it should not retrigger the connection worker since
there's no Schema Cache loaded yet.
2025-10-20 18:42:56 +00:00
Laurence Isla c88ddfe18d fix: not logging the JSON message on a PGRST002 error
It happens right after the configuration is loaded and before
the schema cache is queried.
2025-10-20 18:42:56 +00:00
Andrei DziahelandSteve Chavez b7ad217cd3 fix: unbreaks postgrest-profiled-run
* replaces v2-build -> v2-run (which should be v2-exec)
  with one-shot v2-run
* fixes empty postgrest.prof file by sending SIGINT to the profiled
  postgrest process to trigger writing it explicitly
2025-10-17 15:43:41 -05:00
Michał KłeczekandSteve Chavez c08b87749b test: Separated query and loading internal sleep configs
To make schema cache loading wait tests robust it is necessary to provide three separate internal config variables:
* "internal-schema-cache-query-sleep" - introduces delay in schema queries execution
* "internal-schema-cache-load-sleep" - introduces delay between schema queries execution and processing their results
* "internal-schema-cache-relationship-load-sleep" - introduces delay in processing relationship query results

Thanks to these changes it is now possible to test various schema loading scenarios with the right granularity robustly (eg. make sure requests wait for schema loading but not for relationship loading).
2025-10-17 13:42:35 -05:00
Taimoor ZaeemandSteve Chavez 07681d1b5b fix: loading utf-8 config files with ascii locale set
Updates configurator-pg to version 0.2.11. This resolves #4386.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-10-14 09:05:34 -05:00
Taimoor ZaeemandWolfgang Walther 7ba5e32bcc chore: update pull request template according to commitlint
Updates pull request template to avoid inconsistencies.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-10-14 09:28:53 +00:00
renovate[bot]andWolfgang Walther 3885c643ec chore(deps): update ubuntu:noble docker digest to 66460d5 2025-10-13 10:21:47 +00:00
Taimoor ZaeemandSteve Chavez 66a8d04610 nix: setup linter to lint python files
This sets up the `ruff` linter for python code linting.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-10-11 23:38:00 -05:00
Wolfgang WaltherandSteve Chavez 8e35d543bd nix: adjust release tool to new versioning scheme
Resolves #4166
2025-10-10 18:19:33 -05:00
Jens TroegerandSteve Chavez 041d4f8ed2 test(pytest): remove unused variables and clean other minor lint to please ruff 2025-10-10 13:23:05 -05:00
renovate[bot]andWolfgang Walther 4d764500b1 chore(deps): update ubuntu:noble docker digest to 59a458b 2025-10-10 07:33:58 +00:00
Taimoor ZaeemandSteve Chavez 7b85333b88 chore: correct value of log-query in example config file
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-10-09 12:17:07 -05:00
Taimoor ZaeemandSteve Chavez af6edc575d refactor: move example config file to Config.hs
We usually miss adding or changing the example config
file when we do changes to our configurations. Moving
the example config file from CLI module to Config module
would make it less easy to miss doing the associated
changes to the example config file.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-10-09 12:17:07 -05:00
Jens TroegerandWolfgang Walther 4ce859bf46 test(pytest): remove star imports to please ruff linter 2025-10-09 10:10:40 +00:00
Wolfgang Walther 32eb6ebf19 nix: revert to build shared libs in development mode
This was disabled to reduce rebuilds for local development slightly.
However it caused random other errors, so reverting.
2025-10-09 11:59:49 +02:00
Wolfgang Walther e4bdc3bc4c chore: fix repo warning on build 2025-10-09 11:57:16 +02:00
c1d9728dc8 test(pytest): move pytest fixtures to conftest.py
There are a few benefits for this:

  - All fixtures in one module, so single source of truth.

  - The fixtures are automatically imported and injected by pytest
    so no explicit imports needed for these.

  - Linters won't complain about redefinition of outer scope objects.

Co-authored-by: Jens Troeger <jens.troeger@light-speed.de>
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-10-08 13:54:32 -05:00
steve-chavezandWolfgang Walther 322216c810 chore: remove split-sections
It's a problem for macos on x86_64 and it's no longer needed as mentioned on
https://github.com/PostgREST/postgrest/discussions/4384#discussioncomment-14614110
2025-10-08 06:29:12 +00:00
Laurence Isla 6d8a95f607 chore: fix broken logo URL in Docker Hub README 2025-10-08 02:40:58 +00:00
renovate[bot]andWolfgang Walther ea2c64da54 chore(deps): update ubuntu:noble docker digest to 728785b 2025-10-05 08:36:50 +00:00
steve-chavez 09b088d8ff change: change log-query string value to bool
BREAKING CHANGE

As discussed on https://github.com/PostgREST/postgrest/issues/3934#issuecomment-3362806777,
this changes log-query to use a bool value, this:

- Simplifies config for users.
- Reduces effort in testing the different combinations.

Closes #3934
2025-10-03 12:17:12 -05:00
steve-chavez e4458abd33 chore(changelog): adjust jwt sieve change 2025-10-03 12:17:12 -05:00
renovate[bot]andWolfgang Walther 8abc1559c7 chore(deps): update peter-evans/dockerhub-description action to v5 2025-10-02 10:03:53 +00:00
renovate[bot]andWolfgang Walther b6ac0279a5 chore(deps): update ubuntu:noble docker digest to fdb6c9c 2025-10-02 10:03:22 +00:00
renovate[bot]andWolfgang Walther 36ac916c72 chore(deps): update dependency macos to v15 2025-10-02 09:25:21 +00:00
renovate[bot]andWolfgang Walther 253453d8a6 chore(deps): update docker/login-action action to v3.6.0 2025-10-02 09:25:07 +00:00
Taimoor ZaeemandSteve Chavez a9a1763328 add: --ready flag for postgrest healthcheck
The `--ready` flag is a wrapper around the admin server
`/ready` request. This is done through using an http client
library in postgrest.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-09-28 18:01:10 -05:00
Taimoor ZaeemandSteve Chavez ab2cd766c2 docs: document config file parameter on CLI page
The CLI page had the config file parameter missing. This commit
adds that. Also adds the CLI usage output which shows all
available options. Closes #4300.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-09-26 19:26:47 -05:00
Wolfgang Walther 0478e6bbfe ci: allow manually running linkcheck workflow 2025-09-25 10:28:37 +02:00
Wolfgang Walther f2d13c242e chore: ignore failing linkcheck 2025-09-25 10:27:13 +02:00
renovate[bot]andWolfgang Walther bc01687d7c chore(deps): update actions/cache action to v4.3.0 2025-09-25 08:22:10 +00:00
renovate[bot]andWolfgang Walther f894901ad1 chore(deps): update nixbuild/nix-quick-install-action action to v34 2025-09-25 08:18:28 +00:00
Taimoor ZaeemandSteve Chavez d064faebb9 test(io): remove unused module imports
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-09-22 12:38:47 -05:00
Steve ChavezandGitHub bcf3457c28 docs: note index on automatic tsvector (#4344) 2025-09-19 13:49:30 -05:00
Steve ChavezandGitHub 9f056ac756 fix: not logging tx variables and pre-request (#4341)
Addresses the first item on https://github.com/PostgREST/postgrest/issues/3934
2025-09-19 12:16:10 -05:00
Taimoor ZaeemandWolfgang Walther 98de226dfd test(io): move reusable functions to postgrest.py
Moves the `set_statement_timeout` and `reset_statement_timeout`
function to postgrest.py. This cleans up test_io.py so it only
contains tests. Also makes these functions reusable in other
other tests like test_cli.py.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-09-18 16:51:00 +00:00
steve-chavez 4a81f6b1a6 test: refactor test_log_query
It's easier to maintain without using explicit array indexes for
matching the logs
2025-09-17 19:08:10 -05:00
Taimoor ZaeemandWolfgang Walther ef3a25a626 test(io): remove unused duplicate freeport() function
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-09-17 14:41:03 +00:00
renovate[bot]andWolfgang Walther 2b8f8c4c47 chore(deps): update ubuntu:noble docker digest to 353675e 2025-09-17 10:32:38 +00:00
renovate[bot]andWolfgang Walther 6019e4890d chore(deps): update haskell-actions/setup action to v2.8.2 2025-09-16 19:13:16 +00:00
Taimoor ZaeemandWolfgang Walther 089ffae94c chore: add changelog for v13.0.7
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-09-16 09:02:13 +00:00
renovate[bot]andWolfgang Walther 52c346f952 chore(deps): update ubuntu:noble docker digest to 590e57a 2025-09-16 09:01:55 +00:00
steve-chavez baea47bc85 refactor: make Query module pure
This splits off the transaction concerns into a MainTx module

It's just moving some functions inside the Query module to the MainTx module.
Some types names were also changed so they don't have the "Query" prefix, to avoid confusion.
2025-09-15 23:43:26 -05:00
steve-chavez 15e04903b1 fix: not logging explain query for estimated count
Fixes https://github.com/PostgREST/postgrest/issues/4319
2025-09-15 17:07:12 -05:00
steve-chavez a75ec75fff refactor: put CallReadPlan into CrudPlan 2025-09-15 17:07:12 -05:00
Taimoor ZaeemandWolfgang Walther 2d3d6256ab test(coverage): set hpctixfile to a unique name
Sets hpctixfile to be unique for every postgrest process that
is run.

Previously, this was based on the test name, but issues arise
when two postgrest processes are run under the same test, which
generates two files where one gets overwritten by the other.

Consequently, coverage data used to get lost, which is now fixed
with this commit.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-09-15 10:39:13 +00:00
steve-chavez 0acf5a30e6 refactor: remove unnecessary RSPlan constructor
This also removes some unreachable paths in Query module logic.
2025-09-11 20:51:51 -05:00
steve-chavez 4650e7299f chore(changelog): add breaking change for JWT kid 2025-09-11 14:50:21 -05:00
steve-chavez 0bb127057f docs: clarify wording on kid validation 2025-09-11 14:50:21 -05:00
Taimoor ZaeemandSteve Chavez 59ca9e7f0d test(io): move jwt secret length test to test_cli.py
Closes #4316.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-09-11 12:01:21 -05:00
steve-chavez cddfb6cf5e fix: not logging OpenAPI queries
Closes https://github.com/PostgREST/postgrest/issues/4226.

This requires moving query generation to the top App.hs module.

At this point is also simple to log the transaction variables + the
pre-request function call but this is not done here to reduce scope.
2025-09-10 09:04:12 -05:00
steve-chavez 1d2a3e8501 refactor: pass AuthResult to txVarQuery
Saves one parameter to the function.
2025-09-10 09:04:12 -05:00
steve-chavez 966a611d7f refactor: inline txVarQuery and preReqQuery
This way it's easier to see which queries are being executed.

Also use whenJust instead of traverse_.
2025-09-10 09:04:12 -05:00
Taimoor ZaeemandWolfgang Walther 19ac284f25 test(io): correct config error tests in test_cli.py
Towards fixing #4316. The config error tests were not
correctly tested. This commit allows capturing stderr
output and then checks the error messages properly.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-09-10 10:05:10 +00:00
Taimoor ZaeemandSteve Chavez d512d5c105 refactor: move escapeHostName to Network.hs
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-09-05 08:51:33 -05:00
renovate[bot]andWolfgang Walther b265f7bf7e chore(deps): update codecov/codecov-action action to v5.5.1 2025-09-04 19:25:46 +00:00
steve-chavez 1dca23e7af refactor: move accessibleFuncs to SqlFragment 2025-09-04 09:35:33 -05:00
steve-chavez 9c07070263 refactor: move accessibleTables to SqlFragment 2025-09-04 09:35:33 -05:00
steve-chavez 1ace298a1a refactor: move schemaCacheDescription to SqlFragment
This doesn't belong in the schema cache module, as
it's unused there. It's only used on Query.hs.
2025-09-04 09:35:33 -05:00
renovate[bot]andWolfgang Walther 96d2b69af4 chore(deps): update ubuntu:noble docker digest to 9cbed75 2025-09-03 13:53:55 +00:00
steve-chavez b2ce322d56 refactor: move snippet logic inside Statements.hs
Some snippet logic was unnecessarily at the Query.hs level.

This is better as we can see what inputs we need to form the main
queries.
2025-09-02 16:26:15 -05:00
steve-chavez 0844743c30 refactor: clarify Plan module and comment 2025-09-02 16:26:15 -05:00
steve-chavez 3e776213b7 refactor: move decoding to Query.hs
Towards solving https://github.com/PostgREST/postgrest/issues/3934.

This makes the Statements internal module pure now it only returns
Hasql.Snippet.
2025-09-02 16:26:15 -05:00
renovate[bot]andWolfgang Walther 00d92aa474 chore(deps): update ubuntu:noble docker digest to f3b7f1b 2025-09-02 08:17:07 +00:00
Taimoor ZaeemandWolfgang Walther 0d7986bd71 chore: add changelog for v13.0.6
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-09-02 08:16:28 +00:00
Taimoor ZaeemandSteve Chavez 41b6ebea58 refactor: deduplicate host resolution logic and observations
Replaces the "API server listening on unix socket" with simpler
"API server listening on " observation. This allows refactoring
redundant code.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-09-01 15:00:20 -05:00
Taimoor ZaeemandSteve Chavez ef9ac61d9e test(log): postgrest listening on unix socket observation
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-08-30 13:43:18 -05:00
Laurence Isla 043b1f97ee fix: empty enum in 'preferParams' openAPI parameter 2025-08-29 22:57:56 +00:00
Taimoor ZaeemandSteve Chavez 7a2840fba6 fix: format of IPv6 address logged at PostgREST startup
The IPv6 address logged at the startup like `::1:80` was
wrong because the port isn't clearly separated. This commit
corrects it, now logging as `[::1]:80`.

This is done in accordance to RFC 3986. In short, we did this
have a clear separation between the port and host because
the components of an IPv6 are separated with the ':' character.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-08-29 13:02:20 -05:00
renovate[bot]andWolfgang Walther 08a6a9d9be chore(deps): update nixbuild/nix-quick-install-action action to v33 2025-08-25 13:55:55 +00:00
Taimoor ZaeemandWolfgang Walther 09c1e2b24c chore: add changelog for v13.0.5
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-08-25 06:51:51 +00:00
Laurence Isla bf90f95443 fix: logging the Haskell type instead of the listener error message directly
Previously: Just "connection error..."
Now: connection error...
2025-08-22 19:27:38 +00:00
Taimoor ZaeemandSteve Chavez dfe5ba3863 fix(admin): metrics endpoint not responding with Content-Type header
The prometheus metrics text format requires `Content-Type` header
for correct scraping which fails otherwise. Closes #4271.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-08-20 16:13:55 -05:00
renovate[bot]andWolfgang Walther 7657607ca5 chore(deps): update codecov/codecov-action action to v5.5.0 2025-08-20 18:11:02 +00:00
steve-chavez f01d5bfeb3 docs: reorder computed relationships for clarity
Ensure the function inlining is mentioned first
2025-08-19 12:24:47 -05:00
Taimoor ZaeemandSteve Chavez 0ae1cdfc66 refactor(query): make fromJsonBodyF function more readable
The `fromJsonBodyF` function was very unreadable and making
changes were difficult. This commit improves the readability
for easier future changes and improvements.

Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-08-19 11:58:19 -05:00
steve-chavez 1dce4a9321 Revert "fix: geojson invalid query on unavailable PostGIS"
This reverts commit 0f1ca8faac.

Reverting for now as it adds one more query to the schema cache and
there's no clear way forward on how to integrate the fix with the
current schema cache queries.

See discussion on
https://github.com/PostgREST/postgrest/pull/4246#pullrequestreview-3093174224.
2025-08-13 14:54:53 -05:00
renovate[bot]andWolfgang Walther c9272b0088 chore(deps): update ubuntu:noble docker digest to 7c06e91 2025-08-13 07:19:30 +00:00
renovate[bot]andWolfgang Walther 1cd8934bb7 chore(deps): update actions/checkout action to v5 2025-08-11 15:15:57 +00:00
renovate[bot]andWolfgang Walther b915968f30 chore(deps): update actions/cache action to v4.2.4 2025-08-07 14:24:41 +00:00
steve-chavez 0f1ca8faac fix: geojson invalid query on unavailable PostGIS
Closes https://github.com/PostgREST/postgrest/issues/4245.

It adds a query on schema cache construction, but doesn't add a new
attribute to the SchemaCache type. We only need the information to be
passed to the `initialMediaHandlers` function to build the builtin media
handlers.
2025-08-06 10:20:01 -05:00
renovate[bot]andWolfgang Walther 7870add603 chore(deps): update actions/download-artifact action to v5 2025-08-06 07:45:01 +00:00
renovate[bot]andWolfgang Walther a2da255931 chore(deps): update docker/login-action action to v3.5.0 2025-08-04 17:05:52 +00:00
renovate[bot]andWolfgang Walther 26bc13b57f chore(deps): update haskell-actions/setup action to v2.8.1 2025-08-03 16:27:13 +00:00
steve-chavez fa09e4af0d docs: add redirects for broken hashes on auth.rst 2025-07-30 05:28:00 -05:00
steve-chavez fd127f449c docs: correct order of jwt claims sections 2025-07-30 05:14:43 -05:00
steve-chavez ad25479185 chore: put bounded jwt cache as addition 2025-07-30 04:53:22 -05:00
Taimoor ZaeemandSteve Chavez 6a37e1c37b chore: correct recent change log entry
Signed-off-by: Taimoor Zaeem <taimoorzaeem@gmail.com>
2025-07-30 02:46:01 -05:00
Michal KleczekandGitHub 77ff11de95 feat: JWT cache implementation based on sieve algorithm (#4084)
Changes:

1. Refactoring and some cleanup of JWT handling code:
* Instead of caching AuthResult cache decoded claims (which signature was verified). Validating claims and determining role is done after cache lookup
* Cleaned up API so that usage of it is simplified: lookupJwtCache cache key >>= parseClaims configJwtAud time
* Handling of JwtCacheState initialization and updates of configuration is encapsulated in Auth.JwtCache module

2. Generic high performance (hopefully) scalable, dynamically resizeable cache implementation based on stm, stm-hamt and sieve algorithm. It also integrates with PostgREST measurements infrastructure providing usage stats (ie. hit ratio, evictions count)
2025-07-29 18:51:41 -05:00
steve-chavez ac155a9391 docs: clarify auth page
Reorganizes the auth page and clarifies some links
2025-07-28 15:43:38 -05:00
Taimoor ZaeemandSteve Chavez 96571aa0f4 refactor: move request body to a Payload.hs module 2025-07-26 08:50:56 +02:00
steve-chavez aa58e37f03 refactor: add PreQuery module
Small step towards solving #3934.

Adds a module that builds SQL snippets for the transaction variables
and the pre-request function.
2025-07-23 10:06:32 +02:00
Taimoor ZaeemandGitHub 5e0da40ac4 chore: update README.md and CONTRIBUTING.md (#4218)
- Moved the CONTRIBUTING.md file to project root.
- Fixed broken nightly release link.
- Update README.md
2025-07-22 13:50:00 +00:00
renovate[bot]andWolfgang Walther 2b065553a0 chore(config): migrate config .github/renovate.json 2025-07-21 20:04:54 +00:00
renovate[bot]andWolfgang Walther cd04360a43 chore(deps): update ubuntu:noble docker digest to a08e551 2025-07-19 12:16:42 +00:00
Taimoor ZaeemandWolfgang Walther 96bbbe35e1 test: adjust jwt claims error test to avoid failure
The JWT claims IO test fails too often. This breaks our
workflows. This commit adjusts the edge cases in test to
prevent flakiness.
2025-07-19 12:16:29 +00:00
steve-chavez 161f37f1f1 test: remove jwt decoding from mixed loadtest
We now have dedicated JWT loadtests, so it's no longer necessary
to conflate JWT here.
2025-07-18 07:25:53 -05:00
Wolfgang WaltherandSteve Chavez 25e56f32e4 nix: avoid updating cabal on every run
Resolves #4206
2025-07-17 11:47:44 -05:00
Laurence Isla 66caaffbe3 chore: update sponsor 2025-07-16 19:54:53 -05:00
steve-chavez 2d4967c4b4 nix: reduce loadtest result verbosity 2025-07-16 17:38:33 -05:00
Wolfgang Walther d013a1fd77 docs: adjust some redirects
Those were reported in the weekly linkcheck.
2025-07-16 11:03:50 +02:00
renovate[bot]andWolfgang Walther b91e241b9a chore(deps): update ubuntu:noble docker digest to c4570d2 2025-07-16 09:00:23 +00:00
renovate[bot]andWolfgang Walther 84564b9e3e chore(deps): update ubuntu:noble docker digest to a328b84 2025-07-16 08:51:25 +00:00
Taimoor ZaeemandSteve Chavez d4caa0f5f5 fix: empty spread embeddings return unexpected SQL error
Fixes the SQL error from postgres when an empty spread embeddings
like `...table()` is requested.
2025-07-13 14:16:27 -05:00
Wolfgang Walther d31c873cd5 chore(changelog): add removal of PostgreSQL 12 2025-07-11 19:21:40 +02:00
Wolfgang Walther f32a8861c5 nix: add system argument to postgrest-push-cachix
This allows building aarch64-darwin derivations on a remote builder and
then pushing them to cachix before triggering CI.
2025-07-11 15:53:06 +00:00
Wolfgang Walther 18932b7a4a nix: use upstream GHC for static build
This way we benefit from NixOS' binary cache to deliver GHC for us and
don't need to cache it ourselves.

This will become relevant once we do that for more platforms.
2025-07-11 15:53:06 +00:00
Wolfgang Walther 067c181bb8 nix: remove libpq overlay
We have fixed the static build of PostgreSQL upstream, so we don't need
our separate overlay anymore. One more step towards building a static
executable on other platforms.
2025-07-11 15:53:06 +00:00
Wolfgang Walther 83c533e557 nix: refactor derivation tests for static package
Instead of rolling our own, we can use some tooling from nix / nixpkgs.

We drop the "statically linked" check, because our goal is to compile
mostly-static executables to darwin, too. However, those will never be
fully static, because they always link to the platform's libc.
2025-07-11 15:53:06 +00:00
Wolfgang Walther 55eafdf352 chore(deps): update nixpkgs to 25.05 2025-07-11 15:53:06 +00:00
Wolfgang Walther 70f449654f nix: fix some darwin sandbox issues 2025-07-11 15:53:06 +00:00
Wolfgang Walther bd61cf9b60 nix: re-introduce postgrest-nixpkgs-upgrade
Resolves #4161
2025-07-11 15:53:06 +00:00
Wolfgang Walther 6bf2cb09a6 remove: drop support for PostgreSQL 12
This has been EOL since November and has been dropped from nixpkgs.
2025-07-11 15:53:06 +00:00
renovate[bot]andWolfgang Walther fbb243ee0c chore(deps): update docker/setup-buildx-action action to v3.11.1 2025-07-08 20:06:24 +00:00
renovate[bot]andWolfgang Walther 7ce2b3c4e9 chore(deps): update ubuntu:noble docker digest to 440dcf6 2025-07-08 20:05:31 +00:00
Wolfgang Walther 7e2f06d217 chore: bump some hackage dependencies
All of these were tested via stackage 23.27 which required allow-newer
for them.
2025-07-08 20:01:40 +00:00
Wolfgang Walther cedb8d9cd9 chore: fix stack's nix integration
The pkgconfig package has been renamed to pkg-config in... February
2019. So this has not been working for quite some time.
2025-07-08 20:28:47 +02:00
Wolfgang Walther 39e2a154f0 chore: remove left-over comment for Ubuntu (arm)
We have been building with GHC 9.6 on that platform for a while.
2025-07-08 17:55:24 +00:00
Wolfgang Walther a4ab4ce2f3 chore: build with GHC 9.8.4 for cabal 2025-07-08 17:55:24 +00:00
Wolfgang Walther cfabbe67b8 chore: update hackage index-state 2025-07-08 17:55:24 +00:00
Wolfgang Walther a91a222613 chore: stackage 22.41 -> 22.44
Updates stackage to 22.44, which is also supported on FreeBSD, where GHC
9.6.7 is available again.
2025-07-08 17:55:24 +00:00
steve-chavez a183dea9e4 docs: clarify health checks empty response body 2025-07-08 12:45:15 -05:00
Wolfgang Walther b28df988ef chore: remove Paths_postgrest module
The dependency on it was removed in #3608 already, but we forgot to
remove it from postgrest.cabal, which caused it to still be built.

We didn't realize because all references of it were stripped away by
dead code elimination anyway.
2025-07-08 16:36:42 +00:00
Joel JakobssonandGitHub 01c23a8124 fix(openapi): respect function volatility for GET methods
The OpenAPI specification was incorrectly exposing GET methods for
VOLATILE functions, even though such functions properly reject GET
requests at runtime with "405 Method Not Allowed".  This created
a mismatch between the advertised API specification and the actual
runtime behavior.

VOLATILE functions should only be callable via POST since they may
have side effects, while STABLE and IMMUTABLE functions can safely
be called via GET since they don't modify database state.

Fix by checking the pdVolatility field in makeProcPathItem() and
only including GET methods in the OpenAPI PathItem for non-volatile
functions.

The runtime behavior was already correct; this fixes only the
OpenAPI documentation generation.
2025-07-07 15:26:51 +00:00
steve-chavez e3f8a95b72 nix: complete jwt loadtests
Completes the jwt loadtests, now we have non-cached, cached + worst case
for the cache.

- jwt-hs (edited): now has the cache disabled
- jwt-hs-cache: cache enabled for jwt-hs
- jwt-hs-cache-worst: worst case of the jwt-hs cache
- jwt-rsa (edited): now has the cache disabled
- jwt-rsa-cache: cache enabled for jwt-rsa
- jwt-rsa-cache-worst: worst case of the jwt-rsa cache

Also deletes `nix/tools/generate_targets_rsa.py` and uses a single
python script.

Should prove what's mentioned on
https://github.com/PostgREST/postgrest/pull/4084#issuecomment-2998170423
2025-07-06 19:14:17 -05:00
steve-chavez a87e31b767 nix: shorten loadtest script 2025-07-06 19:14:17 -05:00
Taimoor ZaeemandSteve Chavez 4daa4533d3 docs: horizontal filtering on table-valued functions 2025-07-02 14:28:49 -05:00
Taimoor ZaeemandSteve Chavez e37385c893 nix: run hlint on multiple threads 2025-06-30 12:14:06 -05:00
Taimoor ZaeemandGitHub 4a0eb7febe fix: OpenAPI broken docs link (#4169) 2025-06-27 15:14:34 -05:00
Taimoor ZaeemandSteve Chavez f295130544 chore: update postgrest scripts in nix README.md
Adds `postgrest-commitlint` and removes `postgrest-nixpkgs-upgrade`.
2025-06-26 12:16:49 -05:00
Wolfgang Walther 9d5968a45a 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 09:44:08 +00:00
Wolfgang Walther 9519278c5e nix: remove mem_pct from loadtest monitor 2025-06-25 15:05:33 +00:00
Wolfgang Walther a91ce4a159 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 06:34:09 +00:00
Wolfgang WaltherandLaurence Isla 969e4afb26 nix: provide shell completions via flake packages 2025-06-24 01:44:44 +00:00
Wolfgang WaltherandLaurence Isla de25b26f3e nix: reduce closure size of default package in flake.nix
Splitting the executable in a separate output avoids distributing all of
the libraries and documentation, which are not needed when just running
PostgREST.

Reduces closure size from 4.3G to 73.9M for the flake exported packages.

Resolves #4149
2025-06-24 01:44:44 +00:00
steve-chavez 67bd3526c2 nix: use json file for generated jwk on loadtest
Having the JWK as .http file was confusing.

Also split genTargets into two to avoid confusion.
2025-06-20 11:27:44 -05:00
Taimoor ZaeemandSteve Chavez c7e5db34e4 chore: add changelog for v13.0.3 and v13.0.4 2025-06-20 09:45:04 -05:00
steve-chavez c8adfebb1f nix: fix postgrest-loadtest-to-markdown on NAs
The python script failed when presenting NAs with:

TypeError: boolean value of NA is ambiguous
2025-06-19 16:48:44 -05:00
steve-chavez 49de3c1ab8 nix: adjacent process monitoring report columns 2025-06-19 16:36:25 -05:00
steve-chavez e1c71c92d6 nix: add traces to loadtest commands 2025-06-19 15:04:16 -05:00
Taimoor ZaeemandWolfgang Walther 330df05891 ci: only run commitlint on github PRs
The `commitlint` script disrupts our release workflow if failed
on push event. We also don't need it on push because once pushed,
git commit history can't be amended.
2025-06-18 17:03:04 +00:00
Taimoor ZaeemandSteve Chavez 63a492291a nix: allow mixed casing in commitlint subject message 2025-06-17 18:10:37 -05:00
Taimoor ZaeemandSteve Chavez 0b69ad0e65 ci: fix commitlint CI failure 2025-06-17 18:10:37 -05:00
steve-chavez bc99ec8285 nix: add group to postgrest-loadtest-report
This is so the results are easier to skim
2025-06-17 17:58:49 -05:00
steve-chavez 7dbb8cd987 nix: better monitoring loadtest report readability 2025-06-17 16:38:06 -05:00
Taimoor ZaeemandGitHub 38332d9462 nix: add commitlint to lint commit messages (#4128)
Adds a new script `postgrest-commitlint` to lint commit messages.
2025-06-17 16:15:14 -05:00
renovate[bot]andWolfgang Walther cba7fc950b chore(deps): update ubuntu:noble docker digest to b59d215 2025-06-17 20:37:37 +00:00
renovate[bot]andWolfgang Walther 143d23f1b8 chore(deps): update docker/setup-buildx-action action to v3.11.0 2025-06-17 20:36:39 +00:00
steve-chavez 47763df590 nix: add process monitoring for loadtests
Closes https://github.com/PostgREST/postgrest/issues/4107.

Adds two python scripts:

- monitor_pid.py: monitors the postgrest process each second
  until it exits, then outputs a csv with the results. The nix wrappers
  use the `loadtest` dir for the output.
- merge_monitor_result.py: receives a list of csvs and merges them into
  a single markdown table. The nix wrappers use the `loadtest/*.csv`
  files for the input.

The nix `postgrest-with-pgrst` and `postgrest-loadtest-report` commands
use these scripts to add monitoring for `postgrest-loadtest` and
`postgrest-loadtest-against`.
2025-06-17 00:10:58 -05:00
steve-chavez 53604c9db2 nix: increase jwt-hs loadtest TOTAL_TARGETS
This is so the process monitoring results are more meaningful.

* remove the amount of total targets from the loadtest kind name
  since the targets can change.

* clarifies the top comment on the generate_targets python script

* increase TOTAL_TARGETS on jwt-hs loadtest
2025-06-17 00:10:58 -05:00
steve-chavez ba7aaa8ab1 nix: dedup vegeta command in nix loadtest 2025-06-17 00:10:58 -05:00
Taimoor ZaeemandGitHub c318d46469 fix: jwt-aud config not failing when set to invalid URI (#4140)
The `jwt-aud` config was not validated when containing ':'
character according to RFC 3986. This fix validates it and
fails at startup if it is invalid.
2025-06-16 15:40:47 -05:00
Laurence Isla a2892ab1dd fix: regression that makes fts not work on domain types based on tsvector 2025-06-16 16:53:29 +00:00
Laurence Isla 1879c813c8 fix: detect the correct base type of recursive domains in schema cache for tables and views
In OpenAPI it shows the correct base type in properties' definitions (including enums).
2025-06-16 16:53:29 +00:00
Taimoor ZaeemandGitHub 2eb9e4a8a4 test: upserts with case sensitive table and sequence name (#4131)
Closes #3712.
2025-06-07 12:39:50 -05:00
Laurence Isla a9d4237bc2 chore: update UTM tracking for Euronodes 2025-06-06 12:55:55 -05:00
Laurence Isla f85113669d chore: update sponsor 2025-06-06 14:53:20 +00:00
Michal KleczekandGitHub 69072116be test: Use RSA 4096 algorithm for JWT load test (#4118)
Until now we had a load test with 50k unique JWTs signed with symmetric key.
This commit adds a new load test with 10k JWTs signed with RSA 4096.

Existing -k jwt parameter was changed to -k jwt-hs-50k.
New test is run with -k jwt-rsa-10k parameter.

Additionally a new parameter --jwtcache=off was added to turn off JWT caching in the above load tests.
2025-06-05 07:44:05 -05:00
steve-chavez b2f8786821 nix: detect unused python code
Now `postgrest-lint` shows:

```
Linting workflows...
Scanning nix files for unused code...
Scanning python files for unused code...
nix/tools/generate_targets.py:13: unused variable 'JWT_DURATION' (60% confidence)
test/io/test_cli.py:6: unused import 'repeat' (90% confidence)
```

Also corrected the above detected files
2025-06-03 20:47:39 -05:00
Taimoor ZaeemandSteve Chavez dc46aea15e fix: max-affected preference not failing for rpc with strict handling 2025-06-03 14:31:17 -05:00
Laurence Isla e33ce843dd chore: add changelog entry for v13.0.2 2025-06-02 18:09:29 -05:00
Laurence Isla f7fa717411 fix: regression that makes order by with nulls order not work alongside limits 2025-06-02 17:44:57 +00:00
Michał KłeczekandSteve Chavez a409a2cb94 Extracted JWT validation functions to a separate module.
This change introduces a PostgREST.Auth.Jwt module containing JWT validation functions.

The reason to extract them from Auth is to enable JwtCache module to reuse them without introducing module dependency cycle.
2025-06-02 12:26:39 -05:00
Michał KłeczekandSteve Chavez 7e3fb2ba08 Define MetricsState as record and use Applicative to initialize one 2025-06-02 11:49:01 -05:00
renovate[bot]andWolfgang Walther 9084d8c358 chore(deps): update codecov/codecov-action action to v5.4.3 2025-06-02 06:15:33 +00:00
Taimoor ZaeemandWolfgang Walther 4ae5db57dc chore: add changelog entry for v13.0.1 2025-06-02 06:14:50 +00:00
Taimoor ZaeemandSteve Chavez c5ccf742d6 feat: improve error details of PGRST301 error 2025-05-31 13:30:32 -05:00
Thilo HohltandSteve Chavez f6316b86e4 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 06:42:38 -05:00
Taimoor ZaeemandSteve Chavez aebdc716c7 refactor: move jwt error messages to Error.hs module 2025-05-31 06:41:22 -05:00
steve-chavez 72f6036432 docs: external authentication page in explanations
- Move page from External JWT generation
2025-05-30 17:59:51 -05:00
steve-chavez afc989403d docs: move jwt using SSL to tutorial 1
Removes jwt.io example in favor of a bash script
2025-05-30 17:59:51 -05:00
Taimoor ZaeemandGitHub ea9d4f49b2 fix: log db-schemas and db-extra-search-path in schema cache load error (#4108) 2025-05-30 10:13:19 -05:00
Laurence Isla 9c40a9c6df docs: JWK kid parameter validation 2025-05-29 21:01:39 +00:00
Taimoor ZaeemandSteve Chavez 000cb87ec1 fix: allow db-extra-search-path to accept empty value 2025-05-29 14:25:28 -05:00
steve-chavez 054ee463a1 docs: remove Greenplum integration
They're not really a sponsor, so it's not fair to include them.
2025-05-28 17:43:16 -05:00
Wolfgang Walther 07d9747c40 docs: fix functions link in api/preferences
External link syntax was used instead of internal reference.
2025-05-28 09:09:13 +02:00
Wolfgang Walther 2d5d5ed704 docs: apply redirects
Those redirect, so we might as well hardcode the them.
2025-05-28 09:09:13 +02:00
Wolfgang Walther 52d2bd93f5 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:09:10 +02:00
steve-chavez da467fe3b6 nix: add PGRST_ADMIN_SERVER_PORT to postgrest-run
It's frequently needed during manual testing.
2025-05-27 20:12:00 -05:00
steve-chavez 52cf4658c5 docs: update architecture HTTP link
It links directly to warp page, which is confusing. Link to the
same page reference instead, which finally links to warp.
2025-05-26 13:44:20 -05:00
Taimoor ZaeemandSteve Chavez 94d6ac2f2e docs: explain use of max-affected preference with rpc 2025-05-22 12:23:52 -05:00
Taimoor ZaeemandSteve Chavez 5f9e0a2739 test: add missing tests for max-affected preference with rpc 2025-05-22 12:23:52 -05:00
Wolfgang Walther 7933685ffb docs: Update sphinx-rtd-theme to 3.0.2 2025-05-22 06:07:26 +00:00
Taimoor ZaeemandSteve Chavez 1258ea663c docs: add note in JWT Role Extraction section
Add a note describing that the used JSPath DSL does not
strictly follow the JSONPath as described in RFC 9535
2025-05-18 13:36:43 -05:00
steve-chavez 520631913f docs: add warning for duplicate keys in spread 2025-05-15 17:42:13 -05:00
steve-chavez 10e31c4e8c nix: clarify output of loadtest jwt 2025-05-15 16:38:59 -05:00
steve-chavez a659b0005a docs: clarify spread join table 2025-05-15 15:50:54 -05:00
steve-chavez 23f11e3abb docs: clarify fts 2025-05-15 11:57:08 -05:00
steve-chavez 211e27a4d7 docs: clarify spread feature 2025-05-15 11:14:06 -05:00
Laurence Isla 0b3c8c98d8 feat: improve error response when the requested schema is invalid
- It now shows the invalid schema in the "message"
- The exposed schemas are now listed in the "hint" instead of the "message"
2025-05-15 02:21:00 +00:00
renovate[bot]andWolfgang Walther 605d82285d chore(deps): update haskell-actions/setup action to v2.8.0 2025-05-14 19:41:02 +00:00
Taimoor ZaeemandWolfgang Walther 07f51f323e refactor: remove unused optValue function from Config.hs module 2025-05-14 17:41:57 +00:00
Taimoor ZaeemandSteve Chavez 8b63ea82ec test: add test for duplicate entries in pg_description with same OID 2025-05-14 09:31:48 -05:00
Taimoor ZaeemandGitHub 8390df0fa5 fix: jwt error return status 400 for invalid role (#4081) 2025-05-13 15:09:25 -05:00
Taimoor ZaeemandLaurence Isla 1609e32c3a test: add test when using .aud in jwt-role-claim-key 2025-05-10 16:19:22 +00:00
Laurence Isla c5cba11ecb docs: fix link to SQL Query Logs 2025-05-09 20:59:51 -05:00
steve-chavez 257a7d1488 docs: jwt role extraction link to config
The feature section was missing a link to its config.

Also shorten the section name.
2025-05-09 20:45:37 -05:00
Wolfgang Walther bcf66253bd chore: adjust changelog for db-extra-search-path
Came up in #4073
2025-05-09 14:56:53 +02:00
Wolfgang Walther 451676c5be bump version to 13.1 2025-05-08 21:48:51 +02:00
199 changed files with 7120 additions and 3660 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
freebsd_instance: freebsd_instance:
image_family: freebsd-14-2 image_family: freebsd-14-3
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-yaml stack-21.7.yaml stack build -j 1 --local-bin-path . --copy-bins
strip postgrest strip postgrest
bin_artifacts: bin_artifacts:
+12 -11
View File
@@ -4,17 +4,18 @@ When submitting a new feature or fix:
- Add a new entry to the CHANGELOG - https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md#unreleased - Add a new entry to the CHANGELOG - https://github.com/PostgREST/postgrest/blob/main/CHANGELOG.md#unreleased
- If relevant, update the docs - If relevant, update the docs
- Use a prefix for the PR title or commits, e.g. "fix: description of the fix". - Use a prefix for the PR title or commits, e.g. "fix: description of the fix".
+ `fix`, bug fixes + `add`, Add a new feature
+ `feat`, new features added + `amend`, To amend an unrealease commit
+ `perf`, performance improvements + `change`, Breaking changes
+ `docs`, updating the documentation + `chore`, Maintenance, update sponsors, changelog, readme etc
+ `nix`, related to the Nix development environment + `ci`, CI configuration files and scripts
+ `ci`, related to the Continuous Integration modules + `docs`, Documentation
+ `test`, related to the testing modules + `fix`, Bug fix
+ `refactor`, refactoring code + `nix`, Related to Nix
+ `deprecate`, deprecating a feature + `perf`, Performance improvements
+ `changelog`, updating the CHANGELOG + `refactor`, Refactoring code
+ `chore`, maintenance (build process, updating sponsors, etc.) + `remove`, Remove a feature or fix
+ `test`, Adding tests
+ Other prefixes may be used if necessary + Other prefixes may be used if necessary
- If there's a breaking change, add `BREAKING CHANGE` and an explanation to your commit message - If there's a breaking change, add `BREAKING CHANGE` and an explanation to your commit message
--> -->
@@ -112,7 +112,7 @@ runs:
echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT" echo "artifacts=${artifacts}" >> "$GITHUB_OUTPUT"
- name: Save artifact to GitHub Actions - name: Save artifact to GitHub Actions
if: steps.find-task.outputs.task_found if: steps.find-task.outputs.task_found
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: ${{ inputs.upload }} name: ${{ inputs.upload }}
path: ${{ steps.download.outputs.artifacts }} path: ${{ steps.download.outputs.artifacts }}
+2 -2
View File
@@ -19,14 +19,14 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }} if: ${{ startsWith(github.ref, 'refs/heads/') || (inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
with: with:
path: ${{ inputs.path }} path: ${{ inputs.path }}
key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }} key: ${{ runner.os }}-${{ inputs.prefix }}-${{ inputs.suffix }}
restore-keys: | restore-keys: |
${{ runner.os }}-${{ inputs.prefix }}- ${{ runner.os }}-${{ inputs.prefix }}-
- uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 - uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }} if: ${{ !startsWith(github.ref, 'refs/heads/') && !(inputs.save-prs && startsWith(github.ref, 'refs/pull/')) }}
with: with:
path: ${{ inputs.path }} path: ${{ inputs.path }}
+2 -2
View File
@@ -11,12 +11,12 @@ inputs:
runs: runs:
using: composite using: composite
steps: steps:
- uses: nixbuild/nix-quick-install-action@5bb6a3b3abe66fd09bbf250dce8ada94f856a703 # v30 - uses: nixbuild/nix-quick-install-action@2c9db80fb984ceb1bcaa77cdda3fdf8cfba92035 # v34
with: with:
nix_conf: |- nix_conf: |-
always-allow-substitutes = true always-allow-substitutes = true
max-jobs = auto max-jobs = auto
- uses: cachix/cachix-action@0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad # v16 - uses: cachix/cachix-action@1eb2ef646ac0255473d23a5907ad7b04ce94065c # v17
with: with:
name: postgrest name: postgrest
authToken: ${{ inputs.authToken }} authToken: ${{ inputs.authToken }}
+39 -10
View File
@@ -3,7 +3,7 @@
"extends": [ "extends": [
"config:best-practices" "config:best-practices"
], ],
"baseBranches": [ "baseBranchPatterns": [
"main", "main",
"/^v[0-9]+/" "/^v[0-9]+/"
], ],
@@ -13,27 +13,56 @@
}, },
"packageRules": [ "packageRules": [
{ {
"matchBaseBranches": [ "/^v[0-9]+/" ], "matchBaseBranches": [
"matchManagers": ["haskell-cabal"], "/^v[0-9]+/"
],
"matchManagers": [
"haskell-cabal"
],
"enabled": false "enabled": false
}, },
{ {
"matchBaseBranches": [ "/^v[0-9]+/" ], "matchBaseBranches": [
"/^v[0-9]+/"
],
"groupName": "all dependencies" "groupName": "all dependencies"
}, },
{ {
"matchManagers": ["haskell-cabal"], "matchManagers": [
"matchPackageNames": ["base", "bytestring", "containers", "directory", "mtl", "parsec", "process", "text"], "haskell-cabal"
],
"matchPackageNames": [
"base",
"bytestring",
"containers",
"directory",
"mtl",
"parsec",
"process",
"text"
],
"groupName": "GHC dependencies" "groupName": "GHC dependencies"
}, },
{ {
"matchManagers": ["haskell-cabal"], "matchManagers": [
"matchPackageNames": ["hasql", "hasql-dynamic-statements", "hasql-notifications", "hasql-transaction", "hasql-pool"], "haskell-cabal"
],
"matchPackageNames": [
"hasql",
"hasql-dynamic-statements",
"hasql-notifications",
"hasql-transaction",
"hasql-pool"
],
"groupName": "hasql" "groupName": "hasql"
}, },
{ {
"matchManagers": ["haskell-cabal"], "matchManagers": [
"matchPackageNames": ["fuzzyset"], "haskell-cabal"
],
"matchPackageNames": [
"fuzzyset"
],
"allowedVersions": "<0.3" "allowedVersions": "<0.3"
} }
] ]
+52
View File
@@ -0,0 +1,52 @@
name: Backport
on:
pull_request_target:
types:
- closed
- labeled
jobs:
backport:
name: Backport
runs-on: ubuntu-24.04
# It triggers only when PR is already merged on either:
#
# - The merge event itself (action != labeled) or
# - A label event with the right label (backport ...).
#
# The result will be that we can add the label before or after merge,
# but the workflow will only run once the PR had been merged.
if: >
github.event.pull_request.merged &&
(
github.event.action != 'labeled' ||
startsWith(github.event.label.name, 'backport')
)
steps:
# This actions creates the github token using the postgrest app secrets
- name: Create Github App Token
id: app-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ vars.POSTGREST_CI_APP_ID }}
private-key: ${{ secrets.POSTGREST_CI_PRIVATE_KEY }}
permission-contents: write
permission-pull-requests: write
permission-workflows: write # required when backporting CI changes
# This is required for backport action to cherry-pick the PR
- name: Fetch PR ref
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.sha }}
token: ${{ steps.app-token.outputs.token }}
# Backport action that creates the PR with given settings
- name: Create backport PR
uses: korthout/backport-action@3c06f323a58619da1e8522229ebc8d5de2633e46 # v4.3.0
with:
github_token: ${{ steps.app-token.outputs.token }}
pull_description: 'Backport for #${pull_number}.'
pull_title: '${target_branch}: ${pull_title}'
+14 -23
View File
@@ -33,16 +33,16 @@ jobs:
name: Nix - Linux x86-64 static name: Nix - Linux x86-64 static
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
- name: Build static executable - name: Build static executable
run: nix-build -A postgrestStatic run: nix-build -A postgrestStatic -A postgrestStatic.tests
- name: Save built executable as artifact - name: Save built executable as artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: postgrest-linux-static-x86-64 name: postgrest-linux-static-x86-64
path: result/bin/postgrest path: result/bin/postgrest
@@ -51,7 +51,7 @@ jobs:
- name: Build Docker image - name: Build Docker image
run: nix-build -A docker.image --out-link postgrest-docker.tar.gz run: nix-build -A docker.image --out-link postgrest-docker.tar.gz
- name: Save built Docker image as artifact - name: Save built Docker image as artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: postgrest-docker-x86-64 name: postgrest-docker-x86-64
path: postgrest-docker.tar.gz path: postgrest-docker.tar.gz
@@ -60,9 +60,9 @@ jobs:
macos: macos:
name: Nix - MacOS name: Nix - MacOS
runs-on: macos-14 runs-on: macos-15
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
@@ -105,15 +105,6 @@ jobs:
artifact: postgrest-macos-aarch64 artifact: postgrest-macos-aarch64
deps: brew link --force libpq deps: brew link --force libpq
- name: MacOS x86-64
runs-on: macos-13
cache: |
~/.stack/pantry
~/.stack/snapshots
~/.stack/stack.sqlite3
artifact: postgrest-macos-x86-64
deps: brew link --force libpq
- name: Windows - name: Windows
runs-on: windows-2022 runs-on: windows-2022
cache: | cache: |
@@ -126,11 +117,11 @@ jobs:
name: Stack - ${{ matrix.name }} name: Stack - ${{ matrix.name }}
runs-on: ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }}
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: haskell-actions/setup@64445b6b5dd545faf5f8e2acee8253eb5c2b29aa # v2.7.11 - uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
with: with:
# This must match the version in stack.yaml's resolver # This must match the version in stack.yaml's resolver
ghc-version: 9.6.6 ghc-version: 9.6.7
enable-stack: true enable-stack: true
stack-no-global: true stack-no-global: true
stack-setup-ghc: true stack-setup-ghc: true
@@ -155,7 +146,7 @@ jobs:
- name: Strip Executable - name: Strip Executable
run: strip result/postgrest* run: strip result/postgrest*
- name: Save built executable as artifact - name: Save built executable as artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: ${{ matrix.artifact }} name: ${{ matrix.artifact }}
path: | path: |
@@ -168,7 +159,7 @@ jobs:
name: Stack - FreeBSD from CirrusCI name: Stack - FreeBSD from CirrusCI
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: ./.github/actions/artifact-from-cirrus - uses: ./.github/actions/artifact-from-cirrus
with: with:
token: ${{ github.token }} token: ${{ github.token }}
@@ -180,13 +171,13 @@ jobs:
cabal: cabal:
strategy: strategy:
matrix: matrix:
ghc: ['9.6.6', '9.8.2'] ghc: ['9.6.7', '9.8.4']
fail-fast: false fail-fast: false
name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }} name: Cabal - Linux x86-64 - GHC ${{ matrix.ghc }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: haskell-actions/setup@64445b6b5dd545faf5f8e2acee8253eb5c2b29aa # v2.7.11 - uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0
with: with:
ghc-version: ${{ matrix.ghc }} ghc-version: ${{ matrix.ghc }}
- name: Cache .cabal - name: Cache .cabal
+22 -1
View File
@@ -20,7 +20,7 @@ jobs:
name: Lint & Style name: Lint & Style
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
@@ -30,3 +30,24 @@ jobs:
run: postgrest-lint run: postgrest-lint
- name: Run style check (auto-format with `nix-shell --run postgrest-style`) - name: Run style check (auto-format with `nix-shell --run postgrest-style`)
run: postgrest-style-check run: postgrest-style-check
commit:
if: github.event_name != 'push' # we don't run this on a push, a failure on push disrupts the release workflow
name: Commit
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 100 # fetch history (last 100 commits) instead of default shallow clone history, this is deemed enough for a PR history
- name: Setup Nix Environment
uses: ./.github/actions/setup-nix
with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: gitTools.commitCheck.bin
- name: Run commitlint (check locally with `nix-shell --run postgrest-commitlint`)
run: |
# Fetch target branch explicitly
git fetch origin ${{ github.base_ref }}
# Run commitlint
postgrest-commitlint --from origin/${{ github.base_ref }} --to HEAD
+2 -2
View File
@@ -50,14 +50,14 @@ jobs:
- test - test
- build - build
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
ssh-key: ${{ secrets.POSTGREST_SSH_KEY }} ssh-key: ${{ secrets.POSTGREST_SSH_KEY }}
- name: Tag latest commit - name: Tag latest commit
run: | run: |
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)" cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
if [[ "$cabal_version" == *.*.* ]]; then if [[ "$cabal_version" == *.* ]]; then
git fetch --tags git fetch --tags
if [ -z "$(git tag --list "v$cabal_version")" ]; then if [ -z "$(git tag --list "v$cabal_version")" ]; then
+2 -2
View File
@@ -27,7 +27,7 @@ jobs:
name: Build name: Build
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
@@ -41,7 +41,7 @@ jobs:
name: Spellcheck name: Spellcheck
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
+2 -1
View File
@@ -3,12 +3,13 @@ name: Linkcheck
on: on:
schedule: schedule:
- cron: '1 2 * * 3' - cron: '1 2 * * 3'
workflow_dispatch:
jobs: jobs:
linkcheck: linkcheck:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
+12 -15
View File
@@ -26,7 +26,7 @@ jobs:
needs: needs:
- build - build
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Check the version to be released - name: Check the version to be released
run: | run: |
cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)" cabal_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
@@ -49,7 +49,7 @@ jobs:
echo "Relevant extract from CHANGELOG.md:" echo "Relevant extract from CHANGELOG.md:"
cat CHANGES.md cat CHANGES.md
- name: Save CHANGES.md as artifact - name: Save CHANGES.md as artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: release-changes name: release-changes
path: CHANGES.md path: CHANGES.md
@@ -64,9 +64,9 @@ jobs:
needs: needs:
- prepare - prepare
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download all artifacts - name: Download all artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with: with:
path: artifacts path: artifacts
- name: Create release bundle with archives for all builds - name: Create release bundle with archives for all builds
@@ -81,9 +81,6 @@ jobs:
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-aarch64.tar.xz" \ tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-aarch64.tar.xz" \
-C artifacts/postgrest-macos-aarch64 postgrest -C artifacts/postgrest-macos-aarch64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-macos-x86-64.tar.xz" \
-C artifacts/postgrest-macos-x86-64 postgrest
tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \ tar cJvf "release-bundle/postgrest-${GITHUB_REF_NAME}-freebsd-x86-64.tar.xz" \
-C artifacts/postgrest-freebsd-x86-64 postgrest -C artifacts/postgrest-freebsd-x86-64 postgrest
@@ -94,7 +91,7 @@ jobs:
artifacts/postgrest-windows-x86-64/postgrest.exe artifacts/postgrest-windows-x86-64/postgrest.exe
- name: Save release bundle - name: Save release bundle
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with: with:
name: release-bundle name: release-bundle
path: release-bundle path: release-bundle
@@ -138,17 +135,17 @@ jobs:
env: env:
DOCKER_REPO: ${{ vars.DOCKER_REPO }} DOCKER_REPO: ${{ vars.DOCKER_REPO }}
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Download x86-64 Docker image - name: Download x86-64 Docker image
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with: with:
name: postgrest-docker-x86-64 name: postgrest-docker-x86-64
- name: Download aarch64 binary - name: Download aarch64 binary
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with: with:
name: postgrest-ubuntu-aarch64 name: postgrest-ubuntu-aarch64
- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0 - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with: with:
username: ${{ vars.DOCKER_USER }} username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }} password: ${{ secrets.DOCKER_PASS }}
@@ -194,8 +191,8 @@ jobs:
vars.DOCKER_REPO && vars.DOCKER_USER && vars.DOCKER_REPO && vars.DOCKER_USER &&
github.ref == 'refs/tags/devel' github.ref == 'refs/tags/devel'
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: peter-evans/dockerhub-description@432a30c9e07499fd01da9f8a49f0faf9e0ca5b77 # v4.0.2 - uses: peter-evans/dockerhub-description@1b9a80c056b620d92cedb9d9b5a223409c68ddfa # v5.0.0
with: with:
username: ${{ vars.DOCKER_USER }} username: ${{ vars.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }} password: ${{ secrets.DOCKER_PASS }}
+38 -22
View File
@@ -39,17 +39,19 @@ jobs:
# https://github.com/actions/runner/issues/241#issuecomment-842566950 # https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}" shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: tests.coverage.bin tests.testDoctests.bin tests.testSpecIdempotence.bin tools: tests.coverage.bin tests.testDoctests.bin tests.testSpecIdempotence.bin cabalTools.update.bin
- run: postgrest-cabal-update
- name: Run coverage (IO tests and Spec tests against PostgreSQL 15) - name: Run coverage (IO tests and Spec tests against PostgreSQL 15)
run: postgrest-coverage run: postgrest-coverage
- name: Upload coverage to codecov - name: Upload coverage to codecov
uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d # v5.4.2 uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
with: with:
files: ./coverage/codecov.json files: ./coverage/codecov.json
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@@ -67,7 +69,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
pgVersion: [12, 13, 14, 15, 16, 17] pgVersion: [13, 14, 15, 16, 17]
name: PG ${{ matrix.pgVersion }} name: PG ${{ matrix.pgVersion }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
defaults: defaults:
@@ -76,36 +78,45 @@ jobs:
# https://github.com/actions/runner/issues/241#issuecomment-842566950 # https://github.com/actions/runner/issues/241#issuecomment-842566950
shell: script -qec "bash --noprofile --norc -eo pipefail {0}" shell: script -qec "bash --noprofile --norc -eo pipefail {0}"
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: tests.testSpec.bin tests.testIO.bin tests.testBigSchema.bin withTools.postgresql-${{ matrix.pgVersion }}.bin tools: tests.testSpec.bin tests.testObservability.bin tests.testIO.bin tests.testBigSchema.bin withTools.pg-${{ matrix.pgVersion }}.bin cabalTools.update.bin
- run: postgrest-cabal-update
- name: Run spec tests - name: Run spec tests
if: always() if: always()
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-spec run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-spec
- name: Run observability tests
if: always()
run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-observability
- name: Run IO tests - name: Run IO tests
if: always() if: always()
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-io -vv run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-io -vv
- name: Run IO tests on a big schema - name: Run IO tests on a big schema
if: always() if: always()
run: postgrest-with-postgresql-${{ matrix.pgVersion }} postgrest-test-big-schema -vv run: postgrest-with-pg-${{ matrix.pgVersion }} postgrest-test-big-schema -vv
memory: memory:
name: Memory name: Memory
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: tests.testMemory.bin tools: tests.testMemory.bin cabalTools.update.bin
- run: postgrest-cabal-update
- name: Run memory tests - name: Run memory tests
run: postgrest-test-memory run: postgrest-test-memory
@@ -113,40 +124,45 @@ jobs:
loadtest: loadtest:
strategy: strategy:
matrix: matrix:
kind: ['mixed', 'jwt'] kind: ['mixed', 'jwt-hs', 'jwt-hs-cache', 'jwt-hs-cache-worst', 'jwt-rsa', 'jwt-rsa-cache', 'jwt-rsa-cache-worst']
name: Loadtest name: Loadtest
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Nix Environment - name: Setup Nix Environment
uses: ./.github/actions/setup-nix uses: ./.github/actions/setup-nix
with: with:
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
tools: loadtest.loadtestAgainst.bin loadtest.report.bin tools: loadtest.loadtestAgainst.bin loadtest.report.bin cabalTools.update.bin
- uses: WyriHaximus/github-action-get-previous-tag@04e8485ecb6487243907e330d522ff60f02283ce # v1.4.0
id: get-latest-tag - run: postgrest-cabal-update
with:
prefix: v
- name: Run loadtest - name: Run loadtest
env:
TARGET_BRANCH: ${{ github.base_ref || github.ref_name }}
run: | run: |
postgrest-loadtest-against -k ${{ matrix.kind }} main ${{ steps.get-latest-tag.outputs.tag }} if [ "$TARGET_BRANCH" = "main" ]; then
postgrest-loadtest-report >> "$GITHUB_STEP_SUMMARY" latest_tag=$(git tag --sort=-creatordate --list "v*" | head -n1)
else
latest_tag=$(git tag --merged HEAD --sort=-creatordate "v*" | head -n1)
fi
postgrest-loadtest-against -k ${{ matrix.kind }} "$TARGET_BRANCH" "$latest_tag"
postgrest-loadtest-report -g ${{ matrix.kind }} >> "$GITHUB_STEP_SUMMARY"
flake: flake:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
runs-on: runs-on:
- macos-13 # x86_64-darwin
- macos-14 # aarch64-darwin - macos-14 # aarch64-darwin
- ubuntu-24.04 # x86_64-linux - ubuntu-24.04 # x86_64-linux
- ubuntu-24.04-arm # aarch64-linux - ubuntu-24.04-arm # aarch64-linux
name: Flake Check name: Flake Check
runs-on: ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }}
steps: steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Setup Nix Environment - name: Setup Nix Environment
+1
View File
@@ -25,3 +25,4 @@ loadtest
.history .history
.docs-build .docs-build
gen_targets.http gen_targets.http
gen_jwk.json
+15 -10
View File
@@ -13,26 +13,26 @@ 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://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="static/neon.jpg"> <img width="296px" src="static/supabase.svg">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="static/code-build.png"> <img width="296px" src="static/euronodes.svg">
</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://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>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank"> <a href="https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/supabase.png"> <img width="296px" src="static/bytebase.svg">
</a> </a>
</td> </td>
</tr> </tr>
@@ -89,8 +89,13 @@ 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://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="222px" src="static/gnuhost.png"> <img width="222px" src="static/code-build.png">
</a>
</td>
<td align="center" valign="middle">
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/tembo.png">
</a> </a>
</td> </td>
</tr> </tr>
+173 -3
View File
@@ -1,10 +1,176 @@
# Change Log # Change Log
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file. From version `14.0` onwards PostgREST follows a `MAJOR.PATCH` two-part versioning. Only even-numbered MAJOR versions will be released, reserving odd-numbered MAJOR versions for development.
This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased ## Unreleased
## [14.10] - 2026-04-16
### Added
- Log when the pool is released during schema cache reload on `log-level=debug` by @mkleczek in #4668
### Fixed
- Fix unnecessary connection pool flushes during schema cache reloading by @mkleczek in #4645
## [14.9] - 2026-04-10
### Added
- Log host, port and pg version of listener database connection by @mkleczek in #4617 #4618
### Fixed
- Remove red herring warp logs on default log-level, only emit them on `log-level=debug` by @steve-chavez in #4799
## [14.8] - 2026-04-03
### Added
- Log a `HINT` when the LISTEN channel stops working due to a PostgreSQL bug by @laurenceisla in #4581
### Fixed
- Fix invalid OpenAPI 2.0 format for integer types (`smallint`, `integer`, `bigint`) by @arturbent0 in #4641
## [14.7] - 2026-03-20
### Fixed
- Fix not logging SIGTERM and SIGINT by @steve-chavez in #4728
## [14.6] - 2026-03-06
### Fixed
- Fix leaking table and function names when calculating error hint by @taimoorzaeem in #4675
## [14.5] - 2026-02-12
### Fixed
- Don't hide async exceptions in logs by @stevechavez in #4646
## [14.4] - 2026-01-29
### Fixed
- Ensure Listener connections are released by @mkleczek in #4614
- Fix incorrectly filtering the returned representation for PATCH requests when using `or/and` filters by @laurenceisla in #3707
- Fix listener running with exception masked after first failure by @mkleczek #4615
## [14.3] - 2026-01-03
### Fixed
- Fix performance and high memory usage of relation hint calculation by @mkleczek in #4462, #4463
## [14.2] - 2025-12-18
### Fixed
- Fix `hasSingleUnnamedParam` incorrectly matching functions with named parameters by @joelonsql in #4553
+ Functions with a single named parameter (e.g., `foo(data json)`) no longer incorrectly match the single-param fallback, returning a clean `PGRST202` error instead of a confusing PostgreSQL `42883` error.
- Fix misleading logs on unsupported PostgreSQL versions by @taimoorzaeem in #4519
- Fix regression where the `PGRST103` error response was truncated by @laurenceisla in #4455
+ Happened when an `offset` was greater than the rows requested and `Prefer: count=exact` was sent.
- Fix not returning `Content-Length` on empty HTTP `201` responses by @laurenceisla in #4518
- Fix inaccurate Server-Timing header durations by @steve-chavez in #4522
- Fix inaccurate "Schema cache queried" logs by @steve-chavez in #4522
## [14.1] - 2025-11-05
## Fixed
- Fix `db-pre-config` function failing when function names are pg reserved words by @taimoorzaeem in #4380
- Fix `server-host=!6` incorrectly binds to IPv4 address by @taimoorzaeem in #3202
## [14.0] - 2025-10-24
### Added
- Bounded JWT cache using the SIEVE algorithm by @mkleczek in #4084
+ It now uses a fixed size cache instead of arbitrary sized cache.
- Add `--ready` flag for postgrest healthcheck by @taimoorzaeem in #4239
### Fixed
- Fix not logging OpenAPI queries when `log-query=main-query` is enabled by @steve-chavez in #4226
- Fix not logging explain query when `log-query=main-query` is enabled by @steve-chavez in #4319
- Fix not logging transaction variables and db-pre-request function when `log-query=main-query` is enabled by @steve-chavez in #3934
- Fix not logging the JSON message to stderr on a `PGRST002` error by @laurenceisla in #4129
- Fix reloading the Schema Cache unnecessarily on a `PGRST002` error by @laurenceisla in #4367
- Fix schema cache loading taking a long time for large schemas by @mkleczek in #4360, #3704
### Changed
- Drop support for PostgreSQL EOL version 12 by @wolfgangwalther in #3865
- From now on PostgREST will follow a `MAJOR.PATCH` two-part versioning. Only even-numbered MAJOR versions will be released, reserving odd-numbered MAJOR versions for development.
- Replaced `jwt-cache-max-lifetime` config with `jwt-cache-max-entries` by @mkleczek in #4084
- `log-query` config now takes a boolean instead of a string value by @steve-chavez in #3934
## [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 #4080
- 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
- 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
### Fixed
## [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 ## [13.0.0] - 2025-05-08
### Added ### Added
@@ -47,8 +213,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #2052, Dropped support for PostgreSQL 11 - @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 - #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 - #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 config - @wolfgangwalther - #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. - 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 - #3757, Remove support for `Prefer: params=single-object` - @joelonsql
+ This preference was deprecated in favor of Functions with an array of JSON objects + This preference was deprecated in favor of Functions with an array of JSON objects
- #3013, Drop support for Limited updates/deletes - #3013, Drop support for Limited updates/deletes
@@ -56,6 +223,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
- #3956, Drop `/config` endpoint of admin server - @steve-chavez - #3956, Drop `/config` endpoint of admin server - @steve-chavez
+ The endpoint was at risk of being left unprotected when exposing it. + The endpoint was at risk of being left unprotected when exposing it.
+ The accompanying `admin-server-config-enabled` config was also dropped. + The accompanying `admin-server-config-enabled` config was also dropped.
- #3598, PostgREST now validates the `kid` parameter of the JWT - @wolfgangwalther
+ If the JWT contains a ``kid`` parameter, then PostgREST will look for the JSON Web Key in the `jwt-secret`.
+ If the JWT doesn't contain a `kid`, the behavior should be backwards compatible. PostgREST will try each key in the `jwt-secret` one by one until it finds one that works.
- #3697, #3602, Querying non-existent table now returns `PGRST205` error instead of empty json - @taimoorzaeem - #3697, #3602, Querying non-existent table now returns `PGRST205` error instead of empty json - @taimoorzaeem
- #3600, #3926, Improve JWT errors - @taimoorzaeem - #3600, #3926, Improve JWT errors - @taimoorzaeem
+ Return `PGRST301` error when `Bearer` in auth header is sent empty + Return `PGRST301` error when `Bearer` in auth header is sent empty
+1 -1
View File
@@ -21,7 +21,7 @@ For questions on how to use PostgREST, please use
### Reporting an Issue ### Reporting an Issue
* Make sure you test against the latest [stable release](https://github.com/PostgREST/postgrest/releases/latest) * Make sure you test against the latest [stable release](https://github.com/PostgREST/postgrest/releases/latest)
and also against the latest [nightly release](https://github.com/PostgREST/postgrest/releases/tag/nightly). and also against the latest [devel release](https://github.com/PostgREST/postgrest/releases/tag/devel).
It is possible we already fixed the bug you're experiencing. It is possible we already fixed the bug you're experiencing.
* Provide steps to reproduce the issue, including your OS version and * Provide steps to reproduce the issue, including your OS version and
+1 -1
View File
@@ -2,7 +2,7 @@
# The x86-64 is a single-static-binary image built via Nix, see: # The x86-64 is a single-static-binary image built via Nix, see:
# nix/tools/docker/README.md # nix/tools/docker/README.md
FROM ubuntu:noble@sha256:6015f66923d7afbc53558d7ccffd325d43b4e249f41a6e93eef074c9505d2233 AS postgrest FROM ubuntu:noble@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b AS postgrest
RUN apt-get update -y \ RUN apt-get update -y \
&& apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \ && apt install -y --no-install-recommends libpq-dev zlib1g-dev jq gcc libnuma-dev \
+14 -10
View File
@@ -22,26 +22,26 @@ 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://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="static/neon.jpg"> <img width="296px" src="static/supabase.svg">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="static/code-build.png"> <img width="296px" src="static/euronodes.svg">
</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://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>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank"> <a href="https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="static/supabase.png"> <img width="296px" src="static/bytebase.svg">
</a> </a>
</td> </td>
</tr> </tr>
@@ -52,8 +52,8 @@ Big thanks to our sponsors! You can join them by supporting PostgREST on [Patreo
## Usage ## Usage
1. Download the binary ([latest release](https://github.com/PostgREST/postgrest/releases/latest)) 1. See the docs for [how to install PostgREST on your platform](https://docs.postgrest.org/en/stable/explanations/install.html). You can also [use Docker](https://docs.postgrest.org/en/stable/explanations/install.html#docker).
for your platform.
2. Invoke for help: 2. Invoke for help:
```bash ```bash
@@ -142,6 +142,10 @@ You can help PostgREST ongoing maintenance and development by making a regular d
Every donation will be spent on making PostgREST better for the whole community. Every donation will be spent on making PostgREST better for the whole community.
## Contributing
Contributions are always welcome and appreciated. Please see the [Contributing guidelines](https://github.com/PostgREST/postgrest/blob/main/CONTRIBUTING.md).
## Thanks ## Thanks
The PostgREST organization is grateful to: The PostgREST organization is grateful to:
-2
View File
@@ -1,4 +1,2 @@
packages: postgrest.cabal packages: postgrest.cabal
tests: true tests: true
package *
ghc-options: -split-sections
+1 -1
View File
@@ -1 +1 @@
index-state: hackage.haskell.org 2025-02-01T14:59:33Z index-state: hackage.haskell.org 2025-10-29T04:02:18Z
+25 -12
View File
@@ -43,7 +43,6 @@ let
allOverlays.build-toolbox allOverlays.build-toolbox
allOverlays.checked-shell-script allOverlays.checked-shell-script
allOverlays.gitignore allOverlays.gitignore
allOverlays.postgresql-libpq
(allOverlays.haskell-packages { inherit compiler; }) (allOverlays.haskell-packages { inherit compiler; })
allOverlays.slocat allOverlays.slocat
]; ];
@@ -54,16 +53,17 @@ let
postgresqlVersions = postgresqlVersions =
[ [
{ name = "postgresql-17"; postgresql = pkgs.postgresql_17.withPackages (p: [ p.postgis p.pg_safeupdate ]); } { name = "pg-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 = "pg-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 = "pg-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 = "pg-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 = "pg-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 ]); }
]; ];
haskellPackages = pkgs.haskell.packages."${compiler}";
# Dynamic derivation for PostgREST # Dynamic derivation for PostgREST
postgrest = pkgs.lib.pipe (pkgs.haskell.packages."${compiler}".callCabal2nix name src { }) [ postgrest = pkgs.lib.pipe (haskellPackages.callCabal2nix name src { }) [
# To allow ghc-datasize to be used. # To allow ghc-datasize to be used.
lib.disableLibraryProfiling lib.disableLibraryProfiling
# We are never going to use dynamic haskell libraries anyway. "Dynamic" refers to how # We are never going to use dynamic haskell libraries anyway. "Dynamic" refers to how
@@ -75,7 +75,7 @@ let
# 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";
inherit (pkgs.haskell) lib; inherit (pkgs.haskell) lib;
in in
@@ -84,9 +84,14 @@ rec {
# Derivation for the PostgREST Haskell package, including the executable, # Derivation for the PostgREST Haskell package, including the executable,
# libraries and documentation. We disable running the test suite on Nix # libraries and documentation. We disable running the test suite on Nix
# builds, as they require a database to be set up. # builds, as they require a database to be set up. We split the binary
postgrestPackage = # into a separate output, so that the default distribution via flake.nix
lib.dontCheck postgrest; # has a much smaller closure size.
postgrestPackage = pkgs.lib.pipe postgrest [
lib.dontCheck
lib.enableSeparateBinOutput
(haskellPackages.generateOptparseApplicativeCompletions [ "postgrest" ])
];
# Profiled dynamic executable. # Profiled dynamic executable.
postgrestProfiled = pkgs.lib.pipe postgrestPackage [ postgrestProfiled = pkgs.lib.pipe postgrestPackage [
@@ -119,10 +124,18 @@ rec {
docs = docs =
pkgs.callPackage nix/tools/docs.nix { }; pkgs.callPackage nix/tools/docs.nix { };
# Git tools.
gitTools =
pkgs.callPackage nix/tools/gitTools.nix { };
# Load testing tools. # Load testing tools.
loadtest = loadtest =
pkgs.callPackage nix/tools/loadtest.nix { inherit withTools; }; pkgs.callPackage nix/tools/loadtest.nix { inherit 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.nix { };
+8 -8
View File
@@ -19,26 +19,26 @@ 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://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/neon.jpg"> <img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.svg">
</a> </a>
</td> </td>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank"> <a href="https://www.euronodes.com/postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/code-build.png"> <img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/euronodes.svg">
</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://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>
<td align="center" valign="middle"> <td align="center" valign="middle">
<a href="https://supabase.io?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage" target="_blank"> <a href="https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png"> <img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/bytebase.svg">
</a> </a>
</td> </td>
</tr> </tr>
+10 -5
View File
@@ -40,9 +40,14 @@ database "PostgreSQL" {
:user: :user:
hexagon Proxy hexagon Proxy
:user: .r-> Proxy :user: .r-> Proxy : request with JWT
HTTPAPI <.l- Proxy HTTPAPI <.l- Proxy
hexagon ExternalAuth
ExternalAuth -u[hidden]- Proxy
:user: .r-> ExternalAuth : login
:user: <.r- ExternalAuth : JWT
:operator: .d-> HTTPADMIN :operator: .d-> HTTPADMIN
:operator: .d-> CLI :operator: .d-> CLI
@@ -51,9 +56,8 @@ PostgreSQL <.developer : "\t"
Listener -r.> "PostgreSQL" Listener -r.> "PostgreSQL"
"Connection Pool" -r.> "PostgreSQL" : "\t\t" "Connection Pool" -r.> "PostgreSQL" : "\t\t"
note bottom of Auth note bottom of Auth
Authenticates the user request Validates the JWT
end note end note
note bottom of ApiRequest note bottom of ApiRequest
@@ -72,6 +76,7 @@ note top of Listener
LISTEN session LISTEN session
end note end note
url of ExternalAuth is [[../explanations/external_auth.html]]
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]]
@@ -82,8 +87,8 @@ 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 [[https://aosabook.org/en/posa/warp.html]] url of HTTPADMIN is [[../explanations/architecture.html#http]]
url of HTTPAPI is [[https://aosabook.org/en/posa/warp.html]] url of HTTPAPI is [[../explanations/architecture.html#http]]
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: 28 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: 29 KiB

+4 -3
View File
@@ -12,7 +12,6 @@
# All configuration values have a default; values that are commented out # All configuration values have a default; values that are commented out
# serve to show the default. # serve to show the default.
import sys
import os import os
# If extensions (or modules to document with autodoc) are in another directory, # If extensions (or modules to document with autodoc) are in another directory,
@@ -50,7 +49,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 = "14"
# 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 +113,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 = {"display_version": False} html_theme_options = {}
# 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 = []
@@ -300,8 +299,10 @@ linkcheck_ignore = [
# 403 only in CI / GitHub Actions # 403 only in CI / GitHub Actions
r"https://www.patreon.com/postgrest", r"https://www.patreon.com/postgrest",
r"https://blog.frankel.ch/poor-man-api", r"https://blog.frankel.ch/poor-man-api",
r"https://www.cybertec-postgresql.com/.*",
# Odd SSL error # Odd SSL error
r"https://www.dripdepot.com", r"https://www.dripdepot.com",
r"https://www.euronodes.com",
# New GitHub UI delays comment load, so anchor fails # New GitHub UI delays comment load, so anchor fails
r"https://github.com/.*#issuecomment", r"https://github.com/.*#issuecomment",
# Random 500 Internal Server Error # Random 500 Internal Server Error
+2 -2
View File
@@ -6,7 +6,7 @@ Community Tutorials
* `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ - * `Building a Contacts List with PostgREST and Vue.js <https://www.youtube.com/watch?v=iHtsALtD5-U>`_ -
In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet. In this video series, DigitalOcean shows how to build and deploy an Nginx + PostgREST(using a managed PostgreSQL database) + Vue.js webapp in an Ubuntu server droplet.
* `PostgREST + Auth0: Create REST API in mintutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service. * `PostgREST + Auth0: Create REST API in minutes, and add social login using Auth0 <https://samkhawase.com/blog/postgrest-1-introduction/>`_ - A step-by-step tutorial to show how to dockerize and integrate Auth0 to PostgREST service.
* `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ - * `"CodeLess" backend using postgres, postgrest and oauth2 authentication with keycloak <https://www.mathieupassenaud.fr/codeless_backend/>`_ -
A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service). A step-by-step tutorial for using PostgREST with KeyCloak(hosted on a managed service).
@@ -34,7 +34,7 @@ Templates
Example Apps Example Apps
------------ ------------
* `archtika <https://github.com/archtika/archtika>`_ - selfhosted CMS * `archtika <https://github.com/thiloho/archtika>`_ - self-hosted CMS
* `delibrium-postgrest <https://gitlab.com/delibrium/delibrium-postgrest/>`_ - example school API and front-end in Vue.js * `delibrium-postgrest <https://gitlab.com/delibrium/delibrium-postgrest/>`_ - example school API and front-end in Vue.js
* `ETH-transactions-storage <https://github.com/Adamant-im/ETH-transactions-storage>`_ - indexer for Ethereum to get transaction list by ETH address * `ETH-transactions-storage <https://github.com/Adamant-im/ETH-transactions-storage>`_ - indexer for Ethereum to get transaction list by ETH address
* `general <https://github.com/PierreRochard/general>`_ - example auth back-end * `general <https://github.com/PierreRochard/general>`_ - example auth back-end
+1 -1
View File
@@ -13,7 +13,7 @@ A role can be thought of as either a database user, or a group of database users
Roles for Each Web User Roles for Each Web User
----------------------- -----------------------
PostgREST can accommodate either viewpoint. If you treat a role as a single user then the :ref:`jwt_impersonation` does most of what you need. When an authenticated user makes a request PostgREST will switch into the database role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable. PostgREST can accommodate either viewpoint. If you treat a role as a single user then :ref:`user_impersonation` does most of what you need. When an authenticated user makes a request PostgREST will switch into the database role for that user, which in addition to restricting queries, is available to SQL through the :code:`current_user` variable.
You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example <https://www.enterprisedb.com:443/blog/application-users-vs-row-level-security>`_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users. You can use row-level security to flexibly restrict visibility and access for the current user. Here is an `example <https://www.enterprisedb.com:443/blog/application-users-vs-row-level-security>`_ from Tomas Vondra, a chat table storing messages sent between users. Users can insert rows into it to send messages to other users, and query it to see messages sent to them by other users.
@@ -1,6 +1,6 @@
.. _external_jwt: .. _external_auth:
External JWT Generation External Authentication
----------------------- -----------------------
JWT from Auth0 JWT from Auth0
@@ -9,21 +9,3 @@ JWT from Auth0
An external service like `Auth0 <https://auth0.com/>`_ can do the hard work transforming OAuth from Github, Twitter, Google etc into a JWT suitable for PostgREST. Auth0 can also handle email signup and password reset flows. An external service like `Auth0 <https://auth0.com/>`_ can do the hard work transforming OAuth from Github, Twitter, Google etc into a JWT suitable for PostgREST. Auth0 can also handle email signup and password reset flows.
To use Auth0, create `an application <https://auth0.com/docs/get-started/applications>`_ for your app and `an API <https://auth0.com/docs/get-started/apis>`_ for your PostgREST server. Auth0 supports both HS256 and RS256 scheme for the issued tokens for APIs. For simplicity, you may first try HS256 scheme while creating your API on Auth0. Your application should use your PostgREST API's `API identifier <https://auth0.com/docs/get-started/apis/api-settings>`_ by setting it with the `audience parameter <https://auth0.com/docs/secure/tokens/access-tokens/get-access-tokens#control-access-token-audience>`_ during the authorization request. This will ensure that Auth0 will issue an access token for your PostgREST API. For PostgREST to verify the access token, you will need to set ``jwt-secret`` on PostgREST config file with your API's signing secret. To use Auth0, create `an application <https://auth0.com/docs/get-started/applications>`_ for your app and `an API <https://auth0.com/docs/get-started/apis>`_ for your PostgREST server. Auth0 supports both HS256 and RS256 scheme for the issued tokens for APIs. For simplicity, you may first try HS256 scheme while creating your API on Auth0. Your application should use your PostgREST API's `API identifier <https://auth0.com/docs/get-started/apis/api-settings>`_ by setting it with the `audience parameter <https://auth0.com/docs/secure/tokens/access-tokens/get-access-tokens#control-access-token-audience>`_ during the authorization request. This will ensure that Auth0 will issue an access token for your PostgREST API. For PostgREST to verify the access token, you will need to set ``jwt-secret`` on PostgREST config file with your API's signing secret.
JWT using OpenSSL
~~~~~~~~~~~~~~~~~
To manually generate a JWT using ``openssl`` commands, you can use the following script. This may be useful for testing JWT related features of PostgREST.
.. code:: bash
#!/bin/bash
set -e
TEST_JWT_SECRET='test_secret_that_is_at_least_32_characters_long'
_base64 () { openssl base64 -e -A | tr '+/' '-_' | tr -d '='; }
header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | _base64)
exp=$(( EPOCHSECONDS + 60*60 )) # 1 hour
payload=$(echo -n "{\"role\":\"test_role\",\"exp\":$exp}" | _base64)
signature=$(echo -n "$header.$payload" | openssl dgst -sha256 -hmac "$TEST_JWT_SECRET" -binary | _base64)
echo -n "$header.$payload.$signature"
+6 -1
View File
@@ -16,7 +16,7 @@ Supported PostgreSQL versions
============================= =============================
=============== ================================= =============== =================================
**Supported** PostgreSQL >= 12 **Supported** PostgreSQL >= 13
=============== ================================= =============== =================================
PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_. PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_.
@@ -75,8 +75,12 @@ You can get the `official PostgREST Docker image <https://hub.docker.com/r/postg
.. code-block:: bash .. code-block:: bash
# pull the latest version
docker pull postgrest/postgrest docker pull postgrest/postgrest
# to pull a particular version, use one of the versions on https://hub.docker.com/r/postgrest/postgrest/tags
docker pull postgrest/postgrest:<version>
To configure the container image, use :ref:`env_variables_config`. To configure the container image, use :ref:`env_variables_config`.
There are two ways to run the PostgREST container: with an existing external database, or through docker-compose. There are two ways to run the PostgREST container: with an existing external database, or through docker-compose.
@@ -142,6 +146,7 @@ To avoid having to install the database at all, you can run both it and the serv
ports: ports:
- "3000:3000" - "3000:3000"
environment: environment:
PGRST_SERVER_HOST: 0.0.0.0 # necessary for `postgrest --ready` flag to work
PGRST_DB_URI: postgres://app_user:password@db:5432/app_db PGRST_DB_URI: postgres://app_user:password@db:5432/app_db
PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000 PGRST_OPENAPI_SERVER_PROXY_URI: http://127.0.0.1:3000
depends_on: depends_on:
@@ -43,7 +43,7 @@ As in :ref:`sql_user_management`, we create the :code:`pgcrypto` and :code:`pgjw
CREATE EXTENSION pgcrypto WITH SCHEMA ext_pgcrypto; CREATE EXTENSION pgcrypto WITH SCHEMA ext_pgcrypto;
Concerning the `pgjwt extension <https://github.com/michelp/pgjwt>`_, please cf. to :ref:`client_auth`. Concerning the `pgjwt extension <https://github.com/michelp/pgjwt>`_, please cf. to :ref:`jwt-from-sql`.
.. code-block:: postgres .. code-block:: postgres
@@ -157,7 +157,7 @@ Here we use the username instead of the email address to identify a user.
Logins Logins
~~~~~~ ~~~~~~
As described in :ref:`client_auth`, we'll create a JWT token inside our login function. Note that you'll need to adjust the secret key which is hard-coded in this example to a secure (at least thirty-two character) secret of your choosing. As described in :ref:`jwt-from-sql`, we'll create a JWT token inside our login function. Note that you'll need to adjust the secret key which is hard-coded in this example to a secure (at least thirty-two character) secret of your choosing.
.. code-block:: postgres .. code-block:: postgres
+3 -1
View File
@@ -3,7 +3,7 @@
SQL User Management SQL User Management
=================== ===================
As mentioned on :ref:`jwt_generation`, an external service can provide user management and coordinate with the PostgREST server using JWT. Its also possible to support logins entirely through SQL. Its a fair bit of work, so get ready. As mentioned on :ref:`jwt_generation`, an external service can provide user management and coordinate with the PostgREST server using JWT. It's also possible to support logins entirely through SQL. It's a fair bit of work, so get ready.
Storing Users and Passwords Storing Users and Passwords
--------------------------- ---------------------------
@@ -110,6 +110,8 @@ Then, add ``db-anon-role`` to the configuration file to allow anonymous requests
db-anon-role = "anon" db-anon-role = "anon"
.. _jwt-from-sql:
JWT from SQL JWT from SQL
~~~~~~~~~~~~ ~~~~~~~~~~~~
+57 -35
View File
@@ -38,48 +38,52 @@ 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
.. container:: img-dark
.. image:: ../static/supabase-dark.svg
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
.. container:: img-light
.. 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
.. container:: img-dark
.. image:: ../static/euronodes.svg
:target: https://www.euronodes.com/postgrest
.. container:: img-light
.. image:: ../static/euronodes.svg
:target: https://www.euronodes.com/postgrest
|
.. container:: img-dark .. container:: img-dark
.. image:: ../static/neon-dark.jpg .. image:: ../static/neon-dark.jpg
:target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest :target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
.. container:: img-light .. container:: img-light
.. image:: ../static/neon.jpg .. image:: ../static/neon.jpg
:target: https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest :target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
.. container:: img-dark .. container:: img-dark
.. image:: ../static/code-build-dark.png .. image:: ../static/bytebase-dark.svg
:target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest :target: https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest
.. container:: img-light .. container:: img-light
.. image:: ../static/code-build.png .. image:: ../static/bytebase.svg
:target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest :target: https://www.bytebase.com/?utm_source=sponsor&utm_campaign=postgrest
|
.. image:: ../static/tembo.png
:target: https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest
.. container:: img-dark
.. image:: ../static/supabase-dark.png
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
.. container:: img-light
.. image:: ../static/supabase.png
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
.. image:: _static/empty.png
:target: #sponsors
.. 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
| |
@@ -109,10 +113,17 @@ Getting Support
The project has a friendly and growing community. For discussions, use the Github `discussions page <https://github.com/PostgREST/postgrest/discussions>`_. You can also report or search for bugs/features on the Github `issues <https://github.com/PostgREST/postgrest/issues>`_ page. The project has a friendly and growing community. For discussions, use the Github `discussions page <https://github.com/PostgREST/postgrest/discussions>`_. You can also report or search for bugs/features on the Github `issues <https://github.com/PostgREST/postgrest/issues>`_ page.
Release Notes Releases
------------- --------
The release notes are published on `PostgREST's GitHub release page <https://github.com/PostgREST/postgrest/releases>`_. PostgREST follows ``MAJOR.PATCH`` two-part versioning:
- ``MAJOR``: feature release, may deprecate or remove things.
- ``PATCH``: fix/security release only; no features, no behavior changes.
Starting from ``v14.0``, only even-numbered MAJOR versions will be released, reserving odd-numbered MAJOR versions for development.
All the releases are published on `PostgREST's GitHub release page <https://github.com/PostgREST/postgrest/releases>`_.
Tutorials Tutorials
--------- ---------
@@ -209,20 +220,14 @@ In Production
Here are some companies that use PostgREST in production. Here are some companies that use PostgREST in production.
* `Catarse <https://www.catarse.me>`_ * `Catarse <https://www.catarse.me>`_
* `Datrium <https://www.datrium.com>`_
* `Drip Depot <https://www.dripdepot.com>`_ * `Drip Depot <https://www.dripdepot.com>`_
* `Image-charts <https://www.image-charts.com>`_ * `Image-charts <https://www.image-charts.com>`_
* `Moat <https://www.oracle.com/advertising/>`_
* `Netwo <https://www.netwo.io>`_ * `Netwo <https://www.netwo.io>`_
* `Nimbus <https://www.nimbusfacility.com/sg/home>`_ * `Nimbus <https://www.nimbusfacility.com/sg/home>`_
- See how Nimbus uses PostgREST in `Paul Copplestone's blog post <https://paul.copplest.one/blog/nimbus-tech-2019-04.html>`_. - See how Nimbus uses PostgREST in `Paul Copplestone's blog post <https://paul.copplest.one/blog/nimbus-tech-2019-04.html>`_.
* `OpenBooking <https://openbooking.ch>`_ * `OpenBooking <https://openbooking.ch>`_
* `Supabase <https://supabase.com>`_ * `Supabase <https://supabase.com>`_
.. Failing links
* `eGull <http://www.egull.co>`_
* `MotionDynamic - Fast highly dynamic video generation at scale <https://motiondynamic.tech>`_
Testimonials Testimonials
------------ ------------
@@ -266,4 +271,21 @@ Testimonials
Contributing Contributing
------------ ------------
Please see the `Contributing guidelines <https://github.com/PostgREST/postgrest/blob/main/.github/CONTRIBUTING.md>`_ in the main PostgREST repository. Please see the `Contributing guidelines <https://github.com/PostgREST/postgrest/blob/main/CONTRIBUTING.md>`_ in the main PostgREST repository.
.. raw:: html
<script type="text/javascript">
let hash = window.location.hash;
const redirects = {
// Tables and Views
'#release-notes': '#releases',
};
let willRedirectTo = redirects[hash];
if (willRedirectTo) {
window.location.href = willRedirectTo;
}
</script>
-6
View File
@@ -1,6 +0,0 @@
Greenplum
#########
`Greenplum <https://blogs.vmware.com/tanzu/tanzu-greenplum/>`_ has been reported to work by adding ``LOGIN`` to the :ref:`anonymous and user roles <roles>`.
For more details, see https://github.com/PostgREST/postgrest/issues/2021.
+3 -2
View File
@@ -46,7 +46,6 @@ Github
Google Google
grantor grantor
GraphQL GraphQL
Greenplum
gte gte
GUC GUC
Haskell Haskell
@@ -104,7 +103,6 @@ Observability
Okta Okta
OpenAPI OpenAPI
openapi openapi
OpenSSL
ov ov
parametrized parametrized
passphrase passphrase
@@ -176,6 +174,7 @@ unikernel
unix unix
updatable updatable
unfulfillable unfulfillable
unselected
Untyped Untyped
UPSERT UPSERT
Upsert Upsert
@@ -195,3 +194,5 @@ Websockets
webuser webuser
wfts wfts
www www
debouncing
deduplicates
+1 -1
View File
@@ -12,7 +12,7 @@ Health Check
You can enable a health check to verify if PostgREST is available for client requests. Also to check the status of its internal state. You can enable a health check to verify if PostgREST is available for client requests. Also to check the status of its internal state.
Two endpoints ``live`` and ``ready`` will then be available. Two endpoints ``live`` and ``ready`` will then be available. Both these endpoints reply with a status code and empty response body.
.. important:: .. important::
@@ -173,4 +173,4 @@ Domain Representations avoid all the above drawbacks. Their only drawback is tha
Why not create a `base type <https://www.postgresql.org/docs/current/sql-createtype.html#id-1.9.3.94.5.8>`_ instead? ``CREATE TYPE app_uuid (INTERNALLENGTH = 22, INPUT = app_uuid_parser, OUTPUT = app_uuid_formatter)``. Why not create a `base type <https://www.postgresql.org/docs/current/sql-createtype.html#id-1.9.3.94.5.8>`_ instead? ``CREATE TYPE app_uuid (INTERNALLENGTH = 22, INPUT = app_uuid_parser, OUTPUT = app_uuid_formatter)``.
Creating base types need superuser, which is restricted on cloud hosted databases. Additionally this way lets how the data is presented dictate how the data is stored which would be backwards. Creating base types need superuser, which is restricted on cloud hosted databases. Additionally this way lets "how the data is presented" dictate "how the data is stored" which would be backwards.
+17
View File
@@ -294,6 +294,23 @@ Let's get its :ref:`explain_plan` when calling it with filters applied:
Notice there's no "Function Scan" node in the plan, which tells us it has been inlined. Notice there's no "Function Scan" node in the plan, which tells us it has been inlined.
Horizontal Filtering
~~~~~~~~~~~~~~~~~~~~
Table-valued functions support horizontal filtering on selected and unselected columns.
For example, the following RPC with filter on unselected column returns:
.. code-block:: bash
curl "http://localhost:3000/rpc/getallprojects?select=id,client_id&name=like.OSX"
.. code-block:: json
[
{ "id": 4, "client_id": 2 }
]
.. _scalar_functions: .. _scalar_functions:
Scalar functions Scalar functions
+33 -1
View File
@@ -242,7 +242,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. This is also supported through ``RPC`` calls. 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.
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.
@@ -264,3 +264,35 @@ To illustrate the use of this preference, consider the following scenario where
"details": "The query affects 14 rows", "details": "The query affects 14 rows",
"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:
.. code-block:: postgres
CREATE FUNCTION test.delete_items()
RETURNS SETOF items AS $$
DELETE FROM items WHERE id < 15 RETURNING *;
$$ LANGUAGE SQL;
.. code-block:: bash
curl -i "http://localhost:3000/rpc/delete_items" \
-H "Content-Type: application/json" \
-H "Prefer: handling=strict, max-affected=10"
.. code-block:: http
HTTP/1.1 400 Bad Request
.. code-block:: json
{
"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.
+84 -60
View File
@@ -143,7 +143,7 @@ Since the table name is plural, we can be more accurate by making it singular wi
One-to-many relationships One-to-many relationships
------------------------- -------------------------
The **foreign key reference** establishes the inverse one-to-many relationship. In this case, ``films`` returns as a JSON array because of the to-many end. The **foreign key reference** establishes the inverse one-to-many relationship. In this case, ``films`` returns as a JSON array because of the "to-many" end.
.. code-block:: bash .. code-block:: bash
@@ -251,6 +251,12 @@ Computed Relationships
You can manually define relationships by using functions. This is useful for database objects that can't define foreign keys, like `Foreign Data Wrappers <https://wiki.postgresql.org/wiki/Foreign_data_wrappers>`_. You can manually define relationships by using functions. This is useful for database objects that can't define foreign keys, like `Foreign Data Wrappers <https://wiki.postgresql.org/wiki/Foreign_data_wrappers>`_.
Computed relationships have good performance as their intended design enable `function inlining <https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions#Inlining_conditions_for_table_functions>`_.
.. important::
- Always use ``SETOF`` when creating computed relationships. Functions can return a table without using ``SETOF``, but bear in mind that PostgreSQL will not inline them. e.g. ``RETURNS <table_name>`` is not inlinable.
Assuming there's a foreign table ``premieres`` that we want to relate to ``films``. Assuming there's a foreign table ``premieres`` that we want to relate to ``films``.
.. code-block:: postgres .. code-block:: postgres
@@ -283,6 +289,10 @@ The name of the function ``film`` is arbitrary and can be used to do the embeddi
".." ".."
] ]
.. warning::
- Make sure to correctly label the ``to-one`` part of the relationship. When using the ``ROWS 1`` estimation, PostgREST will expect a single row to be returned. If that is not the case, it will unnest the embedding and return repeated values for the top level resource.
Now let's define the opposite one-to-many relationship. Now let's define the opposite one-to-many relationship.
.. code-block:: postgres .. code-block:: postgres
@@ -331,12 +341,6 @@ Thanks to overloaded functions, you can use the same function name for different
Computed relationships have good performance as their intended design enable `function inlining <https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions#Inlining_conditions_for_table_functions>`_. Computed relationships have good performance as their intended design enable `function inlining <https://wiki.postgresql.org/wiki/Inlining_of_SQL_functions#Inlining_conditions_for_table_functions>`_.
.. warning::
- Always use ``SETOF`` when creating computed relationships. Functions can return a table without using ``SETOF``, but bear in mind that PostgreSQL will not inline them.
- Make sure to correctly label the ``to-one`` part of the relationship. When using the ``ROWS 1`` estimation, PostgREST will expect a single row to be returned. If that is not the case, it will unnest the embedding and return repeated values for the top level resource.
.. _embed_disamb: .. _embed_disamb:
.. _target_disamb: .. _target_disamb:
.. _hint_disamb: .. _hint_disamb:
@@ -1150,27 +1154,19 @@ For example, to arrange the films in descending order using the director's last
Spread embedded resource Spread embedded resource
======================== ========================
The ``...`` operator lets you "spread" an embedded resource. You can modify the shape of the embedded resources by using the spread syntax (``...``).
That is, it removes the surrounding JSON object for the embedded resource columns.
.. note::
The spread operator ``...`` is borrowed from the Javascript `spread syntax <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax>`_.
.. _spread_to_one_embed: .. _spread_to_one_embed:
Spread To-One relationships Spread To-One relationships
--------------------------- ---------------------------
This applies to :ref:`one-to-one <one-to-one>` and :ref:`many-to-one <many-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.
Take the following example:
.. 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_last_name:last_name)" \ -d "select=title,...directors(director_first_name:first_name, director_last_name:last_name)" \
-d "title=like.*Workers*" -d "title=like.*Workers*"
.. code-block:: json .. code-block:: json
@@ -1178,48 +1174,22 @@ Take the following example:
[ [
{ {
"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 ``"directors"`` object. Also the embed columns can be aliased normally. 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.
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:
.. code-block:: bash
# curl "http://localhost:3000/films?select=title,actors:roles(character,...actors(first_name,last_name))&title=like.*Lighthouse*"
curl --get "http://localhost:3000/films" \
-d "select=title,actors:roles(character,...actors(first_name,last_name))" \
-d "title=like.*Lighthouse*"
.. code-block:: json
[
{
"title": "The Lighthouse",
"actors": [
{
"character": "Thomas Wake",
"first_name": "Willem",
"last_name": "Dafoe"
}
]
}
]
.. _spread_to_many_embed: .. _spread_to_many_embed:
Spread To-Many relationships Spread To-Many relationships
---------------------------- ----------------------------
The spread columns in :ref:`one-to-many <one-to-many>` or :ref:`many-to-many <many-to-many>` relationships will show the data in arrays. 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 -g "http://localhost:3000/directors?select=first_name,...films(film_titles:title,film_years:year)&first_name=like.Quentin*"
curl --get "http://localhost:3000/directors" \ curl --get "http://localhost:3000/directors" \
-d "select=first_name,...films(film_titles:title,film_years:year)" \ -d "select=first_name,...films(film_titles:title,film_years:year)" \
-d "first_name=like.Quentin*" -d "first_name=like.Quentin*"
@@ -1240,16 +1210,17 @@ The spread columns in :ref:`one-to-many <one-to-many>` or :ref:`many-to-many <ma
} }
] ]
Note that there is no ``films`` array of objects. 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.
By default, the order of the values inside the resulting array is unspecified but `it is safe to assume <https://www.postgresql.org/message-id/15950.1491843689%40sss.pgh.pa.us>`_ that all the columns return the values in the same unspecified order. Order in spread to-many
From the previous result, we can say that "Pulp Fiction" premiered in 1994 and "Reservoir Dogs" in 1992. ~~~~~~~~~~~~~~~~~~~~~~~
You can still order all the resulting arrays explicitly. For example, to order by the release year:
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 .. code-block:: bash
# curl -g "http://localhost:3000/directors?select=first_name,...films(film_titles:title,film_years:year)&first_name=like.Quentin*&films.order=year"
curl --get "http://localhost:3000/directors" \ curl --get "http://localhost:3000/directors" \
-d "select=first_name,...films(film_titles:title,film_years:year)" \ -d "select=first_name,...films(film_titles:title,film_years:year)" \
-d "first_name=like.Quentin*" \ -d "first_name=like.Quentin*" \
@@ -1271,14 +1242,37 @@ You can still order all the resulting arrays explicitly. For example, to order b
} }
] ]
Nesting Spreads .. warning::
~~~~~~~~~~~~~~~
For example, let's nest ``...technical_specs`` (one-to-one) and ``...roles`` (one-to-many) inside ``...films``: Aliasing spreaded columns is recommended since JSON allows duplicate keys. Example:
.. code-block:: bash .. code-block:: bash
# curl -g "http://localhost:3000/directors?select=first_name,...films(film_titles:title,film_years:year,...technical_specs(film_runtimes:runtime),...roles(film_characters:character))&first_name=like.Quentin*&films.order=year&films.roles.order=character" 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" \ 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 "select=first_name,...films(film_titles:title,film_years:year,...technical_specs(film_runtimes:runtime),...roles(film_characters:character))" \
@@ -1310,6 +1304,36 @@ For example, let's nest ``...technical_specs`` (one-to-one) and ``...roles`` (on
} }
] ]
All the elements inside ``films`` are selected in the same order, including both nested resources. Note that:
For example, we can say that "Reservoir Dogs" premiered in 1992, its runtime is 1:39:00 and it has the following characters: ``[ "Mr. Pink", "Mr. White" ]``.
Note that the data inside to-many nested resources can also be ordered (``roles`` by the ``character`` name in our example). - 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" \
-d "select=title,actors:roles(character,...actors(first_name,last_name))" \
-d "title=like.*Lighthouse*"
.. code-block:: json
[
{
"title": "The Lighthouse",
"actors": [
{
"character": "Thomas Wake",
"first_name": "Willem",
"last_name": "Dafoe"
}
]
}
]
+25 -8
View File
@@ -175,23 +175,29 @@ To ensure best performance on larger data sets, an `appropriate index <https://w
Full-Text Search Full-Text Search
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
The :code:`fts` filter mentioned above has a number of options to support flexible textual queries, namely the choice of plain vs phrase search and the language used for stemming. Suppose that :code:`tsearch` is a table with column :code:`my_tsv`, of type `tsvector <https://www.postgresql.org/docs/current/datatype-textsearch.html>`_. The following examples illustrate the possibilities. 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 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 "http://localhost:3000/tsearch?my_tsv=fts(french).amusant" curl --get "http://localhost:3000/people" \
-d "my_tsv=fts(french).amusant"
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3000/tsearch?my_tsv=plfts.The%20Fat%20Cats" curl --get "http://localhost:3000/people" \
-d "my_tsv=plfts.The%20Fat%20Cats"
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3000/tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats" curl --get "http://localhost:3000/people" \
-d "my_tsv=not.phfts(english).The%20Fat%20Cats"
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3000/tsearch?my_tsv=not.wfts(french).amusant" curl --get "http://localhost:3000/people" \
-d "my_tsv=not.wfts(french).amusant"
.. _fts_to_tsvector: .. _fts_to_tsvector:
@@ -199,15 +205,26 @@ 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>`_. 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 ``fts`` on ``text`` and ``json`` types out of the box, for example. This allows using the ``fts`` operator on ``text`` and ``json`` types out of the box.
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3000/tsearch?my_text_column=fts(french).amusant" curl --get "http://localhost:3000/people" \
-d "my_text_column=fts(french).amusant"
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3000/tsearch?my_json_column=not.phfts(english).The%20Fat%20Cats" curl --get "http://localhost:3000/people" \
-d "my_json_column=not.phfts(english).The%20Fat%20Cats"
.. important::
To ensure this operation is fast, you need to create an index on the expression:
.. code-block:: postgres
CREATE INDEX idx_people_col ON people
USING GIN (to_tsvector('french', my_text_column));
.. _v_filter: .. _v_filter:
+1 -1
View File
@@ -14,7 +14,7 @@ Custom Queries
The PostgREST URL grammar limits the kinds of queries clients can perform. It prevents arbitrary, potentially poorly constructed and slow client queries. It's good for quality of service, but means database administrators must create custom views and functions to provide richer endpoints. The most common causes for custom endpoints are The PostgREST URL grammar limits the kinds of queries clients can perform. It prevents arbitrary, potentially poorly constructed and slow client queries. It's good for quality of service, but means database administrators must create custom views and functions to provide richer endpoints. The most common causes for custom endpoints are
* Table unions * SET operators like `UNION, INTERSECT and EXCEPT <https://www.postgresql.org/docs/current/queries-union.html>`_.
* More complicated joins than those provided by :ref:`resource_embedding`. * More complicated joins than those provided by :ref:`resource_embedding`.
* Geo-spatial queries that require an argument, like "points near (lat,lon)" * Geo-spatial queries that require an argument, like "points near (lat,lon)"
+92 -38
View File
@@ -31,7 +31,7 @@ The authenticator role is used for connecting to the database and should be conf
.. _user_impersonation: .. _user_impersonation:
User Impersonation User Impersonation
------------------ ~~~~~~~~~~~~~~~~~~
The picture below shows how the server handles authentication. If auth succeeds, it switches into the user role specified by the request, otherwise it switches into the anonymous role (if it's set in :ref:`db-anon-role`). The picture below shows how the server handles authentication. If auth succeeds, it switches into the user role specified by the request, otherwise it switches into the anonymous role (if it's set in :ref:`db-anon-role`).
@@ -43,12 +43,13 @@ This role switching mechanism is called **user impersonation**. In PostgreSQL it
The impersonated roles will have their settings applied. See :ref:`impersonated_settings`. The impersonated roles will have their settings applied. See :ref:`impersonated_settings`.
.. _jwt_impersonation: .. _jwt_auth:
JWT-Based User Impersonation JWT Authentication
---------------------------- ------------------
We use `JSON Web Tokens <https://jwt.io/>`_ to authenticate API requests, this allows us to be stateless and not require database lookups for verification. As you'll recall a JWT contains a list of cryptographically signed claims. All claims are allowed but PostgREST cares specifically about a claim called role. We use `JSON Web Tokens <https://datatracker.ietf.org/doc/html/rfc7519/>`_ to authenticate API requests, this allows us to be stateless and not require database lookups for verification.
As you'll recall a JWT contains a list of cryptographically signed claims. All claims are allowed but PostgREST cares specifically about a claim called role (configurable with :ref:`jwt_role_extract`).
.. code:: json .. code:: json
@@ -72,17 +73,10 @@ Note that the database administrator must allow the authenticator role to switch
If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role. The database administrator must set the anonymous role permissions correctly to prevent anonymous users from seeing or changing things they shouldn't. If the client included no JWT (or one without a role claim) then PostgREST switches into the anonymous role. The database administrator must set the anonymous role permissions correctly to prevent anonymous users from seeing or changing things they shouldn't.
.. _jwt_generation: .. _bearer_auth:
JWT Generation Bearer Authentication
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~
You can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_jwt`).
.. _client_auth:
Client Auth
~~~~~~~~~~~
To make an authenticated request the client must include an :code:`Authorization` HTTP header with the value :code:`Bearer <jwt>`. For instance: To make an authenticated request the client must include an :code:`Authorization` HTTP header with the value :code:`Bearer <jwt>`. For instance:
@@ -93,24 +87,29 @@ To make an authenticated request the client must include an :code:`Authorization
The ``Bearer`` header value can be used with or without capitalization(``bearer``). The ``Bearer`` header value can be used with or without capitalization(``bearer``).
.. _jwt_caching: .. _jwt_generation:
JWT Caching JWT Generation
----------- ~~~~~~~~~~~~~~
PostgREST validates ``JWTs`` on every request. We can cache ``JWTs`` to avoid this performance overhead. 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`).
To enable JWT caching, the config :code:`jwt-cache-max-lifetime` is to be set. It is the maximum number of seconds for which the cache stores the JWT validation results. The cache uses the :code:`exp` claim to set the cache entry lifetime. If the JWT does not have an :code:`exp` claim, it uses the config value. See :ref:`jwt-cache-max-lifetime` for more details. .. _jwt_signature:
.. note:: JWT Signature Verification
--------------------------
You can use the :ref:`server-timing_header` to see the effect of JWT caching. PostgREST supports both symmetric and asymmetric keys for verifying the signature of the token.
Symmetric Keys Symmetric Keys
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~
Each token is cryptographically signed with a secret key. In the case of symmetric cryptography the signer and verifier share the same secret passphrase, which can be configured with :ref:`jwt-secret`. In the case of symmetric cryptography the signer and verifier share the same secret passphrase, which can be configured with :ref:`jwt-secret`.
If it is set to a simple string value like “reallyreallyreallyreallyverysafe” then PostgREST interprets it as an HMAC-SHA256 passphrase. If it is set to a simple string then PostgREST interprets it as an HMAC-SHA256 passphrase.
.. code-block:: ini
jwt-secret = "reallyreallyreallyreallyverysafe"
.. _asym_keys: .. _asym_keys:
@@ -156,26 +155,39 @@ 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"
``kid`` verification
^^^^^^^^^^^^^^^^^^^^
PostgREST has built-in verification of the `key ID parameter <https://www.rfc-editor.org/rfc/rfc7517#section-4.5>`_, useful when working with a JSON Web Key Set.
It goes as follows:
- If the JWT contains a ``kid`` parameter, then PostgREST will look for the JSON Web Key in the :ref:`jwt-secret`.
+ If no key has a matching ``kid`` (or if they don't have one defined), the token will be rejected with a :ref:`401 Unauthorized <pgrst301>` error.
+ If a key matches the ``kid`` value then it will validate the token against that key accordingly.
- If the JWT doesn't have a ``kid``, PostgREST will try each key in the :ref:`jwt-secret` one by one until it finds one that works.
.. _jwt_claims_validation: .. _jwt_claims_validation:
JWT Claims Validation JWT Claims Validation
~~~~~~~~~~~~~~~~~~~~~ ---------------------
PostgREST honors the following `JWT claims <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4>`_: Time-Based claims validation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The time-based JWT claims specified in `RFC 7519 <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4>`_ are validated:
- ``exp`` Expiration Time - ``exp`` Expiration Time
- ``iat`` Issued At - ``iat`` Issued At
- ``nbf`` Not Before - ``nbf`` Not Before
- ``aud`` :ref:`Audience <jwt_aud_validation>`
.. note:: We allow a 30-second clock skew when validating the above claims. In other words, we give an extra 30 seconds before the JWT is rejected if there is a slight discrepancy in the timestamps.
PostgREST allows for a 30-second clock skew when validating the ``exp``, ``iat`` and ``nbf`` claims.
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:
JWT ``aud`` Claim Validation ``aud`` validation
^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ~~~~~~~~~~~~~~~~~~
PostgREST has built-in validation of the `JWT audience claim <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3>`_. 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: It works this way:
@@ -188,12 +200,31 @@ It works this way:
+ 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 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. + 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_claim_key_extract: .. _jwt_caching:
JWT Role Claim Key Extraction JWT Cache
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ---------
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, Microsoft Entra or Keycloak. JWT signature validation (specially :ref:`asym_keys` such as RSA) is slow, we can cache ``JWT`` validation results to avoid this performance overhead.
The JWT cache is bounded and uses the `SIEVE algorithm <https://cachemon.github.io/SIEVE-website>`_ for efficient eviction. The cache is enabled by default and can be configured with :ref:`jwt-cache-max-entries`.
It's recommended to leave the JWT cache enabled as our load tests indicate ~20% more throughput for simple GET requests when using it. This while reducing CPU utilization in exchange for a bit more memory.
:ref:`jwt_cache_metrics` are available.
.. note::
- If the ``jwt-secret`` is changed and the config is reloaded, the JWT cache will reset.
- JWTs that pass :ref:`jwt_signature` are cached, regardless if they pass :ref:`jwt_claims_validation`. We do this to ensure responses stays fast under common failure cases (such as expired JWTs).
- You can use the :ref:`server-timing_header` to see the peformance benefit of JWT caching.
.. _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: The DSL follows the `JSONPath <https://goessner.net/articles/JsonPath/>`_ expression grammar with extended string comparison operators. Supported operators are:
@@ -224,9 +255,12 @@ Usage examples:
jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]" jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]"
jwt-role-claim-key = ".postgrest.roles[?(@ *== \"utho\")]" 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
~~~~~~~~~~~~ ------------
There are at least three types of common critiques against using JWT: 1) against the standard itself, 2) against using libraries with known security vulnerabilities, and 3) against using JWT for web sessions. We'll briefly explain each critique, how PostgREST deals with it, and give recommendations for appropriate user action. There are at least three types of common critiques against using JWT: 1) against the standard itself, 2) against using libraries with known security vulnerabilities, and 3) against using JWT for web sessions. We'll briefly explain each critique, how PostgREST deals with it, and give recommendations for appropriate user action.
@@ -262,3 +296,23 @@ doing custom logic based on the web user info.
END IF; END IF;
END END
$$ LANGUAGE plpgsql; $$ LANGUAGE plpgsql;
.. raw:: html
<script type="text/javascript">
let hash = window.location.hash;
const redirects = {
'#jwt-based-user-impersonation': '#jwt-authentication',
'#client-auth': '#bearer-authentication',
'#jwt-caching': '#jwt-cache',
'#jwk-kid-validation': '#kid-verification',
'#jwt-aud-claim-validation': '#aud-validation',
};
let willRedirectTo = redirects[hash];
if (willRedirectTo) {
window.location.href = willRedirectTo;
}
</script>
+46 -8
View File
@@ -3,23 +3,47 @@
CLI CLI
=== ===
PostgREST provides a CLI with the commands listed below: PostgREST provides a CLI with the options listed below:
.. code:: text
Usage: postgrest [-v|--version] [-e|--example] [--dump-config | --dump-schema | --ready]
[FILENAME]
PostgREST / create a REST API to an existing Postgres
database
Available options:
-h,--help Show this help text
-v,--version Show the version information
-e,--example Show an example configuration file
--dump-config Dump loaded configuration and exit
--dump-schema Dump loaded schema as JSON and exit (for debugging,
output structure is unstable)
--ready Checks the health of PostgREST by doing a request on
the admin server /ready endpoint
FILENAME Path to configuration file
FILENAME
--------
Runs PostgREST with the given :ref:`file_config`.
Help Help
---- ----
.. code:: bash .. code:: bash
$ postgrest [-h|--help] $ postgrest --help
Shows all the commands available. Shows all the options available.
Version Version
------- -------
.. code:: bash .. code:: bash
$ postgrest [-v|--version] $ postgrest --version
Prints the PostgREST version. Prints the PostgREST version.
@@ -28,16 +52,16 @@ Example
.. code:: bash .. code:: bash
$ postgrest [-e|--example] $ postgrest --example
Shows example configuration options. Shows example configuration settings.
Dump Config Dump Config
----------- -----------
.. code:: bash .. code:: bash
$ postgrest [--dump-config] $ postgrest --dump-config
Dumps the loaded :ref:`configuration` values, considering the configuration file, environment variables and :ref:`in_db_config`. Dumps the loaded :ref:`configuration` values, considering the configuration file, environment variables and :ref:`in_db_config`.
@@ -46,6 +70,20 @@ Dump Schema
.. code:: bash .. code:: bash
$ postgrest [--dump-schema] $ postgrest --dump-schema
Dumps the schema cache in JSON format. Dumps the schema cache in JSON format.
Ready Flag
----------
Makes a request to the ``/ready`` endpoint of the :ref:`admin_server`. It exits with a return code of ``0`` on success and ``1`` on failure.
.. code-block:: bash
$ postgrest --ready
OK: http://localhost:3001/ready
.. note::
The ``--ready`` flag cannot be used when :ref:`server-host` is configured with special hostnames. We suggest to change it to ``localhost``.
+17 -22
View File
@@ -315,6 +315,10 @@ 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
@@ -405,7 +409,7 @@ db-pool-max-idletime
**In-Database** `n/a` **In-Database** `n/a`
=============== ================================= =============== =================================
*For backwards compatibility, this config parameter is also available as db-pool-timeout.* *For backwards compatibility, this config parameter is also available as "db-pool-timeout".*
Time in seconds to close idle pool connections. Time in seconds to close idle pool connections.
@@ -599,7 +603,7 @@ 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 an audience for the JWT ``aud`` claim. See :ref:`jwt_aud`.
.. _jwt-role-claim-key: .. _jwt-role-claim-key:
@@ -616,7 +620,7 @@ 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_claim_key_extract` on how to specify key paths and usage examples. See :ref:`jwt_role_extract` on how to specify key paths and usage examples.
.. _jwt-secret: .. _jwt-secret:
@@ -654,20 +658,20 @@ jwt-secret-is-base64
When this is set to :code:`true`, the value derived from :code:`jwt-secret` will be treated as a base64 encoded secret. When this is set to :code:`true`, the value derived from :code:`jwt-secret` will be treated as a base64 encoded secret.
.. _jwt-cache-max-lifetime: .. _jwt-cache-max-entries:
jwt-cache-max-lifetime jwt-cache-max-entries
---------------------- ----------------------
=============== ================================= =============== =================================
**Type** Int **Type** Int
**Default** 0 **Default** 1000
**Reloadable** Y **Reloadable** Y
**Environment** PGRST_JWT_CACHE_MAX_LIFETIME **Environment** PGRST_JWT_CACHE_MAX_ENTRIES
**In-Database** pgrst.jwt_cache_max_lifetime **In-Database** pgrst.jwt_cache_max_entries
=============== ================================= =============== =================================
Maximum number of seconds of lifetime for cached entries. The default :code:`0` disables caching. See :ref:`jwt_caching`. Maximum number of entries in JWT cache. The value :code:`0` disables JWT caching. See :ref:`jwt_caching`.
.. _log-level: .. _log-level:
@@ -710,23 +714,14 @@ log-query
--------- ---------
=============== ================================= =============== =================================
**Type** String **Type** Boolean
**Default** "disabled" **Default** False
**Reloadable** Y **Reloadable** Y
**Environment** PGRST_LOG_QUERY **Environment** PGRST_LOG_QUERY
**In-Database** `n/a` **In-Database** `n/a`
=============== ================================= =============== =================================
Logs the SQL query for the corresponding request at the current :ref:`log-level`. Logs the SQL query for the corresponding request at the current :ref:`log-level`. See :ref:`sql_query_logs`.
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:
@@ -890,7 +885,7 @@ server-timing-enabled
**In-Database** pgrst.server_timing_enabled **In-Database** pgrst.server_timing_enabled
=============== ================================= =============== =================================
Enables the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>`_ header. Enables the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing>`_ header.
See :ref:`server-timing_header`. See :ref:`server-timing_header`.
.. _server-unix-socket: .. _server-unix-socket:
+2 -2
View File
@@ -15,7 +15,7 @@ Dynamic Connection Pool
To conserve system resources, PostgREST uses a dynamic connection pool. This enables the number of connections in the pool to increase and decrease depending on request traffic. To conserve system resources, PostgREST uses a dynamic connection pool. This enables the number of connections in the pool to increase and decrease depending on request traffic.
- If all the connections are being used, a new connection is added. The pool can grow until it reaches the :ref:`db-pool` size. Note that its pointless to set this higher than the ``max_connections`` setting in your database. - If all the connections are being used, a new connection is added. The pool can grow until it reaches the :ref:`db-pool` size. Note that it's pointless to set this higher than the ``max_connections`` setting in your database.
- If a connection is unused for a period of time (:ref:`db-pool-max-idletime`), it will be released. - If a connection is unused for a period of time (:ref:`db-pool-max-idletime`), it will be released.
- For connecting to the database, the :ref:`authenticator <roles>` role is used. You can configure this using :ref:`db-uri`. - For connecting to the database, the :ref:`authenticator <roles>` role is used. You can configure this using :ref:`db-uri`.
@@ -106,4 +106,4 @@ Also set :ref:`db-channel-enabled` to ``false`` since ``LISTEN`` is not compatib
.. note:: .. note::
Its not recommended to use an external connection pooler. `Our benchmarks <https://github.com/PostgREST/postgrest/issues/2294#issuecomment-1139148672>`_ indicate it provides much lower performance than PostgREST built-in pool. It's not recommended to use an external connection pooler. `Our benchmarks <https://github.com/PostgREST/postgrest/issues/2294#issuecomment-1139148672>`_ indicate it provides much lower performance than PostgREST built-in pool.
+5 -1
View File
@@ -267,6 +267,10 @@ Related to the HTTP request elements.
| | | implemented. | | | | implemented. |
| PGRST127 | | | | PGRST127 | | |
+---------------+-------------+-------------------------------------------------------------+ +---------------+-------------+-------------------------------------------------------------+
| .. _pgrst128: | 400 | ``max-affected`` preference is violated with ``RPC`` call. |
| | | See :ref:`prefer_max_affected`. |
| PGRST128 | | |
+---------------+-------------+-------------------------------------------------------------+
.. _pgrst2**: .. _pgrst2**:
@@ -325,7 +329,7 @@ Related to the authentication process using JWT. You can follow the :ref:`tut1`
| 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:`bearer_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>` | | .. _pgrst303: | 401 | :ref:`JWT claims validation <jwt_claims_validation>` |
+4 -2
View File
@@ -4,7 +4,7 @@ Listener
######## ########
PostgREST uses `LISTEN <https://www.postgresql.org/docs/current/sql-listen.html>`_ to reload its :ref:`Schema Cache <schema_reloading_notify>` and :ref:`Configuration <config_reloading_notify>` via `NOTIFY <https://www.postgresql.org/docs/current/sql-notify.html>`_. PostgREST uses `LISTEN <https://www.postgresql.org/docs/current/sql-listen.html>`_ to reload its :ref:`Schema Cache <schema_reloading_notify>` and :ref:`Configuration <config_reloading_notify>` via `NOTIFY <https://www.postgresql.org/docs/current/sql-notify.html>`_.
This is useful in environments where you cant send SIGUSR1 or SIGUSR2 Unix Signals. This is useful in environments where you can't send SIGUSR1 or SIGUSR2 Unix Signals.
Like on cloud managed containers or on Windows systems. Like on cloud managed containers or on Windows systems.
.. code:: postgresql .. code:: postgresql
@@ -46,7 +46,9 @@ This will cause the :ref:`connection_pool` to connect to the read replica host a
.. note:: .. note::
Under the hood, PostgREST forces `target_session_attrs=read-write <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ for the ``LISTEN`` session. - Under the hood, PostgREST forces `target_session_attrs=read-write <https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNECT-TARGET-SESSION-ATTRS>`_ for the ``LISTEN`` session.
So if you specify ``target_session_attrs=read-only`` as mentioned above, PostgREST will override it for the ``LISTEN``.
- ``read-only`` is only available on libpq >= 14, if you use a lower version you will get an error like ``invalid target_session_attrs value: \"read-only\"``.
.. _listener_automatic_recovery: .. _listener_automatic_recovery:
+44 -7
View File
@@ -52,14 +52,12 @@ For diagnostic information about the server itself, PostgREST logs to ``stderr``
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``. To log the SQL queries executed for a request, set the :ref:`log-query` to ``true``. It will be logged based on the current :ref:`log-level` setting.
It will be logged based on the current :ref:`log-level` setting.
For example, with this configuration:
.. code-block:: bash .. code-block:: bash
log-level = "warn" log-level = "warn"
log-query = "main-query" log-query = "true"
The SQL queries will only be logged on ``400`` HTTP errors and up. The SQL queries will only be logged on ``400`` HTTP errors and up.
So, if the user requests a resource without sufficient privileges: So, if the user requests a resource without sufficient privileges:
@@ -122,12 +120,17 @@ Restart the database and watch the log file in real-time to understand how HTTP
Metrics Metrics
======= =======
The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in `Prometheus text format <https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format>`_. The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in `Prometheus text format <https://prometheus.io/docs/instrumenting/exposition_formats/#prometheus-text-format>`_.
.. code-block:: bash .. code-block:: bash
curl "http://localhost:3001/metrics" curl "http://localhost:3001/metrics"
.. code-block:: http
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
# HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load # HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load
# TYPE pgrst_schema_cache_query_time_seconds gauge # TYPE pgrst_schema_cache_query_time_seconds gauge
pgrst_schema_cache_query_time_seconds 1.5937927e-2 pgrst_schema_cache_query_time_seconds 1.5937927e-2
@@ -201,6 +204,40 @@ pgrst_db_pool_max
Max pool connections. Max pool connections.
.. _jwt_cache_metrics:
JWT Cache Metrics
-----------------
Metrics related to the :ref:`jwt_caching`.
pgrst_jwt_cache_requests_total
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
======== =======
**Type** Counter
======== =======
The total number of JWT cache lookups.
pgrst_jwt_cache_hits_total
~~~~~~~~~~~~~~~~~~~~~~~~~~
======== =======
**Type** Counter
======== =======
The total number of JWT cache hits.
pgrst_jwt_cache_evictions_total
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
======== =======
**Type** Counter
======== =======
The total number of JWT cache evictions.
Traces Traces
====== ======
@@ -246,7 +283,7 @@ See :ref:`proxy-status_header`.
Server-Timing Header Server-Timing Header
-------------------- --------------------
You can enable the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing>`_ header by setting :ref:`server-timing-enabled` on. You can enable the `Server-Timing <https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Server-Timing>`_ header by setting :ref:`server-timing-enabled` on.
This header communicates metrics of the different phases in the request-response cycle. This header communicates metrics of the different phases in the request-response cycle.
.. code-block:: bash .. code-block:: bash
@@ -260,7 +297,7 @@ This header communicates metrics of the different phases in the request-response
Server-Timing: jwt;dur=14.9, parse;dur=71.1, plan;dur=109.0, transaction;dur=353.2, response;dur=4.4 Server-Timing: jwt;dur=14.9, parse;dur=71.1, plan;dur=109.0, transaction;dur=353.2, response;dur=4.4
- All the durations (``dur``) are in milliseconds. - All the durations (``dur``) are in milliseconds.
- The ``jwt`` stage is when :ref:`jwt_impersonation` is done. This duration can be lowered with :ref:`jwt_caching`. - The ``jwt`` stage is when :ref:`jwt_auth` is done. This duration can be lowered with :ref:`jwt_caching`.
- On the ``parse`` stage, the :ref:`url_grammar` is parsed. - On the ``parse`` stage, the :ref:`url_grammar` is parsed.
- On the ``plan`` stage, the :ref:`schema_cache` is used to generate the :ref:`main_query` of the transaction. - On the ``plan`` stage, the :ref:`schema_cache` is used to generate the :ref:`main_query` of the transaction.
- The ``transaction`` stage corresponds to the database transaction. See :ref:`transactions`. - The ``transaction`` stage corresponds to the database transaction. See :ref:`transactions`.
+13
View File
@@ -53,6 +53,19 @@ To reload the schema cache from within the database, you can use the ``NOTIFY``
NOTIFY pgrst, 'reload schema' NOTIFY pgrst, 'reload schema'
Debouncing
~~~~~~~~~~
PostgREST does not reload the schema cache for each notification when several ``NOTIFY pgrst`` events are generated quickly after one another.
There are two cases to consider: when notifications are sent within a single transaction and when they are sent across multiple transactions.
In the first case, PostgreSQL deduplicates identical ``NOTIFY`` events within the same transaction. This means that even if multiple ``NOTIFY pgrst`` statements are executed before a ``COMMIT``, only a single notification is delivered to PostgREST.
In the second case, when notifications are sent from separate transactions in a short time span, PostgREST applies a debouncing mechanism to avoid excessive schema cache reloads.
Instead of reloading the schema cache for each notification, events are grouped within a small time window of 100 milliseconds. The reload function is executed once immediately when the first notification is received and once more after the burst of events settles, resulting in at most two executions within that time window.
.. _auto_schema_reloading: .. _auto_schema_reloading:
Automatic Schema Cache Reloading Automatic Schema Cache Reloading
+2 -2
View File
@@ -1,7 +1,7 @@
# This file is auto-generated by postgrest-nixpkgs-upgrade # This file is auto-generated by postgrest-nixpkgs-upgrade
sphinx==7.4.7 sphinx==8.2.3
sphinx-copybutton==0.5.2 sphinx-copybutton==0.5.2
sphinx-rtd-dark-mode==1.3.0 sphinx-rtd-dark-mode==1.3.0
sphinx-rtd-theme==2.0.0 sphinx-rtd-theme==3.0.2
sphinx-tabs==3.4.7 sphinx-tabs==3.4.7
sphinxext-opengraph==0.9.1 sphinxext-opengraph==0.9.1
+33 -11
View File
@@ -52,17 +52,31 @@ 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." Go to `jwt.io <https://jwt.io/#debugger-io>`_ and fill in the fields like this: 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``.
.. figure:: ../_static/tuts/tut1-jwt-io.png .. code:: bash
:alt: jwt.io interface
How to create a token at https://jwt.io #!/bin/bash
set -e
**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. JWT_SECRET='test_secret_that_is_at_least_32_characters_long'
_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::
@@ -145,14 +159,22 @@ 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;
Go back to jwt.io and change the payload to Or in ``bash``:
.. code-block:: json
{ .. code-block:: bash
"role": "todo_user",
"exp": 123456789 exp=$(( EPOCHSECONDS + 5*60 )) # five minutes
}
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
+4 -4
View File
@@ -2,16 +2,16 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1731165248, "lastModified": 1752006229,
"narHash": "sha256-DiHFKIdBmMx5/DUARhVqaxvEIiy4EE6Eqs9Qs4oxme8=", "narHash": "sha256-BeuAPwNM2RBc5bvUTb0j4GRs2yBkDeRCw/8Y3v9Xesc=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "a90280100f41a10914edfe729a4053e60c92b8e3", "rev": "c80edd02003fe3d8af527215a3ac069be9cfd47f",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nixos", "owner": "nixos",
"ref": "nixpkgs-unstable", "ref": "nixpkgs-25.05-darwin",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
+4 -4
View File
@@ -2,7 +2,7 @@
description = "REST API for any Postgres database"; description = "REST API for any Postgres database";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-25.05-darwin";
}; };
nixConfig = { nixConfig = {
@@ -33,8 +33,8 @@
in in
{ {
packages = genSystems (attrs: { packages = genSystems (attrs: {
default = attrs.postgrestPackage; default = attrs.postgrestPackage.bin;
profiled = attrs.postgrestProfiled; profiled = attrs.postgrestProfiled.bin;
} // nixpkgs.lib.optionalAttrs (attrs ? postgrestStatic) { } // nixpkgs.lib.optionalAttrs (attrs ? postgrestStatic) {
static = attrs.postgrestStatic; static = attrs.postgrestStatic;
}); });
@@ -42,7 +42,7 @@
apps = genSystems (attrs: { apps = genSystems (attrs: {
default = { default = {
type = "app"; type = "app";
program = "${attrs.postgrestStatic or attrs.postgrestPackage}/bin/postgrest"; program = "${attrs.postgrestStatic or attrs.postgrestPackage.bin}/bin/postgrest";
meta.description = "REST API for any Postgres database"; meta.description = "REST API for any Postgres database";
}; };
}); });
+15 -15
View File
@@ -72,9 +72,10 @@ 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-parallel-curl
postgrest-check postgrest-push-cachix postgrest-check postgrest-profiled-run
postgrest-clean postgrest-release postgrest-clean postgrest-push-cachix
postgrest-commitlint postgrest-release
postgrest-coverage postgrest-repl postgrest-coverage postgrest-repl
postgrest-coverage-draft-overlay postgrest-run postgrest-coverage-draft-overlay postgrest-run
postgrest-docs-build postgrest-style postgrest-docs-build postgrest-style
@@ -90,15 +91,14 @@ postgrest-gen-ctags postgrest-watch
postgrest-gen-jwt postgrest-with-all postgrest-gen-jwt postgrest-with-all
postgrest-gen-secret postgrest-with-git postgrest-gen-secret postgrest-with-git
postgrest-git-hooks postgrest-with-pgrst postgrest-git-hooks postgrest-with-pgrst
postgrest-hsie-graph-modules postgrest-with-postgresql-12 postgrest-hsie-graph-modules postgrest-with-pg-13
postgrest-hsie-graph-symbols postgrest-with-postgresql-13 postgrest-hsie-graph-symbols postgrest-with-pg-14
postgrest-hsie-minimal-imports postgrest-with-postgresql-14 postgrest-hsie-minimal-imports postgrest-with-pg-15
postgrest-lint postgrest-with-postgresql-15 postgrest-lint postgrest-with-pg-16
postgrest-loadtest postgrest-with-postgresql-16 postgrest-loadtest postgrest-with-pg-17
postgrest-loadtest-against postgrest-with-postgresql-17 postgrest-loadtest-against postgrest-with-slow-pg
postgrest-loadtest-report postgrest-with-slow-pg postgrest-loadtest-report postgrest-with-slow-postgrest
postgrest-nixpkgs-upgrade postgrest-with-slow-postgrest postgrest-nixpkgs-upgrade
postgrest-parallel-curl
... ...
[nix-shell]$ [nix-shell]$
@@ -174,7 +174,7 @@ $ nix-shell --run "postgrest-with-all postgrest-test-spec"
# Run the tests against a specific version of PostgreSQL (use tab-completion in # Run the tests against a specific version of PostgreSQL (use tab-completion in
# nix-shell to see all available versions): # nix-shell to see all available versions):
$ nix-shell --run "postgrest-with-postgresql-13 postgrest-test-spec" $ nix-shell --run "postgrest-with-pg-13 postgrest-test-spec"
``` ```
@@ -284,7 +284,7 @@ Tools like `postgrest-build`, `postgrest-run`, `postgrest-repl` etc. are simple
also run in CI, with the exception of the IO and Memory checks that need to be run also run in CI, with the exception of the IO and Memory checks that need to be run
separately. separately.
`postgrest-with-postgresql-*` take a command as an argument and will run it `postgrest-with-pg-*` take a command as an argument and will run it
with a temporary database. `postgrest-with-all` will run the command against with a temporary database. `postgrest-with-all` will run the command against
all supported PostgreSQL versions. Tests run without `postgrest-with-*` are all supported PostgreSQL versions. Tests run without `postgrest-with-*` are
run against the latest PostgreSQL version by default. run against the latest PostgreSQL version by default.
@@ -380,7 +380,7 @@ 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 is taken from `flake.lock` and
can be updated with `nix flake update`. can be updated with `postgrest-nixpkgs-upgrade`.
### `shell.nix` ### `shell.nix`
-61
View File
@@ -1,61 +0,0 @@
# Creating a separate libpq package is is discussed in
# https://github.com/NixOS/nixpkgs/issues/61580, but nixpkgs has not moved
# forward, yet.
# This package is passed to postgresql-libpq (haskell) which needs to be
# cross-compiled to the static build and possibly other architectures as
# as well. To reduce the number of dependencies that need to be built with
# it, this derivation focuses on building the client libraries only. No
# server, no tests.
{ stdenv
, lib
, openssl
, zlib
, postgresql
, pkg-config
, tzdata
}:
stdenv.mkDerivation {
pname = "libpq";
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 = [
"--without-gssapi"
"--without-icu"
"--without-readline"
"--with-openssl"
"--with-system-tzdata=${tzdata}/share/zoneinfo"
"--sysconfdir=/etc/postgresql"
];
nativeBuildInputs = [ pkg-config tzdata ];
buildInputs = [ openssl zlib ];
buildFlags = [ "submake-libpq" "submake-libpgport" ];
installPhase = ''
runHook preInstall
make -C src/bin/pg_config install
make -C src/common install
make -C src/include install
make -C src/interfaces/libpq install
make -C src/port install
rm -rfv $out/share
runHook postInstall
'';
outputs = [ "out" ];
meta = with lib; {
homepage = "https://www.postgresql.org";
description = "Client API library for PostgreSQL";
license = licenses.postgresql;
};
}
@@ -6,6 +6,7 @@
, coreutils , coreutils
, git , git
, lib , lib
, moreutils
, runCommand , runCommand
, shellcheck , shellcheck
, stdenv , stdenv
@@ -56,7 +57,7 @@ let
# Example: This way `postgrest-watch -h` will return the help output for watch, while # Example: This way `postgrest-watch -h` will return the help output for watch, while
# `postgrest-watch postgrest-test-spec -h` will return the help output for test-spec. # `postgrest-watch postgrest-test-spec -h` will return the help output for test-spec.
# Taken from: https://github.com/matejak/argbash/issues/114#issuecomment-557108274 # Taken from: https://github.com/matejak/argbash/issues/114#issuecomment-557108274
sed '/_positionals_count + 1/a\\t\t\t\tset -- "''${@:1:1}" "--" "''${@:2}"' -i $out sed '/_positionals_count + 1/a\\t\t\t\tset -- "''${@:1:1}" "--" "''${@:2}"' $out | ${moreutils}/bin/sponge $out
''; '';
bash-completion = bash-completion =
@@ -66,7 +67,7 @@ let
'' ''
+ lib.optionalString (positionalCompletion != "") '' + lib.optionalString (positionalCompletion != "") ''
sed 's#COMPREPLY.*compgen -o bashdefault .*$#${escape positionalCompletion}#' -i $out sed 's#COMPREPLY.*compgen -o bashdefault .*$#${escape positionalCompletion}#' $out | ${moreutils}/bin/sponge $out
'' ''
); );
@@ -103,8 +104,7 @@ let
'' ''
+ lib.optionalString withTmpDir '' + lib.optionalString withTmpDir ''
mkdir -p "''${TMPDIR:-/tmp}/postgrest" tmpdir="$(${coreutils}/bin/mktemp -d --tmpdir ${name}-XXX)"
tmpdir="$(${coreutils}/bin/mktemp -d --tmpdir postgrest/${name}-XXX)"
# we keep the tmpdir when an error occurs for debugging # we keep the tmpdir when an error occurs for debugging
trap 'echo Temporary directory kept at: $tmpdir' ERR trap 'echo Temporary directory kept at: $tmpdir' ERR
-1
View File
@@ -3,6 +3,5 @@
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;
postgresql-libpq = import ./postgresql-libpq.nix;
slocat = import ./slocat.nix; slocat = import ./slocat.nix;
} }
+22 -22
View File
@@ -50,34 +50,34 @@ let
# jailbreak, because hspec limit for tests # jailbreak, because hspec limit for tests
fuzzyset = prev.fuzzyset_0_2_4; fuzzyset = prev.fuzzyset_0_2_4;
hasql-pool = lib.dontCheck (prev.callHackageDirect # TODO: Remove once available in nixpkgs haskellPackages
configurator-pg =
prev.callHackageDirect
{ {
pkg = "hasql-pool"; pkg = "configurator-pg";
ver = "1.0.1"; ver = "0.2.11";
sha256 = "sha256-Hf1f7lX0LWkjrb25SDBovCYPRdmUP1H6pAxzi7kT4Gg="; sha256 = "sha256-mtGtNawDJgz2ZIEVca+IYXVu4oNw9xsfJiYWAqAbbgc=";
} }
{ }); { };
hasql-notifications = lib.dontCheck (prev.callHackageDirect # TODO: Remove once available in nixpkgs haskellPackages
streaming-commons =
prev.callHackageDirect
{ {
pkg = "hasql-notifications"; pkg = "streaming-commons";
ver = "0.2.2.2"; ver = "0.2.3.1";
sha256 = "sha256-myKwlug7OgTa/qP6mHfCD+5Q8IhM17JvpJBfSo+M01k="; sha256 = "sha256-Gl2eaJcWe1sxmcE/octWlH9uSnERguf+5H66K4fV87s=";
} }
{ }); { };
# newer nixpkgs already has 0.10., so we fallback to default for forward compat # Downgrade hasql and related packages while we are still on GHC 9.4 for the static build.
jose-jwt = prev.jose-jwt_0_10_0 or prev.jose-jwt; hasql = lib.dontCheck (lib.doJailbreak prev.hasql_1_6_4_4);
hasql-dynamic-statements = lib.dontCheck prev.hasql-dynamic-statements_0_3_1_5;
postgresql-libpq = lib.dontCheck (prev.callHackageDirect hasql-implicits = lib.dontCheck prev.hasql-implicits_0_1_1_3;
{ hasql-notifications = lib.dontCheck prev.hasql-notifications_0_2_2_2;
pkg = "postgresql-libpq"; hasql-pool = lib.dontCheck prev.hasql-pool_1_0_1;
ver = "0.10.1.0"; hasql-transaction = lib.dontCheck prev.hasql-transaction_1_1_0_1;
sha256 = "sha256-tXOMqCO8opMilI9rx0D+njqjIjbZsH168Bzb8Aq8Ff4="; postgresql-binary = lib.dontCheck (lib.doJailbreak prev.postgresql-binary_0_13_1_3);
}
{
postgresql = super.libpq;
});
}; };
in in
{ {
-7
View File
@@ -1,7 +0,0 @@
_: super:
{
# Depending on which nixpkgs version is pinned, libpq might either be available already - or not.
libpq = super.libpq or (super.callPackage ../libpq.nix {
postgresql = super.postgresql_16;
});
}
+13 -53
View File
@@ -8,65 +8,25 @@ let
inherit (pkgs) pkgsStatic; inherit (pkgs) pkgsStatic;
inherit (pkgsStatic.haskell) lib; inherit (pkgsStatic.haskell) lib;
packagesStatic = packagesStatic = pkgsStatic.haskell.packages.native-bignum."${compiler}";
pkgsStatic.haskell.packages."${compiler}".override (old: {
ghc = pkgsStatic.pkgsBuildHost.haskell.compiler."${compiler}".override {
# Using the bundled libffi generally works better for cross-compiling
libffi = null;
# Building sphinx fails on some platforms
enableDocs = false;
# Cross compiling with native bignum works better than with gmp
enableNativeBignum = true;
};
overrides = pkgs.lib.composeExtensions old.overrides (_: prev: {
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
# derivation is built with static libraries anyway.
libraryPkgconfigDepends = [ pkgsStatic.libpq ];
librarySystemDepends = [ ];
}).overrideAttrs (_: prevAttrs: {
buildInputs = prevAttrs.buildInputs ++ [ pkgsStatic.openssl ];
});
});
});
makeExecutableStatic = drv: pkgs.lib.pipe drv [ makeExecutableStatic = drv: pkgs.lib.pipe drv [
lib.compose.justStaticExecutables lib.compose.justStaticExecutables
# To successfully compile a redistributable, fully static executable we need to: # To successfully compile a redistributable, fully static executable we need to:
# 1. make executable really statically linked. # 1. avoid any references to /nix/store to prevent blowing up the closure size.
# 2. avoid any references to /nix/store to prevent blowing up the closure size. (drv: drv.overrideAttrs {
# 3. be able to run the executable. allowedReferences = [
# When checking for references, we ignore the following: pkgsStatic.openssl.etc
# - eeee... are removed references which don't actually exist ];
# - openssl-etc references are purposely designed to be very small })
(lib.compose.overrideCabal (drv: {
postFixup = drv.postFixup + ''
exe="$out/bin/postgrest"
if ! (file "$exe" | grep 'statically linked') then # 2. be able to run the executable.
echo "not a static executable, ldd output:" (drv: drv.overrideAttrs {
ldd "$exe" passthru.tests.version = pkgsStatic.testers.testVersion {
exit 1 package = drv;
fi };
})
echo "Checking for references to /nix/store..."
(${pkgsStatic.binutils}/bin/strings "$exe" \
| grep -v /nix/store/eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee \
| grep -v -etc/etc/ssl \
| grep /nix/store || exit 0 && exit 1)
echo "No references to /nix/store found"
"$exe" --help
'';
}))
]; ];
in in
+16 -5
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[@]}"
''; '';
@@ -34,6 +33,17 @@ let
exec ${cabal-install}/bin/cabal v2-clean exec ${cabal-install}/bin/cabal v2-clean
''; '';
update =
checkedShellScript
{
name = "postgrest-cabal-update";
docs = "Update cabal's package list from hackage.haskell.org";
workingDir = "/";
}
''
exec ${cabal-install}/bin/cabal v2-update
'';
run = run =
checkedShellScript checkedShellScript
{ {
@@ -45,6 +55,7 @@ let
"ARG_USE_ENV([PGRST_DB_POOL], [1], [PostgREST pool size])" "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_DB_POOL_ACQUISITION_TIMEOUT], [1], [PostgREST pool timeout])"
"ARG_USE_ENV([PGRST_JWT_SECRET], [reallyreallyreallyreallyverysafe], [PostgREST JWT secret])" "ARG_USE_ENV([PGRST_JWT_SECRET], [reallyreallyreallyreallyverysafe], [PostgREST JWT secret])"
"ARG_USE_ENV([PGRST_ADMIN_SERVER_PORT], [3001], [PostgREST admin server port])"
"ARG_LEFTOVERS([PostgREST arguments])" "ARG_LEFTOVERS([PostgREST arguments])"
]; ];
workingDir = "/"; workingDir = "/";
@@ -55,6 +66,7 @@ let
export PGRST_DB_POOL export PGRST_DB_POOL
export PGRST_DB_POOL_ACQUISITION_TIMEOUT export PGRST_DB_POOL_ACQUISITION_TIMEOUT
export PGRST_JWT_SECRET export PGRST_JWT_SECRET
export PGRST_ADMIN_SERVER_PORT
exec ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \ exec ${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
postgrest "''${_arg_leftovers[@]}" postgrest "''${_arg_leftovers[@]}"
@@ -83,10 +95,8 @@ let
export PGRST_DB_POOL_ACQUISITION_TIMEOUT export PGRST_DB_POOL_ACQUISITION_TIMEOUT
export PGRST_JWT_SECRET export PGRST_JWT_SECRET
${cabal-install}/bin/cabal v2-update exec ${cabal-install}/bin/cabal --builddir="dist-prof" v2-run --enable-profiling --disable-shared exe:postgrest -- \
${cabal-install}/bin/cabal --builddir="dist-prof" v2-build --enable-profiling --disable-shared exe:postgrest +RTS -p -h -RTS "''${_arg_leftovers[@]}"
${cabal-install}/bin/cabal --builddir="dist-prof" v2-run -- \
postgrest +RTS -p -h -RTS "''${_arg_leftovers[@]}"
''; '';
repl = repl =
@@ -109,6 +119,7 @@ buildToolbox
inherit inherit
build build
clean clean
update
run run
runProfiled runProfiled
repl; repl;
+7 -2
View File
@@ -10,6 +10,7 @@
, hsie , hsie
, nix , nix
, silver-searcher , silver-searcher
, stdenv
, style , style
, tests , tests
, withTools , withTools
@@ -53,10 +54,14 @@ let
Requires authentication with `cachix authtoken ...`. Requires authentication with `cachix authtoken ...`.
''; '';
args =
[
"ARG_OPTIONAL_SINGLE([system], , [System], [${stdenv.system}])"
];
workingDir = "/"; workingDir = "/";
} }
'' ''
${nix}/bin/nix-instantiate \ ${nix}/bin/nix-instantiate --argstr system "$_arg_system" \
| xargs ${nix}/bin/nix-store -qR --include-outputs \ | xargs ${nix}/bin/nix-store -qR --include-outputs \
| ${cachix}/bin/cachix push postgrest | ${cachix}/bin/cachix push postgrest
''; '';
@@ -77,6 +82,7 @@ let
} }
'' ''
${tests}/bin/postgrest-test-spec ${tests}/bin/postgrest-test-spec
${tests}/bin/postgrest-test-observability
${tests}/bin/postgrest-test-doctests ${tests}/bin/postgrest-test-doctests
${tests}/bin/postgrest-test-io ${tests}/bin/postgrest-test-io
${tests}/bin/postgrest-test-big-schema ${tests}/bin/postgrest-test-big-schema
@@ -246,7 +252,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 \
+9
View File
@@ -35,6 +35,9 @@ let
workingDir = "/docs"; workingDir = "/docs";
} }
'' ''
# https://github.com/sphinx-doc/sphinx/issues/11739
export LC_ALL=C
function build() { function build() {
${python}/bin/sphinx-build --color -W -a -n . -b "$@" ${python}/bin/sphinx-build --color -W -a -n . -b "$@"
} }
@@ -119,6 +122,8 @@ let
workingDir = "/docs"; workingDir = "/docs";
} }
'' ''
export LC_ALL=C
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ') FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
# shellcheck disable=SC2086 disable=SC2016 # shellcheck disable=SC2086 disable=SC2016
@@ -139,6 +144,8 @@ let
workingDir = "/docs"; workingDir = "/docs";
} }
'' ''
export LC_ALL=C
FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ') FILES=$(find . -type f -iname '*.rst' | tr '\n' ' ')
tail -n+2 postgrest.dict \ tail -n+2 postgrest.dict \
@@ -157,6 +164,8 @@ let
workingDir = "/docs"; workingDir = "/docs";
} }
'' ''
export LC_ALL=C
${python}/bin/sphinx-build --color -b linkcheck . ../.docs-build ${python}/bin/sphinx-build --color -b linkcheck . ../.docs-build
''; '';
+101 -49
View File
@@ -1,63 +1,53 @@
# generates a file to be used by the vegeta load testing tool # generates a file to be used by the vegeta load testing tool
# It includes a worst case scenario for the JWT cache:
# - all requests will have a unique JWT so no cache hits
# - all jwts have an expiration that will be long enough to be
# valid at time of request but short enough that already
# validated jwts will expire later during the loadtest run
# - the above guarantees JWT cache purging will happen
# - we want this to track resource consumption in the worst case
# And a more normal scenario where non-expiring JWTs are picked
# from an array
import time import time
import hmac
import hashlib
import base64
import json
import argparse import argparse
import sys import sys
import random import random
import jwt
import jwcrypto.jwk as jwk
from typing import Optional
from pathlib import Path
SECRET = b"reallyreallyreallyreallyverysafe"
URL = "http://postgrest" URL = "http://postgrest"
JWT_DURATION = 120
TOTAL_TARGETS = 50000 # tuned by hand to reduce result variance secret_key = b"reallyreallyreallyreallyverysafe"
key = jwk.JWK.generate(kty="RSA", size=4096)
private_key = jwt.algorithms.RSAAlgorithm.from_jwk(key.export_private())
public_key = key.export_public()
def base64url_encode(data: bytes) -> str: def generate_jwt(now: int, exp_inc: Optional[int], is_hs: bool) -> str:
"""URL-safe Base64 encode without padding.""" """Generate an HS256 or RS256 JWT"""
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 = { payload = {
"sub": f"user_{random.getrandbits(32)}", "sub": f"user_{random.getrandbits(32)}",
"iat": now, "iat": now,
"exp": now + exp_inc,
"role": "postgrest_test_author", "role": "postgrest_test_author",
} }
# Encode to JSON and then to Base64URL if exp_inc is not None:
header_b = json.dumps(header, separators=(",", ":")).encode() payload["exp"] = now + exp_inc
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 k = secret_key if is_hs else private_key
signing_input = f"{header_b64}.{payload_b64}".encode() alg = "HS256" if is_hs else "RS256"
signature = hmac.new(SECRET, signing_input, hashlib.sha256).digest() return jwt.encode(payload, k, alg)
signature_b64 = base64url_encode(signature)
return f"{header_b64}.{payload_b64}.{signature_b64}"
# We want to ensure 401 Unauthorized responses don't happen during def append_targets(lines: list[str], token: str):
# JWT validation, this can happen when the jwt `exp` is too short. lines.append(f"OPTIONS {URL}/authors_only")
# At the same time, we want to ensure the `exp` is not too big, lines.append(f"Authorization: Bearer {token}")
# so expires will occur and postgREST will have to clean cached expired JWTs. lines.append("") # blank line to separate requests
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(): def main():
@@ -68,16 +58,77 @@ def main():
"output", "output",
help="Path to write the generated targets file", help="Path to write the generated targets file",
) )
parser.add_argument(
"--worst",
dest="worst",
action=argparse.BooleanOptionalAction,
default=False,
help="Generate worst case targets for a JWT cache",
)
parser.add_argument(
"--rsa",
dest="jwk_path",
metavar="JWK_PATH",
type=Path,
default=None,
help="Path for generating a RSA JWK file to sign tokens with",
)
args = parser.parse_args() args = parser.parse_args()
lines = [] is_hs = args.jwk_path is None
nsamples = 1000
if is_hs:
ntargets = 200000
else:
# The asymmetric targets take too long to compute so we reduce them
ntargets = 50000
if not is_hs:
try:
with open(args.jwk_path, "w") as jwk:
jwk.write(public_key)
print(f"Created {args.jwk_path} file containing the RSA JWK")
except IOError as e:
print(f"Error writing to {args.jwk_path}: {e}", file=sys.stderr)
sys.exit(1)
print(f"Generating {ntargets} targets...")
start_time = time.time() start_time = time.time()
for i in range(TOTAL_TARGETS): now = int(start_time)
token = generate_jwt(estimate_adequate_jwt_exp_increase(i))
lines.append(f"OPTIONS {URL}/authors_only") lines = []
lines.append(f"Authorization: Bearer {token}")
lines.append("") # blank line to separate requests # 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 needs to
# clean cached expired JWTs
if args.worst:
# estimated time takes to build and run postgrest itself
build_run_postgrest_time = 2
# estimated time it takes to generate the targets file
# the division numbers are tuned by hand
if is_hs: # hs generation is much faster
gen_time = ntargets // 66666
else: # asymmetric is slower so the time is higher
gen_time = ntargets // 220
# estimated exp time so some JWTs will expire
inc = build_run_postgrest_time + gen_time
for i in range(ntargets):
token = generate_jwt(now, inc + i // 1000, is_hs)
append_targets(lines, token)
else:
tokens = [generate_jwt(now, None, is_hs) for _ in range(nsamples)]
for i in range(ntargets):
token = random.choice(tokens)
append_targets(lines, token)
try: try:
with open(args.output, "w") as f: with open(args.output, "w") as f:
@@ -87,7 +138,8 @@ def main():
sys.exit(1) sys.exit(1)
elapsed = time.time() - start_time elapsed = time.time() - start_time
print(f"Created {TOTAL_TARGETS} targets in {args.output} ({elapsed:.2f}s)") print(f"Created {ntargets} targets", end=" ")
print(f"in {args.output} ({elapsed:.2f}s)")
if __name__ == "__main__": if __name__ == "__main__":
+60
View File
@@ -0,0 +1,60 @@
{ buildToolbox
, checkedShellScript
, commitlint
, writeText
}:
let
# Rules format: [<severity>, <"always"/"never">, <value>]
commitlintConfig = writeText "commitlint.config.mjs" ''
export default {
rules: {
"type-enum": [2, "always", [
'add', // Add a new feature
'amend', // To amend an unrealease commit
'change', // Breaking changes
'chore', // Update sponsors, changelog, readme etc
'ci', // CI configuration files and scripts
'docs', // Documentation
'fix', // Bug fix
'nix', // Related to Nix
'perf', // Performance improvements
'refactor', // Refactoring code
'remove', // Remove a feature or fix
'test', // Adding tests
]],
'subject-case': [2, 'never', ['pascal-case', 'start-case']],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'subject-max-length': [2, 'always', 80],
'subject-min-length': [2, 'always', 5],
'scope-case': [2, 'always', 'lower-case'],
'body-leading-blank': [2, 'always'],
},
};
'';
commitCheck =
checkedShellScript
{
name = "postgrest-commitlint";
docs = "Script to validate commit messages";
workingDir = "/";
args = [
"ARG_OPTIONAL_SINGLE([from],, [commit ref start from], [main])"
"ARG_OPTIONAL_SINGLE([to],, [commit ref end at], [HEAD])"
];
}
''
# Run commitlint with the given configuration
${commitlint}/bin/commitlint --config ${commitlintConfig} --from "$_arg_from" --to "$_arg_to"
'';
in
buildToolbox
{
name = "postgrest-commitlint";
tools = { inherit commitCheck; };
}
+81 -19
View File
@@ -41,8 +41,9 @@ 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_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest], [mixed])"
"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,jwt])" "ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,jwt-hs,jwt-hs-cache,jwt-hs-cache-worst,jwt-rsa,jwt-rsa-cache,jwt-rsa-cache-worst])"
"ARG_OPTIONAL_SINGLE([monitor], [m], [Monitoring file], [./loadtest/result.csv])"
"ARG_LEFTOVERS([additional vegeta arguments])" "ARG_LEFTOVERS([additional vegeta arguments])"
]; ];
workingDir = "/"; workingDir = "/";
@@ -58,35 +59,67 @@ let
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_SECRET="reallyreallyreallyreallyverysafe"
# set previous PGRST_JWT_CACHE_MAX_LIFETIME configuration so that
# load test works across branches
# TODO clean once PGRST_JWT_CACHE_MAX_ENTRIES merged and released
export PGRST_JWT_CACHE_MAX_LIFETIME="86400" 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 case "$_arg_kind" in
jwt) jwt-hs)
${genTargetsHS} "$_arg_testdir"/gen_targets.http
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
export PGRST_JWT_CACHE_MAX_LIFETIME="0"
;;
${genTargets} "$_arg_testdir"/gen_targets.http jwt-hs-cache)
${genTargetsHS} "$_arg_testdir"/gen_targets.http
;;
# shellcheck disable=SC2145 jwt-hs-cache-worst)
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \ ${genTargetsHS} --worst "$_arg_testdir"/gen_targets.http
${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" jwt-rsa)
${genTargetsHS} --rsa="$_arg_testdir"/gen_jwk.json "$_arg_testdir"/gen_targets.http
export PGRST_JWT_CACHE_MAX_ENTRIES="0"
export PGRST_JWT_CACHE_MAX_LIFETIME="0"
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
;;
jwt-rsa-cache)
${genTargetsHS} --rsa="$_arg_testdir"/gen_jwk.json "$_arg_testdir"/gen_targets.http
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
;;
jwt-rsa-cache-worst)
${genTargetsHS} --worst --rsa="$_arg_testdir"/gen_jwk.json "$_arg_testdir"/gen_targets.http
export PGRST_JWT_SECRET="@$_arg_testdir/gen_jwk.json"
;; ;;
*) *)
;;
esac
if [ "$_arg_kind" == "mixed" ]; then
# shellcheck disable=SC2145 # shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \ ${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withSlowPg} \ ${withTools.withSlowPg} \
${withTools.withPgrst} \ ${withTools.withPgrst} -m "$_arg_monitor" \
${withTools.withSlowPgrst} \ ${withTools.withSlowPgrst} \
sh -c "cd \"$_arg_testdir\" && ${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\"" sh -c "cd \"$_arg_testdir\" && \
${vegeta}/bin/vegeta report -type=text "$_arg_output" ${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
;; else
# shellcheck disable=SC2145
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
${withTools.withPgrst} -m "$_arg_monitor" \
sh -c "cd \"$_arg_testdir\" && \
${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
fi
esac ${vegeta}/bin/vegeta report -type=text "$_arg_output"
''; '';
loadtestAgainst = loadtestAgainst =
@@ -115,11 +148,12 @@ let
workingDir = "/"; workingDir = "/";
} }
'' ''
# run loadtest for every target
for tgt in "''${_arg_target[@]}"; do for tgt in "''${_arg_target[@]}"; do
cat << EOF cat << EOF
Running loadtest on "$tgt"... Running "$_arg_kind" loadtest on "$tgt"...
EOF EOF
@@ -128,7 +162,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} -k "$_arg_kind" -m "$PWD/loadtest/$tgt.csv" --output "$PWD/loadtest/$tgt.bin" --testdir "$PWD/test/load"
cat << EOF cat << EOF
@@ -138,13 +172,15 @@ let
done done
# run loadtest once on HEAD
cat << EOF cat << EOF
Running loadtest on HEAD... Running "$_arg_kind" loadtest on HEAD...
EOF EOF
${loadtest} -k "$_arg_kind" --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load" ${loadtest} -k "$_arg_kind" -m "$PWD/loadtest/head.csv" --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load"
cat << EOF cat << EOF
@@ -181,6 +217,7 @@ let
pd.read_json(sys.stdin) \ pd.read_json(sys.stdin) \
.set_index('param') \ .set_index('param') \
.drop(['branch', 'earliest', 'end', 'latest']) \ .drop(['branch', 'earliest', 'end', 'latest']) \
.fillna("") \
.convert_dtypes() \ .convert_dtypes() \
.to_markdown(sys.stdout, floatfmt='.0f') .to_markdown(sys.stdout, floatfmt='.0f')
''; '';
@@ -191,16 +228,41 @@ let
{ {
name = "postgrest-loadtest-report"; name = "postgrest-loadtest-report";
docs = "Create a report of all loadtest reports as markdown."; docs = "Create a report of all loadtest reports as markdown.";
args = [
"ARG_OPTIONAL_SINGLE([group], [g], [Marker to group results])"
];
workingDir = "/"; workingDir = "/";
} }
'' ''
marker=''${_arg_group:+"($_arg_group)"}
echo -e "## Loadtest results $marker\n"
find loadtest -type f -iname '*.bin' -exec ${reporter} {} \; \ find loadtest -type f -iname '*.bin' -exec ${reporter} {} \; \
| ${jq}/bin/jq '[paths(scalars) as $path | {param: $path | join("."), (.branch): getpath($path)}]' \ | ${jq}/bin/jq '[paths(scalars) as $path | {param: $path | join("."), (.branch): getpath($path)}]' \
| ${jq}/bin/jq --slurp 'flatten | group_by(.param) | map(add)' \ | ${jq}/bin/jq --slurp 'flatten | group_by(.param) | map(add)' \
| ${toMarkdown} | ${toMarkdown}
echo -e "\n\n## Loadtest elapsed seconds vs CPU/MEM usage $marker\n"
find loadtest -type f -iname '*.csv' \
| sort -m \
| ${mergeMonitorResults}
''; '';
genTargets = writers.writePython3 "postgrest-gen-loadtest-targets" { } (builtins.readFile ./generate_targets.py); genTargetsHS =
writers.writePython3 "postgrest-gen-loadtest-targets-hs"
{
libraries = [ python3Packages.pyjwt python3Packages.jwcrypto ];
}
(builtins.readFile ./generate_targets.py);
mergeMonitorResults =
writers.writePython3 "postgrest-merge-monitor-results"
{
libraries = [ python3Packages.pandas python3Packages.tabulate ];
}
(builtins.readFile ./merge_monitor_result.py);
in in
buildToolbox { buildToolbox {
name = "postgrest-loadtest"; name = "postgrest-loadtest";
+44
View File
@@ -0,0 +1,44 @@
import os
import sys
import pandas as pd
KEY = "Elapsed seconds"
BASE_METRICS = ["CPU (%)", "Real (MB)"]
branch_order = []
merged = None
paths = [p.strip() for p in sys.stdin.read().split() if p.strip()]
for csv_path in paths:
# br is branch (variable shortened to pass linter)
br = os.path.splitext(os.path.basename(csv_path))[0]
branch_order.append(br)
df = pd.read_csv(csv_path)
if KEY not in df.columns:
sys.exit(f"{csv_path} is missing the {KEY} column")
for m in BASE_METRICS:
if m not in df.columns:
sys.exit(f"Error: '{csv_path}' missing required column '{m}'.")
# add branch marker to every metric column
df = df.rename(columns={c: f"{c} [{br}]" for c in df.columns if c != KEY})
# outer join so missing rows appear
merged = df if merged is None else merged.merge(df, on=KEY, how="outer")
# Re-order columns so related metrics are adjacent
ordered_cols = [KEY]
for metric in BASE_METRICS:
for br in branch_order:
col_name = f"{metric} [{br}]"
if col_name in merged.columns:
ordered_cols.append(col_name)
merged = merged[ordered_cols]
# replace nan with empty string
merged = merged.fillna("")
merged.to_markdown(sys.stdout, index=False, tablefmt="github")
+57
View File
@@ -0,0 +1,57 @@
# Monitor a process pid with psutil and emits a CSV.
import sys
import time
import psutil
import pandas as pd
KEY = "Elapsed seconds"
BASE_METRICS = ["CPU (%)", "Real (MB)"]
SAMPLE_INTERVAL_SECS = 1
if len(sys.argv) != 2 or not sys.argv[1].isdigit():
sys.exit(f"Usage: {sys.argv[0]} <PID>")
pid = int(sys.argv[1])
try:
proc = psutil.Process(pid)
except psutil.NoSuchProcess:
sys.exit(f"Error: process {pid} not found.")
print(f"Starting monitoring of {pid} pid", file=sys.stderr)
records = []
start = time.time()
# ignore first result as per docs recommendation
# https://psutil.readthedocs.io/en/latest/#psutil.cpu_percent
proc.cpu_percent(None)
while True:
try:
if not proc.is_running():
break
time.sleep(SAMPLE_INTERVAL_SECS)
elapsed_secs = int(time.time() - start)
cpu = proc.cpu_percent(None)
meminfo = proc.memory_info()
bytes_in_MB = 1024**2
rss_mb = meminfo.rss / bytes_in_MB
records.append(
[
str(elapsed_secs),
f"{cpu:.3f}",
f"{rss_mb:.3f}",
]
)
except psutil.NoSuchProcess:
break
end = time.time()
total_time = end - start
print(f"Finished {pid} pid monitoring in {total_time:.3f}", file=sys.stderr)
cols = [KEY] + BASE_METRICS
df = pd.DataFrame(records, columns=cols, dtype=str)
df.to_csv(sys.stdout, index=False)
+28
View File
@@ -0,0 +1,28 @@
{ buildToolbox
, checkedShellScript
}:
# Utility script for pinning the latest stable version of Nixpkgs.
# Instead of running `nix flake update` manually, we run this script
# to also pin readthedocs dependencies at the same time.
let
upgrade =
checkedShellScript
{
name = "postgrest-nixpkgs-upgrade";
docs = "Pin the newest version of Nixpkgs.";
workingDir = "/";
}
''
nix flake update
echo "# This file is auto-generated by postgrest-nixpkgs-upgrade" > docs/requirements.txt
cat "$(nix-build -A docs.requirements)" >> docs/requirements.txt
'';
in
buildToolbox
{
name = "postgrest-nixpkgs";
tools = { inherit upgrade; };
}
+15 -22
View File
@@ -20,27 +20,24 @@ let
git diff --exit-code HEAD postgrest.cabal > /dev/null git diff --exit-code HEAD postgrest.cabal > /dev/null
trap "" ERR trap "" ERR
# TODO: Support C+D bumps when implementing hackage releases
bump () { bump () {
current_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)" current_version="$(grep -oP '^version:\s*\K.*' postgrest.cabal)"
# shellcheck disable=SC2034 # shellcheck disable=SC2034
IFS=. read -r major minor patch <<< "$current_version" IFS=. read -r A B C D <<< "$current_version"
echo "Current version is $current_version" echo "Current version is $current_version"
case "$1" in case "$1" in
major) A)
new_version="$((major+1)).0.0" new_version="$((A+1)).0"
new_docs_version="$((major+1)).0" new_docs_version="$((A+1))"
;; ;;
minor) B)
new_version="$major.$((minor+1)).0" new_version="$A.$((B+1))"
new_docs_version="$major.$((minor+1))" new_docs_version="$A"
;;
patch)
new_version="$major.$minor.$((patch+1))"
new_docs_version="$major.$minor"
;; ;;
devel) devel)
new_version="$major.$((minor+1))" new_version="$((A+1))"
new_docs_version="devel" new_docs_version="devel"
;; ;;
esac esac
@@ -55,13 +52,9 @@ let
today_date_for_changelog="$(date '+%Y-%m-%d')" today_date_for_changelog="$(date '+%Y-%m-%d')"
if [[ "$current_branch" == "main" ]]; then if [[ "$current_branch" == "main" ]]; then
if [[ "$_arg_major" == "on" ]]; then bump A
bump major
else else
bump minor bump B
fi
else
bump patch
fi fi
echo "Updating CHANGELOG.md ..." echo "Updating CHANGELOG.md ..."
@@ -75,10 +68,10 @@ let
bump devel bump devel
# The order of operations is important here: # The order of operations is important here:
# - bump devel is run and $major is upated to the new version # - bump devel is run and $A is upated to the new version
# - the branch is created with the new major, but the commit before the devel bump # - the branch is created with the new A, but the commit before the devel bump
# - the devel bump is committed # - the devel bump is committed
git branch -f "v$major" git branch "v$A"
echo "Committing (devel bump)..." echo "Committing (devel bump)..."
git commit -m "bump version to $new_version" > /dev/null git commit -m "bump version to $new_version" > /dev/null
@@ -90,7 +83,7 @@ let
if [[ "$current_branch" == "main" ]]; then if [[ "$current_branch" == "main" ]]; then
push1="git push $remote $current_branch" push1="git push $remote $current_branch"
push2="git push $remote v$major --force" push2="git push $remote v$A"
else else
push1="git push $remote $current_branch" push1="git push $remote $current_branch"
push2="" push2=""
+26 -1
View File
@@ -7,9 +7,12 @@
, hlint , hlint
, hsie , hsie
, nixpkgs-fmt , nixpkgs-fmt
, python3Packages
, ruff
, silver-searcher , silver-searcher
, statix , statix
, stylish-haskell , stylish-haskell
, writeText
}: }:
let let
style = style =
@@ -49,6 +52,20 @@ let
${git}/bin/git diff-index --exit-code HEAD -- '*.hs' '*.lhs' '*.nix' '*.py' ${git}/bin/git diff-index --exit-code HEAD -- '*.hs' '*.lhs' '*.nix' '*.py'
''; '';
hlintConfig = writeText "hlintConfig.yml" ''
# Arguments passed to hlint
- arguments: [-j, -XQuasiQuotes, -XNoPatternSynonyms]
# Warnings
- warn: { lhs: "a == a", rhs: "True", note: "This comparison always evaluates to True" }
- warn: { lhs: "a /= a", rhs: "False", note: "This comparison always evaluates to False" }
- warn: { lhs: "a < a", rhs: "False", note: "This comparison always evaluates to False" }
- warn: { lhs: "a > a", rhs: "False", note: "This comparison always evaluates to False" }
- warn: { lhs: "a <= a", rhs: "True", note: "This comparison always evaluates to True" }
- warn: { lhs: "a >= a", rhs: "True", note: "This comparison always evaluates to True" }
'';
lint = lint =
checkedShellScript checkedShellScript
{ {
@@ -63,13 +80,21 @@ let
echo "Scanning nix files for unused code..." echo "Scanning nix files for unused code..."
${deadnix}/bin/deadnix -f ${deadnix}/bin/deadnix -f
# ruff has gaps in scanning for unused code, so we use vulture
echo "Scanning python files for unused code..."
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?py$' . \
| xargs ${python3Packages.vulture}/bin/vulture --exclude docs/conf.py
echo "Linting python files..."
${ruff}/bin/ruff check .
echo "Checking consistency of import aliases in Haskell code..." echo "Checking consistency of import aliases in Haskell code..."
${hsie} check-aliases main src ${hsie} check-aliases main src
echo "Linting Haskell files..." echo "Linting Haskell files..."
# --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753 # --vimgrep fixes a bug in ag: https://github.com/ggreer/the_silver_searcher/issues/753
${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \ ${silver-searcher}/bin/ag -l --vimgrep -g '\.l?hs$' . \
| xargs ${hlint}/bin/hlint -X QuasiQuotes -X NoPatternSynonyms | xargs ${hlint}/bin/hlint --hint=${hlintConfig}
''; '';
in in
+28 -17
View File
@@ -28,11 +28,24 @@ 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[@]}"
''; '';
testObservability =
checkedShellScript
{
name = "postgrest-test-observability";
docs = "Run the Haskell observability test suite.";
args = [ "ARG_LEFTOVERS([hspec arguments])" ];
workingDir = "/";
withEnv = postgrest.env;
}
''
${withTools.withPg} -f test/observability/fixtures/load.sql \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability -- "''${_arg_leftovers[@]}"
'';
testDoctests = testDoctests =
checkedShellScript checkedShellScript
{ {
@@ -42,7 +55,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 +69,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,9 +96,8 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest ${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/load.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,9 +111,8 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest ${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/fixtures/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,9 +126,8 @@ let
withEnv = postgrest.env; withEnv = postgrest.env;
} }
'' ''
${cabal-install}/bin/cabal v2-update
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest ${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/fixtures/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 +141,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,8 +169,7 @@ 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 test:observability
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec
( (
trap 'echo Found dead code: Check file list above.' ERR ; trap 'echo Found dead code: Check file list above.' ERR ;
@@ -172,26 +178,31 @@ let
# collect all tests # collect all tests
HPCTIXFILE="$tmpdir"/io.tix \ HPCTIXFILE="$tmpdir"/io.tix \
${withTools.withPg} -f test/io/fixtures.sql \ ${withTools.withPg} -f test/io/fixtures/load.sql \
${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io ${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io
HPCTIXFILE="$tmpdir"/big_schema.tix \ HPCTIXFILE="$tmpdir"/big_schema.tix \
${withTools.withPg} -f test/io/big_schema.sql \ ${withTools.withPg} -f test/io/fixtures/big_schema.sql \
${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest -v test/io/test_big_schema.py ${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest -v test/io/test_big_schema.py
HPCTIXFILE="$tmpdir"/replica.tix \ HPCTIXFILE="$tmpdir"/replica.tix \
${withTools.withPg} --replica -f test/io/replica.sql \ ${withTools.withPg} --replica -f test/io/fixtures/replica.sql \
${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest -v test/io/test_replica.py ${cabal-install}/bin/cabal v2-exec ${devCabalOptions} -- ${ioTestPython}/bin/pytest -v test/io/test_replica.py
HPCTIXFILE="$tmpdir"/spec.tix \ HPCTIXFILE="$tmpdir"/spec.tix \
${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 ${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec
HPCTIXFILE="$tmpdir"/observability.tix \
${withTools.withPg} -f test/observability/fixtures/load.sql \
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:observability
# Note: No coverage for doctests, as doctests leverage GHCi and GHCi does not support hpc # Note: No coverage for doctests, as doctests leverage GHCi and GHCi does not support hpc
# collect all the tix files # collect all the tix files
${ghc}/bin/hpc sum --union --exclude=Paths_postgrest --output="$tmpdir"/tests.tix \ ${ghc}/bin/hpc sum --union --exclude=Paths_postgrest --output="$tmpdir"/tests.tix \
"$tmpdir"/io*.tix "$tmpdir"/big_schema*.tix "$tmpdir"/replica*.tix "$tmpdir"/spec.tix "$tmpdir"/io*.tix "$tmpdir"/big_schema*.tix "$tmpdir"/replica*.tix "$tmpdir"/spec.tix \
"$tmpdir"/observability.tix
# prepare the overlay # prepare the overlay
${ghc}/bin/hpc overlay --output="$tmpdir"/overlay.tix test/coverage.overlay ${ghc}/bin/hpc overlay --output="$tmpdir"/overlay.tix test/coverage.overlay
@@ -246,7 +257,6 @@ let
withPath = [ curl ]; 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-build --enable-profiling --disable-shared exe:postgrest
${cabal-install}/bin/cabal --builddir="dist-prof" v2-exec -- ${withTools.withPg} -f test/spec/fixtures/load.sql \ ${cabal-install}/bin/cabal --builddir="dist-prof" v2-exec -- ${withTools.withPg} -f test/spec/fixtures/load.sql \
test/memory/memory-tests.sh test/memory/memory-tests.sh
@@ -259,6 +269,7 @@ buildToolbox
tools = { tools = {
inherit inherit
testSpec testSpec
testObservability
testDoctests testDoctests
testSpecIdempotence testSpecIdempotence
testIO testIO
+52 -5
View File
@@ -5,8 +5,10 @@
, lib , lib
, postgresqlVersions , postgresqlVersions
, postgrest , postgrest
, python3Packages
, slocat , slocat
, writeText , writeText
, writers
}: }:
let let
withTmpDb = withTmpDb =
@@ -44,7 +46,7 @@ let
} }
# Avoid starting multiple layers of withTmpDb, but make sure to have the last invocation # Avoid starting multiple layers of withTmpDb, but make sure to have the last invocation
# load fixtures. Otherwise postgrest-with-postgresql-xx postgrest-test-io would not be possible. # load fixtures. Otherwise postgrest-with-pg-xx postgrest-test-io would not be possible.
if ! test -v PGHOST; then if ! test -v PGHOST; then
mkdir -p "$tmpdir"/{db,socket} mkdir -p "$tmpdir"/{db,socket}
@@ -72,7 +74,13 @@ let
>> "$setuplog" >> "$setuplog"
log "Starting the database cluster..." log "Starting the database cluster..."
# Instead of listening on a local port, we will listen on a unix domain socket. # Instead of listening on a local port, we will listen on a unix domain socket.
# NOTE: unix domain socket filename name must remain under max limit.
# On Linux, it's 108 chars (including '\0' terminator)
# On MacOS, it's 104 chars
# See: https://serverfault.com/questions/641347/check-if-a-path-exceeds-maximum-for-unix-domain-socket
pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\"all\" " \ pg_ctl -l "$tmpdir/db.log" -w start -o "-F -c listen_addresses=\"\" -c hba_file=$HBA_FILE -k $PGHOST -c log_statement=\"all\" " \
>> "$setuplog" >> "$setuplog"
@@ -327,6 +335,22 @@ let
done done
''; '';
# Broadcast SIGINT to any running postgrest instances on the host. Uses python for cross-platform compatibility.
signalPostgrest =
writers.writePython3 "postgrest-signal-int"
{ libraries = [ python3Packages.psutil ]; }
''
import psutil
import signal
for proc in psutil.process_iter(["name"]):
try:
if proc.info["name"] == "postgrest":
proc.send_signal(signal.SIGINT)
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
'';
withPgrst = withPgrst =
checkedShellScript checkedShellScript
{ {
@@ -336,6 +360,7 @@ let
[ [
"ARG_POSITIONAL_SINGLE([command], [Command to run])" "ARG_POSITIONAL_SINGLE([command], [Command to run])"
"ARG_LEFTOVERS([command arguments])" "ARG_LEFTOVERS([command arguments])"
"ARG_OPTIONAL_SINGLE([monitor], [m], [Enable CPU and memory monitoring of the PostgREST process and output to the designated file as markdown])"
]; ];
positionalCompletion = "_command"; positionalCompletion = "_command";
workingDir = "/"; workingDir = "/";
@@ -348,12 +373,13 @@ let
rm -f result rm -f result
if [ -z "''${PGRST_BUILD_CABAL:-}" ]; then if [ -z "''${PGRST_BUILD_CABAL:-}" ]; then
echo -n "Building postgrest (nix)... " echo -n "Building postgrest (nix)... "
nix-build -A postgrestPackage > "$tmpdir"/build.log 2>&1 || { # Using lib.getBin to also make this work with older checkouts, where .bin was not a thing, yet.
nix-build -E 'with import ./. {}; pkgs.lib.getBin postgrestPackage' > "$tmpdir"/build.log 2>&1 || {
echo "failed, output:" echo "failed, output:"
cat "$tmpdir"/build.log cat "$tmpdir"/build.log
exit 1 exit 1
} }
PGRST_CMD=./result/bin/postgrest PGRST_CMD=$(echo ./result*/bin/postgrest)
else else
echo -n "Building postgrest (cabal)... " echo -n "Building postgrest (cabal)... "
postgrest-build postgrest-build
@@ -361,11 +387,22 @@ let
fi fi
echo "done." echo "done."
echo -n "Starting postgrest... " ver=$($PGRST_CMD ${legacyConfig} --version)
echo -n "Starting $ver... "
$PGRST_CMD ${legacyConfig} > "$tmpdir"/run.log 2>&1 & $PGRST_CMD ${legacyConfig} > "$tmpdir"/run.log 2>&1 &
pid=$! pid=$!
# shellcheck disable=SC2317 # shellcheck disable=SC2317
cleanup() { cleanup() {
# Send INT to all postgrest processes.
# Workaround to trigger dumping postgrest.prof for postgrest-profiled-run
# Caveat: we cannot realistically limit this to the current process' tree,
# since pkill's --parent supports only direct children; therefore this
# would reap neighbor postgrest instances as well, because INT is asking
# the process to terminate too.
# TODO: consider cgroups to make this cleaner
${signalPostgrest}
kill "$pid" || true kill "$pid" || true
} }
trap cleanup EXIT trap cleanup EXIT
@@ -377,9 +414,19 @@ let
} }
echo "done." echo "done."
if [[ -n "$_arg_monitor" ]]; then
${monitorPid} "$pid" > "$_arg_monitor" &
fi
("$_arg_command" "''${_arg_leftovers[@]}") ("$_arg_command" "''${_arg_leftovers[@]}")
''; '';
monitorPid =
writers.writePython3 "postgrest-monitor-pid"
{
libraries = [ python3Packages.pandas python3Packages.tabulate python3Packages.psutil ];
}
(builtins.readFile ./monitor_pid.py);
in in
buildToolbox buildToolbox
{ {
@@ -392,7 +439,7 @@ buildToolbox
withSlowPg withSlowPg
withSlowPgrst; withSlowPgrst;
} // builtins.listToAttrs ( } // builtins.listToAttrs (
# Create a `postgrest-with-postgresql-` for each PostgreSQL version # Create a `postgrest-with-pg-` for each PostgreSQL version
builtins.map (pg: { inherit (pg) name; value = withTmpDb pg; }) postgresqlVersions builtins.map (pg: { inherit (pg) name; value = withTmpDb pg; }) postgresqlVersions
); );
# make latest withPg available for other nix files # make latest withPg available for other nix files
+65 -25
View File
@@ -1,5 +1,5 @@
name: postgrest name: postgrest
version: 13.0.0 version: 14.10
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,19 +16,17 @@ extra-source-files: CHANGELOG.md
cabal-version: >= 1.10 cabal-version: >= 1.10
tested-with: tested-with:
-- stack on FreeBSD -- nix
GHC == 9.4.5 GHC == 9.4.8
-- nix, cabal on Ubuntu (arm)
, GHC == 9.4.8
-- cabal on Ubuntu -- cabal on Ubuntu
-- stack on MacOS, Ubuntu, Windows -- stack on FreeBSD, MacOS, Ubuntu, Windows
, GHC == 9.6.6 , GHC == 9.6.7
-- cabal on Ubuntu -- cabal on Ubuntu
, GHC == 9.8.2 , GHC == 9.8.4
source-repository head source-repository head
type: git type: git
location: git://github.com/PostgREST/postgrest.git location: https://github.com/PostgREST/postgrest.git
flag dev flag dev
default: False default: False
@@ -49,9 +47,12 @@ library
PostgREST.App PostgREST.App
PostgREST.AppState PostgREST.AppState
PostgREST.Auth PostgREST.Auth
PostgREST.Auth.Jwt
PostgREST.Auth.JwtCache PostgREST.Auth.JwtCache
PostgREST.Auth.Types PostgREST.Auth.Types
PostgREST.Cache.Sieve
PostgREST.CLI PostgREST.CLI
PostgREST.Client
PostgREST.Config PostgREST.Config
PostgREST.Config.Database PostgREST.Config.Database
PostgREST.Config.JSPath PostgREST.Config.JSPath
@@ -67,11 +68,13 @@ library
PostgREST.Error PostgREST.Error
PostgREST.Listener PostgREST.Listener
PostgREST.Logger PostgREST.Logger
PostgREST.MainTx
PostgREST.MediaType PostgREST.MediaType
PostgREST.Metrics PostgREST.Metrics
PostgREST.Network PostgREST.Network
PostgREST.Observation PostgREST.Observation
PostgREST.Query PostgREST.Query
PostgREST.Query.PreQuery
PostgREST.Query.QueryBuilder PostgREST.Query.QueryBuilder
PostgREST.Query.SqlFragment PostgREST.Query.SqlFragment
PostgREST.Query.Statements PostgREST.Query.Statements
@@ -85,27 +88,26 @@ library
PostgREST.ApiRequest PostgREST.ApiRequest
PostgREST.ApiRequest.Preferences PostgREST.ApiRequest.Preferences
PostgREST.ApiRequest.QueryParams PostgREST.ApiRequest.QueryParams
PostgREST.ApiRequest.Payload
PostgREST.ApiRequest.Types PostgREST.ApiRequest.Types
PostgREST.Response PostgREST.Response
PostgREST.Response.OpenAPI PostgREST.Response.OpenAPI
PostgREST.Response.GucHeader PostgREST.Response.GucHeader
PostgREST.Response.Performance PostgREST.Response.Performance
PostgREST.TimeIt
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.2 , auto-update >= 0.1.4 && < 0.3
, 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
, 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 , 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.5 , cookie >= 0.4.2 && < 0.6
, 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
@@ -114,17 +116,16 @@ library
, 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.1 , hasql-transaction >= 1.0.1 && < 1.2
, heredoc >= 0.2 && < 0.3 , http-client >= 0.7.19 && < 0.8
, 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
, jose-jwt >= 0.9.6 && < 0.11 , 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
, mtl >= 2.2.2 && < 2.4 , mtl >= 2.2.2 && < 2.4
, neat-interpolation >= 0.5 && < 0.6 , 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
@@ -135,11 +136,10 @@ library
, regex-tdfa >= 1.2.2 && < 1.4 , regex-tdfa >= 1.2.2 && < 1.4
, retry >= 0.7.4 && < 0.10 , retry >= 0.7.4 && < 0.10
, scientific >= 0.3.4 && < 0.4 , scientific >= 0.3.4 && < 0.4
, streaming-commons >= 0.1.1 && < 0.3 , streaming-commons >= 0.2.3.1 && < 0.3
, swagger2 >= 2.4 && < 2.9 , swagger2 >= 2.4 && < 2.9
, text >= 1.2.2 && < 2.2 , text >= 1.2.2 && < 2.2
, time >= 1.6 && < 1.13 , time >= 1.6 && < 1.13
, timeit >= 2.0 && < 2.1
, unordered-containers >= 0.2.8 && < 0.3 , unordered-containers >= 0.2.8 && < 0.3
, unix-compat >= 0.5.4 && < 0.8 , unix-compat >= 0.5.4 && < 0.8
, vault >= 0.3.1.5 && < 0.4 , vault >= 0.3.1.5 && < 0.4
@@ -152,7 +152,11 @@ 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.4 , warp >= 3.3.19 && < 3.5
, stm >= 2.5 && < 3
, stm-hamt >= 1.2 && < 2
, focus >= 1.0 && < 2
, some >= 1.0.4.1 && < 2
-- -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
@@ -261,18 +265,20 @@ test-suite spec
, 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.1 , hasql-transaction >= 1.0.1 && < 1.2
, heredoc >= 0.2 && < 0.3 , heredoc >= 0.2 && < 0.3
, hspec >= 2.3 && < 2.12 , hspec >= 2.3 && < 2.12
, hspec-expectations >= 0.8.4 && < 0.9
, 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 , 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
, process >= 1.4.2 && < 1.7 , process >= 1.4.2 && < 1.7
, prometheus-client >= 1.1.1 && < 1.2.0
, protolude >= 0.3.1 && < 0.4 , protolude >= 0.3.1 && < 0.4
, regex-tdfa >= 1.2.2 && < 1.4 , regex-tdfa >= 1.2.2 && < 1.4
, scientific >= 0.3.4 && < 0.4 , scientific >= 0.3.4 && < 0.4
@@ -287,6 +293,40 @@ test-suite spec
-- https://github.com/PostgREST/postgrest/issues/387 -- https://github.com/PostgREST/postgrest/issues/387
-with-rtsopts=-K33K -with-rtsopts=-K33K
test-suite observability
type: exitcode-stdio-1.0
default-language: Haskell2010
default-extensions: OverloadedStrings
QuasiQuotes
NoImplicitPrelude
hs-source-dirs: test/observability
main-is: Main.hs
other-modules: ObsHelper
Observation.JwtCache
Observation.MetricsSpec
Observation.SchemaCacheSpec
build-depends: base >= 4.9 && < 4.20
, base64-bytestring >= 1 && < 1.3
, bytestring >= 0.10.8 && < 0.13
, hasql-pool >= 1.0.1 && < 1.1
, hasql-transaction >= 1.0.1 && < 1.2
, hspec >= 2.3 && < 2.12
, hspec-expectations >= 0.8.4 && < 0.9
, hspec-wai >= 0.10 && < 0.12
, hspec-wai-json >= 0.10 && < 0.12
, http-types >= 0.12.3 && < 0.13
, jose-jwt >= 0.9.6 && < 0.11
, postgrest
, prometheus-client >= 1.1.1 && < 1.2.0
, protolude >= 0.3.1 && < 0.4
, text >= 1.2.2 && < 2.2
, wai >= 3.2.1 && < 3.3
ghc-options: -threaded -O0 -Werror -Wall -fwarn-identities
-fno-spec-constr -optP-Wno-nonportable-include-path
-fwrite-ide-info
-- https://github.com/PostgREST/postgrest/issues/387
-with-rtsopts=-K33K
test-suite doctests test-suite doctests
type: exitcode-stdio-1.0 type: exitcode-stdio-1.0
default-language: Haskell2010 default-language: Haskell2010
+2
View File
@@ -21,7 +21,9 @@ let
postgrest.cabalTools postgrest.cabalTools
postgrest.devTools postgrest.devTools
postgrest.docs postgrest.docs
postgrest.gitTools
postgrest.loadtest postgrest.loadtest
postgrest.nixpkgsTools
postgrest.release postgrest.release
postgrest.style postgrest.style
postgrest.tests postgrest.tests
+14 -17
View File
@@ -1,5 +1,3 @@
{-# LANGUAGE NamedFieldPuns #-}
module PostgREST.Admin module PostgREST.Admin
( runAdmin ( runAdmin
) where ) where
@@ -10,35 +8,34 @@ import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Handler.Warp as Warp
import Control.Monad.Extra (whenJust) import Control.Monad.Extra (whenJust)
import Network.Socket hiding (addrFamily)
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.MediaType (MediaType (..), toContentType)
import PostgREST.Metrics (metricsToText) import PostgREST.Metrics (metricsToText)
import PostgREST.Network (resolveHost) import PostgREST.Network (resolveSocketToAddress)
import PostgREST.Observation (Observation (..)) import PostgREST.Observation (Observation (..))
import qualified PostgREST.AppState as AppState import qualified PostgREST.AppState as AppState
import qualified Network.Socket as NS
import Protolude import Protolude
runAdmin :: AppState -> Warp.Settings -> IO () runAdmin :: AppState -> Maybe NS.Socket -> NS.Socket -> Warp.Settings -> IO ()
runAdmin appState settings = do runAdmin appState maybeAdminSocket socketREST settings = do
AppConfig{configAdminServerPort} <- AppState.getConfig appState whenJust maybeAdminSocket $ \adminSocket -> do
whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do address <- resolveSocketToAddress adminSocket
host <- resolveHost adminSocket observer $ AdminStartObs address
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 socketREST
observer = AppState.getObserver appState observer = AppState.getObserver appState
-- | PostgREST admin application -- | PostgREST admin application
admin :: AppState.AppState -> Wai.Application admin :: AppState.AppState -> NS.Socket -> Wai.Application
admin appState req respond = do admin appState socketREST req respond = do
isMainAppReachable <- isRight <$> reachMainApp (AppState.getSocketREST appState) isMainAppReachable <- isRight <$> reachMainApp socketREST
isLoaded <- AppState.isLoaded appState isLoaded <- AppState.isLoaded appState
isPending <- AppState.isPending appState isPending <- AppState.isPending appState
@@ -58,7 +55,7 @@ admin appState req respond = do
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 [] mets respond $ Wai.responseLBS HTTP.status200 [toContentType MTTextPlain] mets -- Content-Type is required for prometheus compliance
_ -> _ ->
respond $ Wai.responseLBS HTTP.status404 [] mempty respond $ Wai.responseLBS HTTP.status404 [] mempty
+6 -158
View File
@@ -6,44 +6,30 @@ Description : PostgREST functions to translate HTTP request to a domain type cal
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
module PostgREST.ApiRequest module PostgREST.ApiRequest
( ApiRequest(..) ( ApiRequest(..)
, InvokeMethod(..)
, Mutation(..)
, MediaType(..)
, Action(..)
, DbAction(..)
, Payload(..)
, userApiRequest , userApiRequest
, userPreferences , userPreferences
) where ) where
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM
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.Csv as CSV
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
import qualified Data.List.NonEmpty as NonEmptyList import qualified Data.List.NonEmpty as NonEmptyList
import qualified Data.Map.Strict as M
import qualified Data.Set as S import qualified Data.Set as S
import qualified Data.Text.Encoding as T import qualified Data.Text.Encoding as T
import qualified Data.Vector as V
import Data.Either.Combinators (mapBoth)
import Control.Arrow ((***))
import Data.Aeson.Types (emptyArray, emptyObject)
import Data.List (lookup) import Data.List (lookup)
import Data.Ranged.Ranges (emptyRange, rangeIntersection, import Data.Ranged.Ranges (emptyRange, rangeIntersection,
rangeIsEmpty) rangeIsEmpty)
import Network.HTTP.Types.Header (RequestHeaders, hCookie) import Network.HTTP.Types.Header (RequestHeaders, hCookie)
import Network.HTTP.Types.URI (parseSimpleQuery)
import Network.Wai (Request (..)) import Network.Wai (Request (..))
import Network.Wai.Parse (parseHttpAccept) import Network.Wai.Parse (parseHttpAccept)
import Web.Cookie (parseCookies) import Web.Cookie (parseCookies)
import PostgREST.ApiRequest.Payload (getPayload)
import PostgREST.ApiRequest.QueryParams (QueryParams (..)) import PostgREST.ApiRequest.QueryParams (QueryParams (..))
import PostgREST.ApiRequest.Types (Action (..), DbAction (..),
InvokeMethod (..),
Mutation (..), Payload (..),
RequestBody, Resource (..))
import PostgREST.Config (AppConfig (..), import PostgREST.Config (AppConfig (..),
OpenAPIMode (..)) OpenAPIMode (..))
import PostgREST.Config.Database (TimezoneNames) import PostgREST.Config.Database (TimezoneNames)
@@ -64,44 +50,6 @@ import qualified PostgREST.MediaType as MediaType
import Protolude import Protolude
type RequestBody = LBS.ByteString
data Payload
= ProcessedJSON -- ^ Cached attributes of a JSON payload
{ payRaw :: LBS.ByteString
-- ^ This is the raw ByteString that comes from the request body. We
-- cache this instead of an Aeson Value because it was detected that for
-- large payloads the encoding had high memory usage, see
-- https://github.com/PostgREST/postgrest/pull/1005 for more details
, payKeys :: S.Set Text
-- ^ Keys of the object or if it's an array these keys are guaranteed to
-- be the same across all its objects
}
| ProcessedUrlEncoded { payArray :: [(Text, Text)], payKeys :: S.Set Text }
| RawJSON { payRaw :: LBS.ByteString }
| RawPay { payRaw :: LBS.ByteString }
data InvokeMethod = Inv | InvRead Bool deriving Eq
data Mutation = MutationCreate | MutationDelete | MutationSingleUpsert | MutationUpdate deriving Eq
data Resource
= ResourceRelation Text
| ResourceRoutine Text
| ResourceSchema
data DbAction
= ActRelationRead {dbActQi :: QualifiedIdentifier, actHeadersOnly :: Bool}
| ActRelationMut {dbActQi :: QualifiedIdentifier, actMutation :: Mutation}
| ActRoutine {dbActQi :: QualifiedIdentifier, actInvMethod :: InvokeMethod}
| ActSchemaRead Schema Bool
data Action
= ActDb DbAction
| ActRelationInfo QualifiedIdentifier
| ActRoutineInfo QualifiedIdentifier InvokeMethod
| ActSchemaInfo
{-| {-|
Describes what the user wants to do. This data type is a Describes what the user wants to do. This data type is a
translation of the raw elements of an HTTP request into domain translation of the raw elements of an HTTP request into domain
@@ -207,7 +155,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 $ toList configDbSchemas Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema p $ 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
@@ -240,103 +188,3 @@ getRanges method QueryParams{qsRanges} hdrs
-- The only emptyRange allowed is the limit zero range -- The only emptyRange allowed is the limit zero range
isInvalidRange = topLevelRange == emptyRange && not (hasLimitZero limitRange) isInvalidRange = topLevelRange == emptyRange && not (hasLimitZero limitRange)
topLevelRange = fromMaybe allRange $ HM.lookup "limit" ranges -- if no limit is specified, get all the request rows topLevelRange = fromMaybe allRange $ HM.lookup "limit" ranges -- if no limit is specified, get all the request rows
getPayload :: RequestBody -> MediaType -> QueryParams.QueryParams -> Action -> Either ApiRequestError (Maybe Payload, S.Set FieldName)
getPayload reqBody contentMediaType QueryParams{qsColumns} action = do
checkedPayload <- if shouldParsePayload then payload else Right Nothing
let cols = case (checkedPayload, columns) of
(Just ProcessedJSON{payKeys}, _) -> payKeys
(Just ProcessedUrlEncoded{payKeys}, _) -> payKeys
(Just RawJSON{}, Just cls) -> cls
_ -> S.empty
return (checkedPayload, cols)
where
payload :: Either ApiRequestError (Maybe Payload)
payload = mapBoth InvalidBody Just $ case (contentMediaType, isProc) of
(MTApplicationJSON, _) ->
if isJust columns
then Right $ RawJSON reqBody
else note "All object keys must match" . payloadAttributes reqBody
=<< if LBS.null reqBody && isProc
then Right emptyObject
else first BS.pack $
-- Drop parsing error message in favor of generic one (https://github.com/PostgREST/postgrest/issues/2344)
maybe (Left "Empty or invalid json") Right $ JSON.decode reqBody
(MTTextCSV, _) -> do
json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
(MTUrlEncoded, True) ->
Right $ ProcessedUrlEncoded params (S.fromList $ fst <$> params)
(MTUrlEncoded, False) ->
let paramsMap = HM.fromList $ (identity *** JSON.String) <$> params in
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (HM.keys paramsMap)
(MTTextPlain, True) -> Right $ RawPay reqBody
(MTTextXML, True) -> Right $ RawPay reqBody
(MTOctetStream, True) -> Right $ RawPay reqBody
(ct, _) -> Left $ "Content-Type not acceptable: " <> MediaType.toMime ct
shouldParsePayload = case action of
ActDb (ActRelationMut _ MutationDelete) -> False
ActDb (ActRelationMut _ _) -> True
ActDb (ActRoutine _ Inv) -> True
_ -> False
columns = case action of
ActDb (ActRelationMut _ MutationCreate) -> qsColumns
ActDb (ActRelationMut _ MutationUpdate) -> qsColumns
ActDb (ActRoutine _ Inv) -> qsColumns
_ -> Nothing
isProc = case action of
ActDb (ActRoutine _ _) -> True
_ -> False
params = (T.decodeUtf8 *** T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody)
type CsvData = V.Vector (M.Map Text LBS.ByteString)
{-|
Converts CSV like
a,b
1,hi
2,bye
into a JSON array like
[ {"a": "1", "b": "hi"}, {"a": 2, "b": "bye"} ]
The reason for its odd signature is so that it can compose
directly with CSV.decodeByName
-}
csvToJson :: (CSV.Header, CsvData) -> JSON.Value
csvToJson (_, vals) =
JSON.Array $ V.map rowToJsonObj vals
where
rowToJsonObj = JSON.Object . KM.fromMapText .
M.map (\str ->
if str == "NULL"
then JSON.Null
else JSON.String . T.decodeUtf8 $ LBS.toStrict str
)
payloadAttributes :: RequestBody -> JSON.Value -> Maybe Payload
payloadAttributes raw json =
-- Test that Array contains only Objects having the same keys
case json of
JSON.Array arr ->
case arr V.!? 0 of
Just (JSON.Object o) ->
let canonicalKeys = S.fromList $ K.toText <$> KM.keys o
areKeysUniform = all (\case
JSON.Object x -> S.fromList (K.toText <$> KM.keys x) == canonicalKeys
_ -> False) arr in
if areKeysUniform
then Just $ ProcessedJSON raw canonicalKeys
else Nothing
Just _ -> Nothing
Nothing -> Just emptyPJArray
JSON.Object o -> Just $ ProcessedJSON raw (S.fromList $ K.toText <$> KM.keys o)
-- truncate everything else to an empty array.
_ -> Just emptyPJArray
where
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
+138
View File
@@ -0,0 +1,138 @@
-- |
-- Module : PostgREST.ApiRequest.Payload
-- Description : Parser for PostgREST Request Body
--
-- This module is in charge of parsing the request body (payload)
--
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
module PostgREST.ApiRequest.Payload
( getPayload
) where
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Csv as CSV
import qualified Data.HashMap.Strict as HM
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import qualified Data.Text.Encoding as T
import qualified Data.Vector as V
import Control.Arrow ((***))
import Data.Aeson.Types (emptyArray, emptyObject)
import Data.Either.Combinators (mapBoth)
import Network.HTTP.Types.URI (parseSimpleQuery)
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
import PostgREST.ApiRequest.Types
import PostgREST.Error (ApiRequestError (..))
import PostgREST.MediaType (MediaType (..))
import PostgREST.SchemaCache.Identifiers (FieldName)
import qualified PostgREST.MediaType as MediaType
import Protolude
getPayload :: RequestBody -> MediaType -> QueryParams -> Action -> Either ApiRequestError (Maybe Payload, S.Set FieldName)
getPayload reqBody contentMediaType QueryParams{qsColumns} action = do
checkedPayload <- if shouldParsePayload then payload else Right Nothing
let cols = case (checkedPayload, columns) of
(Just ProcessedJSON{payKeys}, _) -> payKeys
(Just ProcessedUrlEncoded{payKeys}, _) -> payKeys
(Just RawJSON{}, Just cls) -> cls
_ -> S.empty
return (checkedPayload, cols)
where
payload :: Either ApiRequestError (Maybe Payload)
payload = mapBoth InvalidBody Just $ case (contentMediaType, isProc) of
(MTApplicationJSON, _) ->
if isJust columns
then Right $ RawJSON reqBody
else note "All object keys must match" . payloadAttributes reqBody
=<< if LBS.null reqBody && isProc
then Right emptyObject
else first BS.pack $
-- Drop parsing error message in favor of generic one (https://github.com/PostgREST/postgrest/issues/2344)
maybe (Left "Empty or invalid json") Right $ JSON.decode reqBody
(MTTextCSV, _) -> do
json <- csvToJson <$> first BS.pack (CSV.decodeByName reqBody)
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
(MTUrlEncoded, True) ->
Right $ ProcessedUrlEncoded params (S.fromList $ fst <$> params)
(MTUrlEncoded, False) ->
let paramsMap = HM.fromList $ (identity *** JSON.String) <$> params in
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (HM.keys paramsMap)
(MTTextPlain, True) -> Right $ RawPay reqBody
(MTTextXML, True) -> Right $ RawPay reqBody
(MTOctetStream, True) -> Right $ RawPay reqBody
(ct, _) -> Left $ "Content-Type not acceptable: " <> MediaType.toMime ct
shouldParsePayload = case action of
ActDb (ActRelationMut _ MutationDelete) -> False
ActDb (ActRelationMut _ _) -> True
ActDb (ActRoutine _ Inv) -> True
_ -> False
columns = case action of
ActDb (ActRelationMut _ MutationCreate) -> qsColumns
ActDb (ActRelationMut _ MutationUpdate) -> qsColumns
ActDb (ActRoutine _ Inv) -> qsColumns
_ -> Nothing
isProc = case action of
ActDb (ActRoutine _ _) -> True
_ -> False
params = (T.decodeUtf8 *** T.decodeUtf8) <$> parseSimpleQuery (LBS.toStrict reqBody)
type CsvData = V.Vector (M.Map Text LBS.ByteString)
{-|
Converts CSV like
a,b
1,hi
2,bye
into a JSON array like
[ {"a": "1", "b": "hi"}, {"a": 2, "b": "bye"} ]
The reason for its odd signature is so that it can compose
directly with CSV.decodeByName
-}
csvToJson :: (CSV.Header, CsvData) -> JSON.Value
csvToJson (_, vals) =
JSON.Array $ V.map rowToJsonObj vals
where
rowToJsonObj = JSON.Object . KM.fromMapText .
M.map (\str ->
if str == "NULL"
then JSON.Null
else JSON.String . T.decodeUtf8 $ LBS.toStrict str
)
payloadAttributes :: RequestBody -> JSON.Value -> Maybe Payload
payloadAttributes raw json =
-- Test that Array contains only Objects having the same keys
case json of
JSON.Array arr ->
case arr V.!? 0 of
Just (JSON.Object o) ->
let canonicalKeys = S.fromList $ K.toText <$> KM.keys o
areKeysUniform = all (\case
JSON.Object x -> S.fromList (K.toText <$> KM.keys x) == canonicalKeys
_ -> False) arr in
if areKeysUniform
then Just $ ProcessedJSON raw canonicalKeys
else Nothing
Just _ -> Nothing
Nothing -> Just emptyPJArray
JSON.Object o -> Just $ ProcessedJSON raw (S.fromList $ K.toText <$> KM.keys o)
-- truncate everything else to an empty array.
_ -> Just emptyPJArray
where
emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
+6 -1
View File
@@ -19,6 +19,7 @@ module PostgREST.ApiRequest.Preferences
, PreferMaxAffected(..) , PreferMaxAffected(..)
, fromHeaders , fromHeaders
, shouldCount , shouldCount
, shouldExplainCount
, prefAppliedHeader , prefAppliedHeader
) where ) where
@@ -155,7 +156,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
listStripPrefix prefix prefList = listToMaybe $ mapMaybe (BS.stripPrefix prefix) prefList listStripPrefix prefix prefList = listToMaybe $ mapMaybe (BS.stripPrefix prefix) prefList
timezonePref = listStripPrefix "timezone=" prefs timezonePref = listStripPrefix "timezone=" prefs
isTimezonePrefAccepted = (S.member <$> (decodeUtf8 <$> timezonePref) <*> pure acceptedTzNames) == Just True isTimezonePrefAccepted = ((S.member . decodeUtf8 <$> timezonePref) <*> pure acceptedTzNames) == Just True
maxAffectedPref = listStripPrefix "max-affected=" prefs >>= readMaybe . BS.unpack maxAffectedPref = listStripPrefix "max-affected=" prefs >>= readMaybe . BS.unpack
@@ -238,6 +239,10 @@ shouldCount :: Maybe PreferCount -> Bool
shouldCount prefCount = shouldCount prefCount =
prefCount == Just ExactCount || prefCount == Just EstimatedCount prefCount == Just ExactCount || prefCount == Just EstimatedCount
shouldExplainCount :: Maybe PreferCount -> Bool
shouldExplainCount prefCount =
prefCount == Just PlannedCount || prefCount == Just EstimatedCount
-- | Whether to commit or roll back transactions. -- | Whether to commit or roll back transactions.
data PreferTransaction data PreferTransaction
= Commit -- ^ Commit transaction - the default. = Commit -- ^ Commit transaction - the default.
+58 -1
View File
@@ -30,12 +30,69 @@ module PostgREST.ApiRequest.Types
, QuantOperator(..) , QuantOperator(..)
, FtsOperator(..) , FtsOperator(..)
, SelectItem(..) , SelectItem(..)
, Payload (..)
, InvokeMethod (..)
, Mutation (..)
, Resource (..)
, DbAction (..)
, Action (..)
, RequestBody
) where ) where
import PostgREST.SchemaCache.Identifiers (FieldName) import qualified Data.ByteString.Lazy as LBS
import qualified Data.Set as S
import PostgREST.SchemaCache.Identifiers (FieldName,
QualifiedIdentifier (..),
Schema)
import Protolude import Protolude
data InvokeMethod = Inv | InvRead Bool
deriving Eq
data Mutation
= MutationCreate
| MutationDelete
| MutationSingleUpsert
| MutationUpdate
deriving Eq
data Resource
= ResourceRelation Text
| ResourceRoutine Text
| ResourceSchema
data DbAction
= ActRelationRead {dbActQi :: QualifiedIdentifier, actHeadersOnly :: Bool}
| ActRelationMut {dbActQi :: QualifiedIdentifier, actMutation :: Mutation}
| ActRoutine {dbActQi :: QualifiedIdentifier, actInvMethod :: InvokeMethod}
| ActSchemaRead Schema Bool
data Action
= ActDb DbAction
| ActRelationInfo QualifiedIdentifier
| ActRoutineInfo QualifiedIdentifier InvokeMethod
| ActSchemaInfo
type RequestBody = LBS.ByteString
data Payload
= ProcessedJSON -- ^ Cached attributes of a JSON payload
{ payRaw :: LBS.ByteString
-- ^ This is the raw ByteString that comes from the request body. We
-- cache this instead of an Aeson Value because it was detected that for
-- large payloads the encoding had high memory usage, see
-- https://github.com/PostgREST/postgrest/pull/1005 for more details
, payKeys :: S.Set Text
-- ^ Keys of the object or if it's an array these keys are guaranteed to
-- be the same across all its objects
}
| ProcessedUrlEncoded { payArray :: [(Text, Text)], payKeys :: S.Set Text }
| RawJSON { payRaw :: LBS.ByteString }
| RawPay { payRaw :: LBS.ByteString }
-- | The value in `/tbl?select=alias:field.aggregateFunction()::cast` -- | The value in `/tbl?select=alias:field.aggregateFunction()::cast`
data SelectItem data SelectItem
= SelectField = SelectField
+109 -36
View File
@@ -10,17 +10,23 @@ Some of its functionality includes:
- Content Negotiation - Content Negotiation
-} -}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE ViewPatterns #-}
module PostgREST.App module PostgREST.App
( postgrest ( postgrest
, run , run
) where ) where
import GHC.IO.Exception (IOErrorType (..))
import System.IO.Error (ioeGetErrorType)
import Control.Monad.Except (liftEither) import Control.Monad.Except (liftEither)
import Data.Either.Combinators (mapLeft, whenLeft) import Data.Either.Combinators (mapLeft, whenLeft)
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,
setOnException, setPort,
setServerName) setServerName)
import qualified Data.Text.Encoding as T import qualified Data.Text.Encoding as T
@@ -35,6 +41,7 @@ import qualified PostgREST.Cors as Cors
import qualified PostgREST.Error as Error import qualified PostgREST.Error as Error
import qualified PostgREST.Listener as Listener import qualified PostgREST.Listener as Listener
import qualified PostgREST.Logger as Logger import qualified PostgREST.Logger as Logger
import qualified PostgREST.MainTx as MainTx
import qualified PostgREST.Plan as Plan import qualified PostgREST.Plan as Plan
import qualified PostgREST.Query as Query import qualified PostgREST.Query as Query
import qualified PostgREST.Response as Response import qualified PostgREST.Response as Response
@@ -43,49 +50,65 @@ 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.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..), LogLevel (..), import PostgREST.Config (AppConfig (..), LogLevel (..))
LogQuery (..))
import PostgREST.Config.PgVersion (PgVersion (..))
import PostgREST.Error (Error) import PostgREST.Error (Error)
import PostgREST.Network (resolveHost) import PostgREST.Network (resolveSocketToAddress)
import PostgREST.Observation (Observation (..)) import PostgREST.Observation (Observation (..))
import PostgREST.Response.Performance (ServerTiming (..), import PostgREST.Response.Performance (ServerTiming (..),
serverTimingHeader) serverTimingHeader)
import PostgREST.SchemaCache (SchemaCache (..)) import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.TimeIt (timeItT)
import PostgREST.Version (docsVersion, prettyVersion) import PostgREST.Version (docsVersion, prettyVersion)
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Data.List as L import qualified Data.List as L
import Data.Streaming.Network (bindPortTCP,
bindRandomPortTCP)
import qualified Data.Text as T
import qualified Network.HTTP.Types as HTTP import qualified Network.HTTP.Types as HTTP
import qualified Network.Socket as NS import qualified Network.Socket as NS
import PostgREST.Unix (createAndBindDomainSocket)
import Protolude hiding (Handler) import Protolude hiding (Handler)
import System.TimeIt (timeItT)
type Handler = ExceptT Error type Handler = ExceptT Error
run :: AppState -> IO () run :: AppState -> IO ()
run appState = do run appState = do
let observer = AppState.getObserver appState
conf@AppConfig{..} <- AppState.getConfig appState conf@AppConfig{..} <- AppState.getConfig appState
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) (mainSocket, adminSocket) <- initSockets conf
Unix.installSignalHandlers observer (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
Listener.runListener appState Listener.runListener appState
Admin.runAdmin appState (serverSettings conf) Admin.runAdmin appState adminSocket mainSocket (serverSettings conf)
let app = postgrest configLogLevel appState (AppState.schemaCacheLoader appState) let app = postgrest configLogLevel appState (AppState.schemaCacheLoader appState)
case configServerUnixSocket of do
Just path -> do address <- resolveSocketToAddress mainSocket
observer $ AppServerUnixObs path observer $ AppServerAddressObs address
Nothing -> do
port <- NS.socketPort $ AppState.getSocketREST appState
host <- resolveHost $ AppState.getSocketREST appState
observer $ AppServerPortObs (fromJust host) port
Warp.runSettingsSocket (serverSettings conf) (AppState.getSocketREST appState) app Warp.runSettingsSocket (serverSettings conf & setOnException onWarpException) mainSocket app
where
observer = AppState.getObserver appState
onWarpException :: Maybe Wai.Request -> SomeException -> IO ()
onWarpException _ ex =
when (shouldDisplayException ex) $
observer $ WarpServerObs $ show ex
-- Similar to wai defaultShouldDisplayException in
-- https://github.com/yesodweb/wai//blob/8c3882c60f6abe043889fc20c7efd3fa9747fa4a/warp/Network/Wai/Handler/Warp/Settings.hs#L251-L258
-- but without omitting AsyncException since it's important to log for ThreadKilled, StackOverflow and other cases.
-- We want to reuse this to avoid flooding the logs for some transient failure cases.
shouldDisplayException :: SomeException -> Bool
shouldDisplayException se
| Just (_ :: Warp.InvalidRequest) <- fromException se = False
| Just (ioeGetErrorType -> et) <- fromException se, et == ResourceVanished || et == InvalidArgument = False
| otherwise = True
serverSettings :: AppConfig -> Warp.Settings serverSettings :: AppConfig -> Warp.Settings
serverSettings AppConfig{..} = serverSettings AppConfig{..} =
@@ -108,18 +131,21 @@ postgrest logLevel appState connWorker =
Right authResult -> do Right authResult -> do
appConf <- AppState.getConfig appState -- the config must be read again because it can reload appConf <- AppState.getConfig appState -- the config must be read again because it can reload
maybeSchemaCache <- AppState.getSchemaCache appState maybeSchemaCache <- AppState.getSchemaCache appState
pgVer <- AppState.getPgVersion appState
let let
eitherResponse :: IO (Either Error Wai.Response) eitherResponse :: IO (Either Error Wai.Response)
eitherResponse = eitherResponse =
runExceptT $ postgrestResponse appState appConf maybeSchemaCache pgVer authResult req runExceptT $ postgrestResponse appState appConf maybeSchemaCache authResult req
response <- either Error.errorResponseFor identity <$> eitherResponse response <- either Error.errorResponseFor identity <$> eitherResponse
-- Launch the connWorker when the connection is down. The postgrest -- Launch the connWorker when the connection is down. The postgrest
-- function can respond successfully (with a stale schema cache) before -- function can respond successfully (with a stale schema cache) before
-- the connWorker is done. -- the connWorker is done. However, when there's an empty schema cache
when (isServiceUnavailable response) connWorker -- postgrest responds with the error `PGRST002`; this means that the schema
-- cache is still loading, so we don't launch the connWorker here because
-- it would duplicate the loading process, e.g. https://github.com/PostgREST/postgrest/issues/3704
-- TODO: this process may be unnecessary when the Listener is enabled. Revisit once https://github.com/PostgREST/postgrest/issues/1766 is done
when (isServiceUnavailable response && isJust maybeSchemaCache) connWorker
resp <- do resp <- do
delay <- AppState.getNextDelay appState delay <- AppState.getNextDelay appState
return $ addRetryHint delay response return $ addRetryHint delay response
@@ -129,16 +155,18 @@ postgrestResponse
:: AppState.AppState :: AppState.AppState
-> AppConfig -> AppConfig
-> Maybe SchemaCache -> Maybe SchemaCache
-> PgVersion
-> AuthResult -> AuthResult
-> Wai.Request -> Wai.Request
-> Handler IO Wai.Response -> Handler IO Wai.Response
postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@AuthResult{..} req = do postgrestResponse appState conf@AppConfig{..} maybeSchemaCache authResult@AuthResult{..} req = do
let observer = AppState.getObserver appState
sCache <- sCache <-
case maybeSchemaCache of case maybeSchemaCache of
Just sCache -> Just sCache ->
return sCache return sCache
Nothing -> Nothing -> do
lift $ observer SchemaCacheEmptyObs
throwError Error.NoSchemaCacheError throwError Error.NoSchemaCacheError
body <- lift $ Wai.strictRequestBody req body <- lift $ Wai.strictRequestBody req
@@ -150,24 +178,32 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@
(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 prefs req body
(planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache (planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
let query = Query.query conf authResult apiReq plan sCache pgVer let mainQ = Query.mainQuery plan conf apiReq authResult configDbPreRequest
logSQL = lift . AppState.getObserver appState . DBQuery (Query.getSQLQuery query) tx = MainTx.mainTx mainQ conf authResult apiReq plan sCache
obsQuery s = when configLogQuery $ observer $ QueryObs mainQ s
(queryTime, queryResult) <- withTiming $ do (txTime, txResult) <- withTiming $ do
case query of case tx of
Query.NoDbQuery r -> pure r MainTx.NoDbTx r -> pure r
Query.DbQuery{..} -> do MainTx.DbTx{..} -> do
dbRes <- lift $ AppState.usePool appState (dqTransaction dqIsoLevel dqTxMode $ runExceptT dqDbHandler) dbRes <- lift $ AppState.usePool appState (dqTransaction dqIsoLevel dqTxMode $ runExceptT dqDbHandler)
let eitherResp = mapLeft Error.PgErr . mapLeft (Error.PgError (Just authRole /= configDbAnonRole)) $ dbRes let eitherResp = join $ mapLeft (Error.PgErr . Error.PgError (Just authRole /= configDbAnonRole)) dbRes
when (configLogQuery /= LogQueryDisabled) $ whenLeft eitherResp $ logSQL . Error.status
liftEither eitherResp >>= liftEither -- TODO: we use obsQuery twice, one here and one below because in case of an error with the usePool above, the request will finish here and return an error message.
-- This is because of a combination of ExceptT + our Error module which has Wai.responseLBS.
-- This needs refactoring so only the below obsQuery is used.
lift $ whenLeft eitherResp $ obsQuery . Error.status
liftEither eitherResp
(respTime, resp) <- withTiming $ do (respTime, resp) <- withTiming $ do
let response = Response.actionResponse queryResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile let response = Response.actionResponse txResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
when (configLogQuery /= LogQueryDisabled) $ logSQL $ either Error.status Response.pgrstStatus response status' = either Error.status Response.pgrstStatus response
-- TODO: see above obsQuery, only this obsQuery should remain after refactoring (because the QueryObs depends on the status)
lift $ obsQuery status'
liftEither response liftEither response
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime queryTime respTime) resp return $ toWaiResponse (ServerTiming jwtTime parseTime planTime txTime respTime) resp
where where
toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response toWaiResponse :: ServerTiming -> Response.PgrstResponse -> Wai.Response
@@ -199,3 +235,40 @@ addRetryHint delay response = do
isServiceUnavailable :: Wai.Response -> Bool isServiceUnavailable :: Wai.Response -> Bool
isServiceUnavailable response = Wai.responseStatus response == HTTP.status503 isServiceUnavailable response = Wai.responseStatus response == HTTP.status503
type AppSockets = (NS.Socket, Maybe NS.Socket)
initSockets :: AppConfig -> IO AppSockets
initSockets AppConfig{..} = do
let
cfg'usp = configServerUnixSocket
cfg'uspm = configServerUnixSocketMode
cfg'host = configServerHost
cfg'port = configServerPort
cfg'adminHost = configAdminServerHost
cfg'adminPort = configAdminServerPort
sock <- case cfg'usp of
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
-- but we need to have runtime error if we try to use it in Windows, not compile time error
Just path -> createAndBindDomainSocket path cfg'uspm
Nothing -> do
(_, sock) <-
if cfg'port /= 0
then do
sock <- bindPortTCP cfg'port (fromString $ T.unpack cfg'host)
pure (cfg'port, sock)
else do
-- explicitly bind to a random port, returning bound port number
(num, sock) <- bindRandomPortTCP (fromString $ T.unpack cfg'host)
pure (num, sock)
pure sock
adminSock <- case cfg'adminPort of
Just adminPort -> do
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'adminHost)
pure $ Just adminSock
Nothing -> pure Nothing
pure (sock, adminSock)
+24 -77
View File
@@ -13,11 +13,9 @@ module PostgREST.AppState
, getNextListenerDelay , getNextListenerDelay
, getTime , getTime
, getJwtCacheState , getJwtCacheState
, getSocketREST
, getSocketAdmin
, init , init
, initSockets
, initWithPool , initWithPool
, putConfig -- For tests TODO refactoring
, putNextListenerDelay , putNextListenerDelay
, putSchemaCache , putSchemaCache
, putPgVersion , putPgVersion
@@ -32,20 +30,18 @@ module PostgREST.AppState
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import Data.Either.Combinators (whenLeft) import Data.Either.Combinators (whenLeft)
import qualified Data.Text as T (unpack)
import qualified Hasql.Pool as SQL import qualified Hasql.Pool as SQL
import qualified Hasql.Pool.Config as SQL import qualified Hasql.Pool.Config as SQL
import qualified Hasql.Session as SQL 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 PostgREST.Auth.JwtCache as JwtCache 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
import PostgREST.Observation import PostgREST.Observation
import PostgREST.TimeIt (timeItT)
import PostgREST.Version (prettyVersion) import PostgREST.Version (prettyVersion)
import System.TimeIt (timeItT)
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate,
updateAction) updateAction)
@@ -57,7 +53,7 @@ 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.Auth.JwtCache (JwtCacheState, update)
import PostgREST.Config (AppConfig (..), import PostgREST.Config (AppConfig (..),
addFallbackAppName, addFallbackAppName,
readAppConfig) readAppConfig)
@@ -69,11 +65,8 @@ import PostgREST.Config.PgVersion (PgVersion (..),
import PostgREST.SchemaCache (SchemaCache (..), import PostgREST.SchemaCache (SchemaCache (..),
querySchemaCache, querySchemaCache,
showSummary) showSummary)
import PostgREST.SchemaCache.Identifiers (dumpQi) import PostgREST.SchemaCache.Identifiers (quoteQi)
import PostgREST.Unix (createAndBindDomainSocket)
import Data.Streaming.Network (bindPortTCP, bindRandomPortTCP)
import Data.String (IsString (..))
import Protolude import Protolude
data AppState = AppState data AppState = AppState
@@ -99,10 +92,6 @@ 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
-- | Network socket for REST API
, stateSocketREST :: NS.Socket
-- | Network socket for the admin UI
, stateSocketAdmin :: Maybe NS.Socket
-- | Observation handler -- | Observation handler
, stateObserver :: ObservationHandler , stateObserver :: ObservationHandler
-- | JWT Cache -- | JWT Cache
@@ -117,8 +106,6 @@ data SchemaCacheStatus
| SCPending | SCPending
deriving Eq deriving Eq
type AppSockets = (NS.Socket, Maybe NS.Socket)
init :: AppConfig -> IO AppState init :: AppConfig -> IO AppState
init conf@AppConfig{configLogLevel, configDbPoolSize} = do init conf@AppConfig{configLogLevel, configDbPoolSize} = do
loggerState <- Logger.init loggerState <- Logger.init
@@ -127,14 +114,11 @@ init conf@AppConfig{configLogLevel, configDbPoolSize} = do
observer $ AppStartObs prettyVersion observer $ AppStartObs prettyVersion
jwtCacheState <- JwtCache.init
pool <- initPool conf observer pool <- initPool conf observer
(sock, adminSock) <- initSockets conf initWithPool pool conf loggerState metricsState observer --{ stateSocketREST = sock, stateSocketAdmin = adminSock}
state' <- initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState observer
pure state' { stateSocketREST = sock, stateSocketAdmin = adminSock}
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> JwtCache.JwtCacheState -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState initWithPool :: SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState observer = do initWithPool pool conf loggerState metricsState observer = do
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 +131,8 @@ initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState
<*> myThreadId <*> myThreadId
<*> newIORef 0 <*> newIORef 0
<*> newIORef 1 <*> newIORef 1
<*> pure sock
<*> pure adminSock
<*> pure observer <*> pure observer
<*> pure jwtCacheState <*> JwtCache.init conf observer
<*> pure loggerState <*> pure loggerState
<*> pure metricsState <*> pure metricsState
@@ -167,40 +149,6 @@ initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState
destroy :: AppState -> IO () destroy :: AppState -> IO ()
destroy = destroyPool destroy = destroyPool
initSockets :: AppConfig -> IO AppSockets
initSockets AppConfig{..} = do
let
cfg'usp = configServerUnixSocket
cfg'uspm = configServerUnixSocketMode
cfg'host = configServerHost
cfg'port = configServerPort
cfg'adminHost = configAdminServerHost
cfg'adminPort = configAdminServerPort
sock <- case cfg'usp of
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
-- but we need to have runtime error if we try to use it in Windows, not compile time error
Just path -> createAndBindDomainSocket path cfg'uspm
Nothing -> do
(_, sock) <-
if cfg'port /= 0
then do
sock <- bindPortTCP cfg'port (fromString $ T.unpack cfg'host)
pure (cfg'port, sock)
else do
-- explicitly bind to a random port, returning bound port number
(num, sock) <- bindRandomPortTCP (fromString $ T.unpack cfg'host)
pure (num, sock)
pure sock
adminSock <- case cfg'adminPort of
Just adminPort -> do
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'adminHost)
pure $ Just adminSock
Nothing -> pure Nothing
pure (sock, adminSock)
initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool
initPool AppConfig{..} observer = do initPool AppConfig{..} observer = do
SQL.acquire $ SQL.settings SQL.acquire $ SQL.settings
@@ -271,10 +219,14 @@ usePool AppState{stateObserver=observer, stateMainThreadId=mainThreadId, ..} ses
-- | Flush the connection pool so that any future use of the pool will -- | Flush the connection pool so that any future use of the pool will
-- use connections freshly established after this call. -- use connections freshly established after this call.
-- | Emits PoolFlushed observation
flushPool :: AppState -> IO () flushPool :: AppState -> IO ()
flushPool AppState{..} = SQL.release statePool flushPool AppState{..} = do
SQL.release statePool
stateObserver PoolFlushed
-- | Destroy the pool on shutdown. -- | Destroy the pool on shutdown.
-- | Differs from flushPool in not emiting PoolFlushed observation.
destroyPool :: AppState -> IO () destroyPool :: AppState -> IO ()
destroyPool AppState{..} = SQL.release statePool destroyPool AppState{..} = SQL.release statePool
@@ -314,12 +266,6 @@ getTime = stateGetTime
getJwtCacheState :: AppState -> JwtCacheState getJwtCacheState :: AppState -> JwtCacheState
getJwtCacheState = stateJwtCache getJwtCacheState = stateJwtCache
getSocketREST :: AppState -> NS.Socket
getSocketREST = stateSocketREST
getSocketAdmin :: AppState -> Maybe NS.Socket
getSocketAdmin = stateSocketAdmin
getMainThreadId :: AppState -> ThreadId getMainThreadId :: AppState -> ThreadId
getMainThreadId = stateMainThreadId getMainThreadId = stateMainThreadId
@@ -367,8 +313,6 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
observer $ ConnectionRetryObs delay observer $ ConnectionRetryObs delay
putNextListenerDelay appState delay putNextListenerDelay appState delay
flushPool appState
(,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache) (,) <$> qPgVersion <*> (qInDbConfig *> qSchemaCache)
) )
where where
@@ -383,10 +327,12 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
observer ExitDBNoRecoveryObs observer ExitDBNoRecoveryObs
killThread mainThreadId killThread mainThreadId
return Nothing return Nothing
Right actualPgVersion -> do Right actualPgVersion ->
when (actualPgVersion < minimumPgVersion) $ do if actualPgVersion < minimumPgVersion then do
observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion
killThread mainThreadId killThread mainThreadId
return Nothing
else do
observer $ DBConnectedObs $ pgvFullName actualPgVersion observer $ DBConnectedObs $ pgvFullName actualPgVersion
observer $ PoolInit configDbPoolSize observer $ PoolInit configDbPoolSize
putPgVersion appState actualPgVersion putPgVersion appState actualPgVersion
@@ -407,7 +353,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 e observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
return Nothing return Nothing
Right sCache -> do Right sCache -> do
@@ -415,6 +361,10 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
-- IORef on putSchemaCache. This is why SCacheStatus is put at SCPending here to signal the Admin server (using isPending) that we're on a recovery state. -- IORef on putSchemaCache. This is why SCacheStatus is put at SCPending here to signal the Admin server (using isPending) that we're on a recovery state.
putSCacheStatus appState SCPending putSCacheStatus appState SCPending
putSchemaCache appState $ Just sCache putSchemaCache appState $ Just sCache
-- Flush the pool after loading the schema cache to reset any stale session cache entries
-- We do it after successfully querying the schema cache (because this can fail and during retries we would flush the pool repeatedly unnecessarily)
-- and after marking sCacheStatus as pending,
flushPool appState
observer $ SchemaCacheQueriedObs resultTime observer $ SchemaCacheQueriedObs resultTime
(t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache (t, _) <- timeItT $ observer $ SchemaCacheSummaryObs $ showSummary sCache
observer $ SchemaCacheLoadedObs t observer $ SchemaCacheLoadedObs t
@@ -442,7 +392,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
pgVer <- getPgVersion appState pgVer <- getPgVersion appState
dbSettings <- dbSettings <-
if configDbConfig conf then do if configDbConfig conf then do
qDbSettings <- usePool appState (queryDbSettings (dumpQi <$> configDbPreConfig conf) (configDbPreparedStatements conf)) qDbSettings <- usePool appState (queryDbSettings (quoteQi <$> configDbPreConfig conf) (configDbPreparedStatements conf))
case qDbSettings of case qDbSettings of
Left e -> do Left e -> do
observer $ ConfigReadErrorObs e observer $ ConfigReadErrorObs e
@@ -471,10 +421,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
-- After the config has reloaded, jwt-secret might have changed, so -- After the config has reloaded, jwt-secret might have changed, so
-- if it has changed, it is important to invalidate the jwt cache -- if it has changed, it is important to invalidate the jwt cache
-- entries, because they were cached using the old secret -- entries, because they were cached using the old secret
if configJwtSecret conf == configJwtSecret newConf then update (getJwtCacheState appState) newConf
pass
else
JwtCache.emptyCache (getJwtCacheState appState) -- atomic O(1) operation
if startingUp then if startingUp then
pass pass
+12 -146
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE RecordWildCards #-}
{-| {-|
Module : PostgREST.Auth Module : PostgREST.Auth
Description : PostgREST authentication functions. Description : PostgREST authentication functions.
@@ -10,8 +11,6 @@ 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 #-}
module PostgREST.Auth module PostgREST.Auth
( getResult ( getResult
, getJwtDur , getJwtDur
@@ -19,177 +18,44 @@ module PostgREST.Auth
, middleware , middleware
) where ) where
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM
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.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 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.Monad.Except (liftEither)
import Data.Either.Combinators (mapLeft)
import Data.List (lookup) import Data.List (lookup)
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds) import PostgREST.TimeIt (timeItT)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import System.IO.Unsafe (unsafePerformIO) import System.IO.Unsafe (unsafePerformIO)
import System.TimeIt (timeItT)
import PostgREST.AppState (AppState, getConfig, getJwtCacheState, import PostgREST.AppState (AppState, getConfig, getJwtCacheState,
getTime) getTime)
import PostgREST.Auth.Jwt (parseClaims)
import PostgREST.Auth.JwtCache (lookupJwtCache) import PostgREST.Auth.JwtCache (lookupJwtCache)
import PostgREST.Auth.Types (AuthResult (..)) import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..), FilterExp (..), import PostgREST.Config (AppConfig (..))
JSPath, JSPathExp (..)) import PostgREST.Error (Error (..))
import PostgREST.Error (Error (..), JwtError (..))
import Protolude import Protolude
-- | Receives the JWT secret and audience (from config) and a JWT and returns a -- | Validate authorization header
-- JSON object of JWT claims.
parseToken :: AppConfig -> Maybe ByteString -> UTCTime -> ExceptT Error IO JSON.Value
parseToken _ Nothing _ = return JSON.emptyObject
parseToken _ (Just "") _ = throwE . JwtErr $ JwtDecodeError "Empty JWT is sent in Authorization header"
parseToken AppConfig{..} (Just tkn) time = do
secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
tknWith3Parts <- liftEither $ hasThreeParts tkn
eitherContent <- liftIO $ JWT.decode (JWT.keys secret) Nothing tknWith3Parts
content <- liftEither . mapLeft (JwtErr . jwtDecodeError) $ eitherContent
liftEither $ mapLeft JwtErr $ verifyClaims content
where
hasThreeParts :: ByteString -> Either Error ByteString
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
3 -> Right token
n -> Left $ JwtErr $ JwtDecodeError ("Expected 3 parts in JWT; got " <> show n)
jwtDecodeError :: JWT.JwtError -> JwtError
-- The only errors we can get from JWT.decode function are:
-- BadAlgorithm
-- KeyError
-- BadCrypto
jwtDecodeError (JWT.KeyError _) = JwtDecodeError "No suitable key or wrong key type"
jwtDecodeError (JWT.BadAlgorithm _) = JwtDecodeError "Wrong or unsupported encoding algorithm"
jwtDecodeError JWT.BadCrypto = JwtDecodeError "JWT cryptographic operation failed"
-- Control never reaches here, the decode function only returns the above three
jwtDecodeError _ = JwtDecodeError "JWT couldn't be decoded"
verifyClaims :: JWT.JwtContent -> Either JwtError JSON.Value
verifyClaims (JWT.Jws (_, claims)) = case JSON.decodeStrict claims of
Just jclaims@(JSON.Object mclaims) ->
verifyClaim mclaims "exp" isValidExpClaim "JWT expired" >>
verifyClaim mclaims "nbf" isValidNbfClaim "JWT not yet valid" >>
verifyClaim mclaims "iat" isValidIatClaim "JWT issued at future" >>
verifyClaim mclaims "aud" isValidAudClaim "JWT not in audience" >>
return jclaims
_ -> Left $ JwtClaimsError "Parsing claims failed"
-- TODO: We could enable JWE support here (encrypted tokens)
verifyClaims _ = Left $ JwtDecodeError "Unsupported token type"
verifyClaim mclaims claim func err = do
isValid <- maybe (Right True) func (KM.lookup claim mclaims)
unless isValid $ Left $ JwtClaimsError err
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 $ JwtClaimsError "The JWT 'exp' claim must be a number"
isValidNbfClaim :: JSON.Value -> Either JwtError Bool
isValidNbfClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
isValidNbfClaim _ = Left $ JwtClaimsError "The JWT 'nbf' claim must be a number"
isValidIatClaim :: JSON.Value -> Either JwtError Bool
isValidIatClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
isValidIatClaim _ = Left $ JwtClaimsError "The JWT 'iat' claim must be a number"
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 $ JwtClaimsError "The JWT 'aud' claim must be a string or an array of strings"
parseClaims :: Monad m =>
AppConfig -> JSON.Value -> ExceptT Error m AuthResult
parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
-- role defaults to anon if not specified in jwt
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
unquoted <$> walkJSPath (Just jclaims) configJwtRoleClaimKey <|> configDbAnonRole
return AuthResult
{ authClaims = mclaims & KM.insert "role" (JSON.toJSON $ decodeUtf8 role)
, authRole = role
}
where
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
walkJSPath x [] = x
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)) [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
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.String t) = encodeUtf8 t
unquoted v = LBS.toStrict $ JSON.encode v
-- impossible case - just added to please -Wincomplete-patterns
parseClaims _ _ = return AuthResult { authClaims = KM.empty, authRole = mempty }
-- | Validate authorization header.
-- Parse and store JWT claims for future use in the request. -- Parse and store JWT claims for future use in the request.
middleware :: AppState -> Wai.Middleware middleware :: AppState -> Wai.Middleware
middleware appState app req respond = do middleware appState app req respond = do
conf <- getConfig appState conf@AppConfig{..} <- getConfig appState
time <- getTime appState time <- getTime appState
let token = Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req) let token = Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
parseJwt = runExceptT $ parseToken conf token time >>= parseClaims conf parseJwt = runExceptT $ lookupJwtCache jwtCacheState token >>= parseClaims conf time
jwtCacheState = getJwtCacheState appState jwtCacheState = getJwtCacheState appState
-- If ServerTimingEnabled -> calculate JWT validation time -- If ServerTimingEnabled -> calculate JWT validation time
-- If JwtCacheMaxLifetime -> cache JWT validation result req' <- if configServerTimingEnabled then do
req' <- case (configServerTimingEnabled conf, configJwtCacheMaxLifetime conf) of
(True, 0) -> do
(dur, authResult) <- timeItT parseJwt (dur, authResult) <- timeItT parseJwt
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur } pure $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
else do
(True, maxLifetime) -> do
(dur, authResult) <- timeItT $ case token of
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 }
(False, 0) -> do
authResult <- parseJwt authResult <- parseJwt
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult } pure $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
(False, maxLifetime) -> do
authResult <- case token of
Just tkn -> lookupJwtCache jwtCacheState tkn maxLifetime parseJwt time
Nothing -> parseJwt
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
app req' respond app req' respond
+151
View File
@@ -0,0 +1,151 @@
{-|
Module : PostgREST.Auth.Jwt
Description : PostgREST JWT support functions.
This module provides functions to deal with JWT parsing and validation (http://jwt.io).
-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ImpredicativeTypes #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE QuantifiedConstraints #-}
module PostgREST.Auth.Jwt
( parseAndDecodeClaims
, parseClaims) where
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Key as K
import qualified Data.Aeson.KeyMap as KM
import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
import qualified Data.Scientific as Sci
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Jose.Jwk as JWT
import qualified Jose.Jwt as JWT
import Control.Monad.Except (liftEither)
import Data.Either.Combinators (mapLeft)
import Data.Text ()
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..), FilterExp (..), JSPath,
JSPathExp (..), audMatchesCfg)
import PostgREST.Error (Error (..),
JwtClaimsError (AudClaimNotStringOrArray, ExpClaimNotNumber, IatClaimNotNumber, JWTExpired, JWTIssuedAtFuture, JWTNotInAudience, JWTNotYetValid, NbfClaimNotNumber, ParsingClaimsFailed),
JwtDecodeError (..), JwtError (..))
import Data.Aeson ((.:?))
import Data.Aeson.Types (parseMaybe)
import Jose.Jwk (JwkSet)
import Protolude hiding (first)
parseAndDecodeClaims :: (MonadError Error m, MonadIO m) => JwkSet -> ByteString -> m JSON.Object
parseAndDecodeClaims jwkSet token = parseToken jwkSet token >>= decodeClaims
decodeClaims :: MonadError Error m => JWT.JwtContent -> m JSON.Object
decodeClaims (JWT.Jws (_, claims)) = maybe (throwError (JwtErr $ JwtClaimsErr ParsingClaimsFailed)) pure (JSON.decodeStrict claims)
decodeClaims _ = throwError $ JwtErr $ JwtDecodeErr UnsupportedTokenType
validateClaims :: MonadError Error m => UTCTime -> (Text -> Bool) -> JSON.Object -> m ()
validateClaims time audMatches claims = liftEither $ maybeToLeft () (fmap JwtErr . getAlt $ JwtClaimsErr <$> checkForErrors time audMatches claims)
data ValidAud = VAString Text | VAArray [Text] deriving Generic
instance JSON.FromJSON ValidAud where
parseJSON = JSON.genericParseJSON JSON.defaultOptions { JSON.sumEncoding = JSON.UntaggedValue }
checkForErrors :: (Applicative m, Monoid (m JwtClaimsError)) => UTCTime -> (Text -> Bool) -> JSON.Object -> m JwtClaimsError
checkForErrors time audMatches = mconcat
[
claim "exp" ExpClaimNotNumber $ inThePast JWTExpired
, claim "nbf" NbfClaimNotNumber $ inTheFuture JWTNotYetValid
, claim "iat" IatClaimNotNumber $ inTheFuture JWTIssuedAtFuture
, claim "aud" AudClaimNotStringOrArray $ checkValue (not . validAud) JWTNotInAudience
]
where
allowedSkewSeconds = 30 :: Int64
sciToInt = fromMaybe 0 . Sci.toBoundedInteger
toSec = floor . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds
now = toSec time
inTheFuture = checkTime ((now + allowedSkewSeconds) <)
inThePast = checkTime ((now - allowedSkewSeconds) >)
checkTime cond = checkValue (cond. sciToInt)
validAud = \case
(VAString aud) -> audMatches aud
(VAArray auds) -> null auds || any audMatches auds
checkValue invalid msg val =
if invalid val then
pure msg
else
mempty
claim key parseError checkParsed = maybe (pure parseError) (maybe mempty checkParsed) . parseMaybe (.:? key)
-- | Receives the JWT secret and audience (from config) and a JWT and returns a
-- JSON object of JWT claims.
parseToken :: (MonadError Error m, MonadIO m) => JwkSet -> ByteString -> m JWT.JwtContent
parseToken _ "" = throwError $ JwtErr $ JwtDecodeErr EmptyAuthHeader
parseToken secret tkn = do
-- secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
tknWith3Parts <- hasThreeParts tkn
eitherContent <- liftIO $ JWT.decode (JWT.keys secret) Nothing tknWith3Parts
liftEither . mapLeft (JwtErr . jwtDecodeError) $ eitherContent
--liftEither $ mapLeft JwtErr $ verifyClaims content
where
--hasThreeParts :: ByteString -> Either Error ByteString
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
3 -> pure token
n -> throwError $ 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
parseClaims :: (MonadError Error m, MonadIO m) => AppConfig -> UTCTime -> JSON.Object -> m AuthResult
parseClaims cfg@AppConfig{configJwtRoleClaimKey, configDbAnonRole} time mclaims = do
validateClaims time (audMatchesCfg cfg) mclaims
-- role defaults to anon if not specified in jwt
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
unquoted <$> walkJSPath (Just $ JSON.Object mclaims) configJwtRoleClaimKey <|> configDbAnonRole
pure AuthResult
{ authClaims = mclaims & KM.insert "role" (JSON.toJSON $ decodeUtf8 role)
, authRole = role
}
where
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
walkJSPath x [] = x
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)) [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
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.String t) = encodeUtf8 t
unquoted v = LBS.toStrict $ JSON.encode v
+92 -77
View File
@@ -1,99 +1,114 @@
{-| {-|
Module : PostgREST.Auth.JwtCache Module : PostgREST.Auth.JwtCache
Description : PostgREST Jwt Authentication Result Cache. Description : PostgREST JWT validation results Cache.
This module provides functions to deal with the JWT cache This module provides functions to deal with the JWT cache.
-} -}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE StrictData #-}
module PostgREST.Auth.JwtCache module PostgREST.Auth.JwtCache
( init ( init
, update
, JwtCacheState , JwtCacheState
, lookupJwtCache , lookupJwtCache
, emptyCache
) where ) where
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.Cache as C
import qualified Data.Scientific as Sci
import Control.Debounce import PostgREST.Error (Error (..), JwtError (JwtSecretMissing))
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 Control.Concurrent.STM (newTVarIO, readTVar,
writeTVar)
import Control.Concurrent.STM.TVar (TVar)
import Control.Monad.Error.Class (liftEither)
import Data.ByteString hiding (all, init)
import Data.IORef (IORef, newIORef,
readIORef, writeIORef)
import Jose.Jwk (JwkSet)
import PostgREST.Auth.Jwt (parseAndDecodeClaims)
import PostgREST.Cache.Sieve (alwaysValid)
import qualified PostgREST.Cache.Sieve as SC
import PostgREST.Config (AppConfig (..))
import PostgREST.Observation (Observation (JwtCacheEviction, JwtCacheLookup),
ObservationHandler)
import Protolude import Protolude
-- | JWT Cache and IO action that triggers purging old entries from the cache data JwtCacheState = JwtCacheState ObservationHandler (IORef JwtCache)
data JwtCacheState = JwtCacheState
{ jwtCache :: C.Cache ByteString AuthResult class CacheVariant m v where
, purgeCache :: IO () cached :: SC.Cache m ByteString v -> ByteString -> ExceptT Error IO JSON.Object
}
{-|
Jwt caching can have three different configurations:
* missing JWT Key (no caching and throw error when JWT token present in the request)
* JWT cache turned off
* JWT cache turned on
All three options are represented by JwtCache data type.
Handling of reconfiguration is centralized in this module.
-}
data JwtCache =
JwtNoJwks |
JwtNoCache JwkSet |
forall m v. CacheVariant m v => JwtCache JwkSet (TVar Int) (SC.Cache m ByteString v)
instance CacheVariant IO (Either Error JSON.Object) where
cached c = lift . SC.cached c >=> liftEither
instance CacheVariant (ExceptT Error IO) JSON.Object where
cached = SC.cached
decode :: JwtCache -> ByteString -> ExceptT Error IO JSON.Object
decode JwtNoJwks = const $ throwError (JwtErr JwtSecretMissing)
decode (JwtNoCache key) = parseAndDecodeClaims key
decode (JwtCache _ _ c) = cached c
-- | Reconfigure JWT caching and update JwtCacheState accordingly
update :: JwtCacheState -> AppConfig -> IO ()
update (JwtCacheState observationHandler jwtCacheState) config@AppConfig{configJWKS, configJwtCacheMaxEntries} =
let reinitialize =
newJwtCache config observationHandler
>>= writeIORef jwtCacheState
in
readIORef jwtCacheState >>= \case
(JwtCache decodingKey maxSize _) ->
if configJWKS /= Just decodingKey || configJwtCacheMaxEntries <= 0 then
-- reinitialize if key changed or cache disabled
reinitialize
else
-- max size changed - set it and let the cache shrink itself if necessary
atomically $ writeTVar maxSize configJwtCacheMaxEntries
_ -> reinitialize
init :: AppConfig -> ObservationHandler -> IO JwtCacheState
init config = fmap (<$>) JwtCacheState <*> (newJwtCache config >=> newIORef)
-- | Initialize JwtCacheState -- | Initialize JwtCacheState
init :: IO JwtCacheState newJwtCache :: AppConfig -> ObservationHandler -> IO JwtCache
init = do newJwtCache AppConfig{configJWKS, configJwtCacheMaxEntries} observationHandler = do
cache <- C.newCache Nothing -- no default expiration maybe (pure JwtNoJwks) initCache configJWKS
-- purgeExpired has O(n^2) complexity where
-- so we wrap it in debounce to make sure it: initCache key = if configJwtCacheMaxEntries <= 0 then pure (JwtNoCache key) else createCache key configJwtCacheMaxEntries
-- 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 createCache key maxSize = do
lookupJwtCache :: JwtCacheState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult) maxSizeTVar <- newTVarIO maxSize
lookupJwtCache JwtCacheState{jwtCache, purgeCache} token maxLifetime parseJwt utc = do JwtCache key maxSizeTVar <$>
checkCache <- C.lookup jwtCache token notCachingErrors (readTVar maxSizeTVar) key
authResult <- maybe parseJwt (pure . Right) checkCache
case (authResult,checkCache) of notCachingErrors :: STM Int -> JwkSet -> IO (SC.Cache (ExceptT Error IO) ByteString JSON.Object)
-- From comment: notCachingErrors maxSize key = SC.cacheIO (SC.CacheConfig maxSize
-- https://github.com/PostgREST/postgrest/pull/3801#discussion_r1857987914 (parseAndDecodeClaims key)
-- (lift . observationHandler . JwtCacheLookup) -- lookup metrics
-- We purge expired cache entries on a cache miss (const . const $ lift $ observationHandler JwtCacheEviction) -- evictions metrics
-- The reasoning is that: alwaysValid) -- no invalidation for now
--
-- 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 lookupJwtCache :: JwtCacheState -> Maybe ByteString -> ExceptT Error IO JSON.Object
lookupJwtCache (JwtCacheState _ cacheState) k = liftIO (readIORef cacheState) >>= flip (maybe (pure KM.empty)) k . decode
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
+29 -123
View File
@@ -1,5 +1,4 @@
{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
module PostgREST.CLI module PostgREST.CLI
( main ( main
@@ -14,8 +13,6 @@ import qualified Data.ByteString.Lazy as LBS
import qualified Hasql.Transaction.Sessions as SQL import qualified Hasql.Transaction.Sessions as SQL
import qualified Options.Applicative as O import qualified Options.Applicative as O
import Text.Heredoc (str)
import PostgREST.AppState (AppState) import PostgREST.AppState (AppState)
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.Observation (Observation (..)) import PostgREST.Observation (Observation (..))
@@ -24,6 +21,7 @@ import PostgREST.Version (prettyVersion)
import qualified PostgREST.App as App import qualified PostgREST.App as App
import qualified PostgREST.AppState as AppState import qualified PostgREST.AppState as AppState
import qualified PostgREST.Client as Client
import qualified PostgREST.Config as Config import qualified PostgREST.Config as Config
import Protolude import Protolude
@@ -31,16 +29,26 @@ import Protolude
main :: CLI -> IO () main :: CLI -> IO ()
main CLI{cliCommand, cliPath} = do main CLI{cliCommand, cliPath} = do
conf@AppConfig{..} <- conf <-
either panic identity <$> Config.readAppConfig mempty cliPath Nothing mempty mempty either panic identity <$> Config.readAppConfig mempty cliPath Nothing mempty mempty
case cliCommand of
Client adminCmd -> runClientCommand conf adminCmd
Run runCmd -> runAppCommand conf runCmd
-- | Run command using http-client to communicate with an already running postgrest
runClientCommand :: AppConfig -> ClientCommand -> IO ()
runClientCommand conf CmdReady = Client.ready conf
-- | Run postgrest with command
runAppCommand :: AppConfig -> RunCommand -> IO ()
runAppCommand conf@AppConfig{..} runCmd = do
-- Per https://github.com/PostgREST/postgrest/issues/268, we want to -- Per https://github.com/PostgREST/postgrest/issues/268, we want to
-- explicitly close the connections to PostgreSQL on shutdown. -- explicitly close the connections to PostgreSQL on shutdown.
-- 'AppState.destroy' takes care of that. -- 'AppState.destroy' takes care of that.
bracket bracket
(AppState.init conf) (AppState.init conf)
AppState.destroy AppState.destroy
(\appState -> case cliCommand of (\appState -> case runCmd of
CmdDumpConfig -> do CmdDumpConfig -> do
when configDbConfig $ AppState.readInDbConfig True appState when configDbConfig $ AppState.readInDbConfig True appState
putStr . Config.toText =<< AppState.getConfig appState putStr . Config.toText =<< AppState.getConfig appState
@@ -60,7 +68,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 e observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
exitFailure exitFailure
Right sCache -> return $ JSON.encode sCache Right sCache -> return $ JSON.encode sCache
@@ -71,6 +79,13 @@ data CLI = CLI
} }
data Command data Command
= Client ClientCommand
| Run RunCommand
data ClientCommand
= CmdReady
data RunCommand
= CmdRun = CmdRun
| CmdDumpConfig | CmdDumpConfig
| CmdDumpSchema | CmdDumpSchema
@@ -97,7 +112,7 @@ readCLIShowHelp =
<> O.help "Show the version information" <> O.help "Show the version information"
exampleParser = exampleParser =
O.infoOption exampleConfigFile $ O.infoOption Config.exampleConfigFile $
O.long "example" O.long "example"
<> O.short 'e' <> O.short 'e'
<> O.help "Show an example configuration file" <> O.help "Show an example configuration file"
@@ -105,7 +120,7 @@ readCLIShowHelp =
cliParser :: O.Parser CLI cliParser :: O.Parser CLI
cliParser = cliParser =
CLI CLI
<$> (dumpConfigFlag <|> dumpSchemaFlag) <$> (dumpConfigFlag <|> dumpSchemaFlag <|> readyFlag)
<*> O.optional configFileOption <*> O.optional configFileOption
configFileOption = configFileOption =
@@ -114,125 +129,16 @@ readCLIShowHelp =
<> O.help "Path to configuration file" <> O.help "Path to configuration file"
dumpConfigFlag = dumpConfigFlag =
O.flag CmdRun CmdDumpConfig $ O.flag (Run CmdRun) (Run CmdDumpConfig) $
O.long "dump-config" O.long "dump-config"
<> O.help "Dump loaded configuration and exit" <> O.help "Dump loaded configuration and exit"
dumpSchemaFlag = dumpSchemaFlag =
O.flag CmdRun CmdDumpSchema $ O.flag (Run CmdRun) (Run CmdDumpSchema) $
O.long "dump-schema" O.long "dump-schema"
<> O.help "Dump loaded schema as JSON and exit (for debugging, output structure is unstable)" <> O.help "Dump loaded schema as JSON and exit (for debugging, output structure is unstable)"
exampleConfigFile :: [Char] readyFlag =
exampleConfigFile = O.flag (Run CmdRun) (Client CmdReady) $
[str|## Admin server used for checks. It's disabled by default unless a port is specified. O.long "ready"
|# admin-server-port = 3001 <> O.help "Checks the health of PostgREST by doing a request on the admin server /ready endpoint"
|
|## The database role to use when no client authentication is provided
|# db-anon-role = "anon"
|
|## Notification channel for reloading the schema cache
|db-channel = "pgrst"
|
|## Enable or disable the notification channel
|db-channel-enabled = true
|
|## Enable in-database configuration
|db-config = true
|
|## Function for in-database configuration
|## db-pre-config = "postgrest.pre_config"
|
|## Extra schemas to add to the search_path of every request
|db-extra-search-path = "public"
|
|## Limit rows in response
|# db-max-rows = 1000
|
|## Allow getting the EXPLAIN plan through the `Accept: application/vnd.pgrst.plan` header
|# db-plan-enabled = false
|
|## Number of open connections in the pool
|db-pool = 10
|
|## Time in seconds to wait to acquire a slot from the connection pool
|# db-pool-acquisition-timeout = 10
|
|## Time in seconds after which to recycle pool connections
|# db-pool-max-lifetime = 1800
|
|## Time in seconds after which to recycle unused pool connections
|# db-pool-max-idletime = 30
|
|## Allow automatic database connection retrying
|# db-pool-automatic-recovery = true
|
|## Stored proc to exec immediately after auth
|# db-pre-request = "stored_proc_name"
|
|## Enable or disable prepared statements. disabling is only necessary when behind a connection pooler.
|## When disabled, statements will be parametrized but won't be prepared.
|db-prepared-statements = true
|
|## The name of which database schema to expose to REST clients
|db-schemas = "public"
|
|## How to terminate database transactions
|## Possible values are:
|## commit (default)
|## Transaction is always committed, this can not be overriden
|## commit-allow-override
|## Transaction is committed, but can be overriden with Prefer tx=rollback header
|## rollback
|## Transaction is always rolled back, this can not be overriden
|## rollback-allow-override
|## Transaction is rolled back, but can be overriden with Prefer tx=commit header
|db-tx-end = "commit"
|
|## The standard connection URI format, documented at
|## https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING
|db-uri = "postgresql://"
|
|# jwt-aud = "your_audience_claim"
|
|## Jspath to the role claim key
|jwt-role-claim-key = ".role"
|
|## Choose a secret, JSON Web Key (or set) to enable JWT auth
|## (use "@filename" to load from separate file)
|# jwt-secret = "secret_with_at_least_32_characters"
|jwt-secret-is-base64 = false
|
|## Enables and set JWT Cache max lifetime, disables caching with 0
|# jwt-cache-max-lifetime = 0
|
|## Logging level, the admitted values are: crit, error, warn, info and debug.
|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.
|## Admitted values: follow-privileges, ignore-privileges, disabled
|openapi-mode = "follow-privileges"
|
|## Base url for the OpenAPI output
|openapi-server-proxy-uri = ""
|
|## Configurable CORS origins
|# server-cors-allowed-origins = ""
|
|server-host = "!4"
|server-port = 3000
|
|## Allow getting the request-response timing information through the `Server-Timing` header
|server-timing-enabled = false
|
|## Unix socket location
|## if specified it takes precedence over server-port
|# server-unix-socket = "/tmp/pgrst.sock"
|
|## Unix socket file mode
|## When none is provided, 660 is applied by default
|# server-unix-socket-mode = "660"
|]
+218
View File
@@ -0,0 +1,218 @@
{-|
Module : PostgREST.Cache.Sieve
Description : PostgREST cache implementation based on Sieve algorithm.
This module provides implementation of a mutable cache on Sieve algorithm.
-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TupleSections #-}
module PostgREST.Cache.Sieve (
Cache
, CacheConfig (..)
, Discard (..)
, alwaysValid
, cache
, cacheIO
, cached
)
where
import Control.Concurrent.STM
import Control.Monad.Extra (whileM)
import Data.Some
import qualified Focus as F
import Protolude hiding (elem, head)
import qualified StmHamt.SizedHamt as SH
data ListNode k v (b :: Bool) = ListNode {
nextPtr :: NodePtr k v,
prevNextPtrPtr :: NodePtrPtr k v,
elem :: NodeElem k v b
}
data NodeElem :: Type -> Type -> Bool -> Type where
Head :: {
entries :: SH.SizedHamt (HamtEntry k v),
finger :: NodePtrPtr k v
} -> NodeElem k v False
Entry :: Hashable k => {
visited :: TVar Bool,
ekey :: k,
entryValue :: v
} -> NodeElem k v True
type HamtEntry k v = ListNode k v True
type AnyNode k v = Some (ListNode k v)
type NodePtr k v = TVar (AnyNode k v)
type NodePtrPtr k v = TVar (NodePtr k v)
data Discard m v = Refresh (m ()) | Invalid (m v)
data Cache m k v = (MonadIO m, Hashable k) => Cache (ListNode k v False) (CacheConfig m k v)
data CacheConfig m k v = CacheConfig {
maxSize :: STM Int,
load :: k -> m v,
requestListener :: Bool -> m (),
evictionListener :: k -> v -> m (),
validator :: m (k -> v -> Maybe (Discard m v))
}
alwaysValid :: Applicative m => m (k -> v -> Maybe (Discard m v))
alwaysValid = pure (const . const Nothing)
cacheIO :: (MonadIO m, Hashable k) => CacheConfig m k v -> IO (Cache m k v)
cacheIO = atomically . cache
cache :: (MonadIO m, Hashable k) => CacheConfig m k v -> STM (Cache m k v)
cache cacheConfig = mdo
tail <- newTVar (Some head)
entries <- SH.new
finger <- newTVar tail
head <- ListNode tail <$> newTVar tail <*> pure Head {..}
pure $ Cache head cacheConfig
cached :: Cache m k v -> k -> m v
cached (Cache head@ListNode{prevNextPtrPtr=neck, elem=Head{..}} CacheConfig{..}) k = do
checkValid <- validator
tryMaybe
-- Fast path: lookup value, update stats and return the value if found and valid
((liftIO . atomically) (lookup checkValid) >>= notify (requestListener . isJust) >>= validate)
-- Slow path: load/calculate value and insert it (if still not found)
(do
value <- load k
whileM (not <$> tryInsert value)
pure value)
where
tryMaybe f notFound = f >>= maybe notFound pure
notify = ((<$) <*>)
validate = fmap join . traverse (\case
-- valid value
(Right v) -> pure $ Just v
-- refresh value
(Left (Refresh act)) -> act $> Nothing
-- discard value and return alt result
(Left (Invalid res)) -> Just <$> res)
lookup checkValid = SH.focus focus (ekey . elem) k entries
where
focus = F.Focus
-- not found
(pure (Nothing, F.Leave))
-- found
-- check entry validity
(\e@ListNode{elem=Entry{visited, entryValue}} ->
maybe
-- entry valid
(mark visited True $> (Just $ Right entryValue, F.Leave))
-- entry invalid
-- remove it
((removeEntry e $>) . (, F.Remove) . Just . Left)
(checkValid k entryValue)
)
mark t b = whenM ((/= b) <$> readTVar t) (writeTVar t b)
-- perform a single entry eviction and possibly insertion atomically
-- returning False if could not insert
-- (either because entry currently pointed by the finger was visited
-- or because after this entry eviction the cache is still full)
-- so that other threads don't have to wait when visiting entries.
-- First check if entry is still not in the cache - this time inside transaction.
--
-- Execute evictionListener if an entry was evicted
tryInsert value = do
(result, evicted) <- liftIO . atomically $ do
-- Use SH.focus to performa a single lookup instead of 2
-- we cannot modify Hamt from inside focus
-- so if there is any entry to remove
-- we need to delete it after
(res, evictedKey) <- SH.focus focus (ekey . elem) k entries
case evictedKey of
(Just Entry{ekey=entryKey, entryValue}) -> do
SH.focus F.delete (ekey . elem) entryKey entries
pure (res, evictionListener entryKey entryValue)
Nothing -> pure (res, pure ())
evicted $> result
where
focus = F.Focus (do
(hasSpace, evictedKey) <- evictionStep
if hasSpace then do
entry <- newLinkedEntry value
-- done, maybe evicted, insert entry
pure ((True, evictedKey), F.Set entry)
else
-- not done, maybe evicted, don't modify entries
pure ((False, evictedKey), F.Leave))
-- Entry found case
(\ListNode{elem=Entry{visited}} -> do
-- mark as visited
mark visited True
-- done, no evictions, don't modify entries
pure ((True, Nothing), F.Leave))
-- if the cache is full precoesses a single node
-- removing it if it is marked as unvisited
-- or clearing visited mark
-- returns True if there is space in the cache
-- puts evictionListener in state if an entry was evicted
evictionStep = do
currDiff <- liftA2 (-) (SH.size entries) (max 1 <$> maxSize)
if currDiff >= 0 then do
-- no space in the cache
-- need to evict an entry
(nextFinger, evictedKey) <- readTVar finger >>= evict
writeTVar finger nextFinger
-- return if enough space and evicted key if any
pure (isJust evictedKey && currDiff == 0, evictedKey)
else
-- there is space in the cache
pure (True, Nothing)
evict :: TVar (Some (ListNode k v)) -> STM (NodePtr k v, Maybe (NodeElem k v True))
evict = readTVar >=> \case
(Some e@ListNode{nextPtr, prevNextPtrPtr, elem=elem@Entry{visited}}) -> do
ifM (readTVar visited)
(writeTVar visited False $> (nextPtr, Nothing))
(unlinkEntry e *> fmap (, Just elem) (readTVar prevNextPtrPtr))
-- skip head
(Some ListNode{nextPtr, elem=Head{}}) -> evict nextPtr
unlinkEntry :: HamtEntry k v -> STM ()
unlinkEntry (ListNode{nextPtr, prevNextPtrPtr=currPrev}) = do
nextEntry <- readTVar nextPtr
withSome nextEntry $ \e -> do
prevNextPtr <- readTVar currPrev
writeTVar (prevNextPtrPtr e) prevNextPtr
writeTVar prevNextPtr nextEntry
newLinkedEntry v = do
oldNeckNextPtr <- readTVar neck
newNeckNextPtr <- newTVar (Some head)
newNeck <- ListNode newNeckNextPtr <$>
newTVar oldNeckNextPtr <*>
(Entry <$> newTVar False <*> pure k <*> pure v)
-- update pointers
writeTVar oldNeckNextPtr (Some newNeck)
writeTVar neck newNeckNextPtr
-- return HAMT entry
pure newNeck
removeEntry = fmap (*>) unlinkEntry <*> adjustFinger
adjustFinger ListNode{nextPtr, prevNextPtrPtr} =
whenM ((nextPtr ==) <$> readTVar finger) $
readTVar prevNextPtrPtr >>= writeTVar finger
+100
View File
@@ -0,0 +1,100 @@
{-|
Module : PostgREST.Client
Description : PostgREST HTTP client
-}
{-# LANGUAGE NamedFieldPuns #-}
module PostgREST.Client
( ready
) where
import qualified Data.Text as T
import qualified Network.HTTP.Client as HC
import qualified Network.HTTP.Types.Status as HTTP
import Network.HTTP.Client (HttpException (..))
import System.IO (hFlush)
import PostgREST.Config (AppConfig (..))
import PostgREST.Network (isSpecialHostName)
import Protolude
data PgrstClientError
= NoAdminServer
| NoSpecialHostNamesAllowed Text
| PostgRESTNotReady Text
| HTTPConnectionRefused Text
| HTTPExceptionInvalidURL Text
-- | This is invoked by the CLI "--ready" flag.
-- The http-client sends and a request to /ready endpoint
-- and exits with success or failure.
ready :: AppConfig -> IO ()
ready AppConfig{configAdminServerHost, configAdminServerPort} = do
client <- HC.newManager HC.defaultManagerSettings
readyURL <- getURL
req <- HC.parseRequest (T.unpack readyURL) `catch` handleHttpException
resp <- HC.httpLbs req client `catch` handleHttpException
let status = HC.responseStatus resp
if status >= HTTP.status200 && status < HTTP.status300
then printAndExitWithSuccess $ "OK: " <> readyURL
else printAndExitWithFailure $ clientErrorMsg (PostgRESTNotReady readyURL)
where
getURL :: IO Text
getURL =
-- Here, we have three cases:
-- 1. If the admin port config is not defined, we exit
-- with "no admin server error"
-- 2. Otherwise, if admin server is running, then we check if
-- postgrest server-host is configured with special hostname like "*4",
-- if it is, we fail with "no special hostname allowed with "--ready".
-- The reason for this is that we can't know the actual address.
-- 3. Finally, if we know the "actual" hostname and the port, then we
-- construct the URL and return it.
case configAdminServerPort of
Nothing -> printAndExitWithFailure $ clientErrorMsg NoAdminServer
Just port ->
if isSpecialHostName configAdminServerHost
then printAndExitWithFailure $ clientErrorMsg (NoSpecialHostNamesAllowed configAdminServerHost)
else return $ makeReadyUrl port
-- NOTE: http-client automatically resolves hostnames
makeReadyUrl :: Int -> Text
makeReadyUrl p = "http://" <> wrapIfIpv6 configAdminServerHost <> ":" <> (T.pack . show) p <> "/ready"
where
-- IPv6 needs to wrapped in [], it has ':' as separator
wrapIfIpv6 :: Text -> Text
wrapIfIpv6 s
| T.any (== ':') s = "[" <> s <> "]"
| otherwise = s
-- | Handle HTTP exception for "http-client" requests
handleHttpException :: HttpException -> IO a
handleHttpException (HttpExceptionRequest req _) = do
let url = show (HC.getUri req)
printAndExitWithFailure $ clientErrorMsg (HTTPConnectionRefused $ T.pack url)
handleHttpException (InvalidUrlException url _) = do
printAndExitWithFailure $ clientErrorMsg (HTTPExceptionInvalidURL $ T.pack url)
-- | Print the message on stdout and exit with success
printAndExitWithSuccess :: Text -> IO a
printAndExitWithSuccess msg = putStrLn (T.unpack msg) >> hFlush stdout >> exitSuccess
-- | Print the message on stderr and exit with failure
printAndExitWithFailure :: Text -> IO a
printAndExitWithFailure msg = hPutStrLn stderr (T.unpack msg) >> hFlush stderr >> exitWith (ExitFailure 1)
-- | Pgrst client error to error message
clientErrorMsg :: PgrstClientError -> Text
clientErrorMsg err = "ERROR: " <>
case err of
NoAdminServer -> "Admin server is not running. Please check admin-server-port config."
NoSpecialHostNamesAllowed host ->
"The `--ready` flag cannot be used when server-host is configured as \"" <> host <> "\". "
<> "Please update your server-host config to \"localhost\"."
PostgRESTNotReady url -> url
HTTPConnectionRefused url -> "connection refused to " <> url
HTTPExceptionInvalidURL url -> "invalid url - " <> url
+160 -36
View File
@@ -17,7 +17,6 @@ module PostgREST.Config
, JSPathExp(..) , JSPathExp(..)
, FilterExp(..) , FilterExp(..)
, LogLevel(..) , LogLevel(..)
, LogQuery(..)
, OpenAPIMode(..) , OpenAPIMode(..)
, Proxy(..) , Proxy(..)
, toText , toText
@@ -28,6 +27,8 @@ module PostgREST.Config
, parseSecret , parseSecret
, addFallbackAppName , addFallbackAppName
, addTargetSessionAttrs , addTargetSessionAttrs
, exampleConfigFile
, audMatchesCfg
) where ) where
import qualified Data.Aeson as JSON import qualified Data.Aeson as JSON
@@ -36,6 +37,7 @@ import qualified Data.ByteString.Base64 as B64
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.String as S
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.Jwa as JWT
@@ -48,7 +50,7 @@ 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 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)
@@ -66,6 +68,8 @@ import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi,
import Protolude hiding (Proxy, toList) import Protolude hiding (Proxy, toList)
audMatchesCfg :: AppConfig -> Text -> Bool
audMatchesCfg = maybe (const True) (==) . configJwtAudience
data AppConfig = AppConfig data AppConfig = AppConfig
{ configAppSettings :: [(Text, Text)] { configAppSettings :: [(Text, Text)]
@@ -97,9 +101,9 @@ data AppConfig = AppConfig
, configJwtRoleClaimKey :: JSPath , configJwtRoleClaimKey :: JSPath
, configJwtSecret :: Maybe BS.ByteString , configJwtSecret :: Maybe BS.ByteString
, configJwtSecretIsBase64 :: Bool , configJwtSecretIsBase64 :: Bool
, configJwtCacheMaxLifetime :: Int , configJwtCacheMaxEntries :: Int
, configLogLevel :: LogLevel , configLogLevel :: LogLevel
, configLogQuery :: LogQuery , configLogQuery :: Bool
, configOpenApiMode :: OpenAPIMode , configOpenApiMode :: OpenAPIMode
, configOpenApiSecurityActive :: Bool , configOpenApiSecurityActive :: Bool
, configOpenApiServerProxyUri :: Maybe Text , configOpenApiServerProxyUri :: Maybe Text
@@ -114,7 +118,9 @@ data AppConfig = AppConfig
, configAdminServerPort :: Maybe Int , configAdminServerPort :: Maybe Int
, configRoleSettings :: RoleSettings , configRoleSettings :: RoleSettings
, configRoleIsoLvl :: RoleIsolationLvl , configRoleIsoLvl :: RoleIsolationLvl
, configInternalSCSleep :: Maybe Int32 , configInternalSCQuerySleep :: Maybe Int32
, configInternalSCLoadSleep :: Maybe Int32
, configInternalSCRelLoadSleep :: Maybe Int32
} }
data LogLevel = LogCrit | LogError | LogWarn | LogInfo | LogDebug data LogLevel = LogCrit | LogError | LogWarn | LogInfo | LogDebug
@@ -128,14 +134,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
@@ -177,9 +175,9 @@ toText conf =
,("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-entries", show . configJwtCacheMaxEntries)
,("log-level", q . dumpLogLevel . configLogLevel) ,("log-level", q . dumpLogLevel . configLogLevel)
,("log-query", q . dumpLogQuery . configLogQuery) ,("log-query", T.toLower . show . 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)
@@ -256,8 +254,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"] splitOnCommas <$> optValue "db-extra-search-path") <*> (maybe ["public"] splitOnCommasEmptyable <$> optStringEmptyable "db-extra-search-path")
<*> (maybe defaultHoistedAllowList splitOnCommas <$> optValue "db-hoisted-tx-settings") <*> (maybe defaultHoistedAllowList splitOnCommas <$> optString "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 +270,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 (optValue "db-schemas") <*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optString "db-schemas")
(optValue "db-schema")) (optString "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,15 +279,15 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> (fromMaybe "postgresql://" <$> optString "db-uri") <*> (fromMaybe "postgresql://" <$> optString "db-uri")
<*> pure optPath <*> pure optPath
<*> pure Nothing <*> pure Nothing
<*> optString "jwt-aud" <*> optStringOrURI "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
(optBool "jwt-secret-is-base64") (optBool "jwt-secret-is-base64")
(optBool "secret-is-base64")) (optBool "secret-is-base64"))
<*> (fromMaybe 0 <$> optInt "jwt-cache-max-lifetime") <*> (fromMaybe 1000 <$> optInt "jwt-cache-max-entries")
<*> parseLogLevel "log-level" <*> parseLogLevel "log-level"
<*> parseLogQuery "log-query" <*> (fromMaybe False <$> optBool "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"
@@ -305,7 +303,9 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
<*> parseAdminServerPort "admin-server-port" <*> parseAdminServerPort "admin-server-port"
<*> pure roleSettings <*> pure roleSettings
<*> pure roleIsolationLvl <*> pure roleIsolationLvl
<*> optInt "internal-schema-cache-sleep" <*> optInt "internal-schema-cache-query-sleep"
<*> optInt "internal-schema-cache-load-sleep"
<*> optInt "internal-schema-cache-relationship-load-sleep"
where where
parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)] parseAppSettings :: C.Key -> C.Parser C.Config [(Text, Text)]
parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value parseAppSettings key = addFromEnv . fmap (fmap coerceText) <$> C.subassocs key C.value
@@ -365,14 +365,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,8 +396,22 @@ 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
optValue :: C.Key -> C.Parser C.Config (Maybe C.Value) optStringEmptyable :: C.Key -> C.Parser C.Config (Maybe Text)
optValue k = overrideFromDbOrEnvironment C.optional k identity optStringEmptyable k = overrideFromDbOrEnvironment C.optional k coerceText
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
@@ -445,9 +451,12 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
Nothing -> (> 0) <$> (readMaybe s :: Maybe Integer) Nothing -> (> 0) <$> (readMaybe s :: Maybe Integer)
coerceBool _ = Nothing coerceBool _ = Nothing
splitOnCommas :: C.Value -> [Text] splitOnCommas :: Text -> [Text]
splitOnCommas (C.String s) = T.strip <$> T.splitOn "," s splitOnCommas 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"]
@@ -611,3 +620,118 @@ addConnStringOption dbUri key val = dbUri <>
uriFmt = key <> "=" <> toS (escapeURIString isUnescapedInURIComponent $ toS val) uriFmt = key <> "=" <> toS (escapeURIString isUnescapedInURIComponent $ toS val)
keyValFmt = key <> "=" <> "'" <> T.replace "'" "\\'" val <> "'" keyValFmt = key <> "=" <> "'" <> T.replace "'" "\\'" val <> "'"
lookAtOptions x = T.breakOn "?" . snd $ T.breakOnEnd "@" x -- start from after `@` to not mess passwords that include `?`, see https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS lookAtOptions x = T.breakOn "?" . snd $ T.breakOnEnd "@" x -- start from after `@` to not mess passwords that include `?`, see https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING-URIS
-- | Example config file displayed on postgrest "--example" flag
exampleConfigFile :: [Char]
exampleConfigFile = S.unlines
[ "## Admin server used for checks. It's disabled by default unless a port is specified."
, "# admin-server-port = 3001"
, ""
, "## The database role to use when no client authentication is provided"
, "# db-anon-role = \"anon\""
, ""
, "## Notification channel for reloading the schema cache"
, "db-channel = \"pgrst\""
, ""
, "## Enable or disable the notification channel"
, "db-channel-enabled = true"
, ""
, "## Enable in-database configuration"
, "db-config = true"
, ""
, "## Function for in-database configuration"
, "## db-pre-config = \"postgrest.pre_config\""
, ""
, "## Extra schemas to add to the search_path of every request"
, "db-extra-search-path = \"public\""
, ""
, "## Limit rows in response"
, "# db-max-rows = 1000"
, ""
, "## Allow getting the EXPLAIN plan through the `Accept: application/vnd.pgrst.plan` header"
, "# db-plan-enabled = false"
, ""
, "## Number of open connections in the pool"
, "db-pool = 10"
, ""
, "## Time in seconds to wait to acquire a slot from the connection pool"
, "# db-pool-acquisition-timeout = 10"
, ""
, "## Time in seconds after which to recycle pool connections"
, "# db-pool-max-lifetime = 1800"
, ""
, "## Time in seconds after which to recycle unused pool connections"
, "# db-pool-max-idletime = 30"
, ""
, "## Allow automatic database connection retrying"
, "# db-pool-automatic-recovery = true"
, ""
, "## Stored proc to exec immediately after auth"
, "# db-pre-request = \"stored_proc_name\""
, ""
, "## Enable or disable prepared statements. disabling is only necessary when behind a connection pooler."
, "## When disabled, statements will be parametrized but won't be prepared."
, "db-prepared-statements = true"
, ""
, "## The name of which database schema to expose to REST clients"
, "db-schemas = \"public\""
, ""
, "## How to terminate database transactions"
, "## Possible values are:"
, "## commit (default)"
, "## Transaction is always committed, this can not be overriden"
, "## commit-allow-override"
, "## Transaction is committed, but can be overriden with Prefer tx=rollback header"
, "## rollback"
, "## Transaction is always rolled back, this can not be overriden"
, "## rollback-allow-override"
, "## Transaction is rolled back, but can be overriden with Prefer tx=commit header"
, "db-tx-end = \"commit\""
, ""
, "## The standard connection URI format, documented at"
, "## https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING"
, "db-uri = \"postgresql://\""
, ""
, "# jwt-aud = \"your_audience_claim\""
, ""
, "## Jspath to the role claim key"
, "jwt-role-claim-key = \".role\""
, ""
, "## Choose a secret, JSON Web Key (or set) to enable JWT auth"
, "## (use \"@filename\" to load from separate file)"
, "# jwt-secret = \"secret_with_at_least_32_characters\""
, "jwt-secret-is-base64 = false"
, ""
, "## Enables JWT Cache and sets its max size, disables caching with 0"
, "# jwt-cache-max-entries = 0"
, ""
, "## Logging level, the admitted values are: crit, error, warn, info and debug."
, "log-level = \"error\""
, ""
, "## Log the SQL query at the current log-level."
, "log-query = false"
, ""
, "## Determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely."
, "## Admitted values: follow-privileges, ignore-privileges, disabled"
, "openapi-mode = \"follow-privileges\""
, ""
, "## Base url for the OpenAPI output"
, "openapi-server-proxy-uri = \"\""
, ""
, "## Configurable CORS origins"
, "# server-cors-allowed-origins = \"\""
, ""
, "server-host = \"!4\""
, "server-port = 3000"
, ""
, "## Allow getting the request-response timing information through the `Server-Timing` header"
, "server-timing-enabled = false"
, ""
, "## Unix socket location"
, "## if specified it takes precedence over server-port"
, "# server-unix-socket = \"/tmp/pgrst.sock\""
, ""
, "## Unix socket file mode"
, "## When none is provided, 660 is applied by default"
, "# server-unix-socket-mode = \"660\""
]
+1 -5
View File
@@ -3,7 +3,6 @@
module PostgREST.Config.PgVersion module PostgREST.Config.PgVersion
( PgVersion(..) ( PgVersion(..)
, minimumPgVersion , minimumPgVersion
, pgVersion130
, pgVersion140 , pgVersion140
, pgVersion150 , pgVersion150
, pgVersion170 , pgVersion170
@@ -26,10 +25,7 @@ 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 = pgVersion130
pgVersion121 :: PgVersion
pgVersion121 = PgVersion 120001 "12.1" "12.1"
pgVersion130 :: PgVersion pgVersion130 :: PgVersion
pgVersion130 = PgVersion 130000 "13.0" "13.0" pgVersion130 = PgVersion 130000 "13.0" "13.0"
+88 -23
View File
@@ -3,6 +3,7 @@ Module : PostgREST.Error
Description : PostgREST error HTTP responses Description : PostgREST error HTTP responses
-} -}
{-# OPTIONS_GHC -fno-warn-orphans #-} {-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
module PostgREST.Error module PostgREST.Error
@@ -14,6 +15,8 @@ module PostgREST.Error
, PgError(..) , PgError(..)
, Error(..) , Error(..)
, JwtError (..) , JwtError (..)
, JwtDecodeError(..)
, JwtClaimsError(..)
, errorPayload , errorPayload
, status , status
) where ) where
@@ -39,6 +42,7 @@ import Network.HTTP.Types.Header (Header)
import PostgREST.MediaType (MediaType (..)) import PostgREST.MediaType (MediaType (..))
import qualified PostgREST.MediaType as MediaType import qualified PostgREST.MediaType as MediaType
import PostgREST.SchemaCache (SchemaCache (SchemaCache, dbTablesFuzzyIndex))
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..), import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
Schema) Schema)
import PostgREST.SchemaCache.Relationship (Cardinality (..), import PostgREST.SchemaCache.Relationship (Cardinality (..),
@@ -47,10 +51,8 @@ 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 (ErrorBody a, JSON.ToJSON a) => PgrstError a where
status :: a -> HTTP.Status status :: a -> HTTP.Status
headers :: a -> [Header] headers :: a -> [Header]
@@ -86,7 +88,7 @@ data ApiRequestError
| QueryParamError QPError | QueryParamError QPError
| RelatedOrderNotToOne Text Text | RelatedOrderNotToOne Text Text
| UnacceptableFilter Text | UnacceptableFilter Text
| UnacceptableSchema [Text] | UnacceptableSchema Text [Text]
| UnsupportedMethod ByteString | UnsupportedMethod ByteString
| GucHeadersError | GucHeadersError
| GucStatusError | GucStatusError
@@ -96,6 +98,7 @@ data ApiRequestError
| MaxAffectedViolationError Integer | MaxAffectedViolationError Integer
| InvalidResourcePath | InvalidResourcePath
| OpenAPIDisabled | OpenAPIDisabled
| MaxAffectedRpcViolation
deriving Show deriving Show
data QPError = QPError Text Text data QPError = QPError Text Text
@@ -138,6 +141,7 @@ instance PgrstError ApiRequestError where
status MaxAffectedViolationError{} = HTTP.status400 status MaxAffectedViolationError{} = HTTP.status400
status InvalidResourcePath = HTTP.status404 status InvalidResourcePath = HTTP.status404
status OpenAPIDisabled = HTTP.status404 status OpenAPIDisabled = HTTP.status404
status MaxAffectedRpcViolation = HTTP.status400
headers _ = mempty headers _ = mempty
@@ -184,6 +188,7 @@ instance ErrorBody ApiRequestError where
code InvalidResourcePath = "PGRST125" code InvalidResourcePath = "PGRST125"
code OpenAPIDisabled = "PGRST126" code OpenAPIDisabled = "PGRST126"
code NotImplemented{} = "PGRST127" code NotImplemented{} = "PGRST127"
code MaxAffectedRpcViolation = "PGRST128"
-- MESSAGE: Text -- MESSAGE: Text
message (QueryParamError (QPError msg _)) = msg message (QueryParamError (QPError msg _)) = msg
@@ -191,7 +196,7 @@ instance ErrorBody ApiRequestError where
message (InvalidBody errorMessage) = T.decodeUtf8 errorMessage message (InvalidBody errorMessage) = T.decodeUtf8 errorMessage
message (InvalidRange _) = "Requested range not satisfiable" message (InvalidRange _) = "Requested range not satisfiable"
message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators" message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators"
message (UnacceptableSchema schemas) = "The schema must be one of the following: " <> T.intercalate ", " schemas message (UnacceptableSchema sch _) = "Invalid schema: " <> sch
message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts) 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 (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 GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value"
@@ -209,6 +214,7 @@ instance ErrorBody ApiRequestError where
message InvalidResourcePath = "Invalid path specified in request URL" message InvalidResourcePath = "Invalid path specified in request URL"
message OpenAPIDisabled = "Root endpoint metadata is disabled" message OpenAPIDisabled = "Root endpoint metadata is disabled"
message (NotImplemented _) = "Feature not implemented" 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: Maybe JSON.Value
details (QueryParamError (QPError _ dets)) = Just $ JSON.String dets details (QueryParamError (QPError _ dets)) = Just $ JSON.String dets
@@ -230,6 +236,7 @@ instance ErrorBody ApiRequestError where
-- HINT: Maybe JSON.Value -- HINT: Maybe JSON.Value
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter." 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 (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
hint (UnacceptableSchema _ schemas) = Just $ JSON.String $ "Only the following schemas are exposed: " <> T.intercalate ", " schemas
hint _ = Nothing hint _ = Nothing
@@ -243,7 +250,7 @@ data SchemaCacheError
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap | NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine] | NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
| ColumnNotFound Text Text | ColumnNotFound Text Text
| TableNotFound Text Text [Table] | TableNotFound Text Text SchemaCache
deriving Show deriving Show
instance PgrstError SchemaCacheError where instance PgrstError SchemaCacheError where
@@ -306,7 +313,7 @@ instance ErrorBody SchemaCacheError where
where where
onlySingleParams = isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream] 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 (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 (TableNotFound schemaName relName schemaCache) = JSON.String <$> tableNotFoundHint schemaName relName schemaCache
hint _ = Nothing hint _ = Nothing
@@ -379,7 +386,7 @@ noRelBetweenHint parent child schema allRels = ("Perhaps you meant '" <>) <$>
-- Just "Perhaps you meant to call the function api.test" -- Just "Perhaps you meant to call the function api.test"
-- --
-- >>> noRpcHint "api" "other" [] procs [] -- >>> noRpcHint "api" "other" [] procs []
-- Just "Perhaps you meant to call the function api.another" -- Nothing
-- --
-- >>> noRpcHint "api" "noclosealternative" [] procs [] -- >>> noRpcHint "api" "noclosealternative" [] procs []
-- Nothing -- Nothing
@@ -416,18 +423,30 @@ noRpcHint schema procName params allProcs overloadedProcs =
-- E.g. ["val", "param", "name"] into "(name, param, val)" -- E.g. ["val", "param", "name"] into "(name, param, val)"
listToText = ("(" <>) . (<> ")") . T.intercalate ", " . sort listToText = ("(" <>) . (<> ")") . T.intercalate ", " . sort
possibleProcs possibleProcs
| null overloadedProcs = Fuzzy.getOne fuzzySetOfProcs procName | null overloadedProcs = getFuzzyHint HintProcedure fuzzySetOfProcs procName
| otherwise = (procName <>) <$> Fuzzy.getOne fuzzySetOfParams (listToText params) | otherwise = (procName <>) <$> getFuzzyHint HintParams fuzzySetOfParams (listToText params)
-- | -- |
-- Do a fuzzy search in all tables in the same schema and return closest result -- Do a fuzzy search in all tables in the same schema and return closest result
tableNotFoundHint :: Text -> Text -> [Table] -> Maybe Text tableNotFoundHint :: Text -> Text -> SchemaCache -> Maybe Text
tableNotFoundHint schema tblName tblList tableNotFoundHint schema tblName SchemaCache{dbTablesFuzzyIndex}
= fmap (\tbl -> "Perhaps you meant the table '" <> schema <> "." <> tbl <> "'") perhapsTable = fmap (\tbl -> "Perhaps you meant the table '" <> schema <> "." <> tbl <> "'") perhapsTable
where where
perhapsTable = Fuzzy.getOne fuzzyTableSet tblName perhapsTable = (\fuzzySet -> getFuzzyHint HintTable fuzzySet tblName) =<< HM.lookup schema dbTablesFuzzyIndex
fuzzyTableSet = Fuzzy.fromList [ tableName tbl | tbl <- tblList, tableSchema tbl == schema]
data HintType
= HintTable
| HintProcedure
| HintParams
-- | Get hint using Fuzzy Search with at least 0.75 similarity score
getFuzzyHint :: HintType -> Fuzzy.FuzzySet -> Text -> Maybe Text
getFuzzyHint hintType =
let minScore = 0.75 :: Double -- used for table and procedure name hints
in case hintType of
HintTable -> Fuzzy.getOneWithMinScore minScore
HintProcedure -> Fuzzy.getOneWithMinScore minScore
HintParams -> Fuzzy.getOne -- For params, we stick to `getOne` which defaults to 0.33 min score, not a security risk to reveal params
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
@@ -595,6 +614,10 @@ 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
@@ -639,10 +662,32 @@ data Error
deriving Show deriving Show
data JwtError data JwtError
= JwtDecodeError Text = JwtDecodeErr JwtDecodeError
| JwtSecretMissing | JwtSecretMissing
| JwtTokenRequired | JwtTokenRequired
| JwtClaimsError Text | 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 deriving Show
instance PgrstError Error where instance PgrstError Error where
@@ -688,14 +733,14 @@ instance ErrorBody Error where
hint (PgErr err) = hint err hint (PgErr err) = hint err
instance PgrstError JwtError where instance PgrstError JwtError where
status JwtDecodeError{} = HTTP.unauthorized401 status JwtDecodeErr{} = HTTP.unauthorized401
status JwtSecretMissing = HTTP.status500 status JwtSecretMissing = HTTP.status500
status JwtTokenRequired = HTTP.unauthorized401 status JwtTokenRequired = HTTP.unauthorized401
status JwtClaimsError{} = HTTP.unauthorized401 status JwtClaimsErr{} = HTTP.unauthorized401
headers (JwtDecodeError m) = [invalidTokenHeader m] headers e@(JwtDecodeErr _) = [invalidTokenHeader $ message e]
headers JwtTokenRequired = [requiredTokenHeader] headers JwtTokenRequired = [requiredTokenHeader]
headers (JwtClaimsError m) = [invalidTokenHeader m] headers e@(JwtClaimsErr _) = [invalidTokenHeader $ message e]
headers _ = mempty headers _ = mempty
instance JSON.ToJSON JwtError where instance JSON.ToJSON JwtError where
@@ -704,15 +749,35 @@ instance JSON.ToJSON JwtError where
instance ErrorBody JwtError where instance ErrorBody JwtError where
code JwtSecretMissing = "PGRST300" code JwtSecretMissing = "PGRST300"
code (JwtDecodeError _) = "PGRST301" code (JwtDecodeErr _) = "PGRST301"
code JwtTokenRequired = "PGRST302" code JwtTokenRequired = "PGRST302"
code (JwtClaimsError _) = "PGRST303" code (JwtClaimsErr _) = "PGRST303"
message JwtSecretMissing = "Server lacks JWT secret" message JwtSecretMissing = "Server lacks JWT secret"
message (JwtDecodeError msg) = msg 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 JwtTokenRequired = "Anonymous access is disabled"
message (JwtClaimsError msg) = msg 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 details _ = Nothing
hint _ = Nothing hint _ = Nothing
+37 -10
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-} {-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE RecordWildCards #-}
@@ -9,12 +10,20 @@ import qualified Hasql.Connection as SQL
import qualified Hasql.Notifications as SQL import qualified Hasql.Notifications as SQL
import PostgREST.AppState (AppState, getConfig) import PostgREST.AppState (AppState, getConfig)
import PostgREST.Config (AppConfig (..)) import PostgREST.Config (AppConfig (..))
import PostgREST.Observation (Observation (..)) import PostgREST.Observation (Observation (..),
isDbListenerBug)
import PostgREST.Version (prettyVersion) import PostgREST.Version (prettyVersion)
import qualified PostgREST.AppState as AppState import qualified PostgREST.AppState as AppState
import qualified PostgREST.Config as Config import qualified PostgREST.Config as Config
import Control.Arrow ((&&&))
import Data.Bitraversable (bisequence)
import Data.Either.Combinators (whenRight)
import qualified Database.PostgreSQL.LibPQ as LibPQ
import qualified Hasql.Session as SQL
import PostgREST.Config.Database (queryPgVersion)
import PostgREST.Config.PgVersion (pgvFullName)
import Protolude import Protolude
-- | Starts the Listener in a thread -- | Starts the Listener in a thread
@@ -22,17 +31,20 @@ runListener :: AppState -> IO ()
runListener appState = do runListener appState = do
AppConfig{..} <- getConfig appState AppConfig{..} <- getConfig appState
when configDbChannelEnabled $ when configDbChannelEnabled $
void . forkIO $ retryingListen appState void . forkIO . void $ retryingListen appState
-- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost. -- | Starts a LISTEN connection and handles notifications. It recovers with exponential backoff with a cap of 32 seconds, if the LISTEN connection is lost.
retryingListen :: AppState -> IO () -- | This function never returns (but can throw) and return type enforces that.
retryingListen :: AppState -> IO Void
retryingListen appState = do retryingListen appState = do
AppConfig{..} <- AppState.getConfig appState AppConfig{..} <- AppState.getConfig appState
let let
dbChannel = toS configDbChannel dbChannel = toS configDbChannel
handleFinally err = do onError err = do
AppState.putIsListenerOn appState False AppState.putIsListenerOn appState False
observer $ DBListenFail dbChannel (Right err) observer $ DBListenFail dbChannel (Right err)
when (isDbListenerBug err) $
observer DBListenBugHint
unless configDbPoolAutomaticRecovery $ unless configDbPoolAutomaticRecovery $
killThread mainThreadId killThread mainThreadId
@@ -42,14 +54,24 @@ retryingListen appState = do
threadDelay (delay * oneSecondInMicro) threadDelay (delay * oneSecondInMicro)
unless (delay == maxDelay) $ unless (delay == maxDelay) $
AppState.putNextListenerDelay appState (delay * 2) AppState.putNextListenerDelay appState (delay * 2)
-- loop running the listener
retryingListen appState retryingListen appState
-- forkFinally allows to detect if the thread dies -- Execute the listener with with error handling
void . flip forkFinally handleFinally $ do handle onError $ do
dbOrError <- SQL.acquire $ toUtf8 (Config.addTargetSessionAttrs $ Config.addFallbackAppName prettyVersion configDbUri) -- Make sure we don't leak connections on errors
case dbOrError of bracket
-- acquire connection
(SQL.acquire $ toUtf8 (Config.addTargetSessionAttrs $ Config.addFallbackAppName prettyVersion configDbUri))
-- release connection
(`whenRight` releaseConnection) $
-- use connection
\case
Right db -> do Right db -> do
SQL.listen db $ SQL.toPgIdentifier dbChannel SQL.listen db $ SQL.toPgIdentifier dbChannel
(pqHost, pqPort) <- SQL.withLibPQConnection db $ bisequence . (LibPQ.host &&& LibPQ.port)
pgFullName <- SQL.run (queryPgVersion False) db >>= either throwIO (pure . pgvFullName)
AppState.putIsListenerOn appState True AppState.putIsListenerOn appState True
delay <- AppState.getNextListenerDelay appState delay <- AppState.getNextListenerDelay appState
@@ -59,8 +81,11 @@ retryingListen appState = do
-- reset the delay -- reset the delay
AppState.putNextListenerDelay appState 1 AppState.putNextListenerDelay appState 1
observer $ DBListenStart dbChannel observer $ DBListenStart pqHost pqPort pgFullName dbChannel
SQL.waitForNotifications handleNotification db
-- wait for notifications
-- this will never return, in case of an error it will throw and be caught by onError
forever $ SQL.waitForNotifications handleNotification db
Left err -> do Left err -> do
observer $ DBListenFail dbChannel (Left err) observer $ DBListenFail dbChannel (Left err)
@@ -79,3 +104,5 @@ retryingListen appState = do
cacheReloader = cacheReloader =
AppState.schemaCacheLoader appState AppState.schemaCacheLoader appState
releaseConnection = void . forkIO . handle (observer . DBListenerConnectionCleanupFail) . SQL.release
+44 -5
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE RecordWildCards #-}
{-| {-|
Module : PostgREST.Logger Module : PostgREST.Logger
Description : Logging based on the Observation.hs module. Access logs get sent to stdout and server diagnostic get sent to stderr. Description : Logging based on the Observation.hs module. Access logs get sent to stdout and server diagnostic get sent to stderr.
@@ -11,9 +12,15 @@ module PostgREST.Logger
) where ) where
import Control.AutoUpdate (defaultUpdateSettings, import Control.AutoUpdate (defaultUpdateSettings,
mkAutoUpdate, updateAction) mkAutoUpdate,
updateAction)
import Control.Debounce import Control.Debounce
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import qualified Data.Text.Encoding as T
import qualified Hasql.Decoders as HD
import qualified Hasql.DynamicStatements.Snippet as SQL hiding (sql)
import qualified Hasql.DynamicStatements.Statement as SQL
import qualified Hasql.Statement as SQL
import Data.Time (ZonedTime, defaultTimeLocale, formatTime, import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
getZonedTime) getZonedTime)
@@ -26,6 +33,7 @@ import System.IO.Unsafe (unsafePerformIO)
import PostgREST.Config (LogLevel (..)) import PostgREST.Config (LogLevel (..))
import PostgREST.Observation import PostgREST.Observation
import PostgREST.Query (MainQuery (..))
import Protolude import Protolude
@@ -87,19 +95,33 @@ observationLogger loggerState logLevel obs = case obs of
o@(QueryErrorCodeHighObs _) -> do o@(QueryErrorCodeHighObs _) -> do
when (logLevel >= LogError) $ do when (logLevel >= LogError) $ do
logWithZTime loggerState $ observationMessage o logWithZTime loggerState $ observationMessage o
o@SchemaCacheEmptyObs ->
when (logLevel >= LogError) $ do
logWithZTime loggerState $ observationMessage o
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 QueryObs gq status -> do
-- Does not log SQL when it's empty (for OPTIONS requests or for the default OpenAPI output) when (shouldLogResponse logLevel status) $
when (sql /= mempty && shouldLogResponse logLevel status) $ do logMainQ loggerState gq
logWithZTime loggerState $ observationMessage o
o@PoolRequest -> o@PoolRequest ->
when (logLevel >= LogDebug) $ do when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o logWithZTime loggerState $ observationMessage o
o@PoolRequestFullfilled -> o@PoolRequestFullfilled ->
when (logLevel >= LogDebug) $ do when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o logWithZTime loggerState $ observationMessage o
o@PoolFlushed ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o
o@JwtCacheEviction ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o
o@(JwtCacheLookup _) ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o
o@(WarpServerObs _) ->
when (logLevel >= LogDebug) $ do
logWithZTime loggerState $ observationMessage o
o -> o ->
logWithZTime loggerState $ observationMessage o logWithZTime loggerState $ observationMessage o
@@ -107,3 +129,20 @@ logWithZTime :: LoggerState -> Text -> IO ()
logWithZTime loggerState txt = do logWithZTime loggerState txt = do
zTime <- stateGetZTime loggerState zTime <- stateGetZTime loggerState
hPutStrLn stderr $ toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <> txt hPutStrLn stderr $ toS (formatTime defaultTimeLocale "%d/%b/%Y:%T %z: " zTime) <> txt
logMainQ :: LoggerState -> MainQuery -> IO ()
logMainQ loggerState MainQuery{mqOpenAPI=(x, y, z),..} =
let snipts = renderSnippet <$> [mqTxVars, fromMaybe mempty mqPreReq, mqMain, x, y, z, fromMaybe mempty mqExplain]
-- Does not log SQL when it's empty (happens on OPTIONS requests and when the openapi queries are not generated)
logQ q = when (q /= mempty) $ logWithZTime loggerState $ showOnSingleLine '\n' $ T.decodeUtf8 q in
mapM_ logQ snipts
-- TODO: maybe patch upstream hasql-dynamic-statements so we have a less hackish way to convert
-- the SQL.Snippet or maybe don't use hasql-dynamic-statements and resort to plain strings for the queries and use regular hasql
renderSnippet :: SQL.Snippet -> ByteString
renderSnippet snippet =
let SQL.Statement sql _ _ _ = SQL.dynamicallyParameterized snippet decoder prepared
decoder = HD.noResult -- unused
prepared = False -- unused
in
sql
+279
View File
@@ -0,0 +1,279 @@
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RecordWildCards #-}
{-|
Module : PostgREST.MainTx
Description : PostgREST transaction executor
This module parametrizes, prepares, executes SQL queries and decodes their results.
-}
module PostgREST.MainTx
( MainTx (..)
, DbResult (..)
, ResultSet (..)
, mainTx
) where
import Control.Lens ((^?))
import Control.Monad.Extra (whenJust)
import qualified Data.Aeson.Lens as L
import qualified Data.ByteString as BS hiding
(break)
import qualified Data.ByteString.Char8 as BS
import qualified Data.HashMap.Strict as HM
import qualified Data.Set as S
import qualified Hasql.Decoders as HD
import qualified Hasql.DynamicStatements.Statement as SQL
import qualified Hasql.Session as SQL (Session)
import qualified Hasql.Transaction as SQL
import qualified Hasql.Transaction.Sessions as SQL
import qualified PostgREST.Error as Error
import qualified PostgREST.SchemaCache as SchemaCache
import PostgREST.ApiRequest (ApiRequest (..))
import PostgREST.ApiRequest.Preferences (PreferCount (..),
PreferHandling (..),
PreferMaxAffected (..),
PreferTransaction (..),
Preferences (..))
import PostgREST.ApiRequest.Types (Mutation (..))
import PostgREST.Auth.Types (AuthResult (..))
import PostgREST.Config (AppConfig (..),
OpenAPIMode (..))
import PostgREST.Error (Error)
import PostgREST.MediaType (MediaType (..))
import PostgREST.Plan (ActionPlan (..),
CrudPlan (..),
DbActionPlan (..),
InfoPlan (..),
InspectPlan (..))
import PostgREST.Query (MainQuery (..))
import PostgREST.SchemaCache (SchemaCache (..))
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
import PostgREST.SchemaCache.Routine (Routine (..), RoutineMap)
import PostgREST.SchemaCache.Table (TablesMap)
import Protolude hiding (Handler)
type DbHandler = ExceptT Error SQL.Transaction
data MainTx
= DbTx {
dqIsoLevel :: SQL.IsolationLevel
, dqTxMode :: SQL.Mode
, dqDbHandler :: DbHandler DbResult
, dqTransaction :: SQL.IsolationLevel -> SQL.Mode -> SQL.Transaction (Either Error DbResult) -> SQL.Session (Either Error DbResult)
}
| NoDbTx DbResult
data DbResult
= DbCrudResult CrudPlan ResultSet
| DbPlanResult MediaType BS.ByteString
| MaybeDbResult InspectPlan (Maybe (TablesMap, RoutineMap, Maybe Text))
| NoDbResult InfoPlan
-- | Standard result set format used for the mqMain query
data ResultSet
= RSStandard
{ rsTableTotal :: Maybe Int64
-- ^ count of all the table rows
, rsQueryTotal :: Int64
-- ^ count of the query rows
, rsLocation :: [(BS.ByteString, BS.ByteString)]
-- ^ The Location header(only used for inserts) is represented as a list of strings containing
-- variable bindings like @"k1=eq.42"@, or the empty list if there is no location header.
, rsBody :: BS.ByteString
-- ^ the aggregated body of the query
, rsGucHeaders :: Maybe BS.ByteString
-- ^ the HTTP headers to be added to the response
, rsGucStatus :: Maybe Text
-- ^ the HTTP status to be added to the response
, rsInserted :: Maybe Int64
-- ^ the number of rows inserted (Only used for upserts)
}
mainTx :: MainQuery -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> MainTx
mainTx _ _ _ _ (NoDb x) _ = NoDbTx $ NoDbResult x
mainTx genQ@MainQuery{..} conf@AppConfig{..} AuthResult{..} apiReq (Db plan) sCache =
DbTx isoLvl txMode dbHandler transaction
where
transaction = if configDbPreparedStatements then SQL.transaction else SQL.unpreparedTransaction
isoLvl = planIsoLvl conf authRole plan
txMode = planTxMode plan
dbHandler = do
lift $ SQL.statement mempty $ SQL.dynamicallyParameterized mqTxVars
HD.noResult configDbPreparedStatements
lift $ whenJust mqPreReq $ \q ->
SQL.statement mempty $ SQL.dynamicallyParameterized q
HD.noResult configDbPreparedStatements
actionResult genQ plan conf apiReq sCache
planTxMode :: DbActionPlan -> SQL.Mode
planTxMode (DbCrud _ x) = pTxMode x
planTxMode (MayUseDb x) = ipTxmode x
planIsoLvl :: AppConfig -> ByteString -> DbActionPlan -> SQL.IsolationLevel
planIsoLvl AppConfig{configRoleIsoLvl} role actPlan = case actPlan of
DbCrud _ CallReadPlan{crProc} -> fromMaybe roleIsoLvl $ pdIsoLvl crProc
_ -> roleIsoLvl
where
roleIsoLvl = HM.findWithDefault SQL.ReadCommitted role configRoleIsoLvl
actionResult :: MainQuery -> DbActionPlan -> AppConfig -> ApiRequest -> SchemaCache -> ExceptT Error SQL.Transaction DbResult
actionResult MainQuery{..} (DbCrud True plan) conf@AppConfig{..} apiReq _ = do
explRes <- lift $ SQL.statement mempty $ SQL.dynamicallyParameterized mqMain planRow configDbPreparedStatements
optionalRollback conf apiReq
pure $ DbPlanResult (pMedia plan) explRes
actionResult MainQuery{..} (DbCrud _ plan@WrappedReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ = do
resultSet@RSStandard{rsTableTotal=tableTotal} <- lift $ SQL.statement mempty $ dynStmt (HD.singleRow $ standardRow True)
failNotSingular pMedia resultSet
optionalRollback conf apiReq
explainTotal <- lift . fmap join $ traverse (\snip ->
SQL.statement mempty $ SQL.dynamicallyParameterized snip decodeExplain configDbPreparedStatements)
mqExplain
pure $ DbCrudResult plan
resultSet{rsTableTotal=case preferCount of
Just PlannedCount -> explainTotal
Just EstimatedCount -> if tableTotal > (fromIntegral <$> configDbMaxRows)
then max <$> tableTotal <*> explainTotal
else tableTotal
_ -> tableTotal}
where
dynStmt decod = SQL.dynamicallyParameterized mqMain decod configDbPreparedStatements
decodeExplain :: HD.Result (Maybe Int64)
decodeExplain =
let row = HD.singleRow $ column HD.bytea in
(^? L.nth 0 . L.key "Plan" . L.key "Plan Rows" . L._Integral) <$> row
actionResult MainQuery{..} (DbCrud _ plan@MutateReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ = do
resultSet <- lift $ SQL.statement mempty $ dynStmt decodeRow
failMutation resultSet
optionalRollback conf apiReq
pure $ DbCrudResult plan resultSet
where
dynStmt decod = SQL.dynamicallyParameterized mqMain decod configDbPreparedStatements
failMutation resultSet = case mrMutation of
MutationCreate -> do
failNotSingular pMedia resultSet
MutationUpdate -> do
failNotSingular pMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
MutationSingleUpsert -> do
failPut resultSet
MutationDelete -> do
failNotSingular pMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
decodeRow = fromMaybe (RSStandard Nothing 0 mempty mempty Nothing Nothing Nothing) <$> HD.rowMaybe (standardRow False)
actionResult MainQuery{..} (DbCrud _ plan@CallReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ = do
resultSet <- lift $ SQL.statement mempty $ dynStmt decodeRow
optionalRollback conf apiReq
failNotSingular pMedia resultSet
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
pure $ DbCrudResult plan resultSet
where
dynStmt decod = SQL.dynamicallyParameterized mqMain decod configDbPreparedStatements
decodeRow = fromMaybe (RSStandard (Just 0) 0 mempty mempty Nothing Nothing Nothing) <$> HD.rowMaybe (standardRow True)
actionResult MainQuery{mqOpenAPI=(tblsQ, funcsQ, schQ)} (MayUseDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ sCache =
mainActionQuery
where
mainActionQuery = lift $
case configOpenApiMode of
OAFollowPriv -> do
tableAccess <- SQL.statement mempty $ SQL.dynamicallyParameterized tblsQ decodeAccessibleIdentifiers configDbPreparedStatements
accFuncs <- SQL.statement mempty $ SQL.dynamicallyParameterized funcsQ SchemaCache.decodeFuncs configDbPreparedStatements
schDesc <- SQL.statement mempty $ SQL.dynamicallyParameterized schQ decodeSchemaDesc configDbPreparedStatements
let tbls = HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache
pure $ MaybeDbResult plan (Just (tbls, accFuncs, schDesc))
OAIgnorePriv -> do
schDesc <- SQL.statement mempty (SQL.dynamicallyParameterized schQ decodeSchemaDesc configDbPreparedStatements)
let tbls = HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) (SchemaCache.dbTables sCache)
routs = HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) (SchemaCache.dbRoutines sCache)
pure $ MaybeDbResult plan (Just (tbls, routs, schDesc))
OADisabled ->
pure $ MaybeDbResult plan Nothing
decodeSchemaDesc :: HD.Result (Maybe Text)
decodeSchemaDesc = join <$> HD.rowMaybe (nullableColumn HD.text)
decodeAccessibleIdentifiers :: HD.Result (S.Set QualifiedIdentifier)
decodeAccessibleIdentifiers =
let
row = QualifiedIdentifier
<$> column HD.text
<*> column HD.text
in
S.fromList <$> HD.rowList row
-- Makes sure the querystring pk matches the payload pk
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted,
-- PUT /items?id=eq.14 { "id" : 2, .. } is rejected.
-- If this condition is not satisfied then nothing is inserted,
-- check the WHERE for INSERT in QueryBuilder.hs to see how it's done
failPut :: ResultSet -> DbHandler ()
failPut RSStandard{rsQueryTotal=queryTotal} =
when (queryTotal /= 1) $ do
lift SQL.condemn
throwError $ Error.ApiRequestError Error.PutMatchingPkError
-- |
-- Fail a response if a single JSON object was requested and not exactly one
-- was found.
failNotSingular :: MediaType -> ResultSet -> DbHandler ()
failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =
when (elem mediaType [MTVndSingularJSON True, MTVndSingularJSON False] && queryTotal /= 1) $ do
lift SQL.condemn
throwError $ Error.ApiRequestError . Error.SingularityError $ toInteger queryTotal
failExceedsMaxAffectedPref :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> ResultSet -> DbHandler ()
failExceedsMaxAffectedPref (Nothing,_) _ = pure ()
failExceedsMaxAffectedPref (Just (PreferMaxAffected n), handling) RSStandard{rsQueryTotal=queryTotal} = when ((queryTotal > n) && (handling == Just Strict)) $ do
lift SQL.condemn
throwError $ Error.ApiRequestError . Error.MaxAffectedViolationError $ toInteger queryTotal
-- | Set a transaction to roll back if requested
optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
optionalRollback AppConfig{..} ApiRequest{iPreferences=Preferences{..}} = do
lift $ when (shouldRollback || (configDbTxRollbackAll && not shouldCommit)) $ do
SQL.sql "SET CONSTRAINTS ALL IMMEDIATE"
SQL.condemn
where
shouldCommit =
preferTransaction == Just Commit
shouldRollback =
preferTransaction == Just Rollback
-- | We use rowList because when doing EXPLAIN (FORMAT TEXT), the result comes as many rows. FORMAT JSON comes as one.
planRow :: HD.Result BS.ByteString
planRow = BS.unlines <$> HD.rowList (column HD.bytea)
column :: HD.Value a -> HD.Row a
column = HD.column . HD.nonNullable
nullableColumn :: HD.Value a -> HD.Row (Maybe a)
nullableColumn = HD.column . HD.nullable
arrayColumn :: HD.Value a -> HD.Row [a]
arrayColumn = column . HD.listArray . HD.nonNullable
standardRow :: Bool -> HD.Row ResultSet
standardRow noLocation =
RSStandard <$> nullableColumn HD.int8 <*> column HD.int8
<*> (if noLocation then pure mempty else fmap splitKeyValue <$> arrayColumn HD.bytea)
<*> (fromMaybe mempty <$> nullableColumn HD.bytea)
<*> nullableColumn HD.bytea
<*> nullableColumn HD.text
<*> nullableColumn HD.int8
where
splitKeyValue :: ByteString -> (ByteString, ByteString)
splitKeyValue kv =
let (k, v) = BS.break (== '=') kv in
(k, BS.tail v)
+29 -11
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE RecordWildCards #-}
{-| {-|
Module : PostgREST.Logger Module : PostgREST.Logger
Description : Metrics based on the Observation module. See Observation.hs. Description : Metrics based on the Observation module. See Observation.hs.
@@ -19,22 +20,36 @@ import PostgREST.Observation
import Protolude import Protolude
data MetricsState = data MetricsState =
MetricsState Counter Gauge Gauge Gauge (Vector Label1 Counter) Gauge MetricsState {
poolTimeouts :: Counter,
poolAvailable :: Gauge,
poolWaiting :: Gauge,
poolMaxSize :: Gauge,
schemaCacheLoads :: Vector Label1 Counter,
schemaCacheQueryTime :: Gauge,
jwtCacheRequests :: Counter,
jwtCacheHits :: Counter,
jwtCacheEvictions :: Counter
}
init :: Int -> IO MetricsState init :: Int -> IO MetricsState
init configDbPoolSize = do init configDbPoolSize = do
poolTimeouts <- register $ counter (Info "pgrst_db_pool_timeouts_total" "The total number of pool connection timeouts") metricState <- MetricsState <$>
poolAvailable <- register $ gauge (Info "pgrst_db_pool_available" "Available connections in the pool") register (counter (Info "pgrst_db_pool_timeouts_total" "The total number of pool connection timeouts")) <*>
poolWaiting <- register $ gauge (Info "pgrst_db_pool_waiting" "Requests waiting to acquire a pool connection") register (gauge (Info "pgrst_db_pool_available" "Available connections in the pool")) <*>
poolMaxSize <- register $ gauge (Info "pgrst_db_pool_max" "Max pool connections") register (gauge (Info "pgrst_db_pool_waiting" "Requests waiting to acquire a pool connection")) <*>
schemaCacheLoads <- register $ vector "status" $ counter (Info "pgrst_schema_cache_loads_total" "The total number of times the schema cache was loaded") register (gauge (Info "pgrst_db_pool_max" "Max pool connections")) <*>
schemaCacheQueryTime <- register $ gauge (Info "pgrst_schema_cache_query_time_seconds" "The query time in seconds of the last schema cache load") register (vector "status" $ counter (Info "pgrst_schema_cache_loads_total" "The total number of times the schema cache was loaded")) <*>
setGauge poolMaxSize (fromIntegral configDbPoolSize) register (gauge (Info "pgrst_schema_cache_query_time_seconds" "The query time in seconds of the last schema cache load")) <*>
pure $ MetricsState poolTimeouts poolAvailable poolWaiting poolMaxSize schemaCacheLoads schemaCacheQueryTime register (counter (Info "pgrst_jwt_cache_requests_total" "The total number of JWT cache lookups")) <*>
register (counter (Info "pgrst_jwt_cache_hits_total" "The total number of JWT cache hits")) <*>
register (counter (Info "pgrst_jwt_cache_evictions_total" "The total number of JWT cache evictions"))
setGauge (poolMaxSize metricState) (fromIntegral configDbPoolSize)
pure metricState
-- Only some observations are used as metrics -- 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{..} obs = case obs of
(PoolAcqTimeoutObs _) -> do (PoolAcqTimeoutObs _) -> do
incCounter poolTimeouts incCounter poolTimeouts
(HasqlPoolObs (SQL.ConnectionObservation _ status)) -> case status of (HasqlPoolObs (SQL.ConnectionObservation _ status)) -> case status of
@@ -52,8 +67,11 @@ 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
JwtCacheLookup True -> incCounter jwtCacheRequests *> incCounter jwtCacheHits
JwtCacheLookup False -> incCounter jwtCacheRequests
JwtCacheEviction -> incCounter jwtCacheEvictions
_ -> _ ->
pure () pure ()
+52 -8
View File
@@ -1,17 +1,61 @@
module PostgREST.Network module PostgREST.Network
( resolveHost ( resolveSocketToAddress
, escapeHostName
, isSpecialHostName
) where ) where
import Data.IP (fromHostAddress, fromHostAddress6)
import Data.String (IsString (..)) import Data.String (IsString (..))
import qualified Network.Socket as NS import qualified Network.Socket as NS
import Protolude import Protolude
resolveHost :: NS.Socket -> IO (Maybe Text) -- | Resolves the socket to an address depending on the socket type. The Show
resolveHost sock = do -- instance of the socket types automatically resolves it to the correct
-- address. Example resolution:
-- -----------------------------------------------------
-- | IPv4 | IPv6 | Unix |
-- -----------------------------------------------------
-- | 127.0.0.1:80 | [2001:db8::1]:80 | /tmp/pgrst.sock |
-- -----------------------------------------------------
resolveSocketToAddress :: NS.Socket -> IO Text
resolveSocketToAddress sock = do
sn <- NS.getSocketName sock sn <- NS.getSocketName sock
case sn of return $ showSocketAddr sn
NS.SockAddrInet _ hostAddr -> pure $ Just $ fromString $ show $ fromHostAddress hostAddr
NS.SockAddrInet6 _ _ hostAddr6 _ -> pure $ Just $ fromString $ show $ fromHostAddress6 hostAddr6 -- |
_ -> pure Nothing -- >>> let addr_ipv4 = NS.SockAddrInet 80 (NS.tupleToHostAddress (127,0,0,1))
-- >>> let addr_ipv6 = NS.SockAddrInet6 80 0 (0,0,0,1) 0
-- >>> let addr_unix = NS.SockAddrUnix "/tmp/pgrst.sock"
--
-- >>> showSocketAddr addr_ipv4
-- "127.0.0.1:80"
-- >>> showSocketAddr addr_ipv6
-- "[::1]:80"
--
-- >>> showSocketAddr addr_unix
-- "/tmp/pgrst.sock"
showSocketAddr :: NS.SockAddr -> Text
showSocketAddr = fromString . show
-- | When printing special addresses like !4 or *6, we use the following mapping.
-- These special addresses come from:
-- https://hackage.haskell.org/package/streaming-commons-0.2.3.0/docs/\
-- Data-Streaming-Network.html#t:HostPreference
-- TODO: "!6" should not be printed as "0.0.0.0" address.
escapeHostName :: Text -> Text
escapeHostName "*" = "0.0.0.0"
escapeHostName "*4" = "0.0.0.0"
escapeHostName "!4" = "0.0.0.0"
escapeHostName "*6" = "0.0.0.0"
escapeHostName "!6" = "0.0.0.0"
escapeHostName h = h
-- | Check if a hostname is special
isSpecialHostName :: Text -> Bool
isSpecialHostName "*" = True
isSpecialHostName "*4" = True
isSpecialHostName "!4" = True
isSpecialHostName "*6" = True
isSpecialHostName "!6" = True
isSpecialHostName _ = False
+67 -33
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
{-| {-|
Module : PostgREST.Observation Module : PostgREST.Observation
@@ -11,43 +12,47 @@ module PostgREST.Observation
, ObsFatalError(..) , ObsFatalError(..)
, observationMessage , observationMessage
, ObservationHandler , ObservationHandler
, showOnSingleLine
, isDbListenerBug
) 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 Network.HTTP.Types.Status (Status)
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 PostgREST.Query (MainQuery)
import Protolude import Protolude hiding (toList)
import Protolude.Partial (fromJust)
data Observation data Observation
= AdminStartObs (Maybe Text) (Maybe Int) = AdminStartObs Text
| AppStartObs ByteString | AppStartObs ByteString
| AppServerPortObs Text NS.PortNumber | AppServerAddressObs Text
| AppServerUnixObs FilePath
| ExitUnsupportedPgVersion PgVersion PgVersion | ExitUnsupportedPgVersion PgVersion PgVersion
| ExitDBNoRecoveryObs | ExitDBNoRecoveryObs
| ExitDBFatalError ObsFatalError SQL.UsageError | ExitDBFatalError ObsFatalError SQL.UsageError
| DBConnectedObs Text | DBConnectedObs Text
| SchemaCacheErrorObs SQL.UsageError | SchemaCacheEmptyObs
| SchemaCacheErrorObs (NonEmpty Text) [Text] SQL.UsageError
| SchemaCacheQueriedObs Double | SchemaCacheQueriedObs Double
| SchemaCacheSummaryObs Text | SchemaCacheSummaryObs Text
| SchemaCacheLoadedObs Double | SchemaCacheLoadedObs Double
| ConnectionRetryObs Int | ConnectionRetryObs Int
| DBListenStart Text | DBListenStart (Maybe ByteString) (Maybe ByteString) Text Text -- host, port, version string, channel
| DBListenFail Text (Either SQL.ConnectionError (Either SomeException ())) | DBListenFail Text (Either SQL.ConnectionError SomeException)
| DBListenRetry Int | DBListenRetry Int
| DBListenBugHint -- https://github.com/PostgREST/postgrest/issues/3147
| DBListenerGotSCacheMsg ByteString | DBListenerGotSCacheMsg ByteString
| DBListenerGotConfigMsg ByteString | DBListenerGotConfigMsg ByteString
| DBQuery ByteString Status | DBListenerConnectionCleanupFail SomeException
| QueryObs MainQuery Status
| ConfigReadErrorObs SQL.UsageError | ConfigReadErrorObs SQL.UsageError
| ConfigInvalidObs Text | ConfigInvalidObs Text
| ConfigSucceededObs | ConfigSucceededObs
@@ -59,6 +64,12 @@ data Observation
| HasqlPoolObs SQL.Observation | HasqlPoolObs SQL.Observation
| PoolRequest | PoolRequest
| PoolRequestFullfilled | PoolRequestFullfilled
| PoolFlushed
| JwtCacheLookup Bool
| JwtCacheEviction
| TerminationUnixSignalObs Text
| WarpServerObs Text
deriving (Generic)
data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01 data ObsFatalError = ServerAuthError | ServerPgrstBug | ServerError42P05 | ServerError08P01
@@ -66,14 +77,12 @@ type ObservationHandler = Observation -> IO ()
observationMessage :: Observation -> Text observationMessage :: Observation -> Text
observationMessage = \case observationMessage = \case
AdminStartObs host port -> AdminStartObs address ->
"Admin server listening on " <> fromJust host <> ":" <> show (fromIntegral (fromJust port) :: Integer) "Admin server listening on " <> address
AppStartObs ver -> AppStartObs ver ->
"Starting PostgREST " <> T.decodeUtf8 ver <> "..." "Starting PostgREST " <> T.decodeUtf8 ver <> "..."
AppServerPortObs host port -> AppServerAddressObs address ->
"API server listening on " <> host <> ":" <> show port "API server listening on " <> address
AppServerUnixObs sock ->
"API server listening on unix socket " <> show sock
DBConnectedObs ver -> DBConnectedObs ver ->
"Successfully connected to " <> ver "Successfully connected to " <> ver
ExitUnsupportedPgVersion pgVer minPgVer -> ExitUnsupportedPgVersion pgVer minPgVer ->
@@ -88,8 +97,14 @@ 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 usageErr -> SchemaCacheEmptyObs ->
"Failed to load the schema cache. " <> jsonMessage usageErr T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.NoSchemaCacheError
SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
"Failed to load the schema cache using "
<> "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 ->
@@ -100,22 +115,23 @@ observationMessage = \case
"Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..." "Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
QueryPgVersionError usageErr -> QueryPgVersionError usageErr ->
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr "Failed to query the PostgreSQL version. " <> jsonMessage usageErr
DBListenStart channel -> do DBListenStart host port fullName channel -> do
"Listening for database notifications on the " <> show channel <> " channel" "Listener connected to " <> fullName <> " on " <> show (fold $ host <> fmap (":" <>) port) <> " and listening for database 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 database notifications on the " <> show channel <> " channel. " <>
case listenErr of either showListenerConnError showListenerException listenErr
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 database notifications in " <> (show delay::Text) <> " seconds..."
DBListenBugHint ->
"HINT: This is likely a bug in the notification queue, try executing the following to solve it: select pg_notification_queue_usage();"
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 _ -> DBListenerConnectionCleanupFail ex ->
T.decodeUtf8 sql "Failed during listener connection cleanup: " <> showOnSingleLine '\t' (show ex)
QueryObs{} ->
mempty -- TODO pending refactor: The logic for printing the query cannot be done here. Join the observationMessage function into observationLogger to avoid this mempty.
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 ->
@@ -146,14 +162,32 @@ observationMessage = \case
"Trying to borrow a connection from pool" "Trying to borrow a connection from pool"
PoolRequestFullfilled -> PoolRequestFullfilled ->
"Borrowed a connection from the pool" "Borrowed a connection from the pool"
PoolFlushed ->
"Database connection pool flushed"
JwtCacheLookup _ ->
"Looked up a JWT in JWT cache"
JwtCacheEviction ->
"Evicted entry from JWT cache"
TerminationUnixSignalObs signal ->
"Received termination unix signal " <> signal
WarpServerObs txt ->
"Warp server: " <> txt
where where
showMillis :: Double -> Text showMillis :: Double -> Text
showMillis x = toS $ showFFloat (Just 1) (x * 1000) "" showMillis x = toS $ showFFloat (Just 1) x ""
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
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' . 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 :: SomeException -> Text
showListenerException = showOnSingleLine '\t' . show
showOnSingleLine :: Char -> Text -> Text
showOnSingleLine split txt = T.intercalate " " $ T.filter (/= split) <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
isDbListenerBug :: SomeException -> Bool
isDbListenerBug e = "could not access status of transaction" `T.isInfixOf` show e

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