Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc06471f9d | ||
|
|
6dcb0e0b02 | ||
|
|
f54aef4795 | ||
|
|
ea2d3aeb72 | ||
|
|
703fdd949f | ||
|
|
d942ea8438 | ||
|
|
97b6022f5a | ||
|
|
c413833ec8 | ||
|
|
dee7d6f39c | ||
|
|
6fb8077950 | ||
|
|
d4f82919f7 | ||
|
|
610a8be9c4 | ||
|
|
ba42e4610a | ||
|
|
8531c502d7 | ||
|
|
0ba47180ae | ||
|
|
b77605e0d3 | ||
|
|
800b32a59c | ||
|
|
c48b6bc85b | ||
|
|
f899957675 | ||
|
|
609289d2bb | ||
|
|
9e58946ec6 | ||
|
|
48a7b8dee7 | ||
|
|
23a4573a9c | ||
|
|
91814cd4f8 | ||
|
|
8ef5263b04 | ||
|
|
a71f938a44 | ||
|
|
e6d4bfa465 | ||
|
|
b90d26034e | ||
|
|
0230a844b2 | ||
|
|
e0e60fa433 | ||
|
|
7269630538 | ||
|
|
cc2550a58b | ||
|
|
e20dc60e83 | ||
|
|
89fb2878df |
+5
-5
@@ -35,6 +35,11 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
|||||||
<img width="296px" src="static/supabase.png">
|
<img width="296px" src="static/supabase.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||||
|
<img width="296px" src="static/euronodes.svg">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -88,11 +93,6 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
|||||||
<img width="222px" src="static/oblivious.jpg">
|
<img width="222px" src="static/oblivious.jpg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
|
||||||
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
|
||||||
<img width="222px" src="static/gnuhost.png">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
+24
-1
@@ -5,6 +5,28 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
## [13.0.3] - 2025-06-16
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix `max-affected` preference not failing with RPC when `handling=strict` by @taimoorzaeem in #4100
|
||||||
|
- Fix a property definition's type in OpenAPI not showing the correct base type of a recursive domain by @laurenceisla in #4136
|
||||||
|
|
||||||
|
## [13.0.2] - 2025-06-02
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix regression that makes `ORDER BY` with nulls-order not work alongside limits by @laurenceisla in #4109
|
||||||
|
|
||||||
|
## [13.0.1] - 2025-06-01
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fix jwt error returning HTTP status `400` for invalid role by @taimoorzaeem in #3601
|
||||||
|
- Fix `db-extra-search-path` cannot be set to nothing by @taimoorzaeem in #4074
|
||||||
|
+ It can now be disabled by setting it to empty string.
|
||||||
|
+ Schema Cache load error is now logged including `db-schemas` and `db-extra-search-path` config values.
|
||||||
|
|
||||||
## [13.0.0] - 2025-05-08
|
## [13.0.0] - 2025-05-08
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -47,8 +69,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
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ API than you are likely to write from scratch.
|
|||||||
<img width="296px" src="static/supabase.png">
|
<img width="296px" src="static/supabase.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||||
|
<img width="296px" src="static/euronodes.svg">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ write from scratch.
|
|||||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
|
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
||||||
|
<img width="296px" src="static/euronodes.svg">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -82,8 +82,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]]
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 26 KiB |
Vendored
+1
-1
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
+1
-1
@@ -114,7 +114,7 @@ html_theme = "sphinx_rtd_theme"
|
|||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
# further. For a list of options available for each theme, see the
|
# further. For a list of options available for each theme, see the
|
||||||
# documentation.
|
# documentation.
|
||||||
html_theme_options = {"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 = []
|
||||||
|
|||||||
+1
-1
@@ -34,7 +34,7 @@ Templates
|
|||||||
Example Apps
|
Example Apps
|
||||||
------------
|
------------
|
||||||
|
|
||||||
* `archtika <https://github.com/archtika/archtika>`_ - self‑hosted 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,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"
|
|
||||||
+10
-8
@@ -61,7 +61,7 @@ Sponsors
|
|||||||
|
|
|
|
||||||
|
|
||||||
.. image:: ../static/tembo.png
|
.. image:: ../static/tembo.png
|
||||||
:target: https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
:target: https://www.tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
||||||
|
|
||||||
.. container:: img-dark
|
.. container:: img-dark
|
||||||
|
|
||||||
@@ -73,8 +73,15 @@ Sponsors
|
|||||||
.. image:: ../static/supabase.png
|
.. 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
|
: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
|
.. container:: img-dark
|
||||||
:target: #sponsors
|
|
||||||
|
.. image:: ../static/euronodes.svg
|
||||||
|
:target: https://www.euronodes.com/postgrest
|
||||||
|
|
||||||
|
.. container:: img-light
|
||||||
|
|
||||||
|
.. image:: ../static/euronodes.svg
|
||||||
|
:target: https://www.euronodes.com/postgrest
|
||||||
|
|
||||||
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
|
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
|
||||||
to create space and center the logos. It's not easy to layout with restructuredText.
|
to create space and center the logos. It's not easy to layout with restructuredText.
|
||||||
@@ -212,17 +219,12 @@ Here are some companies that use PostgREST in production.
|
|||||||
* `Datrium <https://www.datrium.com>`_
|
* `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
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -1150,27 +1150,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 +1170,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 +1206,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,15 +1238,38 @@ 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
|
||||||
|
|
||||||
|
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
|
.. 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 "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))" \
|
||||||
-d "first_name=like.Quentin*" \
|
-d "first_name=like.Quentin*" \
|
||||||
@@ -1310,6 +1300,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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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,17 @@ 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"
|
||||||
|
|
||||||
.. _v_filter:
|
.. _v_filter:
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ If the client included no JWT (or one without a role claim) then PostgREST switc
|
|||||||
JWT Generation
|
JWT Generation
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
You can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_jwt`).
|
You can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_auth`).
|
||||||
|
|
||||||
.. _client_auth:
|
.. _client_auth:
|
||||||
|
|
||||||
@@ -156,6 +156,19 @@ You can specify the literal value as we saw earlier, or reference a filename to
|
|||||||
|
|
||||||
jwt-secret = "@rsa.jwk.pub"
|
jwt-secret = "@rsa.jwk.pub"
|
||||||
|
|
||||||
|
JWK ``kid`` validation
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
PostgREST has built-in validation of the `key ID parameter <https://www.rfc-editor.org/rfc/rfc7517#section-4.5>`_, useful when working with a JWK Set.
|
||||||
|
It goes as follows:
|
||||||
|
|
||||||
|
- If the JWT contains a ``kid`` parameter, then PostgREST will look for the JWK in the :ref:`jwt-secret`.
|
||||||
|
|
||||||
|
+ If no JWK matches the same ``kid`` value (or if they do not have a ``kid``), then the token will be rejected with a :ref:`401 Unauthorized <pgrst301>` error.
|
||||||
|
+ If a JWK matches the ``kid`` value then it will validate the token against that JWK accordingly.
|
||||||
|
|
||||||
|
- If the JWT does not have a ``kid`` parameter, then PostgREST will validate the token against each JWK in the :ref:`jwt-secret`.
|
||||||
|
|
||||||
.. _jwt_claims_validation:
|
.. _jwt_claims_validation:
|
||||||
|
|
||||||
JWT Claims Validation
|
JWT Claims Validation
|
||||||
@@ -188,12 +201,12 @@ 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_role_extract:
|
||||||
|
|
||||||
JWT Role Claim Key Extraction
|
JWT Role Extraction
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
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.
|
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,6 +237,9 @@ 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
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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:
|
||||||
|
|
||||||
@@ -718,7 +722,7 @@ log-query
|
|||||||
=============== =================================
|
=============== =================================
|
||||||
|
|
||||||
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
|
.. code:: bash
|
||||||
|
|
||||||
@@ -890,7 +894,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:
|
||||||
|
|||||||
@@ -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**:
|
||||||
|
|||||||
@@ -2,6 +2,6 @@
|
|||||||
sphinx==7.4.7
|
sphinx==7.4.7
|
||||||
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
@@ -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.
|
||||||
|
|
||||||
|
|||||||
+21
-8
@@ -2,6 +2,7 @@
|
|||||||
, aspellDicts
|
, aspellDicts
|
||||||
, buildToolbox
|
, buildToolbox
|
||||||
, checkedShellScript
|
, checkedShellScript
|
||||||
|
, fetchPypi
|
||||||
, lib
|
, lib
|
||||||
, plantuml
|
, plantuml
|
||||||
, python3
|
, python3
|
||||||
@@ -10,14 +11,26 @@
|
|||||||
, writers
|
, writers
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
selectPythonPackages = ps: [
|
selectPythonPackages = ps:
|
||||||
ps.sphinx
|
let
|
||||||
ps.sphinx-copybutton
|
# TODO: Remove with next nixpkgs update
|
||||||
ps.sphinx-rtd-dark-mode
|
sphinx-rtd-theme = assert ps.sphinx-rtd-theme.version == "2.0.0"; ps.sphinx-rtd-theme.overrideAttrs rec {
|
||||||
ps.sphinx-rtd-theme
|
version = "3.0.2";
|
||||||
ps.sphinx-tabs
|
src = fetchPypi {
|
||||||
ps.sphinxext-opengraph
|
pname = "sphinx_rtd_theme";
|
||||||
];
|
inherit version;
|
||||||
|
hash = "sha256-t0V7wl3acjsgsIamcLmVPIWeq2CioD7o6yuyPhduX4U=";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
in
|
||||||
|
[
|
||||||
|
ps.sphinx
|
||||||
|
ps.sphinx-copybutton
|
||||||
|
(ps.sphinx-rtd-dark-mode.override { inherit sphinx-rtd-theme; })
|
||||||
|
sphinx-rtd-theme
|
||||||
|
ps.sphinx-tabs
|
||||||
|
ps.sphinxext-opengraph
|
||||||
|
];
|
||||||
|
|
||||||
requirements = writeTextFile {
|
requirements = writeTextFile {
|
||||||
name = "requirements.txt";
|
name = "requirements.txt";
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 13.0.0
|
version: 13.0.3
|
||||||
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
|
||||||
|
|||||||
@@ -407,7 +407,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
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ dumpSchema appState = do
|
|||||||
case result of
|
case result of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
let observer = AppState.getObserver appState
|
let observer = AppState.getObserver appState
|
||||||
observer $ SchemaCacheErrorObs e
|
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
|
||||||
exitFailure
|
exitFailure
|
||||||
Right sCache -> return $ JSON.encode sCache
|
Right sCache -> return $ JSON.encode sCache
|
||||||
|
|
||||||
|
|||||||
+12
-9
@@ -256,8 +256,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 +272,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
|
||||||
@@ -404,8 +404,8 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
optString :: C.Key -> C.Parser C.Config (Maybe Text)
|
optString :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||||
optString k = mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
optString k = mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
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 +445,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"]
|
||||||
|
|
||||||
|
|||||||
@@ -96,6 +96,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 +139,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 +186,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
|
||||||
@@ -209,6 +212,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
|
||||||
@@ -595,6 +599,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
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schema
|
|||||||
SchemaCacheLoadedObs resTime -> do
|
SchemaCacheLoadedObs resTime -> do
|
||||||
withLabel schemaCacheLoads "SUCCESS" incCounter
|
withLabel schemaCacheLoads "SUCCESS" incCounter
|
||||||
setGauge schemaCacheQueryTime resTime
|
setGauge schemaCacheQueryTime resTime
|
||||||
SchemaCacheErrorObs _ -> do
|
SchemaCacheErrorObs{} -> do
|
||||||
withLabel schemaCacheLoads "FAIL" incCounter
|
withLabel schemaCacheLoads "FAIL" incCounter
|
||||||
_ ->
|
_ ->
|
||||||
pure ()
|
pure ()
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ module PostgREST.Observation
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
import Data.List.NonEmpty (toList)
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Encoding as T
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Hasql.Connection as SQL
|
import qualified Hasql.Connection as SQL
|
||||||
@@ -25,7 +26,7 @@ import Numeric (showFFloat)
|
|||||||
import PostgREST.Config.PgVersion
|
import PostgREST.Config.PgVersion
|
||||||
import qualified PostgREST.Error as Error
|
import qualified PostgREST.Error as Error
|
||||||
|
|
||||||
import Protolude
|
import Protolude hiding (toList)
|
||||||
import Protolude.Partial (fromJust)
|
import Protolude.Partial (fromJust)
|
||||||
|
|
||||||
data Observation
|
data Observation
|
||||||
@@ -37,7 +38,7 @@ data Observation
|
|||||||
| ExitDBNoRecoveryObs
|
| ExitDBNoRecoveryObs
|
||||||
| ExitDBFatalError ObsFatalError SQL.UsageError
|
| ExitDBFatalError ObsFatalError SQL.UsageError
|
||||||
| DBConnectedObs Text
|
| DBConnectedObs Text
|
||||||
| SchemaCacheErrorObs SQL.UsageError
|
| SchemaCacheErrorObs (NonEmpty Text) [Text] SQL.UsageError
|
||||||
| SchemaCacheQueriedObs Double
|
| SchemaCacheQueriedObs Double
|
||||||
| SchemaCacheSummaryObs Text
|
| SchemaCacheSummaryObs Text
|
||||||
| SchemaCacheLoadedObs Double
|
| SchemaCacheLoadedObs Double
|
||||||
@@ -88,8 +89,12 @@ 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 ->
|
SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
|
||||||
"Failed to load the schema cache. " <> jsonMessage 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 ->
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
|||||||
RoutineParam (..),
|
RoutineParam (..),
|
||||||
funcReturnsCompositeAlias,
|
funcReturnsCompositeAlias,
|
||||||
funcReturnsScalar,
|
funcReturnsScalar,
|
||||||
funcReturnsSetOfScalar)
|
funcReturnsSetOfScalar,
|
||||||
|
funcReturnsSingle)
|
||||||
import PostgREST.SchemaCache.Table (Column (..), Table (..),
|
import PostgREST.SchemaCache.Table (Column (..), Table (..),
|
||||||
TablesMap,
|
TablesMap,
|
||||||
tableColumnsList,
|
tableColumnsList,
|
||||||
@@ -172,7 +173,7 @@ mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..}
|
|||||||
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi
|
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi
|
||||||
|
|
||||||
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
|
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
|
||||||
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{preferHandling, invalidPrefs},..} invMethod = do
|
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{preferHandling, invalidPrefs, preferMaxAffected},..} invMethod = do
|
||||||
let paramKeys = case invMethod of
|
let paramKeys = case invMethod of
|
||||||
InvRead _ -> S.fromList $ fst <$> qsParams'
|
InvRead _ -> S.fromList $ fst <$> qsParams'
|
||||||
Inv -> iColumns
|
Inv -> iColumns
|
||||||
@@ -192,10 +193,15 @@ callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferenc
|
|||||||
cPlan = callPlan proc apiRequest paramKeys args rPlan
|
cPlan = callPlan proc apiRequest paramKeys args rPlan
|
||||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||||
|
failMaxAffectedRpcReturnsSingle (preferMaxAffected, preferHandling) proc
|
||||||
return $ CallReadPlan rPlan cPlan txMode proc handler mediaType invMethod identifier
|
return $ CallReadPlan rPlan cPlan txMode proc handler mediaType invMethod identifier
|
||||||
where
|
where
|
||||||
qsParams' = QueryParams.qsParams iQueryParams
|
qsParams' = QueryParams.qsParams iQueryParams
|
||||||
|
|
||||||
|
failMaxAffectedRpcReturnsSingle :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> Routine -> Either Error ()
|
||||||
|
failMaxAffectedRpcReturnsSingle (Just (PreferMaxAffected _), Just Strict) rout = if funcReturnsSingle rout then Left $ ApiRequestError MaxAffectedRpcViolation else Right ()
|
||||||
|
failMaxAffectedRpcReturnsSingle _ _ = Right ()
|
||||||
|
|
||||||
hasDefaultSelect :: ReadPlanTree -> Bool
|
hasDefaultSelect :: ReadPlanTree -> Bool
|
||||||
hasDefaultSelect (Node ReadPlan{select=[CoercibleSelectField{csField=CoercibleField{cfName}}]} []) = cfName == "*"
|
hasDefaultSelect (Node ReadPlan{select=[CoercibleSelectField{csField=CoercibleField{cfName}}]} []) = cfName == "*"
|
||||||
hasDefaultSelect _ = False
|
hasDefaultSelect _ = False
|
||||||
|
|||||||
@@ -53,8 +53,8 @@ readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicFor
|
|||||||
(if null logicForest && null relJoinConds
|
(if null logicForest && null relJoinConds
|
||||||
then mempty
|
then mempty
|
||||||
else " WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition relJoinConds)) <> " " <>
|
else " WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition relJoinConds)) <> " " <>
|
||||||
groupF qi select relSelect <>
|
groupF qi select relSelect <> " " <>
|
||||||
orderF qi order <>
|
orderF qi order <> " " <>
|
||||||
limitOffsetF readRange
|
limitOffsetF readRange
|
||||||
where
|
where
|
||||||
fromFrag = fromF relToParent mainQi fromAlias
|
fromFrag = fromF relToParent mainQi fromAlias
|
||||||
|
|||||||
@@ -375,32 +375,40 @@ accessibleFuncs = SQL.Statement sql params decodeFuncs
|
|||||||
(snd >$< arrayParam HE.text)
|
(snd >$< arrayParam HE.text)
|
||||||
sql = funcsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')"
|
sql = funcsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')"
|
||||||
|
|
||||||
funcsSqlQuery :: SqlQuery
|
baseTypesCte :: Text
|
||||||
funcsSqlQuery = encodeUtf8 [trimming|
|
baseTypesCte = [trimming|
|
||||||
-- Recursively get the base types of domains
|
-- Recursively get the base types of domains
|
||||||
WITH
|
|
||||||
base_types AS (
|
base_types AS (
|
||||||
WITH RECURSIVE
|
WITH RECURSIVE
|
||||||
recurse AS (
|
recurse AS (
|
||||||
SELECT
|
SELECT
|
||||||
oid,
|
oid,
|
||||||
typbasetype,
|
typbasetype,
|
||||||
COALESCE(NULLIF(typbasetype, 0), oid) AS base
|
typnamespace AS base_namespace,
|
||||||
|
COALESCE(NULLIF(typbasetype, 0), oid) AS base_type
|
||||||
FROM pg_type
|
FROM pg_type
|
||||||
UNION
|
UNION
|
||||||
SELECT
|
SELECT
|
||||||
t.oid,
|
t.oid,
|
||||||
b.typbasetype,
|
b.typbasetype,
|
||||||
COALESCE(NULLIF(b.typbasetype, 0), b.oid) AS base
|
b.typnamespace AS base_namespace,
|
||||||
|
COALESCE(NULLIF(b.typbasetype, 0), b.oid) AS base_type
|
||||||
FROM recurse t
|
FROM recurse t
|
||||||
JOIN pg_type b ON t.typbasetype = b.oid
|
JOIN pg_type b ON t.typbasetype = b.oid
|
||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
oid,
|
oid,
|
||||||
base
|
base_namespace,
|
||||||
|
base_type
|
||||||
FROM recurse
|
FROM recurse
|
||||||
WHERE typbasetype = 0
|
WHERE typbasetype = 0
|
||||||
),
|
)
|
||||||
|
|]
|
||||||
|
|
||||||
|
funcsSqlQuery :: SqlQuery
|
||||||
|
funcsSqlQuery = encodeUtf8 [trimming|
|
||||||
|
WITH
|
||||||
|
$baseTypesCte,
|
||||||
arguments AS (
|
arguments AS (
|
||||||
SELECT
|
SELECT
|
||||||
oid,
|
oid,
|
||||||
@@ -440,7 +448,7 @@ funcsSqlQuery = encodeUtf8 [trimming|
|
|||||||
-- if any TABLE, INOUT or OUT arguments present, treat as composite
|
-- if any TABLE, INOUT or OUT arguments present, treat as composite
|
||||||
or COALESCE(proargmodes::text[] && '{t,b,o}', false)
|
or COALESCE(proargmodes::text[] && '{t,b,o}', false)
|
||||||
) AS rettype_is_composite,
|
) AS rettype_is_composite,
|
||||||
bt.oid <> bt.base as rettype_is_composite_alias,
|
bt.oid <> bt.base_type as rettype_is_composite_alias,
|
||||||
p.provolatile,
|
p.provolatile,
|
||||||
p.provariadic > 0 as hasvariadic,
|
p.provariadic > 0 as hasvariadic,
|
||||||
lower((regexp_split_to_array((regexp_split_to_array(iso_config, '='))[2], ','))[1]) AS transaction_isolation_level,
|
lower((regexp_split_to_array((regexp_split_to_array(iso_config, '='))[2], ','))[1]) AS transaction_isolation_level,
|
||||||
@@ -449,7 +457,7 @@ funcsSqlQuery = encodeUtf8 [trimming|
|
|||||||
LEFT JOIN arguments a ON a.oid = p.oid
|
LEFT JOIN arguments a ON a.oid = p.oid
|
||||||
JOIN pg_namespace pn ON pn.oid = p.pronamespace
|
JOIN pg_namespace pn ON pn.oid = p.pronamespace
|
||||||
JOIN base_types bt ON bt.oid = p.prorettype
|
JOIN base_types bt ON bt.oid = p.prorettype
|
||||||
JOIN pg_type t ON t.oid = bt.base
|
JOIN pg_type t ON t.oid = bt.base_type
|
||||||
JOIN pg_namespace tn ON tn.oid = t.typnamespace
|
JOIN pg_namespace tn ON tn.oid = t.typnamespace
|
||||||
LEFT JOIN pg_class comp ON comp.oid = t.typrelid
|
LEFT JOIN pg_class comp ON comp.oid = t.typrelid
|
||||||
LEFT JOIN pg_description as d ON d.objoid = p.oid AND d.classoid = 'pg_proc'::regclass
|
LEFT JOIN pg_description as d ON d.objoid = p.oid AND d.classoid = 'pg_proc'::regclass
|
||||||
@@ -615,6 +623,7 @@ tablesSqlQuery =
|
|||||||
-- generated columns are only available from pg >= 10 but the query is agnostic to versions. dep.deptype = 'i' is done because there are other 'a' dependencies on PKs
|
-- generated columns are only available from pg >= 10 but the query is agnostic to versions. dep.deptype = 'i' is done because there are other 'a' dependencies on PKs
|
||||||
encodeUtf8 [trimming|
|
encodeUtf8 [trimming|
|
||||||
WITH
|
WITH
|
||||||
|
$baseTypesCte,
|
||||||
columns AS (
|
columns AS (
|
||||||
SELECT
|
SELECT
|
||||||
c.oid AS relid,
|
c.oid AS relid,
|
||||||
@@ -631,7 +640,7 @@ tablesSqlQuery =
|
|||||||
CASE
|
CASE
|
||||||
WHEN t.typtype = 'd' THEN
|
WHEN t.typtype = 'd' THEN
|
||||||
CASE
|
CASE
|
||||||
WHEN bt.typnamespace = 'pg_catalog'::regnamespace THEN format_type(t.typbasetype, NULL::integer)
|
WHEN bt.base_namespace = 'pg_catalog'::regnamespace THEN format_type(bt.base_type, NULL::integer)
|
||||||
ELSE format_type(a.atttypid, a.atttypmod)
|
ELSE format_type(a.atttypid, a.atttypmod)
|
||||||
END
|
END
|
||||||
ELSE
|
ELSE
|
||||||
@@ -645,7 +654,7 @@ tablesSqlQuery =
|
|||||||
information_schema._pg_truetypid(a.*, t.*),
|
information_schema._pg_truetypid(a.*, t.*),
|
||||||
information_schema._pg_truetypmod(a.*, t.*)
|
information_schema._pg_truetypmod(a.*, t.*)
|
||||||
)::integer AS character_maximum_length,
|
)::integer AS character_maximum_length,
|
||||||
COALESCE(bt.oid, t.oid) AS base_type,
|
bt.base_type,
|
||||||
a.attnum::integer AS position
|
a.attnum::integer AS position
|
||||||
FROM pg_attribute a
|
FROM pg_attribute a
|
||||||
LEFT JOIN pg_description AS d
|
LEFT JOIN pg_description AS d
|
||||||
@@ -656,8 +665,8 @@ tablesSqlQuery =
|
|||||||
ON a.attrelid = c.oid
|
ON a.attrelid = c.oid
|
||||||
JOIN pg_type t
|
JOIN pg_type t
|
||||||
ON a.atttypid = t.oid
|
ON a.atttypid = t.oid
|
||||||
LEFT JOIN pg_type bt
|
LEFT JOIN base_types bt
|
||||||
ON t.typtype = 'd' AND t.typbasetype = bt.oid
|
ON t.oid = bt.oid
|
||||||
LEFT JOIN pg_depend seq
|
LEFT JOIN pg_depend seq
|
||||||
ON seq.refobjid = a.attrelid and seq.refobjsubid = a.attnum and seq.deptype = 'i'
|
ON seq.refobjid = a.attrelid and seq.refobjsubid = a.attnum and seq.deptype = 'i'
|
||||||
WHERE
|
WHERE
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<svg width="917" height="146" viewBox="0 0 917 146" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M19.5462 82.5408H83.3862C80.9862 64.1408 69.1462 52.6208 52.0262 52.6208C35.3862 52.6208 22.1062 64.9408 19.5462 82.5408ZM102.906 92.6208V97.5808H19.7062C23.0662 115.341 37.1462 127.181 55.3862 127.181C67.8662 127.181 79.3862 121.261 86.2662 112.941L98.7462 125.261C88.5062 138.381 73.1462 145.101 54.5862 145.101C23.0662 145.101 0.186172 122.701 0.186172 89.9008C0.186172 58.3808 22.5862 34.7008 52.5062 34.7008C83.3862 34.7008 102.906 57.2608 102.906 92.6208ZM129.367 35.5008V98.7008C129.367 118.701 139.447 127.821 155.927 127.821C173.687 127.821 187.927 112.941 187.927 92.7808V35.5008H206.327V144.301H189.527L187.927 123.181V122.061C182.647 132.301 171.447 145.101 148.407 145.101C126.167 145.101 110.967 131.181 110.967 105.901V35.5008H129.367ZM217.72 144.301V35.5008H234.52L236.12 56.1408C241.88 42.2208 253.24 34.7008 268.6 34.7008C272.44 34.7008 275.96 35.0208 279.16 35.5008L277.72 53.5808C274.68 52.7808 271.16 52.4608 266.36 52.4608C247 52.4608 236.12 68.3008 236.12 91.6608V144.301H217.72ZM278.311 89.9008C278.311 56.7808 300.071 34.7008 332.551 34.7008C365.191 34.7008 386.791 56.7808 386.791 89.9008C386.791 123.021 365.191 145.101 332.551 145.101C300.071 145.101 278.311 123.021 278.311 89.9008ZM297.031 89.7408C297.031 112.141 311.591 127.021 332.551 127.021C353.671 127.021 368.071 112.141 368.071 89.7408C368.071 67.6608 353.671 52.7808 332.551 52.7808C311.591 52.7808 297.031 67.6608 297.031 89.7408Z" fill="#FF0831"/>
|
||||||
|
<path d="M471.711 144.301V86.7008C471.711 64.1408 461.311 51.9808 443.711 51.9808C426.911 51.9808 413.151 69.1008 413.151 88.9408V144.301H394.751V35.5008H411.551L413.151 56.6208V59.5008C418.271 48.3008 428.671 34.7008 449.631 34.7008C473.631 34.7008 490.111 51.6608 490.111 76.9408V144.301H471.711ZM498.467 89.9008C498.467 56.7808 520.227 34.7008 552.707 34.7008C585.347 34.7008 606.947 56.7808 606.947 89.9008C606.947 123.021 585.347 145.101 552.707 145.101C520.227 145.101 498.467 123.021 498.467 89.9008ZM517.187 89.7408C517.187 112.141 531.747 127.021 552.707 127.021C573.827 127.021 588.227 112.141 588.227 89.7408C588.227 67.6608 573.827 52.7808 552.707 52.7808C531.747 52.7808 517.187 67.6608 517.187 89.7408ZM697.147 97.5808V85.5808C697.147 68.1408 682.907 52.4608 664.987 52.4608C647.067 52.4608 632.027 67.6608 632.027 89.7408C632.027 112.141 646.427 127.341 665.307 127.341C684.027 127.341 697.147 115.341 697.147 97.5808ZM698.747 144.301L697.307 127.501C691.707 136.301 681.307 145.101 662.427 145.101C634.107 145.101 613.307 123.021 613.307 89.9008C613.307 56.7808 634.427 34.7008 661.307 34.7008C680.187 34.7008 690.907 44.1408 697.147 53.9008V0.300776H715.547V144.301H698.747ZM744.702 82.5408H808.542C806.142 64.1408 794.302 52.6208 777.182 52.6208C760.542 52.6208 747.262 64.9408 744.702 82.5408ZM828.062 92.6208V97.5808H744.862C748.222 115.341 762.302 127.181 780.542 127.181C793.022 127.181 804.542 121.261 811.422 112.941L823.902 125.261C813.662 138.381 798.302 145.101 779.742 145.101C748.222 145.101 725.342 122.701 725.342 89.9008C725.342 58.3808 747.742 34.7008 777.662 34.7008C808.542 34.7008 828.062 57.2608 828.062 92.6208ZM916.161 115.181C916.161 132.781 900.481 145.101 876.641 145.101C858.721 145.101 844.321 137.581 834.561 127.021L843.841 112.781C851.361 120.781 861.761 128.141 876.801 128.141C889.761 128.141 897.441 122.221 897.441 114.221C897.441 90.5408 836.801 104.941 836.801 67.1808C836.801 47.9808 853.121 34.7008 874.721 34.7008C892.001 34.7008 905.761 42.0608 914.881 53.9008L902.721 65.4208C895.841 56.6208 884.961 52.1408 873.761 52.1408C862.721 52.1408 855.041 58.7008 855.041 67.0208C855.041 89.1008 916.161 75.5008 916.161 115.181Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
@@ -196,6 +196,24 @@ roleclaims:
|
|||||||
- obj_key: obj_value
|
- obj_key: obj_value
|
||||||
expected_status: 401 # fails because it compares an object with a string
|
expected_status: 401 # fails because it compares an object with a string
|
||||||
|
|
||||||
|
jwtaudroleclaims:
|
||||||
|
- key: '.aud'
|
||||||
|
data:
|
||||||
|
aud: postgrest_test_author
|
||||||
|
expected_status: 200
|
||||||
|
- key: '.aud'
|
||||||
|
data:
|
||||||
|
aud: postgrest_test_invalid
|
||||||
|
expected_status: 401
|
||||||
|
- key: '.aud[0]'
|
||||||
|
data:
|
||||||
|
aud: [postgrest_test_author]
|
||||||
|
expected_status: 200
|
||||||
|
- key: '.aud[1]' # succeeds the aud claims check, but fail when hits the db
|
||||||
|
data:
|
||||||
|
aud: [postgrest_test_author, postgrest_test_invalid]
|
||||||
|
expected_status: 401
|
||||||
|
|
||||||
invalidroleclaimkeys:
|
invalidroleclaimkeys:
|
||||||
- 'role.other'
|
- 'role.other'
|
||||||
- '.role##'
|
- '.role##'
|
||||||
|
|||||||
+56
-2
@@ -96,8 +96,7 @@ def test_jwt_errors(defaultenv):
|
|||||||
|
|
||||||
headers = jwtauthheader({"role": "not_existing"}, SECRET)
|
headers = jwtauthheader({"role": "not_existing"}, SECRET)
|
||||||
response = postgrest.session.get("/", headers=headers)
|
response = postgrest.session.get("/", headers=headers)
|
||||||
# TODO: Should this return 401?
|
assert response.status_code == 401
|
||||||
assert response.status_code == 400
|
|
||||||
assert response.json()["message"] == 'role "not_existing" does not exist'
|
assert response.json()["message"] == 'role "not_existing" does not exist'
|
||||||
|
|
||||||
# -31 seconds, because we allow clock skew of 30 seconds
|
# -31 seconds, because we allow clock skew of 30 seconds
|
||||||
@@ -234,6 +233,28 @@ def test_role_claim_key(roleclaim, defaultenv):
|
|||||||
assert response.status_code == roleclaim["expected_status"]
|
assert response.status_code == roleclaim["expected_status"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"jwtaudroleclaim",
|
||||||
|
FIXTURES["jwtaudroleclaims"],
|
||||||
|
ids=lambda claim: claim["key"] + "_" + str(claim["expected_status"]),
|
||||||
|
)
|
||||||
|
def test_jwt_aud_in_role_claim_key(jwtaudroleclaim, defaultenv):
|
||||||
|
"Allows authorization with JWT aud claim in role-claim-key"
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**defaultenv,
|
||||||
|
"PGRST_JWT_AUD": "postgrest_test_author",
|
||||||
|
"PGRST_JWT_ROLE_CLAIM_KEY": jwtaudroleclaim["key"],
|
||||||
|
"PGRST_JWT_SECRET": SECRET,
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = jwtauthheader(jwtaudroleclaim["data"], SECRET)
|
||||||
|
|
||||||
|
with run(env=env) as postgrest:
|
||||||
|
response = postgrest.session.get("/authors_only", headers=headers)
|
||||||
|
assert response.status_code == jwtaudroleclaim["expected_status"]
|
||||||
|
|
||||||
|
|
||||||
def test_iat_claim(defaultenv):
|
def test_iat_claim(defaultenv):
|
||||||
"""
|
"""
|
||||||
A claim with an 'iat' (issued at) attribute should be successful.
|
A claim with an 'iat' (issued at) attribute should be successful.
|
||||||
@@ -1875,3 +1896,36 @@ def test_invalidate_jwt_cache_when_secret_changes(tmp_path, defaultenv):
|
|||||||
# now the request should fail because the cached token is removed
|
# now the request should fail because the cached token is removed
|
||||||
response = postgrest.session.get("/authors_only", headers=headers)
|
response = postgrest.session.get("/authors_only", headers=headers)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
def test_allow_configs_to_be_set_to_empty(defaultenv):
|
||||||
|
'configs that are explicitly set to empty (= "<empty>") should not throw parse error'
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**defaultenv,
|
||||||
|
"PGRST_DB_EXTRA_SEARCH_PATH": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
with run(env=env) as postgrest:
|
||||||
|
response = postgrest.session.get("/projects")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_schema_cache_error_observation(defaultenv):
|
||||||
|
"schema cache error observation should be logged with invalid db-schemas or db-extra-search-path"
|
||||||
|
|
||||||
|
env = {
|
||||||
|
**defaultenv,
|
||||||
|
"PGRST_DB_EXTRA_SEARCH_PATH": "x",
|
||||||
|
}
|
||||||
|
|
||||||
|
with run(env=env, no_startup_stdout=False, wait_for_readiness=False) as postgrest:
|
||||||
|
# TODO: postgrest should exit here, instead it keeps retrying
|
||||||
|
# exitCode = wait_until_exit(postgrest)
|
||||||
|
# assert exitCode == 1
|
||||||
|
|
||||||
|
output = postgrest.read_stdout(nlines=9)
|
||||||
|
assert (
|
||||||
|
"Failed to load the schema cache using db-schemas=public and db-extra-search-path=x"
|
||||||
|
in output[7]
|
||||||
|
)
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ pgErrorCodeMapping = do
|
|||||||
it "works with SchemaCache error" $
|
it "works with SchemaCache error" $
|
||||||
get "/non_existent_table"
|
get "/non_existent_table"
|
||||||
`shouldRespondWith`
|
`shouldRespondWith`
|
||||||
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.json_table'","message":"Could not find the table 'test.non_existent_table' in the schema cache"} |]
|
[json| {"code":"PGRST205","details":null,"hint":"Perhaps you meant the table 'test.collision_test_table'","message":"Could not find the table 'test.non_existent_table' in the schema cache"} |]
|
||||||
{ matchStatus = 404
|
{ matchStatus = 404
|
||||||
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST205"
|
, matchHeaders = [ "Proxy-Status" <:> "PostgREST; error=PGRST205"
|
||||||
, "Content-Length" <:> "172" ]
|
, "Content-Length" <:> "182" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
it "works with Jwt error" $ do
|
it "works with Jwt error" $ do
|
||||||
|
|||||||
@@ -191,3 +191,48 @@ spec =
|
|||||||
""
|
""
|
||||||
{ matchStatus = 204
|
{ matchStatus = 204
|
||||||
, matchHeaders = ["Preference-Applied" <:> "handling=lenient"]}
|
, matchHeaders = ["Preference-Applied" <:> "handling=lenient"]}
|
||||||
|
|
||||||
|
context "test Prefer: max-affected with rpc" $ do
|
||||||
|
it "should fail with rpc when deleting rows more than prefered with returns setof" $
|
||||||
|
request methodPost "/rpc/delete_items_returns_setof"
|
||||||
|
[("Prefer", "handling=strict, max-affected=10")]
|
||||||
|
""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {"code":"PGRST124","details":"The query affects 15 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"} |]
|
||||||
|
{ matchStatus = 400 }
|
||||||
|
|
||||||
|
it "should fail with rpc when deleting rows more than prefered with returns table" $
|
||||||
|
request methodPost "/rpc/delete_items_returns_table"
|
||||||
|
[("Prefer", "handling=strict, max-affected=10")]
|
||||||
|
""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {"code":"PGRST124","details":"The query affects 15 rows","hint":null,"message":"Query result exceeds max-affected preference constraint"} |]
|
||||||
|
{ matchStatus = 400 }
|
||||||
|
|
||||||
|
it "should succeed with rpc deleting rows less than prefered with returns setof" $
|
||||||
|
request methodPost "/rpc/delete_items_returns_setof"
|
||||||
|
[("Prefer", "handling=strict, max-affected=20")]
|
||||||
|
""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},
|
||||||
|
{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},
|
||||||
|
{"id":14},{"id":15}]|]
|
||||||
|
{ matchStatus = 200 }
|
||||||
|
|
||||||
|
it "should succeed with rpc deleting rows less than prefered with returns table" $
|
||||||
|
request methodPost "/rpc/delete_items_returns_table"
|
||||||
|
[("Prefer", "handling=strict, max-affected=20")]
|
||||||
|
""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},
|
||||||
|
{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},
|
||||||
|
{"id":14},{"id":15}]|]
|
||||||
|
{ matchStatus = 200 }
|
||||||
|
|
||||||
|
it "should fail with rpc when returns void with handling=strict" $
|
||||||
|
request methodPost "/rpc/delete_items_returns_void"
|
||||||
|
[("Prefer", "handling=strict, max-affected=20")]
|
||||||
|
""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| {"code":"PGRST128","details":null,"hint":null,"message":"Function must return SETOF or TABLE when max-affected preference is used with handling=strict"} |]
|
||||||
|
{ matchStatus = 400 }
|
||||||
|
|||||||
@@ -223,6 +223,14 @@ spec = do
|
|||||||
, "Content-Range" <:> "2-4/*" ]
|
, "Content-Range" <:> "2-4/*" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it "works alongside order by with nulls order" $
|
||||||
|
get "/clients?select=id,projects(id,tasks(id))&order=id.asc.nullslast&limit=1&projects.order=id.asc.nullsfirst&projects.limit=2"
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json|[{"id":1,"projects":[{"id": 1, "tasks": [{"id": 1}, {"id": 2}]}, {"id": 2, "tasks": [{"id": 3}, {"id": 4}]}]}]|]
|
||||||
|
{ matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Range" <:> "0-0/*"]
|
||||||
|
}
|
||||||
|
|
||||||
context "succeeds if offset equals 0 as a no-op" $ do
|
context "succeeds if offset equals 0 as a no-op" $ do
|
||||||
it "no items" $ do
|
it "no items" $ do
|
||||||
get "/items?offset=0&id=eq.0"
|
get "/items?offset=0&id=eq.0"
|
||||||
|
|||||||
@@ -1450,3 +1450,11 @@ spec =
|
|||||||
{ matchStatus = 200
|
{ matchStatus = 200
|
||||||
, matchHeaders = [matchContentTypeJson]
|
, matchHeaders = [matchContentTypeJson]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context "schema cache duplicate definitions when two entries in pg_description have the same OID" $
|
||||||
|
it "doesn't err with 300 Multiple Choices" $
|
||||||
|
request methodGet "/rpc/collision_test_func?id=1"
|
||||||
|
[] ""
|
||||||
|
`shouldRespondWith`
|
||||||
|
[json| 1 |]
|
||||||
|
{ matchStatus = 200 }
|
||||||
|
|||||||
Vendored
+28
@@ -3800,3 +3800,31 @@ create table factory_buildings (
|
|||||||
factory_id int references factories(id),
|
factory_id int references factories(id),
|
||||||
inspections jsonb
|
inspections jsonb
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- collision test as occured in https://github.com/PostgREST/postgrest/issues/4052
|
||||||
|
create table test.collision_test_table (id integer);
|
||||||
|
comment on table collision_test_table is 'foobarbaz';
|
||||||
|
|
||||||
|
create function test.collision_test_func(id integer)
|
||||||
|
returns int language sql as $$
|
||||||
|
select 1;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
update pg_proc
|
||||||
|
set oid = 'test.collision_test_table'::regclass::oid
|
||||||
|
where oid = 'test.collision_test_func'::regproc::oid;
|
||||||
|
|
||||||
|
comment on function test.collision_test_func(id integer) is 'fizzbuzz';
|
||||||
|
|
||||||
|
|
||||||
|
create or replace function test.delete_items_returns_setof() returns setof items as $$
|
||||||
|
delete from items where id <= 15 returning *; -- deletes 15 items, then return them
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.delete_items_returns_table() returns table(id bigint) as $$
|
||||||
|
delete from items where id <= 15 returning *;
|
||||||
|
$$ language sql;
|
||||||
|
|
||||||
|
create or replace function test.delete_items_returns_void() returns void as $$
|
||||||
|
delete from items;
|
||||||
|
$$ language sql;
|
||||||
|
|||||||
Reference in New Issue
Block a user