Compare commits
@@ -1,5 +1,5 @@
|
|||||||
freebsd_instance:
|
freebsd_instance:
|
||||||
image_family: freebsd-14-3
|
image_family: freebsd-14-2
|
||||||
|
|
||||||
build_task:
|
build_task:
|
||||||
# Don't change this name without adjusting .github/workflows/build.yaml
|
# Don't change this name without adjusting .github/workflows/build.yaml
|
||||||
@@ -35,7 +35,7 @@ build_task:
|
|||||||
- find main src -type f -iname '*.hs' -exec md5sum "{}" +
|
- find main src -type f -iname '*.hs' -exec md5sum "{}" +
|
||||||
|
|
||||||
build_script: |
|
build_script: |
|
||||||
stack build -j 1 --local-bin-path . --copy-bins
|
stack build -j 1 --local-bin-path . --copy-bins --stack-yaml stack-21.7.yaml
|
||||||
strip postgrest
|
strip postgrest
|
||||||
|
|
||||||
bin_artifacts:
|
bin_artifacts:
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
charset = utf-8
|
|
||||||
end_of_line = lf
|
|
||||||
indent_size = 2
|
|
||||||
indent_style = space
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!--
|
||||||
|
Before reporting a bug:
|
||||||
|
If your database schema has changed while the PostgREST server is running,
|
||||||
|
send the server a SIGUSR1 signal or restart it(http://postgrest.org/en/stable/admin.html#schema-reloading)
|
||||||
|
to ensure the schema cache is not stale. This sometimes fixes apparent bugs.
|
||||||
|
-->
|
||||||
|
### Environment
|
||||||
|
|
||||||
|
* PostgreSQL version: (if using docker, specify the image)
|
||||||
|
* PostgREST version: (if using docker, specify the image)
|
||||||
|
* Operating system:
|
||||||
|
|
||||||
|
### Description of issue
|
||||||
|
|
||||||
|
(Expected behavior vs actual behavior)
|
||||||
|
|
||||||
|
(Steps to reproduce: Include a minimal SQL definition plus how you make the request to PostgREST and the response body)
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a bug report to help us improve
|
|
||||||
type: Bug
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Before reporting a bug:
|
|
||||||
If your database schema has changed while the PostgREST server is running,
|
|
||||||
send the server a SIGUSR1 signal or restart it (http://postgrest.org/en/stable/admin.html#schema-reloading) to ensure the schema cache is not stale. This sometimes fixes apparent bugs.
|
|
||||||
-->
|
|
||||||
### Environment
|
|
||||||
|
|
||||||
* PostgreSQL version: (if using docker, specify the image)
|
|
||||||
* PostgREST version: (if using docker, specify the image)
|
|
||||||
* Operating system:
|
|
||||||
|
|
||||||
### Description of issue
|
|
||||||
|
|
||||||
Describe the behavior you expected vs the actual behavior. Include:
|
|
||||||
|
|
||||||
- A minimal SQL definition.
|
|
||||||
- How you make the request to PostgREST (curl command preferred).
|
|
||||||
- The PostgREST response.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an enhancement for this project
|
|
||||||
type: Feature
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
A clear and concise description of what the problem is.
|
|
||||||
|
|
||||||
## Solution
|
|
||||||
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
@@ -8,33 +8,18 @@
|
|||||||
"/^v[0-9]+/"
|
"/^v[0-9]+/"
|
||||||
],
|
],
|
||||||
"rebaseWhen": "conflicted",
|
"rebaseWhen": "conflicted",
|
||||||
"pip_requirements": {
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
|
||||||
"matchBaseBranches": [ "/^v[0-9]+/" ],
|
|
||||||
"matchManagers": ["haskell-cabal"],
|
|
||||||
"enabled": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"matchBaseBranches": [ "/^v[0-9]+/" ],
|
"matchBaseBranches": [ "/^v[0-9]+/" ],
|
||||||
"groupName": "all dependencies"
|
"groupName": "all dependencies"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchManagers": ["haskell-cabal"],
|
"matchPackageNames": ["docutils"],
|
||||||
"matchPackageNames": ["base", "bytestring", "containers", "directory", "mtl", "parsec", "process", "text"],
|
"allowedVersions": "<0.21.0"
|
||||||
"groupName": "GHC dependencies"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"matchManagers": ["haskell-cabal"],
|
"matchPackageNames": ["macos"],
|
||||||
"matchPackageNames": ["hasql", "hasql-dynamic-statements", "hasql-notifications", "hasql-transaction", "hasql-pool"],
|
"allowedVersions": "<13"
|
||||||
"groupName": "hasql"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"matchManagers": ["haskell-cabal"],
|
|
||||||
"matchPackageNames": ["fuzzyset"],
|
|
||||||
"allowedVersions": "<0.3"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
name: Docs
|
name: Docs
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_call:
|
push:
|
||||||
secrets:
|
branches:
|
||||||
CACHIX_AUTH_TOKEN:
|
- main
|
||||||
required: false
|
- v[0-9]+
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -51,3 +51,17 @@ jobs:
|
|||||||
run: postgrest-docs-spellcheck
|
run: postgrest-docs-spellcheck
|
||||||
- name: Run dictcheck
|
- name: Run dictcheck
|
||||||
run: postgrest-docs-dictcheck
|
run: postgrest-docs-dictcheck
|
||||||
|
|
||||||
|
|
||||||
|
linkcheck:
|
||||||
|
name: Linkcheck
|
||||||
|
if: github.base_ref == 'main'
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
- name: Setup Nix Environment
|
||||||
|
uses: ./.github/actions/setup-nix
|
||||||
|
with:
|
||||||
|
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
||||||
|
tools: docs.linkcheck.bin
|
||||||
|
- run: postgrest-docs-linkcheck
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ site
|
|||||||
.#*
|
.#*
|
||||||
*.swp
|
*.swp
|
||||||
result*
|
result*
|
||||||
dist-*
|
dist-newstyle
|
||||||
postgrest.hp
|
postgrest.hp
|
||||||
postgrest.prof
|
postgrest.prof
|
||||||
__pycache__
|
__pycache__
|
||||||
@@ -24,4 +24,3 @@ coverage
|
|||||||
loadtest
|
loadtest
|
||||||
.history
|
.history
|
||||||
.docs-build
|
.docs-build
|
||||||
gen_targets.http
|
|
||||||
|
|||||||
@@ -5,6 +5,6 @@ python:
|
|||||||
install:
|
install:
|
||||||
- requirements: docs/requirements.txt
|
- requirements: docs/requirements.txt
|
||||||
build:
|
build:
|
||||||
os: ubuntu-24.04
|
os: ubuntu-22.04
|
||||||
tools:
|
tools:
|
||||||
python: "3.11"
|
python: "3.11"
|
||||||
|
|||||||
@@ -13,21 +13,21 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="296px" src="static/neon.jpg">
|
<img width="296px" src="static/gnuhost.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="296px" src="static/tembo.png">
|
<img width="296px" src="static/neon.jpg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr></tr>
|
<tr></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="296px" src="static/euronodes.svg">
|
<img width="296px" src="static/code-build.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
@@ -35,6 +35,11 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
|||||||
<img width="296px" src="static/supabase.svg">
|
<img width="296px" src="static/supabase.svg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
|
<img width="296px" src="static/tembo.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -88,11 +93,6 @@ PostgREST ongoing development is only possible thanks to our Sponsors and Backer
|
|||||||
<img width="222px" src="static/oblivious.jpg">
|
<img width="222px" src="static/oblivious.jpg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
|
||||||
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
|
||||||
<img width="222px" src="static/code-build.png">
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -5,126 +5,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
## [13.0.8] - 2025-10-24
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix loading utf-8 config files with `ASCII` locale set by @taimoorzaeem in #4386
|
|
||||||
|
|
||||||
## [13.0.7] - 2025-09-14
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- Improve the `PGRST106` error when the requested schema is invalid by @laurenceisla in #4089
|
|
||||||
+ It now shows the invalid schema in the `message` field.
|
|
||||||
+ The exposed schemas are now listed in the `hint` instead of the `message` field.
|
|
||||||
- Improve error details of `PGRST301` error by @taimoorzaeem in #4051
|
|
||||||
|
|
||||||
## [13.0.6] - 2025-08-30
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix logging the Haskell type instead of the listener error message directly by @laurenceisla in #3588
|
|
||||||
- Fix format of `IPv6` address logged at PostgREST startup by @taimoorzaeem in #4291
|
|
||||||
- Fix empty enum in `preferParams` OpenAPI parameter by @laurenceisla in #4292
|
|
||||||
|
|
||||||
## [13.0.5] - 2025-08-24
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix OpenAPI broken docs link by @taimoorzaeem in #4048
|
|
||||||
- Fix OpenAPI specification incorrectly exposing GET methods for volatile functions by @joelonsql in #4174
|
|
||||||
- Fix empty spread embeddings return unexpected SQL error by @taimoorzaeem in #3887
|
|
||||||
- Fix `/metrics` endpoint not responding with `Content-Type` header by @taimoorzaeem in #4271
|
|
||||||
|
|
||||||
## [13.0.4] - 2025-06-17
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix regression that makes full-text search not work on domain types based on `tsvector` by @laurenceisla in #4135
|
|
||||||
- Fix `jwt-aud` config not failing when set to an invalid URI by @taimoorzaeem in #4132
|
|
||||||
|
|
||||||
## [13.0.3] - 2025-06-16
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix `max-affected` preference not failing with RPC when `handling=strict` by @taimoorzaeem in #4100
|
|
||||||
- Fix a property definition's type in OpenAPI not showing the correct base type of a recursive domain by @laurenceisla in #4136
|
|
||||||
|
|
||||||
## [13.0.2] - 2025-06-02
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix regression that makes `ORDER BY` with nulls-order not work alongside limits by @laurenceisla in #4109
|
|
||||||
|
|
||||||
## [13.0.1] - 2025-06-01
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Fix jwt error returning HTTP status `400` for invalid role by @taimoorzaeem in #3601
|
|
||||||
- Fix `db-extra-search-path` cannot be set to nothing by @taimoorzaeem in #4074
|
|
||||||
+ It can now be disabled by setting it to empty string.
|
|
||||||
+ Schema Cache load error is now logged including `db-schemas` and `db-extra-search-path` config values.
|
|
||||||
|
|
||||||
## [13.0.0] - 2025-05-08
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- #3558, Add the `admin-server-host` config to set the host for the admin server - @develop7
|
|
||||||
- #3607, Log to stderr when the JWT secret is less than 32 characters long - @laurenceisla
|
|
||||||
- #2858, Performance improvements when calling RPCs via GET using indexes in more cases - @wolfgangwalther
|
|
||||||
- #3560, Log resolved host in "Listening on ..." messages - @develop7
|
|
||||||
- #3727, Log maximum pool size - @steve-chavez
|
|
||||||
- #1536, Add string comparison feature for jwt-role-claim-key - @taimoorzaeem
|
|
||||||
- #3747, Allow `not_null` value for the `is` operator - @taimoorzaeem
|
|
||||||
- #2255, Apply `to_tsvector()` explicitly to the full-text search filtered column (excluding `tsvector` types) - @laurenceisla
|
|
||||||
- #1578, Log the main SQL query to stderr at the current `log-level` when `log-query=main-query` - @laurenceisla
|
|
||||||
- #3903, Log connection pool borrows on `log-level=debug` - @taimoorzaeem
|
|
||||||
- #3041, Allow spreading one-to-many and many-to-many embedded resources - @laurenceisla
|
|
||||||
+ The selected columns in the embedded resources are aggregated into arrays
|
|
||||||
+ Aggregates are not supported
|
|
||||||
- #2967, Add `Proxy-Status` header for better error response - @taimoorzaeem
|
|
||||||
- #4016, Add `Content-Length` response header - @laurenceisla
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- #3693, Prevent spread embedding to allow aggregates when they are disabled - @laurenceisla
|
|
||||||
- #3693, A nested spread embedding now correctly groups by the fields of its top parent relationship - @laurenceisla
|
|
||||||
- #3693, Fix spread embedding errors when using the `count()` aggregate without a field - @laurenceisla
|
|
||||||
+ Fixed `"column reference <col> is ambiguous"` error when selecting `?select=...table(col,count())`
|
|
||||||
+ Fixed `"column <json_aggregate>.<alias> does not exist"` error when selecting `?select=...table(aias:count())`
|
|
||||||
- #3727, Clarify "listening" logs - @steve-chavez
|
|
||||||
- #3795, Clarify `Accept: vnd.pgrst.object` error message - @steve-chavez
|
|
||||||
- #3697, #3602, Handle queries on non-existing table gracefully - @taimoorzaeem
|
|
||||||
- #3600, #3926, Improve JWT errors - @taimoorzaeem
|
|
||||||
- #3013, Fix `order=` with POST, PATCH, PUT and DELETE requests - @taimoorzaeem
|
|
||||||
- #3965, Fix filter on unselected columns in a table-valued function - @taimoorzaeem
|
|
||||||
- #4052, Fix schema cache load duplicate objects with different object type but same oid - @taimoorzaeem
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- #2052, Dropped support for PostgreSQL 9.6 - @wolfgangwalther
|
|
||||||
- #2052, Dropped support for PostgreSQL 10 - @wolfgangwalther
|
|
||||||
- #2052, Dropped support for PostgreSQL 11 - @wolfgangwalther
|
|
||||||
- #3508, PostgREST now fails to start when `server-port` and `admin-server-port` config options are the same - @develop7
|
|
||||||
- #3607, PostgREST now fails to start when the JWT secret is less than 32 characters long - @laurenceisla
|
|
||||||
- #3644, Fail schema cache lookup with invalid `db-schemas` or `db-extra-search-path` config - @wolfgangwalther
|
|
||||||
- Previously, this would silently return 200 - OK on the root endpoint, but don't provide any usable endpoints.
|
|
||||||
- Note: This also applies when deleting the `public` schema - both config options default to that.
|
|
||||||
- #3757, Remove support for `Prefer: params=single-object` - @joelonsql
|
|
||||||
+ This preference was deprecated in favor of Functions with an array of JSON objects
|
|
||||||
- #3013, Drop support for Limited updates/deletes
|
|
||||||
+ The feature was complicated and largely unused.
|
|
||||||
- #3956, Drop `/config` endpoint of admin server - @steve-chavez
|
|
||||||
+ The endpoint was at risk of being left unprotected when exposing it.
|
|
||||||
+ The accompanying `admin-server-config-enabled` config was also dropped.
|
|
||||||
- #3697, #3602, Querying non-existent table now returns `PGRST205` error instead of empty json - @taimoorzaeem
|
|
||||||
- #3600, #3926, Improve JWT errors - @taimoorzaeem
|
|
||||||
+ Return `PGRST301` error when `Bearer` in auth header is sent empty
|
|
||||||
+ Diagnostic error messages instead of exposed internals
|
|
||||||
+ Return new `PGRST303` error when jwt claims decoding fails
|
|
||||||
- #3906, Return `PGRST125` and `PGRST126` errors instead of empty json - @taimoorzaeem
|
|
||||||
|
|
||||||
## [12.2.12] - 2025-05-01
|
## [12.2.12] - 2025-05-01
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
@@ -132,11 +12,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
- #3956, Fix exposing admin server `/config` by default - @steve-chavez
|
- #3956, Fix exposing admin server `/config` by default - @steve-chavez
|
||||||
+ The above endpoint is now disabled unless the `admin-server-config-enabled` config is set to `true`
|
+ The above endpoint is now disabled unless the `admin-server-config-enabled` config is set to `true`
|
||||||
|
|
||||||
## [12.2.11] - 2025-04-22
|
## [12.2.11] - 2025-04-21
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- #4030, Fix regression with parameter `charset=utf-8` in mediatype - @taimoorzaeem
|
- #4030, Fix regression with parameter `charset=utf-8` in mediatype - @taimoorzaeem
|
||||||
|
|
||||||
## [12.2.10] - 2025-04-18
|
## [12.2.10] - 2025-04-18
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
This repository follows the same contribution guidelines as the main PostgREST repository contribution guidelines:
|
||||||
|
|
||||||
|
https://github.com/PostgREST/postgrest/blob/main/.github/CONTRIBUTING.md
|
||||||
@@ -22,21 +22,21 @@ API than you are likely to write from scratch.
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="296px" src="static/neon.jpg">
|
<img width="296px" src="static/gnuhost.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="296px" src="static/tembo.png">
|
<img width="296px" src="static/neon.jpg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr></tr>
|
<tr></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="296px" src="static/euronodes.svg">
|
<img width="296px" src="static/code-build.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
@@ -44,6 +44,11 @@ API than you are likely to write from scratch.
|
|||||||
<img width="296px" src="static/supabase.svg">
|
<img width="296px" src="static/supabase.svg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
|
<img width="296px" src="static/tembo.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
index-state: hackage.haskell.org 2025-10-13T04:53:27Z
|
index-state: hackage.haskell.org 2025-02-01T14:59:33Z
|
||||||
|
|||||||
@@ -3,15 +3,7 @@
|
|||||||
, compiler ? "ghc948"
|
, compiler ? "ghc948"
|
||||||
|
|
||||||
, # Commit of the Nixpkgs repository that we want to use.
|
, # Commit of the Nixpkgs repository that we want to use.
|
||||||
# It defaults to reading the inputs from flake.lock, which serves
|
nixpkgsVersion ? import nix/nixpkgs-version.nix
|
||||||
# as a compatibility layer for non-flake builds / default.nix / shell.nix.
|
|
||||||
nixpkgsVersion ? let
|
|
||||||
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit (lock.nodes.nixpkgs.locked) owner repo rev;
|
|
||||||
tarballHash = lock.nodes.nixpkgs.locked.narHash;
|
|
||||||
}
|
|
||||||
|
|
||||||
, # Nix files that describe the Nixpkgs repository. We evaluate the expression
|
, # Nix files that describe the Nixpkgs repository. We evaluate the expression
|
||||||
# using `import` below.
|
# using `import` below.
|
||||||
@@ -27,7 +19,7 @@ let
|
|||||||
"postgrest";
|
"postgrest";
|
||||||
|
|
||||||
# PostgREST source files, filtered based on the rules in the .gitignore files
|
# PostgREST source files, filtered based on the rules in the .gitignore files
|
||||||
# and file extensions. We want to include as little as possible, as the files
|
# and file extensions. We want to include as litte as possible, as the files
|
||||||
# added here will increase the space used in the Nix store and trigger the
|
# added here will increase the space used in the Nix store and trigger the
|
||||||
# build of new Nix derivations when changed.
|
# build of new Nix derivations when changed.
|
||||||
src =
|
src =
|
||||||
@@ -44,6 +36,9 @@ let
|
|||||||
allOverlays.checked-shell-script
|
allOverlays.checked-shell-script
|
||||||
allOverlays.gitignore
|
allOverlays.gitignore
|
||||||
allOverlays.postgresql-libpq
|
allOverlays.postgresql-libpq
|
||||||
|
allOverlays.postgresql-legacy
|
||||||
|
allOverlays.postgresql-future
|
||||||
|
allOverlays.postgis
|
||||||
(allOverlays.haskell-packages { inherit compiler; })
|
(allOverlays.haskell-packages { inherit compiler; })
|
||||||
allOverlays.slocat
|
allOverlays.slocat
|
||||||
];
|
];
|
||||||
@@ -54,28 +49,34 @@ let
|
|||||||
|
|
||||||
postgresqlVersions =
|
postgresqlVersions =
|
||||||
[
|
[
|
||||||
{ name = "postgresql-17"; postgresql = pkgs.postgresql_17.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
|
||||||
{ name = "postgresql-16"; postgresql = pkgs.postgresql_16.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
{ name = "postgresql-16"; postgresql = pkgs.postgresql_16.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
{ name = "postgresql-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
{ name = "postgresql-15"; postgresql = pkgs.postgresql_15.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
{ name = "postgresql-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
{ name = "postgresql-14"; postgresql = pkgs.postgresql_14.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
{ name = "postgresql-13"; postgresql = pkgs.postgresql_13.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
{ name = "postgresql-12"; postgresql = pkgs.postgresql_12.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
{ name = "postgresql-12"; postgresql = pkgs.postgresql_12.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
|
{ name = "postgresql-11"; postgresql = pkgs.postgresql_11.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
|
{ name = "postgresql-10"; postgresql = pkgs.postgresql_10.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
|
{ name = "postgresql-9_6"; postgresql = pkgs.postgresql_9_6.withPackages (p: [ p.postgis p.pg_safeupdate ]); }
|
||||||
];
|
];
|
||||||
|
|
||||||
# Dynamic derivation for PostgREST
|
# Dynamic derivation for PostgREST
|
||||||
postgrest = pkgs.lib.pipe (pkgs.haskell.packages."${compiler}".callCabal2nix name src { }) [
|
postgrest =
|
||||||
# To allow ghc-datasize to be used.
|
pkgs.haskell.packages."${compiler}".callCabal2nix name src { };
|
||||||
lib.disableLibraryProfiling
|
|
||||||
# We are never going to use dynamic haskell libraries anyway. "Dynamic" refers to how
|
|
||||||
# non-haskell deps are linked. All haskell dependencies are always statically linked.
|
|
||||||
lib.disableSharedLibraries
|
|
||||||
];
|
|
||||||
|
|
||||||
staticHaskellPackage = import nix/static.nix { inherit compiler name pkgs src; };
|
staticHaskellPackage = import nix/static.nix { inherit compiler name pkgs src; };
|
||||||
|
|
||||||
# Options passed to cabal in dev tools and tests
|
# Options passed to cabal in dev tools and tests
|
||||||
devCabalOptions =
|
devCabalOptions =
|
||||||
"-f dev --test-show-detail=direct --disable-shared";
|
"-f dev --test-show-detail=direct";
|
||||||
|
|
||||||
|
profiledHaskellPackages =
|
||||||
|
pkgs.haskell.packages."${compiler}".extend (_: super:
|
||||||
|
{
|
||||||
|
mkDerivation =
|
||||||
|
args:
|
||||||
|
super.mkDerivation (args // { enableLibraryProfiling = true; });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
inherit (pkgs.haskell) lib;
|
inherit (pkgs.haskell) lib;
|
||||||
in
|
in
|
||||||
@@ -89,11 +90,12 @@ rec {
|
|||||||
lib.dontCheck postgrest;
|
lib.dontCheck postgrest;
|
||||||
|
|
||||||
# Profiled dynamic executable.
|
# Profiled dynamic executable.
|
||||||
postgrestProfiled = pkgs.lib.pipe postgrestPackage [
|
postgrestProfiled =
|
||||||
lib.enableExecutableProfiling
|
lib.enableExecutableProfiling (
|
||||||
lib.enableLibraryProfiling
|
lib.dontHaddock (
|
||||||
lib.dontHaddock
|
lib.dontCheck (profiledHaskellPackages.callCabal2nix name src { })
|
||||||
];
|
)
|
||||||
|
);
|
||||||
|
|
||||||
inherit (postgrest) env;
|
inherit (postgrest) env;
|
||||||
|
|
||||||
@@ -123,9 +125,17 @@ rec {
|
|||||||
loadtest =
|
loadtest =
|
||||||
pkgs.callPackage nix/tools/loadtest.nix { inherit withTools; };
|
pkgs.callPackage nix/tools/loadtest.nix { inherit withTools; };
|
||||||
|
|
||||||
|
# Script for running memory tests.
|
||||||
|
memory =
|
||||||
|
pkgs.callPackage nix/tools/memory.nix { inherit postgrestProfiled withTools; };
|
||||||
|
|
||||||
|
# Utility for updating the pinned version of Nixpkgs.
|
||||||
|
nixpkgsTools =
|
||||||
|
pkgs.callPackage nix/tools/nixpkgsTools.nix { };
|
||||||
|
|
||||||
# Scripts for publishing new releases.
|
# Scripts for publishing new releases.
|
||||||
release =
|
release =
|
||||||
pkgs.callPackage nix/tools/release.nix { };
|
pkgs.callPackage nix/tools/release { };
|
||||||
|
|
||||||
# Linting and styling tools.
|
# Linting and styling tools.
|
||||||
style =
|
style =
|
||||||
|
|||||||
@@ -19,21 +19,21 @@ write from scratch.
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://gnuhost.eu/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/neon.jpg">
|
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/gnuhost.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
<a href="https://neon.tech/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/tembo.png">
|
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/neon.jpg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr></tr>
|
<tr></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
<a href="https://www.euronodes.com/postgrest" target="_blank">
|
<a href="https://code.build/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
<img width="296px" src="static/euronodes.svg">
|
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/code-build.png">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td align="center" valign="middle">
|
<td align="center" valign="middle">
|
||||||
@@ -41,6 +41,11 @@ write from scratch.
|
|||||||
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.svg">
|
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/supabase.svg">
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
<td align="center" valign="middle">
|
||||||
|
<a href="https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest" target="_blank">
|
||||||
|
<img width="296px" src="https://raw.githubusercontent.com/PostgREST/postgrest/main/static/tembo.png">
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -59,7 +64,7 @@ The image is built from scratch using
|
|||||||
[Nix](https://nixos.org/nixpkgs/manual/#sec-pkgs-dockerTools) instead of a
|
[Nix](https://nixos.org/nixpkgs/manual/#sec-pkgs-dockerTools) instead of a
|
||||||
`Dockerfile`, which yields a highly secure and optimized image. This is also why
|
`Dockerfile`, which yields a highly secure and optimized image. This is also why
|
||||||
no commands are listed in the image history. See the [PostgREST
|
no commands are listed in the image history. See the [PostgREST
|
||||||
repository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for
|
respository](https://github.com/PostgREST/postgrest/tree/main/nix/tools/docker) for
|
||||||
details on the build process and how to inspect the image.
|
details on the build process and how to inspect the image.
|
||||||
|
|
||||||
This does not apply to the arm64 variant, which is based on Ubuntu.
|
This does not apply to the arm64 variant, which is based on Ubuntu.
|
||||||
|
|||||||
@@ -60,10 +60,6 @@ note bottom of ApiRequest
|
|||||||
Parses the URL syntax
|
Parses the URL syntax
|
||||||
end note
|
end note
|
||||||
|
|
||||||
note bottom of Plan
|
|
||||||
Generates internal AST
|
|
||||||
end note
|
|
||||||
|
|
||||||
note bottom of Query
|
note bottom of Query
|
||||||
Generates the SQL
|
Generates the SQL
|
||||||
end note
|
end note
|
||||||
@@ -75,15 +71,12 @@ end note
|
|||||||
url of Admin is [[../references/admin_server.html#admin-server]]
|
url of Admin is [[../references/admin_server.html#admin-server]]
|
||||||
url of API is [[../explanations/schema_isolation.html]]
|
url of API is [[../explanations/schema_isolation.html]]
|
||||||
url of Auth is [[../references/auth.html#authn]]
|
url of Auth is [[../references/auth.html#authn]]
|
||||||
url of ApiRequest is [[../explanations/architecture.html#api-request]]
|
|
||||||
url of Plan is [[../explanations/architecture.html#plan]]
|
|
||||||
url of Query is [[../explanations/architecture.html#query]]
|
|
||||||
url of Authorization is [[../explanations/db_authz.html]]
|
url of Authorization is [[../explanations/db_authz.html]]
|
||||||
url of CLI is [[../references/cli.html#cli]]
|
url of CLI is [[../references/cli.html#cli]]
|
||||||
url of "Connection Pool" is [[../references/connection_pool.html]]
|
url of "Connection Pool" is [[../references/connection_pool.html]]
|
||||||
url of Config is [[../references/configuration.html#configuration]]
|
url of Config is [[../references/configuration.html#configuration]]
|
||||||
url of HTTPADMIN is [[../explanations/architecture.html#http]]
|
url of HTTPADMIN is [[https://aosabook.org/en/posa/warp.html]]
|
||||||
url of HTTPAPI is [[../explanations/architecture.html#http]]
|
url of HTTPAPI is [[https://aosabook.org/en/posa/warp.html]]
|
||||||
url of Listener is [[../references/listener.html#listener]]
|
url of Listener is [[../references/listener.html#listener]]
|
||||||
url of Proxy is [[../explanations/nginx.html]]
|
url of Proxy is [[../explanations/nginx.html]]
|
||||||
url of "Schema Cache" is [[../references/schema_cache.html#schema-cache]]
|
url of "Schema Cache" is [[../references/schema_cache.html#schema-cache]]
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 156 B After Width: | Height: | Size: 468 B |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="391px" preserveAspectRatio="none" style="width:328px;height:391px;" version="1.1" viewBox="0 0 328 391" width="328px" zoomAndPan="magnify"><defs/><g><!--cluster PostgreSQL--><g id="cluster_PostgreSQL"><path d="M6,16 C6,6 158.5,6 158.5,6 C158.5,6 311,6 311,16 L311,293.59 C311,303.59 158.5,303.59 158.5,303.59 C158.5,303.59 6,303.59 6,293.59 L6,16 " fill="none" style="stroke:#E7E7E7;stroke-width:1.0;"/><path d="M6,16 C6,26 158.5,26 158.5,26 C158.5,26 311,26 311,16 " fill="none" style="stroke:#E7E7E7;stroke-width:1.0;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="92.2305" x="112.3848" y="40.9951">PostgreSQL</text></g><!--cluster public--><g id="cluster_public"><polygon fill="none" points="30,74,40,64,153,64,153,146.29,143,156.29,30,156.29,30,74" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="143" x2="153" y1="74" y2="64"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="30" x2="143" y1="74" y2="74"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="143" x2="143" y1="74" y2="156.29"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="47.9063" x="63.5469" y="89.9951">public</text></g><!--cluster API--><g id="cluster_API"><polygon fill="none" points="70,190.29,80,180.29,246,180.29,246,269.59,236,279.59,70,279.59,70,190.29" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="236" x2="246" y1="190.29" y2="180.29"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="70" x2="236" y1="190.29" y2="190.29"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="236" x2="236" y1="190.29" y2="279.59"/><text fill="#FFFFFF" font-family="sans-serif" font-size="20" font-weight="bold" lengthAdjust="spacing" textLength="34.668" x="136.666" y="211.8545">api</text></g><!--entity tables_public--><g id="elem_tables_public"><rect fill="#313139" height="36.2969" rx="2.5" ry="2.5" style="stroke:#E7E7E7;stroke-width:0.5;" width="62.752" x="71.62" y="104"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="42.752" x="81.62" y="126.9951">tables</text></g><!--entity extensions--><g id="elem_extensions"><polygon fill="none" points="169.14,109,179.14,99,294.8695,99,294.8695,135.2969,284.8695,145.2969,169.14,145.2969,169.14,109" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="284.8695" x2="294.8695" y1="109" y2="99"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="169.14" x2="284.8695" y1="109" y2="109"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="284.8695" x2="284.8695" y1="109" y2="145.2969"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="85.7295" x="184.14" y="131.9951">extensions</text></g><!--entity vf_api--><g id="elem_vf_api"><rect fill="#313139" height="36.2969" rx="2.5" ry="2.5" style="stroke:#E7E7E7;stroke-width:0.5;" width="144.6465" x="85.68" y="227.29"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="124.6465" x="95.68" y="250.2851">views + functions</text></g><!--entity PostgREST--><g id="elem_PostgREST"><ellipse cx="158" cy="352.59" fill="#313139" rx="8" ry="8" style="stroke:#E7E7E7;stroke-width:0.5;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="74.6895" x="120.6553" y="382.5851">PostgREST</text></g><!--reverse link tables_public to vf_api--><g id="link_tables_public_vf_api"><path d="M110.03,146.6 C110.03,169.85 110.03,203.55 110.03,226.86 " fill="none" id="tables_public-backto-vf_api" style="stroke:#E7E7E7;stroke-width:1.0;"/><polygon fill="#E7E7E7" points="110.03,140.6,106.03,149.6,110.03,145.6,114.03,149.6,110.03,140.6" style="stroke:#E7E7E7;stroke-width:1.0;"/></g><!--reverse link extensions to vf_api--><g id="link_extensions_vf_api"><path d="M199.73,151.63 C199.73,175.24 199.73,205.18 199.73,226.88 " fill="none" id="extensions-backto-vf_api" style="stroke:#E7E7E7;stroke-width:1.0;"/><polygon fill="#E7E7E7" points="199.73,145.63,195.73,154.63,199.73,150.63,203.73,154.63,199.73,145.63" style="stroke:#E7E7E7;stroke-width:1.0;"/></g><!--link vf_api to PostgREST--><g id="link_vf_api_PostgREST"><path d="M158,269.62 C158,292.9 158,320.34 158,337.81 " fill="none" id="vf_api-PostgREST" style="stroke:#E7E7E7;stroke-width:3.0;"/><polygon fill="#E7E7E7" points="158,263.62,154,272.62,158,268.62,162,272.62,158,263.62" style="stroke:#E7E7E7;stroke-width:3.0;"/><polygon fill="#E7E7E7" points="158,343.81,162,334.81,158,338.81,154,334.81,158,343.81" style="stroke:#E7E7E7;stroke-width:3.0;"/></g><!--SRC=[KypCIyufJKbLqDFJBqxEqCqipjShpSq10000]--></g></svg>
|
<?xml version="1.0" encoding="us-ascii" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" contentStyleType="text/css" height="411px" preserveAspectRatio="none" style="width:315px;height:411px;" version="1.1" viewBox="0 0 315 411" width="315px" zoomAndPan="magnify"><defs/><g><!--cluster PostgreSQL--><g id="cluster_PostgreSQL"><path d="M6,16 C6,6 152,6 152,6 C152,6 298,6 298,16 L298,309.14 C298,319.14 152,319.14 152,319.14 C152,319.14 6,319.14 6,309.14 L6,16 " fill="none" style="stroke:#E7E7E7;stroke-width:1.0;"/><path d="M6,16 C6,26 152,26 152,26 C152,26 298,26 298,16 " fill="none" style="stroke:#E7E7E7;stroke-width:1.0;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="81.375" x="111.3125" y="42.9659">PostgreSQL</text></g><!--cluster public--><g id="cluster_public"><polygon fill="none" points="30,77,40,67,150,67,150,155.07,140,165.07,30,165.07,30,77" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="140" x2="150" y1="77" y2="67"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="30" x2="140" y1="77" y2="77"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="140" x2="140" y1="77" y2="165.07"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="44.625" x="63.6875" y="94.9659">public</text></g><!--cluster API--><g id="cluster_API"><polygon fill="none" points="68,199.07,78,189.07,236,189.07,236,285.14,226,295.14,68,295.14,68,199.07" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="226" x2="236" y1="199.07" y2="189.07"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="68" x2="226" y1="199.07" y2="199.07"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="226" x2="226" y1="199.07" y2="295.14"/><text fill="#FFFFFF" font-family="sans-serif" font-size="20" font-weight="bold" lengthAdjust="spacing" textLength="29.875" x="133.0625" y="223.45">api</text></g><!--entity tables_public--><g id="elem_tables_public"><rect fill="#313139" height="39.0679" rx="2.5" ry="2.5" style="stroke:#E7E7E7;stroke-width:0.5;" width="61" x="69.5" y="110"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="41" x="79.5" y="134.9659">tables</text></g><!--entity extensions--><g id="elem_extensions"><polygon fill="none" points="165.81,115,175.81,105,282.185,105,282.185,144.0679,272.185,154.0679,165.81,154.0679,165.81,115" style="stroke:#E7E7E7;stroke-width:1.0;"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="272.185" x2="282.185" y1="115" y2="105"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="165.81" x2="272.185" y1="115" y2="115"/><line style="stroke:#E7E7E7;stroke-width:1.0;" x1="272.185" x2="272.185" y1="115" y2="154.0679"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" font-weight="bold" lengthAdjust="spacing" textLength="76.375" x="180.81" y="139.9659">extensions</text></g><!--entity vf_api--><g id="elem_vf_api"><rect fill="#313139" height="39.0679" rx="2.5" ry="2.5" style="stroke:#E7E7E7;stroke-width:0.5;" width="136" x="84" y="240.07"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="116" x="94" y="265.0359">views + functions</text></g><!--entity PostgREST--><g id="elem_PostgREST"><ellipse cx="152" cy="371.14" fill="#313139" rx="8" ry="8" style="stroke:#E7E7E7;stroke-width:0.5;"/><text fill="#FFFFFF" font-family="sans-serif" font-size="14" lengthAdjust="spacing" textLength="70" x="117" y="403.1059">PostgREST</text></g><!--reverse link tables_public to vf_api--><g id="link_tables_public_vf_api"><path d="M107.25,155.29 C107.25,179.74 107.25,215.13 107.25,239.67 " fill="none" id="tables_public-backto-vf_api" style="stroke:#E7E7E7;stroke-width:1.0;"/><polygon fill="#E7E7E7" points="107.25,149.29,103.25,158.29,107.25,154.29,111.25,158.29,107.25,149.29" style="stroke:#E7E7E7;stroke-width:1.0;"/></g><!--reverse link extensions to vf_api--><g id="link_extensions_vf_api"><path d="M192.91,160.31 C192.91,185.07 192.91,216.68 192.91,239.61 " fill="none" id="extensions-backto-vf_api" style="stroke:#E7E7E7;stroke-width:1.0;"/><polygon fill="#E7E7E7" points="192.91,154.31,188.91,163.31,192.91,159.31,196.91,163.31,192.91,154.31" style="stroke:#E7E7E7;stroke-width:1.0;"/></g><!--link vf_api to PostgREST--><g id="link_vf_api_PostgREST"><path d="M152,285.55 C152,309.98 152,338.34 152,356.18 " fill="none" id="vf_api-PostgREST" style="stroke:#E7E7E7;stroke-width:3.0;"/><polygon fill="#E7E7E7" points="152,279.55,148,288.55,152,284.55,156,288.55,152,279.55" style="stroke:#E7E7E7;stroke-width:3.0;"/><polygon fill="#E7E7E7" points="152,362.18,156,353.18,152,357.18,148,353.18,152,362.18" style="stroke:#E7E7E7;stroke-width:3.0;"/></g><!--SRC=[KypCIyufJKbLqDFJBqxEqCqipjShpSq10000]--></g></svg>
|
||||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.1 KiB |
@@ -50,7 +50,7 @@ source_suffix = ".rst"
|
|||||||
master_doc = "index"
|
master_doc = "index"
|
||||||
|
|
||||||
# This is overriden by readthedocs with the version tag anyway
|
# This is overriden by readthedocs with the version tag anyway
|
||||||
version = "13.0"
|
version = "12.2"
|
||||||
# To avoid repetition in <title> we set this to an empty string.
|
# To avoid repetition in <title> we set this to an empty string.
|
||||||
release = ""
|
release = ""
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ html_theme = "sphinx_rtd_theme"
|
|||||||
# Theme options are theme-specific and customize the look and feel of a theme
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
# further. For a list of options available for each theme, see the
|
# further. For a list of options available for each theme, see the
|
||||||
# documentation.
|
# documentation.
|
||||||
html_theme_options = {}
|
html_theme_options = {"display_version": False}
|
||||||
|
|
||||||
# Add any paths that contain custom themes here, relative to this directory.
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
# html_theme_path = []
|
# html_theme_path = []
|
||||||
@@ -297,9 +297,6 @@ user_agent = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
linkcheck_ignore = [
|
linkcheck_ignore = [
|
||||||
# 403 only in CI / GitHub Actions
|
|
||||||
r"https://www.patreon.com/postgrest",
|
|
||||||
r"https://blog.frankel.ch/poor-man-api",
|
|
||||||
# Odd SSL error
|
# Odd SSL error
|
||||||
r"https://www.dripdepot.com",
|
r"https://www.dripdepot.com",
|
||||||
r"https://www.euronodes.com",
|
r"https://www.euronodes.com",
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ A request might be rejected at this level if it's invalid. For example when prov
|
|||||||
Plan
|
Plan
|
||||||
----
|
----
|
||||||
|
|
||||||
Using the Schema Cache, `Plan.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Plan.hs>`_ generates an internal AST, filling out-of-band SQL details (like an ``ON CONFLICT (pk)`` clause) required to complete the user request.
|
Using the Schema Cache, `Plan.hs <https://github.com/PostgREST/postgrest/blob/main/src/PostgREST/Plan.hs>`_ fills in out-of-band SQL details (like an ``ON CONFLICT (pk)`` clause) required to complete the user request.
|
||||||
|
|
||||||
A request might be rejected at this level if it's invalid. For example when doing resource embedding on a nonexistent resource.
|
A request might be rejected at this level if it's invalid. For example when doing resource embedding on a nonexistent resource.
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,16 @@ Supported PostgreSQL versions
|
|||||||
=============================
|
=============================
|
||||||
|
|
||||||
=============== =================================
|
=============== =================================
|
||||||
**Supported** PostgreSQL >= 12
|
**Supported** PostgreSQL >= 9.6
|
||||||
=============== =================================
|
=============== =================================
|
||||||
|
|
||||||
PostgREST works with all PostgreSQL versions still `officially supported <https://www.postgresql.org/support/versioning/>`_.
|
PostgREST works with all PostgreSQL versions starting from 9.6.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
Support for PostgreSQL versions 9.6, 10 and 11 is deprecated. From this on version onwards, PostgREST will only support non-end-of-life PostgreSQL versions.
|
||||||
|
|
||||||
|
See https://www.postgresql.org/support/versioning/.
|
||||||
|
|
||||||
Running PostgREST
|
Running PostgREST
|
||||||
=================
|
=================
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ Sponsors
|
|||||||
.. image:: ../static/cybertec.svg
|
.. image:: ../static/cybertec.svg
|
||||||
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
|
:target: https://www.cybertec-postgresql.com/en/?utm_source=postgrest.org&utm_medium=referral&utm_campaign=postgrest
|
||||||
|
|
||||||
|
.. image:: ../static/gnuhost.png
|
||||||
|
:target: https://euronodes.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||||
|
|
||||||
.. container:: img-dark
|
.. container:: img-dark
|
||||||
|
|
||||||
.. image:: ../static/neon-dark.jpg
|
.. image:: ../static/neon-dark.jpg
|
||||||
@@ -48,20 +51,17 @@ Sponsors
|
|||||||
.. image:: ../static/neon.jpg
|
.. image:: ../static/neon.jpg
|
||||||
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
|
:target: https://neon.com/?utm_source=sponsor&utm_campaign=postgrest
|
||||||
|
|
||||||
.. image:: ../static/tembo.png
|
|
||||||
:target: https://www.tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
|
||||||
|
|
||||||
|
|
|
|
||||||
|
|
||||||
.. container:: img-dark
|
.. container:: img-dark
|
||||||
|
|
||||||
.. image:: ../static/euronodes.svg
|
.. image:: ../static/code-build-dark.png
|
||||||
:target: https://www.euronodes.com/postgrest
|
:target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
|
||||||
|
|
||||||
.. container:: img-light
|
.. container:: img-light
|
||||||
|
|
||||||
.. image:: ../static/euronodes.svg
|
.. image:: ../static/code-build.png
|
||||||
:target: https://www.euronodes.com/postgrest
|
:target: https://code.build/?utm_source=sponsor&utm_campaign=postgrest
|
||||||
|
|
||||||
.. container:: img-dark
|
.. container:: img-dark
|
||||||
|
|
||||||
@@ -73,11 +73,14 @@ Sponsors
|
|||||||
.. image:: ../static/supabase.svg
|
.. image:: ../static/supabase.svg
|
||||||
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
:target: https://supabase.com/?utm_source=postgrest%20backers&utm_medium=open%20source%20partner&utm_campaign=postgrest%20backers%20github&utm_term=homepage
|
||||||
|
|
||||||
|
.. image:: ../static/tembo.png
|
||||||
|
:target: https://tembo.io/?utm_source=sponsor&utm_campaign=postgrest
|
||||||
|
|
||||||
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
|
.. The static/empty.png(created with `convert -size 320x95 xc:#fcfcfc empty.png`) is an ugly workaround
|
||||||
to create space and center the logos. It's not easy to layout with restructuredText.
|
to create space and center the logos. It's not easy to layout with restructuredText.
|
||||||
|
|
||||||
.. image:: _static/empty.png
|
.. .. image:: _static/empty.png
|
||||||
:target: #sponsors
|
:target: #sponsors
|
||||||
|
|
||||||
|
|
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
.. _external_auth:
|
.. _external_jwt:
|
||||||
|
|
||||||
External Authentication
|
External JWT Generation
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
JWT from Auth0
|
JWT from Auth0
|
||||||
@@ -3,7 +3,6 @@ api
|
|||||||
API's
|
API's
|
||||||
APIs
|
APIs
|
||||||
APISIX
|
APISIX
|
||||||
AST
|
|
||||||
aud
|
aud
|
||||||
Auth
|
Auth
|
||||||
auth
|
auth
|
||||||
@@ -28,12 +27,10 @@ CSV
|
|||||||
durations
|
durations
|
||||||
DDL
|
DDL
|
||||||
DOM
|
DOM
|
||||||
DSL
|
|
||||||
DevOps
|
DevOps
|
||||||
dockerize
|
dockerize
|
||||||
enum
|
enum
|
||||||
Enums
|
Enums
|
||||||
Entra
|
|
||||||
eq
|
eq
|
||||||
ETH
|
ETH
|
||||||
Ethereum
|
Ethereum
|
||||||
@@ -70,11 +67,9 @@ isdistinct
|
|||||||
JS
|
JS
|
||||||
js
|
js
|
||||||
JSON
|
JSON
|
||||||
JSPath
|
|
||||||
JWK
|
JWK
|
||||||
JWT
|
JWT
|
||||||
jwt
|
jwt
|
||||||
Keycloak
|
|
||||||
Kubernetes
|
Kubernetes
|
||||||
localhost
|
localhost
|
||||||
login
|
login
|
||||||
@@ -98,11 +93,10 @@ npm
|
|||||||
nxl
|
nxl
|
||||||
nxr
|
nxr
|
||||||
OAuth
|
OAuth
|
||||||
ORM
|
|
||||||
Observability
|
Observability
|
||||||
Okta
|
|
||||||
OpenAPI
|
OpenAPI
|
||||||
openapi
|
openapi
|
||||||
|
ORM
|
||||||
ov
|
ov
|
||||||
parametrized
|
parametrized
|
||||||
passphrase
|
passphrase
|
||||||
|
|||||||
@@ -55,6 +55,27 @@ Metrics
|
|||||||
|
|
||||||
Provides :ref:`metrics`.
|
Provides :ref:`metrics`.
|
||||||
|
|
||||||
|
.. _runtime_config:
|
||||||
|
|
||||||
|
Runtime Configuration
|
||||||
|
=====================
|
||||||
|
|
||||||
|
Provides a ``config`` endpoint that returns the runtime :ref:`configuration`. This requires setting :ref:`admin-server-config-enabled`.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl "http://localhost:3001/config"
|
||||||
|
|
||||||
|
.. code-block::
|
||||||
|
|
||||||
|
db-aggregates-enabled = false
|
||||||
|
db-anon-role = "web_anon"
|
||||||
|
db-channel = "pgrst"
|
||||||
|
db-channel-enabled = false
|
||||||
|
...
|
||||||
|
|
||||||
|
.. _runtime_schema_cache:
|
||||||
|
|
||||||
Runtime Schema Cache
|
Runtime Schema Cache
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
|||||||
@@ -225,10 +225,10 @@ If we also want to get the total ``amount`` grouped by the ``order_date`` of the
|
|||||||
Note that the aggregate is done within the embedded resource ``orders``.
|
Note that the aggregate is done within the embedded resource ``orders``.
|
||||||
It is not affected by any of the columns from the top-level relationship ``customers``.
|
It is not affected by any of the columns from the top-level relationship ``customers``.
|
||||||
|
|
||||||
Aggregates in To-One Spreads
|
Using Aggregates in Spreads
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
All the aggregates inside a :ref:`one-to-one or many-to-one spread embedded resource <spread_to_one_embed>` will be hoisted to the top-level relationship.
|
All the aggregates inside a :ref:`spread embedded resource <spread_embed>` will be hoisted to the top-level relationship.
|
||||||
In other words, it will behave as if the aggregate was done in the top-level relationship itself. For example:
|
In other words, it will behave as if the aggregate was done in the top-level relationship itself. For example:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
@@ -251,7 +251,3 @@ This will take the ``max`` and ``min`` subscription date of every customer and g
|
|||||||
"min": "2016-02-11"
|
"min": "2016-02-11"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Aggregates inside to-many spreads are not supported
|
|
||||||
|
|||||||
@@ -131,6 +131,10 @@ For this the ``Content-Type: application/json`` header must be included in the r
|
|||||||
|
|
||||||
If an overloaded function has a single ``json`` or ``jsonb`` unnamed parameter, PostgREST will call this function as a fallback provided that no other overloaded function is found with the parameters sent in the POST request.
|
If an overloaded function has a single ``json`` or ``jsonb`` unnamed parameter, PostgREST will call this function as a fallback provided that no other overloaded function is found with the parameters sent in the POST request.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Sending the JSON request body as a single argument is also possible with :ref:`Prefer: params=single-object <prefer_params>` but this method is **deprecated**.
|
||||||
|
|
||||||
.. _function_single_unnamed:
|
.. _function_single_unnamed:
|
||||||
|
|
||||||
Functions with a single unnamed parameter
|
Functions with a single unnamed parameter
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ For extra customization, the OpenAPI output contains a "description" field for e
|
|||||||
COMMENT ON TABLE monotremes IS
|
COMMENT ON TABLE monotremes IS
|
||||||
'Freakish mammals lay the best eggs for breakfast';
|
'Freakish mammals lay the best eggs for breakfast';
|
||||||
|
|
||||||
COMMENT ON VIEW monotremes_v IS
|
|
||||||
'Only the platypus is publicly visible';
|
|
||||||
|
|
||||||
COMMENT ON COLUMN monotremes.has_venomous_claw IS
|
COMMENT ON COLUMN monotremes.has_venomous_claw IS
|
||||||
'Sometimes breakfast is not worth it';
|
'Sometimes breakfast is not worth it';
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ The following preferences are supported.
|
|||||||
- ``Prefer: missing``. See :ref:`prefer_missing`.
|
- ``Prefer: missing``. See :ref:`prefer_missing`.
|
||||||
- ``Prefer: max-affected``, See :ref:`prefer_max_affected`.
|
- ``Prefer: max-affected``, See :ref:`prefer_max_affected`.
|
||||||
- ``Prefer: tx``. See :ref:`prefer_tx`.
|
- ``Prefer: tx``. See :ref:`prefer_tx`.
|
||||||
|
- ``Prefer: params``. See :ref:`prefer_params`.
|
||||||
|
|
||||||
.. _prefer_handling:
|
.. _prefer_handling:
|
||||||
|
|
||||||
@@ -242,7 +243,7 @@ Will result in:
|
|||||||
Max Affected
|
Max Affected
|
||||||
============
|
============
|
||||||
|
|
||||||
You can set a limit to the amount of resources affected in a request by sending ``max-affected`` preference. This feature works in combination with ``handling=strict`` preference. ``max-affected`` would be ignored with lenient handling. The "affected resources" are the number of rows returned by ``DELETE`` and ``PATCH`` requests.
|
You can set a limit to the amount of resources affected in a request by sending ``max-affected`` preference. This feature works in combination with ``handling=strict`` preference. ``max-affected`` would be ignored with lenient handling. The "affected resources" are the number of rows returned by ``DELETE`` and ``PATCH`` requests. This is also supported through ``RPC`` calls.
|
||||||
|
|
||||||
To illustrate the use of this preference, consider the following scenario where the ``items`` table contains 14 rows.
|
To illustrate the use of this preference, consider the following scenario where the ``items`` table contains 14 rows.
|
||||||
|
|
||||||
@@ -265,34 +266,30 @@ To illustrate the use of this preference, consider the following scenario where
|
|||||||
"hint": null
|
"hint": null
|
||||||
}
|
}
|
||||||
|
|
||||||
With :ref:`RPC <functions>`, the preference is honored completely on the basis of the number of rows returned in the result set of the function. This can be useful for complex mutation queries using `data-modifying statements <https://www.postgresql.org/docs/current/queries-with.html#QUERIES-WITH-MODIFYING>`_. A simple example:
|
.. _prefer_params:
|
||||||
|
|
||||||
|
Single JSON object as Function Parameter
|
||||||
|
----------------------------------------
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Using this preference is **deprecated** in favor of :ref:`function_single_json`.
|
||||||
|
|
||||||
|
:code:`Prefer: params=single-object` allows sending the JSON request body as the single argument of a :ref:`function <functions>`.
|
||||||
|
|
||||||
.. code-block:: postgres
|
.. code-block:: postgres
|
||||||
|
|
||||||
CREATE FUNCTION test.delete_items()
|
CREATE FUNCTION mult_them(param json) RETURNS int AS $$
|
||||||
RETURNS SETOF items AS $$
|
SELECT (param->>'x')::int * (param->>'y')::int
|
||||||
DELETE FROM items WHERE id < 15 RETURNING *;
|
|
||||||
$$ LANGUAGE SQL;
|
$$ LANGUAGE SQL;
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
curl -i "http://localhost:3000/rpc/delete_items" \
|
curl "http://localhost:3000/rpc/mult_them" \
|
||||||
-H "Content-Type: application/json" \
|
-X POST -H "Content-Type: application/json" \
|
||||||
-H "Prefer: handling=strict, max-affected=10"
|
-H "Prefer: params=single-object" \
|
||||||
|
-d '{ "x": 4, "y": 2 }'
|
||||||
.. code-block:: http
|
|
||||||
|
|
||||||
HTTP/1.1 400 Bad Request
|
|
||||||
|
|
||||||
.. code-block:: json
|
.. code-block:: json
|
||||||
|
|
||||||
{
|
8
|
||||||
"code": "PGRST124",
|
|
||||||
"message": "Query result exceeds max-affected preference constraint",
|
|
||||||
"details": "The query affects 14 rows",
|
|
||||||
"hint": null
|
|
||||||
}
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
It is important for functions to return ``SETOF`` or ``TABLE`` when called with ``max-affected`` preference. A violation of this would cause a :ref:`PGRST128 <pgrst128>` error.
|
|
||||||
|
|||||||
@@ -209,25 +209,18 @@ The join table is also detected if the composite key has additional columns.
|
|||||||
One-to-one relationships
|
One-to-one relationships
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
One-to-one relationships are detected in two ways. (We'll use the ``films`` and ``technical_specs`` tables from the :ref:`sample film database <erd_film>` as an example).
|
One-to-one relationships are detected in two ways.
|
||||||
|
|
||||||
- When the foreign key is also a primary key.
|
- When the foreign key is a primary key as specified in the :ref:`sample film database <erd_film>`.
|
||||||
|
- When the foreign key has a unique constraint.
|
||||||
|
|
||||||
.. code-block:: postgres
|
.. code-block:: postgres
|
||||||
|
|
||||||
create table technical_specs(
|
create table technical_specs(
|
||||||
film_id int references films(id) primary key
|
film_id int references films(id) unique,
|
||||||
-- ...
|
runtime time,
|
||||||
);
|
camera text,
|
||||||
|
sound text
|
||||||
- Or when the foreign key has a unique constraint.
|
|
||||||
|
|
||||||
.. code-block:: postgres
|
|
||||||
|
|
||||||
create table technical_specs(
|
|
||||||
id int primary key
|
|
||||||
, film_id int references films(id) unique
|
|
||||||
-- ...
|
|
||||||
);
|
);
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
@@ -934,12 +927,7 @@ Filters can also be applied on nested embedded resources:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
# curl "http://localhost:3000/films?select=*,roles(*,actors(*))&roles.actors.order=last_name&roles.actors.first_name=like.*Tom*"
|
curl "http://localhost:3000/films?select=*,roles(*,actors(*))&roles.actors.order=last_name&roles.actors.first_name=like.*Tom*"
|
||||||
|
|
||||||
curl --get "http://localhost:3000/films" \
|
|
||||||
-d "select=*,roles(*,actors(*))" \
|
|
||||||
-d "roles.actors.order=last_name" \
|
|
||||||
-d "roles.actors.first_name=like.*Tom*"
|
|
||||||
|
|
||||||
The result will show the nested actors named Tom and order them by last name. Aliases can also be used instead of the resource names to filter the nested tables.
|
The result will show the nested actors named Tom and order them by last name. Aliases can also be used instead of the resource names to filter the nested tables.
|
||||||
|
|
||||||
@@ -1150,19 +1138,14 @@ For example, to arrange the films in descending order using the director's last
|
|||||||
Spread embedded resource
|
Spread embedded resource
|
||||||
========================
|
========================
|
||||||
|
|
||||||
You can modify the shape of the embedded resources by using the spread syntax (``...``).
|
On many-to-one and one-to-one relationships, you can "spread" the embedded resource. That is, remove the surrounding JSON object for the embedded resource columns.
|
||||||
|
|
||||||
.. _spread_to_one_embed:
|
|
||||||
|
|
||||||
Spread To-One relationships
|
|
||||||
---------------------------
|
|
||||||
|
|
||||||
Spread on resources forming :ref:`one-to-one <one-to-one>` and :ref:`many-to-one <many-to-one>` relationships, will lift the embedded columns to the top object.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
|
# curl "http://localhost:3000/films?select=title,...directors(director_last_name:last_name)&title=like.*Workers*"
|
||||||
|
|
||||||
curl --get "http://localhost:3000/films" \
|
curl --get "http://localhost:3000/films" \
|
||||||
-d "select=title,...directors(director_first_name:first_name, director_last_name:last_name)" \
|
-d "select=title,...directors(director_last_name:last_name)" \
|
||||||
-d "title=like.*Workers*"
|
-d "title=like.*Workers*"
|
||||||
|
|
||||||
.. code-block:: json
|
.. code-block:: json
|
||||||
@@ -1170,148 +1153,17 @@ Spread on resources forming :ref:`one-to-one <one-to-one>` and :ref:`many-to-one
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"title": "Workers Leaving The Lumière Factory In Lyon",
|
"title": "Workers Leaving The Lumière Factory In Lyon",
|
||||||
"director_first_name": "Louis",
|
|
||||||
"director_last_name": "Lumière"
|
"director_last_name": "Lumière"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
Note that there is no wrapping ``"directors"`` object, unlike regularly embedding :ref:`many-to-one <many-to-one>` relationships. Also note that embedded columns can be aliased normally.
|
Note that there is no ``"directors"`` object. Also the embed columns can be aliased normally.
|
||||||
|
|
||||||
.. _spread_to_many_embed:
|
You can use this to get the columns of a join table in a many-to-many relationship. For instance, to get films and its actors, but including the ``character`` column from the roles table:
|
||||||
|
|
||||||
Spread To-Many relationships
|
|
||||||
----------------------------
|
|
||||||
|
|
||||||
Spread on resources forming :ref:`one-to-many <one-to-many>` and :ref:`many-to-many <many-to-many>` relationships, will convert the embedded columns into correlated arrays.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
curl --get "http://localhost:3000/directors" \
|
# curl "http://localhost:3000/films?select=title,actors:roles(character,...actors(first_name,last_name))&title=like.*Lighthouse*"
|
||||||
-d "select=first_name,...films(film_titles:title,film_years:year)" \
|
|
||||||
-d "first_name=like.Quentin*"
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"first_name": "Quentin",
|
|
||||||
"film_titles": [
|
|
||||||
"Pulp Fiction",
|
|
||||||
"Reservoir Dogs"
|
|
||||||
],
|
|
||||||
"film_years": [
|
|
||||||
1994,
|
|
||||||
1992
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Note that ``films`` is no longer an array of objects, unlike regularly embedding :ref:`one-to-many`. The embedded columns become arrays and they're correlated—in the above result, we can say that "Pulp Fiction" premiered in 1994 and "Reservoir Dogs" in 1992.
|
|
||||||
|
|
||||||
Order in spread to-many
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
In the above example, the order of the values inside the correlated arrays is unspecified, but all the values are guaranteed to be in the same unspecified order.
|
|
||||||
|
|
||||||
You can order the correlated arrays explicitly. For example, to order by the film year:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl --get "http://localhost:3000/directors" \
|
|
||||||
-d "select=first_name,...films(film_titles:title,film_years:year)" \
|
|
||||||
-d "first_name=like.Quentin*" \
|
|
||||||
-d "films.order=year"
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"first_name": "Quentin",
|
|
||||||
"film_titles": [
|
|
||||||
"Reservoir Dogs",
|
|
||||||
"Pulp Fiction"
|
|
||||||
],
|
|
||||||
"film_years": [
|
|
||||||
1992,
|
|
||||||
1994
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
.. warning::
|
|
||||||
|
|
||||||
Aliasing spreaded columns is recommended since JSON allows duplicate keys. Example:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl --get "localhost:3000/projects" \
|
|
||||||
-d "select=id,name,...clients(id,name)"
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
[{"id":1,"name":"Windows 7","id":1,"name":"Microsoft"},
|
|
||||||
{"id":2,"name":"Windows 10","id":1,"name":"Microsoft"},
|
|
||||||
{"id":3,"name":"IOS","id":2,"name":"Apple"},
|
|
||||||
{"id":4,"name":"OSX","id":2,"name":"Apple"},
|
|
||||||
{"id":5,"name":"Orphan","id":null,"name":null}]
|
|
||||||
|
|
||||||
This can be a problem in Javascript objects, since only the last duplicated key will be considered. To solve it do:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl --get "localhost:3000/projects" \
|
|
||||||
-d "select=id,name,...clients(client_id:id,client_name:name)"
|
|
||||||
|
|
||||||
|
|
||||||
Multiple Spreads
|
|
||||||
----------------
|
|
||||||
|
|
||||||
You can use multiple spreads at any level. For example, let's spread ``technical_specs`` and ``roles`` into ``films`` and then spread ``films`` into ``directors``:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl --get "http://localhost:3000/directors" \
|
|
||||||
-d "select=first_name,...films(film_titles:title,film_years:year,...technical_specs(film_runtimes:runtime),...roles(film_characters:character))" \
|
|
||||||
-d "first_name=like.Quentin*" \
|
|
||||||
-d "films.order=year" \
|
|
||||||
-d "films.roles.order=character"
|
|
||||||
|
|
||||||
.. code-block:: json
|
|
||||||
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"first_name": "Quentin",
|
|
||||||
"film_titles": [
|
|
||||||
"Reservoir Dogs",
|
|
||||||
"Pulp Fiction"
|
|
||||||
],
|
|
||||||
"film_years": [
|
|
||||||
1992,
|
|
||||||
1994
|
|
||||||
],
|
|
||||||
"film_runtimes": [
|
|
||||||
"01:39:00",
|
|
||||||
"02:29:00"
|
|
||||||
]
|
|
||||||
"film_characters": [
|
|
||||||
[ "Mr. Pink", "Mr. White" ],
|
|
||||||
[ "Mia Wallace", "Vincent Vega" ]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
Note that:
|
|
||||||
|
|
||||||
- All the ``film_*`` arrays are correlated—"Reservoir Dogs" premiered in 1992, its runtime is 1:39:00 and it has the following characters: ``[ "Mr. Pink", "Mr. White" ]``.
|
|
||||||
- The ``film_*`` arrays are ordered by ``year`` (due to ``films.order=year``).
|
|
||||||
- The bottom level array ``film_characters`` is ordered (due to ``films.roles.order=character``).
|
|
||||||
|
|
||||||
Spread a join table
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
Spread can be used to move the columns of a join table in a :ref:`many-to-many <many-to-many>` to the top object. For instance, to get the ``character`` column of the ``roles`` join table into ``actors``:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl --get "http://localhost:3000/films" \
|
curl --get "http://localhost:3000/films" \
|
||||||
-d "select=title,actors:roles(character,...actors(first_name,last_name))" \
|
-d "select=title,actors:roles(character,...actors(first_name,last_name))" \
|
||||||
@@ -1332,4 +1184,6 @@ Spread can be used to move the columns of a join table in a :ref:`many-to-many <
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
The spread operator ``...`` is borrowed from the Javascript `spread syntax <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax>`_.
|
||||||
|
|||||||
@@ -100,15 +100,17 @@ This returns
|
|||||||
|
|
||||||
{ "id": 1 }
|
{ "id": 1 }
|
||||||
|
|
||||||
|
with a :code:`Content-Type: application/vnd.pgrst.object+json`.
|
||||||
|
|
||||||
When a singular response is requested but no entries are found, the server responds with an error message and 406 Not Acceptable status code rather than the usual empty array and 200 status:
|
When a singular response is requested but no entries are found, the server responds with an error message and 406 Not Acceptable status code rather than the usual empty array and 200 status:
|
||||||
|
|
||||||
.. code-block:: json
|
.. code-block:: json
|
||||||
|
|
||||||
{
|
{
|
||||||
"code": "PGRST116",
|
"message": "JSON object requested, multiple (or no) rows returned",
|
||||||
"message": "Cannot coerce the result to a single JSON object",
|
"details": "Results contain 0 rows, application/vnd.pgrst.object+json requires 1 row",
|
||||||
"details": "The result contains 0 rows",
|
"hint": null,
|
||||||
"hint": null
|
"code": "PGRST505"
|
||||||
}
|
}
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ imatch :code:`~*` ~* operator, see :ref:`pattern_matching`
|
|||||||
in :code:`IN` one of a list of values, e.g. :code:`?a=in.(1,2,3)`
|
in :code:`IN` one of a list of values, e.g. :code:`?a=in.(1,2,3)`
|
||||||
– also supports commas in quoted strings like
|
– also supports commas in quoted strings like
|
||||||
:code:`?a=in.("hi,there","yes,you")`
|
:code:`?a=in.("hi,there","yes,you")`
|
||||||
is :code:`IS` checking for exact equality (null,not_null,true,false,unknown)
|
is :code:`IS` checking for exact equality (null,true,false,unknown)
|
||||||
isdistinct :code:`IS DISTINCT FROM` not equal, treating :code:`NULL` as a comparable value
|
isdistinct :code:`IS DISTINCT FROM` not equal, treating :code:`NULL` as a comparable value
|
||||||
fts :code:`@@` :ref:`fts` using to_tsquery
|
fts :code:`@@` :ref:`fts` using to_tsquery
|
||||||
plfts :code:`@@` :ref:`fts` using plainto_tsquery
|
plfts :code:`@@` :ref:`fts` using plainto_tsquery
|
||||||
@@ -175,47 +175,25 @@ To ensure best performance on larger data sets, an `appropriate index <https://w
|
|||||||
Full-Text Search
|
Full-Text Search
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
The :code:`fts` operator has a number of options to support flexible textual queries, namely the choice of plain vs phrase search and the language used for stemming.
|
The :code:`fts` filter mentioned above has a number of options to support flexible textual queries, namely the choice of plain vs phrase search and the language used for stemming. Suppose that :code:`tsearch` is a table with column :code:`my_tsv`, of type `tsvector <https://www.postgresql.org/docs/current/datatype-textsearch.html>`_. The following examples illustrate the possibilities.
|
||||||
|
|
||||||
The following examples illustrate the possibilities, assuming column :code:`my_tsv` is of type `tsvector <https://www.postgresql.org/docs/current/datatype-textsearch.html>`_.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
curl --get "http://localhost:3000/people" \
|
curl "http://localhost:3000/tsearch?my_tsv=fts(french).amusant"
|
||||||
-d "my_tsv=fts(french).amusant"
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
curl --get "http://localhost:3000/people" \
|
curl "http://localhost:3000/tsearch?my_tsv=plfts.The%20Fat%20Cats"
|
||||||
-d "my_tsv=plfts.The%20Fat%20Cats"
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
curl --get "http://localhost:3000/people" \
|
curl "http://localhost:3000/tsearch?my_tsv=not.phfts(english).The%20Fat%20Cats"
|
||||||
-d "my_tsv=not.phfts(english).The%20Fat%20Cats"
|
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
curl --get "http://localhost:3000/people" \
|
curl "http://localhost:3000/tsearch?my_tsv=not.wfts(french).amusant"
|
||||||
-d "my_tsv=not.wfts(french).amusant"
|
|
||||||
|
|
||||||
.. _fts_to_tsvector:
|
Using `websearch_to_tsquery` requires PostgreSQL of version at least 11.0 and will raise an error in earlier versions of the database.
|
||||||
|
|
||||||
Automatic ``tsvector`` conversion
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
If the filtered column is not of type ``tsvector``, then it will be automatically converted using `to_tsvector() <https://www.postgresql.org/docs/current/functions-textsearch.html#TEXTSEARCH-FUNCTIONS-TABLE>`_.
|
|
||||||
This allows using the ``fts`` operator on ``text`` and ``json`` types out of the box.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl --get "http://localhost:3000/people" \
|
|
||||||
-d "my_text_column=fts(french).amusant"
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl --get "http://localhost:3000/people" \
|
|
||||||
-d "my_json_column=not.phfts(english).The%20Fat%20Cats"
|
|
||||||
|
|
||||||
.. _v_filter:
|
.. _v_filter:
|
||||||
|
|
||||||
@@ -705,6 +683,31 @@ Deletions also support :ref:`prefer_return`, :ref:`resource_embedding` and :ref:
|
|||||||
|
|
||||||
Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`.
|
Beware of accidentally deleting all rows in a table. To learn to prevent that see :ref:`block_fulltable`.
|
||||||
|
|
||||||
|
.. _limited_update_delete:
|
||||||
|
|
||||||
|
Limited Update/Delete
|
||||||
|
=====================
|
||||||
|
|
||||||
|
You can limit the amount of affected rows by :ref:`update` or :ref:`delete` with the ``limit`` query parameter. For this, you must add an explicit ``order`` on a unique column(s).
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl -X PATCH "/users?limit=10&order=id&last_login=lt.2020-01-01" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{ "status": "inactive" }'
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
curl -X DELETE "http://localhost:3000/users?limit=10&order=id&status=eq.inactive"
|
||||||
|
|
||||||
|
If your table has no unique columns, you can use the `ctid <https://www.postgresql.org/docs/current/ddl-system-columns.html>`_ system column.
|
||||||
|
|
||||||
|
Using ``offset`` to target a different subset of rows is also possible.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
There is no native ``UPDATE...LIMIT`` or ``DELETE...LIMIT`` support in PostgreSQL; the generated query simulates that behavior and is based on `this Crunchy Data blog post <https://www.crunchydata.com/blog/simulating-update-or-delete-with-limit-in-postgres-ctes-to-the-rescue>`_.
|
||||||
|
|
||||||
.. raw:: html
|
.. raw:: html
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ If the client included no JWT (or one without a role claim) then PostgREST switc
|
|||||||
JWT Generation
|
JWT Generation
|
||||||
~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~
|
||||||
|
|
||||||
You can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_auth`).
|
You can create a valid JWT either from inside your database (see :ref:`sql_user_management`) or via an external service (see :ref:`external_jwt`).
|
||||||
|
|
||||||
.. _client_auth:
|
.. _client_auth:
|
||||||
|
|
||||||
@@ -156,21 +156,6 @@ You can specify the literal value as we saw earlier, or reference a filename to
|
|||||||
|
|
||||||
jwt-secret = "@rsa.jwk.pub"
|
jwt-secret = "@rsa.jwk.pub"
|
||||||
|
|
||||||
JWK ``kid`` validation
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
PostgREST has built-in validation of the `key ID parameter <https://www.rfc-editor.org/rfc/rfc7517#section-4.5>`_, useful when working with a JWK Set.
|
|
||||||
It goes as follows:
|
|
||||||
|
|
||||||
- If the JWT contains a ``kid`` parameter, then PostgREST will look for the JWK in the :ref:`jwt-secret`.
|
|
||||||
|
|
||||||
+ If no JWK matches the same ``kid`` value (or if they do not have a ``kid``), then the token will be rejected with a :ref:`401 Unauthorized <pgrst301>` error.
|
|
||||||
+ If a JWK matches the ``kid`` value then it will validate the token against that JWK accordingly.
|
|
||||||
|
|
||||||
- If the JWT does not have a ``kid`` parameter, then PostgREST will validate the token against each JWK in the :ref:`jwt-secret`.
|
|
||||||
|
|
||||||
.. _jwt_claims_validation:
|
|
||||||
|
|
||||||
JWT Claims Validation
|
JWT Claims Validation
|
||||||
~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@@ -179,67 +164,10 @@ PostgREST honors the following `JWT claims <https://datatracker.ietf.org/doc/htm
|
|||||||
- ``exp`` Expiration Time
|
- ``exp`` Expiration Time
|
||||||
- ``iat`` Issued At
|
- ``iat`` Issued At
|
||||||
- ``nbf`` Not Before
|
- ``nbf`` Not Before
|
||||||
- ``aud`` :ref:`Audience <jwt_aud_validation>`
|
- ``aud`` Audience, see :ref:`jwt-aud`
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
PostgREST allows for a 30-second clock skew when validating the ``exp``, ``iat`` and ``nbf`` claims.
|
PostgREST allows for a 30-second clock skew when validating the ``exp`` and ``iat`` claims. In other words, it gives an extra 30 seconds before the token is rejected if there is a slight discrepancy in the timestamps.
|
||||||
In other words, it gives an extra 30 seconds before the token is rejected if there is a slight discrepancy in the timestamps.
|
|
||||||
|
|
||||||
.. _jwt_aud_validation:
|
|
||||||
|
|
||||||
JWT ``aud`` Claim Validation
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
|
|
||||||
PostgREST has built-in validation of the `JWT audience claim <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3>`_.
|
|
||||||
It works this way:
|
|
||||||
|
|
||||||
- If :ref:`jwt-aud` is not set (the default), PostgREST identifies with all audiences and allows the JWT for any ``aud`` claim.
|
|
||||||
- If :ref:`jwt-aud` is set to a specific audience, PostgREST will check if this audience is present in the ``aud`` claim:
|
|
||||||
|
|
||||||
+ If the ``aud`` value is a JSON string, it will match it to the :ref:`jwt-aud`.
|
|
||||||
+ If the ``aud`` value is a JSON array of strings, it will search every element for a match.
|
|
||||||
+ If the match fails or if the ``aud`` value is not a string or array of strings, then the token will be rejected with a :ref:`401 Unauthorized <pgrst303>` error.
|
|
||||||
+ If the ``aud`` key **is not present** or if its value is ``null`` or ``[]``, PostgREST will interpret this token as allowed for all audiences and will complete the request.
|
|
||||||
|
|
||||||
.. _jwt_role_extract:
|
|
||||||
|
|
||||||
JWT Role Extraction
|
|
||||||
~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. It's configured by :ref:`jwt-role-claim-key`. This can be used to consume a JWT provided by a third party service like Auth0, Okta, Microsoft Entra or Keycloak.
|
|
||||||
|
|
||||||
The DSL follows the `JSONPath <https://goessner.net/articles/JsonPath/>`_ expression grammar with extended string comparison operators. Supported operators are:
|
|
||||||
|
|
||||||
- ``==`` selects the first array element that exactly matches the right operand
|
|
||||||
- ``!=`` selects the first array element that does not match the right operand
|
|
||||||
- ``^==`` selects the first array element that starts with the right operand
|
|
||||||
- ``==^`` selects the first array element that ends with the right operand
|
|
||||||
- ``*==`` selects the first array element that contains the right operand
|
|
||||||
|
|
||||||
Usage examples:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
# {"postgrest":{"roles": ["other", "author"]}}
|
|
||||||
# the DSL accepts characters that are alphanumerical or one of "_$@" as keys
|
|
||||||
jwt-role-claim-key = ".postgrest.roles[1]"
|
|
||||||
|
|
||||||
# {"https://www.example.com/role": { "key": "author" }}
|
|
||||||
# non-alphanumerical characters can go inside quotes(escaped in the config value)
|
|
||||||
jwt-role-claim-key = ".\"https://www.example.com/role\".key"
|
|
||||||
|
|
||||||
# {"postgrest":{"roles": ["other", "author"]}}
|
|
||||||
# `@` represents the current element in the array
|
|
||||||
# all the these match the string "author"
|
|
||||||
jwt-role-claim-key = ".postgrest.roles[?(@ == \"author\")]"
|
|
||||||
jwt-role-claim-key = ".postgrest.roles[?(@ != \"other\")]"
|
|
||||||
jwt-role-claim-key = ".postgrest.roles[?(@ ^== \"aut\")]"
|
|
||||||
jwt-role-claim-key = ".postgrest.roles[?(@ ==^ \"hor\")]"
|
|
||||||
jwt-role-claim-key = ".postgrest.roles[?(@ *== \"utho\")]"
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
The string comparison operators are implemented as a custom extension to the JSPath and does not strictly follow the `RFC 9535 <https://www.rfc-editor.org/rfc/rfc9535.html>`_.
|
|
||||||
|
|
||||||
JWT Security
|
JWT Security
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|||||||
@@ -146,21 +146,6 @@ To reload the configuration from within the database, you can use the ``NOTIFY``
|
|||||||
List of parameters
|
List of parameters
|
||||||
==================
|
==================
|
||||||
|
|
||||||
.. _admin-server-host:
|
|
||||||
|
|
||||||
admin-server-host
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
=============== =======================
|
|
||||||
**Type** String
|
|
||||||
**Default** `server-host` value
|
|
||||||
**Reloadable** N
|
|
||||||
**Environment** PGRST_ADMIN_SERVER_HOST
|
|
||||||
**In-Database** `n/a`
|
|
||||||
=============== =======================
|
|
||||||
|
|
||||||
Specifies the host for the :ref:`admin_server`. Defaults to :ref:`server-host` value.
|
|
||||||
|
|
||||||
.. _admin-server-port:
|
.. _admin-server-port:
|
||||||
|
|
||||||
admin-server-port
|
admin-server-port
|
||||||
@@ -174,7 +159,31 @@ admin-server-port
|
|||||||
**In-Database** `n/a`
|
**In-Database** `n/a`
|
||||||
=============== =======================
|
=============== =======================
|
||||||
|
|
||||||
Specifies the port for the :ref:`admin_server`. Cannot be equal to :ref:`server-port`.
|
Specifies the port for the :ref:`admin_server`.
|
||||||
|
|
||||||
|
.. _admin-server-config-enabled:
|
||||||
|
|
||||||
|
admin-server-config-enabled
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
.. danger::
|
||||||
|
|
||||||
|
The ``/config`` endpoint contains sensitive information, don't enable this if you're exposing the Admin Server publicly.
|
||||||
|
|
||||||
|
To safely enable this you can use a proxy like :ref:`nginx` to:
|
||||||
|
|
||||||
|
- Ensure ``/config`` are only available to local networks.
|
||||||
|
- Only expose ``/live`` and ``/ready`` to public networks.
|
||||||
|
|
||||||
|
=============== =================================
|
||||||
|
**Type** Boolean
|
||||||
|
**Default** False
|
||||||
|
**Reloadable** N
|
||||||
|
**Environment** PGRST_ADMIN_SERVER_CONFIG_ENABLED
|
||||||
|
**In-Database** `n/a`
|
||||||
|
=============== =================================
|
||||||
|
|
||||||
|
Enables the admin server :ref:`runtime_config` and :ref:`runtime_schema_cache` endpoints.
|
||||||
|
|
||||||
.. _app.settings.*:
|
.. _app.settings.*:
|
||||||
|
|
||||||
@@ -191,10 +200,6 @@ app.settings.*
|
|||||||
|
|
||||||
Arbitrary settings that can be used to pass in secret keys directly as strings, or via OS environment variables. For instance: :code:`app.settings.jwt_secret = "$(MYAPP_JWT_SECRET)"` will take :code:`MYAPP_JWT_SECRET` from the environment and make it available to PostgreSQL functions as :code:`current_setting('app.settings.jwt_secret')`.
|
Arbitrary settings that can be used to pass in secret keys directly as strings, or via OS environment variables. For instance: :code:`app.settings.jwt_secret = "$(MYAPP_JWT_SECRET)"` will take :code:`MYAPP_JWT_SECRET` from the environment and make it available to PostgreSQL functions as :code:`current_setting('app.settings.jwt_secret')`.
|
||||||
|
|
||||||
When using the environment variable `PGRST_APP_SETTINGS_*` form, the remainder of the variable is used as the new name. Case is not important : :code:`PGRST_APP_SETTINGS_MY_ENV_VARIABLE=some_value` can be accessed in postgres as :code:`current_setting('app.settings.my_env_variable')`.
|
|
||||||
|
|
||||||
The :code:`current_setting` function has `an optional boolean second <https://www.postgresql.org/docs/current/functions-admin.html#FUNCTIONS-ADMIN-SET>`_ argument to avoid it from raising an error if the value was not defined. Default values to :code:`app.settings` can then be given by combining this argument with :code:`coalesce` and :code:`nullif` : :code:`coalesce(nullif(current_setting('app.settings.my_custom_variable', true), ''), 'default value')`. The use of :code:`nullif` is necessary because if set in a transaction, the setting is sometimes not "rolled back" to :code:`null`. See also :ref:`this section <guc_req_headers_cookies_claims>` for more information on this behaviour.
|
|
||||||
|
|
||||||
.. _db-aggregates-enabled:
|
.. _db-aggregates-enabled:
|
||||||
|
|
||||||
db-aggregates-enabled
|
db-aggregates-enabled
|
||||||
@@ -315,10 +320,6 @@ db-extra-search-path
|
|||||||
|
|
||||||
Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``.
|
Multiple schemas can be added in a comma-separated string, e.g. ``public, extensions``.
|
||||||
|
|
||||||
.. important::
|
|
||||||
|
|
||||||
We default this config to ``public`` because it is the most common schema used to install PostgreSQL extensions such as :ref:`PostGIS <ww_postgis>`. You can disable this by setting this config to ``""``.
|
|
||||||
|
|
||||||
.. _db-hoisted-tx-settings:
|
.. _db-hoisted-tx-settings:
|
||||||
|
|
||||||
db-hoisted-tx-settings
|
db-hoisted-tx-settings
|
||||||
@@ -603,7 +604,11 @@ jwt-aud
|
|||||||
**In-Database** pgrst.jwt_aud
|
**In-Database** pgrst.jwt_aud
|
||||||
=============== =================================
|
=============== =================================
|
||||||
|
|
||||||
Specifies an audience for the JWT ``aud`` claim. See :ref:`jwt_aud_validation`.
|
Specifies the `JWT audience claim <https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3>`_. If this claim is present in the client provided JWT then you must set this to the same value as in the JWT, otherwise verifying the JWT will fail.
|
||||||
|
|
||||||
|
.. warning::
|
||||||
|
|
||||||
|
Using this setting will only reject tokens with a different audience claim. Tokens **without** audience claim will still be accepted.
|
||||||
|
|
||||||
.. _jwt-role-claim-key:
|
.. _jwt-role-claim-key:
|
||||||
|
|
||||||
@@ -620,7 +625,17 @@ jwt-role-claim-key
|
|||||||
|
|
||||||
*For backwards compatibility, this config parameter is also available without prefix as "role-claim-key".*
|
*For backwards compatibility, this config parameter is also available without prefix as "role-claim-key".*
|
||||||
|
|
||||||
See :ref:`jwt_role_extract` on how to specify key paths and usage examples.
|
A JSPath DSL that specifies the location of the :code:`role` key in the JWT claims. This can be used to consume a JWT provided by a third party service like Auth0, Okta or Keycloak. Usage examples:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
# {"postgrest":{"roles": ["other", "author"]}}
|
||||||
|
# the DSL accepts characters that are alphanumerical or one of "_$@" as keys
|
||||||
|
jwt-role-claim-key = ".postgrest.roles[1]"
|
||||||
|
|
||||||
|
# {"https://www.example.com/role": { "key": "author }}
|
||||||
|
# non-alphanumerical characters can go inside quotes(escaped in the config value)
|
||||||
|
jwt-role-claim-key = ".\"https://www.example.com/role\".key"
|
||||||
|
|
||||||
.. _jwt-secret:
|
.. _jwt-secret:
|
||||||
|
|
||||||
@@ -708,30 +723,6 @@ log-level
|
|||||||
|
|
||||||
Because currently there's no buffering for logging, the levels with minimal logging(``crit/error``) will increase throughput.
|
Because currently there's no buffering for logging, the levels with minimal logging(``crit/error``) will increase throughput.
|
||||||
|
|
||||||
.. _log-query:
|
|
||||||
|
|
||||||
log-query
|
|
||||||
---------
|
|
||||||
|
|
||||||
=============== =================================
|
|
||||||
**Type** String
|
|
||||||
**Default** "disabled"
|
|
||||||
**Reloadable** Y
|
|
||||||
**Environment** PGRST_LOG_QUERY
|
|
||||||
**In-Database** `n/a`
|
|
||||||
=============== =================================
|
|
||||||
|
|
||||||
Logs the SQL query for the corresponding request at the current :ref:`log-level`.
|
|
||||||
See :ref:`sql_query_logs`.
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
# Logs the main SQL query
|
|
||||||
log-query = "main-query"
|
|
||||||
|
|
||||||
# Disables logging the SQL query
|
|
||||||
log-query = "disabled"
|
|
||||||
|
|
||||||
.. _openapi-mode:
|
.. _openapi-mode:
|
||||||
|
|
||||||
openapi-mode
|
openapi-mode
|
||||||
@@ -845,12 +836,6 @@ server-host
|
|||||||
* :code:`*6` - any IPv4 or IPv6 hostname, IPv6 preferred
|
* :code:`*6` - any IPv4 or IPv6 hostname, IPv6 preferred
|
||||||
* :code:`!6` - any IPv6 hostname
|
* :code:`!6` - any IPv6 hostname
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
.. code:: bash
|
|
||||||
|
|
||||||
server-host = "127.0.0.1"
|
|
||||||
|
|
||||||
.. _server-port:
|
.. _server-port:
|
||||||
|
|
||||||
server-port
|
server-port
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ Errors
|
|||||||
|
|
||||||
PostgREST error messages follow the PostgreSQL error structure. It includes ``MESSAGE``, ``DETAIL``, ``HINT``, ``ERRCODE`` and will add an HTTP status code to the response.
|
PostgREST error messages follow the PostgreSQL error structure. It includes ``MESSAGE``, ``DETAIL``, ``HINT``, ``ERRCODE`` and will add an HTTP status code to the response.
|
||||||
|
|
||||||
.. _postgresql_errors:
|
|
||||||
|
|
||||||
Errors from PostgreSQL
|
Errors from PostgreSQL
|
||||||
======================
|
======================
|
||||||
|
|
||||||
@@ -207,6 +205,14 @@ Related to the HTTP request elements.
|
|||||||
| | | specified in the ``select`` part of the query string. |
|
| | | specified in the ``select`` part of the query string. |
|
||||||
| PGRST108 | | See :ref:`embed_filters`. |
|
| PGRST108 | | See :ref:`embed_filters`. |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
|
| .. _pgrst109: | 400 | Restricting a Deletion or an Update using limits must |
|
||||||
|
| | | include the ordering of a unique column. |
|
||||||
|
| PGRST109 | | See :ref:`limited_update_delete`. |
|
||||||
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
|
| .. _pgrst110: | 400 | When restricting a Deletion or an Update using limits |
|
||||||
|
| | | modifies more rows than the maximum specified in the limit. |
|
||||||
|
| PGRST110 | | See :ref:`limited_update_delete`. |
|
||||||
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
| .. _pgrst111: | 500 | An invalid ``response.headers`` was set. |
|
| .. _pgrst111: | 500 | An invalid ``response.headers`` was set. |
|
||||||
| | | See :ref:`guc_resp_hdrs`. |
|
| | | See :ref:`guc_resp_hdrs`. |
|
||||||
| PGRST111 | | |
|
| PGRST111 | | |
|
||||||
@@ -235,6 +241,10 @@ Related to the HTTP request elements.
|
|||||||
| | | there is no many-to-one or one-to-one relationship between |
|
| | | there is no many-to-one or one-to-one relationship between |
|
||||||
| PGRST118 | | them. |
|
| PGRST118 | | them. |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
|
| .. _pgrst119: | 400 | Could not use the spread operator on the related table |
|
||||||
|
| | | because there is no many-to-one or one-to-one relationship |
|
||||||
|
| PGRST119 | | between them. |
|
||||||
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
| .. _pgrst120: | 400 | An embedded resource can only be filtered using the |
|
| .. _pgrst120: | 400 | An embedded resource can only be filtered using the |
|
||||||
| | | ``is.null`` or ``not.is.null`` :ref:`operators <operators>`.|
|
| | | ``is.null`` or ``not.is.null`` :ref:`operators <operators>`.|
|
||||||
| PGRST120 | | |
|
| PGRST120 | | |
|
||||||
@@ -247,31 +257,6 @@ Related to the HTTP request elements.
|
|||||||
| | | ``Prefer: handling=strict``. See :ref:`prefer_handling`. |
|
| | | ``Prefer: handling=strict``. See :ref:`prefer_handling`. |
|
||||||
| PGRST122 | | |
|
| PGRST122 | | |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
| .. _pgrst123: | 400 | Aggregate functions are disabled. |
|
|
||||||
| | | See :ref:`db-aggregates-enabled`. |
|
|
||||||
| PGRST123 | | |
|
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
|
||||||
| .. _pgrst124: | 400 | ``max-affected`` preference is violated. |
|
|
||||||
| | | See :ref:`prefer_max_affected`. |
|
|
||||||
| PGRST124 | | |
|
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
|
||||||
| .. _pgrst125: | 404 | Invalid path is specified in request URL. |
|
|
||||||
| | | |
|
|
||||||
| PGRST125 | | |
|
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
|
||||||
| .. _pgrst126: | 404 | Open API config is disabled but API root path is |
|
|
||||||
| | | accessed. See :ref:`openapi-mode`. |
|
|
||||||
| PGRST126 | | |
|
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
|
||||||
| .. _pgrst127: | 400 | The feature specified in the ``details`` field is not |
|
|
||||||
| | | implemented. |
|
|
||||||
| PGRST127 | | |
|
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
|
||||||
| .. _pgrst128: | 400 | ``max-affected`` preference is violated with ``RPC`` call. |
|
|
||||||
| | | See :ref:`prefer_max_affected`. |
|
|
||||||
| PGRST128 | | |
|
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
|
||||||
|
|
||||||
|
|
||||||
.. _pgrst2**:
|
.. _pgrst2**:
|
||||||
|
|
||||||
@@ -305,10 +290,6 @@ Related to a :ref:`schema_cache`. Most of the time, these errors are solved by :
|
|||||||
| | | in the ``columns`` query parameter is not found. |
|
| | | in the ``columns`` query parameter is not found. |
|
||||||
| PGRST204 | | |
|
| PGRST204 | | |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
| .. _pgrst205: | 404 | Caused when the :ref:`table specified <tables_views>` in |
|
|
||||||
| | | the URI is not found. |
|
|
||||||
| PGRST205 | | |
|
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
|
||||||
|
|
||||||
.. _pgrst3**:
|
.. _pgrst3**:
|
||||||
|
|
||||||
@@ -324,18 +305,14 @@ Related to the authentication process using JWT. You can follow the :ref:`tut1`
|
|||||||
| | | configuration. |
|
| | | configuration. |
|
||||||
| PGRST300 | | |
|
| PGRST300 | | |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
| .. _pgrst301: | 401 | Provided JWT couldn't be decoded or it is invalid. |
|
| .. _pgrst301: | 401 | Any error related to the verification of the JWT, |
|
||||||
| | | |
|
| | | which means that the JWT provided is invalid in some way. |
|
||||||
| PGRST301 | | |
|
| PGRST301 | | |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
| .. _pgrst302: | 401 | Attempted to do a request without |
|
| .. _pgrst302: | 401 | Attempted to do a request without |
|
||||||
| | | :ref:`authentication <client_auth>` when the anonymous role |
|
| | | :ref:`authentication <client_auth>` when the anonymous role |
|
||||||
| PGRST302 | | is disabled by not setting it in :ref:`db-anon-role`. |
|
| PGRST302 | | is disabled by not setting it in :ref:`db-anon-role`. |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
| .. _pgrst303: | 401 | :ref:`JWT claims validation <jwt_claims_validation>` |
|
|
||||||
| | | or parsing failed. |
|
|
||||||
| PGRST303 | | |
|
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
|
||||||
|
|
||||||
.. The Internal Errors Group X** is always at the end
|
.. The Internal Errors Group X** is always at the end
|
||||||
|
|
||||||
@@ -354,8 +331,6 @@ Internal errors. If you encounter any of these, you may have stumbled on a Postg
|
|||||||
| PGRSTX00 | | |
|
| PGRSTX00 | | |
|
||||||
+---------------+-------------+-------------------------------------------------------------+
|
+---------------+-------------+-------------------------------------------------------------+
|
||||||
|
|
||||||
.. _custom_errors:
|
|
||||||
|
|
||||||
Custom Errors
|
Custom Errors
|
||||||
=============
|
=============
|
||||||
|
|
||||||
@@ -452,24 +427,3 @@ For non standard HTTP status, you can optionally add ``status_text`` to describe
|
|||||||
detail = '{"status":419,"status_text":"Page Expired","headers":{"X-Powered-By":"Nerd Rage"}}';
|
detail = '{"status":419,"status_text":"Page Expired","headers":{"X-Powered-By":"Nerd Rage"}}';
|
||||||
|
|
||||||
If PostgREST can't parse the JSON objects ``message`` and ``detail``, it will throw a ``PGRST121`` error. See :ref:`Errors from PostgREST<pgrst1**>`.
|
If PostgREST can't parse the JSON objects ``message`` and ``detail``, it will throw a ``PGRST121`` error. See :ref:`Errors from PostgREST<pgrst1**>`.
|
||||||
|
|
||||||
.. _proxy-status_header:
|
|
||||||
|
|
||||||
Proxy-Status Header
|
|
||||||
===================
|
|
||||||
|
|
||||||
For error cases, the standard `Proxy-Status <https://www.rfc-editor.org/rfc/rfc9209.html#name-the-proxy-status-http-field>`_ header is returned with the error code. The error code comes from either :ref:`PostgREST <pgrst_errors>`, :ref:`PostgreSQL <postgresql_errors>` or :ref:`Custom <custom_errors>` errors. This is useful when doing ``HEAD`` requests where the HTTP status is not descriptive enough.
|
|
||||||
|
|
||||||
For example, doing a request on a table with high count (say 30_000_000), we get:
|
|
||||||
|
|
||||||
.. code-block:: http
|
|
||||||
|
|
||||||
HEAD /table HTTP/1.1
|
|
||||||
Prefer: count=exact
|
|
||||||
|
|
||||||
.. code-block:: http
|
|
||||||
|
|
||||||
HTTP/1.1 500 Internal Server Error
|
|
||||||
Proxy-Status: PostgREST; error=57014
|
|
||||||
|
|
||||||
The PostgreSQL error code ``57014`` (`ref <https://www.postgresql.org/docs/current/errcodes-appendix.html>`_) reveals that the error is due to a short ``statement_timeout`` value.
|
|
||||||
|
|||||||
@@ -15,14 +15,12 @@ Observability allows measuring a system's current state based on the data it gen
|
|||||||
Logs
|
Logs
|
||||||
====
|
====
|
||||||
|
|
||||||
PostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, the HTTP response status and the response body size in bytes if available.
|
PostgREST logs basic request information to ``stdout``, including the authenticated user if available, the requesting IP address and user agent, the URL requested, and HTTP response status.
|
||||||
|
|
||||||
With :ref:`log-level` set to ``info``, we get:
|
|
||||||
|
|
||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 56 "" "curl/7.64.0"
|
127.0.0.1 - user [26/Jul/2021:01:56:38 -0500] "GET /clients HTTP/1.1" 200 - "" "curl/7.64.0"
|
||||||
127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 162 "" "curl/7.64.0"
|
127.0.0.1 - anonymous [26/Jul/2021:01:56:48 -0500] "GET /unexistent HTTP/1.1" 404 - "" "curl/7.64.0"
|
||||||
|
|
||||||
For diagnostic information about the server itself, PostgREST logs to ``stderr``:
|
For diagnostic information about the server itself, PostgREST logs to ``stderr``:
|
||||||
|
|
||||||
@@ -33,52 +31,22 @@ For diagnostic information about the server itself, PostgREST logs to ``stderr``
|
|||||||
.. code::
|
.. code::
|
||||||
|
|
||||||
06/May/2024:08:16:11 -0500: Starting PostgREST 12.1...
|
06/May/2024:08:16:11 -0500: Starting PostgREST 12.1...
|
||||||
|
06/May/2024:08:16:11 -0500: Attempting to connect to the database...
|
||||||
06/May/2024:08:16:11 -0500: Successfully connected to PostgreSQL 14.10 (Ubuntu 14.10-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit
|
06/May/2024:08:16:11 -0500: Successfully connected to PostgreSQL 14.10 (Ubuntu 14.10-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit
|
||||||
06/May/2024:08:16:11 -0500: Connection Pool initialized with a maximum size of 10 connections
|
06/May/2024:08:16:11 -0500: Listening on port 3000
|
||||||
06/May/2024:08:16:11 -0500: API server listening on port 3000
|
06/May/2024:08:16:11 -0500: Listening for notifications on the "pgrst" channel
|
||||||
06/May/2024:08:16:11 -0500: Listening for database notifications on the "pgrst" channel
|
|
||||||
06/May/2024:08:16:11 -0500: Config reloaded
|
06/May/2024:08:16:11 -0500: Config reloaded
|
||||||
06/May/2024:08:16:11 -0500: Schema cache queried in 3.8 milliseconds
|
06/May/2024:08:16:11 -0500: Schema cache queried in 3.8 milliseconds
|
||||||
06/May/2024:08:16:11 -0500: Schema cache loaded 15 Relations, 8 Relationships, 8 Functions, 0 Domain Representations, 4 Media Type Handlers
|
06/May/2024:08:16:11 -0500: Schema cache loaded 15 Relations, 8 Relationships, 8 Functions, 0 Domain Representations, 4 Media Type Handlers
|
||||||
06/May/2024:14:11:27 -0500: Received a config reload message on the "pgrst" channel
|
06/May/2024:14:11:27 -0500: Received a config reload message on the "pgrst" channel
|
||||||
06/May/2024:14:11:27 -0500: Config reloaded
|
06/May/2024:14:11:27 -0500: Config reloaded
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Logs are based on the ``log-level`` setting. See :ref:`log-level`.
|
|
||||||
|
|
||||||
.. _sql_query_logs:
|
|
||||||
|
|
||||||
SQL Query Logs
|
|
||||||
--------------
|
|
||||||
|
|
||||||
To log the :ref:`main SQL query <main_query>` executed for a request, set the :ref:`log-query` to ``main-query``.
|
|
||||||
It will be logged based on the current :ref:`log-level` setting.
|
|
||||||
For example, with this configuration:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
log-level = "warn"
|
|
||||||
log-query = "main-query"
|
|
||||||
|
|
||||||
The SQL queries will only be logged on ``400`` HTTP errors and up.
|
|
||||||
So, if the user requests a resource without sufficient privileges:
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl "localhost:3000/protected_table"
|
|
||||||
|
|
||||||
This will be logged by PostgREST:
|
|
||||||
|
|
||||||
.. code::
|
|
||||||
|
|
||||||
17/Feb/2025:17:28:15 -0500: WITH pgrst_source AS ( SELECT "public"."protected_table".* FROM "public"."protected_table" ) SELECT null::bigint AS total_result_set, pg_catalog.count(_postgrest_t) AS page_total, coalesce(json_agg(_postgrest_t), '[]') AS body, nullif(current_setting('response.headers', true), '') AS response_headers, nullif(current_setting('response.status', true), '') AS response_status, '' AS response_inserted FROM ( SELECT * FROM pgrst_source ) _postgrest_t
|
|
||||||
127.0.0.1 - web_anon [17/Feb/2025:17:28:15 -0500] "GET /protected_table HTTP/1.1" 401 99 "" "curl/8.7.1"
|
|
||||||
|
|
||||||
Database Logs
|
Database Logs
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
Additionally, to find all the SQL operations, you can watch the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below.
|
Currently PostgREST doesn't log the SQL commands executed against the underlying database.
|
||||||
|
|
||||||
|
To find the SQL operations, you can watch the database logs. By default PostgreSQL does not keep these logs, so you'll need to make the configuration changes below.
|
||||||
|
|
||||||
Find :code:`postgresql.conf` inside your PostgreSQL data directory (to find that, issue the command :code:`show data_directory;`). Either find the settings scattered throughout the file and change them to the following values, or append this block of code to the end of the configuration file.
|
Find :code:`postgresql.conf` inside your PostgreSQL data directory (to find that, issue the command :code:`show data_directory;`). Either find the settings scattered throughout the file and change them to the following values, or append this block of code to the end of the configuration file.
|
||||||
|
|
||||||
@@ -128,11 +96,6 @@ The ``metrics`` endpoint on the :ref:`admin_server` endpoint provides metrics in
|
|||||||
|
|
||||||
curl "http://localhost:3001/metrics"
|
curl "http://localhost:3001/metrics"
|
||||||
|
|
||||||
.. code-block:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Type: text/plain; charset=utf-8
|
|
||||||
|
|
||||||
# HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load
|
# HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load
|
||||||
# TYPE pgrst_schema_cache_query_time_seconds gauge
|
# TYPE pgrst_schema_cache_query_time_seconds gauge
|
||||||
pgrst_schema_cache_query_time_seconds 1.5937927e-2
|
pgrst_schema_cache_query_time_seconds 1.5937927e-2
|
||||||
@@ -241,11 +204,6 @@ You can enable tracing HTTP requests by setting :ref:`server-trace-header`. Spec
|
|||||||
HTTP/1.1 200 OK
|
HTTP/1.1 200 OK
|
||||||
X-Request-Id: 123
|
X-Request-Id: 123
|
||||||
|
|
||||||
Proxy-Status Header
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
See :ref:`proxy-status_header`.
|
|
||||||
|
|
||||||
.. _server-timing_header:
|
.. _server-timing_header:
|
||||||
|
|
||||||
Server-Timing Header
|
Server-Timing Header
|
||||||
@@ -275,27 +233,6 @@ This header communicates metrics of the different phases in the request-response
|
|||||||
|
|
||||||
We're working on lowering the duration of the ``parse`` and ``plan`` stages on https://github.com/PostgREST/postgrest/issues/2816.
|
We're working on lowering the duration of the ``parse`` and ``plan`` stages on https://github.com/PostgREST/postgrest/issues/2816.
|
||||||
|
|
||||||
.. _content-length_header:
|
|
||||||
|
|
||||||
Content-Length Header
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
You can verify the response body size in bytes in the `Content-Length header <https://httpwg.org/specs/rfc9110.html#field.content-length>`_.
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
curl -i 'localhost:3000/users'
|
|
||||||
|
|
||||||
.. code-block:: http
|
|
||||||
|
|
||||||
HTTP/1.1 200 OK
|
|
||||||
Content-Length: 104
|
|
||||||
|
|
||||||
Note that this header won't be returned on ``HEAD`` requests for optimization purposes (see :ref:`head_req`).
|
|
||||||
This is in line with `RFC 9110 <https://httpwg.org/specs/rfc9110.html#field.content-length>`_.
|
|
||||||
|
|
||||||
The body size is also present in the :ref:`PostgREST logs <pgrst_logging>`.
|
|
||||||
|
|
||||||
.. _explain_plan:
|
.. _explain_plan:
|
||||||
|
|
||||||
Execution plan
|
Execution plan
|
||||||
|
|||||||
@@ -284,10 +284,11 @@ However, starting from PostgreSQL 15, you can grant privileges for these setting
|
|||||||
|
|
||||||
GRANT SET ON PARAMETER <setting> TO <authenticator>;
|
GRANT SET ON PARAMETER <setting> TO <authenticator>;
|
||||||
|
|
||||||
Hoisted Function Settings
|
Function Settings
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
PostgREST can "hoist" function settings to transaction-scoped settings. This allows functions settings to override the impersonated and connection role settings.
|
In addition to :ref:`impersonated_settings`, PostgREST will also apply function settings as transaction-scoped settings. This allows functions settings to override
|
||||||
|
the impersonated and connection role settings.
|
||||||
|
|
||||||
.. code-block:: postgres
|
.. code-block:: postgres
|
||||||
|
|
||||||
@@ -302,7 +303,7 @@ When calling the above function (see :ref:`functions`), the statement timeout wi
|
|||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
Only the settings in :ref:`db-hoisted-tx-settings` will be hoisted.
|
Only the transactions that are hoisted by config :ref:`db-hoisted-tx-settings` will be applied.
|
||||||
|
|
||||||
.. _main_query:
|
.. _main_query:
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
# This file is auto-generated by postgrest-nixpkgs-upgrade
|
docutils==0.20.1
|
||||||
sphinx==7.4.7
|
sphinx-copybutton
|
||||||
sphinx-copybutton==0.5.2
|
sphinx-intl
|
||||||
sphinx-rtd-dark-mode==1.3.0
|
sphinx-rtd-theme>=0.5.1
|
||||||
sphinx-rtd-theme==3.0.2
|
sphinx-rtd-dark-mode>=1.3.0
|
||||||
sphinx-tabs==3.4.7
|
sphinx-tabs>=3.2.0
|
||||||
|
sphinx>=5.0.2
|
||||||
sphinxext-opengraph==0.9.1
|
sphinxext-opengraph==0.9.1
|
||||||
|
urllib3==2.2.3
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
pacman -S postgrest
|
pacman -S postgrest
|
||||||
|
|
||||||
.. tab:: Nix via nixpkgs
|
.. tab:: Nix
|
||||||
|
|
||||||
You can install PostgREST from nixpkgs.
|
You can install PostgREST from nixpkgs.
|
||||||
|
|
||||||
@@ -36,17 +36,6 @@
|
|||||||
|
|
||||||
nix-env -i postgrest
|
nix-env -i postgrest
|
||||||
|
|
||||||
.. tab:: Nix via flake
|
|
||||||
|
|
||||||
You can install PostgREST via flake.
|
|
||||||
|
|
||||||
.. code:: nix
|
|
||||||
|
|
||||||
{
|
|
||||||
inputs.postgrest.url = "github:postgrest/postgrest";
|
|
||||||
# ...
|
|
||||||
}
|
|
||||||
|
|
||||||
.. group-tab:: Windows
|
.. group-tab:: Windows
|
||||||
|
|
||||||
You can install PostgREST using `Chocolatey <https://community.chocolatey.org/packages/postgrest>`_ or `Scoop <https://github.com/ScoopInstaller/Scoop>`_.
|
You can install PostgREST using `Chocolatey <https://community.chocolatey.org/packages/postgrest>`_ or `Scoop <https://github.com/ScoopInstaller/Scoop>`_.
|
||||||
|
|||||||
@@ -213,8 +213,12 @@ You should see something similar to:
|
|||||||
.. code-block:: text
|
.. code-block:: text
|
||||||
|
|
||||||
Starting PostgREST 12.0.2...
|
Starting PostgREST 12.0.2...
|
||||||
Successfully connected to PostgreSQL 14.10 (Ubuntu 14.10-0ubuntu0.22.04.1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, 64-bit
|
Attempting to connect to the database...
|
||||||
API server listening on port 3000
|
Connection successful
|
||||||
|
Listening on port 3000
|
||||||
|
Config reloaded
|
||||||
|
Listening for notifications on the pgrst channel
|
||||||
|
Schema cache loaded
|
||||||
|
|
||||||
It's now ready to serve web requests. There are many nice graphical API exploration tools you can use, but for this tutorial we'll use :code:`curl` because it's likely to be installed on your system already. Open a new terminal (leaving the one open that PostgREST is running inside). Try doing an HTTP request for the todos.
|
It's now ready to serve web requests. There are many nice graphical API exploration tools you can use, but for this tutorial we'll use :code:`curl` because it's likely to be installed on your system already. Open a new terminal (leaving the one open that PostgREST is running inside). Try doing an HTTP request for the todos.
|
||||||
|
|
||||||
|
|||||||
@@ -52,31 +52,17 @@ Check that the :code:`tutorial.conf` (created in the previous tutorial) has the
|
|||||||
|
|
||||||
If the PostgREST server is still running from the previous tutorial, restart it to load the updated configuration file.
|
If the PostgREST server is still running from the previous tutorial, restart it to load the updated configuration file.
|
||||||
|
|
||||||
.. _tut1_step3:
|
|
||||||
|
|
||||||
Step 3. Sign a Token
|
Step 3. Sign a Token
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
Ordinarily your own code in the database or in another server will create and sign authentication tokens, but for this tutorial we will make one "by hand" using ``bash`` and ``openssl``.
|
Ordinarily your own code in the database or in another server will create and sign authentication tokens, but for this tutorial we will make one "by hand." Go to `jwt.io <https://jwt.io/#debugger-io>`_ and fill in the fields like this:
|
||||||
|
|
||||||
.. code:: bash
|
.. figure:: ../_static/tuts/tut1-jwt-io.png
|
||||||
|
:alt: jwt.io interface
|
||||||
|
|
||||||
#!/bin/bash
|
How to create a token at https://jwt.io
|
||||||
set -e
|
|
||||||
|
|
||||||
JWT_SECRET='test_secret_that_is_at_least_32_characters_long'
|
**Remember to fill in the secret you generated rather than the word "secret".** After you have filled in the secret and payload, the encoded data on the left will update. Copy the encoded token.
|
||||||
|
|
||||||
_base64 () { openssl base64 -e -A | tr '+/' '-_' | tr -d '='; }
|
|
||||||
|
|
||||||
header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | _base64)
|
|
||||||
|
|
||||||
payload=$(echo -n "{\"role\":\"todo_user\"}" | _base64)
|
|
||||||
|
|
||||||
signature=$(echo -n "$header.$payload" | openssl dgst -sha256 -hmac "$JWT_SECRET" -binary | _base64)
|
|
||||||
|
|
||||||
echo -n "$header.$payload.$signature"
|
|
||||||
|
|
||||||
**Remember to fill in the secret you generated rather than keeping the "test_secret_that_is_at_least_32_characters_long".** After you have filled in the secret and payload, the encoded data on the left will update. Copy the encoded token.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
@@ -159,22 +145,14 @@ To observe expiration in action, we'll add an :code:`exp` claim of five minutes
|
|||||||
|
|
||||||
select extract(epoch from now() + '5 minutes'::interval) :: integer;
|
select extract(epoch from now() + '5 minutes'::interval) :: integer;
|
||||||
|
|
||||||
Or in ``bash``:
|
Go back to jwt.io and change the payload to
|
||||||
|
|
||||||
|
.. code-block:: json
|
||||||
|
|
||||||
.. code-block:: bash
|
{
|
||||||
|
"role": "todo_user",
|
||||||
exp=$(( EPOCHSECONDS + 5*60 )) # five minutes
|
"exp": 123456789
|
||||||
|
}
|
||||||
echo $exp
|
|
||||||
|
|
||||||
Go back to :ref:`tut1_step3` and change the payload to
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
payload=$(echo -n "{\"role\":\"todo_user\",\"exp\":123456789}" | _base64)
|
|
||||||
|
|
||||||
echo -n "$header.$payload.$signature"
|
|
||||||
|
|
||||||
**NOTE**: Don't forget to change the dummy epoch value :code:`123456789` in the snippet above to the epoch value returned by the :code:`psql` command.
|
**NOTE**: Don't forget to change the dummy epoch value :code:`123456789` in the snippet above to the epoch value returned by the :code:`psql` command.
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"nodes": {
|
|
||||||
"nixpkgs": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1731165248,
|
|
||||||
"narHash": "sha256-DiHFKIdBmMx5/DUARhVqaxvEIiy4EE6Eqs9Qs4oxme8=",
|
|
||||||
"owner": "nixos",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"rev": "a90280100f41a10914edfe729a4053e60c92b8e3",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nixos",
|
|
||||||
"ref": "nixpkgs-unstable",
|
|
||||||
"repo": "nixpkgs",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": "nixpkgs"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"root": "root",
|
|
||||||
"version": 7
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
{
|
|
||||||
description = "REST API for any Postgres database";
|
|
||||||
|
|
||||||
inputs = {
|
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
|
|
||||||
};
|
|
||||||
|
|
||||||
nixConfig = {
|
|
||||||
extra-substituters = "https://postgrest.cachix.org";
|
|
||||||
extra-trusted-public-keys = "postgrest.cachix.org-1:icgW4R15fz1+LqvhPjt4EnX/r19AaqxiVV+1olwlZtI=";
|
|
||||||
};
|
|
||||||
|
|
||||||
outputs = { nixpkgs, ... }:
|
|
||||||
let
|
|
||||||
systems = [
|
|
||||||
"aarch64-darwin"
|
|
||||||
"aarch64-linux"
|
|
||||||
"x86_64-darwin"
|
|
||||||
"x86_64-linux"
|
|
||||||
];
|
|
||||||
|
|
||||||
pgrstFor = system: import ./default.nix {
|
|
||||||
inherit system;
|
|
||||||
nixpkgsVersion = {
|
|
||||||
owner = "nixos";
|
|
||||||
repo = "nixpkgs";
|
|
||||||
inherit (nixpkgs) rev;
|
|
||||||
tarballHash = nixpkgs.narHash;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
genSystems = f: nixpkgs.lib.genAttrs systems (system: f (pgrstFor system));
|
|
||||||
in
|
|
||||||
{
|
|
||||||
packages = genSystems (attrs: {
|
|
||||||
default = attrs.postgrestPackage;
|
|
||||||
profiled = attrs.postgrestProfiled;
|
|
||||||
} // nixpkgs.lib.optionalAttrs (attrs ? postgrestStatic) {
|
|
||||||
static = attrs.postgrestStatic;
|
|
||||||
});
|
|
||||||
|
|
||||||
apps = genSystems (attrs: {
|
|
||||||
default = {
|
|
||||||
type = "app";
|
|
||||||
program = "${attrs.postgrestStatic or attrs.postgrestPackage}/bin/postgrest";
|
|
||||||
meta.description = "REST API for any Postgres database";
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -72,53 +72,45 @@ The PostgREST utilities available in `nix-shell` all have names that begin with
|
|||||||
```bash
|
```bash
|
||||||
# Note: The utilities listed here might not be up to date.
|
# Note: The utilities listed here might not be up to date.
|
||||||
[nix-shell]$ postgrest-<tab>
|
[nix-shell]$ postgrest-<tab>
|
||||||
postgrest-build postgrest-profiled-run
|
postgrest-build postgrest-test-spec
|
||||||
postgrest-check postgrest-push-cachix
|
postgrest-check postgrest-watch
|
||||||
postgrest-clean postgrest-release
|
postgrest-clean postgrest-with-all
|
||||||
postgrest-coverage postgrest-repl
|
postgrest-coverage postgrest-with-postgresql-10
|
||||||
postgrest-coverage-draft-overlay postgrest-run
|
postgrest-lint postgrest-with-postgresql-11
|
||||||
postgrest-docs-build postgrest-style
|
postgrest-run postgrest-with-postgresql-12
|
||||||
postgrest-docs-check postgrest-style-check
|
postgrest-style postgrest-with-postgresql-13
|
||||||
postgrest-docs-dictcheck postgrest-test-big-schema
|
postgrest-style-check postgrest-with-postgresql-9.6
|
||||||
postgrest-docs-linkcheck postgrest-test-doctests
|
postgrest-test-io
|
||||||
postgrest-docs-render postgrest-test-io
|
|
||||||
postgrest-docs-serve postgrest-test-memory
|
|
||||||
postgrest-docs-spellcheck postgrest-test-replica
|
|
||||||
postgrest-dump-minimal-imports postgrest-test-spec
|
|
||||||
postgrest-dump-schema postgrest-test-spec-idempotence
|
|
||||||
postgrest-gen-ctags postgrest-watch
|
|
||||||
postgrest-gen-jwt postgrest-with-all
|
|
||||||
postgrest-gen-secret postgrest-with-git
|
|
||||||
postgrest-git-hooks postgrest-with-pgrst
|
|
||||||
postgrest-hsie-graph-modules postgrest-with-postgresql-12
|
|
||||||
postgrest-hsie-graph-symbols postgrest-with-postgresql-13
|
|
||||||
postgrest-hsie-minimal-imports postgrest-with-postgresql-14
|
|
||||||
postgrest-lint postgrest-with-postgresql-15
|
|
||||||
postgrest-loadtest postgrest-with-postgresql-16
|
|
||||||
postgrest-loadtest-against postgrest-with-postgresql-17
|
|
||||||
postgrest-loadtest-report postgrest-with-slow-pg
|
|
||||||
postgrest-nixpkgs-upgrade postgrest-with-slow-postgrest
|
|
||||||
postgrest-parallel-curl
|
|
||||||
...
|
...
|
||||||
|
|
||||||
[nix-shell]$
|
[nix-shell]$
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The `docker` module has large dependencies to be build before the shell becomes
|
Some additional modules like `memory`, `docker` and `release`
|
||||||
|
have large dependencies that would need to be built before the shell becomes
|
||||||
available, which could take an especially long time if the cachix binary cache
|
available, which could take an especially long time if the cachix binary cache
|
||||||
is not used. You can activate it by passing a flag to `nix-shell` with
|
is not used. You can activate those by passing a flag to `nix-shell` with
|
||||||
`nix-shell --arg docker true`. This will make the respective utilities available:
|
`nix-shell --arg <module> true`. This will make the respective utilities available:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
$ nix-shell --arg docker true
|
$ nix-shell --arg memory true
|
||||||
[nix-shell]$ postgrest-docker-<tab>
|
[nix-shell]$ postgrest-<tab>
|
||||||
postgrest-docker-load
|
postgrest-build postgrest-test-spec
|
||||||
|
postgrest-check postgrest-watch
|
||||||
|
postgrest-clean postgrest-with-all
|
||||||
|
postgrest-coverage postgrest-with-postgresql-10
|
||||||
|
postgrest-lint postgrest-with-postgresql-11
|
||||||
|
postgrest-run postgrest-with-postgresql-12
|
||||||
|
postgrest-style postgrest-with-postgresql-13
|
||||||
|
postgrest-style-check postgrest-with-postgresql-9.6
|
||||||
|
postgrest-test-io
|
||||||
|
postgrest-test-memory
|
||||||
...
|
...
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Note that `postgrest-docker-load` is now also available.
|
Note that `postgrest-test-memory` is now also available.
|
||||||
|
|
||||||
To run one-off commands, you can also use `nix-shell --run <command>`, which
|
To run one-off commands, you can also use `nix-shell --run <command>`, which
|
||||||
will launch the Nix shell, run that one command and exit. Note that the tab
|
will launch the Nix shell, run that one command and exit. Note that the tab
|
||||||
@@ -379,8 +371,10 @@ that).
|
|||||||
|
|
||||||
We also use `default.nix` to load our pinned version of the `nixpkgs`
|
We also use `default.nix` to load our pinned version of the `nixpkgs`
|
||||||
repository. This set of packages will always be the same, independently from
|
repository. This set of packages will always be the same, independently from
|
||||||
where or when you use it. The pinned version is taken from `flake.lock` and
|
where or when you use it. The pinned version can be upgraded with the small
|
||||||
can be updated with `nix flake update`.
|
`nixpkgs-upgrade` utility. Running `nixpkgs-upgrade > nix/nixpkgs-version.nix`
|
||||||
|
in `nix-shell` will upgrade the pinned version to the latest `nixpkgs-unstable`
|
||||||
|
version.
|
||||||
|
|
||||||
### `shell.nix`
|
### `shell.nix`
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
{ buildPythonPackage
|
||||||
|
, fetchFromGitHub
|
||||||
|
, lib
|
||||||
|
, nose
|
||||||
|
, sphinx
|
||||||
|
, sphinx-rtd-theme
|
||||||
|
}:
|
||||||
|
|
||||||
|
buildPythonPackage rec {
|
||||||
|
pname = "sphinx-rtd-dark-mode";
|
||||||
|
version = "1.3.0";
|
||||||
|
format = "setuptools";
|
||||||
|
|
||||||
|
src = fetchFromGitHub {
|
||||||
|
owner = "MrDogeBro";
|
||||||
|
repo = "sphinx_rtd_dark_mode";
|
||||||
|
rev = "refs/tags/v${version}";
|
||||||
|
hash = "sha256-N5KG2Wqn9wfGNY3VH4FnBce1aZUbnvVmwD10Loe0Qn4=";
|
||||||
|
};
|
||||||
|
|
||||||
|
propagatedBuildInputs = [
|
||||||
|
sphinx-rtd-theme
|
||||||
|
];
|
||||||
|
|
||||||
|
nativeCheckInputs = [
|
||||||
|
nose
|
||||||
|
sphinx
|
||||||
|
];
|
||||||
|
|
||||||
|
checkPhase = ''
|
||||||
|
runHook preCheck
|
||||||
|
nosetests tests
|
||||||
|
runHook postCheck
|
||||||
|
'';
|
||||||
|
|
||||||
|
pythonImportsCheck = [
|
||||||
|
"sphinx_rtd_dark_mode"
|
||||||
|
];
|
||||||
|
|
||||||
|
meta = with lib; {
|
||||||
|
description = "Adds a toggleable dark mode to the Read the Docs theme for Sphinx.";
|
||||||
|
homepage = "https://github.com/MrDogeBro/sphinx_rtd_dark_mode";
|
||||||
|
changelog = "https://github.com/MrDogeBro/sphinx_rtd_dark_mode/releases/tag/v${version}";
|
||||||
|
license = licenses.mit;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -19,10 +19,6 @@ stdenv.mkDerivation {
|
|||||||
pname = "libpq";
|
pname = "libpq";
|
||||||
inherit (postgresql) src version patches;
|
inherit (postgresql) src version patches;
|
||||||
|
|
||||||
__structuredAttrs = true;
|
|
||||||
env.CFLAGS = "-fdata-sections -ffunction-sections"
|
|
||||||
+ (if stdenv.cc.isClang then " -flto" else " -fmerge-constants -Wl,--gc-sections");
|
|
||||||
|
|
||||||
configureFlags = [
|
configureFlags = [
|
||||||
"--without-gssapi"
|
"--without-gssapi"
|
||||||
"--without-icu"
|
"--without-icu"
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Pinned version of Nixpkgs, generated with postgrest-nixpkgs-upgrade.
|
||||||
|
{
|
||||||
|
owner = "NixOS";
|
||||||
|
repo = "nixpkgs";
|
||||||
|
date = "2024-05-29";
|
||||||
|
rev = "a15e8d1b3d9e6496c4a3214e2104f6d28dfa7df7";
|
||||||
|
tarballHash = "sha256:0r4a4165f4n1zlpnyjrdrwrg86n1b6g3axsdh3j5iizpmjrlxmd7";
|
||||||
|
}
|
||||||
@@ -3,6 +3,9 @@
|
|||||||
checked-shell-script = import ./checked-shell-script;
|
checked-shell-script = import ./checked-shell-script;
|
||||||
gitignore = import ./gitignore.nix;
|
gitignore = import ./gitignore.nix;
|
||||||
haskell-packages = import ./haskell-packages.nix;
|
haskell-packages = import ./haskell-packages.nix;
|
||||||
|
postgis = import ./postgis.nix;
|
||||||
postgresql-libpq = import ./postgresql-libpq.nix;
|
postgresql-libpq = import ./postgresql-libpq.nix;
|
||||||
|
postgresql-legacy = import ./postgresql-legacy.nix;
|
||||||
|
postgresql-future = import ./postgresql-future.nix;
|
||||||
slocat = import ./slocat.nix;
|
slocat = import ./slocat.nix;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,17 +48,14 @@ let
|
|||||||
|
|
||||||
# Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329
|
# Before upgrading fuzzyset to 0.3, check: https://github.com/PostgREST/postgrest/issues/3329
|
||||||
# jailbreak, because hspec limit for tests
|
# jailbreak, because hspec limit for tests
|
||||||
fuzzyset = prev.fuzzyset_0_2_4;
|
fuzzyset = lib.doJailbreak
|
||||||
|
(prev.callHackageDirect
|
||||||
# TODO: Remove once available in nixpkgs haskellPackages
|
|
||||||
configurator-pg =
|
|
||||||
prev.callHackageDirect
|
|
||||||
{
|
{
|
||||||
pkg = "configurator-pg";
|
pkg = "fuzzyset";
|
||||||
ver = "0.2.11";
|
ver = "0.2.4";
|
||||||
sha256 = "sha256-mtGtNawDJgz2ZIEVca+IYXVu4oNw9xsfJiYWAqAbbgc=";
|
sha256 = "sha256-lpkrTFcR0B4rT/P6x7ui31Twgq7BBj6KIvjKyqXKdpc=";
|
||||||
}
|
}
|
||||||
{ };
|
{ });
|
||||||
|
|
||||||
hasql-pool = lib.dontCheck (prev.callHackageDirect
|
hasql-pool = lib.dontCheck (prev.callHackageDirect
|
||||||
{
|
{
|
||||||
@@ -66,7 +63,13 @@ let
|
|||||||
ver = "1.0.1";
|
ver = "1.0.1";
|
||||||
sha256 = "sha256-Hf1f7lX0LWkjrb25SDBovCYPRdmUP1H6pAxzi7kT4Gg=";
|
sha256 = "sha256-Hf1f7lX0LWkjrb25SDBovCYPRdmUP1H6pAxzi7kT4Gg=";
|
||||||
}
|
}
|
||||||
{ });
|
{ }
|
||||||
|
);
|
||||||
|
|
||||||
|
postgresql-libpq = lib.dontCheck
|
||||||
|
(prev.postgresql-libpq.override {
|
||||||
|
postgresql = super.libpq;
|
||||||
|
});
|
||||||
|
|
||||||
hasql-notifications = lib.dontCheck (prev.callHackageDirect
|
hasql-notifications = lib.dontCheck (prev.callHackageDirect
|
||||||
{
|
{
|
||||||
@@ -74,20 +77,9 @@ let
|
|||||||
ver = "0.2.2.2";
|
ver = "0.2.2.2";
|
||||||
sha256 = "sha256-myKwlug7OgTa/qP6mHfCD+5Q8IhM17JvpJBfSo+M01k=";
|
sha256 = "sha256-myKwlug7OgTa/qP6mHfCD+5Q8IhM17JvpJBfSo+M01k=";
|
||||||
}
|
}
|
||||||
{ });
|
{ }
|
||||||
|
);
|
||||||
|
|
||||||
# newer nixpkgs already has 0.10., so we fallback to default for forward compat
|
|
||||||
jose-jwt = prev.jose-jwt_0_10_0 or prev.jose-jwt;
|
|
||||||
|
|
||||||
postgresql-libpq = lib.dontCheck (prev.callHackageDirect
|
|
||||||
{
|
|
||||||
pkg = "postgresql-libpq";
|
|
||||||
ver = "0.10.1.0";
|
|
||||||
sha256 = "sha256-tXOMqCO8opMilI9rx0D+njqjIjbZsH168Bzb8Aq8Ff4=";
|
|
||||||
}
|
|
||||||
{
|
|
||||||
postgresql = super.libpq;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
final: prev:
|
||||||
|
let
|
||||||
|
postgis_3_2_3 = rec {
|
||||||
|
version = "3.2.3";
|
||||||
|
src = final.fetchurl {
|
||||||
|
url = "https://download.osgeo.org/postgis/source/postgis-${version}.tar.gz";
|
||||||
|
sha256 = "sha256-G02LXHVuWrpZ77wYM7Iu/k1lYneO7KVvpJf+susTZow=";
|
||||||
|
};
|
||||||
|
meta.broken = false;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
postgresql_11 = prev.postgresql_11.override { this = final.postgresql_11; } // {
|
||||||
|
pkgs = prev.postgresql_11.pkgs // {
|
||||||
|
postgis = prev.postgresql_11.pkgs.postgis.overrideAttrs (_: postgis_3_2_3);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
postgresql_10 = prev.postgresql_10.override { this = final.postgresql_11; } // {
|
||||||
|
pkgs = prev.postgresql_10.pkgs // {
|
||||||
|
postgis = prev.postgresql_10.pkgs.postgis.overrideAttrs (_: postgis_3_2_3);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
postgresql_9_6 = prev.postgresql_9_6.override { this = final.postgresql_11; } // {
|
||||||
|
pkgs = prev.postgresql_9_6.pkgs // {
|
||||||
|
postgis = prev.postgresql_9_6.pkgs.postgis.overrideAttrs (_: postgis_3_2_3);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
_: _:
|
||||||
|
# Overlay that adds future versions of PostgreSQL that are supported by
|
||||||
|
# PostgREST.
|
||||||
|
{
|
||||||
|
## Example for including a postgresql version from a specific nixpks commit:
|
||||||
|
##
|
||||||
|
# postgresql_16 =
|
||||||
|
# let
|
||||||
|
# rev = "5148520bfab61f99fd25fb9ff7bfbb50dad3c9db";
|
||||||
|
# tarballHash = "1dfjmz65h8z4lk845724vypzmf3dbgsdndjpj8ydlhx6c7rpcq3p";
|
||||||
|
#
|
||||||
|
# pinnedPkgs =
|
||||||
|
# builtins.fetchTarball {
|
||||||
|
# url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||||
|
# sha256 = tarballHash;
|
||||||
|
# };
|
||||||
|
# in
|
||||||
|
# (import pinnedPkgs { }).pkgs.postgresql_16;
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
_: _:
|
||||||
|
# Overlay that adds legacy versions of PostgreSQL that are supported by
|
||||||
|
# PostgREST.
|
||||||
|
{
|
||||||
|
# PostgreSQL 9.6 was removed from Nixpkgs with
|
||||||
|
# https://github.com/NixOS/nixpkgs/commit/757dd008b2f2926fc0f7688fa8189f930ea47521
|
||||||
|
# We pin its parent commit to get the last version that was available.
|
||||||
|
postgresql_9_6 =
|
||||||
|
let
|
||||||
|
rev = "571cbf3d1db477058303cef8754fb85a14e90eb7";
|
||||||
|
tarballHash = "0q74wn418i1bn5sssacmw8ykpmqvzr0s93sj6pbs3rf6bf134fkz";
|
||||||
|
pinnedPkgs =
|
||||||
|
builtins.fetchTarball {
|
||||||
|
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||||
|
sha256 = tarballHash;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
(import pinnedPkgs { }).pkgs.postgresql_9_6;
|
||||||
|
|
||||||
|
# PostgreSQL 10 was removed from Nixpkgs with
|
||||||
|
# https://github.com/NixOS/nixpkgs/commit/aa1483114bb329fee7e1266100b8d8921ed4723f
|
||||||
|
# We pin its parent commit to get the last version that was available.
|
||||||
|
postgresql_10 =
|
||||||
|
let
|
||||||
|
rev = "79661ba7e2fb96ebefbb537458a5bbae9dc5bd1a";
|
||||||
|
tarballHash = "0rn796pfn4sg90ai9fdnwmr10a2s835p1arazzgz46h6s5cxvq97";
|
||||||
|
pinnedPkgs =
|
||||||
|
builtins.fetchTarball {
|
||||||
|
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||||
|
sha256 = tarballHash;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
(import pinnedPkgs { }).pkgs.postgresql_10;
|
||||||
|
|
||||||
|
# PostgreSQL 11 was removed from Nixpkgs with
|
||||||
|
# https://github.com/NixOS/nixpkgs/commit/1220a4d4dd1a4590780a5e1c18d1333a121be366
|
||||||
|
# We pin its parent commit to get the last version that was available.
|
||||||
|
postgresql_11 =
|
||||||
|
let
|
||||||
|
rev = "f5458516e42cc5cb4123cc2d93f45c240548aa18";
|
||||||
|
tarballHash = "1h03621sxfhw4z6ya74k6c2lyx3z7pvf2jcg4vs7i01yz2m6w3cv";
|
||||||
|
pinnedPkgs =
|
||||||
|
builtins.fetchTarball {
|
||||||
|
url = "https://github.com/nixos/nixpkgs/archive/${rev}.tar.gz";
|
||||||
|
sha256 = tarballHash;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
(import pinnedPkgs { }).pkgs.postgresql_11;
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
_: super:
|
_: super:
|
||||||
{
|
{
|
||||||
# Depending on which nixpkgs version is pinned, libpq might either be available already - or not.
|
libpq = super.callPackage ../libpq.nix {
|
||||||
libpq = super.libpq or (super.callPackage ../libpq.nix {
|
|
||||||
postgresql = super.postgresql_16;
|
postgresql = super.postgresql_16;
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,12 @@
|
|||||||
, src
|
, src
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
# This builds a static PostgREST executable based on pkgsStatic.
|
# This builds a static PostgREST exectuable based on pkgsStatic.
|
||||||
inherit (pkgs) pkgsStatic;
|
# pkgsStatic is based on musl, so is a kind of cross-compilation.
|
||||||
|
# We still make this explicit here via pkgsCross, because we need
|
||||||
|
# to get postgresql/libpq for musl, too.
|
||||||
|
pkgsCross = pkgs.pkgsCross.musl64;
|
||||||
|
inherit (pkgsCross) pkgsStatic;
|
||||||
inherit (pkgsStatic.haskell) lib;
|
inherit (pkgsStatic.haskell) lib;
|
||||||
|
|
||||||
packagesStatic =
|
packagesStatic =
|
||||||
@@ -21,16 +25,9 @@ let
|
|||||||
|
|
||||||
overrides = pkgs.lib.composeExtensions old.overrides (_: prev: {
|
overrides = pkgs.lib.composeExtensions old.overrides (_: prev: {
|
||||||
postgresql-libpq = (lib.overrideCabal prev.postgresql-libpq {
|
postgresql-libpq = (lib.overrideCabal prev.postgresql-libpq {
|
||||||
# TODO: This section can be simplified when this PR has made it's way to us:
|
|
||||||
# https://github.com/NixOS/nixpkgs/pull/286370
|
|
||||||
# Additionally, we need to use the default version in nixpkgs, otherwise the
|
|
||||||
# override will not be active as well.
|
|
||||||
# Using use-pkg-config flag, because pg_config won't work when cross-compiling
|
|
||||||
configureFlags = [ "-fuse-pkg-config" ];
|
|
||||||
# postgresql doesn't build in the fully static overlay - but the default
|
# postgresql doesn't build in the fully static overlay - but the default
|
||||||
# derivation is built with static libraries anyway.
|
# derivation is built with static libraries anyway.
|
||||||
libraryPkgconfigDepends = [ pkgsStatic.libpq ];
|
libraryPkgconfigDepends = [ pkgsCross.libpq ];
|
||||||
librarySystemDepends = [ ];
|
|
||||||
}).overrideAttrs (_: prevAttrs: {
|
}).overrideAttrs (_: prevAttrs: {
|
||||||
buildInputs = prevAttrs.buildInputs ++ [ pkgsStatic.openssl ];
|
buildInputs = prevAttrs.buildInputs ++ [ pkgsStatic.openssl ];
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ let
|
|||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
${cabal-install}/bin/cabal v2-update
|
|
||||||
exec ${cabal-install}/bin/cabal v2-build ${devCabalOptions} "''${_arg_leftovers[@]}"
|
exec ${cabal-install}/bin/cabal v2-build ${devCabalOptions} "''${_arg_leftovers[@]}"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
@@ -60,35 +59,6 @@ let
|
|||||||
postgrest "''${_arg_leftovers[@]}"
|
postgrest "''${_arg_leftovers[@]}"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|
||||||
runProfiled =
|
|
||||||
checkedShellScript
|
|
||||||
{
|
|
||||||
name = "postgrest-profiled-run";
|
|
||||||
docs = "Run a profiled build of postgREST. This will generate a postgrest.prof file that can be used to do optimization.";
|
|
||||||
args =
|
|
||||||
[
|
|
||||||
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [PostgREST anonymous role])"
|
|
||||||
"ARG_USE_ENV([PGRST_DB_POOL], [1], [PostgREST pool size])"
|
|
||||||
"ARG_USE_ENV([PGRST_DB_POOL_ACQUISITION_TIMEOUT], [1], [PostgREST pool timeout])"
|
|
||||||
"ARG_USE_ENV([PGRST_JWT_SECRET], [reallyreallyreallyreallyverysafe], [PostgREST JWT secret])"
|
|
||||||
"ARG_LEFTOVERS([PostgREST arguments])"
|
|
||||||
];
|
|
||||||
workingDir = "/";
|
|
||||||
withEnv = postgrest.env;
|
|
||||||
}
|
|
||||||
''
|
|
||||||
export PGRST_DB_ANON_ROLE
|
|
||||||
export PGRST_DB_POOL
|
|
||||||
export PGRST_DB_POOL_ACQUISITION_TIMEOUT
|
|
||||||
export PGRST_JWT_SECRET
|
|
||||||
|
|
||||||
${cabal-install}/bin/cabal v2-update
|
|
||||||
${cabal-install}/bin/cabal --builddir="dist-prof" v2-build --enable-profiling --disable-shared exe:postgrest
|
|
||||||
${cabal-install}/bin/cabal --builddir="dist-prof" v2-run -- \
|
|
||||||
postgrest +RTS -p -h -RTS "''${_arg_leftovers[@]}"
|
|
||||||
'';
|
|
||||||
|
|
||||||
repl =
|
repl =
|
||||||
checkedShellScript
|
checkedShellScript
|
||||||
{
|
{
|
||||||
@@ -110,7 +80,6 @@ buildToolbox
|
|||||||
build
|
build
|
||||||
clean
|
clean
|
||||||
run
|
run
|
||||||
runProfiled
|
|
||||||
repl;
|
repl;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,7 +246,6 @@ let
|
|||||||
}
|
}
|
||||||
''
|
''
|
||||||
mkdir -p "$_arg_dumpdir"
|
mkdir -p "$_arg_dumpdir"
|
||||||
${cabal-install}/bin/cabal v2-update
|
|
||||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} \
|
${cabal-install}/bin/cabal v2-build ${devCabalOptions} \
|
||||||
--builddir="$tmpdir" \
|
--builddir="$tmpdir" \
|
||||||
--ghc-option=-ddump-minimal-imports \
|
--ghc-option=-ddump-minimal-imports \
|
||||||
@@ -334,29 +333,20 @@ let
|
|||||||
checkedShellScript
|
checkedShellScript
|
||||||
{
|
{
|
||||||
name = "postgrest-gen-jwt";
|
name = "postgrest-gen-jwt";
|
||||||
docs = ''
|
docs = "Generate a JWT";
|
||||||
Generate a JWT. Example: postgrest-gen-jwt --exp 10 postgrest_test_author
|
|
||||||
|
|
||||||
# This can be used to quickly prove a JWT expiry
|
|
||||||
$ curl localhost:3000/authors_only -H "Authorization: Bearer \$(postgrest-gen-jwt --exp -31 postgrest_test_author)"
|
|
||||||
'';
|
|
||||||
args = [
|
args = [
|
||||||
"ARG_POSITIONAL_SINGLE([role], [role for the jwt payload])"
|
"ARG_POSITIONAL_SINGLE([role], [role for the jwt payload])"
|
||||||
"ARG_OPTIONAL_SINGLE([secret],, [secret used to sign the JWT], [reallyreallyreallyreallyverysafe])"
|
"ARG_OPTIONAL_SINGLE([secret],, [secret used to sign the JWT], [reallyreallyreallyreallyverysafe])"
|
||||||
"ARG_OPTIONAL_SINGLE([exp],, [seconds for JWT expiry, it accepts negative values], [3600])"
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
# Based on https://stackoverflow.com/questions/59002949/how-to-create-a-json-web-token-jwt-using-openssl-shell-commands
|
# From https://stackoverflow.com/questions/59002949/how-to-create-a-json-web-token-jwt-using-openssl-shell-commands
|
||||||
|
|
||||||
# Construct the header
|
# Construct the header
|
||||||
jwt_header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | base64 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//)
|
jwt_header=$(echo -n '{"alg":"HS256","typ":"JWT"}' | base64 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//)
|
||||||
|
|
||||||
# Construct the exp value
|
|
||||||
expiry=$((EPOCHSECONDS + _arg_exp))
|
|
||||||
|
|
||||||
# Construct the payload
|
# Construct the payload
|
||||||
payload=$(echo -n "{\"role\": \"$_arg_role\", \"exp\": $expiry}" | base64 | sed s/\+/-/g |sed 's/\//_/g' | sed -E s/=+$//)
|
payload=$(echo -n "{\"role\":\"$_arg_role\"}" | base64 | sed s/\+/-/g |sed 's/\//_/g' | sed -E s/=+$//)
|
||||||
|
|
||||||
# Convert secret to hex
|
# Convert secret to hex
|
||||||
hexsecret=$(echo -n "$_arg_secret" | xxd -p | paste -sd "")
|
hexsecret=$(echo -n "$_arg_secret" | xxd -p | paste -sd "")
|
||||||
|
|||||||
@@ -2,40 +2,23 @@
|
|||||||
, aspellDicts
|
, aspellDicts
|
||||||
, buildToolbox
|
, buildToolbox
|
||||||
, checkedShellScript
|
, checkedShellScript
|
||||||
, fetchPypi
|
|
||||||
, lib
|
|
||||||
, plantuml
|
|
||||||
, python3
|
, python3
|
||||||
, python3Packages
|
, python3Packages
|
||||||
, writeTextFile
|
|
||||||
, writers
|
, writers
|
||||||
|
, plantuml
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
selectPythonPackages = ps:
|
selectPythonPackages = ps: [
|
||||||
let
|
ps.sphinx
|
||||||
# TODO: Remove with next nixpkgs update
|
ps.sphinx_rtd_theme
|
||||||
sphinx-rtd-theme = assert ps.sphinx-rtd-theme.version == "2.0.0"; ps.sphinx-rtd-theme.overrideAttrs rec {
|
ps.livereload
|
||||||
version = "3.0.2";
|
ps.sphinx-tabs
|
||||||
src = fetchPypi {
|
ps.sphinx-copybutton
|
||||||
pname = "sphinx_rtd_theme";
|
ps.sphinxext-opengraph
|
||||||
inherit version;
|
(ps.callPackage ../docs-extensions/sphinx-rtd-dark-mode.nix { })
|
||||||
hash = "sha256-t0V7wl3acjsgsIamcLmVPIWeq2CioD7o6yuyPhduX4U=";
|
# TODO: Remove override once new sphinx-intl version (> 2.1.0) is released and available in nixpkgs
|
||||||
};
|
(ps.sphinx-intl.overrideAttrs (drv: { nativeBuildInputs = drv.nativeBuildInputs ++ [ ps.six ]; }))
|
||||||
};
|
];
|
||||||
in
|
|
||||||
[
|
|
||||||
ps.sphinx
|
|
||||||
ps.sphinx-copybutton
|
|
||||||
(ps.sphinx-rtd-dark-mode.override { inherit sphinx-rtd-theme; })
|
|
||||||
sphinx-rtd-theme
|
|
||||||
ps.sphinx-tabs
|
|
||||||
ps.sphinxext-opengraph
|
|
||||||
];
|
|
||||||
|
|
||||||
requirements = writeTextFile {
|
|
||||||
name = "requirements.txt";
|
|
||||||
text = lib.concatMapStringsSep "\n" (pkg: "${pkg.pname}==${pkg.version}") (selectPythonPackages python3Packages);
|
|
||||||
};
|
|
||||||
|
|
||||||
python = python3.withPackages selectPythonPackages;
|
python = python3.withPackages selectPythonPackages;
|
||||||
|
|
||||||
@@ -90,7 +73,7 @@ let
|
|||||||
server =
|
server =
|
||||||
writers.writePython3
|
writers.writePython3
|
||||||
"postgrest-docs-server"
|
"postgrest-docs-server"
|
||||||
{ libraries = selectPythonPackages python3Packages ++ [ python3Packages.livereload ]; }
|
{ libraries = selectPythonPackages python3Packages; }
|
||||||
''
|
''
|
||||||
import sys
|
import sys
|
||||||
from livereload import Server, shell
|
from livereload import Server, shell
|
||||||
@@ -200,5 +183,4 @@ buildToolbox
|
|||||||
serve
|
serve
|
||||||
spellcheck;
|
spellcheck;
|
||||||
};
|
};
|
||||||
extra = { inherit requirements; };
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,94 +0,0 @@
|
|||||||
# generates a file to be used by the vegeta load testing tool
|
|
||||||
import time
|
|
||||||
import hmac
|
|
||||||
import hashlib
|
|
||||||
import base64
|
|
||||||
import json
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
import random
|
|
||||||
|
|
||||||
SECRET = b"reallyreallyreallyreallyverysafe"
|
|
||||||
URL = "http://postgrest"
|
|
||||||
JWT_DURATION = 120
|
|
||||||
TOTAL_TARGETS = 50000 # tuned by hand to reduce result variance
|
|
||||||
|
|
||||||
|
|
||||||
def base64url_encode(data: bytes) -> str:
|
|
||||||
"""URL-safe Base64 encode without padding."""
|
|
||||||
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
|
|
||||||
|
|
||||||
|
|
||||||
def generate_jwt(exp_inc: int) -> str:
|
|
||||||
"""Generate an HS256 JWT"""
|
|
||||||
# Header & payload
|
|
||||||
header = {"alg": "HS256", "typ": "JWT"}
|
|
||||||
now = int(time.time())
|
|
||||||
payload = {
|
|
||||||
"sub": f"user_{random.getrandbits(32)}",
|
|
||||||
"iat": now,
|
|
||||||
"exp": now + exp_inc,
|
|
||||||
"role": "postgrest_test_author",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Encode to JSON and then to Base64URL
|
|
||||||
header_b = json.dumps(header, separators=(",", ":")).encode()
|
|
||||||
payload_b = json.dumps(payload, separators=(",", ":")).encode()
|
|
||||||
header_b64 = base64url_encode(header_b)
|
|
||||||
payload_b64 = base64url_encode(payload_b)
|
|
||||||
|
|
||||||
# Sign (HMAC‑SHA256) the "<header>.<payload>" string
|
|
||||||
signing_input = f"{header_b64}.{payload_b64}".encode()
|
|
||||||
signature = hmac.new(SECRET, signing_input, hashlib.sha256).digest()
|
|
||||||
signature_b64 = base64url_encode(signature)
|
|
||||||
|
|
||||||
return f"{header_b64}.{payload_b64}.{signature_b64}"
|
|
||||||
|
|
||||||
|
|
||||||
# We want to ensure 401 Unauthorized responses don't happen during
|
|
||||||
# JWT validation, this can happen when the jwt `exp` is too short.
|
|
||||||
# At the same time, we want to ensure the `exp` is not too big,
|
|
||||||
# so expires will occur and postgREST will have to clean cached expired JWTs.
|
|
||||||
def estimate_adequate_jwt_exp_increase(iteration: int) -> int:
|
|
||||||
# estimated time takes to build and run postgrest itself
|
|
||||||
build_run_postgrest_time = 2
|
|
||||||
# estimated time it takes to generate the targets file
|
|
||||||
file_generation_time = TOTAL_TARGETS // (10**-5)
|
|
||||||
# estimated exp time so some JWTs will expire
|
|
||||||
dynamic_exp_inc = iteration // 1000
|
|
||||||
|
|
||||||
return build_run_postgrest_time + file_generation_time + dynamic_exp_inc
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
description="Generate Vegeta targets with unique JWTs"
|
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"output",
|
|
||||||
help="Path to write the generated targets file",
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
|
||||||
|
|
||||||
lines = []
|
|
||||||
start_time = time.time()
|
|
||||||
|
|
||||||
for i in range(TOTAL_TARGETS):
|
|
||||||
token = generate_jwt(estimate_adequate_jwt_exp_increase(i))
|
|
||||||
lines.append(f"OPTIONS {URL}/authors_only")
|
|
||||||
lines.append(f"Authorization: Bearer {token}")
|
|
||||||
lines.append("") # blank line to separate requests
|
|
||||||
|
|
||||||
try:
|
|
||||||
with open(args.output, "w") as f:
|
|
||||||
f.write("\n".join(lines))
|
|
||||||
except IOError as e:
|
|
||||||
print(f"Error writing to {args.output}: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
elapsed = time.time() - start_time
|
|
||||||
print(f"Created {TOTAL_TARGETS} targets in {args.output} ({elapsed:.2f}s)")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -41,8 +41,6 @@ let
|
|||||||
args = [
|
args = [
|
||||||
"ARG_OPTIONAL_SINGLE([output], [o], [Filename to dump json output to], [./loadtest/result.bin])"
|
"ARG_OPTIONAL_SINGLE([output], [o], [Filename to dump json output to], [./loadtest/result.bin])"
|
||||||
"ARG_OPTIONAL_SINGLE([testdir], [t], [Directory to load tests and fixtures from], [./test/load])"
|
"ARG_OPTIONAL_SINGLE([testdir], [t], [Directory to load tests and fixtures from], [./test/load])"
|
||||||
"ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest (mixed: repeat mixed requests, jwt: run once over many requests with unique jwts)], [mixed])"
|
|
||||||
"ARG_TYPE_GROUP_SET([KIND], [KIND], [kind], [mixed,jwt])"
|
|
||||||
"ARG_LEFTOVERS([additional vegeta arguments])"
|
"ARG_LEFTOVERS([additional vegeta arguments])"
|
||||||
];
|
];
|
||||||
workingDir = "/";
|
workingDir = "/";
|
||||||
@@ -57,36 +55,17 @@ let
|
|||||||
export PGRST_DB_POOL="1"
|
export PGRST_DB_POOL="1"
|
||||||
export PGRST_DB_TX_END="rollback-allow-override"
|
export PGRST_DB_TX_END="rollback-allow-override"
|
||||||
export PGRST_LOG_LEVEL="crit"
|
export PGRST_LOG_LEVEL="crit"
|
||||||
export PGRST_JWT_SECRET="reallyreallyreallyreallyverysafe"
|
|
||||||
export PGRST_JWT_CACHE_MAX_LIFETIME="86400"
|
|
||||||
|
|
||||||
mkdir -p "$(dirname "$_arg_output")"
|
mkdir -p "$(dirname "$_arg_output")"
|
||||||
abs_output="$(realpath "$_arg_output")"
|
abs_output="$(realpath "$_arg_output")"
|
||||||
|
|
||||||
case "$_arg_kind" in
|
# shellcheck disable=SC2145
|
||||||
jwt)
|
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
||||||
|
${withTools.withSlowPg} \
|
||||||
${genTargets} "$_arg_testdir"/gen_targets.http
|
${withTools.withPgrst} \
|
||||||
|
${withTools.withSlowPgrst} \
|
||||||
# shellcheck disable=SC2145
|
sh -c "cd \"$_arg_testdir\" && ${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
||||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
||||||
${withTools.withPgrst} \
|
|
||||||
sh -c "cd \"$_arg_testdir\" && ${runner} -lazy -targets gen_targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
|
||||||
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
|
|
||||||
# shellcheck disable=SC2145
|
|
||||||
${withTools.withPg} -f "$_arg_testdir"/fixtures.sql \
|
|
||||||
${withTools.withSlowPg} \
|
|
||||||
${withTools.withPgrst} \
|
|
||||||
${withTools.withSlowPgrst} \
|
|
||||||
sh -c "cd \"$_arg_testdir\" && ${runner} -targets targets.http -output \"$abs_output\" \"''${_arg_leftovers[@]}\""
|
|
||||||
${vegeta}/bin/vegeta report -type=text "$_arg_output"
|
|
||||||
;;
|
|
||||||
|
|
||||||
esac
|
|
||||||
'';
|
'';
|
||||||
|
|
||||||
loadtestAgainst =
|
loadtestAgainst =
|
||||||
@@ -104,7 +83,6 @@ let
|
|||||||
'';
|
'';
|
||||||
args = [
|
args = [
|
||||||
"ARG_POSITIONAL_INF([target], [Commit-ish reference to compare with], 1)"
|
"ARG_POSITIONAL_INF([target], [Commit-ish reference to compare with], 1)"
|
||||||
"ARG_OPTIONAL_SINGLE([kind], [k], [Kind of loadtest], [mixed])"
|
|
||||||
];
|
];
|
||||||
positionalCompletion =
|
positionalCompletion =
|
||||||
''
|
''
|
||||||
@@ -128,7 +106,7 @@ let
|
|||||||
# Save the results in the current working tree, too,
|
# Save the results in the current working tree, too,
|
||||||
# otherwise they'd be lost in the temporary working tree
|
# otherwise they'd be lost in the temporary working tree
|
||||||
# created by withTools.withGit.
|
# created by withTools.withGit.
|
||||||
${withTools.withGit} "$tgt" ${loadtest} -k "$_arg_kind" --output "$PWD/loadtest/$tgt.bin" --testdir "$PWD/test/load"
|
${withTools.withGit} "$tgt" ${loadtest} --output "$PWD/loadtest/$tgt.bin" --testdir "$PWD/test/load"
|
||||||
|
|
||||||
cat << EOF
|
cat << EOF
|
||||||
|
|
||||||
@@ -144,7 +122,7 @@ let
|
|||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
${loadtest} -k "$_arg_kind" --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load"
|
${loadtest} --output "$PWD/loadtest/head.bin" --testdir "$PWD/test/load"
|
||||||
|
|
||||||
cat << EOF
|
cat << EOF
|
||||||
|
|
||||||
@@ -200,7 +178,6 @@ let
|
|||||||
| ${toMarkdown}
|
| ${toMarkdown}
|
||||||
'';
|
'';
|
||||||
|
|
||||||
genTargets = writers.writePython3 "postgrest-gen-loadtest-targets" { } (builtins.readFile ./generate_targets.py);
|
|
||||||
in
|
in
|
||||||
buildToolbox {
|
buildToolbox {
|
||||||
name = "postgrest-loadtest";
|
name = "postgrest-loadtest";
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# The memory tests have large dependencies (a profiled build of PostgREST)
|
||||||
|
# and are run less often than the spec tests, so we don't include them in
|
||||||
|
# the default test environment. We make them available through a separate module.
|
||||||
|
# TODO both of these require reentering the nix-shell if you make a change to the code
|
||||||
|
{ buildToolbox
|
||||||
|
, checkedShellScript
|
||||||
|
, curl
|
||||||
|
, postgrestProfiled
|
||||||
|
, withTools
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
test =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-test-memory";
|
||||||
|
docs = "Run the memory tests.";
|
||||||
|
workingDir = "/";
|
||||||
|
withPath = [ postgrestProfiled curl ];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
${withTools.withPg} -f test/spec/fixtures/load.sql test/memory/memory-tests.sh
|
||||||
|
'';
|
||||||
|
|
||||||
|
runProfiled =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
name = "postgrest-profiled-run";
|
||||||
|
docs = "Run a profiled build of postgREST. This will generate a postgrest.prof file that can be used to do optimization. Note: if you make a change to the code, you must reenter the nix-shell for an updated profiled build.";
|
||||||
|
args =
|
||||||
|
[
|
||||||
|
"ARG_USE_ENV([PGRST_DB_ANON_ROLE], [postgrest_test_anonymous], [PostgREST anonymous role])"
|
||||||
|
"ARG_USE_ENV([PGRST_DB_POOL], [1], [PostgREST pool size])"
|
||||||
|
"ARG_USE_ENV([PGRST_DB_POOL_ACQUISITION_TIMEOUT], [1], [PostgREST pool timeout])"
|
||||||
|
"ARG_LEFTOVERS([PostgREST arguments])"
|
||||||
|
];
|
||||||
|
workingDir = "/";
|
||||||
|
withPath = [ postgrestProfiled ];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
export PGRST_DB_ANON_ROLE
|
||||||
|
export PGRST_DB_POOL
|
||||||
|
export PGRST_DB_POOL_ACQUISITION_TIMEOUT
|
||||||
|
|
||||||
|
postgrest +RTS -p -h -RTS "''${_arg_leftovers[@]}"
|
||||||
|
'';
|
||||||
|
in
|
||||||
|
buildToolbox
|
||||||
|
{
|
||||||
|
name = "postgrest-memory";
|
||||||
|
tools = { inherit test runProfiled; };
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{ buildToolbox
|
||||||
|
, checkedShellScript
|
||||||
|
, coreutils
|
||||||
|
, curl
|
||||||
|
, jq
|
||||||
|
, nix
|
||||||
|
}:
|
||||||
|
# Utility script for pinning the latest stable version of Nixpkgs.
|
||||||
|
|
||||||
|
# Instead of pinning Nixpkgs based on the huge Git repository, we reference a
|
||||||
|
# specific tarball that only contains the source of the revision that we want
|
||||||
|
# to pin.
|
||||||
|
let
|
||||||
|
name =
|
||||||
|
"postgrest-nixpkgs-upgrade";
|
||||||
|
|
||||||
|
refUrl =
|
||||||
|
"https://api.github.com/repos/nixos/nixpkgs/git/matching-refs/heads/nixpkgs-";
|
||||||
|
|
||||||
|
githubV3Header =
|
||||||
|
"Accept: application/vnd.github.v3+json";
|
||||||
|
|
||||||
|
tarballUrlBase =
|
||||||
|
"https://github.com/nixos/nixpkgs/archive/";
|
||||||
|
|
||||||
|
upgrade =
|
||||||
|
checkedShellScript
|
||||||
|
{
|
||||||
|
inherit name;
|
||||||
|
docs = "Pin the newest stable version of Nixpkgs.";
|
||||||
|
workingDir = "/";
|
||||||
|
}
|
||||||
|
''
|
||||||
|
# The list of refs is sorted. The first result will be nixpkgs-unstable, the second the latest stable branch.
|
||||||
|
commitHash="$(${curl}/bin/curl "${refUrl}" -H "${githubV3Header}" | ${jq}/bin/jq -r 'sort_by(.ref) | reverse | .[1].object.sha')"
|
||||||
|
tarballUrl="${tarballUrlBase}$commitHash.tar.gz"
|
||||||
|
tarballHash="$(${nix}/bin/nix-prefetch-url --unpack "$tarballUrl")"
|
||||||
|
currentDate="$(${coreutils}/bin/date --iso)"
|
||||||
|
|
||||||
|
cat > nix/nixpkgs-version.nix << EOF
|
||||||
|
# Pinned version of Nixpkgs, generated with ${name}.
|
||||||
|
{
|
||||||
|
owner = "NixOS";
|
||||||
|
repo = "nixpkgs";
|
||||||
|
date = "$currentDate";
|
||||||
|
rev = "$commitHash";
|
||||||
|
tarballHash = "$tarballHash";
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
'';
|
||||||
|
|
||||||
|
in
|
||||||
|
buildToolbox
|
||||||
|
{
|
||||||
|
name = "postgrest-nixpkgs";
|
||||||
|
tools = { inherit upgrade; };
|
||||||
|
}
|
||||||
@@ -29,28 +29,22 @@ let
|
|||||||
case "$1" in
|
case "$1" in
|
||||||
major)
|
major)
|
||||||
new_version="$((major+1)).0.0"
|
new_version="$((major+1)).0.0"
|
||||||
new_docs_version="$((major+1)).0"
|
|
||||||
;;
|
;;
|
||||||
minor)
|
minor)
|
||||||
new_version="$major.$((minor+1)).0"
|
new_version="$major.$((minor+1)).0"
|
||||||
new_docs_version="$major.$((minor+1))"
|
|
||||||
;;
|
;;
|
||||||
patch)
|
patch)
|
||||||
new_version="$major.$minor.$((patch+1))"
|
new_version="$major.$minor.$((patch+1))"
|
||||||
new_docs_version="$major.$minor"
|
|
||||||
;;
|
;;
|
||||||
devel)
|
devel)
|
||||||
new_version="$major.$((minor+1))"
|
new_version="$major.$((minor+1))"
|
||||||
new_docs_version="devel"
|
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo "Updating postgrest.cabal ..."
|
echo "Updating postgrest.cabal ..."
|
||||||
sed -i -E "s/^(version:\s+).*$/\1$new_version/" postgrest.cabal > /dev/null
|
sed -i -E "s/^(version:\s+).*$/\1$new_version/" postgrest.cabal > /dev/null
|
||||||
echo "Updating docs/conf.py ..."
|
|
||||||
sed -i -E "s/^(version = ).*$/\1\"$new_docs_version\"/" docs/conf.py > /dev/null
|
|
||||||
|
|
||||||
git add postgrest.cabal docs/conf.py > /dev/null
|
git add postgrest.cabal > /dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
today_date_for_changelog="$(date '+%Y-%m-%d')"
|
today_date_for_changelog="$(date '+%Y-%m-%d')"
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
{ buildToolbox
|
{ buildToolbox
|
||||||
, cabal-install
|
, cabal-install
|
||||||
, checkedShellScript
|
, checkedShellScript
|
||||||
, curl
|
|
||||||
, devCabalOptions
|
, devCabalOptions
|
||||||
, ghc
|
, ghc
|
||||||
, glibcLocales ? null
|
, glibcLocales ? null
|
||||||
@@ -28,7 +27,6 @@ let
|
|||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
${cabal-install}/bin/cabal v2-update
|
|
||||||
${withTools.withPg} -f test/spec/fixtures/load.sql \
|
${withTools.withPg} -f test/spec/fixtures/load.sql \
|
||||||
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec -- "''${_arg_leftovers[@]}"
|
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec -- "''${_arg_leftovers[@]}"
|
||||||
'';
|
'';
|
||||||
@@ -42,7 +40,6 @@ let
|
|||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
${cabal-install}/bin/cabal v2-update
|
|
||||||
# This makes nix-env -iA tests.doctests.bin work.
|
# This makes nix-env -iA tests.doctests.bin work.
|
||||||
export NIX_GHC=${postgrest.env.NIX_GHC}
|
export NIX_GHC=${postgrest.env.NIX_GHC}
|
||||||
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:doctests
|
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:doctests
|
||||||
@@ -57,7 +54,6 @@ let
|
|||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
${cabal-install}/bin/cabal v2-update
|
|
||||||
${withTools.withPg} -f test/spec/fixtures/load.sql \
|
${withTools.withPg} -f test/spec/fixtures/load.sql \
|
||||||
${runtimeShell} -c " \
|
${runtimeShell} -c " \
|
||||||
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec && \
|
${cabal-install}/bin/cabal v2-run ${devCabalOptions} test:spec && \
|
||||||
@@ -85,8 +81,7 @@ let
|
|||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
${cabal-install}/bin/cabal v2-update
|
${cabal-install}/bin/cabal v2-build ${devCabalOptions}
|
||||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
|
||||||
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/fixtures.sql \
|
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/fixtures.sql \
|
||||||
${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io "''${_arg_leftovers[@]}"
|
${ioTestPython}/bin/pytest --ignore=test/io/test_big_schema.py --ignore=test/io/test_replica.py -v test/io "''${_arg_leftovers[@]}"
|
||||||
'';
|
'';
|
||||||
@@ -101,8 +96,7 @@ let
|
|||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
${cabal-install}/bin/cabal v2-update
|
${cabal-install}/bin/cabal v2-build ${devCabalOptions}
|
||||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
|
||||||
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/big_schema.sql \
|
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} -f test/io/big_schema.sql \
|
||||||
${ioTestPython}/bin/pytest -v test/io/test_big_schema.py "''${_arg_leftovers[@]}"
|
${ioTestPython}/bin/pytest -v test/io/test_big_schema.py "''${_arg_leftovers[@]}"
|
||||||
'';
|
'';
|
||||||
@@ -117,8 +111,7 @@ let
|
|||||||
withEnv = postgrest.env;
|
withEnv = postgrest.env;
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
${cabal-install}/bin/cabal v2-update
|
${cabal-install}/bin/cabal v2-build ${devCabalOptions}
|
||||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest
|
|
||||||
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} --replica -f test/io/replica.sql \
|
${cabal-install}/bin/cabal v2-exec -- ${withTools.withPg} --replica -f test/io/replica.sql \
|
||||||
${ioTestPython}/bin/pytest -v test/io/test_replica.py "''${_arg_leftovers[@]}"
|
${ioTestPython}/bin/pytest -v test/io/test_replica.py "''${_arg_leftovers[@]}"
|
||||||
'';
|
'';
|
||||||
@@ -133,7 +126,6 @@ let
|
|||||||
withPath = [ jq ];
|
withPath = [ jq ];
|
||||||
}
|
}
|
||||||
''
|
''
|
||||||
${cabal-install}/bin/cabal v2-update
|
|
||||||
${withTools.withPg} -f test/spec/fixtures/load.sql \
|
${withTools.withPg} -f test/spec/fixtures/load.sql \
|
||||||
${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
|
${cabal-install}/bin/cabal v2-run ${devCabalOptions} --verbose=0 -- \
|
||||||
postgrest --dump-schema
|
postgrest --dump-schema
|
||||||
@@ -162,7 +154,6 @@ let
|
|||||||
rm -rf coverage/*
|
rm -rf coverage/*
|
||||||
|
|
||||||
# build once before running all the tests
|
# build once before running all the tests
|
||||||
${cabal-install}/bin/cabal v2-update
|
|
||||||
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec
|
${cabal-install}/bin/cabal v2-build ${devCabalOptions} exe:postgrest lib:postgrest test:spec
|
||||||
|
|
||||||
(
|
(
|
||||||
@@ -236,22 +227,6 @@ let
|
|||||||
sed -i 's|^module \(.*\):|module \1/|g' test/coverage.overlay
|
sed -i 's|^module \(.*\):|module \1/|g' test/coverage.overlay
|
||||||
'';
|
'';
|
||||||
|
|
||||||
testMemory =
|
|
||||||
checkedShellScript
|
|
||||||
{
|
|
||||||
name = "postgrest-test-memory";
|
|
||||||
docs = "Run the memory tests.";
|
|
||||||
workingDir = "/";
|
|
||||||
withEnv = postgrest.env;
|
|
||||||
withPath = [ curl ];
|
|
||||||
}
|
|
||||||
''
|
|
||||||
${cabal-install}/bin/cabal v2-update
|
|
||||||
${cabal-install}/bin/cabal --builddir="dist-prof" v2-build --enable-profiling --disable-shared exe:postgrest
|
|
||||||
${cabal-install}/bin/cabal --builddir="dist-prof" v2-exec -- ${withTools.withPg} -f test/spec/fixtures/load.sql \
|
|
||||||
test/memory/memory-tests.sh
|
|
||||||
'';
|
|
||||||
|
|
||||||
in
|
in
|
||||||
buildToolbox
|
buildToolbox
|
||||||
{
|
{
|
||||||
@@ -266,7 +241,6 @@ buildToolbox
|
|||||||
testReplica
|
testReplica
|
||||||
dumpSchema
|
dumpSchema
|
||||||
coverage
|
coverage
|
||||||
coverageDraftOverlay
|
coverageDraftOverlay;
|
||||||
testMemory;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 13.0.8
|
version: 12.2.12
|
||||||
synopsis: REST API for any Postgres database
|
synopsis: REST API for any Postgres database
|
||||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
for tables, views, and functions, supporting all HTTP methods that security
|
for tables, views, and functions, supporting all HTTP methods that security
|
||||||
@@ -16,13 +16,14 @@ extra-source-files: CHANGELOG.md
|
|||||||
cabal-version: >= 1.10
|
cabal-version: >= 1.10
|
||||||
|
|
||||||
tested-with:
|
tested-with:
|
||||||
-- nix
|
|
||||||
GHC == 9.4.8
|
|
||||||
-- cabal on Ubuntu
|
|
||||||
-- stack on FreeBSD, MacOS, Ubuntu, Windows
|
-- stack on FreeBSD, MacOS, Ubuntu, Windows
|
||||||
, GHC == 9.6.7
|
GHC == 9.4.5
|
||||||
|
-- nix, cabal on Ubuntu (arm)
|
||||||
|
, GHC == 9.4.8
|
||||||
-- cabal on Ubuntu
|
-- cabal on Ubuntu
|
||||||
, GHC == 9.8.4
|
, GHC == 9.6.4
|
||||||
|
-- cabal on Ubuntu
|
||||||
|
, GHC == 9.8.2
|
||||||
|
|
||||||
source-repository head
|
source-repository head
|
||||||
type: git
|
type: git
|
||||||
@@ -47,8 +48,6 @@ library
|
|||||||
PostgREST.App
|
PostgREST.App
|
||||||
PostgREST.AppState
|
PostgREST.AppState
|
||||||
PostgREST.Auth
|
PostgREST.Auth
|
||||||
PostgREST.Auth.JwtCache
|
|
||||||
PostgREST.Auth.Types
|
|
||||||
PostgREST.CLI
|
PostgREST.CLI
|
||||||
PostgREST.Config
|
PostgREST.Config
|
||||||
PostgREST.Config.Database
|
PostgREST.Config.Database
|
||||||
@@ -67,7 +66,6 @@ library
|
|||||||
PostgREST.Logger
|
PostgREST.Logger
|
||||||
PostgREST.MediaType
|
PostgREST.MediaType
|
||||||
PostgREST.Metrics
|
PostgREST.Metrics
|
||||||
PostgREST.Network
|
|
||||||
PostgREST.Observation
|
PostgREST.Observation
|
||||||
PostgREST.Query
|
PostgREST.Query
|
||||||
PostgREST.Query.QueryBuilder
|
PostgREST.Query.QueryBuilder
|
||||||
@@ -89,43 +87,44 @@ library
|
|||||||
PostgREST.Response.GucHeader
|
PostgREST.Response.GucHeader
|
||||||
PostgREST.Response.Performance
|
PostgREST.Response.Performance
|
||||||
PostgREST.Version
|
PostgREST.Version
|
||||||
|
other-modules: Paths_postgrest
|
||||||
build-depends: base >= 4.9 && < 4.20
|
build-depends: base >= 4.9 && < 4.20
|
||||||
, HTTP >= 4000.3.7 && < 4000.5
|
, HTTP >= 4000.3.7 && < 4000.5
|
||||||
, Ranged-sets >= 0.3 && < 0.5
|
, Ranged-sets >= 0.3 && < 0.5
|
||||||
, aeson >= 2.0.3 && < 2.3
|
, aeson >= 2.0.3 && < 2.3
|
||||||
, auto-update >= 0.1.4 && < 0.3
|
, auto-update >= 0.1.4 && < 0.2
|
||||||
, base64-bytestring >= 1 && < 1.3
|
, base64-bytestring >= 1 && < 1.3
|
||||||
, bytestring >= 0.10.8 && < 0.13
|
, bytestring >= 0.10.8 && < 0.13
|
||||||
, cache >= 0.1.3 && < 0.2.0
|
, cache >= 0.1.3 && < 0.2.0
|
||||||
, case-insensitive >= 1.2 && < 1.3
|
, case-insensitive >= 1.2 && < 1.3
|
||||||
, cassava >= 0.4.5 && < 0.6
|
, cassava >= 0.4.5 && < 0.6
|
||||||
, clock >= 0.8.3 && < 0.9.0
|
, clock >= 0.8.3 && < 0.9.0
|
||||||
, configurator-pg >= 0.2.11 && < 0.3
|
, configurator-pg >= 0.2 && < 0.3
|
||||||
, containers >= 0.5.7 && < 0.7
|
, containers >= 0.5.7 && < 0.7
|
||||||
, cookie >= 0.4.2 && < 0.6
|
, contravariant-extras >= 0.3.3 && < 0.4
|
||||||
|
, cookie >= 0.4.2 && < 0.5
|
||||||
, directory >= 1.2.6 && < 1.4
|
, directory >= 1.2.6 && < 1.4
|
||||||
, either >= 4.4.1 && < 5.1
|
, either >= 4.4.1 && < 5.1
|
||||||
, extra >= 1.7.0 && < 2.0
|
, extra >= 1.7.0 && < 2.0
|
||||||
, fuzzyset >= 0.2.4 && < 0.3
|
, fuzzyset >= 0.2.4 && < 0.3
|
||||||
|
, gitrev >= 1.2 && < 1.4
|
||||||
, hasql >= 1.6.1.1 && < 1.7
|
, hasql >= 1.6.1.1 && < 1.7
|
||||||
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
, hasql-dynamic-statements >= 0.3.1 && < 0.4
|
||||||
, hasql-notifications >= 0.2.2.2 && < 0.2.3
|
, hasql-notifications >= 0.2.2.2 && < 0.2.3
|
||||||
, hasql-pool >= 1.0.1 && < 1.1
|
, hasql-pool >= 1.0.1 && < 1.1
|
||||||
, hasql-transaction >= 1.0.1 && < 1.2
|
, hasql-transaction >= 1.0.1 && < 1.1
|
||||||
, heredoc >= 0.2 && < 0.3
|
, heredoc >= 0.2 && < 0.3
|
||||||
, http-types >= 0.12.2 && < 0.13
|
, http-types >= 0.12.2 && < 0.13
|
||||||
, insert-ordered-containers >= 0.2.2 && < 0.3
|
, insert-ordered-containers >= 0.2.2 && < 0.3
|
||||||
, iproute >= 1.7.0 && < 1.8
|
, interpolatedstring-perl6 >= 1 && < 1.1
|
||||||
, jose-jwt >= 0.9.6 && < 0.11
|
, jose >= 0.8.5.1 && < 0.12
|
||||||
, lens >= 4.14 && < 5.4
|
, lens >= 4.14 && < 5.3
|
||||||
, lens-aeson >= 1.0.1 && < 1.3
|
, lens-aeson >= 1.0.1 && < 1.3
|
||||||
, mtl >= 2.2.2 && < 2.4
|
, mtl >= 2.2.2 && < 2.4
|
||||||
, neat-interpolation >= 0.5 && < 0.6
|
, network >= 2.6 && < 3.2
|
||||||
, network >= 2.6 && < 3.3
|
|
||||||
, network-uri >= 2.6.1 && < 2.8
|
, network-uri >= 2.6.1 && < 2.8
|
||||||
, optparse-applicative >= 0.13 && < 0.19
|
, optparse-applicative >= 0.13 && < 0.19
|
||||||
, parsec >= 3.1.11 && < 3.2
|
, parsec >= 3.1.11 && < 3.2
|
||||||
-- Technically unused, can be removed after updating to hasql >= 1.7
|
|
||||||
, postgresql-libpq >= 0.10
|
, postgresql-libpq >= 0.10
|
||||||
, prometheus-client >= 1.1.1 && < 1.2.0
|
, prometheus-client >= 1.1.1 && < 1.2.0
|
||||||
, protolude >= 0.3.1 && < 0.4
|
, protolude >= 0.3.1 && < 0.4
|
||||||
@@ -149,7 +148,7 @@ library
|
|||||||
-- for unix sockets; this is tested in test/io/test_io.py. See
|
-- for unix sockets; this is tested in test/io/test_io.py. See
|
||||||
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
|
-- https://github.com/kazu-yamamoto/logger/commit/3a71ca70afdbb93d4ecf0083eeba1fbbbcab3fc3
|
||||||
, wai-logger >= 2.4.0
|
, wai-logger >= 2.4.0
|
||||||
, warp >= 3.3.19 && < 3.5
|
, warp >= 3.3.19 && < 3.4
|
||||||
-- -fno-spec-constr may help keep compile time memory use in check,
|
-- -fno-spec-constr may help keep compile time memory use in check,
|
||||||
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
-- see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304
|
||||||
-- -optP-Wno-nonportable-include-path
|
-- -optP-Wno-nonportable-include-path
|
||||||
@@ -205,7 +204,7 @@ test-suite spec
|
|||||||
Feature.Auth.AuthSpec
|
Feature.Auth.AuthSpec
|
||||||
Feature.Auth.BinaryJwtSecretSpec
|
Feature.Auth.BinaryJwtSecretSpec
|
||||||
Feature.Auth.NoAnonSpec
|
Feature.Auth.NoAnonSpec
|
||||||
Feature.Auth.NoJwtSecretSpec
|
Feature.Auth.NoJwtSpec
|
||||||
Feature.ConcurrentSpec
|
Feature.ConcurrentSpec
|
||||||
Feature.CorsSpec
|
Feature.CorsSpec
|
||||||
Feature.ExtraSearchPathSpec
|
Feature.ExtraSearchPathSpec
|
||||||
@@ -228,6 +227,7 @@ test-suite spec
|
|||||||
Feature.Query.ErrorSpec
|
Feature.Query.ErrorSpec
|
||||||
Feature.Query.InsertSpec
|
Feature.Query.InsertSpec
|
||||||
Feature.Query.JsonOperatorSpec
|
Feature.Query.JsonOperatorSpec
|
||||||
|
Feature.Query.LimitedMutationSpec
|
||||||
Feature.Query.MultipleSchemaSpec
|
Feature.Query.MultipleSchemaSpec
|
||||||
Feature.Query.NullsStripSpec
|
Feature.Query.NullsStripSpec
|
||||||
Feature.Query.PgSafeUpdateSpec
|
Feature.Query.PgSafeUpdateSpec
|
||||||
@@ -253,19 +253,19 @@ test-suite spec
|
|||||||
, aeson >= 2.0.3 && < 2.3
|
, aeson >= 2.0.3 && < 2.3
|
||||||
, aeson-qq >= 0.8.1 && < 0.9
|
, aeson-qq >= 0.8.1 && < 0.9
|
||||||
, async >= 2.1.1 && < 2.3
|
, async >= 2.1.1 && < 2.3
|
||||||
|
, auto-update >= 0.1.4 && < 0.2
|
||||||
, base64-bytestring >= 1 && < 1.3
|
, base64-bytestring >= 1 && < 1.3
|
||||||
, bytestring >= 0.10.8 && < 0.13
|
, bytestring >= 0.10.8 && < 0.13
|
||||||
, case-insensitive >= 1.2 && < 1.3
|
, case-insensitive >= 1.2 && < 1.3
|
||||||
, containers >= 0.5.7 && < 0.7
|
, containers >= 0.5.7 && < 0.7
|
||||||
, hasql-pool >= 1.0.1 && < 1.1
|
, hasql-pool >= 1.0.1 && < 1.1
|
||||||
, hasql-transaction >= 1.0.1 && < 1.2
|
, hasql-transaction >= 1.0.1 && < 1.1
|
||||||
, heredoc >= 0.2 && < 0.3
|
, heredoc >= 0.2 && < 0.3
|
||||||
, hspec >= 2.3 && < 2.12
|
, hspec >= 2.3 && < 2.12
|
||||||
, hspec-wai >= 0.10 && < 0.12
|
, hspec-wai >= 0.10 && < 0.12
|
||||||
, hspec-wai-json >= 0.10 && < 0.12
|
, hspec-wai-json >= 0.10 && < 0.12
|
||||||
, http-types >= 0.12.3 && < 0.13
|
, http-types >= 0.12.3 && < 0.13
|
||||||
, jose-jwt >= 0.9.6 && < 0.11
|
, lens >= 4.14 && < 5.3
|
||||||
, lens >= 4.14 && < 5.4
|
|
||||||
, lens-aeson >= 1.0.1 && < 1.3
|
, lens-aeson >= 1.0.1 && < 1.3
|
||||||
, monad-control >= 1.0.1 && < 1.1
|
, monad-control >= 1.0.1 && < 1.1
|
||||||
, postgrest
|
, postgrest
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
# We highly recommend that use the PostgREST binary cache by installing cachix
|
# We highly recommend that use the PostgREST binary cache by installing cachix
|
||||||
# (https://app.cachix.org/) and running `cachix use postgrest`.
|
# (https://app.cachix.org/) and running `cachix use postgrest`.
|
||||||
{ docker ? false
|
{ docker ? false
|
||||||
|
, memory ? false
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
postgrest =
|
postgrest =
|
||||||
@@ -22,12 +23,14 @@ let
|
|||||||
postgrest.devTools
|
postgrest.devTools
|
||||||
postgrest.docs
|
postgrest.docs
|
||||||
postgrest.loadtest
|
postgrest.loadtest
|
||||||
postgrest.release
|
postgrest.nixpkgsTools
|
||||||
postgrest.style
|
postgrest.style
|
||||||
postgrest.tests
|
postgrest.tests
|
||||||
postgrest.withTools
|
postgrest.withTools
|
||||||
|
postgrest.release
|
||||||
]
|
]
|
||||||
++ lib.optional docker postgrest.docker;
|
++ lib.optional docker postgrest.docker
|
||||||
|
++ lib.optional memory postgrest.memory;
|
||||||
|
|
||||||
in
|
in
|
||||||
lib.overrideDerivation postgrest.env (
|
lib.overrideDerivation postgrest.env (
|
||||||
|
|||||||
@@ -11,17 +11,19 @@ import qualified Network.Wai.Handler.Warp as Warp
|
|||||||
|
|
||||||
import Control.Monad.Extra (whenJust)
|
import Control.Monad.Extra (whenJust)
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
|
|
||||||
import Network.Socket
|
import Network.Socket
|
||||||
import Network.Socket.ByteString
|
import Network.Socket.ByteString
|
||||||
|
|
||||||
import PostgREST.AppState (AppState)
|
import PostgREST.AppState (AppState)
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..))
|
||||||
import PostgREST.MediaType (MediaType (..), toContentType)
|
|
||||||
import PostgREST.Metrics (metricsToText)
|
import PostgREST.Metrics (metricsToText)
|
||||||
import PostgREST.Network (resolveHost)
|
|
||||||
import PostgREST.Observation (Observation (..))
|
import PostgREST.Observation (Observation (..))
|
||||||
|
|
||||||
import qualified PostgREST.AppState as AppState
|
import qualified PostgREST.AppState as AppState
|
||||||
|
import qualified PostgREST.Config as Config
|
||||||
|
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -29,8 +31,7 @@ runAdmin :: AppState -> Warp.Settings -> IO ()
|
|||||||
runAdmin appState settings = do
|
runAdmin appState settings = do
|
||||||
AppConfig{configAdminServerPort} <- AppState.getConfig appState
|
AppConfig{configAdminServerPort} <- AppState.getConfig appState
|
||||||
whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do
|
whenJust (AppState.getSocketAdmin appState) $ \adminSocket -> do
|
||||||
host <- resolveHost adminSocket
|
observer $ AdminStartObs configAdminServerPort
|
||||||
observer $ AdminStartObs host configAdminServerPort
|
|
||||||
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
|
void . forkIO $ Warp.runSettingsSocket settings adminSocket adminApp
|
||||||
where
|
where
|
||||||
adminApp = admin appState
|
adminApp = admin appState
|
||||||
@@ -54,12 +55,18 @@ admin appState req respond = do
|
|||||||
| otherwise = HTTP.status500
|
| otherwise = HTTP.status500
|
||||||
in
|
in
|
||||||
respond $ Wai.responseLBS status [] mempty
|
respond $ Wai.responseLBS status [] mempty
|
||||||
|
["config"] -> do
|
||||||
|
config@Config.AppConfig{configAdminServerConfigEnabled} <- AppState.getConfig appState
|
||||||
|
if configAdminServerConfigEnabled then
|
||||||
|
respond $ Wai.responseLBS HTTP.status200 [] (LBS.fromStrict $ encodeUtf8 $ Config.toText config)
|
||||||
|
else
|
||||||
|
respond $ Wai.responseLBS HTTP.status404 [] mempty
|
||||||
["schema_cache"] -> do
|
["schema_cache"] -> do
|
||||||
sCache <- AppState.getSchemaCache appState
|
sCache <- AppState.getSchemaCache appState
|
||||||
respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache)
|
respond $ Wai.responseLBS HTTP.status200 [] (maybe mempty JSON.encode sCache)
|
||||||
["metrics"] -> do
|
["metrics"] -> do
|
||||||
mets <- metricsToText
|
mets <- metricsToText
|
||||||
respond $ Wai.responseLBS HTTP.status200 [toContentType MTTextPlain] mets -- Content-Type is required for prometheus compliance
|
respond $ Wai.responseLBS HTTP.status200 [] mets
|
||||||
_ ->
|
_ ->
|
||||||
respond $ Wai.responseLBS HTTP.status404 [] mempty
|
respond $ Wai.responseLBS HTTP.status404 [] mempty
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ Description : PostgREST functions to translate HTTP request to a domain type cal
|
|||||||
-}
|
-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
-- TODO: This module shouldn't depend on SchemaCache
|
||||||
module PostgREST.ApiRequest
|
module PostgREST.ApiRequest
|
||||||
( ApiRequest(..)
|
( ApiRequest(..)
|
||||||
, InvokeMethod(..)
|
, InvokeMethod(..)
|
||||||
@@ -13,7 +14,6 @@ module PostgREST.ApiRequest
|
|||||||
, DbAction(..)
|
, DbAction(..)
|
||||||
, Payload(..)
|
, Payload(..)
|
||||||
, userApiRequest
|
, userApiRequest
|
||||||
, userPreferences
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
@@ -44,16 +44,16 @@ import Network.Wai.Parse (parseHttpAccept)
|
|||||||
import Web.Cookie (parseCookies)
|
import Web.Cookie (parseCookies)
|
||||||
|
|
||||||
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
|
import PostgREST.ApiRequest.QueryParams (QueryParams (..))
|
||||||
|
import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
||||||
|
RangeError (..))
|
||||||
import PostgREST.Config (AppConfig (..),
|
import PostgREST.Config (AppConfig (..),
|
||||||
OpenAPIMode (..))
|
OpenAPIMode (..))
|
||||||
import PostgREST.Config.Database (TimezoneNames)
|
|
||||||
import PostgREST.Error (ApiRequestError (..),
|
|
||||||
RangeError (..))
|
|
||||||
import PostgREST.MediaType (MediaType (..))
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
convertToLimitZeroRange,
|
convertToLimitZeroRange,
|
||||||
hasLimitZero,
|
hasLimitZero,
|
||||||
rangeRequested)
|
rangeRequested)
|
||||||
|
import PostgREST.SchemaCache (SchemaCache (..))
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier (..),
|
QualifiedIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
@@ -128,8 +128,8 @@ data ApiRequest = ApiRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
-- | Examines HTTP request and translates it into user intent.
|
-- | Examines HTTP request and translates it into user intent.
|
||||||
userApiRequest :: AppConfig -> Preferences.Preferences -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
userApiRequest :: AppConfig -> Request -> RequestBody -> SchemaCache -> Either ApiRequestError ApiRequest
|
||||||
userApiRequest conf prefs req reqBody = do
|
userApiRequest conf req reqBody sCache = do
|
||||||
resource <- getResource conf $ pathInfo req
|
resource <- getResource conf $ pathInfo req
|
||||||
(schema, negotiatedByProfile) <- getSchema conf hdrs method
|
(schema, negotiatedByProfile) <- getSchema conf hdrs method
|
||||||
act <- getAction resource schema method
|
act <- getAction resource schema method
|
||||||
@@ -141,7 +141,7 @@ userApiRequest conf prefs req reqBody = do
|
|||||||
, iRange = ranges
|
, iRange = ranges
|
||||||
, iTopLevelRange = topLevelRange
|
, iTopLevelRange = topLevelRange
|
||||||
, iPayload = payload
|
, iPayload = payload
|
||||||
, iPreferences = prefs
|
, iPreferences = Preferences.fromHeaders (configDbTxAllowOverride conf) (dbTimezones sCache) hdrs
|
||||||
, iQueryParams = qPrms
|
, iQueryParams = qPrms
|
||||||
, iColumns = columns
|
, iColumns = columns
|
||||||
, iHeaders = iHdrs
|
, iHeaders = iHdrs
|
||||||
@@ -162,21 +162,15 @@ userApiRequest conf prefs req reqBody = do
|
|||||||
contentMediaType = maybe MTApplicationJSON MediaType.decodeMediaType $ lookupHeader "content-type"
|
contentMediaType = maybe MTApplicationJSON MediaType.decodeMediaType $ lookupHeader "content-type"
|
||||||
actIsInvokeSafe x = case x of {ActDb (ActRoutine _ (InvRead _)) -> True; _ -> False}
|
actIsInvokeSafe x = case x of {ActDb (ActRoutine _ (InvRead _)) -> True; _ -> False}
|
||||||
|
|
||||||
-- | Parses the Prefer header
|
|
||||||
userPreferences :: AppConfig -> Request -> TimezoneNames -> Preferences.Preferences
|
|
||||||
userPreferences conf req timezones = Preferences.fromHeaders (configDbTxAllowOverride conf) timezones $ requestHeaders req
|
|
||||||
|
|
||||||
getResource :: AppConfig -> [Text] -> Either ApiRequestError Resource
|
getResource :: AppConfig -> [Text] -> Either ApiRequestError Resource
|
||||||
getResource AppConfig{configOpenApiMode, configDbRootSpec} = \case
|
getResource AppConfig{configOpenApiMode, configDbRootSpec} = \case
|
||||||
[] ->
|
[] -> case configDbRootSpec of
|
||||||
case (configOpenApiMode,configDbRootSpec) of
|
Just (QualifiedIdentifier _ pathName) -> Right $ ResourceRoutine pathName
|
||||||
(OADisabled,_) -> Left OpenAPIDisabled
|
Nothing | configOpenApiMode == OADisabled -> Left NotFound
|
||||||
(_, Just qi) -> Right $ ResourceRoutine (qiName qi)
|
| otherwise -> Right ResourceSchema
|
||||||
(_, Nothing) -> Right ResourceSchema
|
|
||||||
|
|
||||||
[table] -> Right $ ResourceRelation table
|
[table] -> Right $ ResourceRelation table
|
||||||
["rpc", pName] -> Right $ ResourceRoutine pName
|
["rpc", pName] -> Right $ ResourceRoutine pName
|
||||||
_ -> Left InvalidResourcePath
|
_ -> Left NotFound
|
||||||
|
|
||||||
getAction :: Resource -> Schema -> ByteString -> Either ApiRequestError Action
|
getAction :: Resource -> Schema -> ByteString -> Either ApiRequestError Action
|
||||||
getAction resource schema method =
|
getAction resource schema method =
|
||||||
@@ -207,7 +201,7 @@ getAction resource schema method =
|
|||||||
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
|
getSchema :: AppConfig -> RequestHeaders -> ByteString -> Either ApiRequestError (Schema, Bool)
|
||||||
getSchema AppConfig{configDbSchemas} hdrs method = do
|
getSchema AppConfig{configDbSchemas} hdrs method = do
|
||||||
case profile of
|
case profile of
|
||||||
Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema p $ toList configDbSchemas
|
Just p | p `notElem` configDbSchemas -> Left $ UnacceptableSchema $ toList configDbSchemas
|
||||||
| otherwise -> Right (p, True)
|
| otherwise -> Right (p, True)
|
||||||
Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated
|
Nothing -> Right (defaultSchema, length configDbSchemas /= 1) -- if we have many schemas, assume the default schema was negotiated
|
||||||
where
|
where
|
||||||
@@ -224,8 +218,9 @@ getSchema AppConfig{configDbSchemas} hdrs method = do
|
|||||||
lookupHeader = flip lookup hdrs
|
lookupHeader = flip lookup hdrs
|
||||||
|
|
||||||
getRanges :: ByteString -> QueryParams -> RequestHeaders -> Either ApiRequestError (NonnegRange, HM.HashMap Text NonnegRange)
|
getRanges :: ByteString -> QueryParams -> RequestHeaders -> Either ApiRequestError (NonnegRange, HM.HashMap Text NonnegRange)
|
||||||
getRanges method QueryParams{qsRanges} hdrs
|
getRanges method QueryParams{qsOrder,qsRanges} hdrs
|
||||||
| isInvalidRange = Left $ InvalidRange (if rangeIsEmpty headerRange then LowerGTUpper else NegativeLimit)
|
| isInvalidRange = Left $ InvalidRange (if rangeIsEmpty headerRange then LowerGTUpper else NegativeLimit)
|
||||||
|
| method `elem` ["PATCH", "DELETE"] && not (null qsRanges) && null qsOrder = Left LimitNoOrderError
|
||||||
| method == "PUT" && topLevelRange /= allRange = Left PutLimitNotAllowedError
|
| method == "PUT" && topLevelRange /= allRange = Left PutLimitNotAllowedError
|
||||||
| otherwise = Right (topLevelRange, ranges)
|
| otherwise = Right (topLevelRange, ranges)
|
||||||
where
|
where
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
-- Module: PostgREST.ApiRequest.Preferences
|
-- Module: PostgREST.ApiRequest.Preferences
|
||||||
-- Description: Track client preferences to be employed when processing requests
|
-- Description: Track client preferences to be employed when processing requests
|
||||||
--
|
--
|
||||||
-- Track client preferences set in HTTP 'Prefer' headers according to RFC7240[1].
|
-- Track client prefences set in HTTP 'Prefer' headers according to RFC7240[1].
|
||||||
--
|
--
|
||||||
-- [1] https://datatracker.ietf.org/doc/html/rfc7240
|
-- [1] https://datatracker.ietf.org/doc/html/rfc7240
|
||||||
--
|
--
|
||||||
@@ -12,6 +12,7 @@ module PostgREST.ApiRequest.Preferences
|
|||||||
, PreferCount(..)
|
, PreferCount(..)
|
||||||
, PreferHandling(..)
|
, PreferHandling(..)
|
||||||
, PreferMissing(..)
|
, PreferMissing(..)
|
||||||
|
, PreferParameters(..)
|
||||||
, PreferRepresentation(..)
|
, PreferRepresentation(..)
|
||||||
, PreferResolution(..)
|
, PreferResolution(..)
|
||||||
, PreferTransaction(..)
|
, PreferTransaction(..)
|
||||||
@@ -36,6 +37,7 @@ import Protolude
|
|||||||
-- >>> import Text.Pretty.Simple (pPrint)
|
-- >>> import Text.Pretty.Simple (pPrint)
|
||||||
-- >>> deriving instance Show PreferResolution
|
-- >>> deriving instance Show PreferResolution
|
||||||
-- >>> deriving instance Show PreferRepresentation
|
-- >>> deriving instance Show PreferRepresentation
|
||||||
|
-- >>> deriving instance Show PreferParameters
|
||||||
-- >>> deriving instance Show PreferCount
|
-- >>> deriving instance Show PreferCount
|
||||||
-- >>> deriving instance Show PreferTransaction
|
-- >>> deriving instance Show PreferTransaction
|
||||||
-- >>> deriving instance Show PreferMissing
|
-- >>> deriving instance Show PreferMissing
|
||||||
@@ -49,6 +51,7 @@ data Preferences
|
|||||||
= Preferences
|
= Preferences
|
||||||
{ preferResolution :: Maybe PreferResolution
|
{ preferResolution :: Maybe PreferResolution
|
||||||
, preferRepresentation :: Maybe PreferRepresentation
|
, preferRepresentation :: Maybe PreferRepresentation
|
||||||
|
, preferParameters :: Maybe PreferParameters
|
||||||
, preferCount :: Maybe PreferCount
|
, preferCount :: Maybe PreferCount
|
||||||
, preferTransaction :: Maybe PreferTransaction
|
, preferTransaction :: Maybe PreferTransaction
|
||||||
, preferMissing :: Maybe PreferMissing
|
, preferMissing :: Maybe PreferMissing
|
||||||
@@ -68,6 +71,7 @@ data Preferences
|
|||||||
-- Preferences
|
-- Preferences
|
||||||
-- { preferResolution = Just IgnoreDuplicates
|
-- { preferResolution = Just IgnoreDuplicates
|
||||||
-- , preferRepresentation = Nothing
|
-- , preferRepresentation = Nothing
|
||||||
|
-- , preferParameters = Nothing
|
||||||
-- , preferCount = Just ExactCount
|
-- , preferCount = Just ExactCount
|
||||||
-- , preferTransaction = Nothing
|
-- , preferTransaction = Nothing
|
||||||
-- , preferMissing = Nothing
|
-- , preferMissing = Nothing
|
||||||
@@ -85,6 +89,7 @@ data Preferences
|
|||||||
-- Preferences
|
-- Preferences
|
||||||
-- { preferResolution = Just IgnoreDuplicates
|
-- { preferResolution = Just IgnoreDuplicates
|
||||||
-- , preferRepresentation = Nothing
|
-- , preferRepresentation = Nothing
|
||||||
|
-- , preferParameters = Nothing
|
||||||
-- , preferCount = Just ExactCount
|
-- , preferCount = Just ExactCount
|
||||||
-- , preferTransaction = Nothing
|
-- , preferTransaction = Nothing
|
||||||
-- , preferMissing = Just ApplyNulls
|
-- , preferMissing = Just ApplyNulls
|
||||||
@@ -117,6 +122,7 @@ data Preferences
|
|||||||
-- Preferences
|
-- Preferences
|
||||||
-- { preferResolution = Nothing
|
-- { preferResolution = Nothing
|
||||||
-- , preferRepresentation = Just Full
|
-- , preferRepresentation = Just Full
|
||||||
|
-- , preferParameters = Nothing
|
||||||
-- , preferCount = Just ExactCount
|
-- , preferCount = Just ExactCount
|
||||||
-- , preferTransaction = Just Commit
|
-- , preferTransaction = Just Commit
|
||||||
-- , preferMissing = Just ApplyDefaults
|
-- , preferMissing = Just ApplyDefaults
|
||||||
@@ -131,6 +137,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
|
|||||||
Preferences
|
Preferences
|
||||||
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
|
{ preferResolution = parsePrefs [MergeDuplicates, IgnoreDuplicates]
|
||||||
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
|
, preferRepresentation = parsePrefs [Full, None, HeadersOnly]
|
||||||
|
, preferParameters = parsePrefs [SingleObject]
|
||||||
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
, preferCount = parsePrefs [ExactCount, PlannedCount, EstimatedCount]
|
||||||
, preferTransaction = if allowTxDbOverride then parsePrefs [Commit, Rollback] else Nothing
|
, preferTransaction = if allowTxDbOverride then parsePrefs [Commit, Rollback] else Nothing
|
||||||
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
|
, preferMissing = parsePrefs [ApplyDefaults, ApplyNulls]
|
||||||
@@ -144,6 +151,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
|
|||||||
mapToHeadVal = map toHeaderValue
|
mapToHeadVal = map toHeaderValue
|
||||||
acceptedPrefs = mapToHeadVal [MergeDuplicates, IgnoreDuplicates] ++
|
acceptedPrefs = mapToHeadVal [MergeDuplicates, IgnoreDuplicates] ++
|
||||||
mapToHeadVal [Full, None, HeadersOnly] ++
|
mapToHeadVal [Full, None, HeadersOnly] ++
|
||||||
|
mapToHeadVal [SingleObject] ++
|
||||||
mapToHeadVal [ExactCount, PlannedCount, EstimatedCount] ++
|
mapToHeadVal [ExactCount, PlannedCount, EstimatedCount] ++
|
||||||
mapToHeadVal [Commit, Rollback] ++
|
mapToHeadVal [Commit, Rollback] ++
|
||||||
mapToHeadVal [ApplyDefaults, ApplyNulls] ++
|
mapToHeadVal [ApplyDefaults, ApplyNulls] ++
|
||||||
@@ -171,7 +179,7 @@ fromHeaders allowTxDbOverride acceptedTzNames headers =
|
|||||||
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
|
prefMap = Map.fromList . fmap (\pref -> (toHeaderValue pref, pref))
|
||||||
|
|
||||||
prefAppliedHeader :: Preferences -> Maybe HTTP.Header
|
prefAppliedHeader :: Preferences -> Maybe HTTP.Header
|
||||||
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone, preferMaxAffected } =
|
prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferParameters, preferCount, preferTransaction, preferMissing, preferHandling, preferTimezone, preferMaxAffected } =
|
||||||
if null prefsVals
|
if null prefsVals
|
||||||
then Nothing
|
then Nothing
|
||||||
else Just (HTTP.hPreferenceApplied, combined)
|
else Just (HTTP.hPreferenceApplied, combined)
|
||||||
@@ -181,6 +189,7 @@ prefAppliedHeader Preferences {preferResolution, preferRepresentation, preferCou
|
|||||||
toHeaderValue <$> preferResolution
|
toHeaderValue <$> preferResolution
|
||||||
, toHeaderValue <$> preferMissing
|
, toHeaderValue <$> preferMissing
|
||||||
, toHeaderValue <$> preferRepresentation
|
, toHeaderValue <$> preferRepresentation
|
||||||
|
, toHeaderValue <$> preferParameters
|
||||||
, toHeaderValue <$> preferCount
|
, toHeaderValue <$> preferCount
|
||||||
, toHeaderValue <$> preferTransaction
|
, toHeaderValue <$> preferTransaction
|
||||||
, toHeaderValue <$> preferHandling
|
, toHeaderValue <$> preferHandling
|
||||||
@@ -222,6 +231,15 @@ instance ToHeaderValue PreferRepresentation where
|
|||||||
toHeaderValue None = "return=minimal"
|
toHeaderValue None = "return=minimal"
|
||||||
toHeaderValue HeadersOnly = "return=headers-only"
|
toHeaderValue HeadersOnly = "return=headers-only"
|
||||||
|
|
||||||
|
-- | How to pass parameters to stored procedures.
|
||||||
|
-- TODO: deprecated. Remove on next major version.
|
||||||
|
data PreferParameters
|
||||||
|
= SingleObject -- ^ Pass all parameters as a single json object to a stored procedure.
|
||||||
|
deriving Eq
|
||||||
|
|
||||||
|
instance ToHeaderValue PreferParameters where
|
||||||
|
toHeaderValue SingleObject = "params=single-object"
|
||||||
|
|
||||||
-- | How to determine the count of (expected) results
|
-- | How to determine the count of (expected) results
|
||||||
data PreferCount
|
data PreferCount
|
||||||
= ExactCount -- ^ Exact count (slower).
|
= ExactCount -- ^ Exact count (slower).
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import PostgREST.SchemaCache.Identifiers (FieldName)
|
|||||||
import PostgREST.ApiRequest.Types (AggregateFunction (..),
|
import PostgREST.ApiRequest.Types (AggregateFunction (..),
|
||||||
EmbedParam (..), EmbedPath, Field,
|
EmbedParam (..), EmbedPath, Field,
|
||||||
Filter (..), FtsOperator (..),
|
Filter (..), FtsOperator (..),
|
||||||
Hint, IsVal (..), JoinType (..),
|
Hint, JoinType (..),
|
||||||
JsonOperand (..),
|
JsonOperand (..),
|
||||||
JsonOperation (..), JsonPath,
|
JsonOperation (..), JsonPath,
|
||||||
ListVal, LogicOperator (..),
|
ListVal, LogicOperator (..),
|
||||||
@@ -54,11 +54,10 @@ import PostgREST.ApiRequest.Types (AggregateFunction (..),
|
|||||||
OpQuantifier (..), Operation (..),
|
OpQuantifier (..), Operation (..),
|
||||||
OrderDirection (..),
|
OrderDirection (..),
|
||||||
OrderNulls (..), OrderTerm (..),
|
OrderNulls (..), OrderTerm (..),
|
||||||
QuantOperator (..),
|
QPError (..), QuantOperator (..),
|
||||||
SelectItem (..),
|
SelectItem (..),
|
||||||
SimpleOperator (..), SingleVal)
|
SimpleOperator (..), SingleVal,
|
||||||
|
TrileanVal (..))
|
||||||
import PostgREST.Error (QPError (..))
|
|
||||||
|
|
||||||
import Protolude hiding (Sum, try)
|
import Protolude hiding (Sum, try)
|
||||||
|
|
||||||
@@ -641,7 +640,7 @@ pOpExpr pSVal = do
|
|||||||
pOperation = pIn <|> pIs <|> pIsDist <|> try pFts <|> try pSimpleOp <|> try pQuantOp <?> "operator (eq, gt, ...)"
|
pOperation = pIn <|> pIs <|> pIsDist <|> try pFts <|> try pSimpleOp <|> try pQuantOp <?> "operator (eq, gt, ...)"
|
||||||
|
|
||||||
pIn = In <$> (try (string "in" *> pDelimiter) *> pListVal)
|
pIn = In <$> (try (string "in" *> pDelimiter) *> pListVal)
|
||||||
pIs = Is <$> (try (string "is" *> pDelimiter) *> pIsVal)
|
pIs = Is <$> (try (string "is" *> pDelimiter) *> pTriVal)
|
||||||
|
|
||||||
pIsDist = IsDistinctFrom <$> (try (string "isdistinct" *> pDelimiter) *> pSVal)
|
pIsDist = IsDistinctFrom <$> (try (string "isdistinct" *> pDelimiter) *> pSVal)
|
||||||
|
|
||||||
@@ -654,12 +653,11 @@ pOpExpr pSVal = do
|
|||||||
quant <- optionMaybe $ try (between (char '(') (char ')') (try (string "any" $> QuantAny) <|> string "all" $> QuantAll))
|
quant <- optionMaybe $ try (between (char '(') (char ')') (try (string "any" $> QuantAny) <|> string "all" $> QuantAll))
|
||||||
pDelimiter *> (OpQuant op quant <$> pSVal)
|
pDelimiter *> (OpQuant op quant <$> pSVal)
|
||||||
|
|
||||||
pIsVal = try (ciString "null" $> IsNull)
|
pTriVal = try (ciString "null" $> TriNull)
|
||||||
<|> try (ciString "not_null" $> IsNotNull)
|
<|> try (ciString "unknown" $> TriUnknown)
|
||||||
<|> try (ciString "true" $> IsTriTrue)
|
<|> try (ciString "true" $> TriTrue)
|
||||||
<|> try (ciString "false" $> IsTriFalse)
|
<|> try (ciString "false" $> TriFalse)
|
||||||
<|> try (ciString "unknown" $> IsTriUnknown)
|
<?> "null or trilean value (unknown, true, false)"
|
||||||
<?> "isVal: (null, not_null, true, false, unknown)"
|
|
||||||
|
|
||||||
pFts = do
|
pFts = do
|
||||||
op <- try (string "fts" $> FilterFts)
|
op <- try (string "fts" $> FilterFts)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ module PostgREST.ApiRequest.Types
|
|||||||
, Cast
|
, Cast
|
||||||
, Depth
|
, Depth
|
||||||
, EmbedParam(..)
|
, EmbedParam(..)
|
||||||
|
, ApiRequestError(..)
|
||||||
, EmbedPath
|
, EmbedPath
|
||||||
, Field
|
, Field
|
||||||
, Filter(..)
|
, Filter(..)
|
||||||
@@ -13,7 +14,6 @@ module PostgREST.ApiRequest.Types
|
|||||||
, JsonOperand(..)
|
, JsonOperand(..)
|
||||||
, JsonOperation(..)
|
, JsonOperation(..)
|
||||||
, JsonPath
|
, JsonPath
|
||||||
, Language
|
|
||||||
, ListVal
|
, ListVal
|
||||||
, LogicOperator(..)
|
, LogicOperator(..)
|
||||||
, LogicTree(..)
|
, LogicTree(..)
|
||||||
@@ -24,15 +24,23 @@ module PostgREST.ApiRequest.Types
|
|||||||
, OrderDirection(..)
|
, OrderDirection(..)
|
||||||
, OrderNulls(..)
|
, OrderNulls(..)
|
||||||
, OrderTerm(..)
|
, OrderTerm(..)
|
||||||
|
, QPError(..)
|
||||||
|
, RaiseError(..)
|
||||||
|
, RangeError(..)
|
||||||
, SingleVal
|
, SingleVal
|
||||||
, IsVal(..)
|
, TrileanVal(..)
|
||||||
, SimpleOperator(..)
|
, SimpleOperator(..)
|
||||||
, QuantOperator(..)
|
, QuantOperator(..)
|
||||||
, FtsOperator(..)
|
, FtsOperator(..)
|
||||||
, SelectItem(..)
|
, SelectItem(..)
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName)
|
import PostgREST.MediaType (MediaType (..))
|
||||||
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
|
QualifiedIdentifier)
|
||||||
|
import PostgREST.SchemaCache.Relationship (Relationship,
|
||||||
|
RelationshipsMap)
|
||||||
|
import PostgREST.SchemaCache.Routine (Routine (..))
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -60,6 +68,51 @@ data SelectItem
|
|||||||
}
|
}
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
data ApiRequestError
|
||||||
|
= AggregatesNotAllowed
|
||||||
|
| AmbiguousRelBetween Text Text [Relationship]
|
||||||
|
| AmbiguousRpc [Routine]
|
||||||
|
| MediaTypeError [ByteString]
|
||||||
|
| InvalidBody ByteString
|
||||||
|
| InvalidFilters
|
||||||
|
| InvalidPreferences [ByteString]
|
||||||
|
| InvalidRange RangeError
|
||||||
|
| InvalidRpcMethod ByteString
|
||||||
|
| LimitNoOrderError
|
||||||
|
| NotFound
|
||||||
|
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
||||||
|
| NoRpc Text Text [Text] Bool MediaType Bool [QualifiedIdentifier] [Routine]
|
||||||
|
| NotEmbedded Text
|
||||||
|
| PutLimitNotAllowedError
|
||||||
|
| QueryParamError QPError
|
||||||
|
| RelatedOrderNotToOne Text Text
|
||||||
|
| SpreadNotToOne Text Text
|
||||||
|
| UnacceptableFilter Text
|
||||||
|
| UnacceptableSchema [Text]
|
||||||
|
| UnsupportedMethod ByteString
|
||||||
|
| ColumnNotFound Text Text
|
||||||
|
| GucHeadersError
|
||||||
|
| GucStatusError
|
||||||
|
| OffLimitsChangesError Int64 Integer
|
||||||
|
| PutMatchingPkError
|
||||||
|
| SingularityError Integer
|
||||||
|
| PGRSTParseError RaiseError
|
||||||
|
| MaxAffectedViolationError Integer
|
||||||
|
deriving Show
|
||||||
|
|
||||||
|
data QPError = QPError Text Text
|
||||||
|
deriving Show
|
||||||
|
data RaiseError
|
||||||
|
= MsgParseError ByteString
|
||||||
|
| DetParseError ByteString
|
||||||
|
| NoDetail
|
||||||
|
deriving Show
|
||||||
|
data RangeError
|
||||||
|
= NegativeLimit
|
||||||
|
| LowerGTUpper
|
||||||
|
| OutOfBounds Text Text
|
||||||
|
deriving Show
|
||||||
|
|
||||||
type NodeName = Text
|
type NodeName = Text
|
||||||
type Depth = Integer
|
type Depth = Integer
|
||||||
|
|
||||||
@@ -122,7 +175,7 @@ data JsonOperation
|
|||||||
deriving (Eq, Show, Ord)
|
deriving (Eq, Show, Ord)
|
||||||
|
|
||||||
-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text
|
-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text
|
||||||
-- because we reuse our escaping functions and let pg do the casting with
|
-- because we reuse our escaping functons and let pg do the casting with
|
||||||
-- '1'::int
|
-- '1'::int
|
||||||
data JsonOperand
|
data JsonOperand
|
||||||
= JKey { jVal :: Text }
|
= JKey { jVal :: Text }
|
||||||
@@ -165,7 +218,7 @@ data Operation
|
|||||||
= Op SimpleOperator SingleVal
|
= Op SimpleOperator SingleVal
|
||||||
| OpQuant QuantOperator (Maybe OpQuantifier) SingleVal
|
| OpQuant QuantOperator (Maybe OpQuantifier) SingleVal
|
||||||
| In ListVal
|
| In ListVal
|
||||||
| Is IsVal
|
| Is TrileanVal
|
||||||
| IsDistinctFrom SingleVal
|
| IsDistinctFrom SingleVal
|
||||||
| Fts FtsOperator (Maybe Language) SingleVal
|
| Fts FtsOperator (Maybe Language) SingleVal
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
@@ -178,13 +231,12 @@ type SingleVal = Text
|
|||||||
-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)
|
-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)
|
||||||
type ListVal = [Text]
|
type ListVal = [Text]
|
||||||
|
|
||||||
data IsVal
|
-- | Three-valued logic values
|
||||||
= IsNull
|
data TrileanVal
|
||||||
| IsNotNull
|
= TriTrue
|
||||||
-- Trilean values
|
| TriFalse
|
||||||
| IsTriTrue
|
| TriNull
|
||||||
| IsTriFalse
|
| TriUnknown
|
||||||
| IsTriUnknown
|
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
-- Operators that are quantifiable, i.e. they can be used with the any/all modifiers
|
-- Operators that are quantifiable, i.e. they can be used with the any/all modifiers
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ module PostgREST.App
|
|||||||
|
|
||||||
|
|
||||||
import Control.Monad.Except (liftEither)
|
import Control.Monad.Except (liftEither)
|
||||||
import Data.Either.Combinators (mapLeft, whenLeft)
|
import Data.Either.Combinators (mapLeft)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.String (IsString (..))
|
import Data.String (IsString (..))
|
||||||
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
||||||
@@ -42,12 +42,10 @@ import qualified PostgREST.Unix as Unix (installSignalHandlers)
|
|||||||
|
|
||||||
import PostgREST.ApiRequest (ApiRequest (..))
|
import PostgREST.ApiRequest (ApiRequest (..))
|
||||||
import PostgREST.AppState (AppState)
|
import PostgREST.AppState (AppState)
|
||||||
import PostgREST.Auth.Types (AuthResult (..))
|
import PostgREST.Auth (AuthResult (..))
|
||||||
import PostgREST.Config (AppConfig (..), LogLevel (..),
|
import PostgREST.Config (AppConfig (..), LogLevel (..))
|
||||||
LogQuery (..))
|
|
||||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||||
import PostgREST.Error (Error)
|
import PostgREST.Error (Error)
|
||||||
import PostgREST.Network (resolveHost)
|
|
||||||
import PostgREST.Observation (Observation (..))
|
import PostgREST.Observation (Observation (..))
|
||||||
import PostgREST.Response.Performance (ServerTiming (..),
|
import PostgREST.Response.Performance (ServerTiming (..),
|
||||||
serverTimingHeader)
|
serverTimingHeader)
|
||||||
@@ -68,6 +66,8 @@ run appState = do
|
|||||||
let observer = AppState.getObserver appState
|
let observer = AppState.getObserver appState
|
||||||
conf@AppConfig{..} <- AppState.getConfig appState
|
conf@AppConfig{..} <- AppState.getConfig appState
|
||||||
|
|
||||||
|
observer $ AppStartObs prettyVersion
|
||||||
|
|
||||||
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
|
AppState.schemaCacheLoader appState -- Loads the initial SchemaCache
|
||||||
Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
|
Unix.installSignalHandlers (AppState.getMainThreadId appState) (AppState.schemaCacheLoader appState) (AppState.readInDbConfig False appState)
|
||||||
|
|
||||||
@@ -82,8 +82,7 @@ run appState = do
|
|||||||
observer $ AppServerUnixObs path
|
observer $ AppServerUnixObs path
|
||||||
Nothing -> do
|
Nothing -> do
|
||||||
port <- NS.socketPort $ AppState.getSocketREST appState
|
port <- NS.socketPort $ AppState.getSocketREST appState
|
||||||
host <- resolveHost $ AppState.getSocketREST appState
|
observer $ AppServerPortObs port
|
||||||
observer $ AppServerPortObs (fromJust host) port
|
|
||||||
|
|
||||||
Warp.runSettingsSocket (serverSettings conf) (AppState.getSocketREST appState) app
|
Warp.runSettingsSocket (serverSettings conf) (AppState.getSocketREST appState) app
|
||||||
|
|
||||||
@@ -144,28 +143,11 @@ postgrestResponse appState conf@AppConfig{..} maybeSchemaCache pgVer authResult@
|
|||||||
body <- lift $ Wai.strictRequestBody req
|
body <- lift $ Wai.strictRequestBody req
|
||||||
|
|
||||||
let jwtTime = if configServerTimingEnabled then Auth.getJwtDur req else Nothing
|
let jwtTime = if configServerTimingEnabled then Auth.getJwtDur req else Nothing
|
||||||
timezones = dbTimezones sCache
|
|
||||||
prefs = ApiRequest.userPreferences conf req timezones
|
|
||||||
|
|
||||||
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestError $ ApiRequest.userApiRequest conf prefs req body
|
(parseTime, apiReq@ApiRequest{..}) <- withTiming $ liftEither . mapLeft Error.ApiRequestError $ ApiRequest.userApiRequest conf req body sCache
|
||||||
(planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
|
(planTime, plan) <- withTiming $ liftEither $ Plan.actionPlan iAction conf apiReq sCache
|
||||||
|
(queryTime, queryResult) <- withTiming $ Query.runQuery appState conf authResult apiReq plan sCache pgVer (Just authRole /= configDbAnonRole)
|
||||||
let query = Query.query conf authResult apiReq plan sCache pgVer
|
(respTime, resp) <- withTiming $ liftEither $ Response.actionResponse queryResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
|
||||||
logSQL = lift . AppState.getObserver appState . DBQuery (Query.getSQLQuery query)
|
|
||||||
|
|
||||||
(queryTime, queryResult) <- withTiming $ do
|
|
||||||
case query of
|
|
||||||
Query.NoDbQuery r -> pure r
|
|
||||||
Query.DbQuery{..} -> do
|
|
||||||
dbRes <- lift $ AppState.usePool appState (dqTransaction dqIsoLevel dqTxMode $ runExceptT dqDbHandler)
|
|
||||||
let eitherResp = mapLeft Error.PgErr . mapLeft (Error.PgError (Just authRole /= configDbAnonRole)) $ dbRes
|
|
||||||
when (configLogQuery /= LogQueryDisabled) $ whenLeft eitherResp $ logSQL . Error.status
|
|
||||||
liftEither eitherResp >>= liftEither
|
|
||||||
|
|
||||||
(respTime, resp) <- withTiming $ do
|
|
||||||
let response = Response.actionResponse queryResult apiReq (T.decodeUtf8 prettyVersion, docsVersion) conf sCache iSchema iNegotiatedByProfile
|
|
||||||
when (configLogQuery /= LogQueryDisabled) $ logSQL $ either Error.status Response.pgrstStatus response
|
|
||||||
liftEither response
|
|
||||||
|
|
||||||
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime queryTime respTime) resp
|
return $ toWaiResponse (ServerTiming jwtTime parseTime planTime queryTime respTime) resp
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
module PostgREST.AppState
|
module PostgREST.AppState
|
||||||
( AppState
|
( AppState
|
||||||
|
, AuthResult(..)
|
||||||
|
, JwtCacheState(..)
|
||||||
, destroy
|
, destroy
|
||||||
, getConfig
|
, getConfig
|
||||||
, getSchemaCache
|
, getSchemaCache
|
||||||
@@ -30,7 +32,10 @@ module PostgREST.AppState
|
|||||||
, isPending
|
, isPending
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
import qualified Data.Aeson.KeyMap as KM
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
import qualified Data.Cache as C
|
||||||
import Data.Either.Combinators (whenLeft)
|
import Data.Either.Combinators (whenLeft)
|
||||||
import qualified Data.Text as T (unpack)
|
import qualified Data.Text as T (unpack)
|
||||||
import qualified Hasql.Pool as SQL
|
import qualified Hasql.Pool as SQL
|
||||||
@@ -39,7 +44,6 @@ import qualified Hasql.Session as SQL
|
|||||||
import qualified Hasql.Transaction.Sessions as SQL
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
import qualified Network.HTTP.Types.Status as HTTP
|
import qualified Network.HTTP.Types.Status as HTTP
|
||||||
import qualified Network.Socket as NS
|
import qualified Network.Socket as NS
|
||||||
import qualified PostgREST.Auth.JwtCache as JwtCache
|
|
||||||
import qualified PostgREST.Error as Error
|
import qualified PostgREST.Error as Error
|
||||||
import qualified PostgREST.Logger as Logger
|
import qualified PostgREST.Logger as Logger
|
||||||
import qualified PostgREST.Metrics as Metrics
|
import qualified PostgREST.Metrics as Metrics
|
||||||
@@ -57,7 +61,6 @@ import Data.IORef (IORef, atomicWriteIORef, newIORef,
|
|||||||
readIORef)
|
readIORef)
|
||||||
import Data.Time.Clock (UTCTime, getCurrentTime)
|
import Data.Time.Clock (UTCTime, getCurrentTime)
|
||||||
|
|
||||||
import PostgREST.Auth.JwtCache (JwtCacheState)
|
|
||||||
import PostgREST.Config (AppConfig (..),
|
import PostgREST.Config (AppConfig (..),
|
||||||
addFallbackAppName,
|
addFallbackAppName,
|
||||||
readAppConfig)
|
readAppConfig)
|
||||||
@@ -76,6 +79,17 @@ import Data.Streaming.Network (bindPortTCP, bindRandomPortTCP)
|
|||||||
import Data.String (IsString (..))
|
import Data.String (IsString (..))
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
data AuthResult = AuthResult
|
||||||
|
{ authClaims :: KM.KeyMap JSON.Value
|
||||||
|
, authRole :: BS.ByteString
|
||||||
|
}
|
||||||
|
|
||||||
|
-- | JWT Cache and IO action that triggers purging old entries from the cache
|
||||||
|
data JwtCacheState = JwtCacheState
|
||||||
|
{ jwtCache :: C.Cache ByteString AuthResult
|
||||||
|
, purgeCache :: IO ()
|
||||||
|
}
|
||||||
|
|
||||||
data AppState = AppState
|
data AppState = AppState
|
||||||
-- | Database connection pool
|
-- | Database connection pool
|
||||||
{ statePool :: SQL.Pool
|
{ statePool :: SQL.Pool
|
||||||
@@ -99,14 +113,14 @@ data AppState = AppState
|
|||||||
, stateNextDelay :: IORef Int
|
, stateNextDelay :: IORef Int
|
||||||
-- | Keeps track of the next delay for the listener
|
-- | Keeps track of the next delay for the listener
|
||||||
, stateNextListenerDelay :: IORef Int
|
, stateNextListenerDelay :: IORef Int
|
||||||
|
-- | JWT Cache
|
||||||
|
, jwtCacheState :: JwtCacheState
|
||||||
-- | Network socket for REST API
|
-- | Network socket for REST API
|
||||||
, stateSocketREST :: NS.Socket
|
, stateSocketREST :: NS.Socket
|
||||||
-- | Network socket for the admin UI
|
-- | Network socket for the admin UI
|
||||||
, stateSocketAdmin :: Maybe NS.Socket
|
, stateSocketAdmin :: Maybe NS.Socket
|
||||||
-- | Observation handler
|
-- | Observation handler
|
||||||
, stateObserver :: ObservationHandler
|
, stateObserver :: ObservationHandler
|
||||||
-- | JWT Cache
|
|
||||||
, stateJwtCache :: JwtCache.JwtCacheState
|
|
||||||
, stateLogger :: Logger.LoggerState
|
, stateLogger :: Logger.LoggerState
|
||||||
, stateMetrics :: Metrics.MetricsState
|
, stateMetrics :: Metrics.MetricsState
|
||||||
}
|
}
|
||||||
@@ -125,16 +139,23 @@ init conf@AppConfig{configLogLevel, configDbPoolSize} = do
|
|||||||
metricsState <- Metrics.init configDbPoolSize
|
metricsState <- Metrics.init configDbPoolSize
|
||||||
let observer = liftA2 (>>) (Logger.observationLogger loggerState configLogLevel) (Metrics.observationMetrics metricsState)
|
let observer = liftA2 (>>) (Logger.observationLogger loggerState configLogLevel) (Metrics.observationMetrics metricsState)
|
||||||
|
|
||||||
observer $ AppStartObs prettyVersion
|
|
||||||
|
|
||||||
jwtCacheState <- JwtCache.init
|
|
||||||
pool <- initPool conf observer
|
pool <- initPool conf observer
|
||||||
(sock, adminSock) <- initSockets conf
|
(sock, adminSock) <- initSockets conf
|
||||||
state' <- initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState observer
|
state' <- initWithPool (sock, adminSock) pool conf loggerState metricsState observer
|
||||||
pure state' { stateSocketREST = sock, stateSocketAdmin = adminSock}
|
pure state' { stateSocketREST = sock, stateSocketAdmin = adminSock}
|
||||||
|
|
||||||
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> JwtCache.JwtCacheState -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
|
initWithPool :: AppSockets -> SQL.Pool -> AppConfig -> Logger.LoggerState -> Metrics.MetricsState -> ObservationHandler -> IO AppState
|
||||||
initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState observer = do
|
initWithPool (sock, adminSock) pool conf loggerState metricsState observer = do
|
||||||
|
cache <- C.newCache Nothing
|
||||||
|
-- purgeExpired has O(n^2) complexity
|
||||||
|
-- so we wrap it in debounce to make sure it:
|
||||||
|
-- 1) is executed asynchronously
|
||||||
|
-- 2) only a single purge operation is running at a time
|
||||||
|
debounce <- mkDebounce defaultDebounceSettings
|
||||||
|
-- debounceFreq is set to default 1 second
|
||||||
|
{ debounceAction = C.purgeExpired cache
|
||||||
|
, debounceEdge = leadingEdge
|
||||||
|
}
|
||||||
|
|
||||||
appState <- AppState pool
|
appState <- AppState pool
|
||||||
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
<$> newIORef minimumPgVersion -- assume we're in a supported version when starting, this will be corrected on a later step
|
||||||
@@ -147,10 +168,10 @@ initWithPool (sock, adminSock) pool conf jwtCacheState loggerState metricsState
|
|||||||
<*> myThreadId
|
<*> myThreadId
|
||||||
<*> newIORef 0
|
<*> newIORef 0
|
||||||
<*> newIORef 1
|
<*> newIORef 1
|
||||||
|
<*> pure (JwtCacheState cache debounce)
|
||||||
<*> pure sock
|
<*> pure sock
|
||||||
<*> pure adminSock
|
<*> pure adminSock
|
||||||
<*> pure observer
|
<*> pure observer
|
||||||
<*> pure jwtCacheState
|
|
||||||
<*> pure loggerState
|
<*> pure loggerState
|
||||||
<*> pure metricsState
|
<*> pure metricsState
|
||||||
|
|
||||||
@@ -174,8 +195,7 @@ initSockets AppConfig{..} = do
|
|||||||
cfg'uspm = configServerUnixSocketMode
|
cfg'uspm = configServerUnixSocketMode
|
||||||
cfg'host = configServerHost
|
cfg'host = configServerHost
|
||||||
cfg'port = configServerPort
|
cfg'port = configServerPort
|
||||||
cfg'adminHost = configAdminServerHost
|
cfg'adminport = configAdminServerPort
|
||||||
cfg'adminPort = configAdminServerPort
|
|
||||||
|
|
||||||
sock <- case cfg'usp of
|
sock <- case cfg'usp of
|
||||||
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
|
-- I'm not using `streaming-commons`' bindPath function here because it's not defined for Windows,
|
||||||
@@ -193,16 +213,16 @@ initSockets AppConfig{..} = do
|
|||||||
pure (num, sock)
|
pure (num, sock)
|
||||||
pure sock
|
pure sock
|
||||||
|
|
||||||
adminSock <- case cfg'adminPort of
|
adminSock <- case cfg'adminport of
|
||||||
Just adminPort -> do
|
Just adminPort -> do
|
||||||
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'adminHost)
|
adminSock <- bindPortTCP adminPort (fromString $ T.unpack cfg'host)
|
||||||
pure $ Just adminSock
|
pure $ Just adminSock
|
||||||
Nothing -> pure Nothing
|
Nothing -> pure Nothing
|
||||||
|
|
||||||
pure (sock, adminSock)
|
pure (sock, adminSock)
|
||||||
|
|
||||||
initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool
|
initPool :: AppConfig -> ObservationHandler -> IO SQL.Pool
|
||||||
initPool AppConfig{..} observer = do
|
initPool AppConfig{..} observer =
|
||||||
SQL.acquire $ SQL.settings
|
SQL.acquire $ SQL.settings
|
||||||
[ SQL.size configDbPoolSize
|
[ SQL.size configDbPoolSize
|
||||||
, SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout
|
, SQL.acquisitionTimeout $ fromIntegral configDbPoolAcquisitionTimeout
|
||||||
@@ -312,7 +332,7 @@ getTime :: AppState -> IO UTCTime
|
|||||||
getTime = stateGetTime
|
getTime = stateGetTime
|
||||||
|
|
||||||
getJwtCacheState :: AppState -> JwtCacheState
|
getJwtCacheState :: AppState -> JwtCacheState
|
||||||
getJwtCacheState = stateJwtCache
|
getJwtCacheState = jwtCacheState
|
||||||
|
|
||||||
getSocketREST :: AppState -> NS.Socket
|
getSocketREST :: AppState -> NS.Socket
|
||||||
getSocketREST = stateSocketREST
|
getSocketREST = stateSocketREST
|
||||||
@@ -388,7 +408,6 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
|||||||
observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion
|
observer $ ExitUnsupportedPgVersion actualPgVersion minimumPgVersion
|
||||||
killThread mainThreadId
|
killThread mainThreadId
|
||||||
observer $ DBConnectedObs $ pgvFullName actualPgVersion
|
observer $ DBConnectedObs $ pgvFullName actualPgVersion
|
||||||
observer $ PoolInit configDbPoolSize
|
|
||||||
putPgVersion appState actualPgVersion
|
putPgVersion appState actualPgVersion
|
||||||
return $ Just actualPgVersion
|
return $ Just actualPgVersion
|
||||||
|
|
||||||
@@ -407,7 +426,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
|||||||
Left e -> do
|
Left e -> do
|
||||||
putSCacheStatus appState SCPending
|
putSCacheStatus appState SCPending
|
||||||
putSchemaCache appState Nothing
|
putSchemaCache appState Nothing
|
||||||
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
|
observer $ SchemaCacheErrorObs e
|
||||||
return Nothing
|
return Nothing
|
||||||
|
|
||||||
Right sCache -> do
|
Right sCache -> do
|
||||||
@@ -437,7 +456,7 @@ retryingSchemaCacheLoad appState@AppState{stateObserver=observer, stateMainThrea
|
|||||||
-- | Reads the in-db config and reads the config file again
|
-- | Reads the in-db config and reads the config file again
|
||||||
-- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue.
|
-- | We don't retry reading the in-db config after it fails immediately, because it could have user errors. We just report the error and continue.
|
||||||
readInDbConfig :: Bool -> AppState -> IO ()
|
readInDbConfig :: Bool -> AppState -> IO ()
|
||||||
readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
|
readInDbConfig startingUp appState@AppState{stateObserver=observer, jwtCacheState=JwtCacheState{jwtCache}} = do
|
||||||
conf <- getConfig appState
|
conf <- getConfig appState
|
||||||
pgVer <- getPgVersion appState
|
pgVer <- getPgVersion appState
|
||||||
dbSettings <-
|
dbSettings <-
|
||||||
@@ -474,7 +493,7 @@ readInDbConfig startingUp appState@AppState{stateObserver=observer} = do
|
|||||||
if configJwtSecret conf == configJwtSecret newConf then
|
if configJwtSecret conf == configJwtSecret newConf then
|
||||||
pass
|
pass
|
||||||
else
|
else
|
||||||
JwtCache.emptyCache (getJwtCacheState appState) -- atomic O(1) operation
|
C.purge jwtCache -- atomic O(1) operation
|
||||||
|
|
||||||
if startingUp then
|
if startingUp then
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -10,124 +10,76 @@ Authentication should always be implemented in an external service.
|
|||||||
In the test suite there is an example of simple login function that can be used for a
|
In the test suite there is an example of simple login function that can be used for a
|
||||||
very simple authentication system inside the PostgreSQL database.
|
very simple authentication system inside the PostgreSQL database.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
module PostgREST.Auth
|
module PostgREST.Auth
|
||||||
( getResult
|
( AuthResult (..)
|
||||||
|
, getResult
|
||||||
, getJwtDur
|
, getJwtDur
|
||||||
, getRole
|
, getRole
|
||||||
, middleware
|
, middleware
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import qualified Crypto.JWT as JWT
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.Aeson.Key as K
|
import qualified Data.Aeson.Key as K
|
||||||
import qualified Data.Aeson.KeyMap as KM
|
import qualified Data.Aeson.KeyMap as KM
|
||||||
import qualified Data.Aeson.Types as JSON
|
import qualified Data.Aeson.Types as JSON
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.ByteString.Internal as BS
|
|
||||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||||
|
import qualified Data.Cache as C
|
||||||
import qualified Data.Scientific as Sci
|
import qualified Data.Scientific as Sci
|
||||||
import qualified Data.Text as T
|
|
||||||
import qualified Data.Vault.Lazy as Vault
|
import qualified Data.Vault.Lazy as Vault
|
||||||
import qualified Data.Vector as V
|
import qualified Data.Vector as V
|
||||||
import qualified Jose.Jwk as JWT
|
|
||||||
import qualified Jose.Jwt as JWT
|
|
||||||
import qualified Network.HTTP.Types.Header as HTTP
|
import qualified Network.HTTP.Types.Header as HTTP
|
||||||
import qualified Network.Wai as Wai
|
import qualified Network.Wai as Wai
|
||||||
import qualified Network.Wai.Middleware.HttpAuth as Wai
|
import qualified Network.Wai.Middleware.HttpAuth as Wai
|
||||||
|
|
||||||
|
import Control.Lens (set)
|
||||||
import Control.Monad.Except (liftEither)
|
import Control.Monad.Except (liftEither)
|
||||||
import Data.Either.Combinators (mapLeft)
|
import Data.Either.Combinators (mapLeft)
|
||||||
import Data.List (lookup)
|
import Data.List (lookup)
|
||||||
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
|
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
|
||||||
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
|
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
|
||||||
|
import System.Clock (TimeSpec (..))
|
||||||
import System.IO.Unsafe (unsafePerformIO)
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
import System.TimeIt (timeItT)
|
import System.TimeIt (timeItT)
|
||||||
|
|
||||||
import PostgREST.AppState (AppState, getConfig, getJwtCacheState,
|
import PostgREST.AppState (AppState, AuthResult (..),
|
||||||
getTime)
|
JwtCacheState (..), getConfig,
|
||||||
import PostgREST.Auth.JwtCache (lookupJwtCache)
|
getJwtCacheState, getTime)
|
||||||
import PostgREST.Auth.Types (AuthResult (..))
|
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
|
||||||
import PostgREST.Config (AppConfig (..), FilterExp (..),
|
import PostgREST.Error (Error (..))
|
||||||
JSPath, JSPathExp (..))
|
|
||||||
import PostgREST.Error (Error (..), JwtClaimsError (..),
|
|
||||||
JwtDecodeError (..), JwtError (..))
|
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
-- | Receives the JWT secret and audience (from config) and a JWT and returns a
|
-- | Receives the JWT secret and audience (from config) and a JWT and returns a
|
||||||
-- JSON object of JWT claims.
|
-- JSON object of JWT claims.
|
||||||
parseToken :: AppConfig -> Maybe ByteString -> UTCTime -> ExceptT Error IO JSON.Value
|
parseToken :: Monad m =>
|
||||||
parseToken _ Nothing _ = return JSON.emptyObject
|
AppConfig -> LByteString -> UTCTime -> ExceptT Error m JSON.Value
|
||||||
parseToken _ (Just "") _ = throwE . JwtErr $ JwtDecodeErr EmptyAuthHeader
|
parseToken _ "" _ = return JSON.emptyObject
|
||||||
parseToken AppConfig{..} (Just tkn) time = do
|
parseToken AppConfig{..} token time = do
|
||||||
secret <- liftEither . maybeToRight (JwtErr JwtSecretMissing) $ configJWKS
|
secret <- liftEither . maybeToRight JwtTokenMissing $ configJWKS
|
||||||
tknWith3Parts <- liftEither $ hasThreeParts tkn
|
eitherClaims <-
|
||||||
eitherContent <- liftIO $ JWT.decode (JWT.keys secret) Nothing tknWith3Parts
|
lift . runExceptT $
|
||||||
content <- liftEither . mapLeft (JwtErr . jwtDecodeError) $ eitherContent
|
JWT.verifyClaimsAt validation secret time =<< JWT.decodeCompact token
|
||||||
liftEither $ mapLeft JwtErr $ verifyClaims content
|
liftEither . mapLeft jwtClaimsError $ JSON.toJSON <$> eitherClaims
|
||||||
where
|
where
|
||||||
hasThreeParts :: ByteString -> Either Error ByteString
|
validation =
|
||||||
hasThreeParts token = case length $ BS.split (BS.c2w '.') token of
|
JWT.defaultJWTValidationSettings audienceCheck & set JWT.allowedSkew 30
|
||||||
3 -> Right token
|
|
||||||
n -> Left $ JwtErr $ JwtDecodeErr $ UnexpectedParts n
|
|
||||||
jwtDecodeError :: JWT.JwtError -> JwtError
|
|
||||||
-- The only errors we can get from JWT.decode function are:
|
|
||||||
-- BadAlgorithm
|
|
||||||
-- KeyError
|
|
||||||
-- BadCrypto
|
|
||||||
jwtDecodeError (JWT.KeyError m) = JwtDecodeErr $ KeyError m
|
|
||||||
jwtDecodeError (JWT.BadAlgorithm m) = JwtDecodeErr $ BadAlgorithm m
|
|
||||||
jwtDecodeError JWT.BadCrypto = JwtDecodeErr BadCrypto
|
|
||||||
-- Control never reaches here, the decode function only returns the above three
|
|
||||||
jwtDecodeError _ = JwtDecodeErr UnreachableDecodeError
|
|
||||||
|
|
||||||
verifyClaims :: JWT.JwtContent -> Either JwtError JSON.Value
|
audienceCheck :: JWT.StringOrURI -> Bool
|
||||||
verifyClaims (JWT.Jws (_, claims)) = case JSON.decodeStrict claims of
|
audienceCheck = maybe (const True) (==) configJwtAudience
|
||||||
Just jclaims@(JSON.Object mclaims) ->
|
|
||||||
verifyClaim mclaims "exp" isValidExpClaim JWTExpired >>
|
|
||||||
verifyClaim mclaims "nbf" isValidNbfClaim JWTNotYetValid >>
|
|
||||||
verifyClaim mclaims "iat" isValidIatClaim JWTIssuedAtFuture >>
|
|
||||||
verifyClaim mclaims "aud" isValidAudClaim JWTNotInAudience >>
|
|
||||||
return jclaims
|
|
||||||
_ -> Left $ JwtClaimsErr ParsingClaimsFailed
|
|
||||||
-- TODO: We could enable JWE support here (encrypted tokens)
|
|
||||||
verifyClaims _ = Left $ JwtDecodeErr UnsupportedTokenType
|
|
||||||
|
|
||||||
verifyClaim mclaims claim func err = do
|
jwtClaimsError :: JWT.JWTError -> Error
|
||||||
isValid <- maybe (Right True) func (KM.lookup claim mclaims)
|
jwtClaimsError JWT.JWTExpired = JwtTokenInvalid "JWT expired"
|
||||||
unless isValid $ Left $ JwtClaimsErr err
|
jwtClaimsError e = JwtTokenInvalid $ show e
|
||||||
|
|
||||||
allowedSkewSeconds = 30 :: Int64
|
|
||||||
now = floor . nominalDiffTimeToSeconds $ utcTimeToPOSIXSeconds time
|
|
||||||
sciToInt = fromMaybe 0 . Sci.toBoundedInteger
|
|
||||||
allStrings = all (\case (JSON.String _) -> True; _ -> False)
|
|
||||||
|
|
||||||
isValidExpClaim :: JSON.Value -> Either JwtError Bool
|
|
||||||
isValidExpClaim (JSON.Number secs) = Right $ now <= (sciToInt secs + allowedSkewSeconds)
|
|
||||||
isValidExpClaim _ = Left $ JwtClaimsErr ExpClaimNotNumber
|
|
||||||
|
|
||||||
isValidNbfClaim :: JSON.Value -> Either JwtError Bool
|
|
||||||
isValidNbfClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
|
||||||
isValidNbfClaim _ = Left $ JwtClaimsErr NbfClaimNotNumber
|
|
||||||
|
|
||||||
isValidIatClaim :: JSON.Value -> Either JwtError Bool
|
|
||||||
isValidIatClaim (JSON.Number secs) = Right $ now >= (sciToInt secs - allowedSkewSeconds)
|
|
||||||
isValidIatClaim _ = Left $ JwtClaimsErr IatClaimNotNumber
|
|
||||||
|
|
||||||
isValidAudClaim :: JSON.Value -> Either JwtError Bool
|
|
||||||
isValidAudClaim JSON.Null = Right True -- {"aud": null} is valid for all audiences
|
|
||||||
isValidAudClaim (JSON.String str) = Right $ maybe (const True) (==) configJwtAudience str
|
|
||||||
isValidAudClaim (JSON.Array arr)
|
|
||||||
| null arr = Right True -- {"aud": []} is valid for all audiences
|
|
||||||
| allStrings arr = Right $ maybe True (\a -> JSON.String a `elem` arr) configJwtAudience
|
|
||||||
isValidAudClaim _ = Left $ JwtClaimsErr AudClaimNotStringOrArray
|
|
||||||
|
|
||||||
parseClaims :: Monad m =>
|
parseClaims :: Monad m =>
|
||||||
AppConfig -> JSON.Value -> ExceptT Error m AuthResult
|
AppConfig -> JSON.Value -> ExceptT Error m AuthResult
|
||||||
parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
|
parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
|
||||||
-- role defaults to anon if not specified in jwt
|
-- role defaults to anon if not specified in jwt
|
||||||
role <- liftEither . maybeToRight (JwtErr JwtTokenRequired) $
|
role <- liftEither . maybeToRight JwtTokenRequired $
|
||||||
unquoted <$> walkJSPath (Just jclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
unquoted <$> walkJSPath (Just jclaims) configJwtRoleClaimKey <|> configDbAnonRole
|
||||||
return AuthResult
|
return AuthResult
|
||||||
{ authClaims = mclaims & KM.insert "role" (JSON.toJSON $ decodeUtf8 role)
|
{ authClaims = mclaims & KM.insert "role" (JSON.toJSON $ decodeUtf8 role)
|
||||||
@@ -138,20 +90,8 @@ parseClaims AppConfig{..} jclaims@(JSON.Object mclaims) = do
|
|||||||
walkJSPath x [] = x
|
walkJSPath x [] = x
|
||||||
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest
|
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (KM.lookup (K.fromText key) o) rest
|
||||||
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
|
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
|
||||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (EqualsCond txt)] = findFirstMatch (==) txt ar
|
|
||||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (NotEqualsCond txt)] = findFirstMatch (/=) txt ar
|
|
||||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (StartsWithCond txt)] = findFirstMatch T.isPrefixOf txt ar
|
|
||||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (EndsWithCond txt)] = findFirstMatch T.isSuffixOf txt ar
|
|
||||||
walkJSPath (Just (JSON.Array ar)) [JSPFilter (ContainsCond txt)] = findFirstMatch T.isInfixOf txt ar
|
|
||||||
walkJSPath _ _ = Nothing
|
walkJSPath _ _ = Nothing
|
||||||
|
|
||||||
findFirstMatch matchWith pattern = foldr checkMatch Nothing
|
|
||||||
where
|
|
||||||
checkMatch (JSON.String txt) acc
|
|
||||||
| pattern `matchWith` txt = Just $ JSON.String txt
|
|
||||||
| otherwise = acc
|
|
||||||
checkMatch _ acc = acc
|
|
||||||
|
|
||||||
unquoted :: JSON.Value -> BS.ByteString
|
unquoted :: JSON.Value -> BS.ByteString
|
||||||
unquoted (JSON.String t) = encodeUtf8 t
|
unquoted (JSON.String t) = encodeUtf8 t
|
||||||
unquoted v = LBS.toStrict $ JSON.encode v
|
unquoted v = LBS.toStrict $ JSON.encode v
|
||||||
@@ -165,11 +105,10 @@ middleware appState app req respond = do
|
|||||||
conf <- getConfig appState
|
conf <- getConfig appState
|
||||||
time <- getTime appState
|
time <- getTime appState
|
||||||
|
|
||||||
let token = Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
|
let token = fromMaybe "" $ Wai.extractBearerAuth =<< lookup HTTP.hAuthorization (Wai.requestHeaders req)
|
||||||
parseJwt = runExceptT $ parseToken conf token time >>= parseClaims conf
|
parseJwt = runExceptT $ parseToken conf (LBS.fromStrict token) time >>= parseClaims conf
|
||||||
jwtCacheState = getJwtCacheState appState
|
|
||||||
|
|
||||||
-- If ServerTimingEnabled -> calculate JWT validation time
|
-- If DbPlanEnabled -> calculate JWT validation time
|
||||||
-- If JwtCacheMaxLifetime -> cache JWT validation result
|
-- If JwtCacheMaxLifetime -> cache JWT validation result
|
||||||
req' <- case (configServerTimingEnabled conf, configJwtCacheMaxLifetime conf) of
|
req' <- case (configServerTimingEnabled conf, configJwtCacheMaxLifetime conf) of
|
||||||
(True, 0) -> do
|
(True, 0) -> do
|
||||||
@@ -177,9 +116,7 @@ middleware appState app req respond = do
|
|||||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
|
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
|
||||||
|
|
||||||
(True, maxLifetime) -> do
|
(True, maxLifetime) -> do
|
||||||
(dur, authResult) <- timeItT $ case token of
|
(dur, authResult) <- timeItT $ getJWTFromCache appState token maxLifetime parseJwt time
|
||||||
Just tkn -> lookupJwtCache jwtCacheState tkn maxLifetime parseJwt time
|
|
||||||
Nothing -> parseJwt
|
|
||||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
|
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult & Vault.insert jwtDurKey dur }
|
||||||
|
|
||||||
(False, 0) -> do
|
(False, 0) -> do
|
||||||
@@ -187,13 +124,57 @@ middleware appState app req respond = do
|
|||||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
||||||
|
|
||||||
(False, maxLifetime) -> do
|
(False, maxLifetime) -> do
|
||||||
authResult <- case token of
|
authResult <- getJWTFromCache appState token maxLifetime parseJwt time
|
||||||
Just tkn -> lookupJwtCache jwtCacheState tkn maxLifetime parseJwt time
|
|
||||||
Nothing -> parseJwt
|
|
||||||
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
return $ req { Wai.vault = Wai.vault req & Vault.insert authResultKey authResult }
|
||||||
|
|
||||||
app req' respond
|
app req' respond
|
||||||
|
|
||||||
|
-- | Used to retrieve and insert JWT to JWT Cache
|
||||||
|
getJWTFromCache :: AppState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
|
||||||
|
getJWTFromCache appState token maxLifetime parseJwt utc = do
|
||||||
|
let JwtCacheState{..} = getJwtCacheState appState
|
||||||
|
checkCache <- C.lookup jwtCache token
|
||||||
|
authResult <- maybe parseJwt (pure . Right) checkCache
|
||||||
|
|
||||||
|
case (authResult,checkCache) of
|
||||||
|
-- From comment:
|
||||||
|
-- https://github.com/PostgREST/postgrest/pull/3801#discussion_r1857987914
|
||||||
|
--
|
||||||
|
-- We purge expired cache entries on a cache miss
|
||||||
|
-- The reasoning is that:
|
||||||
|
--
|
||||||
|
-- 1. We expect it to be rare (otherwise there is no point of the cache)
|
||||||
|
-- 2. It makes sure the cache is not growing (as inserting new entries
|
||||||
|
-- does garbage collection)
|
||||||
|
-- 3. Since this is time expiration based cache there is no real risk of
|
||||||
|
-- starvation - sooner or later we are going to have a cache miss.
|
||||||
|
|
||||||
|
(Right res, Nothing) -> do -- cache miss
|
||||||
|
|
||||||
|
let timeSpec = getTimeSpec res maxLifetime utc
|
||||||
|
|
||||||
|
-- insert new cache entry
|
||||||
|
C.insert' jwtCache timeSpec token res
|
||||||
|
|
||||||
|
-- Execute IO action to purge the cache
|
||||||
|
-- It is assumed this action returns immidiately
|
||||||
|
-- so that request processing is not blocked.
|
||||||
|
purgeCache
|
||||||
|
|
||||||
|
_ -> pure ()
|
||||||
|
|
||||||
|
return authResult
|
||||||
|
|
||||||
|
-- Used to extract JWT exp claim and add to JWT Cache
|
||||||
|
getTimeSpec :: AuthResult -> Int -> UTCTime -> Maybe TimeSpec
|
||||||
|
getTimeSpec res maxLifetime utc = do
|
||||||
|
let expireJSON = KM.lookup "exp" (authClaims res)
|
||||||
|
utcToSecs = floor . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds
|
||||||
|
sciToInt = fromMaybe 0 . Sci.toBoundedInteger
|
||||||
|
case expireJSON of
|
||||||
|
Just (JSON.Number seconds) -> Just $ TimeSpec (sciToInt seconds - utcToSecs utc) 0
|
||||||
|
_ -> Just $ TimeSpec (fromIntegral maxLifetime :: Int64) 0
|
||||||
|
|
||||||
authResultKey :: Vault.Key (Either Error AuthResult)
|
authResultKey :: Vault.Key (Either Error AuthResult)
|
||||||
authResultKey = unsafePerformIO Vault.newKey
|
authResultKey = unsafePerformIO Vault.newKey
|
||||||
{-# NOINLINE authResultKey #-}
|
{-# NOINLINE authResultKey #-}
|
||||||
|
|||||||
@@ -1,99 +0,0 @@
|
|||||||
{-|
|
|
||||||
Module : PostgREST.Auth.JwtCache
|
|
||||||
Description : PostgREST Jwt Authentication Result Cache.
|
|
||||||
|
|
||||||
This module provides functions to deal with the JWT cache
|
|
||||||
-}
|
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
|
||||||
module PostgREST.Auth.JwtCache
|
|
||||||
( init
|
|
||||||
, JwtCacheState
|
|
||||||
, lookupJwtCache
|
|
||||||
, emptyCache
|
|
||||||
) where
|
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
|
||||||
import qualified Data.Aeson.KeyMap as KM
|
|
||||||
import qualified Data.Cache as C
|
|
||||||
import qualified Data.Scientific as Sci
|
|
||||||
|
|
||||||
import Control.Debounce
|
|
||||||
|
|
||||||
import Data.Time.Clock (UTCTime, nominalDiffTimeToSeconds)
|
|
||||||
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
|
|
||||||
import System.Clock (TimeSpec (..))
|
|
||||||
|
|
||||||
import PostgREST.Auth.Types (AuthResult (..))
|
|
||||||
import PostgREST.Error (Error (..))
|
|
||||||
|
|
||||||
import Protolude
|
|
||||||
|
|
||||||
-- | JWT Cache and IO action that triggers purging old entries from the cache
|
|
||||||
data JwtCacheState = JwtCacheState
|
|
||||||
{ jwtCache :: C.Cache ByteString AuthResult
|
|
||||||
, purgeCache :: IO ()
|
|
||||||
}
|
|
||||||
|
|
||||||
-- | Initialize JwtCacheState
|
|
||||||
init :: IO JwtCacheState
|
|
||||||
init = do
|
|
||||||
cache <- C.newCache Nothing -- no default expiration
|
|
||||||
-- purgeExpired has O(n^2) complexity
|
|
||||||
-- so we wrap it in debounce to make sure it:
|
|
||||||
-- 1) is executed asynchronously
|
|
||||||
-- 2) only a single purge operation is running at a time
|
|
||||||
debounce <- mkDebounce defaultDebounceSettings
|
|
||||||
-- debounceFreq is set to default 1 second
|
|
||||||
{ debounceAction = C.purgeExpired cache
|
|
||||||
, debounceEdge = leadingEdge
|
|
||||||
}
|
|
||||||
pure $ JwtCacheState cache debounce
|
|
||||||
|
|
||||||
-- | Used to retrieve and insert JWT to JWT Cache
|
|
||||||
lookupJwtCache :: JwtCacheState -> ByteString -> Int -> IO (Either Error AuthResult) -> UTCTime -> IO (Either Error AuthResult)
|
|
||||||
lookupJwtCache JwtCacheState{jwtCache, purgeCache} token maxLifetime parseJwt utc = do
|
|
||||||
checkCache <- C.lookup jwtCache token
|
|
||||||
authResult <- maybe parseJwt (pure . Right) checkCache
|
|
||||||
|
|
||||||
case (authResult,checkCache) of
|
|
||||||
-- From comment:
|
|
||||||
-- https://github.com/PostgREST/postgrest/pull/3801#discussion_r1857987914
|
|
||||||
--
|
|
||||||
-- We purge expired cache entries on a cache miss
|
|
||||||
-- The reasoning is that:
|
|
||||||
--
|
|
||||||
-- 1. We expect it to be rare (otherwise there is no point of the cache)
|
|
||||||
-- 2. It makes sure the cache is not growing (as inserting new entries
|
|
||||||
-- does garbage collection)
|
|
||||||
-- 3. Since this is time expiration based cache there is no real risk of
|
|
||||||
-- starvation - sooner or later we are going to have a cache miss.
|
|
||||||
|
|
||||||
(Right res, Nothing) -> do -- cache miss
|
|
||||||
|
|
||||||
let timeSpec = getTimeSpec res maxLifetime utc
|
|
||||||
|
|
||||||
-- insert new cache entry
|
|
||||||
C.insert' jwtCache (Just timeSpec) token res
|
|
||||||
|
|
||||||
-- Execute IO action to purge the cache
|
|
||||||
-- It is assumed this action returns immidiately
|
|
||||||
-- so that request processing is not blocked.
|
|
||||||
purgeCache
|
|
||||||
|
|
||||||
_ -> pure ()
|
|
||||||
|
|
||||||
return authResult
|
|
||||||
|
|
||||||
-- Used to extract JWT exp claim and add to JWT Cache
|
|
||||||
getTimeSpec :: AuthResult -> Int -> UTCTime -> TimeSpec
|
|
||||||
getTimeSpec res maxLifetime utc = do
|
|
||||||
let expireJSON = KM.lookup "exp" (authClaims res)
|
|
||||||
utcToSecs = floor . nominalDiffTimeToSeconds . utcTimeToPOSIXSeconds
|
|
||||||
sciToInt = fromMaybe 0 . Sci.toBoundedInteger
|
|
||||||
case expireJSON of
|
|
||||||
Just (JSON.Number seconds) -> TimeSpec (sciToInt seconds - utcToSecs utc) 0
|
|
||||||
_ -> TimeSpec (fromIntegral maxLifetime :: Int64) 0
|
|
||||||
|
|
||||||
-- | Empty the cache (done when the config is reloaded)
|
|
||||||
emptyCache :: JwtCacheState -> IO ()
|
|
||||||
emptyCache JwtCacheState{jwtCache} = C.purge jwtCache
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
module PostgREST.Auth.Types
|
|
||||||
( AuthResult (..) )
|
|
||||||
where
|
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
|
||||||
import qualified Data.Aeson.KeyMap as KM
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
|
|
||||||
-- | Parse result for JWT Claims
|
|
||||||
data AuthResult = AuthResult
|
|
||||||
{ authClaims :: KM.KeyMap JSON.Value
|
|
||||||
, authRole :: BS.ByteString
|
|
||||||
}
|
|
||||||
@@ -60,7 +60,7 @@ dumpSchema appState = do
|
|||||||
case result of
|
case result of
|
||||||
Left e -> do
|
Left e -> do
|
||||||
let observer = AppState.getObserver appState
|
let observer = AppState.getObserver appState
|
||||||
observer $ SchemaCacheErrorObs configDbSchemas configDbExtraSearchPath e
|
observer $ SchemaCacheErrorObs e
|
||||||
exitFailure
|
exitFailure
|
||||||
Right sCache -> return $ JSON.encode sCache
|
Right sCache -> return $ JSON.encode sCache
|
||||||
|
|
||||||
@@ -128,6 +128,9 @@ exampleConfigFile =
|
|||||||
[str|## Admin server used for checks. It's disabled by default unless a port is specified.
|
[str|## Admin server used for checks. It's disabled by default unless a port is specified.
|
||||||
|# admin-server-port = 3001
|
|# admin-server-port = 3001
|
||||||
|
|
|
|
||||||
|
|## Whether to enable the /config endpoint of the admin server
|
||||||
|
|# admin-server-config-enabled = false
|
||||||
|
|
|
||||||
|## The database role to use when no client authentication is provided
|
|## The database role to use when no client authentication is provided
|
||||||
|# db-anon-role = "anon"
|
|# db-anon-role = "anon"
|
||||||
|
|
|
|
||||||
@@ -209,9 +212,6 @@ exampleConfigFile =
|
|||||||
|## Logging level, the admitted values are: crit, error, warn, info and debug.
|
|## Logging level, the admitted values are: crit, error, warn, info and debug.
|
||||||
|log-level = "error"
|
|log-level = "error"
|
||||||
|
|
|
|
||||||
|## Log the requested SQL query at the current log-level.
|
|
||||||
|log-query = "disabled"
|
|
||||||
|
|
|
||||||
|## Determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely.
|
|## Determine if the OpenAPI output should follow or ignore role privileges or be disabled entirely.
|
||||||
|## Admitted values: follow-privileges, ignore-privileges, disabled
|
|## Admitted values: follow-privileges, ignore-privileges, disabled
|
||||||
|openapi-mode = "follow-privileges"
|
|openapi-mode = "follow-privileges"
|
||||||
|
|||||||
@@ -15,9 +15,7 @@ module PostgREST.Config
|
|||||||
, Environment
|
, Environment
|
||||||
, JSPath
|
, JSPath
|
||||||
, JSPathExp(..)
|
, JSPathExp(..)
|
||||||
, FilterExp(..)
|
|
||||||
, LogLevel(..)
|
, LogLevel(..)
|
||||||
, LogQuery(..)
|
|
||||||
, OpenAPIMode(..)
|
, OpenAPIMode(..)
|
||||||
, Proxy(..)
|
, Proxy(..)
|
||||||
, toText
|
, toText
|
||||||
@@ -30,25 +28,28 @@ module PostgREST.Config
|
|||||||
, addTargetSessionAttrs
|
, addTargetSessionAttrs
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import qualified Crypto.JOSE.Types as JOSE
|
||||||
|
import qualified Crypto.JWT as JWT
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.ByteString.Base64 as B64
|
import qualified Data.ByteString.Base64 as B64
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
import qualified Data.Configurator as C
|
import qualified Data.Configurator as C
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Encoding as T
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Jose.Jwa as JWT
|
|
||||||
import qualified Jose.Jwk as JWT
|
|
||||||
|
|
||||||
|
import Control.Lens (preview)
|
||||||
import Control.Monad (fail)
|
import Control.Monad (fail)
|
||||||
|
import Crypto.JWT (JWK, JWKSet, StringOrURI, stringOrUri)
|
||||||
|
import Data.Aeson (toJSON)
|
||||||
import Data.Either.Combinators (mapLeft)
|
import Data.Either.Combinators (mapLeft)
|
||||||
import Data.List (lookup)
|
import Data.List (lookup)
|
||||||
import Data.List.NonEmpty (fromList, toList)
|
import Data.List.NonEmpty (fromList, toList)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Scientific (floatingOrInteger)
|
import Data.Scientific (floatingOrInteger)
|
||||||
import Jose.Jwk (Jwk, JwkSet)
|
import Network.URI (escapeURIString,
|
||||||
import Network.URI (escapeURIString, isURI,
|
|
||||||
isUnescapedInURIComponent)
|
isUnescapedInURIComponent)
|
||||||
import Numeric (readOct, showOct)
|
import Numeric (readOct, showOct)
|
||||||
import System.Environment (getEnvironment)
|
import System.Environment (getEnvironment)
|
||||||
@@ -56,9 +57,8 @@ import System.Posix.Types (FileMode)
|
|||||||
|
|
||||||
import PostgREST.Config.Database (RoleIsolationLvl,
|
import PostgREST.Config.Database (RoleIsolationLvl,
|
||||||
RoleSettings)
|
RoleSettings)
|
||||||
import PostgREST.Config.JSPath (FilterExp (..), JSPath,
|
import PostgREST.Config.JSPath (JSPath, JSPathExp (..),
|
||||||
JSPathExp (..), dumpJSPath,
|
dumpJSPath, pRoleClaimKey)
|
||||||
pRoleClaimKey)
|
|
||||||
import PostgREST.Config.Proxy (Proxy (..),
|
import PostgREST.Config.Proxy (Proxy (..),
|
||||||
isMalformedProxyUri, toURI)
|
isMalformedProxyUri, toURI)
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi,
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier, dumpQi,
|
||||||
@@ -92,14 +92,13 @@ data AppConfig = AppConfig
|
|||||||
, configDbTxRollbackAll :: Bool
|
, configDbTxRollbackAll :: Bool
|
||||||
, configDbUri :: Text
|
, configDbUri :: Text
|
||||||
, configFilePath :: Maybe FilePath
|
, configFilePath :: Maybe FilePath
|
||||||
, configJWKS :: Maybe JwkSet
|
, configJWKS :: Maybe JWKSet
|
||||||
, configJwtAudience :: Maybe Text
|
, configJwtAudience :: Maybe StringOrURI
|
||||||
, configJwtRoleClaimKey :: JSPath
|
, configJwtRoleClaimKey :: JSPath
|
||||||
, configJwtSecret :: Maybe BS.ByteString
|
, configJwtSecret :: Maybe BS.ByteString
|
||||||
, configJwtSecretIsBase64 :: Bool
|
, configJwtSecretIsBase64 :: Bool
|
||||||
, configJwtCacheMaxLifetime :: Int
|
, configJwtCacheMaxLifetime :: Int
|
||||||
, configLogLevel :: LogLevel
|
, configLogLevel :: LogLevel
|
||||||
, configLogQuery :: LogQuery
|
|
||||||
, configOpenApiMode :: OpenAPIMode
|
, configOpenApiMode :: OpenAPIMode
|
||||||
, configOpenApiSecurityActive :: Bool
|
, configOpenApiSecurityActive :: Bool
|
||||||
, configOpenApiServerProxyUri :: Maybe Text
|
, configOpenApiServerProxyUri :: Maybe Text
|
||||||
@@ -110,8 +109,8 @@ data AppConfig = AppConfig
|
|||||||
, configServerTimingEnabled :: Bool
|
, configServerTimingEnabled :: Bool
|
||||||
, configServerUnixSocket :: Maybe FilePath
|
, configServerUnixSocket :: Maybe FilePath
|
||||||
, configServerUnixSocketMode :: FileMode
|
, configServerUnixSocketMode :: FileMode
|
||||||
, configAdminServerHost :: Text
|
|
||||||
, configAdminServerPort :: Maybe Int
|
, configAdminServerPort :: Maybe Int
|
||||||
|
, configAdminServerConfigEnabled :: Bool
|
||||||
, configRoleSettings :: RoleSettings
|
, configRoleSettings :: RoleSettings
|
||||||
, configRoleIsoLvl :: RoleIsolationLvl
|
, configRoleIsoLvl :: RoleIsolationLvl
|
||||||
, configInternalSCSleep :: Maybe Int32
|
, configInternalSCSleep :: Maybe Int32
|
||||||
@@ -128,14 +127,6 @@ dumpLogLevel = \case
|
|||||||
LogInfo -> "info"
|
LogInfo -> "info"
|
||||||
LogDebug -> "debug"
|
LogDebug -> "debug"
|
||||||
|
|
||||||
data LogQuery = LogQueryMain | LogQueryDisabled
|
|
||||||
deriving (Eq)
|
|
||||||
|
|
||||||
dumpLogQuery :: LogQuery -> Text
|
|
||||||
dumpLogQuery = \case
|
|
||||||
LogQueryMain -> "main-query"
|
|
||||||
LogQueryDisabled -> "disabled"
|
|
||||||
|
|
||||||
data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
|
data OpenAPIMode = OAFollowPriv | OAIgnorePriv | OADisabled
|
||||||
deriving Eq
|
deriving Eq
|
||||||
|
|
||||||
@@ -173,13 +164,12 @@ toText conf =
|
|||||||
,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig)
|
,("db-pre-config", q . maybe mempty dumpQi . configDbPreConfig)
|
||||||
,("db-tx-end", q . showTxEnd)
|
,("db-tx-end", q . showTxEnd)
|
||||||
,("db-uri", q . configDbUri)
|
,("db-uri", q . configDbUri)
|
||||||
,("jwt-aud", q . fromMaybe mempty . configJwtAudience)
|
,("jwt-aud", T.decodeUtf8 . LBS.toStrict . JSON.encode . maybe "" toJSON . configJwtAudience)
|
||||||
,("jwt-role-claim-key", q . T.intercalate mempty . fmap dumpJSPath . configJwtRoleClaimKey)
|
,("jwt-role-claim-key", q . T.intercalate mempty . fmap dumpJSPath . configJwtRoleClaimKey)
|
||||||
,("jwt-secret", q . T.decodeUtf8 . showJwtSecret)
|
,("jwt-secret", q . T.decodeUtf8 . showJwtSecret)
|
||||||
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
|
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
|
||||||
,("jwt-cache-max-lifetime", show . configJwtCacheMaxLifetime)
|
,("jwt-cache-max-lifetime", show . configJwtCacheMaxLifetime)
|
||||||
,("log-level", q . dumpLogLevel . configLogLevel)
|
,("log-level", q . dumpLogLevel . configLogLevel)
|
||||||
,("log-query", q . dumpLogQuery . configLogQuery)
|
|
||||||
,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode)
|
,("openapi-mode", q . dumpOpenApiMode . configOpenApiMode)
|
||||||
,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive)
|
,("openapi-security-active", T.toLower . show . configOpenApiSecurityActive)
|
||||||
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
|
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
|
||||||
@@ -190,8 +180,8 @@ toText conf =
|
|||||||
,("server-timing-enabled", T.toLower . show . configServerTimingEnabled)
|
,("server-timing-enabled", T.toLower . show . configServerTimingEnabled)
|
||||||
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
|
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
|
||||||
,("server-unix-socket-mode", q . T.pack . showSocketMode)
|
,("server-unix-socket-mode", q . T.pack . showSocketMode)
|
||||||
,("admin-server-host", q . configAdminServerHost)
|
|
||||||
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
|
,("admin-server-port", maybe "\"\"" show . configAdminServerPort)
|
||||||
|
,("admin-server-config-enabled", T.toLower . show . configAdminServerConfigEnabled)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- quote all app.settings
|
-- quote all app.settings
|
||||||
@@ -235,18 +225,16 @@ readAppConfig dbSettings optPath prevDbUri roleSettings roleIsolationLvl = do
|
|||||||
Left err ->
|
Left err ->
|
||||||
return . Left $ "Error in config " <> err
|
return . Left $ "Error in config " <> err
|
||||||
Right parsedConfig ->
|
Right parsedConfig ->
|
||||||
mapLeft show <$> decodeLoadFiles parsedConfig
|
Right <$> decodeLoadFiles parsedConfig
|
||||||
where
|
where
|
||||||
-- Both C.ParseError and IOError are shown here
|
-- Both C.ParseError and IOError are shown here
|
||||||
loadConfig :: FilePath -> IO (Either SomeException C.Config)
|
loadConfig :: FilePath -> IO (Either SomeException C.Config)
|
||||||
loadConfig = try . C.load
|
loadConfig = try . C.load
|
||||||
|
|
||||||
decodeLoadFiles :: AppConfig -> IO (Either IOException AppConfig)
|
decodeLoadFiles :: AppConfig -> IO AppConfig
|
||||||
decodeLoadFiles parsedConfig = try $
|
decodeLoadFiles parsedConfig =
|
||||||
decodeJWKS =<<
|
decodeJWKS <$>
|
||||||
decodeSecret =<<
|
(decodeSecret =<< readSecretFile =<< readDbUriFile prevDbUri parsedConfig)
|
||||||
readSecretFile =<<
|
|
||||||
readDbUriFile prevDbUri parsedConfig
|
|
||||||
|
|
||||||
parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> RoleIsolationLvl -> C.Parser C.Config AppConfig
|
parser :: Maybe FilePath -> Environment -> [(Text, Text)] -> RoleSettings -> RoleIsolationLvl -> C.Parser C.Config AppConfig
|
||||||
parser optPath env dbSettings roleSettings roleIsolationLvl =
|
parser optPath env dbSettings roleSettings roleIsolationLvl =
|
||||||
@@ -256,8 +244,8 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
|
<*> (fmap encodeUtf8 <$> optString "db-anon-role")
|
||||||
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
<*> (fromMaybe "pgrst" <$> optString "db-channel")
|
||||||
<*> (fromMaybe True <$> optBool "db-channel-enabled")
|
<*> (fromMaybe True <$> optBool "db-channel-enabled")
|
||||||
<*> (maybe ["public"] splitOnCommasEmptyable <$> optStringEmptyable "db-extra-search-path")
|
<*> (maybe ["public"] splitOnCommas <$> optValue "db-extra-search-path")
|
||||||
<*> (maybe defaultHoistedAllowList splitOnCommas <$> optString "db-hoisted-tx-settings")
|
<*> (maybe defaultHoistedAllowList splitOnCommas <$> optValue "db-hoisted-tx-settings")
|
||||||
<*> optWithAlias (optInt "db-max-rows")
|
<*> optWithAlias (optInt "db-max-rows")
|
||||||
(optInt "max-rows")
|
(optInt "max-rows")
|
||||||
<*> (fromMaybe False <$> optBool "db-plan-enabled")
|
<*> (fromMaybe False <$> optBool "db-plan-enabled")
|
||||||
@@ -272,8 +260,8 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
<*> (fromMaybe True <$> optBool "db-prepared-statements")
|
||||||
<*> (fmap toQi <$> optWithAlias (optString "db-root-spec")
|
<*> (fmap toQi <$> optWithAlias (optString "db-root-spec")
|
||||||
(optString "root-spec"))
|
(optString "root-spec"))
|
||||||
<*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optString "db-schemas")
|
<*> (fromList . maybe ["public"] splitOnCommas <$> optWithAlias (optValue "db-schemas")
|
||||||
(optString "db-schema"))
|
(optValue "db-schema"))
|
||||||
<*> (fromMaybe True <$> optBool "db-config")
|
<*> (fromMaybe True <$> optBool "db-config")
|
||||||
<*> (fmap toQi <$> optString "db-pre-config")
|
<*> (fmap toQi <$> optString "db-pre-config")
|
||||||
<*> parseTxEnd "db-tx-end" snd
|
<*> parseTxEnd "db-tx-end" snd
|
||||||
@@ -281,7 +269,7 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
|
<*> (fromMaybe "postgresql://" <$> optString "db-uri")
|
||||||
<*> pure optPath
|
<*> pure optPath
|
||||||
<*> pure Nothing
|
<*> pure Nothing
|
||||||
<*> optStringOrURI "jwt-aud"
|
<*> parseJwtAudience "jwt-aud"
|
||||||
<*> parseRoleClaimKey "jwt-role-claim-key" "role-claim-key"
|
<*> parseRoleClaimKey "jwt-role-claim-key" "role-claim-key"
|
||||||
<*> (fmap encodeUtf8 <$> optString "jwt-secret")
|
<*> (fmap encodeUtf8 <$> optString "jwt-secret")
|
||||||
<*> (fromMaybe False <$> optWithAlias
|
<*> (fromMaybe False <$> optWithAlias
|
||||||
@@ -289,20 +277,18 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
(optBool "secret-is-base64"))
|
(optBool "secret-is-base64"))
|
||||||
<*> (fromMaybe 0 <$> optInt "jwt-cache-max-lifetime")
|
<*> (fromMaybe 0 <$> optInt "jwt-cache-max-lifetime")
|
||||||
<*> parseLogLevel "log-level"
|
<*> parseLogLevel "log-level"
|
||||||
<*> parseLogQuery "log-query"
|
|
||||||
<*> parseOpenAPIMode "openapi-mode"
|
<*> parseOpenAPIMode "openapi-mode"
|
||||||
<*> (fromMaybe False <$> optBool "openapi-security-active")
|
<*> (fromMaybe False <$> optBool "openapi-security-active")
|
||||||
<*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
|
<*> parseOpenAPIServerProxyURI "openapi-server-proxy-uri"
|
||||||
<*> parseCORSAllowedOrigins "server-cors-allowed-origins"
|
<*> parseCORSAllowedOrigins "server-cors-allowed-origins"
|
||||||
<*> (defaultServerHost <$> optString "server-host")
|
<*> (fromMaybe "!4" <$> optString "server-host")
|
||||||
<*> parseServerPort "server-port"
|
<*> (fromMaybe 3000 <$> optInt "server-port")
|
||||||
<*> (fmap (CI.mk . encodeUtf8) <$> optString "server-trace-header")
|
<*> (fmap (CI.mk . encodeUtf8) <$> optString "server-trace-header")
|
||||||
<*> (fromMaybe False <$> optBool "server-timing-enabled")
|
<*> (fromMaybe False <$> optBool "server-timing-enabled")
|
||||||
<*> (fmap T.unpack <$> optString "server-unix-socket")
|
<*> (fmap T.unpack <$> optString "server-unix-socket")
|
||||||
<*> parseSocketFileMode "server-unix-socket-mode"
|
<*> parseSocketFileMode "server-unix-socket-mode"
|
||||||
<*> (defaultServerHost <$> optWithAlias (optString "admin-server-host")
|
<*> optInt "admin-server-port"
|
||||||
(optString "server-host"))
|
<*> (fromMaybe False <$> optBool "admin-server-config-enabled")
|
||||||
<*> parseAdminServerPort "admin-server-port"
|
|
||||||
<*> pure roleSettings
|
<*> pure roleSettings
|
||||||
<*> pure roleIsolationLvl
|
<*> pure roleIsolationLvl
|
||||||
<*> optInt "internal-schema-cache-sleep"
|
<*> optInt "internal-schema-cache-sleep"
|
||||||
@@ -314,17 +300,6 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
fromEnv = M.mapKeys fromJust $ M.filterWithKey (\k _ -> isJust k) $ M.mapKeys normalize env
|
fromEnv = M.mapKeys fromJust $ M.filterWithKey (\k _ -> isJust k) $ M.mapKeys normalize env
|
||||||
normalize k = ("app.settings." <>) <$> T.stripPrefix "PGRST_APP_SETTINGS_" (toS k)
|
normalize k = ("app.settings." <>) <$> T.stripPrefix "PGRST_APP_SETTINGS_" (toS k)
|
||||||
|
|
||||||
parseServerPort :: C.Key -> C.Parser C.Config Int
|
|
||||||
parseServerPort k = fromMaybe 3000 <$> optInt k
|
|
||||||
|
|
||||||
parseAdminServerPort :: C.Key -> C.Parser C.Config (Maybe Int)
|
|
||||||
parseAdminServerPort k = do
|
|
||||||
serverPort <- parseServerPort "server-port"
|
|
||||||
optInt k >>= \case
|
|
||||||
Nothing -> pure Nothing
|
|
||||||
Just asp | asp == serverPort -> fail "admin-server-port cannot be the same as server-port"
|
|
||||||
| otherwise -> pure $ Just asp
|
|
||||||
|
|
||||||
parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
|
parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
|
||||||
parseSocketFileMode k =
|
parseSocketFileMode k =
|
||||||
optString k >>= \case
|
optString k >>= \case
|
||||||
@@ -354,6 +329,14 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
Just val | isMalformedProxyUri val -> fail "Malformed proxy uri, a correct example: https://example.com:8443/basePath"
|
Just val | isMalformedProxyUri val -> fail "Malformed proxy uri, a correct example: https://example.com:8443/basePath"
|
||||||
| otherwise -> pure $ Just val
|
| otherwise -> pure $ Just val
|
||||||
|
|
||||||
|
parseJwtAudience :: C.Key -> C.Parser C.Config (Maybe StringOrURI)
|
||||||
|
parseJwtAudience k =
|
||||||
|
optString k >>= \case
|
||||||
|
Nothing -> pure Nothing -- no audience in config file
|
||||||
|
Just aud -> case preview stringOrUri (T.unpack aud) of
|
||||||
|
Nothing -> fail "Invalid Jwt audience. Check your configuration."
|
||||||
|
aud' -> pure aud'
|
||||||
|
|
||||||
parseLogLevel :: C.Key -> C.Parser C.Config LogLevel
|
parseLogLevel :: C.Key -> C.Parser C.Config LogLevel
|
||||||
parseLogLevel k =
|
parseLogLevel k =
|
||||||
optString k >>= \case
|
optString k >>= \case
|
||||||
@@ -365,14 +348,6 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
Just "debug" -> pure LogDebug
|
Just "debug" -> pure LogDebug
|
||||||
Just _ -> fail "Invalid logging level. Check your configuration."
|
Just _ -> fail "Invalid logging level. Check your configuration."
|
||||||
|
|
||||||
parseLogQuery :: C.Key -> C.Parser C.Config LogQuery
|
|
||||||
parseLogQuery k =
|
|
||||||
optString k >>= \case
|
|
||||||
Nothing -> pure LogQueryDisabled
|
|
||||||
Just "disabled" -> pure LogQueryDisabled
|
|
||||||
Just "main-query" -> pure LogQueryMain
|
|
||||||
Just _ -> fail "Invalid SQL logging value. Check your configuration."
|
|
||||||
|
|
||||||
parseTxEnd :: C.Key -> ((Bool, Bool) -> Bool) -> C.Parser C.Config Bool
|
parseTxEnd :: C.Key -> ((Bool, Bool) -> Bool) -> C.Parser C.Config Bool
|
||||||
parseTxEnd k f =
|
parseTxEnd k f =
|
||||||
optString k >>= \case
|
optString k >>= \case
|
||||||
@@ -404,22 +379,8 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
optString :: C.Key -> C.Parser C.Config (Maybe Text)
|
optString :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||||
optString k = mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
optString k = mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
||||||
|
|
||||||
optStringEmptyable :: C.Key -> C.Parser C.Config (Maybe Text)
|
optValue :: C.Key -> C.Parser C.Config (Maybe C.Value)
|
||||||
optStringEmptyable k = overrideFromDbOrEnvironment C.optional k coerceText
|
optValue k = overrideFromDbOrEnvironment C.optional k identity
|
||||||
|
|
||||||
optStringOrURI :: C.Key -> C.Parser C.Config (Maybe Text)
|
|
||||||
optStringOrURI k = do
|
|
||||||
stringOrURI <- mfilter (/= "") <$> overrideFromDbOrEnvironment C.optional k coerceText
|
|
||||||
-- If the string contains ':' then it should
|
|
||||||
-- be a valid URI according to RFC 3986
|
|
||||||
case stringOrURI of
|
|
||||||
Just s -> if T.isInfixOf ":" s then validateURI s else return (Just s)
|
|
||||||
Nothing -> return Nothing
|
|
||||||
where
|
|
||||||
validateURI :: Text -> C.Parser C.Config (Maybe Text)
|
|
||||||
validateURI s = if isURI (T.unpack s)
|
|
||||||
then return $ Just s
|
|
||||||
else fail "jwt-aud should be a string or a valid URI"
|
|
||||||
|
|
||||||
optInt :: (Read i, Integral i) => C.Key -> C.Parser C.Config (Maybe i)
|
optInt :: (Read i, Integral i) => C.Key -> C.Parser C.Config (Maybe i)
|
||||||
optInt k = join <$> overrideFromDbOrEnvironment C.optional k coerceInt
|
optInt k = join <$> overrideFromDbOrEnvironment C.optional k coerceInt
|
||||||
@@ -459,18 +420,12 @@ parser optPath env dbSettings roleSettings roleIsolationLvl =
|
|||||||
Nothing -> (> 0) <$> (readMaybe s :: Maybe Integer)
|
Nothing -> (> 0) <$> (readMaybe s :: Maybe Integer)
|
||||||
coerceBool _ = Nothing
|
coerceBool _ = Nothing
|
||||||
|
|
||||||
splitOnCommas :: Text -> [Text]
|
splitOnCommas :: C.Value -> [Text]
|
||||||
splitOnCommas s = T.strip <$> T.splitOn "," s
|
splitOnCommas (C.String s) = T.strip <$> T.splitOn "," s
|
||||||
|
splitOnCommas _ = []
|
||||||
splitOnCommasEmptyable :: Text -> [Text]
|
|
||||||
splitOnCommasEmptyable "" = []
|
|
||||||
splitOnCommasEmptyable s = T.strip <$> T.splitOn "," s
|
|
||||||
|
|
||||||
defaultHoistedAllowList = ["statement_timeout","plan_filter.statement_cost_limit","default_transaction_isolation"]
|
defaultHoistedAllowList = ["statement_timeout","plan_filter.statement_cost_limit","default_transaction_isolation"]
|
||||||
|
|
||||||
defaultServerHost :: Maybe Text -> Text
|
|
||||||
defaultServerHost = fromMaybe "!4"
|
|
||||||
|
|
||||||
-- | Read the JWT secret from a file if configJwtSecret is actually a
|
-- | Read the JWT secret from a file if configJwtSecret is actually a
|
||||||
-- filepath(has @ as its prefix). To check if the JWT secret is provided is
|
-- filepath(has @ as its prefix). To check if the JWT secret is provided is
|
||||||
-- in fact a file path, it must be decoded as 'Text' to be processed.
|
-- in fact a file path, it must be decoded as 'Text' to be processed.
|
||||||
@@ -495,30 +450,24 @@ decodeSecret conf@AppConfig{..} =
|
|||||||
decodeB64 = B64.decode . encodeUtf8 . T.strip . replaceUrlChars . decodeUtf8
|
decodeB64 = B64.decode . encodeUtf8 . T.strip . replaceUrlChars . decodeUtf8
|
||||||
replaceUrlChars = T.replace "_" "/" . T.replace "-" "+" . T.replace "." "="
|
replaceUrlChars = T.replace "_" "/" . T.replace "-" "+" . T.replace "." "="
|
||||||
|
|
||||||
-- | Parse `jwt-secret` configuration option and turn into a JWKS.
|
-- | Parse `jwt-secret` configuration option and turn into a JWKSet.
|
||||||
--
|
--
|
||||||
-- There are three ways to specify `jwt-secret`: text secret, JSON Web Key
|
-- There are three ways to specify `jwt-secret`: text secret, JSON Web Key
|
||||||
-- (JWK), or JSON Web Key Set (JWKS). The first two are converted into a JwkSet
|
-- (JWK), or JSON Web Key Set (JWKS). The first two are converted into a JWKSet
|
||||||
-- with one key and the last is converted as is.
|
-- with one key and the last is converted as is.
|
||||||
decodeJWKS :: AppConfig -> IO AppConfig
|
decodeJWKS :: AppConfig -> AppConfig
|
||||||
decodeJWKS conf = do
|
decodeJWKS conf =
|
||||||
jwks <- case configJwtSecret conf of
|
conf { configJWKS = parseSecret <$> configJwtSecret conf }
|
||||||
Just s -> either fail (pure . Just) $ parseSecret s
|
|
||||||
Nothing -> pure Nothing
|
|
||||||
return $ conf { configJWKS = jwks }
|
|
||||||
|
|
||||||
parseSecret :: ByteString -> Either [Char] JwkSet
|
parseSecret :: ByteString -> JWKSet
|
||||||
parseSecret bytes =
|
parseSecret bytes =
|
||||||
case maybeJWKSet of
|
fromMaybe (maybe secret (\jwk' -> JWT.JWKSet [jwk']) maybeJWK)
|
||||||
Just jwk -> Right jwk
|
maybeJWKSet
|
||||||
Nothing -> maybe validateSecret (\jwk' -> Right $ JWT.JwkSet [jwk']) maybeJWK
|
|
||||||
where
|
where
|
||||||
maybeJWKSet = JSON.decodeStrict bytes :: Maybe JwkSet
|
maybeJWKSet = JSON.decodeStrict bytes :: Maybe JWKSet
|
||||||
maybeJWK = JSON.decodeStrict bytes :: Maybe Jwk
|
maybeJWK = JSON.decodeStrict bytes :: Maybe JWK
|
||||||
secret = JWT.JwkSet [JWT.SymmetricJwk bytes Nothing (Just JWT.Sig) (Just $ JWT.Signed JWT.HS256)]
|
secret = JWT.JWKSet [JWT.fromKeyMaterial keyMaterial]
|
||||||
validateSecret
|
keyMaterial = JWT.OctKeyMaterial . JWT.OctKeyParameters $ JOSE.Base64Octets bytes
|
||||||
| BS.length bytes < 32 = Left "The JWT secret must be at least 32 characters long."
|
|
||||||
| otherwise = Right secret
|
|
||||||
|
|
||||||
-- | Read database uri from a separate file if `db-uri` is a filepath.
|
-- | Read database uri from a separate file if `db-uri` is a filepath.
|
||||||
readDbUriFile :: Maybe Text -> AppConfig -> IO AppConfig
|
readDbUriFile :: Maybe Text -> AppConfig -> IO AppConfig
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import qualified Hasql.Statement as SQL
|
|||||||
import qualified Hasql.Transaction as SQL
|
import qualified Hasql.Transaction as SQL
|
||||||
import qualified Hasql.Transaction.Sessions as SQL
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
|
|
||||||
import NeatInterpolation (trimming)
|
import Text.InterpolatedString.Perl6 (q, qc)
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ queryDbSettings preConfFunc prepared =
|
|||||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
||||||
transaction SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings prepared
|
transaction SQL.ReadCommitted SQL.Read $ SQL.statement dbSettingsNames $ SQL.Statement sql (arrayParam HE.text) decodeSettings prepared
|
||||||
where
|
where
|
||||||
sql = encodeUtf8 [trimming|
|
sql = [qc|
|
||||||
WITH
|
WITH
|
||||||
role_setting AS (
|
role_setting AS (
|
||||||
SELECT setdatabase as database,
|
SELECT setdatabase as database,
|
||||||
@@ -109,25 +109,25 @@ queryDbSettings preConfFunc prepared =
|
|||||||
substr(setting, 1, strpos(setting, '=') - 1) as k,
|
substr(setting, 1, strpos(setting, '=') - 1) as k,
|
||||||
substr(setting, strpos(setting, '=') + 1) as v
|
substr(setting, strpos(setting, '=') + 1) as v
|
||||||
FROM role_setting
|
FROM role_setting
|
||||||
${preConfigF}
|
{preConfigF}
|
||||||
)
|
)
|
||||||
SELECT DISTINCT ON (key)
|
SELECT DISTINCT ON (key)
|
||||||
replace(k, '${prefix}', '') AS key,
|
replace(k, '{prefix}', '') AS key,
|
||||||
v AS value
|
v AS value
|
||||||
FROM kv_settings
|
FROM kv_settings
|
||||||
WHERE k = ANY($$1) AND v IS NOT NULL
|
WHERE k = ANY($1) AND v IS NOT NULL
|
||||||
ORDER BY key, database DESC NULLS LAST;
|
ORDER BY key, database DESC NULLS LAST;
|
||||||
|]
|
|]
|
||||||
preConfigF = case preConfFunc of
|
preConfigF = case preConfFunc of
|
||||||
Nothing -> mempty
|
Nothing -> mempty
|
||||||
Just func -> [trimming|
|
Just func -> [qc|
|
||||||
UNION
|
UNION
|
||||||
SELECT
|
SELECT
|
||||||
null as database,
|
null as database,
|
||||||
x as k,
|
x as k,
|
||||||
current_setting(x, true) as v
|
current_setting(x, true) as v
|
||||||
FROM unnest($$1) x
|
FROM unnest($1) x
|
||||||
JOIN ${func}() _ ON TRUE
|
JOIN {func}() _ ON TRUE
|
||||||
|]::Text
|
|]::Text
|
||||||
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ queryRoleSettings pgVer prepared =
|
|||||||
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
||||||
transaction SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) prepared
|
transaction SQL.ReadCommitted SQL.Read $ SQL.statement mempty $ SQL.Statement sql HE.noParams (processRows <$> rows) prepared
|
||||||
where
|
where
|
||||||
sql = encodeUtf8 [trimming|
|
sql = [q|
|
||||||
with
|
with
|
||||||
role_setting as (
|
role_setting as (
|
||||||
select r.rolname, unnest(r.rolconfig) as setting
|
select r.rolname, unnest(r.rolconfig) as setting
|
||||||
@@ -161,15 +161,14 @@ queryRoleSettings pgVer prepared =
|
|||||||
i.value as iso_lvl,
|
i.value as iso_lvl,
|
||||||
coalesce(array_agg(row(kv.key, kv.value)) filter (where key <> 'default_transaction_isolation'), '{}') as role_settings
|
coalesce(array_agg(row(kv.key, kv.value)) filter (where key <> 'default_transaction_isolation'), '{}') as role_settings
|
||||||
from kv_settings kv
|
from kv_settings kv
|
||||||
join pg_settings ps on ps.name = kv.key and (ps.context = 'user' ${hasParameterPrivilege})
|
join pg_settings ps on ps.name = kv.key |] <>
|
||||||
|
(if pgVer >= pgVersion150
|
||||||
|
then "and (ps.context = 'user' or has_parameter_privilege(current_user::regrole::oid, ps.name, 'set')) "
|
||||||
|
else "and ps.context = 'user' ") <> [q|
|
||||||
left join iso_setting i on i.rolname = kv.rolname
|
left join iso_setting i on i.rolname = kv.rolname
|
||||||
group by kv.rolname, i.value;
|
group by kv.rolname, i.value;
|
||||||
|]
|
|]
|
||||||
|
|
||||||
hasParameterPrivilege
|
|
||||||
| pgVer >= pgVersion150 = "or has_parameter_privilege(current_user::regrole::oid, ps.name, 'set')"
|
|
||||||
| otherwise = ""
|
|
||||||
|
|
||||||
processRows :: [(Text, Maybe Text, [(Text, Text)])] -> (RoleSettings, RoleIsolationLvl)
|
processRows :: [(Text, Maybe Text, [(Text, Text)])] -> (RoleSettings, RoleIsolationLvl)
|
||||||
processRows rs =
|
processRows rs =
|
||||||
let
|
let
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
{-# OPTIONS_GHC -Wno-unused-do-bind #-}
|
|
||||||
module PostgREST.Config.JSPath
|
module PostgREST.Config.JSPath
|
||||||
( JSPath
|
( JSPath
|
||||||
, JSPathExp(..)
|
, JSPathExp(..)
|
||||||
, FilterExp(..)
|
|
||||||
, dumpJSPath
|
, dumpJSPath
|
||||||
, pRoleClaimKey
|
, pRoleClaimKey
|
||||||
) where
|
) where
|
||||||
@@ -16,37 +14,18 @@ import Text.Read (read)
|
|||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
-- | full jspath, e.g. .property[0].attr.detail[?(@ == "role1")]
|
-- | full jspath, e.g. .property[0].attr.detail
|
||||||
type JSPath = [JSPathExp]
|
type JSPath = [JSPathExp]
|
||||||
|
|
||||||
-- NOTE: We only accept one JSPFilter expr (at the end of input)
|
-- | jspath expression, e.g. .property, .property[0] or ."property-dash"
|
||||||
-- | jspath expression
|
|
||||||
data JSPathExp
|
data JSPathExp
|
||||||
= JSPKey Text -- .property or ."property-dash"
|
= JSPKey Text
|
||||||
| JSPIdx Int -- [0]
|
| JSPIdx Int
|
||||||
| JSPFilter FilterExp -- [?(@ == "match")]
|
|
||||||
|
|
||||||
data FilterExp
|
|
||||||
= EqualsCond Text
|
|
||||||
| NotEqualsCond Text
|
|
||||||
| StartsWithCond Text
|
|
||||||
| EndsWithCond Text
|
|
||||||
| ContainsCond Text
|
|
||||||
|
|
||||||
dumpJSPath :: JSPathExp -> Text
|
dumpJSPath :: JSPathExp -> Text
|
||||||
-- TODO: this needs to be quoted properly for special chars
|
-- TODO: this needs to be quoted properly for special chars
|
||||||
dumpJSPath (JSPKey k) = "." <> show k
|
dumpJSPath (JSPKey k) = "." <> show k
|
||||||
dumpJSPath (JSPIdx i) = "[" <> show i <> "]"
|
dumpJSPath (JSPIdx i) = "[" <> show i <> "]"
|
||||||
dumpJSPath (JSPFilter cond) = "[?(@" <> expr <> ")]"
|
|
||||||
where
|
|
||||||
expr =
|
|
||||||
case cond of
|
|
||||||
EqualsCond text -> " == " <> show text
|
|
||||||
NotEqualsCond text -> " != " <> show text
|
|
||||||
StartsWithCond text -> " ^== " <> show text
|
|
||||||
EndsWithCond text -> " ==^ " <> show text
|
|
||||||
ContainsCond text -> " *== " <> show text
|
|
||||||
|
|
||||||
|
|
||||||
-- Used for the config value "role-claim-key"
|
-- Used for the config value "role-claim-key"
|
||||||
pRoleClaimKey :: Text -> Either Text JSPath
|
pRoleClaimKey :: Text -> Either Text JSPath
|
||||||
@@ -54,47 +33,19 @@ pRoleClaimKey selStr =
|
|||||||
mapLeft show $ P.parse pJSPath ("failed to parse role-claim-key value (" <> toS selStr <> ")") (toS selStr)
|
mapLeft show $ P.parse pJSPath ("failed to parse role-claim-key value (" <> toS selStr <> ")") (toS selStr)
|
||||||
|
|
||||||
pJSPath :: P.Parser JSPath
|
pJSPath :: P.Parser JSPath
|
||||||
pJSPath = P.many1 pJSPathExp <* P.eof
|
pJSPath = toJSPath <$> (period *> pPath `P.sepBy` period <* P.eof)
|
||||||
|
where
|
||||||
|
toJSPath :: [(Text, Maybe Int)] -> JSPath
|
||||||
|
toJSPath = concatMap (\(key, idx) -> JSPKey key : maybeToList (JSPIdx <$> idx))
|
||||||
|
period = P.char '.' <?> "period (.)"
|
||||||
|
pPath :: P.Parser (Text, Maybe Int)
|
||||||
|
pPath = (,) <$> pJSPKey <*> P.optionMaybe pJSPIdx
|
||||||
|
|
||||||
pJSPathExp :: P.Parser JSPathExp
|
pJSPKey :: P.Parser Text
|
||||||
pJSPathExp = pJSPKey <|> pJSPFilter <|> pJSPIdx
|
pJSPKey = toS <$> P.many1 (P.alphaNum <|> P.oneOf "_$@") <|> pQuotedValue <?> "attribute name [a..z0..9_$@])"
|
||||||
|
|
||||||
pJSPKey :: P.Parser JSPathExp
|
pJSPIdx :: P.Parser Int
|
||||||
pJSPKey = do
|
pJSPIdx = P.char '[' *> (read <$> P.many1 P.digit) <* P.char ']' <?> "array index [0..n]"
|
||||||
P.char '.'
|
|
||||||
val <- toS <$> P.many1 (P.alphaNum <|> P.oneOf "_$@") <|> pQuotedValue
|
|
||||||
return (JSPKey val) <?> "pJSPKey: JSPath attribute key"
|
|
||||||
|
|
||||||
pJSPIdx :: P.Parser JSPathExp
|
|
||||||
pJSPIdx = do
|
|
||||||
P.char '['
|
|
||||||
num <- read <$> P.many1 P.digit
|
|
||||||
P.char ']'
|
|
||||||
return (JSPIdx num) <?> "pJSPIdx: JSPath array index"
|
|
||||||
|
|
||||||
pJSPFilter :: P.Parser JSPathExp
|
|
||||||
pJSPFilter = do
|
|
||||||
P.try $ P.string "[?("
|
|
||||||
condition <- pFilterConditionParser
|
|
||||||
P.char ')'
|
|
||||||
P.char ']'
|
|
||||||
P.eof -- this should be the last jspath expression
|
|
||||||
return (JSPFilter condition) <?> "pJSPFilter: JSPath filter exp"
|
|
||||||
|
|
||||||
pFilterConditionParser :: P.Parser FilterExp
|
|
||||||
pFilterConditionParser = do
|
|
||||||
P.char '@'
|
|
||||||
P.spaces
|
|
||||||
filt <- matchOperator
|
|
||||||
P.spaces
|
|
||||||
filt <$> pQuotedValue
|
|
||||||
where
|
|
||||||
matchOperator =
|
|
||||||
P.try (P.string "==^" $> EndsWithCond)
|
|
||||||
<|> P.try (P.string "==" $> EqualsCond)
|
|
||||||
<|> P.try (P.string "!=" $> NotEqualsCond)
|
|
||||||
<|> P.try (P.string "^==" $> StartsWithCond)
|
|
||||||
<|> P.try (P.string "*==" $> ContainsCond)
|
|
||||||
|
|
||||||
pQuotedValue :: P.Parser Text
|
pQuotedValue :: P.Parser Text
|
||||||
pQuotedValue = toS <$> (P.char '"' *> P.many (P.noneOf "\"") <* P.char '"')
|
pQuotedValue = toS <$> (P.char '"' *> P.many (P.noneOf "\"") <* P.char '"')
|
||||||
|
|||||||
@@ -3,10 +3,17 @@
|
|||||||
module PostgREST.Config.PgVersion
|
module PostgREST.Config.PgVersion
|
||||||
( PgVersion(..)
|
( PgVersion(..)
|
||||||
, minimumPgVersion
|
, minimumPgVersion
|
||||||
|
, pgVersion96
|
||||||
|
, pgVersion100
|
||||||
|
, pgVersion109
|
||||||
|
, pgVersion110
|
||||||
|
, pgVersion112
|
||||||
|
, pgVersion114
|
||||||
|
, pgVersion120
|
||||||
|
, pgVersion121
|
||||||
, pgVersion130
|
, pgVersion130
|
||||||
, pgVersion140
|
, pgVersion140
|
||||||
, pgVersion150
|
, pgVersion150
|
||||||
, pgVersion170
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
@@ -26,7 +33,28 @@ instance Ord PgVersion where
|
|||||||
|
|
||||||
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
||||||
minimumPgVersion :: PgVersion
|
minimumPgVersion :: PgVersion
|
||||||
minimumPgVersion = pgVersion121
|
minimumPgVersion = pgVersion96
|
||||||
|
|
||||||
|
pgVersion96 :: PgVersion
|
||||||
|
pgVersion96 = PgVersion 90600 "9.6" "9.6"
|
||||||
|
|
||||||
|
pgVersion100 :: PgVersion
|
||||||
|
pgVersion100 = PgVersion 100000 "10" "10"
|
||||||
|
|
||||||
|
pgVersion109 :: PgVersion
|
||||||
|
pgVersion109 = PgVersion 100009 "10.9" "10.9"
|
||||||
|
|
||||||
|
pgVersion110 :: PgVersion
|
||||||
|
pgVersion110 = PgVersion 110000 "11.0" "11.0"
|
||||||
|
|
||||||
|
pgVersion112 :: PgVersion
|
||||||
|
pgVersion112 = PgVersion 110002 "11.2" "11.2"
|
||||||
|
|
||||||
|
pgVersion114 :: PgVersion
|
||||||
|
pgVersion114 = PgVersion 110004 "11.4" "11.4"
|
||||||
|
|
||||||
|
pgVersion120 :: PgVersion
|
||||||
|
pgVersion120 = PgVersion 120000 "12.0" "12.0"
|
||||||
|
|
||||||
pgVersion121 :: PgVersion
|
pgVersion121 :: PgVersion
|
||||||
pgVersion121 = PgVersion 120001 "12.1" "12.1"
|
pgVersion121 = PgVersion 120001 "12.1" "12.1"
|
||||||
@@ -39,6 +67,3 @@ pgVersion140 = PgVersion 140000 "14.0" "14.0"
|
|||||||
|
|
||||||
pgVersion150 :: PgVersion
|
pgVersion150 :: PgVersion
|
||||||
pgVersion150 = PgVersion 150000 "15.0" "15.0"
|
pgVersion150 = PgVersion 150000 "15.0" "15.0"
|
||||||
|
|
||||||
pgVersion170 :: PgVersion
|
|
||||||
pgVersion170 = PgVersion 170000 "17.0" "17.0"
|
|
||||||
|
|||||||
@@ -8,27 +8,21 @@ Description : PostgREST error HTTP responses
|
|||||||
module PostgREST.Error
|
module PostgREST.Error
|
||||||
( errorResponseFor
|
( errorResponseFor
|
||||||
, ApiRequestError(..)
|
, ApiRequestError(..)
|
||||||
, QPError(..)
|
|
||||||
, RangeError(..)
|
|
||||||
, SchemaCacheError(..)
|
|
||||||
, PgError(..)
|
, PgError(..)
|
||||||
, Error(..)
|
, Error(..)
|
||||||
, JwtError (..)
|
|
||||||
, JwtDecodeError(..)
|
|
||||||
, JwtClaimsError(..)
|
|
||||||
, errorPayload
|
, errorPayload
|
||||||
, status
|
, status
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
import qualified Data.FuzzySet as Fuzzy
|
import qualified Data.FuzzySet as Fuzzy
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Data.Map.Internal as M
|
import qualified Data.Map.Internal as M
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Encoding as T
|
import qualified Data.Text.Encoding as T
|
||||||
|
import qualified Data.Text.Encoding.Error as T
|
||||||
import qualified Hasql.Pool as SQL
|
import qualified Hasql.Pool as SQL
|
||||||
import qualified Hasql.Session as SQL
|
import qualified Hasql.Session as SQL
|
||||||
import qualified Network.HTTP.Types.Status as HTTP
|
import qualified Network.HTTP.Types.Status as HTTP
|
||||||
@@ -38,8 +32,12 @@ import Network.Wai (Response, responseLBS)
|
|||||||
|
|
||||||
import Network.HTTP.Types.Header (Header)
|
import Network.HTTP.Types.Header (Header)
|
||||||
|
|
||||||
import PostgREST.MediaType (MediaType (..))
|
import PostgREST.ApiRequest.Types (ApiRequestError (..),
|
||||||
import qualified PostgREST.MediaType as MediaType
|
QPError (..),
|
||||||
|
RaiseError (..),
|
||||||
|
RangeError (..))
|
||||||
|
import PostgREST.MediaType (MediaType (..))
|
||||||
|
import qualified PostgREST.MediaType as MediaType
|
||||||
|
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..),
|
||||||
Schema)
|
Schema)
|
||||||
@@ -49,11 +47,10 @@ import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
|||||||
RelationshipsMap)
|
RelationshipsMap)
|
||||||
import PostgREST.SchemaCache.Routine (Routine (..),
|
import PostgREST.SchemaCache.Routine (Routine (..),
|
||||||
RoutineParam (..))
|
RoutineParam (..))
|
||||||
import PostgREST.SchemaCache.Table (Table (..))
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
class (ErrorBody a, JSON.ToJSON a) => PgrstError a where
|
class (JSON.ToJSON a) => PgrstError a where
|
||||||
status :: a -> HTTP.Status
|
status :: a -> HTTP.Status
|
||||||
headers :: a -> [Header]
|
headers :: a -> [Header]
|
||||||
|
|
||||||
@@ -62,272 +59,200 @@ class (ErrorBody a, JSON.ToJSON a) => PgrstError a where
|
|||||||
|
|
||||||
errorResponseFor :: a -> Response
|
errorResponseFor :: a -> Response
|
||||||
errorResponseFor err =
|
errorResponseFor err =
|
||||||
let
|
let baseHeader = MediaType.toContentType MTApplicationJSON in
|
||||||
baseHeader = MediaType.toContentType MTApplicationJSON
|
responseLBS (status err) (baseHeader : headers err) $ errorPayload err
|
||||||
cLHeader body = (,) "Content-Length" (show $ LBS.length body) :: Header
|
|
||||||
in
|
|
||||||
responseLBS (status err) (baseHeader : cLHeader (errorPayload err) : headers err) $ errorPayload err
|
|
||||||
|
|
||||||
class ErrorBody a where
|
|
||||||
code :: a -> Text
|
|
||||||
message :: a -> Text
|
|
||||||
details :: a -> Maybe JSON.Value
|
|
||||||
hint :: a -> Maybe JSON.Value
|
|
||||||
|
|
||||||
data ApiRequestError
|
|
||||||
= AggregatesNotAllowed
|
|
||||||
| MediaTypeError [ByteString]
|
|
||||||
| InvalidBody ByteString
|
|
||||||
| InvalidFilters
|
|
||||||
| InvalidPreferences [ByteString]
|
|
||||||
| InvalidRange RangeError
|
|
||||||
| InvalidRpcMethod ByteString
|
|
||||||
| NotEmbedded Text
|
|
||||||
| NotImplemented Text
|
|
||||||
| PutLimitNotAllowedError
|
|
||||||
| QueryParamError QPError
|
|
||||||
| RelatedOrderNotToOne Text Text
|
|
||||||
| UnacceptableFilter Text
|
|
||||||
| UnacceptableSchema Text [Text]
|
|
||||||
| UnsupportedMethod ByteString
|
|
||||||
| GucHeadersError
|
|
||||||
| GucStatusError
|
|
||||||
| PutMatchingPkError
|
|
||||||
| SingularityError Integer
|
|
||||||
| PGRSTParseError RaiseError
|
|
||||||
| MaxAffectedViolationError Integer
|
|
||||||
| InvalidResourcePath
|
|
||||||
| OpenAPIDisabled
|
|
||||||
| MaxAffectedRpcViolation
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
data QPError = QPError Text Text
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
data RaiseError
|
|
||||||
= MsgParseError ByteString
|
|
||||||
| DetParseError ByteString
|
|
||||||
| NoDetail
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
data RangeError
|
|
||||||
= NegativeLimit
|
|
||||||
| LowerGTUpper
|
|
||||||
| OutOfBounds Text Text
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
instance PgrstError ApiRequestError where
|
instance PgrstError ApiRequestError where
|
||||||
status AggregatesNotAllowed{} = HTTP.status400
|
status AggregatesNotAllowed{} = HTTP.status400
|
||||||
|
status AmbiguousRelBetween{} = HTTP.status300
|
||||||
|
status AmbiguousRpc{} = HTTP.status300
|
||||||
status MediaTypeError{} = HTTP.status406
|
status MediaTypeError{} = HTTP.status406
|
||||||
status InvalidBody{} = HTTP.status400
|
status InvalidBody{} = HTTP.status400
|
||||||
status InvalidFilters = HTTP.status405
|
status InvalidFilters = HTTP.status405
|
||||||
status InvalidPreferences{} = HTTP.status400
|
status InvalidPreferences{} = HTTP.status400
|
||||||
status InvalidRpcMethod{} = HTTP.status405
|
status InvalidRpcMethod{} = HTTP.status405
|
||||||
status InvalidRange{} = HTTP.status416
|
status InvalidRange{} = HTTP.status416
|
||||||
|
status NotFound = HTTP.status404
|
||||||
|
|
||||||
|
status NoRelBetween{} = HTTP.status400
|
||||||
|
status NoRpc{} = HTTP.status404
|
||||||
status NotEmbedded{} = HTTP.status400
|
status NotEmbedded{} = HTTP.status400
|
||||||
status NotImplemented{} = HTTP.status400
|
|
||||||
status PutLimitNotAllowedError = HTTP.status400
|
status PutLimitNotAllowedError = HTTP.status400
|
||||||
status QueryParamError{} = HTTP.status400
|
status QueryParamError{} = HTTP.status400
|
||||||
status RelatedOrderNotToOne{} = HTTP.status400
|
status RelatedOrderNotToOne{} = HTTP.status400
|
||||||
|
status SpreadNotToOne{} = HTTP.status400
|
||||||
status UnacceptableFilter{} = HTTP.status400
|
status UnacceptableFilter{} = HTTP.status400
|
||||||
status UnacceptableSchema{} = HTTP.status406
|
status UnacceptableSchema{} = HTTP.status406
|
||||||
status UnsupportedMethod{} = HTTP.status405
|
status UnsupportedMethod{} = HTTP.status405
|
||||||
|
status LimitNoOrderError = HTTP.status400
|
||||||
|
status ColumnNotFound{} = HTTP.status400
|
||||||
status GucHeadersError = HTTP.status500
|
status GucHeadersError = HTTP.status500
|
||||||
status GucStatusError = HTTP.status500
|
status GucStatusError = HTTP.status500
|
||||||
|
status OffLimitsChangesError{} = HTTP.status400
|
||||||
status PutMatchingPkError = HTTP.status400
|
status PutMatchingPkError = HTTP.status400
|
||||||
status SingularityError{} = HTTP.status406
|
status SingularityError{} = HTTP.status406
|
||||||
status PGRSTParseError{} = HTTP.status500
|
status PGRSTParseError{} = HTTP.status500
|
||||||
status MaxAffectedViolationError{} = HTTP.status400
|
status MaxAffectedViolationError{} = HTTP.status400
|
||||||
status InvalidResourcePath = HTTP.status404
|
|
||||||
status OpenAPIDisabled = HTTP.status404
|
|
||||||
status MaxAffectedRpcViolation = HTTP.status400
|
|
||||||
|
|
||||||
headers _ = mempty
|
headers _ = mempty
|
||||||
|
|
||||||
-- Error codes:
|
toJsonPgrstError :: ErrorCode -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value
|
||||||
--
|
toJsonPgrstError code msg details hint = JSON.object [
|
||||||
-- Error codes are grouped by common modules or characteristics
|
"code" .= code
|
||||||
-- New group of errors will be added at the end of all the groups and will have the next prefix in the sequence
|
, "message" .= msg
|
||||||
-- Keep the "PGRST" prefix in every code for an easier search/grep
|
, "details" .= details
|
||||||
-- They are grouped as following:
|
, "hint" .= hint
|
||||||
--
|
]
|
||||||
-- PGRST0xx -> Connection Error
|
|
||||||
-- PGRST1xx -> ApiRequest Error
|
|
||||||
-- PGRST2xx -> SchemaCache Error
|
|
||||||
-- PGRST3xx -> JWT authentication Error
|
|
||||||
-- PGRSTXxx -> Internal Hasql Error
|
|
||||||
|
|
||||||
instance ErrorBody ApiRequestError where
|
|
||||||
-- CODE: Text
|
|
||||||
code QueryParamError{} = "PGRST100"
|
|
||||||
code InvalidRpcMethod{} = "PGRST101"
|
|
||||||
code InvalidBody{} = "PGRST102"
|
|
||||||
code InvalidRange{} = "PGRST103"
|
|
||||||
-- code ParseRequestError = "PGRST104" -- no longer used
|
|
||||||
code InvalidFilters = "PGRST105"
|
|
||||||
code UnacceptableSchema{} = "PGRST106"
|
|
||||||
code MediaTypeError{} = "PGRST107"
|
|
||||||
code NotEmbedded{} = "PGRST108"
|
|
||||||
-- code LimitNoOrderError = "PGRST109" -- no longer used
|
|
||||||
-- code OffLimitsChangesError = "PGRST110" -- no longer used
|
|
||||||
code GucHeadersError = "PGRST111"
|
|
||||||
code GucStatusError = "PGRST112"
|
|
||||||
-- code BinaryFieldError = "PGRST113" -- no longer used
|
|
||||||
code PutLimitNotAllowedError = "PGRST114"
|
|
||||||
code PutMatchingPkError = "PGRST115"
|
|
||||||
code SingularityError{} = "PGRST116"
|
|
||||||
code UnsupportedMethod{} = "PGRST117"
|
|
||||||
code RelatedOrderNotToOne{} = "PGRST118"
|
|
||||||
-- code SpreadNotToOne = "PGRST109" -- no longer used
|
|
||||||
code UnacceptableFilter{} = "PGRST120"
|
|
||||||
code PGRSTParseError{} = "PGRST121"
|
|
||||||
code InvalidPreferences{} = "PGRST122"
|
|
||||||
code AggregatesNotAllowed = "PGRST123"
|
|
||||||
code MaxAffectedViolationError{} = "PGRST124"
|
|
||||||
code InvalidResourcePath = "PGRST125"
|
|
||||||
code OpenAPIDisabled = "PGRST126"
|
|
||||||
code NotImplemented{} = "PGRST127"
|
|
||||||
code MaxAffectedRpcViolation = "PGRST128"
|
|
||||||
|
|
||||||
-- MESSAGE: Text
|
|
||||||
message (QueryParamError (QPError msg _)) = msg
|
|
||||||
message (InvalidRpcMethod method) = "Cannot use the " <> T.decodeUtf8 method <> " method on RPC"
|
|
||||||
message (InvalidBody errorMessage) = T.decodeUtf8 errorMessage
|
|
||||||
message (InvalidRange _) = "Requested range not satisfiable"
|
|
||||||
message InvalidFilters = "Filters must include all and only primary key columns with 'eq' operators"
|
|
||||||
message (UnacceptableSchema sch _) = "Invalid schema: " <> sch
|
|
||||||
message (MediaTypeError cts) = "None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)
|
|
||||||
message (NotEmbedded resource) = "'" <> resource <> "' is not an embedded resource in this request"
|
|
||||||
message GucHeadersError = "response.headers guc must be a JSON array composed of objects with a single key and a string value"
|
|
||||||
message GucStatusError = "response.status guc must be a valid status code"
|
|
||||||
message PutLimitNotAllowedError = "limit/offset querystring parameters are not allowed for PUT"
|
|
||||||
message PutMatchingPkError = "Payload values do not match URL in primary key column(s)"
|
|
||||||
message (SingularityError _) = "Cannot coerce the result to a single JSON object"
|
|
||||||
message (UnsupportedMethod method) = "Unsupported HTTP method: " <> T.decodeUtf8 method
|
|
||||||
message (RelatedOrderNotToOne _ target) = "A related order on '" <> target <> "' is not possible"
|
|
||||||
message (UnacceptableFilter target) = "Bad operator on the '" <> target <> "' embedded resource"
|
|
||||||
message (PGRSTParseError _) = "Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error"
|
|
||||||
message (InvalidPreferences _) = "Invalid preferences given with handling=strict"
|
|
||||||
message AggregatesNotAllowed = "Use of aggregate functions is not allowed"
|
|
||||||
message (MaxAffectedViolationError _) = "Query result exceeds max-affected preference constraint"
|
|
||||||
message InvalidResourcePath = "Invalid path specified in request URL"
|
|
||||||
message OpenAPIDisabled = "Root endpoint metadata is disabled"
|
|
||||||
message (NotImplemented _) = "Feature not implemented"
|
|
||||||
message MaxAffectedRpcViolation = "Function must return SETOF or TABLE when max-affected preference is used with handling=strict"
|
|
||||||
|
|
||||||
-- DETAILS: Maybe JSON.Value
|
|
||||||
details (QueryParamError (QPError _ dets)) = Just $ JSON.String dets
|
|
||||||
details (InvalidRange rangeError) = Just $
|
|
||||||
case rangeError of
|
|
||||||
NegativeLimit -> "Limit should be greater than or equal to zero."
|
|
||||||
LowerGTUpper -> "The lower boundary must be lower than or equal to the upper boundary in the Range header."
|
|
||||||
OutOfBounds lower total -> JSON.String $ "An offset of " <> lower <> " was requested, but there are only " <> total <> " rows."
|
|
||||||
details (SingularityError n) = Just $ JSON.String $ T.unwords ["The result contains", show n, "rows"]
|
|
||||||
details (RelatedOrderNotToOne origin target) = Just $ JSON.String $ "'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship"
|
|
||||||
details (UnacceptableFilter _) = Just "Only is null or not is null filters are allowed on embedded resources"
|
|
||||||
details (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorDetails raiseErr
|
|
||||||
details (InvalidPreferences prefs) = Just $ JSON.String $ T.decodeUtf8 ("Invalid preferences: " <> BS.intercalate ", " prefs)
|
|
||||||
details (MaxAffectedViolationError n) = Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"]
|
|
||||||
details (NotImplemented details') = Just $ JSON.String details'
|
|
||||||
|
|
||||||
details _ = Nothing
|
|
||||||
|
|
||||||
-- HINT: Maybe JSON.Value
|
|
||||||
hint (NotEmbedded resource) = Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter."
|
|
||||||
hint (PGRSTParseError raiseErr) = Just $ JSON.String $ pgrstParseErrorHint raiseErr
|
|
||||||
hint (UnacceptableSchema _ schemas) = Just $ JSON.String $ "Only the following schemas are exposed: " <> T.intercalate ", " schemas
|
|
||||||
|
|
||||||
hint _ = Nothing
|
|
||||||
|
|
||||||
instance JSON.ToJSON ApiRequestError where
|
instance JSON.ToJSON ApiRequestError where
|
||||||
toJSON err = toJsonPgrstError
|
toJSON (QueryParamError (QPError message details)) = toJsonPgrstError
|
||||||
(code err) (message err) (details err) (hint err)
|
ApiRequestErrorCode00 message (Just (JSON.String details)) Nothing
|
||||||
|
|
||||||
data SchemaCacheError
|
toJSON (InvalidRpcMethod method) = toJsonPgrstError
|
||||||
= AmbiguousRelBetween Text Text [Relationship]
|
ApiRequestErrorCode01 ("Cannot use the " <> T.decodeUtf8 method <> " method on RPC") Nothing Nothing
|
||||||
| AmbiguousRpc [Routine]
|
|
||||||
| NoRelBetween Text Text (Maybe Text) Text RelationshipsMap
|
|
||||||
| NoRpc Text Text [Text] MediaType Bool [QualifiedIdentifier] [Routine]
|
|
||||||
| ColumnNotFound Text Text
|
|
||||||
| TableNotFound Text Text [Table]
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
instance PgrstError SchemaCacheError where
|
toJSON (InvalidBody errorMessage) = toJsonPgrstError
|
||||||
status AmbiguousRelBetween{} = HTTP.status300
|
ApiRequestErrorCode02 (T.decodeUtf8 errorMessage) Nothing Nothing
|
||||||
status AmbiguousRpc{} = HTTP.status300
|
|
||||||
status NoRelBetween{} = HTTP.status400
|
|
||||||
status NoRpc{} = HTTP.status404
|
|
||||||
status ColumnNotFound{} = HTTP.status400
|
|
||||||
status TableNotFound{} = HTTP.status404
|
|
||||||
|
|
||||||
headers _ = mempty
|
toJSON (InvalidRange rangeError) = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode03
|
||||||
|
"Requested range not satisfiable"
|
||||||
|
(Just $ case rangeError of
|
||||||
|
NegativeLimit -> "Limit should be greater than or equal to zero."
|
||||||
|
LowerGTUpper -> "The lower boundary must be lower than or equal to the upper boundary in the Range header."
|
||||||
|
OutOfBounds lower total -> JSON.String $ "An offset of " <> lower <> " was requested, but there are only " <> total <> " rows.")
|
||||||
|
Nothing
|
||||||
|
|
||||||
instance ErrorBody SchemaCacheError where
|
toJSON InvalidFilters = toJsonPgrstError
|
||||||
code NoRelBetween{} = "PGRST200"
|
ApiRequestErrorCode05 "Filters must include all and only primary key columns with 'eq' operators" Nothing Nothing
|
||||||
code AmbiguousRelBetween{} = "PGRST201"
|
|
||||||
code NoRpc{} = "PGRST202"
|
|
||||||
code AmbiguousRpc{} = "PGRST203"
|
|
||||||
code ColumnNotFound{} = "PGRST204"
|
|
||||||
code TableNotFound{} = "PGRST205"
|
|
||||||
|
|
||||||
message (NoRelBetween parent child _ _ _) = "Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache"
|
toJSON (UnacceptableSchema schemas) = toJsonPgrstError
|
||||||
message (AmbiguousRelBetween parent child _) = "Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'"
|
ApiRequestErrorCode06 ("The schema must be one of the following: " <> T.intercalate ", " schemas) Nothing Nothing
|
||||||
message (NoRpc schema procName argumentKeys contentType isInvPost _ _) = "Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache"
|
|
||||||
where
|
toJSON (MediaTypeError cts) = toJsonPgrstError
|
||||||
onlySingleParams = isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream]
|
ApiRequestErrorCode07 ("None of these media types are available: " <> T.intercalate ", " (map T.decodeUtf8 cts)) Nothing Nothing
|
||||||
func = schema <> "." <> procName
|
|
||||||
|
toJSON NotFound = JSON.object []
|
||||||
|
|
||||||
|
toJSON (NotEmbedded resource) = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode08
|
||||||
|
("'" <> resource <> "' is not an embedded resource in this request")
|
||||||
|
Nothing
|
||||||
|
(Just $ JSON.String $ "Verify that '" <> resource <> "' is included in the 'select' query parameter.")
|
||||||
|
|
||||||
|
toJSON LimitNoOrderError = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode09 "A 'limit' was applied without an explicit 'order'" Nothing (Just "Apply an 'order' using unique column(s)")
|
||||||
|
|
||||||
|
toJSON (OffLimitsChangesError n maxs) = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode10
|
||||||
|
"The maximum number of rows allowed to change was surpassed"
|
||||||
|
(Just $ JSON.String $ T.unwords ["Results contain", show n, "rows changed but the maximum number allowed is", show maxs])
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
toJSON GucHeadersError = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode11 "response.headers guc must be a JSON array composed of objects with a single key and a string value" Nothing Nothing
|
||||||
|
|
||||||
|
toJSON GucStatusError = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode12 "response.status guc must be a valid status code" Nothing Nothing
|
||||||
|
|
||||||
|
toJSON PutLimitNotAllowedError = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode14 "limit/offset querystring parameters are not allowed for PUT" Nothing Nothing
|
||||||
|
|
||||||
|
toJSON PutMatchingPkError = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode15 "Payload values do not match URL in primary key column(s)" Nothing Nothing
|
||||||
|
|
||||||
|
toJSON (SingularityError n) = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode16
|
||||||
|
"JSON object requested, multiple (or no) rows returned"
|
||||||
|
(Just $ JSON.String $ T.unwords ["The result contains", show n, "rows"])
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
toJSON (UnsupportedMethod method) = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode17 ("Unsupported HTTP method: " <> T.decodeUtf8 method) Nothing Nothing
|
||||||
|
|
||||||
|
toJSON (RelatedOrderNotToOne origin target) = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode18
|
||||||
|
("A related order on '" <> target <> "' is not possible")
|
||||||
|
(Just $ JSON.String $ "'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship")
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
toJSON (SpreadNotToOne origin target) = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode19
|
||||||
|
("A spread operation on '" <> target <> "' is not possible")
|
||||||
|
(Just $ JSON.String $ "'" <> origin <> "' and '" <> target <> "' do not form a many-to-one or one-to-one relationship")
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
toJSON (UnacceptableFilter target) = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode20
|
||||||
|
("Bad operator on the '" <> target <> "' embedded resource")
|
||||||
|
(Just "Only is null or not is null filters are allowed on embedded resources")
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
toJSON (PGRSTParseError raiseErr) = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode21
|
||||||
|
"Could not parse JSON in the \"RAISE SQLSTATE 'PGRST'\" error"
|
||||||
|
(Just $ JSON.String $ pgrstParseErrorDetails raiseErr)
|
||||||
|
(Just $ JSON.String $ pgrstParseErrorHint raiseErr)
|
||||||
|
|
||||||
|
toJSON (InvalidPreferences prefs) = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode22
|
||||||
|
"Invalid preferences given with handling=strict"
|
||||||
|
(Just $ JSON.String $ T.decodeUtf8 ("Invalid preferences: " <> BS.intercalate ", " prefs))
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
toJSON AggregatesNotAllowed = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode23 "Use of aggregate functions is not allowed" Nothing Nothing
|
||||||
|
|
||||||
|
toJSON (MaxAffectedViolationError n) = toJsonPgrstError
|
||||||
|
ApiRequestErrorCode24
|
||||||
|
"Query result exceeds max-affected preference constraint"
|
||||||
|
(Just $ JSON.String $ T.unwords ["The query affects", show n, "rows"])
|
||||||
|
Nothing
|
||||||
|
|
||||||
|
toJSON (NoRelBetween parent child embedHint schema allRels) = toJsonPgrstError
|
||||||
|
SchemaCacheErrorCode00
|
||||||
|
("Could not find a relationship between '" <> parent <> "' and '" <> child <> "' in the schema cache")
|
||||||
|
(Just $ JSON.String $ "Searched for a foreign key relationship between '" <> parent <> "' and '" <> child <> maybe mempty ("' using the hint '" <>) embedHint <> "' in the schema '" <> schema <> "', but no matches were found.")
|
||||||
|
(JSON.String <$> noRelBetweenHint parent child schema allRels)
|
||||||
|
|
||||||
|
toJSON (AmbiguousRelBetween parent child rels) = toJsonPgrstError
|
||||||
|
SchemaCacheErrorCode01
|
||||||
|
("Could not embed because more than one relationship was found for '" <> parent <> "' and '" <> child <> "'")
|
||||||
|
(Just $ JSON.toJSONList (compressedRel <$> rels))
|
||||||
|
(Just $ JSON.String $ "Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key.")
|
||||||
|
|
||||||
|
toJSON (NoRpc schema procName argumentKeys hasPreferSingleObject contentType isInvPost allProcs overloadedProcs) =
|
||||||
|
let func = schema <> "." <> procName
|
||||||
prms = T.intercalate ", " argumentKeys
|
prms = T.intercalate ", " argumentKeys
|
||||||
prmsMsg = "(" <> prms <> ")"
|
prmsMsg = "(" <> prms <> ")"
|
||||||
fmtPrms p = if null argumentKeys then " without parameters" else p
|
|
||||||
message (AmbiguousRpc procs) = "Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs]
|
|
||||||
message (ColumnNotFound rel col) = "Could not find the '" <> col <> "' column of '" <> rel <> "' in the schema cache"
|
|
||||||
message (TableNotFound schemaName relName _) = "Could not find the table '" <> schemaName <> "." <> relName <> "' in the schema cache"
|
|
||||||
|
|
||||||
details (NoRelBetween parent child embedHint schema _) = Just $ JSON.String $ "Searched for a foreign key relationship between '" <> parent <> "' and '" <> child <> maybe mempty ("' using the hint '" <>) embedHint <> "' in the schema '" <> schema <> "', but no matches were found."
|
|
||||||
details (AmbiguousRelBetween _ _ rels) = Just $ JSON.toJSONList (compressedRel <$> rels)
|
|
||||||
details (NoRpc schema procName argumentKeys contentType isInvPost _ _) =
|
|
||||||
Just $ JSON.String $ "Searched for the function " <> func <>
|
|
||||||
(case (isInvPost, contentType) of
|
|
||||||
(True, MTTextPlain) -> " with a single unnamed text parameter"
|
|
||||||
(True, MTTextXML) -> " with a single unnamed xml parameter"
|
|
||||||
(True, MTOctetStream) -> " with a single unnamed bytea parameter"
|
|
||||||
(True, MTApplicationJSON) -> fmtPrms prmsDet <> " or with a single unnamed json/jsonb parameter"
|
|
||||||
_ -> fmtPrms prmsDet
|
|
||||||
) <> ", but no matches were found in the schema cache."
|
|
||||||
where
|
|
||||||
func = schema <> "." <> procName
|
|
||||||
prms = T.intercalate ", " argumentKeys
|
|
||||||
prmsDet = " with parameter" <> (if length argumentKeys > 1 then "s " else " ") <> prms
|
prmsDet = " with parameter" <> (if length argumentKeys > 1 then "s " else " ") <> prms
|
||||||
fmtPrms p = if null argumentKeys then " without parameters" else p
|
fmtPrms p = if null argumentKeys then " without parameters" else p
|
||||||
|
onlySingleParams = hasPreferSingleObject || (isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream])
|
||||||
details _ = Nothing
|
in toJsonPgrstError
|
||||||
|
SchemaCacheErrorCode02
|
||||||
hint (NoRelBetween parent child _ schema allRels) = JSON.String <$> noRelBetweenHint parent child schema allRels
|
("Could not find the function " <> func <> (if onlySingleParams then "" else fmtPrms prmsMsg) <> " in the schema cache")
|
||||||
hint (AmbiguousRelBetween _ child rels) = Just $ JSON.String $ "Try changing '" <> child <> "' to one of the following: " <> relHint rels <> ". Find the desired relationship in the 'details' key."
|
(Just $ JSON.String $ "Searched for the function " <> func <>
|
||||||
-- The hint will be null in the case of single unnamed parameter functions
|
(case (hasPreferSingleObject, isInvPost, contentType) of
|
||||||
hint (NoRpc schema procName argumentKeys contentType isInvPost allProcs overloadedProcs) =
|
(True, _, _) -> " with a single json/jsonb parameter"
|
||||||
if onlySingleParams
|
(_, True, MTTextPlain) -> " with a single unnamed text parameter"
|
||||||
|
(_, True, MTTextXML) -> " with a single unnamed xml parameter"
|
||||||
|
(_, True, MTOctetStream) -> " with a single unnamed bytea parameter"
|
||||||
|
(_, True, MTApplicationJSON) -> fmtPrms prmsDet <> " or with a single unnamed json/jsonb parameter"
|
||||||
|
_ -> fmtPrms prmsDet) <>
|
||||||
|
", but no matches were found in the schema cache.")
|
||||||
|
-- The hint will be null in the case of single unnamed parameter functions
|
||||||
|
(if onlySingleParams
|
||||||
then Nothing
|
then Nothing
|
||||||
else JSON.String <$> noRpcHint schema procName argumentKeys allProcs overloadedProcs
|
else JSON.String <$> noRpcHint schema procName argumentKeys allProcs overloadedProcs)
|
||||||
where
|
|
||||||
onlySingleParams = isInvPost && contentType `elem` [MTTextPlain, MTTextXML, MTOctetStream]
|
|
||||||
hint (AmbiguousRpc _) = Just "Try renaming the parameters or the function itself in the database so function overloading can be resolved"
|
|
||||||
hint (TableNotFound schemaName relName tbls) = JSON.String <$> tableNotFoundHint schemaName relName tbls
|
|
||||||
|
|
||||||
hint _ = Nothing
|
toJSON (AmbiguousRpc procs) = toJsonPgrstError
|
||||||
|
SchemaCacheErrorCode03
|
||||||
|
("Could not choose the best candidate function between: " <> T.intercalate ", " [pdSchema p <> "." <> pdName p <> "(" <> T.intercalate ", " [ppName a <> " => " <> ppType a | a <- pdParams p] <> ")" | p <- procs])
|
||||||
|
Nothing
|
||||||
|
(Just "Try renaming the parameters or the function itself in the database so function overloading can be resolved")
|
||||||
|
|
||||||
instance JSON.ToJSON SchemaCacheError where
|
toJSON (ColumnNotFound relName colName) = toJsonPgrstError
|
||||||
toJSON err = toJsonPgrstError
|
SchemaCacheErrorCode04 ("Could not find the '" <> colName <> "' column of '" <> relName <> "' in the schema cache") Nothing Nothing
|
||||||
(code err) (message err) (details err) (hint err)
|
|
||||||
|
|
||||||
toJsonPgrstError :: Text -> Text -> Maybe JSON.Value -> Maybe JSON.Value -> JSON.Value
|
|
||||||
toJsonPgrstError code' message' details' hint' = JSON.object [
|
|
||||||
"code" .= code'
|
|
||||||
, "message" .= message'
|
|
||||||
, "details" .= details'
|
|
||||||
, "hint" .= hint'
|
|
||||||
]
|
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- If no relationship is found then:
|
-- If no relationship is found then:
|
||||||
@@ -426,16 +351,6 @@ noRpcHint schema procName params allProcs overloadedProcs =
|
|||||||
| null overloadedProcs = Fuzzy.getOne fuzzySetOfProcs procName
|
| null overloadedProcs = Fuzzy.getOne fuzzySetOfProcs procName
|
||||||
| otherwise = (procName <>) <$> Fuzzy.getOne fuzzySetOfParams (listToText params)
|
| otherwise = (procName <>) <$> Fuzzy.getOne fuzzySetOfParams (listToText params)
|
||||||
|
|
||||||
-- |
|
|
||||||
-- Do a fuzzy search in all tables in the same schema and return closest result
|
|
||||||
tableNotFoundHint :: Text -> Text -> [Table] -> Maybe Text
|
|
||||||
tableNotFoundHint schema tblName tblList
|
|
||||||
= fmap (\tbl -> "Perhaps you meant the table '" <> schema <> "." <> tbl <> "'") perhapsTable
|
|
||||||
where
|
|
||||||
perhapsTable = Fuzzy.getOne fuzzyTableSet tblName
|
|
||||||
fuzzyTableSet = Fuzzy.fromList [ tableName tbl | tbl <- tblList, tableSchema tbl == schema]
|
|
||||||
|
|
||||||
|
|
||||||
compressedRel :: Relationship -> JSON.Value
|
compressedRel :: Relationship -> JSON.Value
|
||||||
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
|
-- An ambiguousness error cannot happen for computed relationships TODO refactor so this mempty is not needed
|
||||||
compressedRel ComputedRelationship{} = JSON.object mempty
|
compressedRel ComputedRelationship{} = JSON.object mempty
|
||||||
@@ -488,8 +403,6 @@ pgrstParseErrorHint err = case err of
|
|||||||
_ -> "DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'."
|
_ -> "DETAIL must be a JSON object with obligatory keys: 'status', 'headers' and optional key: 'status_text'."
|
||||||
|
|
||||||
data PgError = PgError Authenticated SQL.UsageError
|
data PgError = PgError Authenticated SQL.UsageError
|
||||||
deriving Show
|
|
||||||
|
|
||||||
type Authenticated = Bool
|
type Authenticated = Bool
|
||||||
|
|
||||||
instance PgrstError PgError where
|
instance PgrstError PgError where
|
||||||
@@ -507,81 +420,48 @@ instance PgrstError PgError where
|
|||||||
then [("WWW-Authenticate", "Bearer") :: Header]
|
then [("WWW-Authenticate", "Bearer") :: Header]
|
||||||
else mempty
|
else mempty
|
||||||
|
|
||||||
proxyStatusHeader :: Text -> Header
|
|
||||||
proxyStatusHeader code' = ("Proxy-Status", "PostgREST; error=" <> T.encodeUtf8 code')
|
|
||||||
|
|
||||||
instance JSON.ToJSON PgError where
|
instance JSON.ToJSON PgError where
|
||||||
toJSON (PgError _ usageError) = toJsonPgrstError
|
toJSON (PgError _ usageError) = JSON.toJSON usageError
|
||||||
(code usageError) (message usageError) (details usageError) (hint usageError)
|
|
||||||
|
|
||||||
instance ErrorBody PgError where
|
|
||||||
code (PgError _ usageError) = code usageError
|
|
||||||
message (PgError _ usageError) = message usageError
|
|
||||||
details (PgError _ usageError) = details usageError
|
|
||||||
hint (PgError _ usageError) = hint usageError
|
|
||||||
|
|
||||||
instance JSON.ToJSON SQL.UsageError where
|
instance JSON.ToJSON SQL.UsageError where
|
||||||
toJSON err = toJsonPgrstError
|
toJSON (SQL.ConnectionUsageError e) = toJsonPgrstError
|
||||||
(code err) (message err) (details err) (hint err)
|
ConnectionErrorCode00
|
||||||
|
"Database connection error. Retrying the connection."
|
||||||
|
(Just $ JSON.String $ T.decodeUtf8With T.lenientDecode $ fromMaybe "" e)
|
||||||
|
Nothing
|
||||||
|
|
||||||
instance ErrorBody SQL.UsageError where
|
toJSON (SQL.SessionUsageError e) = JSON.toJSON e -- SQL.Error
|
||||||
code (SQL.ConnectionUsageError _) = "PGRST000"
|
|
||||||
code (SQL.SessionUsageError (SQL.QueryError _ _ e)) = code e
|
|
||||||
code SQL.AcquisitionTimeoutUsageError = "PGRST003"
|
|
||||||
|
|
||||||
message (SQL.ConnectionUsageError _) = "Database connection error. Retrying the connection."
|
toJSON SQL.AcquisitionTimeoutUsageError = toJsonPgrstError
|
||||||
message (SQL.SessionUsageError (SQL.QueryError _ _ e)) = message e
|
ConnectionErrorCode03 "Timed out acquiring connection from connection pool." Nothing Nothing
|
||||||
message SQL.AcquisitionTimeoutUsageError = "Timed out acquiring connection from connection pool."
|
|
||||||
|
|
||||||
details (SQL.ConnectionUsageError e) = JSON.String . T.decodeUtf8 <$> e
|
instance JSON.ToJSON SQL.QueryError where
|
||||||
details (SQL.SessionUsageError (SQL.QueryError _ _ e)) = details e
|
toJSON (SQL.QueryError _ _ e) = JSON.toJSON e
|
||||||
details SQL.AcquisitionTimeoutUsageError = Nothing
|
|
||||||
|
|
||||||
hint (SQL.ConnectionUsageError _) = Nothing
|
|
||||||
hint (SQL.SessionUsageError (SQL.QueryError _ _ e)) = hint e
|
|
||||||
hint SQL.AcquisitionTimeoutUsageError = Nothing
|
|
||||||
|
|
||||||
instance JSON.ToJSON SQL.CommandError where
|
instance JSON.ToJSON SQL.CommandError where
|
||||||
toJSON err = toJsonPgrstError
|
|
||||||
(code err) (message err) (details err) (hint err)
|
|
||||||
|
|
||||||
instance ErrorBody SQL.CommandError where
|
|
||||||
-- Special error raised with code PGRST, to allow full response control
|
-- Special error raised with code PGRST, to allow full response control
|
||||||
code (SQL.ResultError (SQL.ServerError "PGRST" m d _ _)) =
|
toJSON (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p)) =
|
||||||
case parseRaisePGRST m d of
|
case parseRaisePGRST m d of
|
||||||
Right (r, _) -> getCode r
|
Right (r, _) -> JSON.object [
|
||||||
Left e -> code e
|
"code" .= getCode r,
|
||||||
code (SQL.ResultError (SQL.ServerError c _ _ _ _)) = T.decodeUtf8 c
|
"message" .= getMessage r,
|
||||||
|
"details" .= checkMaybe (getDetails r),
|
||||||
|
"hint" .= checkMaybe (getHint r)]
|
||||||
|
Left e -> JSON.toJSON e
|
||||||
|
where
|
||||||
|
checkMaybe = maybe JSON.Null JSON.String
|
||||||
|
|
||||||
code (SQL.ResultError _) = "PGRSTX00" -- Internal Error
|
toJSON (SQL.ResultError (SQL.ServerError c m d h _p)) = JSON.object [
|
||||||
|
"code" .= (T.decodeUtf8 c :: Text),
|
||||||
|
"message" .= (T.decodeUtf8 m :: Text),
|
||||||
|
"details" .= (fmap T.decodeUtf8 d :: Maybe Text),
|
||||||
|
"hint" .= (fmap T.decodeUtf8 h :: Maybe Text)]
|
||||||
|
|
||||||
code (SQL.ClientError _) = "PGRST001"
|
toJSON (SQL.ResultError resultError) = toJsonPgrstError
|
||||||
|
InternalErrorCode00 (show resultError) Nothing Nothing
|
||||||
message (SQL.ResultError (SQL.ServerError "PGRST" m d _ _)) =
|
|
||||||
case parseRaisePGRST m d of
|
|
||||||
Right (r, _) -> getMessage r
|
|
||||||
Left e -> message e
|
|
||||||
message (SQL.ResultError (SQL.ServerError _ m _ _ _)) = T.decodeUtf8 m
|
|
||||||
message (SQL.ResultError resultError) = show resultError -- We never really return this error, because we kill pgrst thread early in App.hs
|
|
||||||
message (SQL.ClientError _) = "Database client error. Retrying the connection."
|
|
||||||
|
|
||||||
details (SQL.ResultError (SQL.ServerError "PGRST" m d _ _)) =
|
|
||||||
case parseRaisePGRST m d of
|
|
||||||
Right (r, _) -> JSON.String <$> getDetails r
|
|
||||||
Left e -> details e
|
|
||||||
details (SQL.ResultError (SQL.ServerError _ _ d _ _)) = JSON.String . T.decodeUtf8 <$> d
|
|
||||||
details (SQL.ClientError d) = JSON.String . T.decodeUtf8 <$> d
|
|
||||||
|
|
||||||
details _ = Nothing
|
|
||||||
|
|
||||||
hint (SQL.ResultError (SQL.ServerError "PGRST" m d _ _p)) =
|
|
||||||
case parseRaisePGRST m d of
|
|
||||||
Right (r, _) -> JSON.String <$> getHint r
|
|
||||||
Left e -> hint e
|
|
||||||
hint (SQL.ResultError (SQL.ServerError _ _ _ h _)) = JSON.String . T.decodeUtf8 <$> h
|
|
||||||
|
|
||||||
hint _ = Nothing
|
|
||||||
|
|
||||||
|
toJSON (SQL.ClientError d) = toJsonPgrstError
|
||||||
|
ConnectionErrorCode01 "Database client error. Retrying the connection." (JSON.String <$> fmap T.decodeUtf8 d) Nothing
|
||||||
|
|
||||||
pgErrorStatus :: Bool -> SQL.UsageError -> HTTP.Status
|
pgErrorStatus :: Bool -> SQL.UsageError -> HTTP.Status
|
||||||
pgErrorStatus _ (SQL.ConnectionUsageError _) = HTTP.status503
|
pgErrorStatus _ (SQL.ConnectionUsageError _) = HTTP.status503
|
||||||
@@ -602,10 +482,6 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
|
|||||||
if BS.isSuffixOf "requires a WHERE clause" m
|
if BS.isSuffixOf "requires a WHERE clause" m
|
||||||
then HTTP.status400 -- special case for pg-safeupdate, which we consider as client error
|
then HTTP.status400 -- special case for pg-safeupdate, which we consider as client error
|
||||||
else HTTP.status500 -- generic function or view server error, e.g. "more than one row returned by a subquery used as an expression"
|
else HTTP.status500 -- generic function or view server error, e.g. "more than one row returned by a subquery used as an expression"
|
||||||
"22023" -> -- invalid_parameter_value. Catch nonexistent role error, see https://github.com/PostgREST/postgrest/issues/3601
|
|
||||||
if BS.isPrefixOf "role" m && BS.isSuffixOf "does not exist" m
|
|
||||||
then HTTP.status401 -- role in jwt does not exist
|
|
||||||
else HTTP.status400
|
|
||||||
'2':'5':_ -> HTTP.status500 -- invalid tx state
|
'2':'5':_ -> HTTP.status500 -- invalid tx state
|
||||||
'2':'8':_ -> HTTP.status403 -- invalid auth specification
|
'2':'8':_ -> HTTP.status403 -- invalid auth specification
|
||||||
'2':'D':_ -> HTTP.status500 -- invalid tx termination
|
'2':'D':_ -> HTTP.status500 -- invalid tx termination
|
||||||
@@ -643,132 +519,41 @@ pgErrorStatus authed (SQL.SessionUsageError (SQL.QueryError _ _ (SQL.ResultError
|
|||||||
|
|
||||||
data Error
|
data Error
|
||||||
= ApiRequestError ApiRequestError
|
= ApiRequestError ApiRequestError
|
||||||
| SchemaCacheErr SchemaCacheError
|
| JwtTokenInvalid Text
|
||||||
| JwtErr JwtError
|
| JwtTokenMissing
|
||||||
|
| JwtTokenRequired
|
||||||
| NoSchemaCacheError
|
| NoSchemaCacheError
|
||||||
| PgErr PgError
|
| PgErr PgError
|
||||||
deriving Show
|
|
||||||
|
|
||||||
data JwtError
|
|
||||||
= JwtDecodeErr JwtDecodeError
|
|
||||||
| JwtSecretMissing
|
|
||||||
| JwtTokenRequired
|
|
||||||
| JwtClaimsErr JwtClaimsError
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
data JwtDecodeError
|
|
||||||
= EmptyAuthHeader
|
|
||||||
| UnexpectedParts Int
|
|
||||||
| KeyError Text
|
|
||||||
| BadAlgorithm Text
|
|
||||||
| BadCrypto
|
|
||||||
| UnsupportedTokenType
|
|
||||||
| UnreachableDecodeError
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
data JwtClaimsError
|
|
||||||
= JWTExpired
|
|
||||||
| JWTNotYetValid
|
|
||||||
| JWTIssuedAtFuture
|
|
||||||
| JWTNotInAudience
|
|
||||||
| ParsingClaimsFailed
|
|
||||||
| ExpClaimNotNumber
|
|
||||||
| NbfClaimNotNumber
|
|
||||||
| IatClaimNotNumber
|
|
||||||
| AudClaimNotStringOrArray
|
|
||||||
deriving Show
|
|
||||||
|
|
||||||
instance PgrstError Error where
|
instance PgrstError Error where
|
||||||
status (ApiRequestError err) = status err
|
status (ApiRequestError err) = status err
|
||||||
status (SchemaCacheErr err) = status err
|
status JwtTokenInvalid{} = HTTP.unauthorized401
|
||||||
status (JwtErr err) = status err
|
status JwtTokenMissing = HTTP.status500
|
||||||
|
status JwtTokenRequired = HTTP.unauthorized401
|
||||||
status NoSchemaCacheError = HTTP.status503
|
status NoSchemaCacheError = HTTP.status503
|
||||||
status (PgErr err) = status err
|
status (PgErr err) = status err
|
||||||
|
|
||||||
headers (ApiRequestError err) = proxyStatusHeader (code err) : headers err
|
headers (ApiRequestError err) = headers err
|
||||||
headers (SchemaCacheErr err) = proxyStatusHeader (code err) : headers err
|
headers (JwtTokenInvalid m) = [invalidTokenHeader m]
|
||||||
headers (JwtErr err) = proxyStatusHeader (code err) : headers err
|
headers JwtTokenRequired = [requiredTokenHeader]
|
||||||
headers (PgErr err) = proxyStatusHeader (code err) : headers err
|
headers (PgErr err) = headers err
|
||||||
headers err@NoSchemaCacheError = proxyStatusHeader (code err) : mempty
|
headers _ = mempty
|
||||||
|
|
||||||
instance JSON.ToJSON Error where
|
instance JSON.ToJSON Error where
|
||||||
toJSON err = toJsonPgrstError
|
toJSON NoSchemaCacheError = toJsonPgrstError
|
||||||
(code err) (message err) (details err) (hint err)
|
ConnectionErrorCode02 "Could not query the database for the schema cache. Retrying." Nothing Nothing
|
||||||
|
|
||||||
instance ErrorBody Error where
|
toJSON JwtTokenMissing = toJsonPgrstError
|
||||||
code (ApiRequestError err) = code err
|
JWTErrorCode00 "Server lacks JWT secret" Nothing Nothing
|
||||||
code (SchemaCacheErr err) = code err
|
|
||||||
code (JwtErr err) = code err
|
|
||||||
code NoSchemaCacheError = "PGRST002"
|
|
||||||
code (PgErr err) = code err
|
|
||||||
|
|
||||||
message (ApiRequestError err) = message err
|
toJSON (JwtTokenInvalid message) = toJsonPgrstError
|
||||||
message (SchemaCacheErr err) = message err
|
JWTErrorCode01 message Nothing Nothing
|
||||||
message (JwtErr err) = message err
|
|
||||||
message NoSchemaCacheError = "Could not query the database for the schema cache. Retrying."
|
|
||||||
message (PgErr err) = message err
|
|
||||||
|
|
||||||
details (ApiRequestError err) = details err
|
toJSON JwtTokenRequired = toJsonPgrstError
|
||||||
details (SchemaCacheErr err) = details err
|
JWTErrorCode02 "Anonymous access is disabled" Nothing Nothing
|
||||||
details (JwtErr err) = details err
|
|
||||||
details NoSchemaCacheError = Nothing
|
|
||||||
details (PgErr err) = details err
|
|
||||||
|
|
||||||
hint (ApiRequestError err) = hint err
|
toJSON (PgErr err) = JSON.toJSON err
|
||||||
hint (SchemaCacheErr err) = hint err
|
toJSON (ApiRequestError err) = JSON.toJSON err
|
||||||
hint (JwtErr err) = hint err
|
|
||||||
hint NoSchemaCacheError = Nothing
|
|
||||||
hint (PgErr err) = hint err
|
|
||||||
|
|
||||||
instance PgrstError JwtError where
|
|
||||||
status JwtDecodeErr{} = HTTP.unauthorized401
|
|
||||||
status JwtSecretMissing = HTTP.status500
|
|
||||||
status JwtTokenRequired = HTTP.unauthorized401
|
|
||||||
status JwtClaimsErr{} = HTTP.unauthorized401
|
|
||||||
|
|
||||||
headers e@(JwtDecodeErr _) = [invalidTokenHeader $ message e]
|
|
||||||
headers JwtTokenRequired = [requiredTokenHeader]
|
|
||||||
headers e@(JwtClaimsErr _) = [invalidTokenHeader $ message e]
|
|
||||||
headers _ = mempty
|
|
||||||
|
|
||||||
instance JSON.ToJSON JwtError where
|
|
||||||
toJSON err = toJsonPgrstError
|
|
||||||
(code err) (message err) (details err) (hint err)
|
|
||||||
|
|
||||||
instance ErrorBody JwtError where
|
|
||||||
code JwtSecretMissing = "PGRST300"
|
|
||||||
code (JwtDecodeErr _) = "PGRST301"
|
|
||||||
code JwtTokenRequired = "PGRST302"
|
|
||||||
code (JwtClaimsErr _) = "PGRST303"
|
|
||||||
|
|
||||||
message JwtSecretMissing = "Server lacks JWT secret"
|
|
||||||
message (JwtDecodeErr e) = case e of
|
|
||||||
EmptyAuthHeader -> "Empty JWT is sent in Authorization header"
|
|
||||||
UnexpectedParts n -> "Expected 3 parts in JWT; got " <> show n
|
|
||||||
KeyError _ -> "No suitable key or wrong key type"
|
|
||||||
BadAlgorithm _ -> "Wrong or unsupported encoding algorithm"
|
|
||||||
BadCrypto -> "JWT cryptographic operation failed"
|
|
||||||
UnsupportedTokenType -> "Unsupported token type"
|
|
||||||
UnreachableDecodeError -> "JWT couldn't be decoded"
|
|
||||||
message JwtTokenRequired = "Anonymous access is disabled"
|
|
||||||
message (JwtClaimsErr e) = case e of
|
|
||||||
JWTExpired -> "JWT expired"
|
|
||||||
JWTNotYetValid -> "JWT not yet valid"
|
|
||||||
JWTIssuedAtFuture -> "JWT issued at future"
|
|
||||||
JWTNotInAudience -> "JWT not in audience"
|
|
||||||
ParsingClaimsFailed -> "Parsing claims failed"
|
|
||||||
ExpClaimNotNumber -> "The JWT 'exp' claim must be a number"
|
|
||||||
NbfClaimNotNumber -> "The JWT 'nbf' claim must be a number"
|
|
||||||
IatClaimNotNumber -> "The JWT 'iat' claim must be a number"
|
|
||||||
AudClaimNotStringOrArray -> "The JWT 'aud' claim must be a string or an array of strings"
|
|
||||||
|
|
||||||
details (JwtDecodeErr jde) = case jde of
|
|
||||||
KeyError dets -> Just $ JSON.String dets
|
|
||||||
BadAlgorithm dets -> Just $ JSON.String dets
|
|
||||||
_ -> Nothing
|
|
||||||
details _ = Nothing
|
|
||||||
|
|
||||||
hint _ = Nothing
|
|
||||||
|
|
||||||
invalidTokenHeader :: Text -> Header
|
invalidTokenHeader :: Text -> Header
|
||||||
invalidTokenHeader m =
|
invalidTokenHeader m =
|
||||||
@@ -816,3 +601,98 @@ parseRaisePGRST m d = do
|
|||||||
det <- maybeToRight (PGRSTParseError NoDetail) d
|
det <- maybeToRight (PGRSTParseError NoDetail) d
|
||||||
detJson <- maybeToRight (PGRSTParseError $ DetParseError det) (JSON.decodeStrict det)
|
detJson <- maybeToRight (PGRSTParseError $ DetParseError det) (JSON.decodeStrict det)
|
||||||
return (msgJson, detJson)
|
return (msgJson, detJson)
|
||||||
|
|
||||||
|
-- Error codes are grouped by common modules or characteristics
|
||||||
|
data ErrorCode
|
||||||
|
-- PostgreSQL connection errors
|
||||||
|
= ConnectionErrorCode00
|
||||||
|
| ConnectionErrorCode01
|
||||||
|
| ConnectionErrorCode02
|
||||||
|
| ConnectionErrorCode03
|
||||||
|
-- API Request errors
|
||||||
|
| ApiRequestErrorCode00
|
||||||
|
| ApiRequestErrorCode01
|
||||||
|
| ApiRequestErrorCode02
|
||||||
|
| ApiRequestErrorCode03
|
||||||
|
-- | ApiRequestErrorCode04 -- no longer used (used to be mapped to ParseRequestError)
|
||||||
|
| ApiRequestErrorCode05
|
||||||
|
| ApiRequestErrorCode06
|
||||||
|
| ApiRequestErrorCode07
|
||||||
|
| ApiRequestErrorCode08
|
||||||
|
| ApiRequestErrorCode09
|
||||||
|
| ApiRequestErrorCode10
|
||||||
|
| ApiRequestErrorCode11
|
||||||
|
-- | ApiRequestErrorCode13 -- no longer used (used to be mapped to BinaryFieldError)
|
||||||
|
| ApiRequestErrorCode12
|
||||||
|
| ApiRequestErrorCode14
|
||||||
|
| ApiRequestErrorCode15
|
||||||
|
| ApiRequestErrorCode16
|
||||||
|
| ApiRequestErrorCode17
|
||||||
|
| ApiRequestErrorCode18
|
||||||
|
| ApiRequestErrorCode19
|
||||||
|
| ApiRequestErrorCode20
|
||||||
|
| ApiRequestErrorCode21
|
||||||
|
| ApiRequestErrorCode22
|
||||||
|
| ApiRequestErrorCode23
|
||||||
|
| ApiRequestErrorCode24
|
||||||
|
-- Schema Cache errors
|
||||||
|
| SchemaCacheErrorCode00
|
||||||
|
| SchemaCacheErrorCode01
|
||||||
|
| SchemaCacheErrorCode02
|
||||||
|
| SchemaCacheErrorCode03
|
||||||
|
| SchemaCacheErrorCode04
|
||||||
|
-- JWT authentication errors
|
||||||
|
| JWTErrorCode00
|
||||||
|
| JWTErrorCode01
|
||||||
|
| JWTErrorCode02
|
||||||
|
-- Internal errors related to the Hasql library
|
||||||
|
| InternalErrorCode00
|
||||||
|
|
||||||
|
instance JSON.ToJSON ErrorCode where
|
||||||
|
toJSON e = JSON.toJSON (buildErrorCode e)
|
||||||
|
|
||||||
|
-- New group of errors will be added at the end of all the groups and will have the next prefix in the sequence
|
||||||
|
-- New errors are added at the end of the group they belong to and will have the next code in the sequence
|
||||||
|
buildErrorCode :: ErrorCode -> Text
|
||||||
|
buildErrorCode code = case code of
|
||||||
|
-- Keep the "PGRST" prefix in every code for an easier search/grep
|
||||||
|
ConnectionErrorCode00 -> "PGRST000"
|
||||||
|
ConnectionErrorCode01 -> "PGRST001"
|
||||||
|
ConnectionErrorCode02 -> "PGRST002"
|
||||||
|
ConnectionErrorCode03 -> "PGRST003"
|
||||||
|
|
||||||
|
ApiRequestErrorCode00 -> "PGRST100"
|
||||||
|
ApiRequestErrorCode01 -> "PGRST101"
|
||||||
|
ApiRequestErrorCode02 -> "PGRST102"
|
||||||
|
ApiRequestErrorCode03 -> "PGRST103"
|
||||||
|
ApiRequestErrorCode05 -> "PGRST105"
|
||||||
|
ApiRequestErrorCode06 -> "PGRST106"
|
||||||
|
ApiRequestErrorCode07 -> "PGRST107"
|
||||||
|
ApiRequestErrorCode08 -> "PGRST108"
|
||||||
|
ApiRequestErrorCode09 -> "PGRST109"
|
||||||
|
ApiRequestErrorCode10 -> "PGRST110"
|
||||||
|
ApiRequestErrorCode11 -> "PGRST111"
|
||||||
|
ApiRequestErrorCode12 -> "PGRST112"
|
||||||
|
ApiRequestErrorCode14 -> "PGRST114"
|
||||||
|
ApiRequestErrorCode15 -> "PGRST115"
|
||||||
|
ApiRequestErrorCode16 -> "PGRST116"
|
||||||
|
ApiRequestErrorCode17 -> "PGRST117"
|
||||||
|
ApiRequestErrorCode18 -> "PGRST118"
|
||||||
|
ApiRequestErrorCode19 -> "PGRST119"
|
||||||
|
ApiRequestErrorCode20 -> "PGRST120"
|
||||||
|
ApiRequestErrorCode21 -> "PGRST121"
|
||||||
|
ApiRequestErrorCode22 -> "PGRST122"
|
||||||
|
ApiRequestErrorCode23 -> "PGRST123"
|
||||||
|
ApiRequestErrorCode24 -> "PGRST124"
|
||||||
|
|
||||||
|
SchemaCacheErrorCode00 -> "PGRST200"
|
||||||
|
SchemaCacheErrorCode01 -> "PGRST201"
|
||||||
|
SchemaCacheErrorCode02 -> "PGRST202"
|
||||||
|
SchemaCacheErrorCode03 -> "PGRST203"
|
||||||
|
SchemaCacheErrorCode04 -> "PGRST204"
|
||||||
|
|
||||||
|
JWTErrorCode00 -> "PGRST300"
|
||||||
|
JWTErrorCode01 -> "PGRST301"
|
||||||
|
JWTErrorCode02 -> "PGRST302"
|
||||||
|
|
||||||
|
InternalErrorCode00 -> "PGRSTX00"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import Data.Time (ZonedTime, defaultTimeLocale, formatTime,
|
|||||||
import qualified Network.Wai as Wai
|
import qualified Network.Wai as Wai
|
||||||
import qualified Network.Wai.Middleware.RequestLogger as Wai
|
import qualified Network.Wai.Middleware.RequestLogger as Wai
|
||||||
|
|
||||||
import Network.HTTP.Types.Status (Status, status400, status500)
|
import Network.HTTP.Types.Status (status400, status500)
|
||||||
import System.IO.Unsafe (unsafePerformIO)
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
|
|
||||||
import PostgREST.Config (LogLevel (..))
|
import PostgREST.Config (LogLevel (..))
|
||||||
@@ -57,27 +57,24 @@ logWithDebounce loggerState action = do
|
|||||||
|
|
||||||
-- TODO stop using this middleware to reuse the same "observer" pattern for all our logs
|
-- TODO stop using this middleware to reuse the same "observer" pattern for all our logs
|
||||||
middleware :: LogLevel -> (Wai.Request -> Maybe BS.ByteString) -> Wai.Middleware
|
middleware :: LogLevel -> (Wai.Request -> Maybe BS.ByteString) -> Wai.Middleware
|
||||||
middleware logLevel getAuthRole =
|
middleware logLevel getAuthRole = case logLevel of
|
||||||
unsafePerformIO $
|
LogCrit -> requestLogger (const False)
|
||||||
|
LogError -> requestLogger (>= status500)
|
||||||
|
LogWarn -> requestLogger (>= status400)
|
||||||
|
LogInfo -> requestLogger (const True)
|
||||||
|
LogDebug -> requestLogger (const True)
|
||||||
|
where
|
||||||
|
requestLogger filterStatus = unsafePerformIO $
|
||||||
Wai.mkRequestLogger Wai.defaultRequestLoggerSettings
|
Wai.mkRequestLogger Wai.defaultRequestLoggerSettings
|
||||||
{ Wai.outputFormat =
|
{ Wai.outputFormat =
|
||||||
Wai.ApacheWithSettings $
|
Wai.ApacheWithSettings $
|
||||||
Wai.defaultApacheSettings &
|
Wai.defaultApacheSettings &
|
||||||
Wai.setApacheRequestFilter (\_ res -> shouldLogResponse logLevel $ Wai.responseStatus res) &
|
Wai.setApacheRequestFilter (\_ res -> filterStatus $ Wai.responseStatus res) &
|
||||||
Wai.setApacheUserGetter getAuthRole
|
Wai.setApacheUserGetter getAuthRole
|
||||||
, Wai.autoFlush = True
|
, Wai.autoFlush = True
|
||||||
, Wai.destination = Wai.Handle stdout
|
, Wai.destination = Wai.Handle stdout
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldLogResponse :: LogLevel -> Status -> Bool
|
|
||||||
shouldLogResponse logLevel = case logLevel of
|
|
||||||
LogCrit -> const False
|
|
||||||
LogError -> (>= status500)
|
|
||||||
LogWarn -> (>= status400)
|
|
||||||
LogInfo -> const True
|
|
||||||
LogDebug -> const True
|
|
||||||
|
|
||||||
-- All observations are logged except some that depend on the log-level
|
|
||||||
observationLogger :: LoggerState -> LogLevel -> ObservationHandler
|
observationLogger :: LoggerState -> LogLevel -> ObservationHandler
|
||||||
observationLogger loggerState logLevel obs = case obs of
|
observationLogger loggerState logLevel obs = case obs of
|
||||||
o@(PoolAcqTimeoutObs _) -> do
|
o@(PoolAcqTimeoutObs _) -> do
|
||||||
@@ -90,16 +87,10 @@ observationLogger loggerState logLevel obs = case obs of
|
|||||||
o@(HasqlPoolObs _) -> do
|
o@(HasqlPoolObs _) -> do
|
||||||
when (logLevel >= LogDebug) $ do
|
when (logLevel >= LogDebug) $ do
|
||||||
logWithZTime loggerState $ observationMessage o
|
logWithZTime loggerState $ observationMessage o
|
||||||
o@(DBQuery sql status) -> do
|
PoolRequest ->
|
||||||
-- Does not log SQL when it's empty (for OPTIONS requests or for the default OpenAPI output)
|
pure ()
|
||||||
when (sql /= mempty && shouldLogResponse logLevel status) $ do
|
PoolRequestFullfilled ->
|
||||||
logWithZTime loggerState $ observationMessage o
|
pure ()
|
||||||
o@PoolRequest ->
|
|
||||||
when (logLevel >= LogDebug) $ do
|
|
||||||
logWithZTime loggerState $ observationMessage o
|
|
||||||
o@PoolRequestFullfilled ->
|
|
||||||
when (logLevel >= LogDebug) $ do
|
|
||||||
logWithZTime loggerState $ observationMessage o
|
|
||||||
o ->
|
o ->
|
||||||
logWithZTime loggerState $ observationMessage o
|
logWithZTime loggerState $ observationMessage o
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
{-|
|
|
||||||
Module : PostgREST.Logger
|
|
||||||
Description : Metrics based on the Observation module. See Observation.hs.
|
|
||||||
-}
|
|
||||||
module PostgREST.Metrics
|
module PostgREST.Metrics
|
||||||
( init
|
( init
|
||||||
, MetricsState (..)
|
, MetricsState (..)
|
||||||
@@ -32,7 +28,6 @@ init configDbPoolSize = do
|
|||||||
setGauge poolMaxSize (fromIntegral configDbPoolSize)
|
setGauge poolMaxSize (fromIntegral configDbPoolSize)
|
||||||
pure $ MetricsState poolTimeouts poolAvailable poolWaiting poolMaxSize schemaCacheLoads schemaCacheQueryTime
|
pure $ MetricsState poolTimeouts poolAvailable poolWaiting poolMaxSize schemaCacheLoads schemaCacheQueryTime
|
||||||
|
|
||||||
-- Only some observations are used as metrics
|
|
||||||
observationMetrics :: MetricsState -> ObservationHandler
|
observationMetrics :: MetricsState -> ObservationHandler
|
||||||
observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schemaCacheLoads schemaCacheQueryTime) obs = case obs of
|
observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schemaCacheLoads schemaCacheQueryTime) obs = case obs of
|
||||||
(PoolAcqTimeoutObs _) -> do
|
(PoolAcqTimeoutObs _) -> do
|
||||||
@@ -52,7 +47,7 @@ observationMetrics (MetricsState poolTimeouts poolAvailable poolWaiting _ schema
|
|||||||
SchemaCacheLoadedObs resTime -> do
|
SchemaCacheLoadedObs resTime -> do
|
||||||
withLabel schemaCacheLoads "SUCCESS" incCounter
|
withLabel schemaCacheLoads "SUCCESS" incCounter
|
||||||
setGauge schemaCacheQueryTime resTime
|
setGauge schemaCacheQueryTime resTime
|
||||||
SchemaCacheErrorObs{} -> do
|
SchemaCacheErrorObs _ -> do
|
||||||
withLabel schemaCacheLoads "FAIL" incCounter
|
withLabel schemaCacheLoads "FAIL" incCounter
|
||||||
_ ->
|
_ ->
|
||||||
pure ()
|
pure ()
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
module PostgREST.Network
|
|
||||||
( resolveHost
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Data.IP (fromHostAddress, fromHostAddress6)
|
|
||||||
import Data.String (IsString (..))
|
|
||||||
import qualified Network.Socket as NS
|
|
||||||
|
|
||||||
import Protolude
|
|
||||||
|
|
||||||
resolveHost :: NS.Socket -> IO (Maybe Text)
|
|
||||||
resolveHost sock = do
|
|
||||||
sn <- NS.getSocketName sock
|
|
||||||
case sn of
|
|
||||||
NS.SockAddrInet _ hostAddr -> pure $ Just $ fromString $ show $ fromHostAddress hostAddr
|
|
||||||
-- The IPv6 addresses are wrapped in [] brackets. This is done in accordance
|
|
||||||
-- to RFC 3986 (https://datatracker.ietf.org/doc/html/rfc3986#section-3.2.2).
|
|
||||||
-- In short, we did this to have a clear separation between the port and host
|
|
||||||
-- because the components of an IPv6 are separated with the ':' character.
|
|
||||||
NS.SockAddrInet6 _ _ hostAddr6 _ -> pure $ Just $ fromString $ "[" ++ show (fromHostAddress6 hostAddr6) ++ "]"
|
|
||||||
_ -> pure Nothing
|
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-|
|
{-|
|
||||||
Module : PostgREST.Observation
|
Module : PostgREST.Observation
|
||||||
Description : This module holds an Observation type which is the core of Observability for PostgREST.
|
Description : Observations that can be used for Logging and Metrics
|
||||||
The Observation and ObservationHandler (the observer) are abstractions that allow centralizing logging and metrics concerns,
|
|
||||||
only observer calls with an Observation constructor are applied at different parts in the codebase.
|
|
||||||
The Logger and Metrics modules then decide which observations to expose. Not all observations need to be logged nor all correspond to a metric.
|
|
||||||
-}
|
-}
|
||||||
module PostgREST.Observation
|
module PostgREST.Observation
|
||||||
( Observation(..)
|
( Observation(..)
|
||||||
@@ -14,31 +11,29 @@ module PostgREST.Observation
|
|||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import Data.List.NonEmpty (toList)
|
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text.Encoding as T
|
import qualified Data.Text.Encoding as T
|
||||||
import qualified Hasql.Connection as SQL
|
import qualified Hasql.Connection as SQL
|
||||||
import qualified Hasql.Pool as SQL
|
import qualified Hasql.Pool as SQL
|
||||||
import qualified Hasql.Pool.Observation as SQL
|
import qualified Hasql.Pool.Observation as SQL
|
||||||
import Network.HTTP.Types.Status (Status)
|
|
||||||
import qualified Network.Socket as NS
|
import qualified Network.Socket as NS
|
||||||
import Numeric (showFFloat)
|
import Numeric (showFFloat)
|
||||||
import PostgREST.Config.PgVersion
|
import PostgREST.Config.PgVersion
|
||||||
import qualified PostgREST.Error as Error
|
import qualified PostgREST.Error as Error
|
||||||
|
|
||||||
import Protolude hiding (toList)
|
import Protolude
|
||||||
import Protolude.Partial (fromJust)
|
import Protolude.Partial (fromJust)
|
||||||
|
|
||||||
data Observation
|
data Observation
|
||||||
= AdminStartObs (Maybe Text) (Maybe Int)
|
= AdminStartObs (Maybe Int)
|
||||||
| AppStartObs ByteString
|
| AppStartObs ByteString
|
||||||
| AppServerPortObs Text NS.PortNumber
|
| AppServerPortObs NS.PortNumber
|
||||||
| AppServerUnixObs FilePath
|
| AppServerUnixObs FilePath
|
||||||
| ExitUnsupportedPgVersion PgVersion PgVersion
|
| ExitUnsupportedPgVersion PgVersion PgVersion
|
||||||
| ExitDBNoRecoveryObs
|
| ExitDBNoRecoveryObs
|
||||||
| ExitDBFatalError ObsFatalError SQL.UsageError
|
| ExitDBFatalError ObsFatalError SQL.UsageError
|
||||||
| DBConnectedObs Text
|
| DBConnectedObs Text
|
||||||
| SchemaCacheErrorObs (NonEmpty Text) [Text] SQL.UsageError
|
| SchemaCacheErrorObs SQL.UsageError
|
||||||
| SchemaCacheQueriedObs Double
|
| SchemaCacheQueriedObs Double
|
||||||
| SchemaCacheSummaryObs Text
|
| SchemaCacheSummaryObs Text
|
||||||
| SchemaCacheLoadedObs Double
|
| SchemaCacheLoadedObs Double
|
||||||
@@ -48,14 +43,12 @@ data Observation
|
|||||||
| DBListenRetry Int
|
| DBListenRetry Int
|
||||||
| DBListenerGotSCacheMsg ByteString
|
| DBListenerGotSCacheMsg ByteString
|
||||||
| DBListenerGotConfigMsg ByteString
|
| DBListenerGotConfigMsg ByteString
|
||||||
| DBQuery ByteString Status
|
|
||||||
| ConfigReadErrorObs SQL.UsageError
|
| ConfigReadErrorObs SQL.UsageError
|
||||||
| ConfigInvalidObs Text
|
| ConfigInvalidObs Text
|
||||||
| ConfigSucceededObs
|
| ConfigSucceededObs
|
||||||
| QueryRoleSettingsErrorObs SQL.UsageError
|
| QueryRoleSettingsErrorObs SQL.UsageError
|
||||||
| QueryErrorCodeHighObs SQL.UsageError
|
| QueryErrorCodeHighObs SQL.UsageError
|
||||||
| QueryPgVersionError SQL.UsageError
|
| QueryPgVersionError SQL.UsageError
|
||||||
| PoolInit Int
|
|
||||||
| PoolAcqTimeoutObs SQL.UsageError
|
| PoolAcqTimeoutObs SQL.UsageError
|
||||||
| HasqlPoolObs SQL.Observation
|
| HasqlPoolObs SQL.Observation
|
||||||
| PoolRequest
|
| PoolRequest
|
||||||
@@ -67,14 +60,14 @@ type ObservationHandler = Observation -> IO ()
|
|||||||
|
|
||||||
observationMessage :: Observation -> Text
|
observationMessage :: Observation -> Text
|
||||||
observationMessage = \case
|
observationMessage = \case
|
||||||
AdminStartObs host port ->
|
AdminStartObs port ->
|
||||||
"Admin server listening on " <> fromJust host <> ":" <> show (fromIntegral (fromJust port) :: Integer)
|
"Admin server listening on port " <> show (fromIntegral (fromJust port) :: Integer)
|
||||||
AppStartObs ver ->
|
AppStartObs ver ->
|
||||||
"Starting PostgREST " <> T.decodeUtf8 ver <> "..."
|
"Starting PostgREST " <> T.decodeUtf8 ver <> "..."
|
||||||
AppServerPortObs host port ->
|
AppServerPortObs port ->
|
||||||
"API server listening on " <> host <> ":" <> show port
|
"Listening on port " <> show port
|
||||||
AppServerUnixObs sock ->
|
AppServerUnixObs sock ->
|
||||||
"API server listening on unix socket " <> show sock
|
"Listening on unix socket " <> show sock
|
||||||
DBConnectedObs ver ->
|
DBConnectedObs ver ->
|
||||||
"Successfully connected to " <> ver
|
"Successfully connected to " <> ver
|
||||||
ExitUnsupportedPgVersion pgVer minPgVer ->
|
ExitUnsupportedPgVersion pgVer minPgVer ->
|
||||||
@@ -89,12 +82,8 @@ observationMessage = \case
|
|||||||
"If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
|
"If you are using connection poolers in transaction mode, try setting db-prepared-statements to false. " <> jsonMessage usageErr
|
||||||
ExitDBFatalError ServerError08P01 usageErr ->
|
ExitDBFatalError ServerError08P01 usageErr ->
|
||||||
"Connection poolers in statement mode are not supported." <> jsonMessage usageErr
|
"Connection poolers in statement mode are not supported." <> jsonMessage usageErr
|
||||||
SchemaCacheErrorObs dbSchemas extraPaths usageErr ->
|
SchemaCacheErrorObs usageErr ->
|
||||||
"Failed to load the schema cache using "
|
"Failed to load the schema cache. " <> jsonMessage usageErr
|
||||||
<> "db-schemas=" <> T.intercalate "," (toList dbSchemas)
|
|
||||||
<> " and "
|
|
||||||
<> "db-extra-search-path=" <> T.intercalate "," extraPaths
|
|
||||||
<> ". " <> jsonMessage usageErr
|
|
||||||
SchemaCacheQueriedObs resultTime ->
|
SchemaCacheQueriedObs resultTime ->
|
||||||
"Schema cache queried in " <> showMillis resultTime <> " milliseconds"
|
"Schema cache queried in " <> showMillis resultTime <> " milliseconds"
|
||||||
SchemaCacheSummaryObs summary ->
|
SchemaCacheSummaryObs summary ->
|
||||||
@@ -106,18 +95,19 @@ observationMessage = \case
|
|||||||
QueryPgVersionError usageErr ->
|
QueryPgVersionError usageErr ->
|
||||||
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr
|
"Failed to query the PostgreSQL version. " <> jsonMessage usageErr
|
||||||
DBListenStart channel -> do
|
DBListenStart channel -> do
|
||||||
"Listening for database notifications on the " <> show channel <> " channel"
|
"Listening for notifications on the " <> show channel <> " channel"
|
||||||
DBListenFail channel listenErr ->
|
DBListenFail channel listenErr ->
|
||||||
"Failed listening for database notifications on the " <> show channel <> " channel. " <>
|
"Failed listening for notifications on the " <> show channel <> " channel. " <> (
|
||||||
either showListenerConnError showListenerException listenErr
|
case listenErr of
|
||||||
|
Left err -> show err
|
||||||
|
Right err -> showListenerError err
|
||||||
|
)
|
||||||
DBListenRetry delay ->
|
DBListenRetry delay ->
|
||||||
"Retrying listening for database notifications in " <> (show delay::Text) <> " seconds..."
|
"Retrying listening for notifications in " <> (show delay::Text) <> " seconds..."
|
||||||
DBListenerGotSCacheMsg channel ->
|
DBListenerGotSCacheMsg channel ->
|
||||||
"Received a schema cache reload message on the " <> show channel <> " channel"
|
"Received a schema cache reload message on the " <> show channel <> " channel"
|
||||||
DBListenerGotConfigMsg channel ->
|
DBListenerGotConfigMsg channel ->
|
||||||
"Received a config reload message on the " <> show channel <> " channel"
|
"Received a config reload message on the " <> show channel <> " channel"
|
||||||
DBQuery sql _ ->
|
|
||||||
T.decodeUtf8 sql
|
|
||||||
ConfigReadErrorObs usageErr ->
|
ConfigReadErrorObs usageErr ->
|
||||||
"Failed to query database settings for the config parameters." <> jsonMessage usageErr
|
"Failed to query database settings for the config parameters." <> jsonMessage usageErr
|
||||||
QueryRoleSettingsErrorObs usageErr ->
|
QueryRoleSettingsErrorObs usageErr ->
|
||||||
@@ -128,8 +118,6 @@ observationMessage = \case
|
|||||||
"Failed reloading config: " <> err
|
"Failed reloading config: " <> err
|
||||||
ConfigSucceededObs ->
|
ConfigSucceededObs ->
|
||||||
"Config reloaded"
|
"Config reloaded"
|
||||||
PoolInit poolSize ->
|
|
||||||
"Connection Pool initialized with a maximum size of " <> show poolSize <> " connections"
|
|
||||||
PoolAcqTimeoutObs usageErr ->
|
PoolAcqTimeoutObs usageErr ->
|
||||||
jsonMessage usageErr
|
jsonMessage usageErr
|
||||||
HasqlPoolObs (SQL.ConnectionObservation uuid status) ->
|
HasqlPoolObs (SQL.ConnectionObservation uuid status) ->
|
||||||
@@ -144,21 +132,15 @@ observationMessage = \case
|
|||||||
SQL.ReleaseConnectionTerminationReason -> "release"
|
SQL.ReleaseConnectionTerminationReason -> "release"
|
||||||
SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message.
|
SQL.NetworkErrorConnectionTerminationReason _ -> "network error" -- usage error is already logged, no need to repeat the same message.
|
||||||
)
|
)
|
||||||
PoolRequest ->
|
_ -> mempty
|
||||||
"Trying to borrow a connection from pool"
|
|
||||||
PoolRequestFullfilled ->
|
|
||||||
"Borrowed a connection from the pool"
|
|
||||||
where
|
where
|
||||||
showMillis :: Double -> Text
|
showMillis :: Double -> Text
|
||||||
showMillis x = toS $ showFFloat (Just 1) (x * 1000) ""
|
showMillis x = toS $ showFFloat (Just 1) (x * 1000) ""
|
||||||
|
|
||||||
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
|
jsonMessage err = T.decodeUtf8 . LBS.toStrict . Error.errorPayload $ Error.PgError False err
|
||||||
|
|
||||||
showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt -- the errors from hasql-notifications come intercalated with "\t\n"
|
showListenerError :: Either SomeException () -> Text
|
||||||
|
showListenerError (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
|
||||||
showListenerConnError :: SQL.ConnectionError -> Text
|
showListenerError (Left e) =
|
||||||
showListenerConnError = maybe "Connection error" (showOnSingleLine . T.decodeUtf8)
|
let showOnSingleLine txt = T.intercalate " " $ T.filter (/= '\t') <$> T.lines txt in -- the errors from hasql-notifications come intercalated with "\t\n"
|
||||||
|
showOnSingleLine $ show e
|
||||||
showListenerException :: Either SomeException () -> Text
|
|
||||||
showListenerException (Right _) = "Failed getting notifications" -- should not happen as the listener will never finish (hasql-notifications uses `forever` internally) with a Right result
|
|
||||||
showListenerException (Left e) = showOnSingleLine $ show e
|
|
||||||
|
|||||||
@@ -25,16 +25,15 @@ module PostgREST.Plan
|
|||||||
, CallReadPlan(..)
|
, CallReadPlan(..)
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Data.HashMap.Strict.InsOrd as HMI
|
import qualified Data.HashMap.Strict.InsOrd as HMI
|
||||||
import qualified Data.List as L
|
import qualified Data.List as L
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
import qualified Data.Text as T
|
|
||||||
import qualified PostgREST.SchemaCache.Routine as Routine
|
import qualified PostgREST.SchemaCache.Routine as Routine
|
||||||
|
|
||||||
import Data.Either.Combinators (mapLeft, mapRight)
|
import Data.Either.Combinators (mapLeft, mapRight)
|
||||||
import Data.List (delete, lookup)
|
import Data.List (delete, lookup)
|
||||||
import Data.Maybe (fromJust)
|
|
||||||
import Data.Tree (Tree (..))
|
import Data.Tree (Tree (..))
|
||||||
|
|
||||||
import PostgREST.ApiRequest (Action (..),
|
import PostgREST.ApiRequest (Action (..),
|
||||||
@@ -44,9 +43,7 @@ import PostgREST.ApiRequest (Action (..),
|
|||||||
Mutation (..),
|
Mutation (..),
|
||||||
Payload (..))
|
Payload (..))
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..))
|
||||||
import PostgREST.Error (ApiRequestError (..),
|
import PostgREST.Error (Error (..))
|
||||||
Error (..),
|
|
||||||
SchemaCacheError (..))
|
|
||||||
import PostgREST.MediaType (MediaType (..))
|
import PostgREST.MediaType (MediaType (..))
|
||||||
import PostgREST.Query.SqlFragment (sourceCTEName)
|
import PostgREST.Query.SqlFragment (sourceCTEName)
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
@@ -72,8 +69,7 @@ import PostgREST.SchemaCache.Routine (MediaHandler (..),
|
|||||||
RoutineParam (..),
|
RoutineParam (..),
|
||||||
funcReturnsCompositeAlias,
|
funcReturnsCompositeAlias,
|
||||||
funcReturnsScalar,
|
funcReturnsScalar,
|
||||||
funcReturnsSetOfScalar,
|
funcReturnsSetOfScalar)
|
||||||
funcReturnsSingle)
|
|
||||||
import PostgREST.SchemaCache.Table (Column (..), Table (..),
|
import PostgREST.SchemaCache.Table (Column (..), Table (..),
|
||||||
TablesMap,
|
TablesMap,
|
||||||
tableColumnsList,
|
tableColumnsList,
|
||||||
@@ -157,34 +153,32 @@ dbActionPlan dbAct conf apiReq sCache = case dbAct of
|
|||||||
|
|
||||||
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Bool -> Either Error CrudPlan
|
wrappedReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> Bool -> Either Error CrudPlan
|
||||||
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do
|
wrappedReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} headersOnly = do
|
||||||
qi <- findTable identifier (dbTables sCache)
|
rPlan <- readPlan identifier conf sCache apiRequest
|
||||||
rPlan <- readPlan qi conf sCache apiRequest
|
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
|
||||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||||
return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly qi
|
return $ WrappedReadPlan rPlan SQL.Read handler mediaType headersOnly identifier
|
||||||
|
|
||||||
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan
|
mutateReadPlan :: Mutation -> ApiRequest -> QualifiedIdentifier -> AppConfig -> SchemaCache -> Either Error CrudPlan
|
||||||
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
|
mutateReadPlan mutation apiRequest@ApiRequest{iPreferences=Preferences{..},..} identifier conf sCache = do
|
||||||
qi <- findTable identifier (dbTables sCache)
|
rPlan <- readPlan identifier conf sCache apiRequest
|
||||||
rPlan <- readPlan qi conf sCache apiRequest
|
mPlan <- mutatePlan mutation identifier apiRequest sCache rPlan
|
||||||
mPlan <- mutatePlan mutation qi apiRequest sCache rPlan
|
|
||||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest qi iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest identifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||||
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation qi
|
return $ MutateReadPlan rPlan mPlan SQL.Write handler mediaType mutation identifier
|
||||||
|
|
||||||
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
|
callReadPlan :: QualifiedIdentifier -> AppConfig -> SchemaCache -> ApiRequest -> InvokeMethod -> Either Error CallReadPlan
|
||||||
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{preferHandling, invalidPrefs, preferMaxAffected},..} invMethod = do
|
callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferences{..},..} invMethod = do
|
||||||
let paramKeys = case invMethod of
|
let paramKeys = case invMethod of
|
||||||
InvRead _ -> S.fromList $ fst <$> qsParams'
|
InvRead _ -> S.fromList $ fst <$> qsParams'
|
||||||
Inv -> iColumns
|
Inv -> iColumns
|
||||||
proc@Function{..} <- mapLeft SchemaCacheErr $
|
proc@Function{..} <- mapLeft ApiRequestError $
|
||||||
findProc identifier paramKeys (dbRoutines sCache) iContentMediaType (invMethod == Inv)
|
findProc identifier paramKeys (preferParameters == Just SingleObject) (dbRoutines sCache) iContentMediaType (invMethod == Inv)
|
||||||
let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations
|
let relIdentifier = QualifiedIdentifier pdSchema (fromMaybe pdName $ Routine.funcTableName proc) -- done so a set returning function can embed other relations
|
||||||
rPlan <- readPlan relIdentifier conf sCache apiRequest
|
rPlan <- readPlan relIdentifier conf sCache apiRequest
|
||||||
let args = case (invMethod, iContentMediaType) of
|
let args = case (invMethod, iContentMediaType) of
|
||||||
(InvRead _, _) -> DirectArgs $ toRpcParams proc qsParams'
|
(InvRead _, _) -> jsonRpcParams proc qsParams'
|
||||||
(Inv, MTUrlEncoded) -> DirectArgs $ maybe mempty (toRpcParams proc . payArray) iPayload
|
(Inv, MTUrlEncoded) -> maybe mempty (jsonRpcParams proc . payArray) iPayload
|
||||||
(Inv, _) -> JsonArgs $ payRaw <$> iPayload
|
(Inv, _) -> maybe mempty payRaw iPayload
|
||||||
txMode = case (invMethod, pdVolatility) of
|
txMode = case (invMethod, pdVolatility) of
|
||||||
(InvRead _, _) -> SQL.Read
|
(InvRead _, _) -> SQL.Read
|
||||||
(Inv, Routine.Stable) -> SQL.Read
|
(Inv, Routine.Stable) -> SQL.Read
|
||||||
@@ -193,15 +187,10 @@ callReadPlan identifier conf sCache apiRequest@ApiRequest{iPreferences=Preferenc
|
|||||||
cPlan = callPlan proc apiRequest paramKeys args rPlan
|
cPlan = callPlan proc apiRequest paramKeys args rPlan
|
||||||
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
(handler, mediaType) <- mapLeft ApiRequestError $ negotiateContent conf apiRequest relIdentifier iAcceptMediaType (dbMediaHandlers sCache) (hasDefaultSelect rPlan)
|
||||||
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
if not (null invalidPrefs) && preferHandling == Just Strict then Left $ ApiRequestError $ InvalidPreferences invalidPrefs else Right ()
|
||||||
failMaxAffectedRpcReturnsSingle (preferMaxAffected, preferHandling) proc
|
|
||||||
return $ CallReadPlan rPlan cPlan txMode proc handler mediaType invMethod identifier
|
return $ CallReadPlan rPlan cPlan txMode proc handler mediaType invMethod identifier
|
||||||
where
|
where
|
||||||
qsParams' = QueryParams.qsParams iQueryParams
|
qsParams' = QueryParams.qsParams iQueryParams
|
||||||
|
|
||||||
failMaxAffectedRpcReturnsSingle :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> Routine -> Either Error ()
|
|
||||||
failMaxAffectedRpcReturnsSingle (Just (PreferMaxAffected _), Just Strict) rout = if funcReturnsSingle rout then Left $ ApiRequestError MaxAffectedRpcViolation else Right ()
|
|
||||||
failMaxAffectedRpcReturnsSingle _ _ = Right ()
|
|
||||||
|
|
||||||
hasDefaultSelect :: ReadPlanTree -> Bool
|
hasDefaultSelect :: ReadPlanTree -> Bool
|
||||||
hasDefaultSelect (Node ReadPlan{select=[CoercibleSelectField{csField=CoercibleField{cfName}}]} []) = cfName == "*"
|
hasDefaultSelect (Node ReadPlan{select=[CoercibleSelectField{csField=CoercibleField{cfName}}]} []) = cfName == "*"
|
||||||
hasDefaultSelect _ = False
|
hasDefaultSelect _ = False
|
||||||
@@ -219,10 +208,10 @@ inspectPlan apiRequest headersOnly schema = do
|
|||||||
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
|
Search a pg proc by matching name and arguments keys to parameters. Since a function can be overloaded,
|
||||||
the name is not enough to find it. An overloaded function can have a different volatility or even a different return type.
|
the name is not enough to find it. An overloaded function can have a different volatility or even a different return type.
|
||||||
-}
|
-}
|
||||||
findProc :: QualifiedIdentifier -> S.Set Text -> RoutineMap -> MediaType -> Bool -> Either SchemaCacheError Routine
|
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> RoutineMap -> MediaType -> Bool -> Either ApiRequestError Routine
|
||||||
findProc qi argumentsKeys allProcs contentMediaType isInvPost =
|
findProc qi argumentsKeys paramsAsSingleObject allProcs contentMediaType isInvPost =
|
||||||
case matchProc of
|
case matchProc of
|
||||||
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) contentMediaType isInvPost (HM.keys allProcs) lookupProcName
|
([], []) -> Left $ NoRpc (qiSchema qi) (qiName qi) (S.toList argumentsKeys) paramsAsSingleObject contentMediaType isInvPost (HM.keys allProcs) lookupProcName
|
||||||
-- If there are no functions with named arguments, fallback to the single unnamed argument function
|
-- If there are no functions with named arguments, fallback to the single unnamed argument function
|
||||||
([], [proc]) -> Right proc
|
([], [proc]) -> Right proc
|
||||||
([], procs) -> Left $ AmbiguousRpc (toList procs)
|
([], procs) -> Left $ AmbiguousRpc (toList procs)
|
||||||
@@ -253,9 +242,13 @@ findProc qi argumentsKeys allProcs contentMediaType isInvPost =
|
|||||||
matchesParams proc =
|
matchesParams proc =
|
||||||
let
|
let
|
||||||
params = pdParams proc
|
params = pdParams proc
|
||||||
|
firstType = (ppType <$> headMay params)
|
||||||
in
|
in
|
||||||
|
-- exceptional case for Prefer: params=single-object
|
||||||
|
if paramsAsSingleObject
|
||||||
|
then length params == 1 && (firstType == Just "json" || firstType == Just "jsonb")
|
||||||
-- If the function has no parameters, the arguments keys must be empty as well
|
-- If the function has no parameters, the arguments keys must be empty as well
|
||||||
if null params
|
else if null params
|
||||||
then null argumentsKeys && not (isInvPost && contentMediaType `elem` [MTOctetStream, MTTextPlain, MTTextXML])
|
then null argumentsKeys && not (isInvPost && contentMediaType `elem` [MTOctetStream, MTTextPlain, MTTextXML])
|
||||||
-- A function has optional and required parameters. Optional parameters have a default value and
|
-- A function has optional and required parameters. Optional parameters have a default value and
|
||||||
-- don't require arguments for the function to be executed, required parameters must have an argument present.
|
-- don't require arguments for the function to be executed, required parameters must have an argument present.
|
||||||
@@ -277,29 +270,27 @@ data ResolverContext = ResolverContext
|
|||||||
, outputType :: Text -- ^ The output type for the response payload; e.g. "csv", "json", "binary".
|
, outputType :: Text -- ^ The output type for the response payload; e.g. "csv", "json", "binary".
|
||||||
}
|
}
|
||||||
|
|
||||||
resolveColumnField :: Column -> Maybe ToTsVector -> CoercibleField
|
resolveColumnField :: Column -> CoercibleField
|
||||||
resolveColumnField col toTsV = CoercibleField (colName col) mempty False toTsV (colNominalType col) (colType col) Nothing (colDefault col) False
|
resolveColumnField col = CoercibleField (colName col) mempty False (colNominalType col) Nothing (colDefault col)
|
||||||
|
|
||||||
resolveTableFieldName :: Table -> FieldName -> Maybe ToTsVector -> CoercibleField
|
resolveTableFieldName :: Table -> FieldName -> CoercibleField
|
||||||
resolveTableFieldName table fieldName toTsV=
|
resolveTableFieldName table fieldName =
|
||||||
fromMaybe (unknownField fieldName []) $ HMI.lookup fieldName (tableColumns table) >>=
|
fromMaybe (unknownField fieldName []) $ HMI.lookup fieldName (tableColumns table) >>=
|
||||||
Just . flip resolveColumnField toTsV
|
Just . resolveColumnField
|
||||||
|
|
||||||
-- | Resolve a type within the context based on the given field name and JSON path. Although there are situations where failure to resolve a field is considered an error (see `resolveOrError`), there are also situations where we allow it (RPC calls). If it should be an error and `resolveOrError` doesn't fit, ensure to check the `cfIRType` isn't empty.
|
-- | Resolve a type within the context based on the given field name and JSON path. Although there are situations where failure to resolve a field is considered an error (see `resolveOrError`), there are also situations where we allow it (RPC calls). If it should be an error and `resolveOrError` doesn't fit, ensure to check the `cfIRType` isn't empty.
|
||||||
resolveTypeOrUnknown :: ResolverContext -> Field -> Maybe ToTsVector -> CoercibleField
|
resolveTypeOrUnknown :: ResolverContext -> Field -> CoercibleField
|
||||||
resolveTypeOrUnknown ResolverContext{..} (fn, jp) toTsV =
|
resolveTypeOrUnknown ResolverContext{..} (fn, jp) =
|
||||||
case res of
|
case res of
|
||||||
-- types that are already json/jsonb don't need to be converted with `to_jsonb` for using arrow operators `data->attr`
|
-- types that are already json/jsonb don't need to be converted with `to_jsonb` for using arrow operators `data->attr`
|
||||||
-- this prevents indexes not applying https://github.com/PostgREST/postgrest/issues/2594
|
-- this prevents indexes not applying https://github.com/PostgREST/postgrest/issues/2594
|
||||||
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp, cfToJson=False}
|
cf@CoercibleField{cfIRType="json"} -> cf{cfJsonPath=jp, cfToJson=False}
|
||||||
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp, cfToJson=False}
|
cf@CoercibleField{cfIRType="jsonb"} -> cf{cfJsonPath=jp, cfToJson=False}
|
||||||
-- Do not apply to_tsvector to tsvector types
|
|
||||||
cf@CoercibleField{cfBaseType="tsvector"} -> cf{cfJsonPath=jp, cfToJson=True, cfToTsVector=Nothing}
|
|
||||||
-- other types will get converted `to_jsonb(col)->attr`, even unknown types
|
-- other types will get converted `to_jsonb(col)->attr`, even unknown types
|
||||||
cf -> cf{cfJsonPath=jp, cfToJson=True}
|
cf -> cf{cfJsonPath=jp, cfToJson=True}
|
||||||
where
|
where
|
||||||
res = fromMaybe (unknownField fn jp) $ HM.lookup qi tables >>=
|
res = fromMaybe (unknownField fn jp) $ HM.lookup qi tables >>=
|
||||||
Just . (\t -> resolveTableFieldName t fn toTsV)
|
Just . flip resolveTableFieldName fn
|
||||||
|
|
||||||
-- | Install any pre-defined data representation from source to target to coerce this reference.
|
-- | Install any pre-defined data representation from source to target to coerce this reference.
|
||||||
--
|
--
|
||||||
@@ -325,15 +316,11 @@ withJsonParse ctx field@CoercibleField{cfIRType} = withTransformer ctx "json" cf
|
|||||||
|
|
||||||
-- | Map the intermediate representation type to the output type defined by the resolver context (normally json), if available.
|
-- | Map the intermediate representation type to the output type defined by the resolver context (normally json), if available.
|
||||||
resolveOutputField :: ResolverContext -> Field -> CoercibleField
|
resolveOutputField :: ResolverContext -> Field -> CoercibleField
|
||||||
resolveOutputField ctx field = withOutputFormat ctx $ resolveTypeOrUnknown ctx field Nothing
|
resolveOutputField ctx field = withOutputFormat ctx $ resolveTypeOrUnknown ctx field
|
||||||
|
|
||||||
-- | Map the query string format of a value (text) into the intermediate representation type, if available.
|
-- | Map the query string format of a value (text) into the intermediate representation type, if available.
|
||||||
resolveQueryInputField :: ResolverContext -> Field -> OpExpr -> CoercibleField
|
resolveQueryInputField :: ResolverContext -> Field -> CoercibleField
|
||||||
resolveQueryInputField ctx field opExpr = withTextParse ctx $ resolveTypeOrUnknown ctx field toTsVector
|
resolveQueryInputField ctx field = withTextParse ctx $ resolveTypeOrUnknown ctx field
|
||||||
where
|
|
||||||
toTsVector = case opExpr of
|
|
||||||
OpExpr _ (Fts _ lang _) -> Just $ ToTsVector lang
|
|
||||||
_ -> Nothing
|
|
||||||
|
|
||||||
-- | Builds the ReadPlan tree on a number of stages.
|
-- | Builds the ReadPlan tree on a number of stages.
|
||||||
-- | Adds filters, order, limits on its respective nodes.
|
-- | Adds filters, order, limits on its respective nodes.
|
||||||
@@ -344,12 +331,13 @@ readPlan qi@QualifiedIdentifier{..} AppConfig{configDbMaxRows, configDbAggregate
|
|||||||
-- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV.
|
-- JSON output format hardcoded for now. In the future we might want to support other output mappings such as CSV.
|
||||||
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
||||||
in
|
in
|
||||||
|
mapLeft ApiRequestError $
|
||||||
treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
|
treeRestrictRange configDbMaxRows (iAction apiRequest) =<<
|
||||||
addToManyOrderSelects =<<
|
|
||||||
hoistSpreadAggFunctions =<<
|
|
||||||
validateAggFunctions configDbAggregates =<<
|
validateAggFunctions configDbAggregates =<<
|
||||||
|
hoistSpreadAggFunctions =<<
|
||||||
addRelSelects =<<
|
addRelSelects =<<
|
||||||
addNullEmbedFilters =<<
|
addNullEmbedFilters =<<
|
||||||
|
validateSpreadEmbeds =<<
|
||||||
addRelatedOrders =<<
|
addRelatedOrders =<<
|
||||||
addAliases =<<
|
addAliases =<<
|
||||||
expandStars ctx =<<
|
expandStars ctx =<<
|
||||||
@@ -365,7 +353,7 @@ initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
|
|||||||
foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi ctx, relName=qiName, depth=rootDepth} []
|
foldr (treeEntry rootDepth) $ Node defReadPlan{from=qi ctx, relName=qiName, depth=rootDepth} []
|
||||||
where
|
where
|
||||||
rootDepth = 0
|
rootDepth = 0
|
||||||
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing Nothing [] rootDepth
|
defReadPlan = ReadPlan [] (QualifiedIdentifier mempty mempty) Nothing [] [] allRange mempty Nothing [] Nothing mempty Nothing Nothing False [] rootDepth
|
||||||
treeEntry :: Depth -> Tree SelectItem -> ReadPlanTree -> ReadPlanTree
|
treeEntry :: Depth -> Tree SelectItem -> ReadPlanTree -> ReadPlanTree
|
||||||
treeEntry depth (Node si fldForest) (Node q rForest) =
|
treeEntry depth (Node si fldForest) (Node q rForest) =
|
||||||
let nxtDepth = succ depth in
|
let nxtDepth = succ depth in
|
||||||
@@ -378,42 +366,26 @@ initReadRequest ctx@ResolverContext{qi=QualifiedIdentifier{..}} =
|
|||||||
SpreadRelation{..} ->
|
SpreadRelation{..} ->
|
||||||
Node q $
|
Node q $
|
||||||
foldr (treeEntry nxtDepth)
|
foldr (treeEntry nxtDepth)
|
||||||
(Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth, relSpread=Just ToOneSpread} [])
|
(Node defReadPlan{from=QualifiedIdentifier qiSchema selRelation, relName=selRelation, relHint=selHint, relJoinType=selJoinType, depth=nxtDepth, relIsSpread=True} [])
|
||||||
fldForest:rForest
|
fldForest:rForest
|
||||||
SelectField{..} ->
|
SelectField{..} ->
|
||||||
Node q{select=CoercibleSelectField (resolveOutputField ctx{qi=from q} selField) selAggregateFunction selAggregateCast selCast selAlias:select q} rForest
|
Node q{select=CoercibleSelectField (resolveOutputField ctx{qi=from q} selField) selAggregateFunction selAggregateCast selCast selAlias:select q} rForest
|
||||||
|
|
||||||
-- If an alias is explicitly specified, it is always respected. However, an alias may be
|
-- If an alias is explicitly specified, it is always respected. However, an alias may be
|
||||||
-- determined automatically in these cases:
|
-- determined automatically in the case of a select term with a JSON path, or in the case
|
||||||
-- * A select term with a JSON path
|
-- of domain representations.
|
||||||
-- * Domain representations
|
addAliases :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
-- * Aggregates in spread relationships
|
|
||||||
addAliases :: ReadPlanTree -> Either Error ReadPlanTree
|
|
||||||
addAliases = Right . fmap addAliasToPlan
|
addAliases = Right . fmap addAliasToPlan
|
||||||
where
|
where
|
||||||
addAliasToPlan rp@ReadPlan{select=sel, relSpread=spr} = rp{select=map (aliasSelectField $ isJust spr) sel}
|
addAliasToPlan rp@ReadPlan{select=sel} = rp{select=map aliasSelectField sel}
|
||||||
|
|
||||||
aliasSelectField :: Bool -> CoercibleSelectField -> CoercibleSelectField
|
aliasSelectField :: CoercibleSelectField -> CoercibleSelectField
|
||||||
aliasSelectField isSpread field@CoercibleSelectField{csField=fieldDetails, csAggFunction=aggFun, csAlias=alias}
|
aliasSelectField field@CoercibleSelectField{csField=fieldDetails, csAggFunction=aggFun, csAlias=alias}
|
||||||
| isJust alias = field
|
| isJust alias || isJust aggFun = field
|
||||||
| isJust aggFun = fieldAliasForSpreadAgg isSpread field
|
|
||||||
| isJsonKeyPath fieldDetails, Just key <- lastJsonKey fieldDetails = field { csAlias = Just key }
|
| isJsonKeyPath fieldDetails, Just key <- lastJsonKey fieldDetails = field { csAlias = Just key }
|
||||||
| isTransformPath fieldDetails = field { csAlias = Just (cfName fieldDetails) }
|
| isTransformPath fieldDetails = field { csAlias = Just (cfName fieldDetails) }
|
||||||
| otherwise = field
|
| otherwise = field
|
||||||
|
|
||||||
-- Spread relationships with non-aliased aggregates can cause problems when selecting the fields in the top level resource.
|
|
||||||
-- The top level won't know the name of the field in this case:
|
|
||||||
-- A nested to-one spread like `/top_table?select=...middle_table(...nested_table(count()))`
|
|
||||||
-- will do a `SELECT nested_table` instead of `SELECT *`, because doing a `COUNT(*)` in `top_table`
|
|
||||||
-- would not return the desired results.
|
|
||||||
--
|
|
||||||
-- That's why we need to use the aggregate name as an alias (e.g. COUNT(...) AS "count").
|
|
||||||
-- Since PostgreSQL labels the columns with the aggregate name, it shouldn't be a problem to
|
|
||||||
-- apply the aliases to all the aggregates regardless if the previous conditions are met.
|
|
||||||
fieldAliasForSpreadAgg True field@CoercibleSelectField{csAggFunction=Just agg} =
|
|
||||||
field { csAlias = Just (T.toLower $ show agg) }
|
|
||||||
fieldAliasForSpreadAgg _ field = field
|
|
||||||
|
|
||||||
isJsonKeyPath CoercibleField{cfJsonPath=(_: _)} = True
|
isJsonKeyPath CoercibleField{cfJsonPath=(_: _)} = True
|
||||||
isJsonKeyPath _ = False
|
isJsonKeyPath _ = False
|
||||||
|
|
||||||
@@ -438,14 +410,13 @@ knownColumnsInContext ResolverContext{..} =
|
|||||||
-- | Expand "select *" into explicit field names of the table in the following situations:
|
-- | Expand "select *" into explicit field names of the table in the following situations:
|
||||||
-- * When there are data representations present.
|
-- * When there are data representations present.
|
||||||
-- * When there is an aggregate function in a given ReadPlan or its parent.
|
-- * When there is an aggregate function in a given ReadPlan or its parent.
|
||||||
-- * When the ReadPlan is a to-many spread relationship
|
expandStars :: ResolverContext -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
expandStars :: ResolverContext -> ReadPlanTree -> Either Error ReadPlanTree
|
|
||||||
expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree
|
expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree
|
||||||
where
|
where
|
||||||
expandStarsForReadPlan :: Bool -> ReadPlanTree -> ReadPlanTree
|
expandStarsForReadPlan :: Bool -> ReadPlanTree -> ReadPlanTree
|
||||||
expandStarsForReadPlan hasAgg (Node rp@ReadPlan{select, from=fromQI, fromAlias=alias, relSpread=spread} children) =
|
expandStarsForReadPlan hasAgg (Node rp@ReadPlan{select, from=fromQI, fromAlias=alias} children) =
|
||||||
let
|
let
|
||||||
newHasAgg = hasAgg || any (isJust . csAggFunction) select || case spread of Just ToManySpread{} -> True; _ -> False
|
newHasAgg = hasAgg || any (isJust . csAggFunction) select
|
||||||
newCtx = adjustContext ctx fromQI alias
|
newCtx = adjustContext ctx fromQI alias
|
||||||
newRPlan = expandStarsForTable newCtx newHasAgg rp
|
newRPlan = expandStarsForTable newCtx newHasAgg rp
|
||||||
in Node newRPlan (map (expandStarsForReadPlan newHasAgg) children)
|
in Node newRPlan (map (expandStarsForReadPlan newHasAgg) children)
|
||||||
@@ -458,33 +429,29 @@ expandStars ctx rPlanTree = Right $ expandStarsForReadPlan False rPlanTree
|
|||||||
adjustContext context fromQI _ = context{qi=fromQI}
|
adjustContext context fromQI _ = context{qi=fromQI}
|
||||||
|
|
||||||
expandStarsForTable :: ResolverContext -> Bool -> ReadPlan -> ReadPlan
|
expandStarsForTable :: ResolverContext -> Bool -> ReadPlan -> ReadPlan
|
||||||
expandStarsForTable ctx@ResolverContext{representations, outputType} hasAgg rp@ReadPlan{select=selectFields, relSpread=spread}
|
expandStarsForTable ctx@ResolverContext{representations, outputType} hasAgg rp@ReadPlan{select=selectFields}
|
||||||
-- We expand if either of the below are true:
|
-- We expand if either of the below are true:
|
||||||
-- * We have a '*' select AND there is an aggregate function in this ReadPlan's sub-tree.
|
-- * We have a '*' select AND there is an aggregate function in this ReadPlan's sub-tree.
|
||||||
-- * We have a '*' select AND the target table has at least one data representation.
|
-- * We have a '*' select AND the target table has at least one data representation.
|
||||||
-- We ignore '*' selects that have an aggregate function attached, unless it's a `COUNT(*)` for a Spread Embed,
|
-- We ignore any '*' selects that have an aggregate function attached (i.e for COUNT(*)).
|
||||||
-- we tag it as "full row" in that case.
|
| hasStarSelect && (hasAgg || hasDataRepresentation) = rp{select = concatMap (expandStarSelectField knownColumns) selectFields}
|
||||||
| hasStarSelect && (hasAgg || hasDataRepresentation) = rp{select = concatMap (expandStarSelectField (isJust spread) knownColumns) selectFields}
|
|
||||||
| otherwise = rp
|
| otherwise = rp
|
||||||
where
|
where
|
||||||
hasStarSelect = "*" `elem` map (cfName . csField) filteredSelectFields
|
hasStarSelect = "*" `elem` map (cfName . csField) filteredSelectFields
|
||||||
filteredSelectFields = filter (shouldExpandOrTag . csAggFunction) selectFields
|
filteredSelectFields = filter (isNothing . csAggFunction) selectFields
|
||||||
shouldExpandOrTag aggFunc = isNothing aggFunc || (isJust spread && aggFunc == Just Count)
|
|
||||||
hasDataRepresentation = any hasOutputRep knownColumns
|
hasDataRepresentation = any hasOutputRep knownColumns
|
||||||
knownColumns = knownColumnsInContext ctx
|
knownColumns = knownColumnsInContext ctx
|
||||||
|
|
||||||
hasOutputRep :: Column -> Bool
|
hasOutputRep :: Column -> Bool
|
||||||
hasOutputRep col = HM.member (colNominalType col, outputType) representations
|
hasOutputRep col = HM.member (colNominalType col, outputType) representations
|
||||||
|
|
||||||
expandStarSelectField :: Bool -> [Column] -> CoercibleSelectField -> [CoercibleSelectField]
|
expandStarSelectField :: [Column] -> CoercibleSelectField -> [CoercibleSelectField]
|
||||||
expandStarSelectField _ columns sel@CoercibleSelectField{csField=CoercibleField{cfName="*", cfJsonPath=[]}, csAggFunction=Nothing} =
|
expandStarSelectField columns sel@CoercibleSelectField{csField=CoercibleField{cfName="*", cfJsonPath=[]}, csAggFunction=Nothing} =
|
||||||
map (\col -> sel { csField = withOutputFormat ctx $ resolveColumnField col Nothing }) columns
|
map (\col -> sel { csField = withOutputFormat ctx $ resolveColumnField col }) columns
|
||||||
expandStarSelectField True _ sel@CoercibleSelectField{csField=fld@CoercibleField{cfName="*", cfJsonPath=[]}, csAggFunction=Just Count} =
|
expandStarSelectField _ selectField = [selectField]
|
||||||
[sel { csField = fld { cfFullRow = True } }]
|
|
||||||
expandStarSelectField _ _ selectField = [selectField]
|
|
||||||
|
|
||||||
-- | Enforces the `max-rows` config on the result
|
-- | Enforces the `max-rows` config on the result
|
||||||
treeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either Error ReadPlanTree
|
treeRestrictRange :: Maybe Integer -> Action -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
treeRestrictRange _ (ActDb (ActRelationMut _ _)) request = Right request
|
treeRestrictRange _ (ActDb (ActRelationMut _ _)) request = Right request
|
||||||
treeRestrictRange maxRows _ request = pure $ nodeRestrictRange maxRows <$> request
|
treeRestrictRange maxRows _ request = pure $ nodeRestrictRange maxRows <$> request
|
||||||
where
|
where
|
||||||
@@ -493,22 +460,21 @@ treeRestrictRange maxRows _ request = pure $ nodeRestrictRange maxRows <$> reque
|
|||||||
|
|
||||||
-- add relationships to the nodes of the tree by traversing the forest while keeping track of the parentNode(https://stackoverflow.com/questions/22721064/get-the-parent-of-a-node-in-data-tree-haskell#comment34627048_22721064)
|
-- add relationships to the nodes of the tree by traversing the forest while keeping track of the parentNode(https://stackoverflow.com/questions/22721064/get-the-parent-of-a-node-in-data-tree-haskell#comment34627048_22721064)
|
||||||
-- also adds aliasing
|
-- also adds aliasing
|
||||||
addRels :: Schema -> Action -> RelationshipsMap -> Maybe ReadPlanTree -> ReadPlanTree -> Either Error ReadPlanTree
|
addRels :: Schema -> Action -> RelationshipsMap -> Maybe ReadPlanTree -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addRels schema action allRels parentNode (Node rPlan@ReadPlan{relName,relHint,relAlias,relSpread,depth} forest) =
|
addRels schema action allRels parentNode (Node rPlan@ReadPlan{relName,relHint,relAlias,depth} forest) =
|
||||||
case parentNode of
|
case parentNode of
|
||||||
Just (Node ReadPlan{from=parentNodeQi, fromAlias=parentAlias} _) ->
|
Just (Node ReadPlan{from=parentNodeQi, fromAlias=parentAlias} _) ->
|
||||||
let
|
let
|
||||||
newReadPlan = (\r ->
|
newReadPlan = (\r ->
|
||||||
let newAlias = Just (qiName (relForeignTable r) <> "_" <> show depth)
|
let newAlias = Just (qiName (relForeignTable r) <> "_" <> show depth)
|
||||||
aggAlias = qiName (relTable r) <> "_" <> fromMaybe relName relAlias <> "_" <> show depth
|
aggAlias = qiName (relTable r) <> "_" <> fromMaybe relName relAlias <> "_" <> show depth in
|
||||||
updSpread = if isJust relSpread && not (relIsToOne r) then Just $ ToManySpread [] [] else relSpread in
|
|
||||||
case r of
|
case r of
|
||||||
Relationship{relCardinality=M2M _} -> -- m2m does internal implicit joins that don't need aliasing
|
Relationship{relCardinality=M2M _} -> -- m2m does internal implicit joins that don't need aliasing
|
||||||
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, relJoinConds=getJoinConditions Nothing parentAlias r, relSpread=updSpread}
|
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, relJoinConds=getJoinConditions Nothing parentAlias r}
|
||||||
ComputedRelationship{} ->
|
ComputedRelationship{} ->
|
||||||
rPlan{from=relForeignTable r, relToParent=Just r{relTableAlias=maybe (relTable r) (QualifiedIdentifier mempty) parentAlias}, relAggAlias=aggAlias, fromAlias=newAlias, relSpread=updSpread}
|
rPlan{from=relForeignTable r, relToParent=Just r{relTableAlias=maybe (relTable r) (QualifiedIdentifier mempty) parentAlias}, relAggAlias=aggAlias, fromAlias=newAlias}
|
||||||
_ ->
|
_ ->
|
||||||
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, fromAlias=newAlias, relJoinConds=getJoinConditions newAlias parentAlias r, relSpread=updSpread}
|
rPlan{from=relForeignTable r, relToParent=Just r, relAggAlias=aggAlias, fromAlias=newAlias, relJoinConds=getJoinConditions newAlias parentAlias r}
|
||||||
) <$> rel
|
) <$> rel
|
||||||
origin = if depth == 1 -- Only on depth 1 we check if the root(depth 0) has an alias so the sourceCTEName alias can be found as a relationship
|
origin = if depth == 1 -- Only on depth 1 we check if the root(depth 0) has an alias so the sourceCTEName alias can be found as a relationship
|
||||||
then fromMaybe (qiName parentNodeQi) parentAlias
|
then fromMaybe (qiName parentNodeQi) parentAlias
|
||||||
@@ -529,7 +495,7 @@ addRels schema action allRels parentNode (Node rPlan@ReadPlan{relName,relHint,re
|
|||||||
in
|
in
|
||||||
Node newReadPlan <$> updateForest (Just $ Node newReadPlan forest)
|
Node newReadPlan <$> updateForest (Just $ Node newReadPlan forest)
|
||||||
where
|
where
|
||||||
updateForest :: Maybe ReadPlanTree -> Either Error [ReadPlanTree]
|
updateForest :: Maybe ReadPlanTree -> Either ApiRequestError [ReadPlanTree]
|
||||||
updateForest rq = addRels schema action allRels rq `traverse` forest
|
updateForest rq = addRels schema action allRels rq `traverse` forest
|
||||||
|
|
||||||
getJoinConditions :: Maybe Alias -> Maybe Alias -> Relationship -> [JoinCondition]
|
getJoinConditions :: Maybe Alias -> Maybe Alias -> Relationship -> [JoinCondition]
|
||||||
@@ -559,12 +525,12 @@ getJoinConditions tblAlias parentAlias Relationship{relTable=qi,relForeignTable=
|
|||||||
-- request is ambiguous and we return an error. In that case the request can
|
-- request is ambiguous and we return an error. In that case the request can
|
||||||
-- be disambiguated by adding precision to the target or by using a hint:
|
-- be disambiguated by adding precision to the target or by using a hint:
|
||||||
-- /origin?select=target!hint(*). The origin can be a table or view.
|
-- /origin?select=target!hint(*). The origin can be a table or view.
|
||||||
findRel :: Schema -> RelationshipsMap -> NodeName -> NodeName -> Maybe Hint -> Either Error Relationship
|
findRel :: Schema -> RelationshipsMap -> NodeName -> NodeName -> Maybe Hint -> Either ApiRequestError Relationship
|
||||||
findRel schema allRels origin target hint =
|
findRel schema allRels origin target hint =
|
||||||
case rels of
|
case rels of
|
||||||
[] -> Left $ SchemaCacheErr $ NoRelBetween origin target hint schema allRels
|
[] -> Left $ NoRelBetween origin target hint schema allRels
|
||||||
[r] -> Right r
|
[r] -> Right r
|
||||||
rs -> Left $ SchemaCacheErr $ AmbiguousRelBetween origin target rs
|
rs -> Left $ AmbiguousRelBetween origin target rs
|
||||||
where
|
where
|
||||||
matchFKSingleCol hint_ card = case card of
|
matchFKSingleCol hint_ card = case card of
|
||||||
O2M{relColumns=[(col, _)]} -> hint_ == col
|
O2M{relColumns=[(col, _)]} -> hint_ == col
|
||||||
@@ -640,7 +606,7 @@ findRel schema allRels origin target hint =
|
|||||||
) $ fromMaybe mempty $ HM.lookup (QualifiedIdentifier schema origin, schema) allRels
|
) $ fromMaybe mempty $ HM.lookup (QualifiedIdentifier schema origin, schema) allRels
|
||||||
|
|
||||||
|
|
||||||
addRelSelects :: ReadPlanTree -> Either Error ReadPlanTree
|
addRelSelects :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addRelSelects node@(Node rp forest)
|
addRelSelects node@(Node rp forest)
|
||||||
| null forest = Right node
|
| null forest = Right node
|
||||||
| otherwise =
|
| otherwise =
|
||||||
@@ -649,9 +615,9 @@ addRelSelects node@(Node rp forest)
|
|||||||
in Right $ Node rp { relSelect = newRelSelects } newForest
|
in Right $ Node rp { relSelect = newRelSelects } newForest
|
||||||
|
|
||||||
generateRelSelectField :: ReadPlanTree -> Maybe RelSelectField
|
generateRelSelectField :: ReadPlanTree -> Maybe RelSelectField
|
||||||
generateRelSelectField (Node rp@ReadPlan{relToParent=Just _, relAggAlias, relSpread = Just _} _) =
|
generateRelSelectField (Node rp@ReadPlan{relToParent=Just _, relAggAlias, relIsSpread = True} _) =
|
||||||
Just $ Spread { rsSpreadSel = generateSpreadSelectFields rp, rsAggAlias = relAggAlias }
|
Just $ Spread { rsSpreadSel = generateSpreadSelectFields rp, rsAggAlias = relAggAlias }
|
||||||
generateRelSelectField (Node ReadPlan{relToParent=Just rel, select, relName, relAlias, relAggAlias, relSpread = Nothing} forest) =
|
generateRelSelectField (Node ReadPlan{relToParent=Just rel, select, relName, relAlias, relAggAlias, relIsSpread = False} forest) =
|
||||||
Just $ JsonEmbed { rsEmbedMode, rsSelName, rsAggAlias = relAggAlias, rsEmptyEmbed }
|
Just $ JsonEmbed { rsEmbedMode, rsSelName, rsAggAlias = relAggAlias, rsEmptyEmbed }
|
||||||
where
|
where
|
||||||
rsSelName = fromMaybe relName relAlias
|
rsSelName = fromMaybe relName relAlias
|
||||||
@@ -680,7 +646,7 @@ generateSpreadSelectFields ReadPlan{select, relSelect} =
|
|||||||
relSelectToSpread (Spread{rsSpreadSel}) =
|
relSelectToSpread (Spread{rsSpreadSel}) =
|
||||||
rsSpreadSel
|
rsSpreadSel
|
||||||
|
|
||||||
-- When aggregates are present in a ReadPlan with a to-one spread, we "hoist"
|
-- When aggregates are present in a ReadPlan that will be spread, we "hoist"
|
||||||
-- to the highest level possible so that their semantics make sense. For instance,
|
-- to the highest level possible so that their semantics make sense. For instance,
|
||||||
-- imagine the user performs the following request:
|
-- imagine the user performs the following request:
|
||||||
-- `GET /projects?select=client_id,...project_invoices(invoice_total.sum())`
|
-- `GET /projects?select=client_id,...project_invoices(invoice_total.sum())`
|
||||||
@@ -701,39 +667,28 @@ generateSpreadSelectFields ReadPlan{select, relSelect} =
|
|||||||
--
|
--
|
||||||
-- The second tuple contains the aggregate function to be applied, the cast, and
|
-- The second tuple contains the aggregate function to be applied, the cast, and
|
||||||
-- the alias, if it was supplied by the user or otherwise determined.
|
-- the alias, if it was supplied by the user or otherwise determined.
|
||||||
--
|
|
||||||
-- No hoisting is done for to-many spreads
|
|
||||||
type HoistedAgg = ((Alias, FieldName), (AggregateFunction, Maybe Cast, Maybe Alias))
|
type HoistedAgg = ((Alias, FieldName), (AggregateFunction, Maybe Cast, Maybe Alias))
|
||||||
|
|
||||||
hoistSpreadAggFunctions :: ReadPlanTree -> Either Error ReadPlanTree
|
hoistSpreadAggFunctions :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
hoistSpreadAggFunctions tree = Right $ fst $ applySpreadAggHoistingToNode tree
|
hoistSpreadAggFunctions tree = Right $ fst $ applySpreadAggHoistingToNode tree
|
||||||
|
|
||||||
applySpreadAggHoistingToNode :: ReadPlanTree -> (ReadPlanTree, [HoistedAgg])
|
applySpreadAggHoistingToNode :: ReadPlanTree -> (ReadPlanTree, [HoistedAgg])
|
||||||
applySpreadAggHoistingToNode (Node rp@ReadPlan{relAggAlias, relToParent, relSpread} children) =
|
applySpreadAggHoistingToNode (Node rp@ReadPlan{relAggAlias, relToParent, relIsSpread} children) =
|
||||||
let (newChildren, childAggLists) = unzip $ map applySpreadAggHoistingToNode children
|
let (newChildren, childAggLists) = unzip $ map applySpreadAggHoistingToNode children
|
||||||
allChildAggLists = concat childAggLists
|
allChildAggLists = concat childAggLists
|
||||||
isToOneSpread = relSpread == Just ToOneSpread
|
(newSelects, aggList) = if depth rp == 0 || (isJust relToParent && not relIsSpread)
|
||||||
(newSelects, aggList) = if depth rp == 0 || (isJust relToParent && not isToOneSpread)
|
|
||||||
then (select rp, [])
|
then (select rp, [])
|
||||||
else hoistFromSelectFields relAggAlias (select rp)
|
else hoistFromSelectFields relAggAlias (select rp)
|
||||||
|
|
||||||
-- If the current `ReadPlan` is a to-one spread rel and it has aggregates hoisted from
|
newRelSelects = if null children
|
||||||
-- child relationships, then it must hoist those aggregates to its parent rel.
|
|
||||||
-- So we update them with the current `relAggAlias`.
|
|
||||||
hoistAgg ((_, fieldName), hoistFunc) = ((relAggAlias, fieldName), hoistFunc)
|
|
||||||
hoistedAggList = if isToOneSpread
|
|
||||||
then aggList ++ map hoistAgg allChildAggLists
|
|
||||||
else aggList
|
|
||||||
|
|
||||||
newRelSelects = if null children || isToOneSpread
|
|
||||||
then relSelect rp
|
then relSelect rp
|
||||||
else map (hoistIntoRelSelectFields allChildAggLists) $ relSelect rp
|
else map (hoistIntoRelSelectFields allChildAggLists) $ relSelect rp
|
||||||
in (Node rp { select = newSelects, relSelect = newRelSelects } newChildren, hoistedAggList)
|
in (Node rp { select = newSelects, relSelect = newRelSelects } newChildren, aggList)
|
||||||
|
|
||||||
-- Hoist aggregate functions from the select list of a ReadPlan, and return the
|
-- Hoist aggregate functions from the select list of a ReadPlan, and return the
|
||||||
-- updated select list and the list of hoisted aggregates.
|
-- updated select list and the list of hoisted aggregates.
|
||||||
hoistFromSelectFields :: Alias -> [CoercibleSelectField] -> ([CoercibleSelectField], [HoistedAgg])
|
hoistFromSelectFields :: Alias -> [CoercibleSelectField] -> ([CoercibleSelectField], [HoistedAgg])
|
||||||
hoistFromSelectFields relAggAlias fields =
|
hoistFromSelectFields aggAlias fields =
|
||||||
let (newFields, maybeAggs) = foldr processField ([], []) fields
|
let (newFields, maybeAggs) = foldr processField ([], []) fields
|
||||||
in (newFields, catMaybes maybeAggs)
|
in (newFields, catMaybes maybeAggs)
|
||||||
where
|
where
|
||||||
@@ -741,21 +696,23 @@ hoistFromSelectFields relAggAlias fields =
|
|||||||
let (modifiedField, maybeAgg) = modifyField field
|
let (modifiedField, maybeAgg) = modifyField field
|
||||||
in (modifiedField : newFields, maybeAgg : aggList)
|
in (modifiedField : newFields, maybeAgg : aggList)
|
||||||
|
|
||||||
modifyField field@CoercibleSelectField{csAggFunction=Just aggFunc, csField, csAggCast, csAlias} =
|
modifyField field =
|
||||||
let determineFieldName = fromMaybe (cfName csField) csAlias
|
case csAggFunction field of
|
||||||
updatedField = field {csAggFunction = Nothing, csAggCast = Nothing}
|
Just aggFunc ->
|
||||||
hoistedField = Just ((relAggAlias, determineFieldName), (aggFunc, csAggCast, csAlias))
|
( field { csAggFunction = Nothing, csAggCast = Nothing },
|
||||||
in (updatedField, hoistedField)
|
Just ((aggAlias, determineFieldName field), (aggFunc, csAggCast field, csAlias field)))
|
||||||
modifyField field = (field, Nothing)
|
Nothing -> (field, Nothing)
|
||||||
|
|
||||||
|
determineFieldName field = fromMaybe (cfName $ csField field) (csAlias field)
|
||||||
|
|
||||||
-- Taking the hoisted aggregates, modify the rel selects to apply the aggregates,
|
-- Taking the hoisted aggregates, modify the rel selects to apply the aggregates,
|
||||||
-- and any applicable casts or aliases.
|
-- and any applicable casts or aliases.
|
||||||
hoistIntoRelSelectFields :: [HoistedAgg] -> RelSelectField -> RelSelectField
|
hoistIntoRelSelectFields :: [HoistedAgg] -> RelSelectField -> RelSelectField
|
||||||
hoistIntoRelSelectFields aggList r@(Spread {rsSpreadSel = spreadSelects, rsAggAlias = relAggAlias}) =
|
hoistIntoRelSelectFields aggList r@(Spread {rsSpreadSel = spreadSelects, rsAggAlias = aggAlias}) =
|
||||||
r { rsSpreadSel = map updateSelect spreadSelects }
|
r { rsSpreadSel = map updateSelect spreadSelects }
|
||||||
where
|
where
|
||||||
updateSelect s =
|
updateSelect s =
|
||||||
case lookup (relAggAlias, ssSelName s) aggList of
|
case lookup (aggAlias, ssSelName s) aggList of
|
||||||
Just (aggFunc, aggCast, fldAlias) ->
|
Just (aggFunc, aggCast, fldAlias) ->
|
||||||
s { ssSelAggFunction = Just aggFunc,
|
s { ssSelAggFunction = Just aggFunc,
|
||||||
ssSelAggCast = aggCast,
|
ssSelAggCast = aggCast,
|
||||||
@@ -763,45 +720,13 @@ hoistIntoRelSelectFields aggList r@(Spread {rsSpreadSel = spreadSelects, rsAggAl
|
|||||||
Nothing -> s
|
Nothing -> s
|
||||||
hoistIntoRelSelectFields _ r = r
|
hoistIntoRelSelectFields _ r = r
|
||||||
|
|
||||||
-- | Handle ordering in a To-Many Spread Relationship
|
validateAggFunctions :: Bool -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
-- * It removes the ordering done in the ReadPlan and moves it to the SpreadType.
|
|
||||||
-- We also select the ordering columns and alias them to avoid collisions. This is because it would be impossible
|
|
||||||
-- to order once it's aggregated if it's not selected in the inner query beforehand.
|
|
||||||
addToManyOrderSelects :: ReadPlanTree -> Either Error ReadPlanTree
|
|
||||||
addToManyOrderSelects (Node rp@ReadPlan{order, select, relAggAlias, relSelect, relSpread = Just ToManySpread {}} forest)
|
|
||||||
| anyAggSel || anyAggRelSel = Left $ ApiRequestError $ NotImplemented "Aggregates are not implemented for one-to-many or many-to-many spreads."
|
|
||||||
| otherwise = Node rp { order = [], relSpread = newRelSpread } <$> addToManyOrderSelects `traverse` forest
|
|
||||||
where
|
|
||||||
newRelSpread = Just ToManySpread { stExtraSelect = addSprExtraSelects, stOrder = addSprOrder}
|
|
||||||
anyAggSel = any (isJust . csAggFunction) select
|
|
||||||
anyAggRelSel = any (\case Spread sels _ -> any (isJust . ssSelAggFunction) sels; _ -> False) relSelect
|
|
||||||
(addSprExtraSelects, addSprOrder) = unzip $ zipWith ordToExtraSelsAndSprOrds [1..] order
|
|
||||||
ordToExtraSelsAndSprOrds i = \case
|
|
||||||
CoercibleOrderTerm fld dir ordr -> (
|
|
||||||
(Nothing, CoercibleSelectField fld Nothing Nothing Nothing (Just $ selOrdAlias (cfName fld) i)),
|
|
||||||
CoercibleOrderTerm (unknownField (selOrdAlias (cfName fld) i) []) dir ordr
|
|
||||||
)
|
|
||||||
CoercibleOrderRelationTerm rel (fld,jp) dir ordr -> (
|
|
||||||
(Just rel, CoercibleSelectField (unknownField fld jp) Nothing Nothing Nothing (Just $ selOrdAlias fld i)),
|
|
||||||
CoercibleOrderTerm (unknownField (selOrdAlias fld i) []) dir ordr
|
|
||||||
)
|
|
||||||
selOrdAlias :: Alias -> Integer -> Alias
|
|
||||||
selOrdAlias name i = relAggAlias <> "_" <> name <> "_" <> show i -- add index to avoid collisions in aliases
|
|
||||||
addToManyOrderSelects (Node rp forest) = Node rp <$> addToManyOrderSelects `traverse` forest
|
|
||||||
|
|
||||||
validateAggFunctions :: Bool -> ReadPlanTree -> Either Error ReadPlanTree
|
|
||||||
validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
|
validateAggFunctions aggFunctionsAllowed (Node rp@ReadPlan {select} forest)
|
||||||
| not aggFunctionsAllowed && any (isJust . csAggFunction) select = Left $ ApiRequestError AggregatesNotAllowed
|
| aggFunctionsAllowed = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
|
||||||
|
| any (isJust . csAggFunction) select = Left AggregatesNotAllowed
|
||||||
| otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
|
| otherwise = Node rp <$> traverse (validateAggFunctions aggFunctionsAllowed) forest
|
||||||
|
|
||||||
-- | Lookup table in the schema cache before creating read plan
|
addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
findTable :: QualifiedIdentifier -> TablesMap -> Either Error QualifiedIdentifier
|
|
||||||
findTable qi@QualifiedIdentifier{..} tableMap =
|
|
||||||
case HM.lookup qi tableMap of
|
|
||||||
Nothing -> Left $ SchemaCacheErr $ TableNotFound qiSchema qiName (HM.elems tableMap)
|
|
||||||
Just _ -> Right qi
|
|
||||||
|
|
||||||
addFilters :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
|
||||||
addFilters ctx ApiRequest{..} rReq =
|
addFilters ctx ApiRequest{..} rReq =
|
||||||
foldr addFilterToNode (Right rReq) flts
|
foldr addFilterToNode (Right rReq) flts
|
||||||
where
|
where
|
||||||
@@ -812,26 +737,29 @@ addFilters ctx ApiRequest{..} rReq =
|
|||||||
ActDb (ActRoutine _ _) -> qsFilters
|
ActDb (ActRoutine _ _) -> qsFilters
|
||||||
_ -> qsFiltersNotRoot
|
_ -> qsFiltersNotRoot
|
||||||
|
|
||||||
addFilterToNode :: (EmbedPath, Filter) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
addFilterToNode :: (EmbedPath, Filter) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addFilterToNode =
|
addFilterToNode =
|
||||||
updateNode (\flt (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest (resolveFilter ctx{qi=fromTable} flt) lf} f)
|
updateNode (\flt (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=addFilterToLogicForest (resolveFilter ctx{qi=fromTable} flt) lf} f)
|
||||||
|
|
||||||
addOrders :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
addOrders :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addOrders ctx ApiRequest{..} rReq = foldr addOrderToNode (Right rReq) qsOrder
|
addOrders ctx ApiRequest{..} rReq =
|
||||||
|
case iAction of
|
||||||
|
ActDb (ActRelationMut _ _) -> Right rReq
|
||||||
|
_ -> foldr addOrderToNode (Right rReq) qsOrder
|
||||||
where
|
where
|
||||||
QueryParams.QueryParams{..} = iQueryParams
|
QueryParams.QueryParams{..} = iQueryParams
|
||||||
|
|
||||||
addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
addOrderToNode :: (EmbedPath, [OrderTerm]) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addOrderToNode = updateNode (\o (Node q f) -> Node q{order=resolveOrder ctx <$> o} f)
|
addOrderToNode = updateNode (\o (Node q f) -> Node q{order=resolveOrder ctx <$> o} f)
|
||||||
|
|
||||||
resolveOrder :: ResolverContext -> OrderTerm -> CoercibleOrderTerm
|
resolveOrder :: ResolverContext -> OrderTerm -> CoercibleOrderTerm
|
||||||
resolveOrder _ (OrderRelationTerm a b c d) = CoercibleOrderRelationTerm a b c d
|
resolveOrder _ (OrderRelationTerm a b c d) = CoercibleOrderRelationTerm a b c d
|
||||||
resolveOrder ctx (OrderTerm fld dir nulls) = CoercibleOrderTerm (resolveTypeOrUnknown ctx fld Nothing) dir nulls
|
resolveOrder ctx (OrderTerm fld dir nulls) = CoercibleOrderTerm (resolveTypeOrUnknown ctx fld) dir nulls
|
||||||
|
|
||||||
-- Validates that the related resource on the order is an embedded resource,
|
-- Validates that the related resource on the order is an embedded resource,
|
||||||
-- e.g. if `clients` is inside the `select` in /projects?order=clients(id)&select=*,clients(*),
|
-- e.g. if `clients` is inside the `select` in /projects?order=clients(id)&select=*,clients(*),
|
||||||
-- and if it's a to-one relationship, it adds the right alias to the OrderRelationTerm so the generated query can succeed.
|
-- and if it's a to-one relationship, it adds the right alias to the OrderRelationTerm so the generated query can succeed.
|
||||||
addRelatedOrders :: ReadPlanTree -> Either Error ReadPlanTree
|
addRelatedOrders :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
||||||
newOrder <- newRelOrder `traverse` order
|
newOrder <- newRelOrder `traverse` order
|
||||||
Node rp{order=newOrder} <$> addRelatedOrders `traverse` forest
|
Node rp{order=newOrder} <$> addRelatedOrders `traverse` forest
|
||||||
@@ -845,9 +773,9 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
name = fromMaybe relName relAlias in
|
name = fromMaybe relName relAlias in
|
||||||
if isToOne == Just True
|
if isToOne == Just True
|
||||||
then Right $ cot{coRelation=relAggAlias}
|
then Right $ cot{coRelation=relAggAlias}
|
||||||
else Left $ ApiRequestError $ RelatedOrderNotToOne (qiName from) name
|
else Left $ RelatedOrderNotToOne (qiName from) name
|
||||||
Nothing ->
|
Nothing ->
|
||||||
Left $ ApiRequestError $ NotEmbedded coRelation
|
Left $ NotEmbedded coRelation
|
||||||
|
|
||||||
-- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null`
|
-- | Searches for null filters on embeds, e.g. `projects=not.is.null` on `GET /clients?select=*,projects(*)&projects=not.is.null`
|
||||||
--
|
--
|
||||||
@@ -855,8 +783,8 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
--
|
--
|
||||||
-- Setup:
|
-- Setup:
|
||||||
--
|
--
|
||||||
-- >>> let nullOp = OpExpr True (Is IsNull)
|
-- >>> let nullOp = OpExpr True (Is TriNull)
|
||||||
-- >>> let nonNullOp = OpExpr False (Is IsNull)
|
-- >>> let nonNullOp = OpExpr False (Is TriNull)
|
||||||
-- >>> let notEqOp = OpExpr True (Op OpNotEqual "val")
|
-- >>> let notEqOp = OpExpr True (Op OpNotEqual "val")
|
||||||
-- >>> :{
|
-- >>> :{
|
||||||
-- -- this represents the `projects(*)` part on `/clients?select=*,projects(*)`
|
-- -- this represents the `projects(*)` part on `/clients?select=*,projects(*)`
|
||||||
@@ -871,7 +799,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
-- relName = "projects",
|
-- relName = "projects",
|
||||||
-- relToParent = Nothing,
|
-- relToParent = Nothing,
|
||||||
-- relJoinConds = [],
|
-- relJoinConds = [],
|
||||||
-- relAlias = Nothing, relAggAlias = "clients_projects_1", relHint = Nothing, relJoinType = Nothing, relSpread = Nothing, depth = 1,
|
-- relAlias = Nothing, relAggAlias = "clients_projects_1", relHint = Nothing, relJoinType = Nothing, relIsSpread = False, depth = 1,
|
||||||
-- relSelect = []
|
-- relSelect = []
|
||||||
-- },
|
-- },
|
||||||
-- subForest = []
|
-- subForest = []
|
||||||
@@ -891,13 +819,13 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
-- where_ = [
|
-- where_ = [
|
||||||
-- CoercibleStmnt (
|
-- CoercibleStmnt (
|
||||||
-- CoercibleFilter {
|
-- CoercibleFilter {
|
||||||
-- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson=False, cfToTsVector = Nothing, cfIRType = "", cfBaseType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False},
|
-- field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson=False, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing},
|
||||||
-- opExpr = op
|
-- opExpr = op
|
||||||
-- }
|
-- }
|
||||||
-- )
|
-- )
|
||||||
-- ],
|
-- ],
|
||||||
-- order = [], range_ = fullRange, relName = "clients", relToParent = Nothing, relJoinConds = [], relAlias = Nothing, relAggAlias = "", relHint = Nothing,
|
-- order = [], range_ = fullRange, relName = "clients", relToParent = Nothing, relJoinConds = [], relAlias = Nothing, relAggAlias = "", relHint = Nothing,
|
||||||
-- relJoinType = Nothing, relSpread = Nothing, depth = 0,
|
-- relJoinType = Nothing, relIsSpread = False, depth = 0,
|
||||||
-- relSelect = []
|
-- relSelect = []
|
||||||
-- },
|
-- },
|
||||||
-- subForest = subForst
|
-- subForest = subForst
|
||||||
@@ -907,7 +835,7 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
|
-- Don't do anything to the filter if there's no embedding (a subtree) on projects. Assume it's a normal filter.
|
||||||
--
|
--
|
||||||
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
|
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nullOp [])
|
||||||
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfToTsVector = Nothing, cfIRType = "", cfBaseType = "", cfTransform = Nothing, cfDefault = Nothing, cfFullRow = False}, opExpr = OpExpr True (Is IsNull)})]
|
-- Right [CoercibleStmnt (CoercibleFilter {field = CoercibleField {cfName = "projects", cfJsonPath = [], cfToJson = False, cfIRType = "", cfTransform = Nothing, cfDefault = Nothing}, opExpr = OpExpr True (Is TriNull)})]
|
||||||
--
|
--
|
||||||
-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.
|
-- If there's an embedding on projects, then change the filter to use the internal aggregate name (`clients_projects_1`) so the filter can succeed later.
|
||||||
--
|
--
|
||||||
@@ -916,43 +844,43 @@ addRelatedOrders (Node rp@ReadPlan{order,from} forest) = do
|
|||||||
--
|
--
|
||||||
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nonNullOp subForestPlan)
|
-- >>> ReadPlan.where_ . rootLabel <$> addNullEmbedFilters (readPlanTree nonNullOp subForestPlan)
|
||||||
-- Right [CoercibleStmnt (CoercibleFilterNullEmbed False "clients_projects_1")]
|
-- Right [CoercibleStmnt (CoercibleFilterNullEmbed False "clients_projects_1")]
|
||||||
addNullEmbedFilters :: ReadPlanTree -> Either Error ReadPlanTree
|
addNullEmbedFilters :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
|
addNullEmbedFilters (Node rp@ReadPlan{where_=curLogic} forest) = do
|
||||||
let forestReadPlans = rootLabel <$> forest
|
let forestReadPlans = rootLabel <$> forest
|
||||||
newLogic <- newNullFilters forestReadPlans `traverse` curLogic
|
newLogic <- newNullFilters forestReadPlans `traverse` curLogic
|
||||||
Node rp{ReadPlan.where_= newLogic} <$> (addNullEmbedFilters `traverse` forest)
|
Node rp{ReadPlan.where_= newLogic} <$> (addNullEmbedFilters `traverse` forest)
|
||||||
where
|
where
|
||||||
newNullFilters :: [ReadPlan] -> CoercibleLogicTree -> Either Error CoercibleLogicTree
|
newNullFilters :: [ReadPlan] -> CoercibleLogicTree -> Either ApiRequestError CoercibleLogicTree
|
||||||
newNullFilters rPlans = \case
|
newNullFilters rPlans = \case
|
||||||
(CoercibleExpr b lOp trees) ->
|
(CoercibleExpr b lOp trees) ->
|
||||||
CoercibleExpr b lOp <$> (newNullFilters rPlans `traverse` trees)
|
CoercibleExpr b lOp <$> (newNullFilters rPlans `traverse` trees)
|
||||||
flt@(CoercibleStmnt (CoercibleFilter CoercibleField{cfName=fld, cfJsonPath=[]} opExpr)) ->
|
flt@(CoercibleStmnt (CoercibleFilter (CoercibleField fld [] _ _ _ _) opExpr)) ->
|
||||||
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
|
let foundRP = find (\ReadPlan{relName, relAlias} -> fld == fromMaybe relName relAlias) rPlans in
|
||||||
case (foundRP, opExpr) of
|
case (foundRP, opExpr) of
|
||||||
(Just ReadPlan{relAggAlias}, OpExpr b (Is IsNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
|
(Just ReadPlan{relAggAlias}, OpExpr b (Is TriNull)) -> Right $ CoercibleStmnt $ CoercibleFilterNullEmbed b relAggAlias
|
||||||
_ -> Right flt
|
_ -> Right flt
|
||||||
flt@(CoercibleStmnt _) ->
|
flt@(CoercibleStmnt _) ->
|
||||||
Right flt
|
Right flt
|
||||||
|
|
||||||
addRanges :: ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
addRanges :: ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addRanges ApiRequest{..} rReq =
|
addRanges ApiRequest{..} rReq =
|
||||||
case iAction of
|
case iAction of
|
||||||
ActDb (ActRelationMut _ _) -> Right rReq
|
ActDb (ActRelationMut _ _) -> Right rReq
|
||||||
_ -> foldr addRangeToNode (Right rReq) =<< ranges
|
_ -> foldr addRangeToNode (Right rReq) =<< ranges
|
||||||
where
|
where
|
||||||
ranges :: Either Error [(EmbedPath, NonnegRange)]
|
ranges :: Either ApiRequestError [(EmbedPath, NonnegRange)]
|
||||||
ranges = first (ApiRequestError . QueryParamError) $ QueryParams.pRequestRange `traverse` HM.toList iRange
|
ranges = first QueryParamError $ QueryParams.pRequestRange `traverse` HM.toList iRange
|
||||||
|
|
||||||
addRangeToNode :: (EmbedPath, NonnegRange) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
addRangeToNode :: (EmbedPath, NonnegRange) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addRangeToNode = updateNode (\r (Node q f) -> Node q{range_=r} f)
|
addRangeToNode = updateNode (\r (Node q f) -> Node q{range_=r} f)
|
||||||
|
|
||||||
addLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either Error ReadPlanTree
|
addLogicTrees :: ResolverContext -> ApiRequest -> ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addLogicTrees ctx ApiRequest{..} rReq =
|
addLogicTrees ctx ApiRequest{..} rReq =
|
||||||
foldr addLogicTreeToNode (Right rReq) qsLogic
|
foldr addLogicTreeToNode (Right rReq) qsLogic
|
||||||
where
|
where
|
||||||
QueryParams.QueryParams{..} = iQueryParams
|
QueryParams.QueryParams{..} = iQueryParams
|
||||||
|
|
||||||
addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
addLogicTreeToNode :: (EmbedPath, LogicTree) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
addLogicTreeToNode = updateNode (\t (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=resolveLogicTree ctx{qi=fromTable} t:lf} f)
|
addLogicTreeToNode = updateNode (\t (Node q@ReadPlan{from=fromTable, where_=lf} f) -> Node q{ReadPlan.where_=resolveLogicTree ctx{qi=fromTable} t:lf} f)
|
||||||
|
|
||||||
resolveLogicTree :: ResolverContext -> LogicTree -> CoercibleLogicTree
|
resolveLogicTree :: ResolverContext -> LogicTree -> CoercibleLogicTree
|
||||||
@@ -960,15 +888,24 @@ resolveLogicTree ctx (Stmnt flt) = CoercibleStmnt $ resolveFilter ctx flt
|
|||||||
resolveLogicTree ctx (Expr b op lts) = CoercibleExpr b op (map (resolveLogicTree ctx) lts)
|
resolveLogicTree ctx (Expr b op lts) = CoercibleExpr b op (map (resolveLogicTree ctx) lts)
|
||||||
|
|
||||||
resolveFilter :: ResolverContext -> Filter -> CoercibleFilter
|
resolveFilter :: ResolverContext -> Filter -> CoercibleFilter
|
||||||
resolveFilter ctx (Filter fld opExpr) = CoercibleFilter{field=resolveQueryInputField ctx fld opExpr, opExpr=opExpr}
|
resolveFilter ctx (Filter fld opExpr) = CoercibleFilter{field=resolveQueryInputField ctx fld, opExpr=opExpr}
|
||||||
|
|
||||||
|
-- Validates that spread embeds are only done on to-one relationships
|
||||||
|
validateSpreadEmbeds :: ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
|
validateSpreadEmbeds (Node rp@ReadPlan{relToParent=Nothing} forest) = Node rp <$> validateSpreadEmbeds `traverse` forest
|
||||||
|
validateSpreadEmbeds (Node rp@ReadPlan{relIsSpread,relToParent=Just rel,relName} forest) = do
|
||||||
|
validRP <- if relIsSpread && not (relIsToOne rel)
|
||||||
|
then Left $ SpreadNotToOne (qiName $ relTable rel) relName -- TODO using relTable is not entirely right because ReadPlan might have an alias, need to store the parent alias on ReadPlan
|
||||||
|
else Right rp
|
||||||
|
Node validRP <$> validateSpreadEmbeds `traverse` forest
|
||||||
|
|
||||||
-- Find a Node of the Tree and apply a function to it
|
-- Find a Node of the Tree and apply a function to it
|
||||||
updateNode :: (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either Error ReadPlanTree -> Either Error ReadPlanTree
|
updateNode :: (a -> ReadPlanTree -> ReadPlanTree) -> (EmbedPath, a) -> Either ApiRequestError ReadPlanTree -> Either ApiRequestError ReadPlanTree
|
||||||
updateNode f ([], a) rr = f a <$> rr
|
updateNode f ([], a) rr = f a <$> rr
|
||||||
updateNode _ _ (Left e) = Left e
|
updateNode _ _ (Left e) = Left e
|
||||||
updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
|
updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
|
||||||
case findNode of
|
case findNode of
|
||||||
Nothing -> Left $ ApiRequestError $ NotEmbedded targetNodeName
|
Nothing -> Left $ NotEmbedded targetNodeName
|
||||||
Just target ->
|
Just target ->
|
||||||
(\node -> Node rootNode $ node : delete target forest) <$>
|
(\node -> Node rootNode $ node : delete target forest) <$>
|
||||||
updateNode f (remainingPath, a) (Right target)
|
updateNode f (remainingPath, a) (Right target)
|
||||||
@@ -977,12 +914,12 @@ updateNode f (targetNodeName:remainingPath, a) (Right (Node rootNode forest)) =
|
|||||||
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> relName == targetNodeName || relAlias == Just targetNodeName) forest
|
findNode = find (\(Node ReadPlan{relName, relAlias} _) -> relName == targetNodeName || relAlias == Just targetNodeName) forest
|
||||||
|
|
||||||
mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan
|
mutatePlan :: Mutation -> QualifiedIdentifier -> ApiRequest -> SchemaCache -> ReadPlanTree -> Either Error MutatePlan
|
||||||
mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq =
|
mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{dbTables, dbRepresentations} readReq = mapLeft ApiRequestError $
|
||||||
case mutation of
|
case mutation of
|
||||||
MutationCreate ->
|
MutationCreate ->
|
||||||
mapRight (\typedColumns -> Insert qi typedColumns body ((,) <$> preferResolution <*> Just confCols) [] returnings pkCols applyDefaults) typedColumnsOrError
|
mapRight (\typedColumns -> Insert qi typedColumns body ((,) <$> preferResolution <*> Just confCols) [] returnings pkCols applyDefaults) typedColumnsOrError
|
||||||
MutationUpdate ->
|
MutationUpdate ->
|
||||||
mapRight (\typedColumns -> Update qi typedColumns body combinedLogic returnings applyDefaults) typedColumnsOrError
|
mapRight (\typedColumns -> Update qi typedColumns body combinedLogic iTopLevelRange rootOrder returnings applyDefaults) typedColumnsOrError
|
||||||
MutationSingleUpsert ->
|
MutationSingleUpsert ->
|
||||||
if null qsLogic &&
|
if null qsLogic &&
|
||||||
qsFilterFields == S.fromList pkCols &&
|
qsFilterFields == S.fromList pkCols &&
|
||||||
@@ -992,8 +929,8 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
|
|||||||
_ -> False) qsFiltersRoot
|
_ -> False) qsFiltersRoot
|
||||||
then mapRight (\typedColumns -> Insert qi typedColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty False) typedColumnsOrError
|
then mapRight (\typedColumns -> Insert qi typedColumns body (Just (MergeDuplicates, pkCols)) combinedLogic returnings mempty False) typedColumnsOrError
|
||||||
else
|
else
|
||||||
Left $ ApiRequestError InvalidFilters
|
Left InvalidFilters
|
||||||
MutationDelete -> Right $ Delete qi combinedLogic returnings
|
MutationDelete -> Right $ Delete qi combinedLogic iTopLevelRange rootOrder returnings
|
||||||
where
|
where
|
||||||
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
ctx = ResolverContext dbTables dbRepresentations qi "json"
|
||||||
confCols = fromMaybe pkCols qsOnConflict
|
confCols = fromMaybe pkCols qsOnConflict
|
||||||
@@ -1001,72 +938,58 @@ mutatePlan mutation qi ApiRequest{iPreferences=Preferences{..}, ..} SchemaCache{
|
|||||||
returnings =
|
returnings =
|
||||||
if preferRepresentation == Just None || isNothing preferRepresentation
|
if preferRepresentation == Just None || isNothing preferRepresentation
|
||||||
then []
|
then []
|
||||||
else S.toList $ inferColsEmbedNeeds readReq pkCols
|
else inferColsEmbedNeeds readReq pkCols
|
||||||
-- TODO: remove fromJust by refactoring later
|
tbl = HM.lookup qi dbTables
|
||||||
-- we can use fromJust, we have already looked up the table before building mutatePlan
|
pkCols = maybe mempty tablePKCols tbl
|
||||||
tbl = fromJust $ HM.lookup qi dbTables
|
|
||||||
pkCols = maybe mempty tablePKCols (Just tbl)
|
|
||||||
logic = map (resolveLogicTree ctx . snd) qsLogic
|
logic = map (resolveLogicTree ctx . snd) qsLogic
|
||||||
|
rootOrder = resolveOrder ctx <$> maybe [] snd (find (\(x, _) -> null x) qsOrder)
|
||||||
combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot
|
combinedLogic = foldr (addFilterToLogicForest . resolveFilter ctx) logic qsFiltersRoot
|
||||||
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
|
body = payRaw <$> iPayload -- the body is assumed to be json at this stage(ApiRequest validates)
|
||||||
applyDefaults = preferMissing == Just ApplyDefaults
|
applyDefaults = preferMissing == Just ApplyDefaults
|
||||||
typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList iColumns
|
typedColumnsOrError = resolveOrError ctx tbl `traverse` S.toList iColumns
|
||||||
|
|
||||||
resolveOrError :: ResolverContext -> Table -> FieldName -> Either Error CoercibleField
|
resolveOrError :: ResolverContext -> Maybe Table -> FieldName -> Either ApiRequestError CoercibleField
|
||||||
resolveOrError ctx table field = case resolveTableFieldName table field Nothing of
|
resolveOrError _ Nothing _ = Left NotFound
|
||||||
CoercibleField{cfIRType=""} -> Left $ SchemaCacheErr $ ColumnNotFound (tableName table) field
|
resolveOrError ctx (Just table) field =
|
||||||
|
case resolveTableFieldName table field of
|
||||||
|
CoercibleField{cfIRType=""} -> Left $ ColumnNotFound (tableName table) field
|
||||||
cf -> Right $ withJsonParse ctx cf
|
cf -> Right $ withJsonParse ctx cf
|
||||||
|
|
||||||
callPlan :: Routine -> ApiRequest -> S.Set FieldName -> CallArgs -> ReadPlanTree -> CallPlan
|
callPlan :: Routine -> ApiRequest -> S.Set FieldName -> LBS.ByteString -> ReadPlanTree -> CallPlan
|
||||||
callPlan proc ApiRequest{} paramKeys args readReq = FunctionCall {
|
callPlan proc ApiRequest{iPreferences=Preferences{..}} paramKeys args readReq = FunctionCall {
|
||||||
funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc)
|
funCQi = QualifiedIdentifier (pdSchema proc) (pdName proc)
|
||||||
, funCParams = callParams
|
, funCParams = callParams
|
||||||
, funCArgs = args
|
, funCArgs = Just args
|
||||||
, funCScalar = funcReturnsScalar proc
|
, funCScalar = funcReturnsScalar proc
|
||||||
, funCSetOfScalar = funcReturnsSetOfScalar proc
|
, funCSetOfScalar = funcReturnsSetOfScalar proc
|
||||||
, funCRetCompositeAlias = funcReturnsCompositeAlias proc
|
, funCRetCompositeAlias = funcReturnsCompositeAlias proc
|
||||||
, funCFilterFields = getFilterFieldNames readReq
|
|
||||||
, funCReturning = inferColsEmbedNeeds readReq []
|
, funCReturning = inferColsEmbedNeeds readReq []
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
|
paramsAsSingleObject = preferParameters == Just SingleObject
|
||||||
specifiedParams = filter (\x -> ppName x `S.member` paramKeys)
|
specifiedParams = filter (\x -> ppName x `S.member` paramKeys)
|
||||||
callParams = case pdParams proc of
|
callParams = case pdParams proc of
|
||||||
[prm] | ppName prm == mempty -> OnePosParam prm
|
[prm] | paramsAsSingleObject -> OnePosParam prm
|
||||||
|
| ppName prm == mempty -> OnePosParam prm
|
||||||
| otherwise -> KeyParams $ specifiedParams [prm]
|
| otherwise -> KeyParams $ specifiedParams [prm]
|
||||||
prms -> KeyParams $ specifiedParams prms
|
prms -> KeyParams $ specifiedParams prms
|
||||||
|
|
||||||
-- | Get filter fields/column names from read plan
|
|
||||||
getFilterFieldNames :: ReadPlanTree -> Set FieldName
|
|
||||||
getFilterFieldNames rpt = S.fromList $ foldr (\rp names -> names <> rpToFieldNames rp) [] rpt
|
|
||||||
where
|
|
||||||
rpToFieldNames :: ReadPlan -> [FieldName]
|
|
||||||
rpToFieldNames = logicTreesToFieldName . ReadPlan.where_
|
|
||||||
|
|
||||||
logicTreesToFieldName :: [CoercibleLogicTree] -> [FieldName]
|
|
||||||
logicTreesToFieldName = concatMap coLogicTreeToFieldNames
|
|
||||||
|
|
||||||
coLogicTreeToFieldNames :: CoercibleLogicTree -> [FieldName]
|
|
||||||
coLogicTreeToFieldNames = \case
|
|
||||||
CoercibleStmnt (CoercibleFilter{field=CoercibleField{cfName}}) -> [cfName]
|
|
||||||
CoercibleStmnt (CoercibleFilterNullEmbed _ cfName) -> [cfName] -- needs test coverage
|
|
||||||
CoercibleExpr _ _ clts -> concatMap coLogicTreeToFieldNames clts
|
|
||||||
|
|
||||||
-- | Infers the columns needed for an embed to be successful after a mutation or a function call.
|
-- | Infers the columns needed for an embed to be successful after a mutation or a function call.
|
||||||
inferColsEmbedNeeds :: ReadPlanTree -> [FieldName] -> S.Set FieldName
|
inferColsEmbedNeeds :: ReadPlanTree -> [FieldName] -> [FieldName]
|
||||||
inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
||||||
-- if * is part of the select, we must not add pk or fk columns manually -
|
-- if * is part of the select, we must not add pk or fk columns manually -
|
||||||
-- otherwise those would be selected and output twice
|
-- otherwise those would be selected and output twice
|
||||||
| "*" `S.member` fldNames = S.singleton "*"
|
| "*" `elem` fldNames = ["*"]
|
||||||
| otherwise = returnings
|
| otherwise = returnings
|
||||||
where
|
where
|
||||||
fldNames = S.fromList $ cfName . csField <$> select
|
fldNames = cfName . csField <$> select
|
||||||
-- Without fkCols, when a mutatePlan to
|
-- Without fkCols, when a mutatePlan to
|
||||||
-- /projects?select=name,clients(name) occurs, the RETURNING SQL part would
|
-- /projects?select=name,clients(name) occurs, the RETURNING SQL part would
|
||||||
-- be `RETURNING name`(see QueryBuilder). This would make the embedding
|
-- be `RETURNING name`(see QueryBuilder). This would make the embedding
|
||||||
-- fail because the following JOIN would need the "client_id" column from
|
-- fail because the following JOIN would need the "client_id" column from
|
||||||
-- projects. So this adds the foreign key columns to ensure the embedding
|
-- projects. So this adds the foreign key columns to ensure the embedding
|
||||||
-- succeeds, result would be `RETURNING name, client_id`.
|
-- succeeds, result would be `RETURNING name, client_id`.
|
||||||
fkCols = S.fromList $ concat $ mapMaybe (\case
|
fkCols = concat $ mapMaybe (\case
|
||||||
Node ReadPlan{relToParent=Just Relationship{relCardinality=O2M _ cols}} _ ->
|
Node ReadPlan{relToParent=Just Relationship{relCardinality=O2M _ cols}} _ ->
|
||||||
Just $ fst <$> cols
|
Just $ fst <$> cols
|
||||||
Node ReadPlan{relToParent=Just Relationship{relCardinality=M2O _ cols}} _ ->
|
Node ReadPlan{relToParent=Just Relationship{relCardinality=M2O _ cols}} _ ->
|
||||||
@@ -1093,8 +1016,8 @@ inferColsEmbedNeeds (Node ReadPlan{select} forest) pkCols
|
|||||||
-- INSERT/POST
|
-- INSERT/POST
|
||||||
returnings =
|
returnings =
|
||||||
if not hasComputedRel
|
if not hasComputedRel
|
||||||
then fldNames <> fkCols <> S.fromList pkCols
|
then S.toList . S.fromList $ fldNames ++ fkCols ++ pkCols
|
||||||
else S.singleton "*" -- on computed relationships we cannot know the required columns for an embedding to succeed, so we just return all
|
else ["*"] -- on computed relationships we cannot know the required columns for an embedding to succeed, so we just return all
|
||||||
|
|
||||||
-- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree
|
-- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree
|
||||||
-- they are later concatenated with AND in the QueryBuilder
|
-- they are later concatenated with AND in the QueryBuilder
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
module PostgREST.Plan.CallPlan
|
module PostgREST.Plan.CallPlan
|
||||||
( CallPlan(..)
|
( CallPlan(..)
|
||||||
, CallParams(..)
|
, CallParams(..)
|
||||||
, CallArgs(..)
|
, jsonRpcParams
|
||||||
, RpcParamValue(..)
|
|
||||||
, toRpcParams
|
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
@@ -21,38 +19,25 @@ import Protolude
|
|||||||
data CallPlan = FunctionCall
|
data CallPlan = FunctionCall
|
||||||
{ funCQi :: QualifiedIdentifier
|
{ funCQi :: QualifiedIdentifier
|
||||||
, funCParams :: CallParams
|
, funCParams :: CallParams
|
||||||
, funCArgs :: CallArgs
|
, funCArgs :: Maybe LBS.ByteString
|
||||||
, funCScalar :: Bool
|
, funCScalar :: Bool
|
||||||
, funCSetOfScalar :: Bool
|
, funCSetOfScalar :: Bool
|
||||||
, funCRetCompositeAlias :: Bool
|
, funCRetCompositeAlias :: Bool
|
||||||
, funCFilterFields :: Set FieldName
|
, funCReturning :: [FieldName]
|
||||||
, funCReturning :: Set FieldName
|
|
||||||
}
|
}
|
||||||
|
|
||||||
data CallParams
|
data CallParams
|
||||||
= KeyParams [RoutineParam] -- ^ Call with key params: func(a := val1, b:= val2)
|
= KeyParams [RoutineParam] -- ^ Call with key params: func(a := val1, b:= val2)
|
||||||
| OnePosParam RoutineParam -- ^ Call with positional params(only one supported): func(val)
|
| OnePosParam RoutineParam -- ^ Call with positional params(only one supported): func(val)
|
||||||
|
|
||||||
data CallArgs
|
|
||||||
= DirectArgs (HM.HashMap Text RpcParamValue)
|
|
||||||
| JsonArgs (Maybe LBS.ByteString)
|
|
||||||
|
|
||||||
-- | RPC query param value `/rpc/func?v=<value>`, used for VARIADIC functions on form-urlencoded POST and GETs
|
|
||||||
-- | It can be fixed `?v=1` or repeated `?v=1&v=2&v=3.
|
|
||||||
data RpcParamValue = Fixed Text | Variadic [Text]
|
|
||||||
instance JSON.ToJSON RpcParamValue where
|
|
||||||
toJSON (Fixed v) = JSON.toJSON v
|
|
||||||
-- Not possible to get here anymore. Variadic arguments are only supported for
|
|
||||||
-- true variadic arguments, but the toJSON instance is only used for the "single unnamed json argument" case.
|
|
||||||
toJSON (Variadic v) = JSON.toJSON v
|
|
||||||
|
|
||||||
-- | Convert rpc params `/rpc/func?a=val1&b=val2` to json `{"a": "val1", "b": "val2"}
|
-- | Convert rpc params `/rpc/func?a=val1&b=val2` to json `{"a": "val1", "b": "val2"}
|
||||||
toRpcParams :: Routine -> [(Text, Text)] -> HM.HashMap Text RpcParamValue
|
jsonRpcParams :: Routine -> [(Text, Text)] -> LBS.ByteString
|
||||||
toRpcParams proc prms =
|
jsonRpcParams proc prms =
|
||||||
if not $ pdHasVariadic proc then -- if proc has no variadic param, save steps and directly convert to map
|
if not $ pdHasVariadic proc then -- if proc has no variadic param, save steps and directly convert to json
|
||||||
HM.fromList $ second Fixed <$> prms
|
JSON.encode $ HM.fromList $ second JSON.toJSON <$> prms
|
||||||
else
|
else
|
||||||
HM.fromListWith mergeParams $ toRpcParamValue proc <$> prms
|
let paramsMap = HM.fromListWith mergeParams $ toRpcParamValue proc <$> prms in
|
||||||
|
JSON.encode paramsMap
|
||||||
where
|
where
|
||||||
mergeParams :: RpcParamValue -> RpcParamValue -> RpcParamValue
|
mergeParams :: RpcParamValue -> RpcParamValue -> RpcParamValue
|
||||||
mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a
|
mergeParams (Variadic a) (Variadic b) = Variadic $ b ++ a
|
||||||
@@ -63,3 +48,10 @@ toRpcParamValue proc (k, v) | prmIsVariadic k = (k, Variadic [v])
|
|||||||
| otherwise = (k, Fixed v)
|
| otherwise = (k, Fixed v)
|
||||||
where
|
where
|
||||||
prmIsVariadic prm = isJust $ find (\RoutineParam{ppName, ppVar} -> ppName == prm && ppVar) $ pdParams proc
|
prmIsVariadic prm = isJust $ find (\RoutineParam{ppName, ppVar} -> ppName == prm && ppVar) $ pdParams proc
|
||||||
|
|
||||||
|
-- | RPC query param value `/rpc/func?v=<value>`, used for VARIADIC functions on form-urlencoded POST and GETs
|
||||||
|
-- | It can be fixed `?v=1` or repeated `?v=1&v=2&v=3.
|
||||||
|
data RpcParamValue = Fixed Text | Variadic [Text]
|
||||||
|
instance JSON.ToJSON RpcParamValue where
|
||||||
|
toJSON (Fixed v) = JSON.toJSON v
|
||||||
|
toJSON (Variadic v) = JSON.toJSON v
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import qualified Data.ByteString.Lazy as LBS
|
|||||||
|
|
||||||
import PostgREST.ApiRequest.Preferences (PreferResolution)
|
import PostgREST.ApiRequest.Preferences (PreferResolution)
|
||||||
import PostgREST.Plan.Types (CoercibleField,
|
import PostgREST.Plan.Types (CoercibleField,
|
||||||
CoercibleLogicTree)
|
CoercibleLogicTree,
|
||||||
|
CoercibleOrderTerm)
|
||||||
|
import PostgREST.RangeQuery (NonnegRange)
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier)
|
QualifiedIdentifier)
|
||||||
|
|
||||||
@@ -30,11 +32,15 @@ data MutatePlan
|
|||||||
, updCols :: [CoercibleField]
|
, updCols :: [CoercibleField]
|
||||||
, updBody :: Maybe LBS.ByteString
|
, updBody :: Maybe LBS.ByteString
|
||||||
, where_ :: [CoercibleLogicTree]
|
, where_ :: [CoercibleLogicTree]
|
||||||
|
, mutRange :: NonnegRange
|
||||||
|
, mutOrder :: [CoercibleOrderTerm]
|
||||||
, returning :: [FieldName]
|
, returning :: [FieldName]
|
||||||
, applyDefs :: Bool
|
, applyDefs :: Bool
|
||||||
}
|
}
|
||||||
| Delete
|
| Delete
|
||||||
{ in_ :: QualifiedIdentifier
|
{ in_ :: QualifiedIdentifier
|
||||||
, where_ :: [CoercibleLogicTree]
|
, where_ :: [CoercibleLogicTree]
|
||||||
|
, mutRange :: NonnegRange
|
||||||
|
, mutOrder :: [CoercibleOrderTerm]
|
||||||
, returning :: [FieldName]
|
, returning :: [FieldName]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ module PostgREST.Plan.ReadPlan
|
|||||||
( ReadPlanTree
|
( ReadPlanTree
|
||||||
, ReadPlan(..)
|
, ReadPlan(..)
|
||||||
, JoinCondition(..)
|
, JoinCondition(..)
|
||||||
, SpreadType(..)
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Data.Tree (Tree (..))
|
import Data.Tree (Tree (..))
|
||||||
@@ -12,8 +11,7 @@ import PostgREST.ApiRequest.Types (Alias, Depth, Hint,
|
|||||||
import PostgREST.Plan.Types (CoercibleLogicTree,
|
import PostgREST.Plan.Types (CoercibleLogicTree,
|
||||||
CoercibleOrderTerm,
|
CoercibleOrderTerm,
|
||||||
CoercibleSelectField (..),
|
CoercibleSelectField (..),
|
||||||
RelSelectField (..),
|
RelSelectField (..))
|
||||||
SpreadType (..))
|
|
||||||
import PostgREST.RangeQuery (NonnegRange)
|
import PostgREST.RangeQuery (NonnegRange)
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName,
|
import PostgREST.SchemaCache.Identifiers (FieldName,
|
||||||
QualifiedIdentifier)
|
QualifiedIdentifier)
|
||||||
@@ -30,7 +28,6 @@ data JoinCondition =
|
|||||||
(QualifiedIdentifier, FieldName)
|
(QualifiedIdentifier, FieldName)
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
-- TODO: Enforce uniqueness of columns by changing to a Set instead of a List where applicable
|
|
||||||
data ReadPlan = ReadPlan
|
data ReadPlan = ReadPlan
|
||||||
{ select :: [CoercibleSelectField]
|
{ select :: [CoercibleSelectField]
|
||||||
, from :: QualifiedIdentifier
|
, from :: QualifiedIdentifier
|
||||||
@@ -45,7 +42,7 @@ data ReadPlan = ReadPlan
|
|||||||
, relAggAlias :: Alias
|
, relAggAlias :: Alias
|
||||||
, relHint :: Maybe Hint
|
, relHint :: Maybe Hint
|
||||||
, relJoinType :: Maybe JoinType
|
, relJoinType :: Maybe JoinType
|
||||||
, relSpread :: Maybe SpreadType
|
, relIsSpread :: Bool
|
||||||
, relSelect :: [RelSelectField]
|
, relSelect :: [RelSelectField]
|
||||||
, depth :: Depth
|
, depth :: Depth
|
||||||
-- ^ used for aliasing
|
-- ^ used for aliasing
|
||||||
|
|||||||
@@ -5,18 +5,15 @@ module PostgREST.Plan.Types
|
|||||||
, CoercibleLogicTree(..)
|
, CoercibleLogicTree(..)
|
||||||
, CoercibleFilter(..)
|
, CoercibleFilter(..)
|
||||||
, TransformerProc
|
, TransformerProc
|
||||||
, ToTsVector(..)
|
|
||||||
, CoercibleOrderTerm(..)
|
, CoercibleOrderTerm(..)
|
||||||
, RelSelectField(..)
|
, RelSelectField(..)
|
||||||
, RelJsonEmbedMode(..)
|
, RelJsonEmbedMode(..)
|
||||||
, SpreadSelectField(..)
|
, SpreadSelectField(..)
|
||||||
, SpreadType(..)
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import PostgREST.ApiRequest.Types (AggregateFunction, Alias, Cast,
|
import PostgREST.ApiRequest.Types (AggregateFunction, Alias, Cast,
|
||||||
Field, JsonPath, Language,
|
Field, JsonPath, LogicOperator,
|
||||||
LogicOperator, OpExpr,
|
OpExpr, OrderDirection, OrderNulls)
|
||||||
OrderDirection, OrderNulls)
|
|
||||||
|
|
||||||
import PostgREST.SchemaCache.Identifiers (FieldName)
|
import PostgREST.SchemaCache.Identifiers (FieldName)
|
||||||
|
|
||||||
@@ -24,9 +21,6 @@ import Protolude
|
|||||||
|
|
||||||
type TransformerProc = Text
|
type TransformerProc = Text
|
||||||
|
|
||||||
newtype ToTsVector = ToTsVector (Maybe Language)
|
|
||||||
deriving (Eq, Show)
|
|
||||||
|
|
||||||
-- | A CoercibleField pairs the name of a query element with any type coercion information we need for some specific use case.
|
-- | A CoercibleField pairs the name of a query element with any type coercion information we need for some specific use case.
|
||||||
-- |
|
-- |
|
||||||
-- | As suggested by the name, it's often a reference to a field in a table but really it can be any nameable element (function parameter, calculation with an alias, etc) with a knowable type.
|
-- | As suggested by the name, it's often a reference to a field in a table but really it can be any nameable element (function parameter, calculation with an alias, etc) with a knowable type.
|
||||||
@@ -39,19 +33,16 @@ newtype ToTsVector = ToTsVector (Maybe Language)
|
|||||||
-- |
|
-- |
|
||||||
-- | The type value is allowed to be the empty string. The analog here is soft type checking in programming languages: sometimes we don't need a variable to have a specified type and things will work anyhow. So the empty type variant is valid when we don't know and *don't need to know* about the specific type in some context. Note that this variation should not be used if it guarantees failure: in that case you should instead raise an error at the planning stage and bail out. For example, we can't parse JSON with `json_to_recordset` without knowing the types of each recipient field, and so error out. Using the empty string for the type would be incorrect and futile. On the other hand we use the empty type for RPC calls since type resolution isn't implemented for RPC, but it's fine because the query still works with Postgres' implicit coercion. In the future, hopefully we will support data representations across the board and then the empty type may be permanently retired.
|
-- | The type value is allowed to be the empty string. The analog here is soft type checking in programming languages: sometimes we don't need a variable to have a specified type and things will work anyhow. So the empty type variant is valid when we don't know and *don't need to know* about the specific type in some context. Note that this variation should not be used if it guarantees failure: in that case you should instead raise an error at the planning stage and bail out. For example, we can't parse JSON with `json_to_recordset` without knowing the types of each recipient field, and so error out. Using the empty string for the type would be incorrect and futile. On the other hand we use the empty type for RPC calls since type resolution isn't implemented for RPC, but it's fine because the query still works with Postgres' implicit coercion. In the future, hopefully we will support data representations across the board and then the empty type may be permanently retired.
|
||||||
data CoercibleField = CoercibleField
|
data CoercibleField = CoercibleField
|
||||||
{ cfName :: FieldName
|
{ cfName :: FieldName
|
||||||
, cfJsonPath :: JsonPath
|
, cfJsonPath :: JsonPath
|
||||||
, cfToJson :: Bool
|
, cfToJson :: Bool
|
||||||
, cfToTsVector :: Maybe ToTsVector -- ^ If the field should be converted using to_tsvector(<language>, <field>)
|
, cfIRType :: Text -- ^ The native Postgres type of the field, the intermediate (IR) type before mapping.
|
||||||
, cfIRType :: Text -- ^ The native Postgres type of the field, the intermediate (IR) type before mapping.
|
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
||||||
, cfBaseType :: Text -- ^ The base type of the field in case of domains, or just the type otherwise (without modifiers in case of pg_catalog types)
|
, cfDefault :: Maybe Text
|
||||||
, cfTransform :: Maybe TransformerProc -- ^ The optional mapping from irType -> targetType.
|
|
||||||
, cfDefault :: Maybe Text
|
|
||||||
, cfFullRow :: Bool -- ^ True if the field represents the whole selected row. Used in spread rels: instead of COUNT(*), it does a COUNT(<row>) in order to not mix with other spreaded resources.
|
|
||||||
} deriving (Eq, Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
unknownField :: FieldName -> JsonPath -> CoercibleField
|
unknownField :: FieldName -> JsonPath -> CoercibleField
|
||||||
unknownField name path = CoercibleField name path False Nothing "" "" Nothing Nothing False
|
unknownField name path = CoercibleField name path False "" Nothing Nothing
|
||||||
|
|
||||||
-- | Like an API request LogicTree, but with coercible field information.
|
-- | Like an API request LogicTree, but with coercible field information.
|
||||||
data CoercibleLogicTree
|
data CoercibleLogicTree
|
||||||
@@ -113,11 +104,3 @@ data SpreadSelectField =
|
|||||||
, ssSelAlias :: Maybe Alias
|
, ssSelAlias :: Maybe Alias
|
||||||
}
|
}
|
||||||
deriving (Eq, Show)
|
deriving (Eq, Show)
|
||||||
|
|
||||||
data SpreadType
|
|
||||||
= ToOneSpread
|
|
||||||
| ToManySpread
|
|
||||||
{ stExtraSelect :: [(Maybe FieldName, CoercibleSelectField)]
|
|
||||||
, stOrder :: [CoercibleOrderTerm]
|
|
||||||
}
|
|
||||||
deriving (Eq, Show)
|
|
||||||
|
|||||||
@@ -2,31 +2,32 @@
|
|||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
-- TODO: This module shouldn't depend on SchemaCache
|
-- TODO: This module shouldn't depend on SchemaCache
|
||||||
module PostgREST.Query
|
module PostgREST.Query
|
||||||
( Query (..)
|
( QueryResult (..)
|
||||||
, QueryResult (..)
|
, runQuery
|
||||||
, query
|
|
||||||
, getSQLQuery
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import Control.Monad.Except (liftEither)
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.Aeson.KeyMap as KM
|
import qualified Data.Aeson.KeyMap as KM
|
||||||
import qualified Data.ByteString as BS
|
import qualified Data.ByteString as BS
|
||||||
import qualified Data.ByteString.Lazy.Char8 as LBS
|
import qualified Data.ByteString.Lazy.Char8 as LBS
|
||||||
|
import Data.Either.Combinators (mapLeft)
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
import qualified Hasql.DynamicStatements.Snippet as SQL (Snippet)
|
import qualified Hasql.DynamicStatements.Snippet as SQL (Snippet)
|
||||||
import qualified Hasql.DynamicStatements.Statement as SQL
|
import qualified Hasql.DynamicStatements.Statement as SQL
|
||||||
import qualified Hasql.Session as SQL (Session)
|
|
||||||
import qualified Hasql.Transaction as SQL
|
import qualified Hasql.Transaction as SQL
|
||||||
import qualified Hasql.Transaction.Sessions as SQL
|
import qualified Hasql.Transaction.Sessions as SQL
|
||||||
|
|
||||||
|
import qualified PostgREST.ApiRequest.Types as ApiRequestTypes
|
||||||
|
import qualified PostgREST.AppState as AppState
|
||||||
import qualified PostgREST.Error as Error
|
import qualified PostgREST.Error as Error
|
||||||
import qualified PostgREST.Query.QueryBuilder as QueryBuilder
|
import qualified PostgREST.Query.QueryBuilder as QueryBuilder
|
||||||
import qualified PostgREST.Query.Statements as Statements
|
import qualified PostgREST.Query.Statements as Statements
|
||||||
|
import qualified PostgREST.RangeQuery as RangeQuery
|
||||||
import qualified PostgREST.SchemaCache as SchemaCache
|
import qualified PostgREST.SchemaCache as SchemaCache
|
||||||
|
|
||||||
|
|
||||||
import PostgREST.ApiRequest (ApiRequest (..),
|
import PostgREST.ApiRequest (ApiRequest (..),
|
||||||
Mutation (..))
|
Mutation (..))
|
||||||
import PostgREST.ApiRequest.Preferences (PreferCount (..),
|
import PostgREST.ApiRequest.Preferences (PreferCount (..),
|
||||||
@@ -36,7 +37,7 @@ import PostgREST.ApiRequest.Preferences (PreferCount (..),
|
|||||||
PreferTransaction (..),
|
PreferTransaction (..),
|
||||||
Preferences (..),
|
Preferences (..),
|
||||||
shouldCount)
|
shouldCount)
|
||||||
import PostgREST.Auth.Types (AuthResult (..))
|
import PostgREST.Auth (AuthResult (..))
|
||||||
import PostgREST.Config (AppConfig (..),
|
import PostgREST.Config (AppConfig (..),
|
||||||
OpenAPIMode (..))
|
OpenAPIMode (..))
|
||||||
import PostgREST.Config.PgVersion (PgVersion (..))
|
import PostgREST.Config.PgVersion (PgVersion (..))
|
||||||
@@ -49,6 +50,7 @@ import PostgREST.Plan (ActionPlan (..),
|
|||||||
InfoPlan (..),
|
InfoPlan (..),
|
||||||
InspectPlan (..))
|
InspectPlan (..))
|
||||||
import PostgREST.Plan.MutatePlan (MutatePlan (..))
|
import PostgREST.Plan.MutatePlan (MutatePlan (..))
|
||||||
|
import PostgREST.Plan.ReadPlan (ReadPlanTree)
|
||||||
import PostgREST.Query.SqlFragment (escapeIdentList, fromQi,
|
import PostgREST.Query.SqlFragment (escapeIdentList, fromQi,
|
||||||
intercalateSnippet,
|
intercalateSnippet,
|
||||||
setConfigWithConstantName,
|
setConfigWithConstantName,
|
||||||
@@ -57,43 +59,41 @@ import PostgREST.Query.SqlFragment (escapeIdentList, fromQi,
|
|||||||
import PostgREST.Query.Statements (ResultSet (..))
|
import PostgREST.Query.Statements (ResultSet (..))
|
||||||
import PostgREST.SchemaCache (SchemaCache (..))
|
import PostgREST.SchemaCache (SchemaCache (..))
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
||||||
import PostgREST.SchemaCache.Routine (Routine (..), RoutineMap)
|
import PostgREST.SchemaCache.Routine (MediaHandler, Routine (..),
|
||||||
|
RoutineMap)
|
||||||
import PostgREST.SchemaCache.Table (TablesMap)
|
import PostgREST.SchemaCache.Table (TablesMap)
|
||||||
|
|
||||||
import Protolude hiding (Handler)
|
import Protolude hiding (Handler)
|
||||||
|
|
||||||
type DbHandler = ExceptT Error SQL.Transaction
|
type DbHandler = ExceptT Error SQL.Transaction
|
||||||
|
|
||||||
data Query
|
|
||||||
= DbQuery {
|
|
||||||
dqIsoLevel :: SQL.IsolationLevel
|
|
||||||
, dqTxMode :: SQL.Mode
|
|
||||||
, dqDbHandler :: DbHandler QueryResult
|
|
||||||
, dqTransaction :: SQL.IsolationLevel -> SQL.Mode -> SQL.Transaction (Either Error QueryResult) -> SQL.Session (Either Error QueryResult)
|
|
||||||
, dqSQL :: ByteString
|
|
||||||
}
|
|
||||||
| NoDbQuery QueryResult
|
|
||||||
|
|
||||||
data QueryResult
|
data QueryResult
|
||||||
= DbCrudResult CrudPlan ResultSet
|
= DbCrudResult CrudPlan ResultSet
|
||||||
| DbCallResult CallReadPlan ResultSet
|
| DbCallResult CallReadPlan ResultSet
|
||||||
| MaybeDbResult InspectPlan (Maybe (TablesMap, RoutineMap, Maybe Text))
|
| MaybeDbResult InspectPlan (Maybe (TablesMap, RoutineMap, Maybe Text))
|
||||||
| NoDbResult InfoPlan
|
| NoDbResult InfoPlan
|
||||||
|
|
||||||
query :: AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> PgVersion -> Query
|
-- TODO This function needs to be free from IO, only App.hs should do IO
|
||||||
query _ _ _ (NoDb x) _ _ = NoDbQuery $ NoDbResult x
|
runQuery :: AppState.AppState -> AppConfig -> AuthResult -> ApiRequest -> ActionPlan -> SchemaCache -> PgVersion -> Bool -> ExceptT Error IO QueryResult
|
||||||
query config AuthResult{..} apiReq (Db plan) sCache pgVer =
|
runQuery _ _ _ _ (NoDb x) _ _ _ = pure $ NoDbResult x
|
||||||
DbQuery isoLvl txMode dbHandler transaction mainSQLQuery
|
runQuery appState config AuthResult{..} apiReq (Db plan) sCache pgVer authenticated = do
|
||||||
|
dbResp <- lift $ do
|
||||||
|
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction
|
||||||
|
AppState.usePool appState (transaction isoLvl txMode $ runExceptT dbHandler)
|
||||||
|
|
||||||
|
resp <-
|
||||||
|
liftEither . mapLeft Error.PgErr $
|
||||||
|
mapLeft (Error.PgError authenticated) dbResp
|
||||||
|
|
||||||
|
liftEither resp
|
||||||
where
|
where
|
||||||
transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction
|
|
||||||
prepared = configDbPreparedStatements config
|
prepared = configDbPreparedStatements config
|
||||||
isoLvl = planIsoLvl config authRole plan
|
isoLvl = planIsoLvl config authRole plan
|
||||||
txMode = planTxMode plan
|
txMode = planTxMode plan
|
||||||
(mainActionQuery, mainSQLQuery) = actionQuery plan config apiReq pgVer sCache
|
|
||||||
dbHandler = do
|
dbHandler = do
|
||||||
setPgLocals plan config authClaims authRole apiReq
|
setPgLocals plan config authClaims authRole apiReq
|
||||||
runPreReq config
|
runPreReq config
|
||||||
mainActionQuery
|
actionQuery plan config apiReq pgVer sCache
|
||||||
|
|
||||||
planTxMode :: DbActionPlan -> SQL.Mode
|
planTxMode :: DbActionPlan -> SQL.Mode
|
||||||
planTxMode (DbCrud x) = pTxMode x
|
planTxMode (DbCrud x) = pTxMode x
|
||||||
@@ -107,99 +107,107 @@ planIsoLvl AppConfig{configRoleIsoLvl} role actPlan = case actPlan of
|
|||||||
where
|
where
|
||||||
roleIsoLvl = HM.findWithDefault SQL.ReadCommitted role configRoleIsoLvl
|
roleIsoLvl = HM.findWithDefault SQL.ReadCommitted role configRoleIsoLvl
|
||||||
|
|
||||||
-- TODO: Generate the Hasql Statement in a diferent module after the OpenAPI functionality is removed
|
actionQuery :: DbActionPlan -> AppConfig -> ApiRequest -> PgVersion -> SchemaCache -> DbHandler QueryResult
|
||||||
actionQuery :: DbActionPlan -> AppConfig -> ApiRequest -> PgVersion -> SchemaCache -> (DbHandler QueryResult, ByteString)
|
|
||||||
actionQuery (DbCrud plan@WrappedReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ _ =
|
|
||||||
(mainActionQuery, mainSQLQuery)
|
|
||||||
where
|
|
||||||
countQuery = QueryBuilder.readPlanToCountQuery wrReadPlan
|
|
||||||
(result, mainSQLQuery) = Statements.prepareRead
|
|
||||||
(QueryBuilder.readPlanToQuery wrReadPlan)
|
|
||||||
(if preferCount == Just EstimatedCount then
|
|
||||||
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
|
||||||
QueryBuilder.limitedQuery countQuery ((+ 1) <$> configDbMaxRows)
|
|
||||||
else
|
|
||||||
countQuery
|
|
||||||
)
|
|
||||||
(shouldCount preferCount)
|
|
||||||
wrMedia
|
|
||||||
wrHandler
|
|
||||||
configDbPreparedStatements
|
|
||||||
mainActionQuery = do
|
|
||||||
resultSet <- lift $ SQL.statement mempty result
|
|
||||||
failNotSingular wrMedia resultSet
|
|
||||||
optionalRollback conf apiReq
|
|
||||||
DbCrudResult plan <$> resultSetWTotal conf apiReq resultSet countQuery
|
|
||||||
|
|
||||||
actionQuery (DbCrud plan@MutateReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ _ =
|
actionQuery (DbCrud plan@WrappedReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} _ _ = do
|
||||||
(mainActionQuery, mainSQLQuery)
|
let countQuery = QueryBuilder.readPlanToCountQuery wrReadPlan
|
||||||
where
|
resultSet <-
|
||||||
(isPut, isInsert, pkCols) = case mrMutatePlan of {Insert{where_,insPkCols} -> ((not . null) where_, True, insPkCols); _ -> (False,False, mempty);}
|
lift . SQL.statement mempty $
|
||||||
(result, mainSQLQuery) = Statements.prepareWrite
|
Statements.prepareRead
|
||||||
(QueryBuilder.readPlanToQuery mrReadPlan)
|
(QueryBuilder.readPlanToQuery wrReadPlan)
|
||||||
(QueryBuilder.mutatePlanToQuery mrMutatePlan)
|
(if preferCount == Just EstimatedCount then
|
||||||
|
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
||||||
|
QueryBuilder.limitedQuery countQuery ((+ 1) <$> configDbMaxRows)
|
||||||
|
else
|
||||||
|
countQuery
|
||||||
|
)
|
||||||
|
(shouldCount preferCount)
|
||||||
|
wrMedia
|
||||||
|
wrHandler
|
||||||
|
configDbPreparedStatements
|
||||||
|
failNotSingular wrMedia resultSet
|
||||||
|
optionalRollback conf apiReq
|
||||||
|
DbCrudResult plan <$> resultSetWTotal conf apiReq resultSet countQuery
|
||||||
|
|
||||||
|
actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationCreate, ..}) conf apiReq _ _ = do
|
||||||
|
resultSet <- writeQuery mrReadPlan mrMutatePlan mrMedia mrHandler apiReq conf
|
||||||
|
failNotSingular mrMedia resultSet
|
||||||
|
optionalRollback conf apiReq
|
||||||
|
pure $ DbCrudResult plan resultSet
|
||||||
|
|
||||||
|
actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationUpdate, ..}) conf apiReq@ApiRequest{iPreferences=Preferences{..}, ..} _ _ = do
|
||||||
|
resultSet <- writeQuery mrReadPlan mrMutatePlan mrMedia mrHandler apiReq conf
|
||||||
|
failNotSingular mrMedia resultSet
|
||||||
|
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
|
||||||
|
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
|
||||||
|
optionalRollback conf apiReq
|
||||||
|
pure $ DbCrudResult plan resultSet
|
||||||
|
|
||||||
|
actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationSingleUpsert, ..}) conf apiReq _ _ = do
|
||||||
|
resultSet <- writeQuery mrReadPlan mrMutatePlan mrMedia mrHandler apiReq conf
|
||||||
|
failPut resultSet
|
||||||
|
optionalRollback conf apiReq
|
||||||
|
pure $ DbCrudResult plan resultSet
|
||||||
|
|
||||||
|
actionQuery (DbCrud plan@MutateReadPlan{mrMutation=MutationDelete, ..}) conf apiReq@ApiRequest{iPreferences=Preferences{..}, ..} _ _ = do
|
||||||
|
resultSet <- writeQuery mrReadPlan mrMutatePlan mrMedia mrHandler apiReq conf
|
||||||
|
failNotSingular mrMedia resultSet
|
||||||
|
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
|
||||||
|
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
|
||||||
|
optionalRollback conf apiReq
|
||||||
|
pure $ DbCrudResult plan resultSet
|
||||||
|
|
||||||
|
actionQuery (DbCall plan@CallReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} pgVer _ = do
|
||||||
|
resultSet <-
|
||||||
|
lift . SQL.statement mempty $
|
||||||
|
Statements.prepareCall
|
||||||
|
crProc
|
||||||
|
(QueryBuilder.callPlanToQuery crCallPlan pgVer)
|
||||||
|
(QueryBuilder.readPlanToQuery crReadPlan)
|
||||||
|
(QueryBuilder.readPlanToCountQuery crReadPlan)
|
||||||
|
(shouldCount preferCount)
|
||||||
|
crMedia
|
||||||
|
crHandler
|
||||||
|
configDbPreparedStatements
|
||||||
|
|
||||||
|
optionalRollback conf apiReq
|
||||||
|
failNotSingular crMedia resultSet
|
||||||
|
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
|
||||||
|
pure $ DbCallResult plan resultSet
|
||||||
|
|
||||||
|
actionQuery (MaybeDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ pgVer sCache =
|
||||||
|
lift $ case configOpenApiMode of
|
||||||
|
OAFollowPriv -> do
|
||||||
|
tableAccess <- SQL.statement [tSchema] (SchemaCache.accessibleTables pgVer configDbPreparedStatements)
|
||||||
|
MaybeDbResult plan . Just <$> ((,,)
|
||||||
|
(HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache)
|
||||||
|
<$> SQL.statement (tSchema, configDbHoistedTxSettings) (SchemaCache.accessibleFuncs pgVer configDbPreparedStatements)
|
||||||
|
<*> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
|
||||||
|
OAIgnorePriv ->
|
||||||
|
MaybeDbResult plan . Just <$> ((,,)
|
||||||
|
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbTables sCache)
|
||||||
|
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbRoutines sCache)
|
||||||
|
<$> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
|
||||||
|
OADisabled ->
|
||||||
|
pure $ MaybeDbResult plan Nothing
|
||||||
|
|
||||||
|
writeQuery :: ReadPlanTree -> MutatePlan -> MediaType -> MediaHandler -> ApiRequest -> AppConfig -> DbHandler ResultSet
|
||||||
|
writeQuery readPlan mutatePlan mType mHandler ApiRequest{iPreferences=Preferences{..}} conf =
|
||||||
|
let
|
||||||
|
(isPut, isInsert, pkCols) = case mutatePlan of {Insert{where_,insPkCols} -> ((not . null) where_, True, insPkCols); _ -> (False,False, mempty);}
|
||||||
|
in
|
||||||
|
lift . SQL.statement mempty $
|
||||||
|
Statements.prepareWrite
|
||||||
|
(QueryBuilder.readPlanToQuery readPlan)
|
||||||
|
(QueryBuilder.mutatePlanToQuery mutatePlan)
|
||||||
isInsert
|
isInsert
|
||||||
isPut
|
isPut
|
||||||
mrMedia
|
mType
|
||||||
mrHandler
|
mHandler
|
||||||
preferRepresentation
|
preferRepresentation
|
||||||
preferResolution
|
preferResolution
|
||||||
pkCols
|
pkCols
|
||||||
configDbPreparedStatements
|
(configDbPreparedStatements conf)
|
||||||
failMutation resultSet = case mrMutation of
|
|
||||||
MutationCreate -> do
|
|
||||||
failNotSingular mrMedia resultSet
|
|
||||||
MutationUpdate -> do
|
|
||||||
failNotSingular mrMedia resultSet
|
|
||||||
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
|
|
||||||
MutationSingleUpsert -> do
|
|
||||||
failPut resultSet
|
|
||||||
MutationDelete -> do
|
|
||||||
failNotSingular mrMedia resultSet
|
|
||||||
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
|
|
||||||
mainActionQuery = do
|
|
||||||
resultSet <- lift $ SQL.statement mempty result
|
|
||||||
failMutation resultSet
|
|
||||||
optionalRollback conf apiReq
|
|
||||||
pure $ DbCrudResult plan resultSet
|
|
||||||
|
|
||||||
actionQuery (DbCall plan@CallReadPlan{..}) conf@AppConfig{..} apiReq@ApiRequest{iPreferences=Preferences{..}} pgVer _ =
|
|
||||||
(mainActionQuery, mainSQLQuery)
|
|
||||||
where
|
|
||||||
(result, mainSQLQuery) = Statements.prepareCall
|
|
||||||
crProc
|
|
||||||
(QueryBuilder.callPlanToQuery crCallPlan pgVer)
|
|
||||||
(QueryBuilder.readPlanToQuery crReadPlan)
|
|
||||||
(QueryBuilder.readPlanToCountQuery crReadPlan)
|
|
||||||
(shouldCount preferCount)
|
|
||||||
crMedia
|
|
||||||
crHandler
|
|
||||||
configDbPreparedStatements
|
|
||||||
mainActionQuery = do
|
|
||||||
resultSet <- lift $ SQL.statement mempty result
|
|
||||||
optionalRollback conf apiReq
|
|
||||||
failNotSingular crMedia resultSet
|
|
||||||
failExceedsMaxAffectedPref (preferMaxAffected,preferHandling) resultSet
|
|
||||||
pure $ DbCallResult plan resultSet
|
|
||||||
|
|
||||||
actionQuery (MaybeDb plan@InspectPlan{ipSchema=tSchema}) AppConfig{..} _ _ sCache =
|
|
||||||
(mainActionQuery, mempty)
|
|
||||||
where
|
|
||||||
mainActionQuery = lift $
|
|
||||||
case configOpenApiMode of
|
|
||||||
OAFollowPriv -> do
|
|
||||||
tableAccess <- SQL.statement [tSchema] (SchemaCache.accessibleTables configDbPreparedStatements)
|
|
||||||
MaybeDbResult plan . Just <$> ((,,)
|
|
||||||
(HM.filterWithKey (\qi _ -> S.member qi tableAccess) $ SchemaCache.dbTables sCache)
|
|
||||||
<$> SQL.statement ([tSchema], configDbHoistedTxSettings) (SchemaCache.accessibleFuncs configDbPreparedStatements)
|
|
||||||
<*> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
|
|
||||||
OAIgnorePriv ->
|
|
||||||
MaybeDbResult plan . Just <$> ((,,)
|
|
||||||
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbTables sCache)
|
|
||||||
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ SchemaCache.dbRoutines sCache)
|
|
||||||
<$> SQL.statement tSchema (SchemaCache.schemaDescription configDbPreparedStatements))
|
|
||||||
OADisabled ->
|
|
||||||
pure $ MaybeDbResult plan Nothing
|
|
||||||
|
|
||||||
-- Makes sure the querystring pk matches the payload pk
|
-- Makes sure the querystring pk matches the payload pk
|
||||||
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted,
|
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted,
|
||||||
@@ -211,7 +219,7 @@ failPut RSPlan{} = pure ()
|
|||||||
failPut RSStandard{rsQueryTotal=queryTotal} =
|
failPut RSStandard{rsQueryTotal=queryTotal} =
|
||||||
when (queryTotal /= 1) $ do
|
when (queryTotal /= 1) $ do
|
||||||
lift SQL.condemn
|
lift SQL.condemn
|
||||||
throwError $ Error.ApiRequestError Error.PutMatchingPkError
|
throwError $ Error.ApiRequestError ApiRequestTypes.PutMatchingPkError
|
||||||
|
|
||||||
resultSetWTotal :: AppConfig -> ApiRequest -> ResultSet -> SQL.Snippet -> DbHandler ResultSet
|
resultSetWTotal :: AppConfig -> ApiRequest -> ResultSet -> SQL.Snippet -> DbHandler ResultSet
|
||||||
resultSetWTotal _ _ rs@RSPlan{} _ = return rs
|
resultSetWTotal _ _ rs@RSPlan{} _ = return rs
|
||||||
@@ -243,14 +251,22 @@ failNotSingular _ RSPlan{} = pure ()
|
|||||||
failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =
|
failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =
|
||||||
when (elem mediaType [MTVndSingularJSON True, MTVndSingularJSON False] && queryTotal /= 1) $ do
|
when (elem mediaType [MTVndSingularJSON True, MTVndSingularJSON False] && queryTotal /= 1) $ do
|
||||||
lift SQL.condemn
|
lift SQL.condemn
|
||||||
throwError $ Error.ApiRequestError . Error.SingularityError $ toInteger queryTotal
|
throwError $ Error.ApiRequestError . ApiRequestTypes.SingularityError $ toInteger queryTotal
|
||||||
|
|
||||||
failExceedsMaxAffectedPref :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> ResultSet -> DbHandler ()
|
failExceedsMaxAffectedPref :: (Maybe PreferMaxAffected, Maybe PreferHandling) -> ResultSet -> DbHandler ()
|
||||||
failExceedsMaxAffectedPref (Nothing,_) _ = pure ()
|
failExceedsMaxAffectedPref (Nothing,_) _ = pure ()
|
||||||
failExceedsMaxAffectedPref _ RSPlan{} = pure ()
|
failExceedsMaxAffectedPref _ RSPlan{} = pure ()
|
||||||
failExceedsMaxAffectedPref (Just (PreferMaxAffected n), handling) RSStandard{rsQueryTotal=queryTotal} = when ((queryTotal > n) && (handling == Just Strict)) $ do
|
failExceedsMaxAffectedPref (Just (PreferMaxAffected n), handling) RSStandard{rsQueryTotal=queryTotal} = when ((queryTotal > n) && (handling == Just Strict)) $ do
|
||||||
lift SQL.condemn
|
lift SQL.condemn
|
||||||
throwError $ Error.ApiRequestError . Error.MaxAffectedViolationError $ toInteger queryTotal
|
throwError $ Error.ApiRequestError . ApiRequestTypes.MaxAffectedViolationError $ toInteger queryTotal
|
||||||
|
|
||||||
|
failsChangesOffLimits :: Maybe Integer -> ResultSet -> DbHandler ()
|
||||||
|
failsChangesOffLimits _ RSPlan{} = pure ()
|
||||||
|
failsChangesOffLimits Nothing _ = pure ()
|
||||||
|
failsChangesOffLimits (Just maxChanges) RSStandard{rsQueryTotal=queryTotal} =
|
||||||
|
when (queryTotal > fromIntegral maxChanges) $ do
|
||||||
|
lift SQL.condemn
|
||||||
|
throwError $ Error.ApiRequestError $ ApiRequestTypes.OffLimitsChangesError queryTotal maxChanges
|
||||||
|
|
||||||
-- | Set a transaction to roll back if requested
|
-- | Set a transaction to roll back if requested
|
||||||
optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
|
optionalRollback :: AppConfig -> ApiRequest -> DbHandler ()
|
||||||
@@ -298,7 +314,3 @@ runPreReq conf = lift $ traverse_ (SQL.statement mempty . stmt) (configDbPreRequ
|
|||||||
("select " <> fromQi req <> "()")
|
("select " <> fromQi req <> "()")
|
||||||
HD.noResult
|
HD.noResult
|
||||||
(configDbPreparedStatements conf)
|
(configDbPreparedStatements conf)
|
||||||
|
|
||||||
getSQLQuery :: Query -> ByteString
|
|
||||||
getSQLQuery DbQuery{dqSQL} = dqSQL
|
|
||||||
getSQLQuery _ = mempty
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
|
||||||
{-|
|
{-|
|
||||||
Module : PostgREST.Query.QueryBuilder
|
Module : PostgREST.Query.QueryBuilder
|
||||||
Description : PostgREST SQL queries generating functions.
|
Description : PostgREST SQL queries generating functions.
|
||||||
@@ -17,18 +16,15 @@ module PostgREST.Query.QueryBuilder
|
|||||||
, limitedQuery
|
, limitedQuery
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.HashMap.Strict as HM
|
|
||||||
import qualified Data.Set as S
|
|
||||||
import qualified Hasql.DynamicStatements.Snippet as SQL
|
import qualified Hasql.DynamicStatements.Snippet as SQL
|
||||||
import qualified Hasql.Encoders as HE
|
|
||||||
|
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Tree (Tree (..))
|
import Data.Tree (Tree (..))
|
||||||
|
|
||||||
import PostgREST.ApiRequest.Preferences (PreferResolution (..))
|
import PostgREST.ApiRequest.Preferences (PreferResolution (..))
|
||||||
import PostgREST.Config.PgVersion (PgVersion, pgVersion130)
|
import PostgREST.Config.PgVersion (PgVersion, pgVersion110,
|
||||||
|
pgVersion130)
|
||||||
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
import PostgREST.SchemaCache.Identifiers (QualifiedIdentifier (..))
|
||||||
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
import PostgREST.SchemaCache.Relationship (Cardinality (..),
|
||||||
Junction (..),
|
Junction (..),
|
||||||
@@ -41,18 +37,19 @@ import PostgREST.Plan.MutatePlan
|
|||||||
import PostgREST.Plan.ReadPlan
|
import PostgREST.Plan.ReadPlan
|
||||||
import PostgREST.Plan.Types
|
import PostgREST.Plan.Types
|
||||||
import PostgREST.Query.SqlFragment
|
import PostgREST.Query.SqlFragment
|
||||||
|
import PostgREST.RangeQuery (allRange)
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
readPlanToQuery :: ReadPlanTree -> SQL.Snippet
|
readPlanToQuery :: ReadPlanTree -> SQL.Snippet
|
||||||
readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds, relSelect, relSpread} forest) =
|
readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicForest,order, range_=readRange, relToParent, relJoinConds, relSelect} forest) =
|
||||||
"SELECT " <>
|
"SELECT " <>
|
||||||
intercalateSnippet ", " (selects ++ sprExtraSelects ++ joinsSelects) <>
|
intercalateSnippet ", " ((pgFmtSelectItem qi <$> (if null select && null forest then defSelect else select)) ++ joinsSelects) <> " " <>
|
||||||
fromFrag <>
|
fromFrag <> " " <>
|
||||||
intercalateSnippet " " joins <>
|
intercalateSnippet " " joins <> " " <>
|
||||||
(if null logicForest && null relJoinConds
|
(if null logicForest && null relJoinConds
|
||||||
then mempty
|
then mempty
|
||||||
else " WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition relJoinConds)) <> " " <>
|
else "WHERE " <> intercalateSnippet " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition relJoinConds)) <> " " <>
|
||||||
groupF qi select relSelect <> " " <>
|
groupF qi select relSelect <> " " <>
|
||||||
orderF qi order <> " " <>
|
orderF qi order <> " " <>
|
||||||
limitOffsetF readRange
|
limitOffsetF readRange
|
||||||
@@ -62,29 +59,25 @@ readPlanToQuery node@(Node ReadPlan{select,from=mainQi,fromAlias,where_=logicFor
|
|||||||
-- gets all the columns in case of an empty select, ignoring/obtaining these columns is done at the aggregation stage
|
-- gets all the columns in case of an empty select, ignoring/obtaining these columns is done at the aggregation stage
|
||||||
defSelect = [CoercibleSelectField (unknownField "*" []) Nothing Nothing Nothing Nothing]
|
defSelect = [CoercibleSelectField (unknownField "*" []) Nothing Nothing Nothing Nothing]
|
||||||
joins = getJoins node
|
joins = getJoins node
|
||||||
selects = pgFmtSelectItem qi <$> (if null select && null forest then defSelect else select)
|
|
||||||
joinsSelects = getJoinSelects node
|
joinsSelects = getJoinSelects node
|
||||||
sprExtraSelects = case relSpread of
|
|
||||||
Just (ToManySpread sels _) -> (\s -> pgFmtSelectItem (maybe qi (QualifiedIdentifier "") $ fst s) $ snd s) <$> sels
|
|
||||||
_ -> mempty
|
|
||||||
|
|
||||||
getJoinSelects :: ReadPlanTree -> [SQL.Snippet]
|
getJoinSelects :: ReadPlanTree -> [SQL.Snippet]
|
||||||
getJoinSelects (Node ReadPlan{relSelect} _) =
|
getJoinSelects (Node ReadPlan{relSelect} _) =
|
||||||
join $ map relSelectToSnippet relSelect
|
mapMaybe relSelectToSnippet relSelect
|
||||||
where
|
where
|
||||||
relSelectToSnippet :: RelSelectField -> [SQL.Snippet]
|
relSelectToSnippet :: RelSelectField -> Maybe SQL.Snippet
|
||||||
relSelectToSnippet fld =
|
relSelectToSnippet fld =
|
||||||
let aggAlias = pgFmtIdent $ rsAggAlias fld
|
let aggAlias = pgFmtIdent $ rsAggAlias fld
|
||||||
in
|
in
|
||||||
case fld of
|
case fld of
|
||||||
JsonEmbed{rsEmptyEmbed = True} ->
|
JsonEmbed{rsEmptyEmbed = True} ->
|
||||||
[]
|
Nothing
|
||||||
JsonEmbed{rsSelName, rsEmbedMode = JsonObject} ->
|
JsonEmbed{rsSelName, rsEmbedMode = JsonObject} ->
|
||||||
["row_to_json(" <> aggAlias <> ".*)::jsonb AS " <> pgFmtIdent rsSelName]
|
Just $ "row_to_json(" <> aggAlias <> ".*)::jsonb AS " <> pgFmtIdent rsSelName
|
||||||
JsonEmbed{rsSelName, rsEmbedMode = JsonArray} ->
|
JsonEmbed{rsSelName, rsEmbedMode = JsonArray} ->
|
||||||
["COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> pgFmtIdent rsSelName]
|
Just $ "COALESCE( " <> aggAlias <> "." <> aggAlias <> ", '[]') AS " <> pgFmtIdent rsSelName
|
||||||
Spread{rsSpreadSel, rsAggAlias} ->
|
Spread{rsSpreadSel, rsAggAlias} ->
|
||||||
pgFmtSpreadSelectItem rsAggAlias <$> rsSpreadSel
|
Just $ intercalateSnippet ", " (pgFmtSpreadSelectItem rsAggAlias <$> rsSpreadSel)
|
||||||
|
|
||||||
getJoins :: ReadPlanTree -> [SQL.Snippet]
|
getJoins :: ReadPlanTree -> [SQL.Snippet]
|
||||||
getJoins (Node _ []) = []
|
getJoins (Node _ []) = []
|
||||||
@@ -96,31 +89,26 @@ getJoins (Node ReadPlan{relSelect} forest) =
|
|||||||
) relSelect
|
) relSelect
|
||||||
|
|
||||||
getJoin :: RelSelectField -> ReadPlanTree -> SQL.Snippet
|
getJoin :: RelSelectField -> ReadPlanTree -> SQL.Snippet
|
||||||
getJoin fld node@(Node ReadPlan{relJoinType, relSpread} _) =
|
getJoin fld node@(Node ReadPlan{relJoinType} _) =
|
||||||
let
|
let
|
||||||
correlatedSubquery sub al cond =
|
correlatedSubquery sub al cond =
|
||||||
" " <> (if relJoinType == Just JTInner then "INNER" else "LEFT") <> " JOIN LATERAL ( " <> sub <> " ) AS " <> al <> " ON " <> cond
|
(if relJoinType == Just JTInner then "INNER" else "LEFT") <> " JOIN LATERAL ( " <> sub <> " ) AS " <> al <> " ON " <> cond
|
||||||
subquery = readPlanToQuery node
|
subquery = readPlanToQuery node
|
||||||
aggAlias = pgFmtIdent $ rsAggAlias fld
|
aggAlias = pgFmtIdent $ rsAggAlias fld
|
||||||
selectSubqAgg = "SELECT json_agg(" <> aggAlias <> ")::jsonb AS " <> aggAlias
|
|
||||||
fromSubqAgg = " FROM (" <> subquery <> " ) AS " <> aggAlias
|
|
||||||
joinCondition = if relJoinType == Just JTInner then aggAlias <> " IS NOT NULL" else "TRUE"
|
|
||||||
in
|
in
|
||||||
case fld of
|
case fld of
|
||||||
JsonEmbed{rsEmbedMode = JsonObject} ->
|
JsonEmbed{rsEmbedMode = JsonObject} ->
|
||||||
correlatedSubquery subquery aggAlias "TRUE"
|
correlatedSubquery subquery aggAlias "TRUE"
|
||||||
Spread{rsSpreadSel, rsAggAlias} ->
|
Spread{} ->
|
||||||
case relSpread of
|
correlatedSubquery subquery aggAlias "TRUE"
|
||||||
Just (ToManySpread _ sprOrder) ->
|
|
||||||
let selSpread = selectSubqAgg <> (if null rsSpreadSel then mempty else ", ") <> intercalateSnippet ", " (pgFmtSpreadJoinSelectItem rsAggAlias sprOrder <$> rsSpreadSel)
|
|
||||||
in correlatedSubquery (selSpread <> fromSubqAgg) aggAlias joinCondition
|
|
||||||
_ ->
|
|
||||||
correlatedSubquery subquery aggAlias "TRUE"
|
|
||||||
JsonEmbed{rsEmbedMode = JsonArray} ->
|
JsonEmbed{rsEmbedMode = JsonArray} ->
|
||||||
correlatedSubquery (selectSubqAgg <> fromSubqAgg) aggAlias joinCondition
|
let
|
||||||
|
subq = "SELECT json_agg(" <> aggAlias <> ")::jsonb AS " <> aggAlias <> " FROM (" <> subquery <> " ) AS " <> aggAlias
|
||||||
|
condition = if relJoinType == Just JTInner then aggAlias <> " IS NOT NULL" else "TRUE"
|
||||||
|
in correlatedSubquery subq aggAlias condition
|
||||||
|
|
||||||
mutatePlanToQuery :: MutatePlan -> SQL.Snippet
|
mutatePlanToQuery :: MutatePlan -> SQL.Snippet
|
||||||
mutatePlanToQuery (Insert mainQi iCols body onConflict putConditions returnings _ applyDefaults) =
|
mutatePlanToQuery (Insert mainQi iCols body onConflct putConditions returnings _ applyDefaults) =
|
||||||
"INSERT INTO " <> fromQi mainQi <> (if null iCols then " " else "(" <> cols <> ") ") <>
|
"INSERT INTO " <> fromQi mainQi <> (if null iCols then " " else "(" <> cols <> ") ") <>
|
||||||
fromJsonBodyF body iCols True False applyDefaults <>
|
fromJsonBodyF body iCols True False applyDefaults <>
|
||||||
-- Only used for PUT
|
-- Only used for PUT
|
||||||
@@ -137,90 +125,95 @@ mutatePlanToQuery (Insert mainQi iCols body onConflict putConditions returnings
|
|||||||
if null iCols
|
if null iCols
|
||||||
then "DO NOTHING"
|
then "DO NOTHING"
|
||||||
else "DO UPDATE SET " <> intercalateSnippet ", " ((pgFmtIdent . cfName) <> const " = EXCLUDED." <> (pgFmtIdent . cfName) <$> iCols) <> (if null putConditions && not mergeDups then mempty else "WHERE " <> addConfigPgrstInserted False)
|
else "DO UPDATE SET " <> intercalateSnippet ", " ((pgFmtIdent . cfName) <> const " = EXCLUDED." <> (pgFmtIdent . cfName) <$> iCols) <> (if null putConditions && not mergeDups then mempty else "WHERE " <> addConfigPgrstInserted False)
|
||||||
) onConflict <> " " <>
|
) onConflct <> " " <>
|
||||||
returningF mainQi returnings
|
returningF mainQi returnings
|
||||||
where
|
where
|
||||||
cols = intercalateSnippet ", " $ pgFmtIdent . cfName <$> iCols
|
cols = intercalateSnippet ", " $ pgFmtIdent . cfName <$> iCols
|
||||||
mergeDups = case onConflict of {Just (MergeDuplicates,_) -> True; _ -> False;}
|
mergeDups = case onConflct of {Just (MergeDuplicates,_) -> True; _ -> False;}
|
||||||
|
|
||||||
mutatePlanToQuery (Update mainQi uCols body logicForest returnings applyDefaults)
|
-- An update without a limit is always filtered with a WHERE
|
||||||
|
mutatePlanToQuery (Update mainQi uCols body logicForest range ordts returnings applyDefaults)
|
||||||
| null uCols =
|
| null uCols =
|
||||||
-- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax
|
-- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax
|
||||||
-- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select=
|
-- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select=
|
||||||
-- the select has to be based on "returnings" to make computed overloaded functions not throw
|
-- the select has to be based on "returnings" to make computed overloaded functions not throw
|
||||||
"SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false"
|
"SELECT " <> emptyBodyReturnedColumns <> " FROM " <> fromQi mainQi <> " WHERE false"
|
||||||
|
|
||||||
| otherwise =
|
| range == allRange =
|
||||||
"UPDATE " <> mainTbl <> " SET " <> cols <> " " <>
|
"UPDATE " <> mainTbl <> " SET " <> nonRangeCols <> " " <>
|
||||||
fromJsonBodyF body uCols False False applyDefaults <>
|
fromJsonBodyF body uCols False False applyDefaults <>
|
||||||
whereLogic <> " " <>
|
whereLogic <> " " <>
|
||||||
returningF mainQi returnings
|
returningF mainQi returnings
|
||||||
|
|
||||||
|
| otherwise =
|
||||||
|
"WITH " <>
|
||||||
|
"pgrst_update_body AS (" <> fromJsonBodyF body uCols True True applyDefaults <> "), " <>
|
||||||
|
"pgrst_affected_rows AS (" <>
|
||||||
|
"SELECT " <> rangeIdF <> " FROM " <> mainTbl <>
|
||||||
|
whereLogic <> " " <>
|
||||||
|
orderF mainQi ordts <> " " <>
|
||||||
|
limitOffsetF range <>
|
||||||
|
") " <>
|
||||||
|
"UPDATE " <> mainTbl <> " SET " <> rangeCols <>
|
||||||
|
"FROM pgrst_affected_rows " <>
|
||||||
|
"WHERE " <> whereRangeIdF <> " " <>
|
||||||
|
returningF mainQi returnings
|
||||||
|
|
||||||
where
|
where
|
||||||
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
|
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
|
||||||
mainTbl = fromQi mainQi
|
mainTbl = fromQi mainQi
|
||||||
emptyBodyReturnedColumns = if null returnings then "NULL" else intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)
|
emptyBodyReturnedColumns = if null returnings then "NULL" else intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings)
|
||||||
cols = intercalateSnippet ", " (pgFmtIdent . cfName <> const " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_body") . cfName <$> uCols)
|
nonRangeCols = intercalateSnippet ", " (pgFmtIdent . cfName <> const " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_body") . cfName <$> uCols)
|
||||||
|
rangeCols = intercalateSnippet ", " ((\col -> pgFmtIdent (cfName col) <> " = (SELECT " <> pgFmtIdent (cfName col) <> " FROM pgrst_update_body) ") <$> uCols)
|
||||||
|
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (cfName . coField <$> ordts)
|
||||||
|
|
||||||
|
mutatePlanToQuery (Delete mainQi logicForest range ordts returnings)
|
||||||
|
| range == allRange =
|
||||||
|
"DELETE FROM " <> fromQi mainQi <> " " <>
|
||||||
|
whereLogic <> " " <>
|
||||||
|
returningF mainQi returnings
|
||||||
|
|
||||||
|
| otherwise =
|
||||||
|
"WITH " <>
|
||||||
|
"pgrst_affected_rows AS (" <>
|
||||||
|
"SELECT " <> rangeIdF <> " FROM " <> fromQi mainQi <>
|
||||||
|
whereLogic <> " " <>
|
||||||
|
orderF mainQi ordts <> " " <>
|
||||||
|
limitOffsetF range <>
|
||||||
|
") " <>
|
||||||
|
"DELETE FROM " <> fromQi mainQi <> " " <>
|
||||||
|
"USING pgrst_affected_rows " <>
|
||||||
|
"WHERE " <> whereRangeIdF <> " " <>
|
||||||
|
returningF mainQi returnings
|
||||||
|
|
||||||
mutatePlanToQuery (Delete mainQi logicForest returnings) =
|
|
||||||
"DELETE FROM " <> fromQi mainQi <> " " <>
|
|
||||||
whereLogic <> " " <>
|
|
||||||
returningF mainQi returnings
|
|
||||||
where
|
where
|
||||||
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
|
whereLogic = if null logicForest then mempty else " WHERE " <> intercalateSnippet " AND " (pgFmtLogicTree mainQi <$> logicForest)
|
||||||
|
(whereRangeIdF, rangeIdF) = mutRangeF mainQi (cfName . coField <$> ordts)
|
||||||
|
|
||||||
callPlanToQuery :: CallPlan -> PgVersion -> SQL.Snippet
|
callPlanToQuery :: CallPlan -> PgVersion -> SQL.Snippet
|
||||||
callPlanToQuery (FunctionCall qi params arguments returnsScalar returnsSetOfScalar returnsCompositeAlias filterFields returnings) pgVer =
|
callPlanToQuery (FunctionCall qi params args returnsScalar returnsSetOfScalar returnsCompositeAlias returnings) pgVer =
|
||||||
"SELECT " <> (if returnsScalar || returnsSetOfScalar then "pgrst_call.pgrst_scalar" else returnedColumns) <> " " <>
|
"SELECT " <> (if returnsScalar || returnsSetOfScalar then "pgrst_call.pgrst_scalar" else returnedColumns) <> " " <>
|
||||||
fromCall
|
fromCall
|
||||||
where
|
where
|
||||||
jsonArgs = case arguments of
|
|
||||||
DirectArgs args -> Just $ JSON.encode args
|
|
||||||
JsonArgs json -> json
|
|
||||||
fromCall = case params of
|
fromCall = case params of
|
||||||
OnePosParam prm -> "FROM " <> callIt (singleParameter jsonArgs $ encodeUtf8 $ ppType prm)
|
OnePosParam prm -> "FROM " <> callIt (singleParameter args $ encodeUtf8 $ ppType prm)
|
||||||
KeyParams [] -> "FROM " <> callIt mempty
|
KeyParams [] -> "FROM " <> callIt mempty
|
||||||
KeyParams prms -> case arguments of
|
KeyParams prms -> fromJsonBodyF args ((\p -> CoercibleField (ppName p) mempty False (ppTypeMaxLength p) Nothing Nothing) <$> prms) False True False <> ", " <>
|
||||||
DirectArgs args -> "FROM " <> callIt (fmtArgs prms args)
|
|
||||||
JsonArgs json -> fromJsonBodyF json ((\p -> CoercibleField (ppName p) mempty False Nothing (ppTypeMaxLength p) mempty Nothing Nothing False) <$> prms) False True False <> ", " <>
|
|
||||||
"LATERAL " <> callIt (fmtParams prms)
|
"LATERAL " <> callIt (fmtParams prms)
|
||||||
|
|
||||||
callIt :: SQL.Snippet -> SQL.Snippet
|
callIt :: SQL.Snippet -> SQL.Snippet
|
||||||
callIt argument | pgVer < pgVersion130 && returnsCompositeAlias = "(SELECT (" <> fromQi qi <> "(" <> argument <> ")).*) pgrst_call"
|
callIt argument | pgVer < pgVersion130 && pgVer >= pgVersion110 && returnsCompositeAlias = "(SELECT (" <> fromQi qi <> "(" <> argument <> ")).*) pgrst_call"
|
||||||
| returnsScalar || returnsSetOfScalar = "(SELECT " <> fromQi qi <> "(" <> argument <> ") pgrst_scalar) pgrst_call"
|
| returnsScalar || returnsSetOfScalar = "(SELECT " <> fromQi qi <> "(" <> argument <> ") pgrst_scalar) pgrst_call"
|
||||||
| otherwise = fromQi qi <> "(" <> argument <> ") pgrst_call"
|
| otherwise = fromQi qi <> "(" <> argument <> ") pgrst_call"
|
||||||
|
|
||||||
fmtParams :: [RoutineParam] -> SQL.Snippet
|
fmtParams :: [RoutineParam] -> SQL.Snippet
|
||||||
fmtParams prms = intercalateSnippet ", "
|
fmtParams prms = intercalateSnippet ", "
|
||||||
((\a -> (if ppVar a then "VARIADIC " else mempty) <> pgFmtIdent (ppName a) <> " := pgrst_body." <> pgFmtIdent (ppName a)) <$> prms)
|
((\a -> (if ppVar a then "VARIADIC " else mempty) <> pgFmtIdent (ppName a) <> " := pgrst_body." <> pgFmtIdent (ppName a)) <$> prms)
|
||||||
|
|
||||||
fmtArgs :: [RoutineParam] -> HM.HashMap Text RpcParamValue -> SQL.Snippet
|
|
||||||
fmtArgs prms args = intercalateSnippet ", " $ fmtArg <$> prms
|
|
||||||
where
|
|
||||||
fmtArg RoutineParam{..} =
|
|
||||||
(if ppVar then "VARIADIC " else mempty) <>
|
|
||||||
pgFmtIdent ppName <>
|
|
||||||
" := " <>
|
|
||||||
encodeArg (HM.lookup ppName args) <>
|
|
||||||
"::" <>
|
|
||||||
SQL.sql (encodeUtf8 ppTypeMaxLength)
|
|
||||||
encodeArg :: Maybe RpcParamValue -> SQL.Snippet
|
|
||||||
encodeArg (Just (Variadic v)) = SQL.encoderAndParam (HE.nonNullable $ HE.foldableArray $ HE.nonNullable HE.text) v
|
|
||||||
encodeArg (Just (Fixed v)) = SQL.encoderAndParam (HE.nonNullable HE.unknown) $ encodeUtf8 v
|
|
||||||
-- Currently not supported: Calling functions without some of their arguments without DEFAULT.
|
|
||||||
-- We could fallback to providing this NULL value in those cases.
|
|
||||||
encodeArg Nothing = "NULL"
|
|
||||||
|
|
||||||
-- the columns here would be the returnings + the columns that would later
|
|
||||||
-- be used by a where clause filter, if they intersect, we remove the duplicates
|
|
||||||
-- and if * is returned then no need to explicitly add filter columns
|
|
||||||
returnedColumns :: SQL.Snippet
|
returnedColumns :: SQL.Snippet
|
||||||
returnedColumns = case S.toList returnings of
|
returnedColumns
|
||||||
[] -> "*"
|
| null returnings = "*"
|
||||||
["*"] -> pgFmtColumn (QualifiedIdentifier mempty "pgrst_call") "*"
|
| otherwise = intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty "pgrst_call") <$> returnings)
|
||||||
_ -> intercalateSnippet ", " (pgFmtColumn (QualifiedIdentifier mempty "pgrst_call") <$> returnedColumns')
|
|
||||||
where
|
|
||||||
returnedColumns' = S.toList $ returnings <> filterFields
|
|
||||||
|
|
||||||
-- | SQL query meant for COUNTing the root node of the Tree.
|
-- | SQL query meant for COUNTing the root node of the Tree.
|
||||||
-- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT.
|
-- It only takes WHERE into account and doesn't include LIMIT/OFFSET because it would reduce the COUNT.
|
||||||
@@ -274,7 +267,7 @@ getQualifiedIdentifier rel mainQi tblAlias = case rel of
|
|||||||
|
|
||||||
-- FROM clause plus implicit joins
|
-- FROM clause plus implicit joins
|
||||||
fromF :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> SQL.Snippet
|
fromF :: Maybe Relationship -> QualifiedIdentifier -> Maybe Alias -> SQL.Snippet
|
||||||
fromF rel mainQi tblAlias = " FROM " <>
|
fromF rel mainQi tblAlias = "FROM " <>
|
||||||
(case rel of
|
(case rel of
|
||||||
-- Due to the use of CTEs on RPC, we need to cast the parameter to the table name in case of function overloading.
|
-- Due to the use of CTEs on RPC, we need to cast the parameter to the table name in case of function overloading.
|
||||||
-- See https://github.com/PostgREST/postgrest/issues/2963#issuecomment-1736557386
|
-- See https://github.com/PostgREST/postgrest/issues/2963#issuecomment-1736557386
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ module PostgREST.Query.SqlFragment
|
|||||||
, fromQi
|
, fromQi
|
||||||
, limitOffsetF
|
, limitOffsetF
|
||||||
, locationF
|
, locationF
|
||||||
|
, mutRangeF
|
||||||
, orderF
|
, orderF
|
||||||
, pgFmtColumn
|
, pgFmtColumn
|
||||||
, pgFmtFilter
|
, pgFmtFilter
|
||||||
@@ -22,7 +23,6 @@ module PostgREST.Query.SqlFragment
|
|||||||
, pgFmtOrderTerm
|
, pgFmtOrderTerm
|
||||||
, pgFmtSelectItem
|
, pgFmtSelectItem
|
||||||
, pgFmtSpreadSelectItem
|
, pgFmtSpreadSelectItem
|
||||||
, pgFmtSpreadJoinSelectItem
|
|
||||||
, fromJsonBodyF
|
, fromJsonBodyF
|
||||||
, responseHeadersF
|
, responseHeadersF
|
||||||
, responseStatusF
|
, responseStatusF
|
||||||
@@ -53,13 +53,12 @@ import qualified Hasql.Encoders as HE
|
|||||||
|
|
||||||
import Control.Arrow ((***))
|
import Control.Arrow ((***))
|
||||||
|
|
||||||
import Data.Foldable (foldr1)
|
import Data.Foldable (foldr1)
|
||||||
import NeatInterpolation (trimming)
|
import Text.InterpolatedString.Perl6 (qc)
|
||||||
|
|
||||||
import PostgREST.ApiRequest.Types (AggregateFunction (..),
|
import PostgREST.ApiRequest.Types (AggregateFunction (..),
|
||||||
Alias, Cast,
|
Alias, Cast,
|
||||||
FtsOperator (..),
|
FtsOperator (..),
|
||||||
IsVal (..),
|
|
||||||
JsonOperand (..),
|
JsonOperand (..),
|
||||||
JsonOperation (..),
|
JsonOperation (..),
|
||||||
JsonPath,
|
JsonPath,
|
||||||
@@ -70,7 +69,8 @@ import PostgREST.ApiRequest.Types (AggregateFunction (..),
|
|||||||
OrderDirection (..),
|
OrderDirection (..),
|
||||||
OrderNulls (..),
|
OrderNulls (..),
|
||||||
QuantOperator (..),
|
QuantOperator (..),
|
||||||
SimpleOperator (..))
|
SimpleOperator (..),
|
||||||
|
TrileanVal (..))
|
||||||
import PostgREST.MediaType (MTVndPlanFormat (..),
|
import PostgREST.MediaType (MTVndPlanFormat (..),
|
||||||
MTVndPlanOption (..))
|
MTVndPlanOption (..))
|
||||||
import PostgREST.Plan.ReadPlan (JoinCondition (..))
|
import PostgREST.Plan.ReadPlan (JoinCondition (..))
|
||||||
@@ -81,7 +81,6 @@ import PostgREST.Plan.Types (CoercibleField (..),
|
|||||||
CoercibleSelectField (..),
|
CoercibleSelectField (..),
|
||||||
RelSelectField (..),
|
RelSelectField (..),
|
||||||
SpreadSelectField (..),
|
SpreadSelectField (..),
|
||||||
ToTsVector (..),
|
|
||||||
unknownField)
|
unknownField)
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
rangeLimit, rangeOffset)
|
rangeLimit, rangeOffset)
|
||||||
@@ -158,10 +157,10 @@ pgBuildArrayLiteral vals =
|
|||||||
|
|
||||||
-- TODO: refactor by following https://github.com/PostgREST/postgrest/pull/1631#issuecomment-711070833
|
-- TODO: refactor by following https://github.com/PostgREST/postgrest/pull/1631#issuecomment-711070833
|
||||||
pgFmtIdent :: Text -> SQL.Snippet
|
pgFmtIdent :: Text -> SQL.Snippet
|
||||||
pgFmtIdent x = SQL.sql . encodeUtf8 $ escapeIdent x
|
pgFmtIdent x = SQL.sql $ escapeIdent x
|
||||||
|
|
||||||
escapeIdent :: Text -> Text
|
escapeIdent :: Text -> ByteString
|
||||||
escapeIdent x = "\"" <> T.replace "\"" "\"\"" (trimNullChars x) <> "\""
|
escapeIdent x = encodeUtf8 $ "\"" <> T.replace "\"" "\"\"" (trimNullChars x) <> "\""
|
||||||
|
|
||||||
-- Only use it if the input comes from the database itself, like on `jsonb_build_object('column_from_a_table', val)..`
|
-- Only use it if the input comes from the database itself, like on `jsonb_build_object('column_from_a_table', val)..`
|
||||||
pgFmtLit :: Text -> Text
|
pgFmtLit :: Text -> Text
|
||||||
@@ -182,7 +181,7 @@ trimNullChars = T.takeWhile (/= '\x0')
|
|||||||
-- >>> escapeIdentList ["schema_1", "schema_2", "SPECIAL \"@/\\#~_-"]
|
-- >>> escapeIdentList ["schema_1", "schema_2", "SPECIAL \"@/\\#~_-"]
|
||||||
-- "\"schema_1\", \"schema_2\", \"SPECIAL \"\"@/\\#~_-\""
|
-- "\"schema_1\", \"schema_2\", \"SPECIAL \"\"@/\\#~_-\""
|
||||||
escapeIdentList :: [Text] -> ByteString
|
escapeIdentList :: [Text] -> ByteString
|
||||||
escapeIdentList schemas = BS.intercalate ", " $ encodeUtf8 . escapeIdent <$> schemas
|
escapeIdentList schemas = BS.intercalate ", " $ escapeIdent <$> schemas
|
||||||
|
|
||||||
asCsvF :: SQL.Snippet
|
asCsvF :: SQL.Snippet
|
||||||
asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
|
asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
|
||||||
@@ -230,11 +229,11 @@ customFuncF _ funcQi RelAnyElement = fromQi funcQi <> "(_postgrest_t)
|
|||||||
customFuncF _ funcQi (RelId target) = fromQi funcQi <> "(_postgrest_t::" <> fromQi target <> ")"
|
customFuncF _ funcQi (RelId target) = fromQi funcQi <> "(_postgrest_t::" <> fromQi target <> ")"
|
||||||
|
|
||||||
locationF :: [Text] -> SQL.Snippet
|
locationF :: [Text] -> SQL.Snippet
|
||||||
locationF pKeys = SQL.sql $ encodeUtf8 [trimming|(
|
locationF pKeys = [qc|(
|
||||||
WITH data AS (SELECT row_to_json(_) AS row FROM ${sourceCTEName} AS _ LIMIT 1)
|
WITH data AS (SELECT row_to_json(_) AS row FROM {sourceCTEName} AS _ LIMIT 1)
|
||||||
SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null'))
|
SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null'))
|
||||||
FROM data CROSS JOIN json_each_text(data.row) AS json_data
|
FROM data CROSS JOIN json_each_text(data.row) AS json_data
|
||||||
WHERE json_data.key IN ('${fmtPKeys}')
|
WHERE json_data.key IN ('{fmtPKeys}')
|
||||||
)|]
|
)|]
|
||||||
where
|
where
|
||||||
fmtPKeys = T.intercalate "','" pKeys
|
fmtPKeys = T.intercalate "','" pKeys
|
||||||
@@ -253,15 +252,9 @@ pgFmtCallUnary :: Text -> SQL.Snippet -> SQL.Snippet
|
|||||||
pgFmtCallUnary f x = SQL.sql (encodeUtf8 f) <> "(" <> x <> ")"
|
pgFmtCallUnary f x = SQL.sql (encodeUtf8 f) <> "(" <> x <> ")"
|
||||||
|
|
||||||
pgFmtField :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
|
pgFmtField :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
|
||||||
pgFmtField table cf = case cfToTsVector cf of
|
pgFmtField table CoercibleField{cfName=fn, cfJsonPath=[]} = pgFmtColumn table fn
|
||||||
Just (ToTsVector lang) -> "to_tsvector(" <> pgFmtFtsLang lang <> fmtFld <> ")"
|
pgFmtField table CoercibleField{cfName=fn, cfToJson=doToJson, cfJsonPath=jp} | doToJson = "to_jsonb(" <> pgFmtColumn table fn <> ")" <> pgFmtJsonPath jp
|
||||||
_ -> fmtFld
|
| otherwise = pgFmtColumn table fn <> pgFmtJsonPath jp
|
||||||
where
|
|
||||||
fmtFld = case cf of
|
|
||||||
CoercibleField{cfFullRow=True} -> pgFmtIdent (qiName table)
|
|
||||||
CoercibleField{cfName=fn, cfJsonPath=[]} -> pgFmtColumn table fn
|
|
||||||
CoercibleField{cfName=fn, cfToJson=doToJson, cfJsonPath=jp} | doToJson -> "to_jsonb(" <> pgFmtColumn table fn <> ")" <> pgFmtJsonPath jp
|
|
||||||
| otherwise -> pgFmtColumn table fn <> pgFmtJsonPath jp
|
|
||||||
|
|
||||||
-- Select the value of a named element from a table, applying its optional coercion mapping if any.
|
-- Select the value of a named element from a table, applying its optional coercion mapping if any.
|
||||||
pgFmtTableCoerce :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
|
pgFmtTableCoerce :: QualifiedIdentifier -> CoercibleField -> SQL.Snippet
|
||||||
@@ -279,7 +272,11 @@ pgFmtSelectItem table CoercibleSelectField{csField=fld, csAggFunction=agg, csAgg
|
|||||||
|
|
||||||
pgFmtSpreadSelectItem :: Alias -> SpreadSelectField -> SQL.Snippet
|
pgFmtSpreadSelectItem :: Alias -> SpreadSelectField -> SQL.Snippet
|
||||||
pgFmtSpreadSelectItem aggAlias SpreadSelectField{ssSelName, ssSelAggFunction, ssSelAggCast, ssSelAlias} =
|
pgFmtSpreadSelectItem aggAlias SpreadSelectField{ssSelName, ssSelAggFunction, ssSelAggCast, ssSelAlias} =
|
||||||
pgFmtApplyAggregate ssSelAggFunction ssSelAggCast (pgFmtFullSelName aggAlias ssSelName) <> pgFmtAs ssSelAlias
|
pgFmtApplyAggregate ssSelAggFunction ssSelAggCast fullSelName <> pgFmtAs ssSelAlias
|
||||||
|
where
|
||||||
|
fullSelName = case ssSelName of
|
||||||
|
"*" -> pgFmtIdent aggAlias <> ".*"
|
||||||
|
_ -> pgFmtIdent aggAlias <> "." <> pgFmtIdent ssSelName
|
||||||
|
|
||||||
pgFmtApplyAggregate :: Maybe AggregateFunction -> Maybe Cast -> SQL.Snippet -> SQL.Snippet
|
pgFmtApplyAggregate :: Maybe AggregateFunction -> Maybe Cast -> SQL.Snippet -> SQL.Snippet
|
||||||
pgFmtApplyAggregate Nothing _ snippet = snippet
|
pgFmtApplyAggregate Nothing _ snippet = snippet
|
||||||
@@ -291,14 +288,6 @@ pgFmtApplyAggregate (Just agg) aggCast snippet =
|
|||||||
convertAggFunction = SQL.sql . BS.map toUpper . BS.pack . show
|
convertAggFunction = SQL.sql . BS.map toUpper . BS.pack . show
|
||||||
aggregatedSnippet = convertAggFunction agg <> "(" <> snippet <> ")"
|
aggregatedSnippet = convertAggFunction agg <> "(" <> snippet <> ")"
|
||||||
|
|
||||||
pgFmtSpreadJoinSelectItem :: Alias -> [CoercibleOrderTerm] -> SpreadSelectField -> SQL.Snippet
|
|
||||||
pgFmtSpreadJoinSelectItem aggAlias order SpreadSelectField{ssSelName, ssSelAlias} =
|
|
||||||
"COALESCE(json_agg(" <> fmtField <> " " <> fmtOrder <> "),'[]')::jsonb" <> " AS " <> fmtAlias
|
|
||||||
where
|
|
||||||
fmtField = pgFmtFullSelName aggAlias ssSelName
|
|
||||||
fmtOrder = orderF (QualifiedIdentifier "" aggAlias) order
|
|
||||||
fmtAlias = pgFmtIdent (fromMaybe ssSelName ssSelAlias)
|
|
||||||
|
|
||||||
pgFmtApplyCast :: Maybe Cast -> SQL.Snippet -> SQL.Snippet
|
pgFmtApplyCast :: Maybe Cast -> SQL.Snippet -> SQL.Snippet
|
||||||
pgFmtApplyCast Nothing snippet = snippet
|
pgFmtApplyCast Nothing snippet = snippet
|
||||||
-- Ideally we'd quote the cast with "pgFmtIdent cast". However, that would invalidate common casts such as "int", "bigint", etc.
|
-- Ideally we'd quote the cast with "pgFmtIdent cast". However, that would invalidate common casts such as "int", "bigint", etc.
|
||||||
@@ -306,11 +295,6 @@ pgFmtApplyCast Nothing snippet = snippet
|
|||||||
-- Not quoting should be fine, we validate the input on Parsers.
|
-- Not quoting should be fine, we validate the input on Parsers.
|
||||||
pgFmtApplyCast (Just cast) snippet = "CAST( " <> snippet <> " AS " <> SQL.sql (encodeUtf8 cast) <> " )"
|
pgFmtApplyCast (Just cast) snippet = "CAST( " <> snippet <> " AS " <> SQL.sql (encodeUtf8 cast) <> " )"
|
||||||
|
|
||||||
pgFmtFullSelName :: Alias -> FieldName -> SQL.Snippet
|
|
||||||
pgFmtFullSelName aggAlias fieldName = case fieldName of
|
|
||||||
"*" -> pgFmtIdent aggAlias <> ".*"
|
|
||||||
_ -> pgFmtIdent aggAlias <> "." <> pgFmtIdent fieldName
|
|
||||||
|
|
||||||
-- TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body
|
-- TODO: At this stage there shouldn't be a Maybe since ApiRequest should ensure that an INSERT/UPDATE has a body
|
||||||
fromJsonBodyF :: Maybe LBS.ByteString -> [CoercibleField] -> Bool -> Bool -> Bool -> SQL.Snippet
|
fromJsonBodyF :: Maybe LBS.ByteString -> [CoercibleField] -> Bool -> Bool -> Bool -> SQL.Snippet
|
||||||
fromJsonBodyF body fields includeSelect includeLimitOne includeDefaults =
|
fromJsonBodyF body fields includeSelect includeLimitOne includeDefaults =
|
||||||
@@ -395,15 +379,13 @@ pgFmtFilter table (CoercibleFilter fld (OpExpr hasNot oper)) = notOp <> " " <> p
|
|||||||
|
|
||||||
-- IS cannot be prepared. `PREPARE boolplan AS SELECT * FROM projects where id IS $1` will give a syntax error.
|
-- IS cannot be prepared. `PREPARE boolplan AS SELECT * FROM projects where id IS $1` will give a syntax error.
|
||||||
-- The above can be fixed by using `PREPARE boolplan AS SELECT * FROM projects where id IS NOT DISTINCT FROM $1;`
|
-- The above can be fixed by using `PREPARE boolplan AS SELECT * FROM projects where id IS NOT DISTINCT FROM $1;`
|
||||||
-- However that would not accept the TRUE/FALSE/NULL/"NOT NULL"/UNKNOWN keywords. See: https://stackoverflow.com/questions/6133525/proper-way-to-set-preparedstatement-parameter-to-null-under-postgres.
|
-- However that would not accept the TRUE/FALSE/NULL/UNKNOWN keywords. See: https://stackoverflow.com/questions/6133525/proper-way-to-set-preparedstatement-parameter-to-null-under-postgres.
|
||||||
-- This is why `IS` operands are whitelisted at the Parsers.hs level
|
-- This is why `IS` operands are whitelisted at the Parsers.hs level
|
||||||
Is isVal -> " IS " <>
|
Is triVal -> " IS " <> case triVal of
|
||||||
case isVal of
|
TriTrue -> "TRUE"
|
||||||
IsNull -> "NULL"
|
TriFalse -> "FALSE"
|
||||||
IsNotNull -> "NOT NULL"
|
TriNull -> "NULL"
|
||||||
IsTriTrue -> "TRUE"
|
TriUnknown -> "UNKNOWN"
|
||||||
IsTriFalse -> "FALSE"
|
|
||||||
IsTriUnknown -> "UNKNOWN"
|
|
||||||
|
|
||||||
IsDistinctFrom val -> " IS DISTINCT FROM " <> unknownLiteral val
|
IsDistinctFrom val -> " IS DISTINCT FROM " <> unknownLiteral val
|
||||||
|
|
||||||
@@ -414,8 +396,9 @@ pgFmtFilter table (CoercibleFilter fld (OpExpr hasNot oper)) = notOp <> " " <> p
|
|||||||
[""] -> "= ANY('{}') "
|
[""] -> "= ANY('{}') "
|
||||||
_ -> "= ANY (" <> pgFmtArrayLiteralForField vals fld <> ") "
|
_ -> "= ANY (" <> pgFmtArrayLiteralForField vals fld <> ") "
|
||||||
|
|
||||||
Fts op lang val -> " " <> ftsOperator op <> "(" <> pgFmtFtsLang lang <> unknownLiteral val <> ") "
|
Fts op lang val -> " " <> ftsOperator op <> "(" <> ftsLang lang <> unknownLiteral val <> ") "
|
||||||
where
|
where
|
||||||
|
ftsLang = maybe mempty (\l -> unknownLiteral l <> ", ")
|
||||||
notOp = if hasNot then "NOT" else mempty
|
notOp = if hasNot then "NOT" else mempty
|
||||||
star c = if c == '*' then '%' else c
|
star c = if c == '*' then '%' else c
|
||||||
fmtQuant q val = case q of
|
fmtQuant q val = case q of
|
||||||
@@ -423,9 +406,6 @@ pgFmtFilter table (CoercibleFilter fld (OpExpr hasNot oper)) = notOp <> " " <> p
|
|||||||
Just QuantAll -> "ALL(" <> val <> ")"
|
Just QuantAll -> "ALL(" <> val <> ")"
|
||||||
Nothing -> val
|
Nothing -> val
|
||||||
|
|
||||||
pgFmtFtsLang :: Maybe Text -> SQL.Snippet
|
|
||||||
pgFmtFtsLang = maybe mempty (\l -> unknownLiteral l <> ", ")
|
|
||||||
|
|
||||||
pgFmtJoinCondition :: JoinCondition -> SQL.Snippet
|
pgFmtJoinCondition :: JoinCondition -> SQL.Snippet
|
||||||
pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
|
pgFmtJoinCondition (JoinCondition (qi1, col1) (qi2, col2)) =
|
||||||
pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2
|
pgFmtColumn qi1 col1 <> " = " <> pgFmtColumn qi2 col2
|
||||||
@@ -522,6 +502,13 @@ currentSettingF setting =
|
|||||||
-- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
|
-- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
|
||||||
"nullif(current_setting('" <> setting <> "', true), '')"
|
"nullif(current_setting('" <> setting <> "', true), '')"
|
||||||
|
|
||||||
|
mutRangeF :: QualifiedIdentifier -> [FieldName] -> (SQL.Snippet, SQL.Snippet)
|
||||||
|
mutRangeF mainQi rangeId =
|
||||||
|
(
|
||||||
|
intercalateSnippet " AND " $ (\col -> pgFmtColumn mainQi col <> " = " <> pgFmtColumn (QualifiedIdentifier mempty "pgrst_affected_rows") col) <$> rangeId
|
||||||
|
, intercalateSnippet ", " (pgFmtColumn mainQi <$> rangeId)
|
||||||
|
)
|
||||||
|
|
||||||
orderF :: QualifiedIdentifier -> [CoercibleOrderTerm] -> SQL.Snippet
|
orderF :: QualifiedIdentifier -> [CoercibleOrderTerm] -> SQL.Snippet
|
||||||
orderF _ [] = mempty
|
orderF _ [] = mempty
|
||||||
orderF qi ordts = "ORDER BY " <> intercalateSnippet ", " (pgFmtOrderTerm qi <$> ordts)
|
orderF qi ordts = "ORDER BY " <> intercalateSnippet ", " (pgFmtOrderTerm qi <$> ordts)
|
||||||
|
|||||||
@@ -56,11 +56,10 @@ data ResultSet
|
|||||||
|
|
||||||
|
|
||||||
prepareWrite :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> MediaType -> MediaHandler ->
|
prepareWrite :: SQL.Snippet -> SQL.Snippet -> Bool -> Bool -> MediaType -> MediaHandler ->
|
||||||
Maybe PreferRepresentation -> Maybe PreferResolution -> [Text] -> Bool -> (SQL.Statement () ResultSet, ByteString)
|
Maybe PreferRepresentation -> Maybe PreferResolution -> [Text] -> Bool -> SQL.Statement () ResultSet
|
||||||
prepareWrite selectQuery mutateQuery isInsert isPut mt handler rep resolution pKeys prepared =
|
prepareWrite selectQuery mutateQuery isInsert isPut mt handler rep resolution pKeys =
|
||||||
(result, sql)
|
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
|
||||||
where
|
where
|
||||||
result@(SQL.Statement sql _ _ _) = SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt prepared
|
|
||||||
checkUpsert snip = if isInsert && (isPut || resolution == Just MergeDuplicates) then snip else "''"
|
checkUpsert snip = if isInsert && (isPut || resolution == Just MergeDuplicates) then snip else "''"
|
||||||
pgrstInsertedF = checkUpsert "nullif(current_setting('pgrst.inserted', true),'')::int"
|
pgrstInsertedF = checkUpsert "nullif(current_setting('pgrst.inserted', true),'')::int"
|
||||||
snippet =
|
snippet =
|
||||||
@@ -94,11 +93,10 @@ prepareWrite selectQuery mutateQuery isInsert isPut mt handler rep resolution pK
|
|||||||
MTVndPlan{} -> planRow
|
MTVndPlan{} -> planRow
|
||||||
_ -> fromMaybe (RSStandard Nothing 0 mempty mempty Nothing Nothing Nothing) <$> HD.rowMaybe (standardRow False)
|
_ -> fromMaybe (RSStandard Nothing 0 mempty mempty Nothing Nothing Nothing) <$> HD.rowMaybe (standardRow False)
|
||||||
|
|
||||||
prepareRead :: SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler -> Bool -> (SQL.Statement () ResultSet, ByteString)
|
prepareRead :: SQL.Snippet -> SQL.Snippet -> Bool -> MediaType -> MediaHandler -> Bool -> SQL.Statement () ResultSet
|
||||||
prepareRead selectQuery countQuery countTotal mt handler prepared =
|
prepareRead selectQuery countQuery countTotal mt handler =
|
||||||
(result, sql)
|
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
|
||||||
where
|
where
|
||||||
result@(SQL.Statement sql _ _ _) = SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt prepared
|
|
||||||
snippet =
|
snippet =
|
||||||
"WITH " <> sourceCTE <> " AS ( " <> selectQuery <> " ) " <>
|
"WITH " <> sourceCTE <> " AS ( " <> selectQuery <> " ) " <>
|
||||||
countCTEF <> " " <>
|
countCTEF <> " " <>
|
||||||
@@ -120,11 +118,10 @@ prepareRead selectQuery countQuery countTotal mt handler prepared =
|
|||||||
|
|
||||||
prepareCall :: Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
|
prepareCall :: Routine -> SQL.Snippet -> SQL.Snippet -> SQL.Snippet -> Bool ->
|
||||||
MediaType -> MediaHandler -> Bool ->
|
MediaType -> MediaHandler -> Bool ->
|
||||||
(SQL.Statement () ResultSet, ByteString)
|
SQL.Statement () ResultSet
|
||||||
prepareCall rout callProcQuery selectQuery countQuery countTotal mt handler prepared =
|
prepareCall rout callProcQuery selectQuery countQuery countTotal mt handler =
|
||||||
(result, sql)
|
SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt
|
||||||
where
|
where
|
||||||
result@(SQL.Statement sql _ _ _) = SQL.dynamicallyParameterized (mtSnippet mt snippet) decodeIt prepared
|
|
||||||
snippet =
|
snippet =
|
||||||
"WITH " <> sourceCTE <> " AS (" <> callProcQuery <> ") " <>
|
"WITH " <> sourceCTE <> " AS (" <> callProcQuery <> ") " <>
|
||||||
countCTEF <>
|
countCTEF <>
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import PostgREST.SchemaCache.Routine (FuncVolatility (..),
|
|||||||
Routine (..))
|
Routine (..))
|
||||||
import PostgREST.SchemaCache.Table (Table (..))
|
import PostgREST.SchemaCache.Table (Table (..))
|
||||||
|
|
||||||
|
import qualified PostgREST.ApiRequest.Types as ApiRequestTypes
|
||||||
import qualified PostgREST.SchemaCache.Routine as Routine
|
import qualified PostgREST.SchemaCache.Routine as Routine
|
||||||
|
|
||||||
import Protolude hiding (Handler, toS)
|
import Protolude hiding (Handler, toS)
|
||||||
@@ -68,8 +69,7 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
|
|||||||
RSStandard{..} -> do
|
RSStandard{..} -> do
|
||||||
let
|
let
|
||||||
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
(status, contentRange) = RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
||||||
cLHeader = if headersOnly then mempty else [contentLengthHeaderStrict rsBody]
|
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
|
||||||
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
|
|
||||||
headers =
|
headers =
|
||||||
[ contentRange
|
[ contentRange
|
||||||
, ( "Content-Location"
|
, ( "Content-Location"
|
||||||
@@ -78,21 +78,20 @@ actionResponse (DbCrudResult WrappedReadPlan{wrMedia, wrHdrsOnly=headersOnly, cr
|
|||||||
<> if BS.null (qsCanonical iQueryParams) then mempty else "?" <> qsCanonical iQueryParams
|
<> if BS.null (qsCanonical iQueryParams) then mempty else "?" <> qsCanonical iQueryParams
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
++ cLHeader
|
|
||||||
++ contentTypeHeaders wrMedia ctxApiRequest
|
++ contentTypeHeaders wrMedia ctxApiRequest
|
||||||
++ prefHeader
|
++ prefHeader
|
||||||
|
|
||||||
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers
|
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers
|
||||||
|
|
||||||
let bod | status == HTTP.status416 = Error.errorPayload $ Error.ApiRequestError $ Error.InvalidRange $
|
let bod | status == HTTP.status416 = Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange $
|
||||||
Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
||||||
| headersOnly = mempty
|
| headersOnly = mempty
|
||||||
| otherwise = LBS.fromStrict rsBody
|
| otherwise = LBS.fromStrict rsBody
|
||||||
|
|
||||||
Right $ PgrstResponse ovStatus ovHeaders bod
|
Right $ PgrstResponse ovStatus ovHeaders bod
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders wrMedia ctxApiRequest) $ LBS.fromStrict plan
|
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders wrMedia ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutatePlan, mrMedia, crudQi=QualifiedIdentifier{..}} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}, ..} _ _ _ _ _ = case resultSet of
|
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutatePlan, mrMedia, crudQi=QualifiedIdentifier{..}} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}, ..} _ _ _ _ _ = case resultSet of
|
||||||
RSStandard{..} -> do
|
RSStandard{..} -> do
|
||||||
@@ -100,7 +99,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
|
|||||||
pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;}
|
pkCols = case mrMutatePlan of { Insert{insPkCols} -> insPkCols; _ -> mempty;}
|
||||||
prefHeader = prefAppliedHeader $
|
prefHeader = prefAppliedHeader $
|
||||||
Preferences (if null pkCols && isNothing (qsOnConflict iQueryParams) then Nothing else preferResolution)
|
Preferences (if null pkCols && isNothing (qsOnConflict iQueryParams) then Nothing else preferResolution)
|
||||||
preferRepresentation preferCount preferTransaction preferMissing preferHandling preferTimezone Nothing []
|
preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone Nothing []
|
||||||
headers =
|
headers =
|
||||||
catMaybes
|
catMaybes
|
||||||
[ if null rsLocation then
|
[ if null rsLocation then
|
||||||
@@ -114,7 +113,6 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
|
|||||||
)
|
)
|
||||||
, Just . RangeQuery.contentRangeH 1 0 $
|
, Just . RangeQuery.contentRangeH 1 0 $
|
||||||
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
||||||
, Just $ contentLengthHeaderStrict rsBody
|
|
||||||
, prefHeader ]
|
, prefHeader ]
|
||||||
|
|
||||||
let isInsertIfGTZero i =
|
let isInsertIfGTZero i =
|
||||||
@@ -133,7 +131,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationCreate, mrMutateP
|
|||||||
|
|
||||||
Right $ PgrstResponse ovStatus ovHeaders bod
|
Right $ PgrstResponse ovStatus ovHeaders bod
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
|
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
|
||||||
RSStandard{..} -> do
|
RSStandard{..} -> do
|
||||||
@@ -141,12 +139,12 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia}
|
|||||||
contentRangeHeader =
|
contentRangeHeader =
|
||||||
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
|
Just . RangeQuery.contentRangeH 0 (rsQueryTotal - 1) $
|
||||||
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
||||||
prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction preferMissing preferHandling preferTimezone preferMaxAffected []
|
prefHeader = prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction preferMissing preferHandling preferTimezone preferMaxAffected []
|
||||||
headers = catMaybes [contentRangeHeader, prefHeader]
|
headers = catMaybes [contentRangeHeader, prefHeader]
|
||||||
|
|
||||||
let (status, headers', body) =
|
let (status, headers', body) =
|
||||||
case preferRepresentation of
|
case preferRepresentation of
|
||||||
Just Full -> (HTTP.status200, headers ++ [contentLengthHeaderStrict rsBody] ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
|
Just Full -> (HTTP.status200, headers ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
|
||||||
Just None -> (HTTP.status204, headers, mempty)
|
Just None -> (HTTP.status204, headers, mempty)
|
||||||
_ -> (HTTP.status204, headers, mempty)
|
_ -> (HTTP.status204, headers, mempty)
|
||||||
|
|
||||||
@@ -155,20 +153,19 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationUpdate, mrMedia}
|
|||||||
Right $ PgrstResponse ovStatus ovHeaders body
|
Right $ PgrstResponse ovStatus ovHeaders body
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
|
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
|
||||||
RSStandard {..} -> do
|
RSStandard {..} -> do
|
||||||
let
|
let
|
||||||
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
|
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone Nothing []
|
||||||
cLHeader = [contentLengthHeaderStrict rsBody]
|
|
||||||
cTHeader = contentTypeHeaders mrMedia ctxApiRequest
|
cTHeader = contentTypeHeaders mrMedia ctxApiRequest
|
||||||
|
|
||||||
let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200
|
let isInsertIfGTZero i = if i > 0 then HTTP.status201 else HTTP.status200
|
||||||
upsertStatus = isInsertIfGTZero $ fromJust rsInserted
|
upsertStatus = isInsertIfGTZero $ fromJust rsInserted
|
||||||
(status, headers, body) =
|
(status, headers, body) =
|
||||||
case preferRepresentation of
|
case preferRepresentation of
|
||||||
Just Full -> (upsertStatus, cLHeader ++ cTHeader ++ prefHeader, LBS.fromStrict rsBody)
|
Just Full -> (upsertStatus, cTHeader ++ prefHeader, LBS.fromStrict rsBody)
|
||||||
Just None -> (HTTP.status204, prefHeader, mempty)
|
Just None -> (HTTP.status204, prefHeader, mempty)
|
||||||
_ -> (HTTP.status204, prefHeader, mempty)
|
_ -> (HTTP.status204, prefHeader, mempty)
|
||||||
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers
|
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status headers
|
||||||
@@ -176,7 +173,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationSingleUpsert, mrM
|
|||||||
Right $ PgrstResponse ovStatus ovHeaders body
|
Right $ PgrstResponse ovStatus ovHeaders body
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
|
actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..}} _ _ _ _ _ = case resultSet of
|
||||||
RSStandard {..} -> do
|
RSStandard {..} -> do
|
||||||
@@ -184,12 +181,12 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia}
|
|||||||
contentRangeHeader =
|
contentRangeHeader =
|
||||||
RangeQuery.contentRangeH 1 0 $
|
RangeQuery.contentRangeH 1 0 $
|
||||||
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
if shouldCount preferCount then Just rsQueryTotal else Nothing
|
||||||
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
|
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing preferRepresentation Nothing preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
|
||||||
headers = contentRangeHeader : prefHeader
|
headers = contentRangeHeader : prefHeader
|
||||||
|
|
||||||
let (status, headers', body) =
|
let (status, headers', body) =
|
||||||
case preferRepresentation of
|
case preferRepresentation of
|
||||||
Just Full -> (HTTP.status200, headers ++ [contentLengthHeaderStrict rsBody] ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
|
Just Full -> (HTTP.status200, headers ++ contentTypeHeaders mrMedia ctxApiRequest, LBS.fromStrict rsBody)
|
||||||
Just None -> (HTTP.status204, headers, mempty)
|
Just None -> (HTTP.status204, headers, mempty)
|
||||||
_ -> (HTTP.status204, headers, mempty)
|
_ -> (HTTP.status204, headers, mempty)
|
||||||
|
|
||||||
@@ -198,7 +195,7 @@ actionResponse (DbCrudResult MutateReadPlan{mrMutation=MutationDelete, mrMedia}
|
|||||||
Right $ PgrstResponse ovStatus ovHeaders body
|
Right $ PgrstResponse ovStatus ovHeaders body
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders mrMedia ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=proc} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..},..} _ _ _ _ _ = case resultSet of
|
actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=proc} resultSet) ctxApiRequest@ApiRequest{iPreferences=Preferences{..},..} _ _ _ _ _ = case resultSet of
|
||||||
RSStandard {..} -> do
|
RSStandard {..} -> do
|
||||||
@@ -206,12 +203,10 @@ actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=p
|
|||||||
(status, contentRange) =
|
(status, contentRange) =
|
||||||
RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
RangeQuery.rangeStatusHeader iTopLevelRange rsQueryTotal rsTableTotal
|
||||||
rsOrErrBody = if status == HTTP.status416
|
rsOrErrBody = if status == HTTP.status416
|
||||||
then Error.errorPayload $ Error.ApiRequestError $ Error.InvalidRange
|
then Error.errorPayload $ Error.ApiRequestError $ ApiRequestTypes.InvalidRange
|
||||||
$ Error.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
$ ApiRequestTypes.OutOfBounds (show $ RangeQuery.rangeOffset iTopLevelRange) (maybe "0" show rsTableTotal)
|
||||||
else LBS.fromStrict rsBody
|
else LBS.fromStrict rsBody
|
||||||
isHeadMethod = invMethod == InvRead True
|
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferParameters preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
|
||||||
prefHeader = maybeToList . prefAppliedHeader $ Preferences Nothing Nothing preferCount preferTransaction Nothing preferHandling preferTimezone preferMaxAffected []
|
|
||||||
cLHeader = if isHeadMethod then mempty else [contentLengthHeaderLazy rsOrErrBody]
|
|
||||||
headers = contentRange : prefHeader
|
headers = contentRange : prefHeader
|
||||||
|
|
||||||
let (status', headers', body) =
|
let (status', headers', body) =
|
||||||
@@ -219,27 +214,25 @@ actionResponse (DbCallResult CallReadPlan{crMedia, crInvMthd=invMethod, crProc=p
|
|||||||
(HTTP.status204, headers, mempty)
|
(HTTP.status204, headers, mempty)
|
||||||
else
|
else
|
||||||
(status,
|
(status,
|
||||||
headers ++ cLHeader ++ contentTypeHeaders crMedia ctxApiRequest,
|
headers ++ contentTypeHeaders crMedia ctxApiRequest,
|
||||||
if isHeadMethod then mempty else rsOrErrBody)
|
if invMethod == InvRead True then mempty else rsOrErrBody)
|
||||||
|
|
||||||
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status' headers'
|
(ovStatus, ovHeaders) <- overrideStatusHeaders rsGucStatus rsGucHeaders status' headers'
|
||||||
|
|
||||||
Right $ PgrstResponse ovStatus ovHeaders body
|
Right $ PgrstResponse ovStatus ovHeaders body
|
||||||
|
|
||||||
RSPlan plan ->
|
RSPlan plan ->
|
||||||
Right $ PgrstResponse HTTP.status200 (contentLengthHeaderStrict plan : contentTypeHeaders crMedia ctxApiRequest) $ LBS.fromStrict plan
|
Right $ PgrstResponse HTTP.status200 (contentTypeHeaders crMedia ctxApiRequest) $ LBS.fromStrict plan
|
||||||
|
|
||||||
actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versions conf sCache schema negotiatedByProfile =
|
actionResponse (MaybeDbResult InspectPlan{ipHdrsOnly=headersOnly} body) _ versions conf sCache schema negotiatedByProfile =
|
||||||
let
|
Right $ PgrstResponse HTTP.status200
|
||||||
rsBody = maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body
|
(MediaType.toContentType MTOpenAPI : maybeToList (profileHeader schema negotiatedByProfile))
|
||||||
cLHeader = if headersOnly then mempty else [contentLengthHeaderLazy rsBody]
|
(maybe mempty (\(x, y, z) -> if headersOnly then mempty else OpenAPI.encode versions conf sCache x y z) body)
|
||||||
in
|
|
||||||
Right $ PgrstResponse HTTP.status200 (MediaType.toContentType MTOpenAPI : cLHeader ++ maybeToList (profileHeader schema negotiatedByProfile)) rsBody
|
|
||||||
|
|
||||||
actionResponse (NoDbResult (RelInfoPlan qi@QualifiedIdentifier{..})) _ _ _ SchemaCache{dbTables} _ _ =
|
actionResponse (NoDbResult (RelInfoPlan identifier)) _ _ _ sCache _ _ =
|
||||||
case HM.lookup qi dbTables of
|
case HM.lookup identifier (dbTables sCache) of
|
||||||
Just tbl -> respondInfo $ allowH tbl
|
Just tbl -> respondInfo $ allowH tbl
|
||||||
Nothing -> Left $ Error.SchemaCacheErr $ Error.TableNotFound qiSchema qiName (HM.elems dbTables)
|
Nothing -> Left $ Error.ApiRequestError ApiRequestTypes.NotFound
|
||||||
where
|
where
|
||||||
allowH table =
|
allowH table =
|
||||||
let hasPK = not . null $ tablePKCols table in
|
let hasPK = not . null $ tablePKCols table in
|
||||||
@@ -259,7 +252,7 @@ actionResponse (NoDbResult SchemaInfoPlan) _ _ _ _ _ _ = respondInfo "OPTIONS,GE
|
|||||||
respondInfo :: ByteString -> Either Error.Error PgrstResponse
|
respondInfo :: ByteString -> Either Error.Error PgrstResponse
|
||||||
respondInfo allowHeader =
|
respondInfo allowHeader =
|
||||||
let allOrigins = ("Access-Control-Allow-Origin", "*") in
|
let allOrigins = ("Access-Control-Allow-Origin", "*") in
|
||||||
Right $ PgrstResponse HTTP.status200 [contentLengthHeaderStrict mempty, allOrigins, (HTTP.hAllow, allowHeader)] mempty
|
Right $ PgrstResponse HTTP.status200 [allOrigins, (HTTP.hAllow, allowHeader)] mempty
|
||||||
|
|
||||||
-- Status and headers can be overridden as per https://postgrest.org/en/stable/references/transactions.html#response-headers
|
-- Status and headers can be overridden as per https://postgrest.org/en/stable/references/transactions.html#response-headers
|
||||||
overrideStatusHeaders :: Maybe Text -> Maybe BS.ByteString -> HTTP.Status -> [HTTP.Header]-> Either Error.Error (HTTP.Status, [HTTP.Header])
|
overrideStatusHeaders :: Maybe Text -> Maybe BS.ByteString -> HTTP.Status -> [HTTP.Header]-> Either Error.Error (HTTP.Status, [HTTP.Header])
|
||||||
@@ -270,20 +263,11 @@ overrideStatusHeaders rsGucStatus rsGucHeaders pgrstStatus pgrstHeaders = do
|
|||||||
|
|
||||||
decodeGucHeaders :: Maybe BS.ByteString -> Either Error.Error [GucHeader]
|
decodeGucHeaders :: Maybe BS.ByteString -> Either Error.Error [GucHeader]
|
||||||
decodeGucHeaders =
|
decodeGucHeaders =
|
||||||
maybe (Right []) $ first (const . Error.ApiRequestError $ Error.GucHeadersError) . JSON.eitherDecode . LBS.fromStrict
|
maybe (Right []) $ first (const . Error.ApiRequestError $ ApiRequestTypes.GucHeadersError) . JSON.eitherDecode . LBS.fromStrict
|
||||||
|
|
||||||
decodeGucStatus :: Maybe Text -> Either Error.Error (Maybe HTTP.Status)
|
decodeGucStatus :: Maybe Text -> Either Error.Error (Maybe HTTP.Status)
|
||||||
decodeGucStatus =
|
decodeGucStatus =
|
||||||
maybe (Right Nothing) $ first (const . Error.ApiRequestError $ Error.GucStatusError) . fmap (Just . toEnum . fst) . decimal
|
maybe (Right Nothing) $ first (const . Error.ApiRequestError $ ApiRequestTypes.GucStatusError) . fmap (Just . toEnum . fst) . decimal
|
||||||
|
|
||||||
contentLengthHeader :: Show b => (a -> b) -> a -> HTTP.Header
|
|
||||||
contentLengthHeader lenFn body = ("Content-Length", show (lenFn body))
|
|
||||||
|
|
||||||
contentLengthHeaderStrict :: BS.ByteString -> HTTP.Header
|
|
||||||
contentLengthHeaderStrict = contentLengthHeader BS.length
|
|
||||||
|
|
||||||
contentLengthHeaderLazy :: LBS.ByteString -> HTTP.Header
|
|
||||||
contentLengthHeaderLazy = contentLengthHeader LBS.length
|
|
||||||
|
|
||||||
contentTypeHeaders :: MediaType -> ApiRequest -> [HTTP.Header]
|
contentTypeHeaders :: MediaType -> ApiRequest -> [HTTP.Header]
|
||||||
contentTypeHeaders mediaType ApiRequest{..} =
|
contentTypeHeaders mediaType ApiRequest{..} =
|
||||||
|
|||||||