Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e732ac94a | ||
|
|
6b2778749f | ||
|
|
36f86827ee | ||
|
|
d78410473e | ||
|
|
501edc718d | ||
|
|
0d6d112b38 | ||
|
|
473ac70789 | ||
|
|
dadfe965b9 | ||
|
|
63ead89470 | ||
|
|
ab23ed7999 | ||
|
|
2da6bd6d1c | ||
|
|
5bfb68b982 | ||
|
|
dc834572d6 | ||
|
|
b48824bddd | ||
|
|
6d326fe341 | ||
|
|
d94cf2ed72 | ||
|
|
27ca6b4e90 | ||
|
|
e0cc4d1571 | ||
|
|
3cef4b70b0 | ||
|
|
6f97c34a86 | ||
|
|
bdac90491d | ||
|
|
5961f7a116 | ||
|
|
17cd2725fd | ||
|
|
5e7606134a | ||
|
|
dfa9055c34 | ||
|
|
6907e7f979 | ||
|
|
8cf68c63d9 | ||
|
|
30b5859b28 | ||
|
|
0a1d83ce8f | ||
|
|
d7511a2637 | ||
|
|
2066220244 | ||
|
|
93f10adb3c | ||
|
|
56bd5d5f91 | ||
|
|
70e95649fd | ||
|
|
2b46afe1ec | ||
|
|
fa1e92fdf2 | ||
|
|
b1a8bd2391 | ||
|
|
69a76a627f | ||
|
|
105671e51a | ||
|
|
ecf0e9213f | ||
|
|
1c6ded16d1 | ||
|
|
6fc9d5191a | ||
|
|
80f09780cc | ||
|
|
9e3454129f | ||
|
|
d34afe861a | ||
|
|
30dfadec7b | ||
|
|
2513c00039 | ||
|
|
100bf494ac | ||
|
|
e8188b0d41 | ||
|
|
3958ebbb05 | ||
|
|
37e7398a85 | ||
|
|
9ea7529f30 | ||
|
|
384767708b | ||
|
|
6bcbb124d2 | ||
|
|
f6c1ff810e | ||
|
|
f80cfbf165 | ||
|
|
d8896be2c1 |
+46
-6
@@ -52,7 +52,7 @@ build-distro-bin: &build-distro-bin
|
||||
key: v1-{{ .Environment.CIRCLE_JOB }}-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
||||
|
||||
jobs:
|
||||
build-test:
|
||||
build-test-9.4:
|
||||
docker:
|
||||
- image: circleci/buildpack-deps:trusty
|
||||
environment:
|
||||
@@ -138,6 +138,38 @@ jobs:
|
||||
name: run tests
|
||||
command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test
|
||||
|
||||
build-test-10:
|
||||
docker:
|
||||
- image: circleci/buildpack-deps:trusty
|
||||
environment:
|
||||
- PGHOST=localhost
|
||||
- image: circleci/postgres:10.5
|
||||
environment:
|
||||
- POSTGRES_USER=circleci
|
||||
- POSTGRES_DB=circleci
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
|
||||
- run:
|
||||
name: install stack & dependencies
|
||||
command: |
|
||||
curl -L https://github.com/commercialhaskell/stack/releases/download/v1.1.2/stack-1.1.2-linux-x86_64.tar.gz | tar zx -C /tmp
|
||||
sudo mv /tmp/stack-1.1.2-linux-x86_64/stack /usr/bin
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgmp-dev
|
||||
sudo apt-get install -y postgresql-client
|
||||
stack setup
|
||||
- run:
|
||||
name: build src and tests
|
||||
command: |
|
||||
stack build --fast -j1
|
||||
stack build --fast --test --no-run-tests
|
||||
- run:
|
||||
name: run tests
|
||||
command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test
|
||||
|
||||
build-prof-test:
|
||||
docker:
|
||||
- image: circleci/buildpack-deps:trusty
|
||||
@@ -224,7 +256,7 @@ workflows:
|
||||
version: 2
|
||||
build-test-release:
|
||||
jobs:
|
||||
- build-test:
|
||||
- build-test-9.4:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
@@ -232,14 +264,19 @@ workflows:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
- build-test-10:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
- build-prof-test:
|
||||
filters:
|
||||
tags:
|
||||
only: /v[0-9]+(\.[0-9]+)*/
|
||||
- centos6:
|
||||
requires:
|
||||
- build-test
|
||||
- build-test-9.4
|
||||
- build-test-9.6
|
||||
- build-test-10
|
||||
- build-prof-test
|
||||
filters:
|
||||
tags:
|
||||
@@ -248,8 +285,9 @@ workflows:
|
||||
ignore: /.*/
|
||||
- centos7:
|
||||
requires:
|
||||
- build-test
|
||||
- build-test-9.4
|
||||
- build-test-9.6
|
||||
- build-test-10
|
||||
- build-prof-test
|
||||
filters:
|
||||
tags:
|
||||
@@ -258,8 +296,9 @@ workflows:
|
||||
ignore: /.*/
|
||||
- ubuntu:
|
||||
requires:
|
||||
- build-test
|
||||
- build-test-9.4
|
||||
- build-test-9.6
|
||||
- build-test-10
|
||||
- build-prof-test
|
||||
filters:
|
||||
tags:
|
||||
@@ -268,8 +307,9 @@ workflows:
|
||||
ignore: /.*/
|
||||
- ubuntui386:
|
||||
requires:
|
||||
- build-test
|
||||
- build-test-9.4
|
||||
- build-test-9.6
|
||||
- build-test-10
|
||||
- build-prof-test
|
||||
filters:
|
||||
tags:
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
### Environment
|
||||
|
||||
* Version of postgrest:
|
||||
* PostgreSQL version: (if using docker, specify the image)
|
||||
* PostgREST version: (if using docker, specify the image)
|
||||
* Operating system:
|
||||
* Running in docker:
|
||||
* [ ] yes
|
||||
* [ ] no
|
||||
|
||||
### Description of issue
|
||||
|
||||
(Expected behavior vs actual behavior.)
|
||||
(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)
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
# Sponsors & Backers
|
||||
|
||||
PostgREST ongoing development is only possible thanks to our Sponsors and Backers, listed below. If you'd like to join them, you can do so by supporting the PostgREST organization on [Patreon](https://www.patreon.com/postgrest).
|
||||
|
||||
## Lead Backers
|
||||
|
||||
- [Christiaan Westerbeek](https://devotis.nl)
|
||||
- [Daniel Babiak](https://github.com/d-babiak)
|
||||
- [Michel Pelletier](https://github.com/michelp/)
|
||||
|
||||
## Backers
|
||||
|
||||
- Tsingson Qin
|
||||
- Jay Hannah
|
||||
+49
-1
@@ -9,8 +9,54 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
### Fixed
|
||||
|
||||
## [5.2.0] - 2018-12-12
|
||||
|
||||
### Added
|
||||
|
||||
- #1205, Add support for parsing JSON Web Key Sets - @russelldavies
|
||||
- #1203, Add support for reading db-uri from a separate file - @zhoufeng1989
|
||||
- #1200, Add db-extra-search-path config for adding schemas to the search_path, solves issues related to extensions created on the public schema - @steve-chavez
|
||||
- #1219, Add ability to quote column names on filters - @steve-chavez
|
||||
|
||||
### Fixed
|
||||
|
||||
- #1182, Fix embedding on views with composite pks - @steve-chavez
|
||||
- #1180, Fix embedding on views with subselects in pg10 - @steve-chavez
|
||||
- #1197, Allow CORS for PUT - @bkylerussell
|
||||
- #1181, Correctly qualify function argument of custom type in public schema - @steve-chavez
|
||||
- #1008, Allow columns that contain spaces in filters - @steve-chavez
|
||||
|
||||
## [5.1.0] - 2018-08-31
|
||||
|
||||
### Added
|
||||
|
||||
- #1099, Add support for getting json/jsonb by array index - @steve-chavez
|
||||
- #1145, Add materialized view columns to OpenAPI output - @steve-chavez
|
||||
- #709, Allow embedding on views with subselects/CTE - @steve-chavez
|
||||
- #1148, OpenAPI: add `required` section for the non-nullable columns - @laughedelic
|
||||
- #1158, Add summary to OpenAPI doc for RPC functions - @mdr1384
|
||||
|
||||
### Fixed
|
||||
|
||||
- #1113, Fix UPSERT failing when having a camel case PK column - @steve-chavez
|
||||
- #945, Fix slow start-up time on big schemas - @steve-chavez
|
||||
- #1129, Fix view embedding when table is capitalized - @steve-chavez
|
||||
- #1149, OpenAPI: Change `GET` response type to array - @laughedelic
|
||||
- #1152, Fix RPC failing when having arguments with reserved or uppercase keywords - @mdr1384
|
||||
- #905, Fix intermittent empty replies - @steve-chavez
|
||||
- #1139, Fix JWTIssuedAtFuture failure for valid iat claim - @steve-chavez
|
||||
- #1141, Fix app.settings resetting on pool timeout - @steve-chavez
|
||||
|
||||
### Changed
|
||||
|
||||
- #1099, Numbers in json path `?select=data->1->>key` now get treated as json array indexes instead of keys - @steve-chavez
|
||||
- #1128, Allow finishing a json path with a single arrow `->`. Now a json can be obtained without resorting to casting, Previously: `/json_arr?select=data->>2::json`, now: `/json_arr?select=data->2` - @steve-chavez
|
||||
- #724, Change server-host default of *4 to 127.0.0.1
|
||||
|
||||
### Deprecated
|
||||
|
||||
- #724, SIGHUP deprecated, SIGUSR1 should be used instead
|
||||
|
||||
## [0.5.0.0] - 2018-05-14
|
||||
|
||||
### Added
|
||||
@@ -32,7 +78,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
### Changed
|
||||
|
||||
- Computed columns now only work if they belong to the db-schema - @steve-chavez
|
||||
- #828, A `SET SCHEMA <db-schema>` is done on each request, this has the following implications:
|
||||
- Computed columns now only work if they belong to the db-schema
|
||||
- Stored procedures might require a `search_path` to work properly, for further details see https://postgrest.org/en/v5.0/api.html#explicit-qualification
|
||||
- To use RPC now the `json_to_record/json_to_recordset` functions are needed, these are available starting from PostgreSQL 9.4 - @steve-chavez
|
||||
- Overloaded functions now depend on the `dbStructure`, restart/sighup may be needed for their correct functioning - @steve-chavez
|
||||
- #1098, Removed support for:
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||

|
||||
|
||||
[](https://circleci.com/gh/PostgREST/postgrest/tree/master)
|
||||
[](https://www.patreon.com/postgrest)
|
||||
[](https://www.paypal.me/postgrest)
|
||||
<a href="https://heroku.com/deploy?template=https://github.com/PostgREST/postgrest">
|
||||
<img src="https://img.shields.io/badge/%E2%86%91_Deploy_to-Heroku-7056bf.svg" alt="Deploy">
|
||||
</a>
|
||||
[](https://gitter.im/begriffs/postgrest)
|
||||
[](http://postgrest.org)
|
||||
[](https://hub.docker.com/r/postgrest/postgrest/)
|
||||
[](https://www.patreon.com/postgrest)
|
||||
[](https://circleci.com/gh/PostgREST/postgrest/tree/master)
|
||||
[](http://hackage.haskell.org/package/postgrest)
|
||||
|
||||
PostgREST serves a fully RESTful API from any existing PostgreSQL
|
||||
database. It provides a cleaner, more standards-compliant, faster
|
||||
API than you are likely to write from scratch.
|
||||
|
||||
### Usage
|
||||
## Usage
|
||||
|
||||
1. Download the binary ([latest release](https://github.com/PostgREST/postgrest/releases/latest))
|
||||
for your platform.
|
||||
@@ -23,7 +25,7 @@ API than you are likely to write from scratch.
|
||||
postgrest --help
|
||||
```
|
||||
|
||||
### Performance
|
||||
## Performance
|
||||
|
||||
TLDR; subsecond response times for up to 2000 requests/sec on Heroku
|
||||
free tier. If you're used to servers written in interpreted languages
|
||||
@@ -51,7 +53,7 @@ by
|
||||
* Using the PostgreSQL binary protocol
|
||||
* Being stateless to allow horizontal scaling
|
||||
|
||||
### Security
|
||||
## Security
|
||||
|
||||
PostgREST [handles
|
||||
authentication](http://postgrest.org/en/stable/auth.html) (via JSON Web
|
||||
@@ -72,7 +74,7 @@ are limited to certain templates using
|
||||
functions, the trigger workaround does not compromise row-level
|
||||
security.
|
||||
|
||||
### Versioning
|
||||
## Versioning
|
||||
|
||||
A robust long-lived API needs the freedom to exist in multiple
|
||||
versions. PostgREST does versioning through database schemas. This
|
||||
@@ -80,7 +82,7 @@ allows you to expose tables and views without making the app brittle.
|
||||
Underlying tables can be superseded and hidden behind public facing
|
||||
views.
|
||||
|
||||
### Self-documentation
|
||||
## Self-documentation
|
||||
|
||||
PostgREST uses the [OpenAPI](https://openapis.org/) standard to
|
||||
generate up-to-date documentation for APIs. You can use a tool like
|
||||
@@ -92,7 +94,7 @@ instance the number of rows returned by an endpoint is reported by -
|
||||
and limited with - range headers. More about
|
||||
[that](http://begriffs.com/posts/2014-03-06-beyond-http-header-links.html).
|
||||
|
||||
### Data Integrity
|
||||
## Data Integrity
|
||||
|
||||
Rather than relying on an Object Relational Mapper and custom
|
||||
imperative coding, this system requires you put declarative constraints
|
||||
@@ -106,12 +108,22 @@ See examples of [PostgreSQL
|
||||
constraints](http://www.tutorialspoint.com/postgresql/postgresql_constraints.htm)
|
||||
and the [API guide](http://postgrest.org/en/stable/api.html).
|
||||
|
||||
### Thanks
|
||||
## Supporting development
|
||||
|
||||
The PostgREST organization is grateful to the generous project
|
||||
[contributors](https://github.com/PostgREST/postgrest/graphs/contributors)
|
||||
who have improved PostgREST immensely with their code and good
|
||||
judgement. See more details in the
|
||||
[changelog](https://github.com/PostgREST/postgrest/blob/master/CHANGELOG.md).
|
||||
You can help PostgREST ongoing maintenance and development by:
|
||||
|
||||
- Making a regular donation through Patreon https://www.patreon.com/postgrest
|
||||
|
||||
- Alternatively, you can make a one-time donation via Paypal https://www.paypal.me/postgrest
|
||||
|
||||
Every donation will be spent on making PostgREST better for the whole community.
|
||||
|
||||
## Thanks
|
||||
|
||||
The PostgREST organization is grateful to:
|
||||
|
||||
- The project [sponsors and backers](https://github.com/PostgREST/postgrest/blob/master/BACKERS.md) who support PostgREST's development.
|
||||
- The project [contributors](https://github.com/PostgREST/postgrest/graphs/contributors) who have improved PostgREST immensely with their code
|
||||
and good judgement. See more details in the [changelog](https://github.com/PostgREST/postgrest/blob/master/CHANGELOG.md).
|
||||
|
||||
The cool logo came from [Mikey Casalaina](https://github.com/casalaina).
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "PostgREST",
|
||||
"description": "RESTful API for any PostgreSQL database.",
|
||||
"logo": "https://halcyon.sh/logo.svg",
|
||||
"logo": "https://avatars2.githubusercontent.com/u/15115011",
|
||||
"repository": "https://github.com/PostgREST/postgrest",
|
||||
"env": {
|
||||
"BUILDPACK_URL": {
|
||||
@@ -10,10 +10,10 @@
|
||||
},
|
||||
"POSTGREST_VER": {
|
||||
"description": "Version of PostgREST to deploy",
|
||||
"value": "0.5.0.0"
|
||||
"value": "5.2.0"
|
||||
},
|
||||
"DB_URI": {
|
||||
"description": "Database connection string",
|
||||
"description": "Database connection string, e.g. postgres://user:pass@xxxxxxx.rds.amazonaws.com/mydb",
|
||||
"required": true
|
||||
},
|
||||
"DB_SCHEMA": {
|
||||
|
||||
+2
-1
@@ -35,7 +35,8 @@ ENV PGRST_DB_URI= \
|
||||
PGRST_SECRET_IS_BASE64=false \
|
||||
PGRST_JWT_AUD= \
|
||||
PGRST_MAX_ROWS= \
|
||||
PGRST_PRE_REQUEST=
|
||||
PGRST_PRE_REQUEST= \
|
||||
PGRST_ROLE_CLAIM_KEY=".role"
|
||||
|
||||
# PostgREST reads /etc/postgrest.conf so map the configuration
|
||||
# file in when you run this container
|
||||
|
||||
@@ -10,6 +10,7 @@ server-proxy-uri = "$(PGRST_SERVER_PROXY_URI)"
|
||||
jwt-secret = "$(PGRST_JWT_SECRET)"
|
||||
secret-is-base64 = "$(PGRST_SECRET_IS_BASE64)"
|
||||
jwt-aud = "$(PGRST_JWT_AUD)"
|
||||
role-claim-key = "$(PGRST_ROLE_CLAIM_KEY)"
|
||||
|
||||
max-rows = "$(PGRST_MAX_ROWS)"
|
||||
pre-request = "$(PGRST_PRE_REQUEST)"
|
||||
|
||||
+58
-50
@@ -3,40 +3,38 @@
|
||||
module Main where
|
||||
|
||||
|
||||
import PostgREST.App (postgrest)
|
||||
import PostgREST.Config (AppConfig (..),
|
||||
minimumPgVersion,
|
||||
prettyVersion, readOptions)
|
||||
import PostgREST.DbStructure (getDbStructure, getPgVersion,
|
||||
fillSessionWithSettings)
|
||||
import PostgREST.Error (encodeError)
|
||||
import PostgREST.OpenAPI (isMalformedProxyUri)
|
||||
import PostgREST.Types (DbStructure, Schema, PgVersion(..))
|
||||
import Protolude hiding (hPutStrLn, replace)
|
||||
import PostgREST.App (postgrest)
|
||||
import PostgREST.Config (AppConfig (..),
|
||||
prettyVersion, readOptions)
|
||||
import PostgREST.DbStructure (getDbStructure, getPgVersion)
|
||||
import PostgREST.Error (encodeError)
|
||||
import PostgREST.OpenAPI (isMalformedProxyUri)
|
||||
import PostgREST.Types (DbStructure, Schema, PgVersion(..), minimumPgVersion)
|
||||
import Protolude hiding (hPutStrLn, replace)
|
||||
|
||||
|
||||
import Control.AutoUpdate (defaultUpdateSettings,
|
||||
mkAutoUpdate, updateAction)
|
||||
import Control.Retry (RetryStatus, capDelay,
|
||||
exponentialBackoff,
|
||||
retrying, rsPreviousDelay)
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import Data.IORef (IORef, atomicWriteIORef,
|
||||
newIORef, readIORef)
|
||||
import Data.String (IsString (..))
|
||||
import Data.Text (pack, replace, stripPrefix, strip)
|
||||
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
|
||||
import Data.Text.IO (hPutStrLn)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import qualified Hasql.Pool as P
|
||||
import qualified Hasql.Session as H
|
||||
import Network.Wai.Handler.Warp (defaultSettings,
|
||||
runSettings, setHost,
|
||||
setPort, setServerName,
|
||||
setTimeout)
|
||||
import System.IO (BufferMode (..),
|
||||
hSetBuffering)
|
||||
import Control.AutoUpdate (defaultUpdateSettings,
|
||||
mkAutoUpdate, updateAction)
|
||||
import Control.Retry (RetryStatus, capDelay,
|
||||
exponentialBackoff,
|
||||
retrying, rsPreviousDelay)
|
||||
import qualified Data.ByteString as BS
|
||||
import qualified Data.ByteString.Base64 as B64
|
||||
import Data.IORef (IORef, atomicWriteIORef,
|
||||
newIORef, readIORef)
|
||||
import Data.String (IsString (..))
|
||||
import Data.Text (pack, replace, stripPrefix, strip)
|
||||
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
|
||||
import Data.Text.IO (hPutStrLn, readFile)
|
||||
import Data.Time.Clock (getCurrentTime)
|
||||
import qualified Hasql.Pool as P
|
||||
import qualified Hasql.Session as H
|
||||
import qualified Hasql.Transaction.Sessions as HT
|
||||
import Network.Wai.Handler.Warp (defaultSettings,
|
||||
runSettings, setHost,
|
||||
setPort, setServerName)
|
||||
import System.IO (BufferMode (..),
|
||||
hSetBuffering)
|
||||
|
||||
#ifndef mingw32_HOST_OS
|
||||
import System.Posix.Signals
|
||||
@@ -64,11 +62,10 @@ connectionWorker
|
||||
:: ThreadId -- ^ This thread is killed if pg version is unsupported
|
||||
-> P.Pool -- ^ The PostgreSQL connection pool
|
||||
-> Schema -- ^ Schema PostgREST is serving up
|
||||
-> [(Text, Text)] -- ^ Settings or Environment passed in through the config
|
||||
-> IORef (Maybe DbStructure) -- ^ mutable reference to 'DbStructure'
|
||||
-> IORef Bool -- ^ Used as a binary Semaphore
|
||||
-> IO ()
|
||||
connectionWorker mainTid pool schema settings refDbStructure refIsWorkerOn = do
|
||||
connectionWorker mainTid pool schema refDbStructure refIsWorkerOn = do
|
||||
isWorkerOn <- readIORef refIsWorkerOn
|
||||
unless isWorkerOn $ do
|
||||
atomicWriteIORef refIsWorkerOn True
|
||||
@@ -86,8 +83,7 @@ connectionWorker mainTid pool schema settings refDbStructure refIsWorkerOn = do
|
||||
("Cannot run in this PostgreSQL version, PostgREST needs at least "
|
||||
<> pgvName minimumPgVersion)
|
||||
killThread mainTid
|
||||
fillSessionWithSettings settings
|
||||
dbStructure <- getDbStructure schema actualPgVersion
|
||||
dbStructure <- HT.transaction HT.ReadCommitted HT.Read $ getDbStructure schema actualPgVersion
|
||||
liftIO $ atomicWriteIORef refDbStructure $ Just dbStructure
|
||||
case result of
|
||||
Left e -> do
|
||||
@@ -143,7 +139,7 @@ main = do
|
||||
--
|
||||
-- readOptions builds the 'AppConfig' from the config file specified on the
|
||||
-- command line
|
||||
conf <- loadSecretFile =<< readOptions
|
||||
conf <- loadDbUriFile =<< loadSecretFile =<< readOptions
|
||||
let host = configHost conf
|
||||
port = configPort conf
|
||||
proxy = configProxyUri conf
|
||||
@@ -152,8 +148,7 @@ main = do
|
||||
appSettings =
|
||||
setHost ((fromString . toS) host) -- Warp settings
|
||||
. setPort port
|
||||
. setServerName (toS $ "postgrest/" <> prettyVersion)
|
||||
. setTimeout 3600 $
|
||||
. setServerName (toS $ "postgrest/" <> prettyVersion) $
|
||||
defaultSettings
|
||||
|
||||
-- Checks that the provided proxy uri is formated correctly
|
||||
@@ -186,7 +181,6 @@ main = do
|
||||
mainTid
|
||||
pool
|
||||
(configSchema conf)
|
||||
(configSettings conf)
|
||||
refDbStructure
|
||||
refIsWorkerOn
|
||||
--
|
||||
@@ -204,15 +198,15 @@ main = do
|
||||
throwTo mainTid UserInterrupt
|
||||
) Nothing
|
||||
|
||||
void $ installHandler sigHUP (
|
||||
Catch $ connectionWorker
|
||||
mainTid
|
||||
pool
|
||||
(configSchema conf)
|
||||
(configSettings conf)
|
||||
refDbStructure
|
||||
refIsWorkerOn
|
||||
) Nothing
|
||||
forM_ [sigHUP, sigUSR1] $ \sig ->
|
||||
void $ installHandler sig (
|
||||
Catch $ connectionWorker
|
||||
mainTid
|
||||
pool
|
||||
(configSchema conf)
|
||||
refDbStructure
|
||||
refIsWorkerOn
|
||||
) Nothing
|
||||
#endif
|
||||
|
||||
|
||||
@@ -230,7 +224,6 @@ main = do
|
||||
mainTid
|
||||
pool
|
||||
(configSchema conf)
|
||||
(configSettings conf)
|
||||
refDbStructure
|
||||
refIsWorkerOn)
|
||||
|
||||
@@ -285,3 +278,18 @@ loadSecretFile conf = extractAndTransform mSecret
|
||||
-- replace: Replace every occurrence of one substring with another
|
||||
replaceUrlChars =
|
||||
replace "_" "/" . replace "-" "+" . replace "." "="
|
||||
|
||||
{-
|
||||
Load database uri from a separate file if `db-uri` is a filepath.
|
||||
-}
|
||||
loadDbUriFile :: AppConfig -> IO AppConfig
|
||||
loadDbUriFile conf = extractDbUri mDbUri
|
||||
where
|
||||
mDbUri = configDatabase conf
|
||||
extractDbUri :: Text -> IO AppConfig
|
||||
extractDbUri dbUri =
|
||||
fmap setDbUri $
|
||||
case stripPrefix "@" dbUri of
|
||||
Nothing -> return dbUri
|
||||
Just filename -> strip <$> readFile (toS filename)
|
||||
setDbUri dbUri = conf {configDatabase = dbUri}
|
||||
|
||||
+25
-19
@@ -2,14 +2,16 @@ name: postgrest
|
||||
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||
for the tables and views, supporting all HTTP verbs that security
|
||||
permits.
|
||||
version: 0.5.0.0
|
||||
version: 5.2.0
|
||||
synopsis: REST API for any Postgres database
|
||||
license: MIT
|
||||
license-file: LICENSE
|
||||
author: Joe Nelson, Adam Baker
|
||||
homepage: https://github.com/PostgREST/postgrest
|
||||
maintainer: cred+github@begriffs.com
|
||||
category: Web
|
||||
homepage: https://postgrest.org
|
||||
maintainer: Steve Chávez <stevechavezast@gmail.com>
|
||||
bug-reports: https://github.com/PostgREST/postgrest/issues
|
||||
category: Executable, PostgreSQL, Network APIs
|
||||
extra-source-files: CHANGELOG.md
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
source-repository head
|
||||
@@ -30,11 +32,12 @@ executable postgrest
|
||||
"-with-rtsopts=-N -I2"
|
||||
default-language: Haskell2010
|
||||
build-depends: auto-update
|
||||
, base
|
||||
, hasql
|
||||
, hasql-pool
|
||||
, base >= 4.8 && < 4.10
|
||||
, hasql >= 1.3 && < 1.4
|
||||
, hasql-pool >= 0.5 && < 0.6
|
||||
, hasql-transaction >= 0.7 && < 0.8
|
||||
, postgrest
|
||||
, protolude
|
||||
, protolude == 0.2.2
|
||||
, text
|
||||
, time
|
||||
, warp
|
||||
@@ -51,7 +54,7 @@ library
|
||||
default-extensions: OverloadedStrings, QuasiQuotes, NoImplicitPrelude
|
||||
build-depends: aeson
|
||||
, ansi-wl-pprint
|
||||
, base >= 4.8 && < 6
|
||||
, base >= 4.8 && < 4.10
|
||||
, base64-bytestring
|
||||
, bytestring
|
||||
, case-insensitive
|
||||
@@ -62,24 +65,23 @@ library
|
||||
, contravariant-extras
|
||||
, either
|
||||
, gitrev
|
||||
, hasql
|
||||
, hasql-pool
|
||||
, hasql-transaction
|
||||
, hasql >= 1.3 && < 1.4
|
||||
, hasql-pool >= 0.5 && < 0.6
|
||||
, hasql-transaction >= 0.7 && < 0.8
|
||||
, heredoc
|
||||
, HTTP
|
||||
, http-types
|
||||
, insert-ordered-containers
|
||||
, interpolatedstring-perl6
|
||||
, jose
|
||||
, jose == 0.7.0.0
|
||||
, lens
|
||||
, lens-aeson
|
||||
, network-uri
|
||||
, optparse-applicative >= 0.13 && < 0.15
|
||||
, parsec
|
||||
, protolude >= 0.2
|
||||
, protolude == 0.2.2
|
||||
, Ranged-sets == 0.3.0
|
||||
, regex-tdfa
|
||||
, safe
|
||||
, scientific
|
||||
, swagger2
|
||||
, text
|
||||
@@ -122,8 +124,11 @@ Test-Suite spec
|
||||
, Feature.ConcurrentSpec
|
||||
, Feature.CorsSpec
|
||||
, Feature.DeleteSpec
|
||||
, Feature.ExtraSearchPathSpec
|
||||
, Feature.InsertSpec
|
||||
, Feature.JsonOperatorSpec
|
||||
, Feature.NoJwtSpec
|
||||
, Feature.PgVersion95Spec
|
||||
, Feature.PgVersion96Spec
|
||||
, Feature.ProxySpec
|
||||
, Feature.QueryLimitedSpec
|
||||
@@ -142,15 +147,16 @@ Test-Suite spec
|
||||
, aeson-qq
|
||||
, async
|
||||
, auto-update
|
||||
, base
|
||||
, base >= 4.8 && < 4.10
|
||||
, bytestring
|
||||
, base64-bytestring
|
||||
, case-insensitive
|
||||
, cassava
|
||||
, containers
|
||||
, contravariant
|
||||
, hasql
|
||||
, hasql-pool
|
||||
, hasql >= 1.3 && < 1.4
|
||||
, hasql-pool >= 0.5 && < 0.6
|
||||
, hasql-transaction >= 0.7 && < 0.8
|
||||
, heredoc
|
||||
, hjsonschema == 1.5.0.1
|
||||
, hspec
|
||||
@@ -162,7 +168,7 @@ Test-Suite spec
|
||||
, monad-control
|
||||
, postgrest
|
||||
, process
|
||||
, protolude
|
||||
, protolude == 0.2.2
|
||||
, regex-tdfa
|
||||
, time
|
||||
, transformers-base
|
||||
|
||||
@@ -105,13 +105,12 @@ userApiRequest :: Schema -> Request -> RequestBody -> Either ApiRequestError Api
|
||||
userApiRequest schema req reqBody
|
||||
| isTargetingProc && method `notElem` ["GET", "POST"] = Left ActionInappropriate
|
||||
| topLevelRange == emptyRange = Left InvalidRange
|
||||
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) undefined payload
|
||||
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) witness payload
|
||||
| otherwise = Right ApiRequest {
|
||||
iAction = action
|
||||
, iTarget = target
|
||||
, iRange = ranges
|
||||
, iAccepts = fromMaybe [CTAny] $
|
||||
map decodeContentType . parseHttpAccept <$> lookupHeader "accept"
|
||||
, iAccepts = maybe [CTAny] (map decodeContentType . parseHttpAccept) $ lookupHeader "accept"
|
||||
, iPayload = relevantPayload
|
||||
, iPreferRepresentation = representation
|
||||
, iPreferSingleObjectParameter = singleObject
|
||||
@@ -130,7 +129,7 @@ userApiRequest schema req reqBody
|
||||
$ rawQueryString req
|
||||
, iJWT = tokenStr
|
||||
, iHeaders = [ (toS $ CI.foldedCase k, toS v) | (k,v) <- hdrs, k /= hAuthorization, k /= hCookie]
|
||||
, iCookies = fromMaybe [] $ parseCookiesText <$> lookupHeader "Cookie"
|
||||
, iCookies = maybe [] parseCookiesText $ lookupHeader "Cookie"
|
||||
}
|
||||
where
|
||||
-- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
|
||||
@@ -143,7 +142,7 @@ userApiRequest schema req reqBody
|
||||
((<> ".") <$> "not":M.keys operators) ++
|
||||
((<> "(") <$> M.keys ftsOperators)
|
||||
isEmbedPath = T.isInfixOf "."
|
||||
isTargetingProc = fromMaybe False $ (== "rpc") <$> listToMaybe path
|
||||
isTargetingProc = (== Just "rpc") $ listToMaybe path
|
||||
payload =
|
||||
case (decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type", action) of
|
||||
(_, ActionInvoke{isReadOnly=True}) ->
|
||||
@@ -215,7 +214,7 @@ userApiRequest schema req reqBody
|
||||
limitParams :: M.HashMap ByteString NonnegRange
|
||||
limitParams = M.fromList [(toS (replaceLast "limit" k), restrictRange (readMaybe =<< (toS <$> v)) allRange) | (k,v) <- qParams, isJust v, endingIn ["limit"] k]
|
||||
offsetParams :: M.HashMap ByteString NonnegRange
|
||||
offsetParams = M.fromList [(toS (replaceLast "limit" k), fromMaybe allRange (rangeGeq <$> (readMaybe =<< (toS <$> v)))) | (k,v) <- qParams, isJust v, endingIn ["offset"] k]
|
||||
offsetParams = M.fromList [(toS (replaceLast "limit" k), maybe allRange rangeGeq (readMaybe =<< (toS <$> v))) | (k,v) <- qParams, isJust v, endingIn ["offset"] k]
|
||||
|
||||
urlRange = M.unionWith f limitParams offsetParams
|
||||
where
|
||||
|
||||
+14
-15
@@ -35,7 +35,7 @@ import PostgREST.ApiRequest ( ApiRequest(..), ContentType(..)
|
||||
, mutuallyAgreeable
|
||||
, userApiRequest
|
||||
)
|
||||
import PostgREST.Auth (jwtClaims, containsRole, parseJWK)
|
||||
import PostgREST.Auth (jwtClaims, containsRole, parseSecret)
|
||||
import PostgREST.Config (AppConfig (..))
|
||||
import PostgREST.DbStructure
|
||||
import PostgREST.DbRequestBuilder( readRequest
|
||||
@@ -61,12 +61,11 @@ import PostgREST.OpenAPI
|
||||
|
||||
import Data.Function (id)
|
||||
import Protolude hiding (intercalate, Proxy)
|
||||
import Safe (headMay)
|
||||
|
||||
postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO UTCTime -> IO () -> Application
|
||||
postgrest conf refDbStructure pool getTime worker =
|
||||
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle
|
||||
jwtSecret = parseJWK <$> configJwtSecret conf in
|
||||
jwtSecret = parseSecret <$> configJwtSecret conf in
|
||||
|
||||
middle $ \ req respond -> do
|
||||
time <- getTime
|
||||
@@ -103,14 +102,14 @@ findProc qi payloadKeys paramsAsSingleObject allProcs =
|
||||
else payloadKeys `S.isSubsetOf` S.fromList (pgaName <$> pdArgs x))
|
||||
) <$> procs
|
||||
|
||||
transactionMode :: Maybe ProcDescription -> Action -> H.Mode
|
||||
transactionMode :: Maybe ProcDescription -> Action -> HT.Mode
|
||||
transactionMode proc action =
|
||||
case action of
|
||||
ActionRead -> HT.Read
|
||||
ActionInfo -> HT.Read
|
||||
ActionInspect -> HT.Read
|
||||
ActionInvoke{isReadOnly=False} ->
|
||||
let v = fromMaybe Volatile $ pdVolatility <$> proc in
|
||||
let v = maybe Volatile pdVolatility proc in
|
||||
if v == Stable || v == Immutable
|
||||
then HT.Read
|
||||
else HT.Write
|
||||
@@ -132,7 +131,7 @@ app dbStructure proc conf apiRequest =
|
||||
Right ((q, cq), bField) -> do
|
||||
let stm = createReadStatement q cq (contentType == CTSingularJSON) shouldCount
|
||||
(contentType == CTTextCSV) bField
|
||||
row <- H.query () stm
|
||||
row <- H.statement () stm
|
||||
let (tableTotal, queryTotal, _ , body) = row
|
||||
(status, contentRange) = rangeHeader queryTotal tableTotal
|
||||
canonical = iCanonicalQS apiRequest
|
||||
@@ -163,7 +162,7 @@ app dbStructure proc conf apiRequest =
|
||||
stm = createWriteStatement sq mq
|
||||
(contentType == CTSingularJSON) isSingle
|
||||
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols
|
||||
row <- H.query (toS pjRaw) stm
|
||||
row <- H.statement (toS pjRaw) stm
|
||||
let (_, _, fs, body) = extractQueryResult row
|
||||
headers = catMaybes [
|
||||
if null fs
|
||||
@@ -192,7 +191,7 @@ app dbStructure proc conf apiRequest =
|
||||
let stm = createWriteStatement sq mq
|
||||
(contentType == CTSingularJSON) False (contentType == CTTextCSV)
|
||||
(iPreferRepresentation apiRequest) []
|
||||
row <- H.query (toS pjRaw) stm
|
||||
row <- H.statement (toS pjRaw) stm
|
||||
let (_, queryTotal, _, body) = extractQueryResult row
|
||||
if contentType == CTSingularJSON
|
||||
&& queryTotal /= 1
|
||||
@@ -225,7 +224,7 @@ app dbStructure proc conf apiRequest =
|
||||
else if S.fromList colNames /= pjKeys
|
||||
then return $ simpleError status400 [] "You must specify all columns in the payload when using PUT"
|
||||
else do
|
||||
row <- H.query (toS pjRaw) $
|
||||
row <- H.statement (toS pjRaw) $
|
||||
createWriteStatement sq mq (contentType == CTSingularJSON) False
|
||||
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) []
|
||||
let (_, queryTotal, _, body) = extractQueryResult row
|
||||
@@ -249,7 +248,7 @@ app dbStructure proc conf apiRequest =
|
||||
(contentType == CTSingularJSON) False
|
||||
(contentType == CTTextCSV)
|
||||
(iPreferRepresentation apiRequest) []
|
||||
row <- H.query mempty stm
|
||||
row <- H.statement mempty stm
|
||||
let (_, queryTotal, _, body) = extractQueryResult row
|
||||
r = contentRangeH 1 0 $
|
||||
toInteger <$> if shouldCount then Just queryTotal else Nothing
|
||||
@@ -287,8 +286,8 @@ app dbStructure proc conf apiRequest =
|
||||
PJObject -> True
|
||||
PJArray _ -> False
|
||||
singular = contentType == CTSingularJSON
|
||||
specifiedPgArgs = filter ((`S.member` pjKeys) . pgaName) $ fromMaybe [] (pdArgs <$> proc)
|
||||
row <- H.query (toS pjRaw) $
|
||||
specifiedPgArgs = filter ((`S.member` pjKeys) . pgaName) $ maybe [] pdArgs proc
|
||||
row <- H.statement (toS pjRaw) $
|
||||
callProc qi specifiedPgArgs returnsScalar q cq shouldCount
|
||||
singular (iPreferSingleObjectParameter apiRequest)
|
||||
(contentType == CTTextCSV)
|
||||
@@ -317,7 +316,7 @@ app dbStructure proc conf apiRequest =
|
||||
toTableInfo :: [Table] -> [(Table, [Column], [Text])]
|
||||
toTableInfo = map (\t -> let (s, tn) = (tableSchema t, tableName t) in (t, tableCols dbStructure s tn, tablePKCols dbStructure s tn))
|
||||
encodeApi ti sd procs = encodeOpenAPI (concat $ M.elems procs) (toTableInfo ti) uri' sd $ dbPrimaryKeys dbStructure
|
||||
body <- encodeApi <$> H.query schema accessibleTables <*> H.query schema schemaDescription <*> H.query schema accessibleProcs
|
||||
body <- encodeApi <$> H.statement schema accessibleTables <*> H.statement schema schemaDescription <*> H.statement schema accessibleProcs
|
||||
return $ responseLBS status200 [toHeader CTOpenAPI] $ toS body
|
||||
|
||||
_ -> return notFound
|
||||
@@ -398,8 +397,8 @@ contentRangeH lower upper total =
|
||||
rangeString
|
||||
| totalNotZero && fromInRange = show lower <> "-" <> show upper
|
||||
| otherwise = "*"
|
||||
totalString = fromMaybe "*" (show <$> total)
|
||||
totalNotZero = fromMaybe True ((/=) 0 <$> total)
|
||||
totalString = maybe "*" show total
|
||||
totalNotZero = maybe True (0 /=) total
|
||||
fromInRange = lower <= upper
|
||||
|
||||
extractQueryResult :: Maybe ResultsWithCount -> ResultsWithCount
|
||||
|
||||
+24
-13
@@ -16,10 +16,11 @@ module PostgREST.Auth (
|
||||
containsRole
|
||||
, jwtClaims
|
||||
, JWTAttempt(..)
|
||||
, parseJWK
|
||||
, parseSecret
|
||||
) where
|
||||
|
||||
import Control.Lens.Operators
|
||||
import Control.Lens (set)
|
||||
import qualified Data.Aeson as JSON
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import Data.Time.Clock (UTCTime)
|
||||
@@ -42,13 +43,13 @@ data JWTAttempt = JWTInvalid JWTError
|
||||
Receives the JWT secret and audience (from config) and a JWT and returns a map
|
||||
of JWT claims.
|
||||
-}
|
||||
jwtClaims :: Maybe JWK -> Maybe StringOrURI -> LByteString -> UTCTime -> Maybe JSPath -> IO JWTAttempt
|
||||
jwtClaims :: Maybe JWKSet -> Maybe StringOrURI -> LByteString -> UTCTime -> Maybe JSPath -> IO JWTAttempt
|
||||
jwtClaims _ _ "" _ _ = return $ JWTClaims M.empty
|
||||
jwtClaims secret audience payload time jspath =
|
||||
case secret of
|
||||
Nothing -> return JWTMissingSecret
|
||||
Just s -> do
|
||||
let validation = defaultJWTValidationSettings (maybe (const True) (==) audience)
|
||||
let validation = set allowedSkew 1 $ defaultJWTValidationSettings (maybe (const True) (==) audience)
|
||||
eJwt <- runExceptT $ do
|
||||
jwt <- decodeCompact payload
|
||||
verifyClaimsAt validation s time jwt
|
||||
@@ -82,19 +83,29 @@ containsRole :: JWTAttempt -> Bool
|
||||
containsRole (JWTClaims claims) = M.member "role" claims
|
||||
containsRole _ = False
|
||||
|
||||
parseJWK :: ByteString -> JWK
|
||||
parseJWK str =
|
||||
fromMaybe (hs256jwk str) (JSON.decode (toS str) :: Maybe JWK)
|
||||
{-|
|
||||
Parse `jwt-secret` configuration option and turn into a JWKSet.
|
||||
|
||||
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
|
||||
with one key and the last is converted as is.
|
||||
-}
|
||||
parseSecret :: ByteString -> JWKSet
|
||||
parseSecret str =
|
||||
fromMaybe (maybe secret (\jwk' -> JWKSet [jwk']) maybeJWK)
|
||||
maybeJWKSet
|
||||
where
|
||||
maybeJWKSet = JSON.decode (toS str) :: Maybe JWKSet
|
||||
maybeJWK = JSON.decode (toS str) :: Maybe JWK
|
||||
secret = JWKSet [jwkFromSecret str]
|
||||
|
||||
{-|
|
||||
Internal helper to generate HMAC-SHA256. When the jwt key in the
|
||||
config file is a simple string rather than a JWK object, we'll
|
||||
apply this function to it.
|
||||
Internal helper to generate a symmetric HMAC-SHA256 JWK from a text secret.
|
||||
-}
|
||||
hs256jwk :: ByteString -> JWK
|
||||
hs256jwk key =
|
||||
jwkFromSecret :: ByteString -> JWK
|
||||
jwkFromSecret key =
|
||||
fromKeyMaterial km
|
||||
& jwkUse .~ Just Sig
|
||||
& jwkAlg .~ (Just $ JWSAlg HS256)
|
||||
& jwkUse ?~ Sig
|
||||
& jwkAlg ?~ JWSAlg HS256
|
||||
where
|
||||
km = OctKeyMaterial (OctKeyParameters (JOSE.Types.Base64Octets key))
|
||||
|
||||
+21
-28
@@ -18,9 +18,6 @@ module PostgREST.Config ( prettyVersion
|
||||
, docsVersion
|
||||
, readOptions
|
||||
, corsPolicy
|
||||
, minimumPgVersion
|
||||
, pgVersion95
|
||||
, pgVersion96
|
||||
, AppConfig (..)
|
||||
)
|
||||
where
|
||||
@@ -42,7 +39,7 @@ import Data.Scientific (floatingOrInteger)
|
||||
import Data.String (String)
|
||||
import Data.Text (dropAround,
|
||||
intercalate, lines,
|
||||
strip, take)
|
||||
strip, take, splitOn)
|
||||
import Data.Text.Encoding (encodeUtf8)
|
||||
import Data.Text.IO (hPutStrLn)
|
||||
import Data.Version (versionBranch)
|
||||
@@ -52,7 +49,7 @@ import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
|
||||
import Options.Applicative hiding (str)
|
||||
import Paths_postgrest (version)
|
||||
import PostgREST.Parsers (pRoleClaimKey)
|
||||
import PostgREST.Types (PgVersion(..), ApiRequestError(..),
|
||||
import PostgREST.Types (ApiRequestError(..),
|
||||
JSPath, JSPathExp(..))
|
||||
import Protolude hiding (hPutStrLn, take,
|
||||
intercalate, (<>))
|
||||
@@ -81,11 +78,12 @@ data AppConfig = AppConfig {
|
||||
, configQuiet :: Bool
|
||||
, configSettings :: [(Text, Text)]
|
||||
, configRoleClaimKey :: Either ApiRequestError JSPath
|
||||
, configExtraSearchPath :: [Text]
|
||||
}
|
||||
|
||||
defaultCorsPolicy :: CorsResourcePolicy
|
||||
defaultCorsPolicy = CorsResourcePolicy Nothing
|
||||
["GET", "POST", "PATCH", "DELETE", "OPTIONS"] ["Authorization"] Nothing
|
||||
["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"] ["Authorization"] Nothing
|
||||
(Just $ 60*60*24) False False True
|
||||
|
||||
-- | CORS policy to be used in by Wai Cors middleware
|
||||
@@ -132,7 +130,7 @@ readOptions = do
|
||||
<*> C.key "db-anon-role"
|
||||
<*> (mfilter (/= "") <$> C.key "server-proxy-uri")
|
||||
<*> C.key "db-schema"
|
||||
<*> (fromMaybe "*4" . mfilter (/= "") <$> C.key "server-host")
|
||||
<*> (fromMaybe "127.0.0.1" . mfilter (/= "") <$> C.key "server-host")
|
||||
<*> (fromMaybe 3000 . join . fmap coerceInt <$> C.key "server-port")
|
||||
<*> (fmap encodeUtf8 . mfilter (/= "") <$> C.key "jwt-secret")
|
||||
<*> (fromMaybe False . join . fmap coerceBool <$> C.key "secret-is-base64")
|
||||
@@ -141,8 +139,9 @@ readOptions = do
|
||||
<*> (join . fmap coerceInt <$> C.key "max-rows")
|
||||
<*> (mfilter (/= "") <$> C.key "pre-request")
|
||||
<*> pure False
|
||||
<*> (fmap parsedPairToTextPair <$> C.subassocs "app.settings")
|
||||
<*> (fmap (fmap coerceText) <$> C.subassocs "app.settings")
|
||||
<*> (maybe (Right [JSPKey "role"]) parseRoleClaimKey <$> C.key "role-claim-key")
|
||||
<*> (maybe ["public"] splitExtraSearchPath <$> C.key "db-extra-search-path")
|
||||
|
||||
case mAppConf of
|
||||
Nothing -> do
|
||||
@@ -152,13 +151,6 @@ readOptions = do
|
||||
return appConf
|
||||
|
||||
where
|
||||
parsedPairToTextPair :: (Name, Value) -> (Text, Text)
|
||||
parsedPairToTextPair (k, v) = (k, newValue)
|
||||
where
|
||||
newValue = case v of
|
||||
String textVal -> textVal
|
||||
_ -> show v
|
||||
|
||||
parseJwtAudience :: Name -> C.ConfigParserM (Maybe StringOrURI)
|
||||
parseJwtAudience k =
|
||||
C.key k >>= \case
|
||||
@@ -168,6 +160,10 @@ readOptions = do
|
||||
(Just "") -> pure Nothing
|
||||
aud' -> pure aud'
|
||||
|
||||
coerceText :: Value -> Text
|
||||
coerceText (String s) = s
|
||||
coerceText v = show v
|
||||
|
||||
coerceInt :: (Read i, Integral i) => Value -> Maybe i
|
||||
coerceInt (Number x) = rightToMaybe $ floatingOrInteger x
|
||||
coerceInt (String x) = readMaybe $ toS x
|
||||
@@ -182,6 +178,10 @@ readOptions = do
|
||||
parseRoleClaimKey (String s) = pRoleClaimKey s
|
||||
parseRoleClaimKey v = pRoleClaimKey $ show v
|
||||
|
||||
splitExtraSearchPath :: Value -> [Text]
|
||||
splitExtraSearchPath (String s) = strip <$> splitOn "," s
|
||||
splitExtraSearchPath _ = []
|
||||
|
||||
opts = info (helper <*> pathParser) $
|
||||
fullDesc
|
||||
<> progDesc (
|
||||
@@ -205,17 +205,17 @@ readOptions = do
|
||||
exampleCfg :: Doc
|
||||
exampleCfg = vsep . map (text . toS) . lines $
|
||||
[str|db-uri = "postgres://user:pass@localhost:5432/dbname"
|
||||
|db-schema = "public"
|
||||
|db-schema = "public" # this schema gets added to the search_path of every request
|
||||
|db-anon-role = "postgres"
|
||||
|db-pool = 10
|
||||
|
|
||||
|server-host = "*4"
|
||||
|server-host = "127.0.0.1"
|
||||
|server-port = 3000
|
||||
|
|
||||
|## base url for swagger output
|
||||
|# server-proxy-uri = ""
|
||||
|
|
||||
|## choose a secret to enable JWT auth
|
||||
|## choose a secret, JSON Web Key (or set) to enable JWT auth
|
||||
|## (use "@filename" to load from separate file)
|
||||
|# jwt-secret = "foo"
|
||||
|# secret-is-base64 = false
|
||||
@@ -229,6 +229,9 @@ readOptions = do
|
||||
|
|
||||
|## jspath to the role claim key
|
||||
|# role-claim-key = ".role"
|
||||
|
|
||||
|## extra schemas to add to the search_path of every request
|
||||
|# db-extra-search-path = "extensions, util"
|
||||
|]
|
||||
|
||||
pathParser :: Parser FilePath
|
||||
@@ -236,13 +239,3 @@ pathParser =
|
||||
strArgument $
|
||||
metavar "FILENAME" <>
|
||||
help "Path to configuration file"
|
||||
|
||||
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
||||
minimumPgVersion :: PgVersion
|
||||
minimumPgVersion = PgVersion 90400 "9.4"
|
||||
|
||||
pgVersion96 :: PgVersion
|
||||
pgVersion96 = PgVersion 90600 "9.6"
|
||||
|
||||
pgVersion95 :: PgVersion
|
||||
pgVersion95 = PgVersion 90500 "9.5"
|
||||
|
||||
@@ -36,7 +36,7 @@ import PostgREST.Parsers
|
||||
import PostgREST.RangeQuery (NonnegRange, restrictRange, allRange)
|
||||
import PostgREST.Types
|
||||
|
||||
import Protolude hiding (from, dropWhile, drop)
|
||||
import Protolude hiding (from)
|
||||
import Text.Regex.TDFA ((=~))
|
||||
import Unsafe (unsafeHead)
|
||||
|
||||
@@ -90,7 +90,7 @@ readRequest maxRows allRels proc apiRequest =
|
||||
-- in a relation where one of the tables matches "TableName"
|
||||
-- replace the name to that table with pg_source
|
||||
-- this "fake" relations is needed so that in a mutate query
|
||||
-- we can look a the "returning *" part which is wrapped with a "with"
|
||||
-- we can look at the "returning *" part which is wrapped with a "with"
|
||||
-- as just another table that has relations with other tables
|
||||
toSourceRelation :: TableName -> Relation -> Maybe Relation
|
||||
toSourceRelation mt r@(Relation t _ ft _ _ rt _ _)
|
||||
@@ -225,13 +225,13 @@ addJoinConditions schema (Node node@(query, nodeProps@(_, relation, _, _, _)) fo
|
||||
addJoinCond jc rq@Select{joinConditions=jcs} = rq{joinConditions=jc:jcs}
|
||||
|
||||
getJoinConditions :: Relation -> [JoinCondition]
|
||||
getJoinConditions (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{tableName=ftN} fcs typ lt lc1 lc2) =
|
||||
getJoinConditions (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{tableName=ftN} fCols typ lt lc1 lc2) =
|
||||
if | typ == Child || typ == Parent ->
|
||||
zipWith (toJoinCondition tN ftN) cols fcs
|
||||
zipWith (toJoinCondition tN ftN) cols fCols
|
||||
| typ == Many ->
|
||||
let ltN = fromMaybe "" (tableName <$> lt) in
|
||||
zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fcs (fromMaybe [] lc2)
|
||||
| typ == Root -> undefined
|
||||
let ltN = maybe "" tableName lt in
|
||||
zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fCols (fromMaybe [] lc2)
|
||||
| typ == Root -> witness
|
||||
where
|
||||
toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition
|
||||
toJoinCondition tb ftb c fc =
|
||||
|
||||
+151
-172
@@ -10,12 +10,11 @@ module PostgREST.DbStructure (
|
||||
, accessibleProcs
|
||||
, schemaDescription
|
||||
, getPgVersion
|
||||
, fillSessionWithSettings
|
||||
) where
|
||||
|
||||
import qualified Hasql.Decoders as HD
|
||||
import qualified Hasql.Encoders as HE
|
||||
import qualified Hasql.Query as H
|
||||
import qualified Hasql.Statement as H
|
||||
|
||||
import Control.Applicative
|
||||
import qualified Data.HashMap.Strict as M
|
||||
@@ -26,26 +25,25 @@ import Data.Text (split, strip,
|
||||
splitOn)
|
||||
import qualified Data.Text as T
|
||||
import qualified Hasql.Session as H
|
||||
import qualified Hasql.Transaction as HT
|
||||
import PostgREST.Types
|
||||
import Text.InterpolatedString.Perl6 (q)
|
||||
import Text.InterpolatedString.Perl6 (q, qc)
|
||||
|
||||
import GHC.Exts (groupWith)
|
||||
import Protolude
|
||||
import Unsafe (unsafeHead)
|
||||
|
||||
import Data.Functor.Contravariant (contramap)
|
||||
import Contravariant.Extras (contrazip2)
|
||||
|
||||
getDbStructure :: Schema -> PgVersion -> H.Session DbStructure
|
||||
getDbStructure :: Schema -> PgVersion -> HT.Transaction DbStructure
|
||||
getDbStructure schema pgVer = do
|
||||
tabs <- H.query () allTables
|
||||
cols <- H.query schema $ allColumns tabs
|
||||
syns <- H.query () $ allSynonyms cols
|
||||
childRels <- H.query () $ allChildRelations tabs cols
|
||||
keys <- H.query () $ allPrimaryKeys tabs
|
||||
procs <- H.query schema allProcs
|
||||
HT.sql "set local schema ''" -- for getting the fully qualified name(schema.name) of every db object
|
||||
tabs <- HT.statement () allTables
|
||||
cols <- HT.statement schema $ allColumns tabs
|
||||
syns <- HT.statement schema $ allSynonyms cols pgVer
|
||||
childRels <- HT.statement () $ allChildRelations tabs cols
|
||||
keys <- HT.statement () $ allPrimaryKeys tabs
|
||||
procs <- HT.statement schema allProcs
|
||||
|
||||
let rels = addManyToManyRelations . addParentRelations $ addViewRelations syns childRels
|
||||
let rels = addManyToManyRelations . addParentRelations $ addViewChildRelations syns childRels
|
||||
cols' = addForeignKeys rels cols
|
||||
keys' = addViewPrimaryKeys syns keys
|
||||
|
||||
@@ -60,70 +58,70 @@ getDbStructure schema pgVer = do
|
||||
|
||||
decodeTables :: HD.Result [Table]
|
||||
decodeTables =
|
||||
HD.rowsList tblRow
|
||||
HD.rowList tblRow
|
||||
where
|
||||
tblRow = Table <$> HD.value HD.text
|
||||
<*> HD.value HD.text
|
||||
<*> HD.nullableValue HD.text
|
||||
<*> HD.value HD.bool
|
||||
tblRow = Table <$> HD.column HD.text
|
||||
<*> HD.column HD.text
|
||||
<*> HD.nullableColumn HD.text
|
||||
<*> HD.column HD.bool
|
||||
|
||||
decodeColumns :: [Table] -> HD.Result [Column]
|
||||
decodeColumns tables =
|
||||
mapMaybe (columnFromRow tables) <$> HD.rowsList colRow
|
||||
mapMaybe (columnFromRow tables) <$> HD.rowList colRow
|
||||
where
|
||||
colRow =
|
||||
(,,,,,,,,,,,)
|
||||
<$> HD.value HD.text <*> HD.value HD.text
|
||||
<*> HD.value HD.text <*> HD.nullableValue HD.text
|
||||
<*> HD.value HD.int4 <*> HD.value HD.bool
|
||||
<*> HD.value HD.text <*> HD.value HD.bool
|
||||
<*> HD.nullableValue HD.int4
|
||||
<*> HD.nullableValue HD.int4
|
||||
<*> HD.nullableValue HD.text
|
||||
<*> HD.nullableValue HD.text
|
||||
<$> HD.column HD.text <*> HD.column HD.text
|
||||
<*> HD.column HD.text <*> HD.nullableColumn HD.text
|
||||
<*> HD.column HD.int4 <*> HD.column HD.bool
|
||||
<*> HD.column HD.text <*> HD.column HD.bool
|
||||
<*> HD.nullableColumn HD.int4
|
||||
<*> HD.nullableColumn HD.int4
|
||||
<*> HD.nullableColumn HD.text
|
||||
<*> HD.nullableColumn HD.text
|
||||
|
||||
decodeRelations :: [Table] -> [Column] -> HD.Result [Relation]
|
||||
decodeRelations tables cols =
|
||||
mapMaybe (relationFromRow tables cols) <$> HD.rowsList relRow
|
||||
mapMaybe (relationFromRow tables cols) <$> HD.rowList relRow
|
||||
where
|
||||
relRow = (,,,,,)
|
||||
<$> HD.value HD.text
|
||||
<*> HD.value HD.text
|
||||
<*> HD.value (HD.array (HD.arrayDimension replicateM (HD.arrayValue HD.text)))
|
||||
<*> HD.value HD.text
|
||||
<*> HD.value HD.text
|
||||
<*> HD.value (HD.array (HD.arrayDimension replicateM (HD.arrayValue HD.text)))
|
||||
<$> HD.column HD.text
|
||||
<*> HD.column HD.text
|
||||
<*> HD.column (HD.array (HD.dimension replicateM (HD.element HD.text)))
|
||||
<*> HD.column HD.text
|
||||
<*> HD.column HD.text
|
||||
<*> HD.column (HD.array (HD.dimension replicateM (HD.element HD.text)))
|
||||
|
||||
decodePks :: [Table] -> HD.Result [PrimaryKey]
|
||||
decodePks tables =
|
||||
mapMaybe (pkFromRow tables) <$> HD.rowsList pkRow
|
||||
mapMaybe (pkFromRow tables) <$> HD.rowList pkRow
|
||||
where
|
||||
pkRow = (,,) <$> HD.value HD.text <*> HD.value HD.text <*> HD.value HD.text
|
||||
pkRow = (,,) <$> HD.column HD.text <*> HD.column HD.text <*> HD.column HD.text
|
||||
|
||||
decodeSynonyms :: [Column] -> HD.Result [Synonym]
|
||||
decodeSynonyms cols =
|
||||
mapMaybe (synonymFromRow cols) <$> HD.rowsList synRow
|
||||
mapMaybe (synonymFromRow cols) <$> HD.rowList synRow
|
||||
where
|
||||
synRow = (,,,,,)
|
||||
<$> HD.value HD.text <*> HD.value HD.text
|
||||
<*> HD.value HD.text <*> HD.value HD.text
|
||||
<*> HD.value HD.text <*> HD.value HD.text
|
||||
<$> HD.column HD.text <*> HD.column HD.text
|
||||
<*> HD.column HD.text <*> HD.column HD.text
|
||||
<*> HD.column HD.text <*> HD.column HD.text
|
||||
|
||||
decodeProcs :: HD.Result (M.HashMap Text [ProcDescription])
|
||||
decodeProcs =
|
||||
-- Duplicate rows for a function means they're overloaded, order these by least args according to ProcDescription Ord instance
|
||||
map sort . M.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addName) <$> HD.rowsList tblRow
|
||||
map sort . M.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addName) <$> HD.rowList tblRow
|
||||
where
|
||||
tblRow = ProcDescription
|
||||
<$> HD.value HD.text
|
||||
<*> HD.nullableValue HD.text
|
||||
<*> (parseArgs <$> HD.value HD.text)
|
||||
<$> HD.column HD.text
|
||||
<*> HD.nullableColumn HD.text
|
||||
<*> (parseArgs <$> HD.column HD.text)
|
||||
<*> (parseRetType
|
||||
<$> HD.value HD.text
|
||||
<*> HD.value HD.text
|
||||
<*> HD.value HD.bool
|
||||
<*> HD.value HD.char)
|
||||
<*> (parseVolatility <$> HD.value HD.char)
|
||||
<$> HD.column HD.text
|
||||
<*> HD.column HD.text
|
||||
<*> HD.column HD.bool
|
||||
<*> HD.column HD.char)
|
||||
<*> (parseVolatility <$> HD.column HD.char)
|
||||
|
||||
addName :: ProcDescription -> (Text, ProcDescription)
|
||||
addName pd = (pdName pd, pd)
|
||||
@@ -159,11 +157,11 @@ decodeProcs =
|
||||
| v == 's' = Stable
|
||||
| otherwise = Volatile -- only 'v' can happen here
|
||||
|
||||
allProcs :: H.Query Schema (M.HashMap Text [ProcDescription])
|
||||
allProcs = H.statement (toS procsSqlQuery) (HE.value HE.text) decodeProcs True
|
||||
allProcs :: H.Statement Schema (M.HashMap Text [ProcDescription])
|
||||
allProcs = H.Statement (toS procsSqlQuery) (HE.param HE.text) decodeProcs True
|
||||
|
||||
accessibleProcs :: H.Query Schema (M.HashMap Text [ProcDescription])
|
||||
accessibleProcs = H.statement (toS sql) (HE.value HE.text) decodeProcs True
|
||||
accessibleProcs :: H.Statement Schema (M.HashMap Text [ProcDescription])
|
||||
accessibleProcs = H.Statement (toS sql) (HE.param HE.text) decodeProcs True
|
||||
where
|
||||
sql = procsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')"
|
||||
|
||||
@@ -186,9 +184,9 @@ procsSqlQuery = [q|
|
||||
WHERE pn.nspname = $1
|
||||
|]
|
||||
|
||||
schemaDescription :: H.Query Schema (Maybe Text)
|
||||
schemaDescription :: H.Statement Schema (Maybe Text)
|
||||
schemaDescription =
|
||||
H.statement sql (HE.value HE.text) (join <$> HD.maybeRow (HD.nullableValue HD.text)) True
|
||||
H.Statement sql (HE.param HE.text) (join <$> HD.rowMaybe (HD.nullableColumn HD.text)) True
|
||||
where
|
||||
sql = [q|
|
||||
select
|
||||
@@ -199,9 +197,9 @@ schemaDescription =
|
||||
where
|
||||
n.nspname = $1 |]
|
||||
|
||||
accessibleTables :: H.Query Schema [Table]
|
||||
accessibleTables :: H.Statement Schema [Table]
|
||||
accessibleTables =
|
||||
H.statement sql (HE.value HE.text) decodeTables True
|
||||
H.Statement sql (HE.param HE.text) decodeTables True
|
||||
where
|
||||
sql = [q|
|
||||
select
|
||||
@@ -247,32 +245,32 @@ Having a Relation{relTable=t1, relColumns=[c1], relFTable=t2, relFColumns=[c2],
|
||||
|
||||
t1.c1------t2.c2
|
||||
|
||||
When only having a t1_view.c1 synonym, we need to add a View to Table Relation
|
||||
When only having a t1_view.c1 synonym, we need to add a View to Table Child Relation
|
||||
|
||||
t1.c1----t2.c2 t1.c1----------t2.c2
|
||||
-> --------/
|
||||
-> ________/
|
||||
/
|
||||
t1_view.c1 t1_view.c1
|
||||
|
||||
|
||||
When only having a t2_view.c2 synonym, we need to add a Table to View Relation
|
||||
When only having a t2_view.c2 synonym, we need to add a Table to View Child Relation
|
||||
|
||||
t1.c1----t2.c2 t1.c1----------t2.c2
|
||||
-> \--------
|
||||
-> \________
|
||||
\
|
||||
t2_view.c2 t2_view.c1
|
||||
|
||||
When having t1_view.c1 and a t2_view.c2 synonyms, we need to add a View to View Relation in addition to the prior
|
||||
When having t1_view.c1 and a t2_view.c2 synonyms, we need to add a View to View Child Relation in addition to the prior
|
||||
|
||||
t1.c1----t2.c2 t1.c1----------t2.c2
|
||||
-> \--------/
|
||||
-> \________/
|
||||
/ \
|
||||
t1_view.c1 t2_view.c2 t1_view.c1-------t2_view.c1
|
||||
|
||||
The logic for composite pks is similar just need to make sure all the Relation columns have synonyms.
|
||||
-}
|
||||
addViewRelations :: [Synonym] -> [Relation] -> [Relation]
|
||||
addViewRelations allSyns = concatMap (\rel ->
|
||||
addViewChildRelations :: [Synonym] -> [Relation] -> [Relation]
|
||||
addViewChildRelations allSyns = concatMap (\rel ->
|
||||
rel : case rel of
|
||||
Relation{relType=Child, relTable, relColumns, relFTable, relFColumns} ->
|
||||
|
||||
@@ -283,18 +281,22 @@ addViewRelations allSyns = concatMap (\rel ->
|
||||
fColsSyns = colSynsGroupedByView relFColumns
|
||||
getView :: [Synonym] -> Table
|
||||
getView = colTable . snd . unsafeHead
|
||||
syns `allSynsOf` cols = S.fromList (fst <$> syns) == S.fromList cols in
|
||||
syns `allSynsOf` cols = S.fromList (fst <$> syns) == S.fromList cols
|
||||
-- Relation is dependent on the order of relColumns and relFColumns to get the join conditions right in the generated query.
|
||||
-- So we need to change the order of the synonyms to match the relColumns
|
||||
-- This could be avoided if the Relation type is improved with a structure that maintains the association of relColumns and relFColumns
|
||||
syns `sortAccordingTo` columns = sortOn (\(k, _) -> L.lookup k $ zip columns [0::Int ..]) syns in
|
||||
|
||||
-- View Table Relations
|
||||
[Relation (getView syns) (snd <$> syns) relFTable relFColumns Child Nothing Nothing Nothing
|
||||
-- View Table Child Relations
|
||||
[Relation (getView syns) (snd <$> syns `sortAccordingTo` relColumns) relFTable relFColumns Child Nothing Nothing Nothing
|
||||
| syns <- colsSyns, syns `allSynsOf` relColumns] ++
|
||||
|
||||
-- Table View Relations
|
||||
[Relation relTable relColumns (getView fSyns) (snd <$> fSyns) Child Nothing Nothing Nothing
|
||||
-- Table View Child Relations
|
||||
[Relation relTable relColumns (getView fSyns) (snd <$> fSyns `sortAccordingTo` relFColumns) Child Nothing Nothing Nothing
|
||||
| fSyns <- fColsSyns, fSyns `allSynsOf` relFColumns] ++
|
||||
|
||||
-- View View Relations
|
||||
[Relation (getView syns) (snd <$> syns) (getView fSyns) (snd <$> fSyns) Child Nothing Nothing Nothing
|
||||
-- View View Child Relations
|
||||
[Relation (getView syns) (snd <$> syns `sortAccordingTo` relColumns) (getView fSyns) (snd <$> fSyns `sortAccordingTo` relFColumns) Child Nothing Nothing Nothing
|
||||
| syns <- colsSyns, fSyns <- fColsSyns, syns `allSynsOf` relColumns, fSyns `allSynsOf` relFColumns]
|
||||
|
||||
_ -> [])
|
||||
@@ -328,9 +330,9 @@ addViewPrimaryKeys syns = concatMap (\pk ->
|
||||
filter (\(col, _) -> colTable col == pkTable pk && colName col == pkName pk) syns in
|
||||
pk : viewPks)
|
||||
|
||||
allTables :: H.Query () [Table]
|
||||
allTables :: H.Statement () [Table]
|
||||
allTables =
|
||||
H.statement sql HE.unit decodeTables True
|
||||
H.Statement sql HE.unit decodeTables True
|
||||
where
|
||||
sql = [q|
|
||||
SELECT
|
||||
@@ -351,9 +353,9 @@ allTables =
|
||||
GROUP BY table_schema, table_name, insertable
|
||||
ORDER BY table_schema, table_name |]
|
||||
|
||||
allColumns :: [Table] -> H.Query Schema [Column]
|
||||
allColumns :: [Table] -> H.Statement Schema [Column]
|
||||
allColumns tabs =
|
||||
H.statement sql (HE.value HE.text) (decodeColumns tabs) True
|
||||
H.Statement sql (HE.param HE.text) (decodeColumns tabs) True
|
||||
where
|
||||
sql = [q|
|
||||
SELECT DISTINCT
|
||||
@@ -388,7 +390,7 @@ allColumns tabs =
|
||||
pg_catalog.pg_namespace n
|
||||
WHERE
|
||||
r.contype IN ('f', 'p')
|
||||
AND c.relkind IN ('r', 'v', 'f', 'mv')
|
||||
AND c.relkind IN ('r', 'v', 'f', 'm')
|
||||
AND r.conrelid = c.oid
|
||||
AND c.relnamespace = n.oid
|
||||
AND n.nspname NOT IN ('pg_catalog', 'information_schema', $1)
|
||||
@@ -492,7 +494,7 @@ allColumns tabs =
|
||||
NOT pg_is_other_temp_schema(nc.oid)
|
||||
AND a.attnum > 0
|
||||
AND NOT a.attisdropped
|
||||
AND (c.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char"]))
|
||||
AND (c.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char", 'm'::"char"]))
|
||||
AND (nc.nspname = $1 OR kc.r_oid IS NOT NULL) /*--filter only columns that are FK/PK or in the api schema */
|
||||
/*--AND (pg_has_role(c.relowner, 'USAGE'::text) OR has_column_privilege(c.oid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text))*/
|
||||
)
|
||||
@@ -536,11 +538,11 @@ columnFromRow tabs (s, t, n, desc, pos, nul, typ, u, l, p, d, e) = buildColumn <
|
||||
buildColumn tbl = Column tbl n desc pos nul typ u l p d (parseEnum e) Nothing
|
||||
table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs
|
||||
parseEnum :: Maybe Text -> [Text]
|
||||
parseEnum str = fromMaybe [] $ split (==',') <$> str
|
||||
parseEnum = maybe [] (split (==','))
|
||||
|
||||
allChildRelations :: [Table] -> [Column] -> H.Query () [Relation]
|
||||
allChildRelations :: [Table] -> [Column] -> H.Statement () [Relation]
|
||||
allChildRelations tabs cols =
|
||||
H.statement sql HE.unit (decodeRelations tabs cols) True
|
||||
H.Statement sql HE.unit (decodeRelations tabs cols) True
|
||||
where
|
||||
sql = [q|
|
||||
SELECT ns1.nspname AS table_schema,
|
||||
@@ -579,9 +581,9 @@ relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) =
|
||||
cols = mapM (findCol rs rt) rcs
|
||||
colsF = mapM (findCol frs frt) frcs
|
||||
|
||||
allPrimaryKeys :: [Table] -> H.Query () [PrimaryKey]
|
||||
allPrimaryKeys :: [Table] -> H.Statement () [PrimaryKey]
|
||||
allPrimaryKeys tabs =
|
||||
H.statement sql HE.unit (decodePks tabs) True
|
||||
H.Statement sql HE.unit (decodePks tabs) True
|
||||
where
|
||||
sql = [q|
|
||||
/*
|
||||
@@ -689,80 +691,71 @@ pkFromRow :: [Table] -> (Schema, Text, Text) -> Maybe PrimaryKey
|
||||
pkFromRow tabs (s, t, n) = PrimaryKey <$> table <*> pure n
|
||||
where table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs
|
||||
|
||||
allSynonyms :: [Column] -> H.Query () [Synonym]
|
||||
allSynonyms cols =
|
||||
H.statement sql HE.unit (decodeSynonyms cols) True
|
||||
where
|
||||
-- query explanation at https://gist.github.com/ruslantalpa/2eab8c930a65e8043d8f
|
||||
sql = [q|
|
||||
with view_columns as (
|
||||
allSynonyms :: [Column] -> PgVersion -> H.Statement Schema [Synonym]
|
||||
allSynonyms cols pgVer =
|
||||
H.Statement sql (HE.param HE.text) (decodeSynonyms cols) True
|
||||
-- query explanation at https://gist.github.com/steve-chavez/7ee0e6590cddafb532e5f00c46275569
|
||||
where
|
||||
subselectRegex :: Text
|
||||
subselectRegex | pgVer < pgVersion100 = ":subselect {.*?:constraintDeps <>} :location"
|
||||
| otherwise = ":subselect {.*?:stmt_len 0} :location"
|
||||
sql = [qc|
|
||||
with
|
||||
views as (
|
||||
select
|
||||
c.oid as view_oid,
|
||||
a.attname::information_schema.sql_identifier as column_name
|
||||
from pg_attribute a
|
||||
join pg_class c on a.attrelid = c.oid
|
||||
join pg_namespace nc on c.relnamespace = nc.oid
|
||||
where
|
||||
not pg_is_other_temp_schema(nc.oid)
|
||||
and a.attnum > 0
|
||||
and not a.attisdropped
|
||||
and (c.relkind = 'v'::"char")
|
||||
and nc.nspname not in ('information_schema', 'pg_catalog')
|
||||
),
|
||||
view_column_usage as (
|
||||
select distinct
|
||||
v.oid as view_oid,
|
||||
nv.nspname::information_schema.sql_identifier as view_schema,
|
||||
v.relname::information_schema.sql_identifier as view_name,
|
||||
nt.nspname::information_schema.sql_identifier as table_schema,
|
||||
t.relname::information_schema.sql_identifier as table_name,
|
||||
a.attname::information_schema.sql_identifier as column_name,
|
||||
pg_get_viewdef(v.oid)::information_schema.character_data as view_definition
|
||||
from pg_namespace nv
|
||||
join pg_class v on nv.oid = v.relnamespace
|
||||
join pg_depend dv on v.oid = dv.refobjid
|
||||
join pg_depend dt on dv.objid = dt.objid
|
||||
join pg_class t on dt.refobjid = t.oid
|
||||
join pg_namespace nt on t.relnamespace = nt.oid
|
||||
join pg_attribute a on t.oid = a.attrelid and dt.refobjsubid = a.attnum
|
||||
|
||||
where
|
||||
nv.nspname not in ('information_schema', 'pg_catalog')
|
||||
and v.relkind = 'v'::"char"
|
||||
and dv.refclassid = 'pg_class'::regclass::oid
|
||||
and dv.classid = 'pg_rewrite'::regclass::oid
|
||||
and dv.deptype = 'i'::"char"
|
||||
and dv.refobjid <> dt.refobjid
|
||||
and dt.classid = 'pg_rewrite'::regclass::oid
|
||||
and dt.refclassid = 'pg_class'::regclass::oid
|
||||
and (t.relkind = any (array['r'::"char", 'v'::"char", 'f'::"char"]))
|
||||
),
|
||||
candidates as (
|
||||
n.nspname as view_schema,
|
||||
c.relname as view_name,
|
||||
r.ev_action as view_definition
|
||||
from pg_class c
|
||||
join pg_namespace n on n.oid = c.relnamespace
|
||||
join pg_rewrite r on r.ev_class = c.oid
|
||||
where (c.relkind = 'v'::char) and n.nspname = $1
|
||||
),
|
||||
removed_subselects as(
|
||||
select
|
||||
vcu.*,
|
||||
(
|
||||
select case when match is not null then coalesce(match[8], match[7], match[4]) end
|
||||
from regexp_matches(
|
||||
CONCAT('SELECT ', SPLIT_PART(vcu.view_definition, 'SELECT', 2)),
|
||||
CONCAT('SELECT.*?((',vcu.table_name,')|(\w+))\.(', vcu.column_name, ')(\s+AS\s+("([^"]+)"|([^, \n\t]+)))?.*?FROM.*?(',vcu.table_schema,'\.|)(\2|',vcu.table_name,'\s+(as\s)?\3)'),
|
||||
'nsi'
|
||||
) match
|
||||
) as view_column_name
|
||||
from view_column_usage as vcu
|
||||
)
|
||||
select
|
||||
c.table_schema,
|
||||
c.table_name,
|
||||
c.column_name as table_column_name,
|
||||
c.view_schema,
|
||||
c.view_name,
|
||||
c.view_column_name
|
||||
from view_columns as vc, candidates as c
|
||||
where
|
||||
vc.view_oid = c.view_oid
|
||||
and vc.column_name = c.view_column_name
|
||||
order by c.view_schema, c.view_name, c.table_name, c.view_column_name
|
||||
|]
|
||||
view_schema, view_name,
|
||||
regexp_replace(view_definition, '{subselectRegex}', '', 'g') as x
|
||||
from views
|
||||
),
|
||||
target_lists as(
|
||||
select
|
||||
view_schema, view_name,
|
||||
regexp_split_to_array(x, 'targetList') as x
|
||||
from removed_subselects
|
||||
),
|
||||
last_target_list_wo_tail as(
|
||||
select
|
||||
view_schema, view_name,
|
||||
(regexp_split_to_array(x[array_upper(x, 1)], ':onConflict'))[1] as x
|
||||
from target_lists
|
||||
),
|
||||
target_entries as(
|
||||
select
|
||||
view_schema, view_name,
|
||||
unnest(regexp_split_to_array(x, 'TARGETENTRY')) as entry
|
||||
from last_target_list_wo_tail
|
||||
),
|
||||
results as(
|
||||
select
|
||||
view_schema, view_name,
|
||||
substring(entry from ':resname (.*?) :') as view_colum_name,
|
||||
substring(entry from ':resorigtbl (.*?) :') as resorigtbl,
|
||||
substring(entry from ':resorigcol (.*?) :') as resorigcol
|
||||
from target_entries
|
||||
)
|
||||
select
|
||||
sch.nspname as table_schema,
|
||||
tbl.relname as table_name,
|
||||
col.attname as table_column_name,
|
||||
res.view_schema,
|
||||
res.view_name,
|
||||
res.view_colum_name
|
||||
from results res
|
||||
join pg_class tbl on tbl.oid::text = res.resorigtbl
|
||||
join pg_attribute col on col.attrelid = tbl.oid and col.attnum::text = res.resorigcol
|
||||
join pg_namespace sch on sch.oid = tbl.relnamespace
|
||||
where resorigtbl <> '0'
|
||||
order by view_schema, view_name, view_colum_name; |]
|
||||
|
||||
synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe Synonym
|
||||
synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
|
||||
@@ -772,21 +765,7 @@ synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
|
||||
findCol s t c = find (\col -> (tableSchema . colTable) col == s && (tableName . colTable) col == t && colName col == c) allCols
|
||||
|
||||
getPgVersion :: H.Session PgVersion
|
||||
getPgVersion = H.query () $ H.statement sql HE.unit versionRow False
|
||||
getPgVersion = H.statement () $ H.Statement sql HE.unit versionRow False
|
||||
where
|
||||
sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')"
|
||||
versionRow = HD.singleRow $ PgVersion <$> HD.value HD.int4 <*> HD.value HD.text
|
||||
|
||||
fillSessionWithSettings :: [(Text, Text)] -> H.Session ()
|
||||
fillSessionWithSettings settings =
|
||||
-- Send all of the config settings to the set_config function, using pgsql's `unnest` to transform arrays of values
|
||||
H.query settings $ H.statement "SELECT set_config(k, v, false) FROM unnest($1, $2) AS f1(k, v)" encoder HD.unit False
|
||||
|
||||
where
|
||||
-- Take a list of (key, value) pairs and encode each as an array to later bind to the query
|
||||
-- see Insert Many section at https://hackage.haskell.org/package/hasql-1.1.1/docs/Hasql-Encoders.html
|
||||
encoder = contramap L.unzip $ contrazip2 (vector HE.text) (vector HE.text)
|
||||
where
|
||||
vector value =
|
||||
HE.value $ HE.array $ HE.arrayDimension foldl' $ HE.arrayValue value
|
||||
|
||||
versionRow = HD.singleRow $ PgVersion <$> HD.column HD.int4 <*> HD.column HD.text
|
||||
|
||||
@@ -118,7 +118,10 @@ instance JSON.ToJSON P.UsageError where
|
||||
"details" .= (toS $ fromMaybe "" e :: Text)]
|
||||
toJSON (P.SessionError e) = JSON.toJSON e -- H.Error
|
||||
|
||||
instance JSON.ToJSON H.Error where
|
||||
instance JSON.ToJSON H.QueryError where
|
||||
toJSON (H.QueryError _ _ e) = JSON.toJSON e
|
||||
|
||||
instance JSON.ToJSON H.CommandError where
|
||||
toJSON (H.ResultError (H.ServerError c m d h)) = case toS c of
|
||||
'P':'T':_ ->
|
||||
JSON.object [
|
||||
@@ -154,7 +157,7 @@ instance JSON.ToJSON H.Error where
|
||||
|
||||
httpStatus :: Bool -> P.UsageError -> HT.Status
|
||||
httpStatus _ (P.ConnectionError _) = HT.status503
|
||||
httpStatus authed (P.SessionError (H.ResultError (H.ServerError c m _ _))) =
|
||||
httpStatus authed (P.SessionError (H.QueryError _ _ (H.ResultError (H.ServerError c m _ _)))) =
|
||||
case toS c of
|
||||
'0':'8':_ -> HT.status503 -- pg connection err
|
||||
'0':'9':_ -> HT.status500 -- triggered action exception
|
||||
@@ -184,5 +187,5 @@ httpStatus authed (P.SessionError (H.ResultError (H.ServerError c m _ _))) =
|
||||
"42501" -> if authed then HT.status403 else HT.status401 -- insufficient privilege
|
||||
'P':'T':n -> fromMaybe HT.status500 (HT.mkStatus <$> readMaybe n <*> pure m)
|
||||
_ -> HT.status400
|
||||
httpStatus _ (P.SessionError (H.ResultError _)) = HT.status500
|
||||
httpStatus _ (P.SessionError (H.ClientError _)) = HT.status503
|
||||
httpStatus _ (P.SessionError (H.QueryError _ _ (H.ResultError _))) = HT.status500
|
||||
httpStatus _ (P.SessionError (H.QueryError _ _ (H.ClientError _))) = HT.status503
|
||||
|
||||
@@ -19,9 +19,9 @@ import PostgREST.ApiRequest (ApiRequest(..))
|
||||
import PostgREST.Auth (JWTAttempt(..))
|
||||
import PostgREST.Config (AppConfig (..), corsPolicy)
|
||||
import PostgREST.Error (simpleError)
|
||||
import PostgREST.QueryBuilder (pgFmtLit, unquoted, pgFmtEnvVar)
|
||||
import PostgREST.QueryBuilder (unquoted, pgFmtSetLocal, pgFmtSetLocalSearchPath)
|
||||
|
||||
import Protolude hiding (concat, null)
|
||||
import Protolude
|
||||
|
||||
runWithClaims :: AppConfig -> JWTAttempt ->
|
||||
(ApiRequest -> H.Transaction Response) ->
|
||||
@@ -32,16 +32,17 @@ runWithClaims conf eClaims app req =
|
||||
JWTInvalid e -> return $ unauthed $ show e
|
||||
JWTMissingSecret -> return $ simpleError status500 [] "Server lacks JWT secret"
|
||||
JWTClaims claims -> do
|
||||
H.sql $ toS.mconcat $ setSchemaSql ++ setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql
|
||||
H.sql $ toS . mconcat $ setSearchPathSql : setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql ++ appSettingsSql
|
||||
mapM_ H.sql customReqCheck
|
||||
app req
|
||||
where
|
||||
headersSql = map (pgFmtEnvVar "request.header.") $ iHeaders req
|
||||
cookiesSql = map (pgFmtEnvVar "request.cookie.") $ iCookies req
|
||||
claimsSql = map (pgFmtEnvVar "request.jwt.claim.") [(c,unquoted v) | (c,v) <- M.toList claimsWithRole]
|
||||
setRoleSql = maybeToList $
|
||||
(\r -> "set local role " <> r <> ";") . toS . pgFmtLit . unquoted <$> M.lookup "role" claimsWithRole
|
||||
setSchemaSql = ["set schema " <> pgFmtLit (configSchema conf) <> ";"] :: [Text]
|
||||
headersSql = pgFmtSetLocal "request.header." <$> iHeaders req
|
||||
cookiesSql = pgFmtSetLocal "request.cookie." <$> iCookies req
|
||||
claimsSql = pgFmtSetLocal "request.jwt.claim." <$> [(c,unquoted v) | (c,v) <- M.toList claimsWithRole]
|
||||
appSettingsSql = pgFmtSetLocal mempty <$> configSettings conf
|
||||
setRoleSql = maybeToList $ (\x ->
|
||||
pgFmtSetLocal mempty ("role", unquoted x)) <$> M.lookup "role" claimsWithRole
|
||||
setSearchPathSql = pgFmtSetLocalSearchPath $ configSchema conf : configExtraSearchPath conf
|
||||
-- role claim defaults to anon if not specified in jwt
|
||||
claimsWithRole = M.union claims (M.singleton "role" anon)
|
||||
anon = JSON.String . toS $ configAnonRole conf
|
||||
|
||||
@@ -42,7 +42,8 @@ makeTableDef pks (t, cs, _) =
|
||||
(tn, (mempty :: Schema)
|
||||
& description .~ tableDescription t
|
||||
& type_ .~ SwaggerObject
|
||||
& properties .~ fromList (map (makeProperty pks) cs))
|
||||
& properties .~ fromList (map (makeProperty pks) cs)
|
||||
& required .~ map colName (filter (not . colNullable) cs))
|
||||
|
||||
makeProperty :: [PrimaryKey] -> Column -> (Text, Referenced Schema)
|
||||
makeProperty pks c = (colName c, Inline s)
|
||||
@@ -59,7 +60,7 @@ makeProperty pks c = (colName c, Inline s)
|
||||
]
|
||||
d =
|
||||
if length n > 1 then
|
||||
Just $ append (fromMaybe "" ((`append` "\n\n") <$> colDescription c)) (intercalate "\n" n)
|
||||
Just $ append (maybe "" (`append` "\n\n") $ colDescription c) (intercalate "\n" n)
|
||||
else
|
||||
colDescription c
|
||||
s =
|
||||
@@ -197,8 +198,11 @@ makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
|
||||
& at 206 ?~ "Partial Content"
|
||||
& at 200 ?~ Inline ((mempty :: Response)
|
||||
& description .~ "OK"
|
||||
& schema ?~ (Ref $ Reference $ tableName t)
|
||||
& schema ?~ Inline (mempty
|
||||
& type_ .~ SwaggerArray
|
||||
& items ?~ (SwaggerItemsObject $ Ref $ Reference $ tableName t)
|
||||
)
|
||||
)
|
||||
postOp = tOp
|
||||
& parameters .~ map ref ["body." <> tn, "preferReturn"]
|
||||
& at 201 ?~ "Created"
|
||||
@@ -219,8 +223,13 @@ makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
|
||||
makeProcPathItem :: ProcDescription -> (FilePath, PathItem)
|
||||
makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
|
||||
where
|
||||
-- Use first line of proc description as summary; rest as description (if present)
|
||||
-- We strip leading newlines from description so that users can include a blank line between summary and description
|
||||
(pSum, pDesc) = fmap fst &&& fmap (dropWhile (=='\n') . snd) $
|
||||
breakOn "\n" <$> pdDescription pd
|
||||
postOp = (mempty :: Operation)
|
||||
& description .~ pdDescription pd
|
||||
& summary .~ pSum
|
||||
& description .~ mfilter (/="") pDesc
|
||||
& parameters .~ makeProcParam pd
|
||||
& tags .~ Set.fromList ["(rpc) " <> pdName pd]
|
||||
& produces ?~ makeMimeList [CTApplicationJSON, CTSingularJSON]
|
||||
|
||||
+33
-21
@@ -3,6 +3,7 @@ module PostgREST.Parsers where
|
||||
import Protolude hiding (try, intercalate, replace, option)
|
||||
import Control.Monad ((>>))
|
||||
import Data.Foldable (foldl1)
|
||||
import Data.Functor (($>))
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import Data.Text (intercalate, replace, strip)
|
||||
import Data.List (init, last)
|
||||
@@ -57,7 +58,7 @@ lexeme p = ws *> p <* ws
|
||||
pTreePath :: Parser (EmbedPath, Field)
|
||||
pTreePath = do
|
||||
p <- pFieldName `sepBy1` pDelimiter
|
||||
jp <- optionMaybe pJsonPath
|
||||
jp <- option [] pJsonPath
|
||||
return (init p, (last p, jp))
|
||||
|
||||
pFieldForest :: Parser [Tree SelectItem]
|
||||
@@ -68,26 +69,39 @@ pFieldForest = pFieldTree `sepBy1` lexeme (char ',')
|
||||
Node <$> pFieldSelect <*> pure []
|
||||
|
||||
pStar :: Parser Text
|
||||
pStar = toS <$> (string "*" *> pure ("*"::ByteString))
|
||||
pStar = toS <$> (string "*" $> ("*"::ByteString))
|
||||
|
||||
pFieldName :: Parser Text
|
||||
pFieldName = do
|
||||
matches <- (many1 (letter <|> digit <|> oneOf "_") `sepBy1` dash) <?> "field name (* or [a..z0..9_])"
|
||||
return $ intercalate "-" $ map toS matches
|
||||
pFieldName =
|
||||
pQuotedValue <|>
|
||||
intercalate "-" . map toS <$> (many1 (letter <|> digit <|> oneOf "_ ") `sepBy1` dash) <?>
|
||||
"field name (* or [a..z0..9_])"
|
||||
where
|
||||
isDash :: GenParser Char st ()
|
||||
isDash = try ( char '-' >> notFollowedBy (char '>') )
|
||||
dash :: Parser Char
|
||||
dash = isDash *> pure '-'
|
||||
dash = isDash $> '-'
|
||||
|
||||
pJsonPathStep :: Parser Text
|
||||
pJsonPathStep = toS <$> try (string "->" *> pFieldName)
|
||||
pJsonPath :: Parser JsonPath
|
||||
pJsonPath = many pJsonOperation
|
||||
where
|
||||
pJsonOperation :: Parser JsonOperation
|
||||
pJsonOperation = pJsonArrow <*> pJsonOperand
|
||||
|
||||
pJsonPath :: Parser [Text]
|
||||
pJsonPath = (<>) <$> many pJsonPathStep <*> ( (:[]) <$> (string "->>" *> pFieldName) )
|
||||
pJsonArrow =
|
||||
try (string "->>" $> J2Arrow) <|>
|
||||
try (string "->" $> JArrow)
|
||||
|
||||
pJsonOperand =
|
||||
let pJKey = JKey . toS <$> pFieldName
|
||||
pJIdx = JIdx . toS <$> ((:) <$> option '+' (char '-') <*> many1 digit) <* pEnd
|
||||
pEnd = try (void $ lookAhead (string "->")) <|>
|
||||
try (void $ lookAhead (string "::")) <|>
|
||||
try eof in
|
||||
try pJIdx <|> try pJKey
|
||||
|
||||
pField :: Parser Field
|
||||
pField = lexeme $ (,) <$> pFieldName <*> optionMaybe pJsonPath
|
||||
pField = lexeme $ (,) <$> pFieldName <*> option [] pJsonPath
|
||||
|
||||
aliasSeparator :: Parser ()
|
||||
aliasSeparator = char ':' >> notFollowedBy (char ':')
|
||||
@@ -112,7 +126,7 @@ pFieldSelect = lexeme $
|
||||
)
|
||||
<|> do
|
||||
s <- pStar
|
||||
return ((s, Nothing), Nothing, Nothing, Nothing)
|
||||
return ((s, []), Nothing, Nothing, Nothing)
|
||||
|
||||
pOpExpr :: Parser SingleVal -> Parser OpExpr
|
||||
pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)) <|> OpExpr False <$> pOperation
|
||||
@@ -139,10 +153,10 @@ pListVal :: Parser ListVal
|
||||
pListVal = lexeme (char '(') *> pListElement `sepBy1` char ',' <* lexeme (char ')')
|
||||
|
||||
pListElement :: Parser Text
|
||||
pListElement = try pQuotedValue <|> (toS <$> many (noneOf ",)"))
|
||||
pListElement = try (pQuotedValue <* notFollowedBy (noneOf ",)")) <|> (toS <$> many (noneOf ",)"))
|
||||
|
||||
pQuotedValue :: Parser Text
|
||||
pQuotedValue = toS <$> (char '"' *> many (noneOf "\"") <* char '"' <* notFollowedBy (noneOf ",)"))
|
||||
pQuotedValue = toS <$> (char '"' *> many (noneOf "\"") <* char '"')
|
||||
|
||||
pDelimiter :: Parser Char
|
||||
pDelimiter = char '.' <?> "delimiter (.)"
|
||||
@@ -172,16 +186,16 @@ pLogicTree = Stmnt <$> try pLogicFilter
|
||||
pLogicFilter :: Parser Filter
|
||||
pLogicFilter = Filter <$> pField <* pDelimiter <*> pOpExpr pLogicSingleVal
|
||||
pNot :: Parser Bool
|
||||
pNot = try (string "not" *> pDelimiter *> pure True)
|
||||
pNot = try (string "not" *> pDelimiter $> True)
|
||||
<|> pure False
|
||||
<?> "negation operator (not)"
|
||||
pLogicOp :: Parser LogicOperator
|
||||
pLogicOp = try (string "and" *> pure And)
|
||||
<|> string "or" *> pure Or
|
||||
pLogicOp = try (string "and" $> And)
|
||||
<|> string "or" $> Or
|
||||
<?> "logic operator (and, or)"
|
||||
|
||||
pLogicSingleVal :: Parser SingleVal
|
||||
pLogicSingleVal = try pQuotedValue <|> try pPgArray <|> (toS <$> many (noneOf ",)"))
|
||||
pLogicSingleVal = try (pQuotedValue <* notFollowedBy (noneOf ",)")) <|> try pPgArray <|> (toS <$> many (noneOf ",)"))
|
||||
where
|
||||
pPgArray :: Parser Text
|
||||
pPgArray = do
|
||||
@@ -222,9 +236,7 @@ pJSPath = toJSPath <$> (period *> pPath `sepBy` period <* eof)
|
||||
pPath = (,) <$> pJSPKey <*> optionMaybe pJSPIdx
|
||||
|
||||
pJSPKey :: Parser Text
|
||||
pJSPKey = toS <$> (many1 (alphaNum <|> oneOf "_$@") <|> pQuoted) <?> "attribute name [a..z0..9_$@])"
|
||||
where
|
||||
pQuoted = char '"' *> many (noneOf "\"") <* char '"'
|
||||
pJSPKey = toS <$> many1 (alphaNum <|> oneOf "_$@") <|> pQuotedValue <?> "attribute name [a..z0..9_$@])"
|
||||
|
||||
pJSPIdx :: Parser Int
|
||||
pJSPIdx = char '[' *> (read <$> many1 digit) <* char ']' <?> "array index [0..n]"
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE LambdaCase #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
{-|
|
||||
Module : PostgREST.QueryBuilder
|
||||
Description : PostgREST SQL generating functions.
|
||||
@@ -22,16 +23,16 @@ module PostgREST.QueryBuilder (
|
||||
, requestToCountQuery
|
||||
, unquoted
|
||||
, ResultsWithCount
|
||||
, pgFmtEnvVar
|
||||
, pgFmtSetLocal
|
||||
, pgFmtSetLocalSearchPath
|
||||
) where
|
||||
|
||||
import qualified Hasql.Query as H
|
||||
import qualified Hasql.Statement as H
|
||||
import qualified Hasql.Encoders as HE
|
||||
import qualified Hasql.Decoders as HD
|
||||
|
||||
import qualified Data.Aeson as JSON
|
||||
|
||||
import PostgREST.Config (pgVersion96)
|
||||
import PostgREST.RangeQuery (rangeLimit, rangeOffset, allRange)
|
||||
import qualified Data.HashMap.Strict as HM
|
||||
import Data.Maybe
|
||||
@@ -47,7 +48,7 @@ import Data.Scientific ( FPFormat (..)
|
||||
, formatScientific
|
||||
, isInteger
|
||||
)
|
||||
import Protolude hiding (from, intercalate, ord, cast, replace)
|
||||
import Protolude hiding ( intercalate, cast, replace)
|
||||
import PostgREST.ApiRequest (PreferRepresentation (..))
|
||||
|
||||
{-| The generic query result format used by API responses. The location header
|
||||
@@ -57,10 +58,10 @@ import PostgREST.ApiRequest (PreferRepresentation (..))
|
||||
type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString)
|
||||
|
||||
standardRow :: HD.Row ResultsWithCount
|
||||
standardRow = (,,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8
|
||||
<*> HD.value header <*> HD.value HD.bytea
|
||||
standardRow = (,,,) <$> HD.nullableColumn HD.int8 <*> HD.column HD.int8
|
||||
<*> HD.column header <*> HD.column HD.bytea
|
||||
where
|
||||
header = HD.array $ HD.arrayDimension replicateM $ HD.arrayValue HD.bytea
|
||||
header = HD.array $ HD.dimension replicateM $ HD.element HD.bytea
|
||||
|
||||
noLocationF :: Text
|
||||
noLocationF = "array[]::text[]"
|
||||
@@ -75,10 +76,10 @@ decodeStandard =
|
||||
|
||||
decodeStandardMay :: HD.Result (Maybe ResultsWithCount)
|
||||
decodeStandardMay =
|
||||
HD.maybeRow standardRow
|
||||
HD.rowMaybe standardRow
|
||||
|
||||
createReadStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool -> Maybe FieldName ->
|
||||
H.Query () ResultsWithCount
|
||||
H.Statement () ResultsWithCount
|
||||
createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField =
|
||||
unicodeStatement sql HE.unit decodeStandard False
|
||||
where
|
||||
@@ -101,9 +102,9 @@ createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField
|
||||
|
||||
createWriteStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool ->
|
||||
PreferRepresentation -> [Text] ->
|
||||
H.Query ByteString (Maybe ResultsWithCount)
|
||||
H.Statement ByteString (Maybe ResultsWithCount)
|
||||
createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys =
|
||||
unicodeStatement sql (HE.value HE.unknown) decodeStandardMay True
|
||||
unicodeStatement sql (HE.param HE.unknown) decodeStandardMay True
|
||||
|
||||
where
|
||||
sql = case rep of
|
||||
@@ -138,9 +139,9 @@ createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys
|
||||
type ProcResults = (Maybe Int64, Int64, ByteString, ByteString)
|
||||
callProc :: QualifiedIdentifier -> [PgArg] -> Bool -> SqlQuery -> SqlQuery -> Bool ->
|
||||
Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool -> PgVersion ->
|
||||
H.Query ByteString (Maybe ProcResults)
|
||||
H.Statement ByteString (Maybe ProcResults)
|
||||
callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle paramsAsSingleObject asCsv asBinary binaryField isObject pgVer =
|
||||
unicodeStatement sql (HE.value HE.unknown) decodeProc True
|
||||
unicodeStatement sql (HE.param HE.unknown) decodeProc True
|
||||
where
|
||||
sql =
|
||||
if returnsScalar then [qc|
|
||||
@@ -172,18 +173,18 @@ callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle para
|
||||
unwords [
|
||||
"_args_record AS (",
|
||||
"SELECT * FROM " <> (if isObject then "json_to_record" else "json_to_recordset") <> "($1)",
|
||||
"AS _(" <> intercalate ", " ((\a -> pgaName a <> " " <> pgaType a) <$> pgArgs) <> ")",
|
||||
"AS _(" <> intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " " <> pgaType a) <$> pgArgs) <> ")",
|
||||
")"]
|
||||
, intercalate ", " ((\a -> pgaName a <> " := (SELECT " <> pgaName a <> " FROM _args_record)") <$> pgArgs))
|
||||
, intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " := (SELECT " <> pgFmtIdent (pgaName a) <> " FROM _args_record)") <$> pgArgs))
|
||||
countResultF = if countTotal then "( "<> countQuery <> ")" else "null::bigint" :: Text
|
||||
_procName = qiName qi
|
||||
responseHeaders =
|
||||
if pgVer >= pgVersion96
|
||||
then "coalesce(nullif(current_setting('response.headers', true), ''), '[]')" :: Text -- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
|
||||
else "'[]'" :: Text
|
||||
decodeProc = HD.maybeRow procRow
|
||||
procRow = (,,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8
|
||||
<*> HD.value HD.bytea <*> HD.value HD.bytea
|
||||
decodeProc = HD.rowMaybe procRow
|
||||
procRow = (,,,) <$> HD.nullableColumn HD.int8 <*> HD.column HD.int8
|
||||
<*> HD.column HD.bytea <*> HD.column HD.bytea
|
||||
scalarBodyF
|
||||
| asBinary = asBinaryF _procName
|
||||
| otherwise = "(row_to_json(_postgrest_t)->" <> pgFmtLit _procName <> ")::character varying"
|
||||
@@ -207,7 +208,7 @@ pgFmtLit x =
|
||||
else slashed
|
||||
|
||||
requestToCountQuery :: Schema -> DbRequest -> SqlQuery
|
||||
requestToCountQuery _ (DbMutate _) = undefined
|
||||
requestToCountQuery _ (DbMutate _) = witness
|
||||
requestToCountQuery schema (DbRead (Node (Select{where_=logicForest}, (mainTbl, _, _, _, _)) _)) =
|
||||
unwords [
|
||||
"SELECT pg_catalog.count(*)",
|
||||
@@ -229,7 +230,7 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls logicForest
|
||||
("LIMIT " <> maybe "ALL" show (rangeLimit range) <> " OFFSET " <> show (rangeOffset range)) `emptyOnFalse` (isParent || range == allRange) ]
|
||||
|
||||
where
|
||||
mainTbl = fromMaybe nodeName (tableName . relTable <$> maybeRelation)
|
||||
mainTbl = maybe nodeName (tableName . relTable) maybeRelation
|
||||
isSelfJoin = maybe False (\r -> relType r /= Root && relTable r == relFTable r) maybeRelation
|
||||
(qi, tables, joinConds) =
|
||||
let depthAlias name dpth = if dpth /= 0 then name <> "_" <> show dpth else name in -- Root node doesn't get aliased
|
||||
@@ -272,7 +273,7 @@ requestToQuery schema isParent (DbRead (Node (Select colSelects tbls logicForest
|
||||
--the following is just to remove the warning
|
||||
--getQueryParts is not total but requestToQuery is called only after addJoinConditions which ensures the only
|
||||
--posible relations are Child Parent Many
|
||||
getQueryParts _ _ = undefined
|
||||
getQueryParts _ _ = witness
|
||||
requestToQuery schema _ (DbMutate (Insert mainTbl pkCols p@(PayloadJSON _ pType pKeys) onConflct logicForest returnings)) =
|
||||
unwords [
|
||||
("WITH " <> ignoredBody) `emptyOnFalse` not payloadIsEmpty,
|
||||
@@ -288,7 +289,7 @@ requestToQuery schema _ (DbMutate (Insert mainTbl pkCols p@(PayloadJSON _ pType
|
||||
-- Only used for PUT
|
||||
("WHERE " <> intercalate " AND " (pgFmtLogicTree (QualifiedIdentifier "" "_") <$> logicForest)) `emptyOnFalse` null logicForest],
|
||||
maybe "" (\x -> (
|
||||
"ON CONFLICT(" <> intercalate ", " pkCols <> ") " <> case x of
|
||||
"ON CONFLICT(" <> intercalate ", " (pgFmtIdent <$> pkCols) <> ") " <> case x of
|
||||
IgnoreDuplicates ->
|
||||
"DO NOTHING"
|
||||
MergeDuplicates ->
|
||||
@@ -380,8 +381,8 @@ fromQi t = (if s == "" then "" else pgFmtIdent s <> ".") <> pgFmtIdent n
|
||||
n = qiName t
|
||||
s = qiSchema t
|
||||
|
||||
unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Query a b
|
||||
unicodeStatement = H.statement . T.encodeUtf8
|
||||
unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Statement a b
|
||||
unicodeStatement = H.Statement . T.encodeUtf8
|
||||
|
||||
emptyOnFalse :: Text -> Bool -> Text
|
||||
emptyOnFalse val cond = if cond then "" else val
|
||||
@@ -394,8 +395,8 @@ pgFmtField :: QualifiedIdentifier -> Field -> SqlFragment
|
||||
pgFmtField table (c, jp) = pgFmtColumn table c <> pgFmtJsonPath jp
|
||||
|
||||
pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> SqlFragment
|
||||
pgFmtSelectItem table (f@(_, jp), Nothing, alias, _) = pgFmtField table f <> pgFmtAs jp alias
|
||||
pgFmtSelectItem table (f@(_, jp), Just cast, alias, _) = "CAST (" <> pgFmtField table f <> " AS " <> cast <> " )" <> pgFmtAs jp alias
|
||||
pgFmtSelectItem table (f@(fName, jp), Nothing, alias, _) = pgFmtField table f <> pgFmtAs fName jp alias
|
||||
pgFmtSelectItem table (f@(fName, jp), Just cast, alias, _) = "CAST (" <> pgFmtField table f <> " AS " <> cast <> " )" <> pgFmtAs fName jp alias
|
||||
|
||||
pgFmtOrderTerm :: QualifiedIdentifier -> OrderTerm -> SqlFragment
|
||||
pgFmtOrderTerm qi ot = unwords [
|
||||
@@ -413,7 +414,7 @@ pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper
|
||||
|
||||
In vals -> pgFmtField table fld <> " " <>
|
||||
let emptyValForIn = "= any('{}') " in -- Workaround because for postgresql "col IN ()" is invalid syntax, we instead do "col = any('{}')"
|
||||
case ((&&) (length vals == 1) . T.null) <$> headMay vals of
|
||||
case (&&) (length vals == 1) . T.null <$> headMay vals of
|
||||
Just False -> sqlOperator "in" <> "(" <> intercalate ", " (map unknownLiteral vals) <> ") "
|
||||
Just True -> emptyValForIn
|
||||
Nothing -> emptyValForIn
|
||||
@@ -447,21 +448,34 @@ pgFmtLogicTree qi (Expr hasNot op forest) = notOp <> " (" <> intercalate (" " <>
|
||||
where notOp = if hasNot then "NOT" else ""
|
||||
pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt
|
||||
|
||||
pgFmtJsonPath :: Maybe JsonPath -> SqlFragment
|
||||
pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x
|
||||
pgFmtJsonPath (Just (x:xs)) = "->" <> pgFmtLit x <> pgFmtJsonPath ( Just xs )
|
||||
pgFmtJsonPath _ = ""
|
||||
pgFmtJsonPath :: JsonPath -> SqlFragment
|
||||
pgFmtJsonPath = \case
|
||||
[] -> ""
|
||||
(JArrow x:xs) -> "->" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
|
||||
(J2Arrow x:xs) -> "->>" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
|
||||
where
|
||||
pgFmtJsonOperand (JKey k) = pgFmtLit k
|
||||
pgFmtJsonOperand (JIdx i) = pgFmtLit i <> "::int"
|
||||
|
||||
pgFmtAs :: Maybe JsonPath -> Maybe Alias -> SqlFragment
|
||||
pgFmtAs Nothing Nothing = ""
|
||||
pgFmtAs (Just xx) Nothing = case lastMay xx of
|
||||
Just alias -> " AS " <> pgFmtIdent alias
|
||||
pgFmtAs :: FieldName -> JsonPath -> Maybe Alias -> SqlFragment
|
||||
pgFmtAs _ [] Nothing = ""
|
||||
pgFmtAs fName jp Nothing = case jOp <$> lastMay jp of
|
||||
Just (JKey key) -> " AS " <> pgFmtIdent key
|
||||
Just (JIdx _) -> " AS " <> pgFmtIdent (fromMaybe fName lastKey)
|
||||
-- We get the lastKey because on:
|
||||
-- `select=data->1->mycol->>2`, we need to show the result as [ {"mycol": ..}, {"mycol": ..} ]
|
||||
-- `select=data->3`, we need to show the result as [ {"data": ..}, {"data": ..} ]
|
||||
where lastKey = jVal <$> find (\case JKey{} -> True; _ -> False) (jOp <$> reverse jp)
|
||||
Nothing -> ""
|
||||
pgFmtAs _ (Just alias) = " AS " <> pgFmtIdent alias
|
||||
pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias
|
||||
|
||||
pgFmtEnvVar :: Text -> (Text, Text) -> SqlFragment
|
||||
pgFmtEnvVar prefix (k, v) =
|
||||
"set local " <> pgFmtIdent (prefix <> k) <> " = " <> pgFmtLit v <> ";"
|
||||
pgFmtSetLocal :: Text -> (Text, Text) -> SqlFragment
|
||||
pgFmtSetLocal prefix (k, v) =
|
||||
"SET LOCAL " <> pgFmtIdent (prefix <> k) <> " = " <> pgFmtLit v <> ";"
|
||||
|
||||
pgFmtSetLocalSearchPath :: [Text] -> SqlFragment
|
||||
pgFmtSetLocalSearchPath vals =
|
||||
"SET LOCAL search_path = " <> intercalate ", " (pgFmtLit <$> vals) <> ";"
|
||||
|
||||
trimNullChars :: Text -> Text
|
||||
trimNullChars = T.takeWhile (/= '\x0')
|
||||
|
||||
@@ -32,14 +32,13 @@ rangeParse range = do
|
||||
case listToMaybe (range =~ rangeRegex :: [[BS.ByteString]]) of
|
||||
Just parsedRange ->
|
||||
let [_, mLower, mUpper] = readMaybe . toS <$> parsedRange
|
||||
lower = fromMaybe emptyRange (rangeGeq <$> mLower)
|
||||
upper = fromMaybe allRange (rangeLeq <$> mUpper) in
|
||||
lower = maybe emptyRange rangeGeq mLower
|
||||
upper = maybe allRange rangeLeq mUpper in
|
||||
rangeIntersection lower upper
|
||||
Nothing -> allRange
|
||||
|
||||
rangeRequested :: RequestHeaders -> NonnegRange
|
||||
rangeRequested headers = fromMaybe allRange $
|
||||
rangeParse <$> lookup hRange headers
|
||||
rangeRequested headers = maybe allRange rangeParse $ lookup hRange headers
|
||||
|
||||
restrictRange :: Maybe Integer -> NonnegRange -> NonnegRange
|
||||
restrictRange Nothing r = r
|
||||
|
||||
+30
-3
@@ -144,6 +144,9 @@ data RelationType = Child | Parent | Many | Root deriving (Show, Eq)
|
||||
The name 'Relation' here is used with the meaning
|
||||
"What is the relation between the current node and the parent node".
|
||||
It has nothing to do with PostgreSQL referring to tables/views as relations.
|
||||
The order of the relColumns and relFColumns should be maintained to get
|
||||
the join conditions right.
|
||||
TODO merge relColumns and relFColumns to a tuple or Data.Bimap
|
||||
-}
|
||||
data Relation = Relation {
|
||||
relTable :: Table
|
||||
@@ -238,8 +241,16 @@ instance Show LogicOperator where
|
||||
data LogicTree = Expr Bool LogicOperator [LogicTree] | Stmnt Filter deriving (Show, Eq)
|
||||
|
||||
type FieldName = Text
|
||||
type JsonPath = [Text]
|
||||
type Field = (FieldName, Maybe JsonPath)
|
||||
{-|
|
||||
Json path operations as specified in https://www.postgresql.org/docs/9.4/static/functions-json.html
|
||||
-}
|
||||
type JsonPath = [JsonOperation]
|
||||
-- | Represents the single arrow `->` or double arrow `->>` operators
|
||||
data JsonOperation = JArrow{jOp :: JsonOperand} | J2Arrow{jOp :: JsonOperand} deriving (Show, Eq)
|
||||
-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text because we reuse our escaping functons and let pg do the casting with '1'::int
|
||||
data JsonOperand = JKey{jVal :: Text} | JIdx{jVal :: Text} deriving (Show, Eq)
|
||||
|
||||
type Field = (FieldName, JsonPath)
|
||||
type Alias = Text
|
||||
type Cast = Text
|
||||
type NodeName = Text
|
||||
@@ -309,7 +320,23 @@ toMime (CTOther ct) = ct
|
||||
data PgVersion = PgVersion {
|
||||
pgvNum :: Int32
|
||||
, pgvName :: Text
|
||||
} deriving (Eq, Ord, Show)
|
||||
} deriving (Eq, Show)
|
||||
|
||||
instance Ord PgVersion where
|
||||
(PgVersion v1 _) `compare` (PgVersion v2 _) = v1 `compare` v2
|
||||
|
||||
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
||||
minimumPgVersion :: PgVersion
|
||||
minimumPgVersion = PgVersion 90400 "9.4"
|
||||
|
||||
pgVersion95 :: PgVersion
|
||||
pgVersion95 = PgVersion 90500 "9.5"
|
||||
|
||||
pgVersion96 :: PgVersion
|
||||
pgVersion96 = PgVersion 90600 "9.6"
|
||||
|
||||
pgVersion100 :: PgVersion
|
||||
pgVersion100 = PgVersion 100000 "10"
|
||||
|
||||
sourceCTEName :: SqlFragment
|
||||
sourceCTEName = "pg_source"
|
||||
|
||||
+6
-4
@@ -5,11 +5,13 @@ extra-deps:
|
||||
- hjsonpointer-1.1.1
|
||||
- hjsonschema-1.5.0.1
|
||||
- Ranged-sets-0.3.0
|
||||
- protolude-0.2
|
||||
- hasql-1.1
|
||||
- hasql-pool-0.4.3
|
||||
- hasql-transaction-0.5.2
|
||||
- protolude-0.2.2
|
||||
- hasql-1.3
|
||||
- hasql-pool-0.5
|
||||
- hasql-transaction-0.7
|
||||
- text-builder-0.5.1.1
|
||||
- jose-0.7.0.0
|
||||
- postgresql-libpq-0.9.4.1
|
||||
ghc-options:
|
||||
postgrest: -O2 -Werror -Wall -fwarn-identities -fno-warn-redundant-constraints
|
||||
nix:
|
||||
|
||||
@@ -167,7 +167,7 @@ spec =
|
||||
|
||||
context "used with POST" $
|
||||
it "includes related data with filters" $
|
||||
request methodPost "/child_entities?entities.or=(id.eq.2,id.eq.3)&select=id,entities(id)"
|
||||
request methodPost "/child_entities?select=id,entities(id)&entities.or=(id.eq.2,id.eq.3)&entities.order=id"
|
||||
[("Prefer", "return=representation")]
|
||||
[json|[{"id":4,"name":"entity 4","parent_id":1},
|
||||
{"id":5,"name":"entity 5","parent_id":2},
|
||||
@@ -193,10 +193,6 @@ spec =
|
||||
it "can query columns that begin with and/or reserved words" $
|
||||
get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200
|
||||
|
||||
it "can query jsonb columns" $
|
||||
get "/grandchild_entities?or=(jsonb_col->a->>b.eq.foo, jsonb_col->>b.eq.bar)&select=id" `shouldRespondWith`
|
||||
[json|[{id: 4}, {id: 5}]|] { matchStatus = 200, matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "fails when using IN without () and provides meaningful error message" $
|
||||
get "/entities?or=(id.in.1,2,id.eq.3)" `shouldRespondWith`
|
||||
[json|{
|
||||
|
||||
@@ -8,7 +8,7 @@ import Network.HTTP.Types
|
||||
import SpecHelper
|
||||
import Network.Wai (Application)
|
||||
|
||||
import Protolude hiding (get)
|
||||
import Protolude
|
||||
-- }}}
|
||||
|
||||
spec :: SpecWith Application
|
||||
|
||||
@@ -8,7 +8,7 @@ import Network.HTTP.Types
|
||||
import SpecHelper
|
||||
import Network.Wai (Application)
|
||||
|
||||
import Protolude hiding (get)
|
||||
import Protolude
|
||||
-- }}}
|
||||
|
||||
spec :: SpecWith Application
|
||||
|
||||
@@ -8,7 +8,7 @@ import Control.Monad.Base
|
||||
import Control.Monad.Trans.Control
|
||||
import Control.Concurrent.Async (mapConcurrently)
|
||||
|
||||
import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai.Internal
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
|
||||
@@ -11,7 +11,7 @@ import SpecHelper
|
||||
import Network.HTTP.Types
|
||||
import Network.Wai (Application)
|
||||
|
||||
import Protolude hiding (get)
|
||||
import Protolude
|
||||
-- }}}
|
||||
|
||||
spec :: SpecWith Application
|
||||
@@ -45,7 +45,7 @@ spec =
|
||||
"true"
|
||||
respHeaders `shouldSatisfy` matchHeader
|
||||
"Access-Control-Allow-Methods"
|
||||
"GET, POST, PATCH, DELETE, OPTIONS, HEAD"
|
||||
"GET, POST, PATCH, PUT, DELETE, OPTIONS, HEAD"
|
||||
respHeaders `shouldSatisfy` matchHeader
|
||||
"Access-Control-Allow-Headers"
|
||||
"Authentication, Foo, Bar, Accept, Accept-Language, Content-Language"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
module Feature.ExtraSearchPathSpec where
|
||||
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
import Network.HTTP.Types
|
||||
|
||||
import SpecHelper
|
||||
import Network.Wai (Application)
|
||||
|
||||
import Protolude
|
||||
|
||||
spec :: SpecWith Application
|
||||
spec = describe "extra search path" $ do
|
||||
|
||||
it "finds the ltree <@ operator on the public schema" $
|
||||
request methodGet "/ltree_sample?path=cd.Top.Science.Astronomy" [] ""
|
||||
`shouldRespondWith` [json|[
|
||||
{"path":"Top.Science.Astronomy"},
|
||||
{"path":"Top.Science.Astronomy.Astrophysics"},
|
||||
{"path":"Top.Science.Astronomy.Cosmology"}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "finds the ltree nlevel function on the public schema, used through a computed column" $
|
||||
request methodGet "/ltree_sample?select=number_of_labels&path=eq.Top.Science" [] ""
|
||||
`shouldRespondWith` [json|[{"number_of_labels":2}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "finds the isn = operator on the extensions schema" $
|
||||
request methodGet "/isn_sample?id=eq.978-0-393-04002-9&select=name" [] ""
|
||||
`shouldRespondWith` [json|[{"name":"Mathematics: From the Birth of Numbers"}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "finds the isn is_valid function on the extensions schema" $
|
||||
request methodGet "/rpc/is_valid_isbn?input=978-0-393-04002-9" [] ""
|
||||
`shouldRespondWith` [json|true|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
@@ -390,16 +390,6 @@ spec = do
|
||||
[json| [{ a: "keepme", b: null }] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can set a json column to escaped value" $ do
|
||||
_ <- post "/json" [json| { data: {"escaped":"bar"} } |]
|
||||
request methodPatch "/json?data->>escaped=eq.bar"
|
||||
[("Prefer", "return=representation")]
|
||||
[json| { "data": { "escaped":" \"bar" } } |]
|
||||
`shouldRespondWith` [json| [{ "data": { "escaped":" \"bar" } }] |]
|
||||
{ matchStatus = 200
|
||||
, matchHeaders = []
|
||||
}
|
||||
|
||||
it "can update based on a computed column" $
|
||||
request methodPatch
|
||||
"/items?always_true=eq.false"
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
module Feature.JsonOperatorSpec where
|
||||
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
import Network.HTTP.Types
|
||||
|
||||
import SpecHelper
|
||||
import Network.Wai (Application)
|
||||
|
||||
import Protolude hiding (get)
|
||||
|
||||
spec :: SpecWith Application
|
||||
spec = describe "json and jsonb operators" $ do
|
||||
context "Shaping response with select parameter" $ do
|
||||
it "obtains a json subfield one level with casting" $
|
||||
get "/complex_items?id=eq.1&select=settings->>foo::json" `shouldRespondWith`
|
||||
[json| [{"foo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "renames json subfield one level with casting" $
|
||||
get "/complex_items?id=eq.1&select=myFoo:settings->>foo::json" `shouldRespondWith`
|
||||
[json| [{"myFoo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "fails on bad casting (data of the wrong format)" $
|
||||
get "/complex_items?select=settings->foo->>bar::integer"
|
||||
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |]
|
||||
{ matchStatus = 400 , matchHeaders = [] }
|
||||
|
||||
it "obtains a json subfield two levels (string)" $
|
||||
get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith`
|
||||
[json| [{"bar":"baz"}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "renames json subfield two levels (string)" $
|
||||
get "/complex_items?id=eq.1&select=myBar:settings->foo->>bar" `shouldRespondWith`
|
||||
[json| [{"myBar":"baz"}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "obtains a json subfield two levels with casting (int)" $
|
||||
get "/complex_items?id=eq.1&select=settings->foo->>int::integer" `shouldRespondWith`
|
||||
[json| [{"int":1}] |] -- the value in the db is an int, but here we expect a string for now
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "renames json subfield two levels with casting (int)" $
|
||||
get "/complex_items?id=eq.1&select=myInt:settings->foo->>int::integer" `shouldRespondWith`
|
||||
[json| [{"myInt":1}] |] -- the value in the db is an int, but here we expect a string for now
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
-- TODO the status code for the error is 404, this is because 42883 represents undefined function
|
||||
-- this works fine for /rpc/unexistent requests, but for this case a 500 seems more appropriate
|
||||
it "fails when a double arrow ->> is followed with a single arrow ->" $ do
|
||||
get "/json_arr?select=data->>c->1"
|
||||
`shouldRespondWith` [json|
|
||||
{"hint":"No operator matches the given name and argument type(s). You might need to add explicit type casts.",
|
||||
"details":null,"code":"42883","message":"operator does not exist: text -> integer"} |]
|
||||
{ matchStatus = 404 , matchHeaders = [] }
|
||||
get "/json_arr?select=data->>c->b"
|
||||
`shouldRespondWith` [json|
|
||||
{"hint":"No operator matches the given name and argument type(s). You might need to add explicit type casts.",
|
||||
"details":null,"code":"42883","message":"operator does not exist: text -> unknown"} |]
|
||||
{ matchStatus = 404 , matchHeaders = [] }
|
||||
|
||||
context "with array index" $ do
|
||||
it "can get array of ints and alias/cast it" $ do
|
||||
get "/json_arr?select=data->>0::int&id=in.(1,2)" `shouldRespondWith`
|
||||
[json| [{"data":1}, {"data":4}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=idx0:data->>0::int,idx1:data->>1::int&id=in.(1,2)" `shouldRespondWith`
|
||||
[json| [{"idx0":1,"idx1":2}, {"idx0":4,"idx1":5}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can get nested array of ints" $ do
|
||||
get "/json_arr?select=data->0->>1::int&id=in.(3,4)" `shouldRespondWith`
|
||||
[json| [{"data":8}, {"data":7}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data->0->0->>1::int&id=in.(3,4)" `shouldRespondWith`
|
||||
[json| [{"data":null}, {"data":6}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can get array of objects" $ do
|
||||
get "/json_arr?select=data->0->>a&id=in.(5,6)" `shouldRespondWith`
|
||||
[json| [{"a":"A"}, {"a":"[1,2,3]"}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data->0->a->>2&id=in.(5,6)" `shouldRespondWith`
|
||||
[json| [{"a":null}, {"a":"3"}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can get array in object keys" $ do
|
||||
get "/json_arr?select=data->c->>0::json&id=in.(7,8)" `shouldRespondWith`
|
||||
[json| [{"c":1}, {"c":{"d": [4,5,6,7,8]}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data->c->0->d->>4::int&id=in.(7,8)" `shouldRespondWith`
|
||||
[json| [{"d":null}, {"d":8}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "only treats well formed numbers as indexes" $
|
||||
get "/json_arr?select=data->0->0xy1->1->23-xy-45->1->xy-6->>0::int&id=eq.9" `shouldRespondWith`
|
||||
[json| [{"xy-6":3}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "finishing json path with single arrow ->" $ do
|
||||
it "works when finishing with a key" $ do
|
||||
get "/json_arr?select=data->c&id=in.(7,8)" `shouldRespondWith`
|
||||
[json| [{"c":[1,2,3]}, {"c":[{"d": [4,5,6,7,8]}]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data->0->a&id=in.(5,6)" `shouldRespondWith`
|
||||
[json| [{"a":"A"}, {"a":[1,2,3]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when finishing with an index" $ do
|
||||
get "/json_arr?select=data->0->a&id=in.(5,6)" `shouldRespondWith`
|
||||
[json| [{"a":"A"}, {"a":[1,2,3]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data->c->0->d&id=eq.8" `shouldRespondWith`
|
||||
[json| [{"d":[4,5,6,7,8]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "filtering response" $ do
|
||||
it "can filter by properties inside json column" $ do
|
||||
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
|
||||
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json?data->foo->>bar=eq.fake" `shouldRespondWith`
|
||||
[json| [] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can filter by properties inside json column using not" $
|
||||
get "/json?data->foo->>bar=not.eq.baz" `shouldRespondWith`
|
||||
[json| [] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can filter by properties inside json column using ->>" $
|
||||
get "/json?data->>id=eq.1" `shouldRespondWith`
|
||||
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can be filtered with and/or" $
|
||||
get "/grandchild_entities?or=(jsonb_col->a->>b.eq.foo, jsonb_col->>b.eq.bar)&select=id" `shouldRespondWith`
|
||||
[json|[{id: 4}, {id: 5}]|] { matchStatus = 200, matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can filter by array indexes" $ do
|
||||
get "/json_arr?select=data&data->>0=eq.1" `shouldRespondWith`
|
||||
[json| [{"data":[1, 2, 3]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data&data->1->>2=eq.13" `shouldRespondWith`
|
||||
[json| [{"data":[[9, 8, 7], [11, 12, 13]]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data&data->1->>b=eq.B" `shouldRespondWith`
|
||||
[json| [{"data":[{"a": "A"}, {"b": "B"}]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data&data->1->b->>1=eq.5" `shouldRespondWith`
|
||||
[json| [{"data":[{"a": [1,2,3]}, {"b": [4,5]}]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can filter jsonb" $ do
|
||||
get "/jsonb_test?data=eq.{\"e\":1}" `shouldRespondWith`
|
||||
[json| [{"id":4,"data":{"e": 1}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/jsonb_test?data->a=eq.{\"b\":2}" `shouldRespondWith`
|
||||
[json| [{"id":1,"data":{"a": {"b": 2}}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/jsonb_test?data->c=eq.[1,2,3]" `shouldRespondWith`
|
||||
[json| [{"id":2,"data":{"c": [1, 2, 3]}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/jsonb_test?data->0=eq.{\"d\":\"test\"}" `shouldRespondWith`
|
||||
[json| [{"id":3,"data":[{"d": "test"}]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "ordering response" $ do
|
||||
it "orders by a json column property asc" $
|
||||
get "/json?order=data->>id.asc" `shouldRespondWith`
|
||||
[json| [{"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}, {"data": {"id": 3}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "orders by a json column with two level property nulls first" $
|
||||
get "/json?order=data->foo->>bar.nullsfirst" `shouldRespondWith`
|
||||
[json| [{"data": {"id": 3}}, {"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "Patching record, in a nonempty table" $
|
||||
it "can set a json column to escaped value" $ do
|
||||
_ <- post "/json" [json| { data: {"escaped":"bar"} } |]
|
||||
request methodPatch "/json?data->>escaped=eq.bar"
|
||||
[("Prefer", "return=representation")]
|
||||
[json| { "data": { "escaped":" \"bar" } } |]
|
||||
`shouldRespondWith` [json| [{ "data": { "escaped":" \"bar" } }] |]
|
||||
{ matchStatus = 200 , matchHeaders = [] }
|
||||
@@ -8,7 +8,7 @@ import Network.HTTP.Types
|
||||
import SpecHelper
|
||||
import Network.Wai (Application)
|
||||
|
||||
import Protolude hiding (get)
|
||||
import Protolude
|
||||
-- }}}
|
||||
|
||||
spec :: SpecWith Application
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
module Feature.PgVersion95Spec where
|
||||
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
|
||||
import SpecHelper
|
||||
import Network.Wai (Application)
|
||||
|
||||
import Protolude hiding (get)
|
||||
|
||||
spec :: SpecWith Application
|
||||
spec = describe "features supported on PostgreSQL 9.5" $
|
||||
context "json array negative index" $ do
|
||||
it "can select with negative indexes" $ do
|
||||
get "/json_arr?select=data->>-1::int&id=in.(1,2)" `shouldRespondWith`
|
||||
[json| [{"data":3}, {"data":6}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data->0->>-2::int&id=in.(3,4)" `shouldRespondWith`
|
||||
[json| [{"data":8}, {"data":7}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data->-2->>a&id=in.(5,6)" `shouldRespondWith`
|
||||
[json| [{"a":"A"}, {"a":"[1,2,3]"}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can filter with negative indexes" $ do
|
||||
get "/json_arr?select=data&data->>-3=eq.1" `shouldRespondWith`
|
||||
[json| [{"data":[1, 2, 3]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data&data->-1->>-3=eq.11" `shouldRespondWith`
|
||||
[json| [{"data":[[9, 8, 7], [11, 12, 13]]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data&data->-1->>b=eq.B" `shouldRespondWith`
|
||||
[json| [{"data":[{"a": "A"}, {"b": "B"}]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data&data->-1->b->>-1=eq.5" `shouldRespondWith`
|
||||
[json| [{"data":[{"a": [1,2,3]}, {"b": [4,5]}]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "should fail on badly formed negatives" $ do
|
||||
get "/json_arr?select=data->>-78xy" `shouldRespondWith`
|
||||
[json|
|
||||
{"details": "unexpected 'x' expecting digit, \"->\", \"::\" or end of input",
|
||||
"message": "\"failed to parse select parameter (data->>-78xy)\" (line 1, column 11)"} |]
|
||||
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data->>--34" `shouldRespondWith`
|
||||
[json|
|
||||
{"details": "unexpected \"-\" expecting digit",
|
||||
"message": "\"failed to parse select parameter (data->>--34)\" (line 1, column 9)"} |]
|
||||
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||
get "/json_arr?select=data->>-xy-4" `shouldRespondWith`
|
||||
[json|
|
||||
{"details":"unexpected \"x\" expecting digit",
|
||||
"message":"\"failed to parse select parameter (data->>-xy-4)\" (line 1, column 9)"} |]
|
||||
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
|
||||
@@ -1,6 +1,6 @@
|
||||
module Feature.PgVersion96Spec where
|
||||
|
||||
import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
module Feature.ProxySpec where
|
||||
|
||||
import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec
|
||||
|
||||
import SpecHelper
|
||||
|
||||
import Network.Wai (Application)
|
||||
|
||||
import Protolude hiding (get)
|
||||
import Protolude
|
||||
|
||||
spec :: SpecWith Application
|
||||
spec =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module Feature.QueryLimitedSpec where
|
||||
|
||||
import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
import Network.HTTP.Types
|
||||
|
||||
+131
-108
@@ -1,6 +1,6 @@
|
||||
module Feature.QuerySpec where
|
||||
|
||||
import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
import Network.HTTP.Types
|
||||
@@ -216,23 +216,6 @@ spec = do
|
||||
[json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "json subfield one level with casting (json)" $
|
||||
get "/complex_items?id=eq.1&select=settings->>foo::json" `shouldRespondWith`
|
||||
[json| [{"foo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "rename json subfield one level with casting (json)" $
|
||||
get "/complex_items?id=eq.1&select=myFoo:settings->>foo::json" `shouldRespondWith`
|
||||
[json| [{"myFoo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "fails on bad casting (data of the wrong format)" $
|
||||
get "/complex_items?select=settings->foo->>bar::integer"
|
||||
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |]
|
||||
{ matchStatus = 400
|
||||
, matchHeaders = []
|
||||
}
|
||||
|
||||
it "fails on bad casting (wrong cast type)" $
|
||||
get "/complex_items?select=id::fakecolumntype"
|
||||
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |]
|
||||
@@ -240,34 +223,12 @@ spec = do
|
||||
, matchHeaders = []
|
||||
}
|
||||
|
||||
|
||||
it "json subfield two levels (string)" $
|
||||
get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith`
|
||||
[json| [{"bar":"baz"}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "rename json subfield two levels (string)" $
|
||||
get "/complex_items?id=eq.1&select=myBar:settings->foo->>bar" `shouldRespondWith`
|
||||
[json| [{"myBar":"baz"}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
|
||||
it "json subfield two levels with casting (int)" $
|
||||
get "/complex_items?id=eq.1&select=settings->foo->>int::integer" `shouldRespondWith`
|
||||
[json| [{"int":1}] |] -- the value in the db is an int, but here we expect a string for now
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "rename json subfield two levels with casting (int)" $
|
||||
get "/complex_items?id=eq.1&select=myInt:settings->foo->>int::integer" `shouldRespondWith`
|
||||
[json| [{"myInt":1}] |] -- the value in the db is an int, but here we expect a string for now
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "requesting parents and children" $
|
||||
get "/projects?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith`
|
||||
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "requesting parent without specifying primary key" $ do
|
||||
it "requesting parent without specifying primary key" $
|
||||
get "/projects?select=name,client(name)" `shouldRespondWith`
|
||||
[json|[
|
||||
{"name":"Windows 7","client":{"name": "Microsoft"}},
|
||||
@@ -277,9 +238,6 @@ spec = do
|
||||
{"name":"Orphan","client":null}
|
||||
]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/articleStars?select=createdAt,article(owner),user(name)&limit=1" `shouldRespondWith`
|
||||
[json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "requesting parent and renaming primary key" $
|
||||
get "/projects?select=name,client(clientId:id,name)" `shouldRespondWith`
|
||||
@@ -351,40 +309,11 @@ spec = do
|
||||
[json|[{"id":1,"tasks":[{"id":1},{"id":2},{"id":3},{"id":4}]},{"id":2,"tasks":[{"id":5},{"id":6},{"id":7}]},{"id":3,"tasks":[{"id":1},{"id":5}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "requesting parents and children on views" $
|
||||
get "/projects_view?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith`
|
||||
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "requesting parents and children on views with renamed keys" $
|
||||
get "/projects_view_alt?t_id=eq.1&select=t_id, name, clients(*), tasks(id, name)" `shouldRespondWith`
|
||||
[json|[{"t_id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "detects parent relations when having many views of a private table" $ do
|
||||
get "/books?select=title,author(name)&id=eq.5" `shouldRespondWith`
|
||||
[json|[ { "title": "Farenheit 451", "author": { "name": "Ray Bradbury" } } ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/forties_books?select=title,author(name)&limit=1" `shouldRespondWith`
|
||||
[json|[ { "title": "1984", "author": { "name": "George Orwell" } } ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/fifties_books?select=title,author(name)&limit=1" `shouldRespondWith`
|
||||
[json|[ { "title": "The Catcher in the Rye", "author": { "name": "J.D. Salinger" } } ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/sixties_books?select=title,author(name)&limit=1" `shouldRespondWith`
|
||||
[json|[ { "title": "To Kill a Mockingbird", "author": { "name": "Harper Lee" } } ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "requesting children with composite key" $
|
||||
get "/users_tasks?user_id=eq.2&task_id=eq.6&select=*, comments(content)" `shouldRespondWith`
|
||||
[json|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "detect relations in views from exposed schema that are based on tables in private schema and have columns renames" $
|
||||
get "/articles?id=eq.1&select=id,articleStars(users(*))" `shouldRespondWith`
|
||||
[json|[{"id":1,"articleStars":[{"users":{"id":1,"name":"Angela Martin"}},{"users":{"id":2,"name":"Michael Scott"}},{"users":{"id":3,"name":"Dwight Schrute"}}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can embed by FK column name" $
|
||||
get "/projects?id=in.(1,3)&select=id,name,client_id(id,name)" `shouldRespondWith`
|
||||
[json|[{"id":1,"name":"Windows 7","client_id":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":{"id":2,"name":"Apple"}}]|]
|
||||
@@ -400,8 +329,109 @@ spec = do
|
||||
[json|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can detect fk relations through views to tables in the public schema" $
|
||||
get "/consumers_view?select=*,orders_view(*)" `shouldRespondWith` 200
|
||||
describe "view embedding" $ do
|
||||
it "can detect fk relations through views to tables in the public schema" $
|
||||
get "/consumers_view?select=*,orders_view(*)" `shouldRespondWith` 200
|
||||
|
||||
it "can request parent without specifying primary key" $
|
||||
get "/articleStars?select=createdAt,article(owner),user(name)&limit=1" `shouldRespondWith`
|
||||
[json|[{"createdAt":"2015-12-08T04:22:57.472738","article":{"owner": "postgrest_test_authenticator"},"user":{"name": "Angela Martin"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can detect relations in views from exposed schema that are based on tables in private schema and have columns renames" $
|
||||
get "/articles?id=eq.1&select=id,articleStars(users(*))" `shouldRespondWith`
|
||||
[json|[{"id":1,"articleStars":[{"users":{"id":1,"name":"Angela Martin"}},{"users":{"id":2,"name":"Michael Scott"}},{"users":{"id":3,"name":"Dwight Schrute"}}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when requesting parents and children on views" $
|
||||
get "/projects_view?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith`
|
||||
[json|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when requesting parents and children on views with renamed keys" $
|
||||
get "/projects_view_alt?t_id=eq.1&select=t_id, name, clients(*), tasks(id, name)" `shouldRespondWith`
|
||||
[json|[{"t_id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "detects parent relations when having many views of a private table" $ do
|
||||
get "/books?select=title,author(name)&id=eq.5" `shouldRespondWith`
|
||||
[json|[ { "title": "Farenheit 451", "author": { "name": "Ray Bradbury" } } ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/forties_books?select=title,author(name)&limit=1" `shouldRespondWith`
|
||||
[json|[ { "title": "1984", "author": { "name": "George Orwell" } } ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/fifties_books?select=title,author(name)&limit=1" `shouldRespondWith`
|
||||
[json|[ { "title": "The Catcher in the Rye", "author": { "name": "J.D. Salinger" } } ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/sixties_books?select=title,author(name)&limit=1" `shouldRespondWith`
|
||||
[json|[ { "title": "To Kill a Mockingbird", "author": { "name": "Harper Lee" } } ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with views that have subselects" $
|
||||
get "/authors_books_number?select=*,books(title)&id=eq.1" `shouldRespondWith`
|
||||
[json|[ {"id":1, "name":"George Orwell","num_in_forties":1,"num_in_fifties":0,"num_in_sixties":0,"num_in_all_decades":1,
|
||||
"books":[{"title":"1984"}]} ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with views that have case subselects" $
|
||||
get "/authors_have_book_in_decade?select=*,books(title)&id=eq.3" `shouldRespondWith`
|
||||
[json|[ {"id":3,"name":"Antoine de Saint-Exupéry","has_book_in_forties":true,"has_book_in_fifties":false,"has_book_in_sixties":false,
|
||||
"books":[{"title":"The Little Prince"}]} ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with views that have subselect in the FROM clause" $
|
||||
get "/forties_and_fifties_books?select=title,first_publisher,author:authors(name)&id=eq.1" `shouldRespondWith`
|
||||
[json|[{"title":"1984","first_publisher":"Secker & Warburg","author":{"name":"George Orwell"}}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with views that have CTE" $
|
||||
get "/odd_years_publications?select=title,publication_year,first_publisher,author:authors(name)&id=in.(1,2,3)" `shouldRespondWith`
|
||||
[json|[
|
||||
{"title":"1984","publication_year":1949,"first_publisher":"Secker & Warburg","author":{"name":"George Orwell"}},
|
||||
{"title":"The Diary of a Young Girl","publication_year":1947,"first_publisher":"Contact Publishing","author":{"name":"Anne Frank"}},
|
||||
{"title":"The Little Prince","publication_year":1947,"first_publisher":"Reynal & Hitchcock","author":{"name":"Antoine de Saint-Exupéry"}} ]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when having a capitalized table name and camelCase fk column" $
|
||||
get "/foos?select=*,bars(*)" `shouldRespondWith` 200
|
||||
|
||||
it "works when embedding a view with a table that has a long compound pk" $ do
|
||||
get "/player_view?select=id,contract(purchase_price)&id=in.(1,3,5,7)" `shouldRespondWith`
|
||||
[json|
|
||||
[{"id":1,"contract":[{"purchase_price":10}]},
|
||||
{"id":3,"contract":[{"purchase_price":30}]},
|
||||
{"id":5,"contract":[{"purchase_price":50}]},
|
||||
{"id":7,"contract":[]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/contract?select=tournament,player_view(first_name)&limit=3" `shouldRespondWith`
|
||||
[json|
|
||||
[{"tournament":"tournament_1","player_view":{"first_name":"first_name_1"}},
|
||||
{"tournament":"tournament_2","player_view":{"first_name":"first_name_2"}},
|
||||
{"tournament":"tournament_3","player_view":{"first_name":"first_name_3"}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works when embedding a view with a view that referes to a table that has a long compound pk" $ do
|
||||
get "/player_view?select=id,contract_view(purchase_price)&id=in.(1,3,5,7)" `shouldRespondWith`
|
||||
[json|
|
||||
[{"id":1,"contract_view":[{"purchase_price":10}]},
|
||||
{"id":3,"contract_view":[{"purchase_price":30}]},
|
||||
{"id":5,"contract_view":[{"purchase_price":50}]},
|
||||
{"id":7,"contract_view":[]}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/contract_view?select=tournament,player_view(first_name)&limit=3" `shouldRespondWith`
|
||||
[json|
|
||||
[{"tournament":"tournament_1","player_view":{"first_name":"first_name_1"}},
|
||||
{"tournament":"tournament_2","player_view":{"first_name":"first_name_2"}},
|
||||
{"tournament":"tournament_3","player_view":{"first_name":"first_name_3"}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "can embed a view that has group by" $
|
||||
get "/projects_count_grouped_by?select=number_of_projects,client(name)&order=number_of_projects" `shouldRespondWith`
|
||||
[json|
|
||||
[{"number_of_projects":1,"client":null},
|
||||
{"number_of_projects":2,"client":{"name":"Microsoft"}},
|
||||
{"number_of_projects":2,"client":{"name":"Apple"}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
describe "path fixed" $ do
|
||||
it "works when requesting children 2 levels" $
|
||||
@@ -409,13 +439,7 @@ spec = do
|
||||
[json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with parent relation" $ do
|
||||
get "/message?select=id,body,sender:person_detail.sender(name,sent),recipient:person_detail.recipient(name,received)&id=lt.4" `shouldRespondWith`
|
||||
[json|
|
||||
[{"id":1,"body":"Hello Jane","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}},
|
||||
{"id":2,"body":"Hi John","sender":{"name":"Jane","sent":1},"recipient":{"name":"John","received":1}},
|
||||
{"id":3,"body":"How are you doing?","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
it "works with parent relation" $
|
||||
get "/message?select=id,body,sender:person.sender(name),recipient:person.recipient(name)&id=lt.4" `shouldRespondWith`
|
||||
[json|
|
||||
[{"id":1,"body":"Hello Jane","sender":{"name":"John"},"recipient":{"name":"Jane"}},
|
||||
@@ -423,6 +447,14 @@ spec = do
|
||||
{"id":3,"body":"How are you doing?","sender":{"name":"John"},"recipient":{"name":"Jane"}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with a parent view relation" $
|
||||
get "/message?select=id,body,sender:person_detail.sender(name,sent),recipient:person_detail.recipient(name,received)&id=lt.4" `shouldRespondWith`
|
||||
[json|
|
||||
[{"id":1,"body":"Hello Jane","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}},
|
||||
{"id":2,"body":"Hi John","sender":{"name":"Jane","sent":1},"recipient":{"name":"John","received":1}},
|
||||
{"id":3,"body":"How are you doing?","sender":{"name":"John","sent":2},"recipient":{"name":"Jane","received":2}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "works with many<->many relation" $
|
||||
get "/tasks?select=id,users:users.users_tasks(id)" `shouldRespondWith`
|
||||
[json|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|]
|
||||
@@ -630,16 +662,6 @@ spec = do
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/*"]
|
||||
}
|
||||
|
||||
it "by a json column property asc" $
|
||||
get "/json?order=data->>id.asc" `shouldRespondWith`
|
||||
[json| [{"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}, {"data": {"id": 3}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "by a json column with two level property nulls first" $
|
||||
get "/json?order=data->foo->>bar.nullsfirst" `shouldRespondWith`
|
||||
[json| [{"data": {"id": 3}}, {"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "without other constraints" $
|
||||
get "/items?order=id.asc" `shouldRespondWith` 200
|
||||
|
||||
@@ -771,22 +793,6 @@ spec = do
|
||||
respHeaders `shouldSatisfy` matchHeader
|
||||
"Content-Location" "/simple_pk"
|
||||
|
||||
describe "jsonb" $ do
|
||||
it "can filter by properties inside json column" $ do
|
||||
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
|
||||
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
get "/json?data->foo->>bar=eq.fake" `shouldRespondWith`
|
||||
[json| [] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
it "can filter by properties inside json column using not" $
|
||||
get "/json?data->foo->>bar=not.eq.baz" `shouldRespondWith`
|
||||
[json| [] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
it "can filter by properties inside json column using ->>" $
|
||||
get "/json?data->>id=eq.1" `shouldRespondWith`
|
||||
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
describe "weird requests" $ do
|
||||
it "can query as normal" $ do
|
||||
@@ -813,6 +819,20 @@ spec = do
|
||||
[json| [{"escapeId":{"so6meIdColumn":1}},{"escapeId":{"so6meIdColumn":3}},{"escapeId":{"so6meIdColumn":5}}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "will select and filter a column that has spaces" $
|
||||
get "/Server%20Today?select=Just%20A%20Server%20Model&Just%20A%20Server%20Model=like.*91*" `shouldRespondWith`
|
||||
[json|[
|
||||
{"Just A Server Model":" IBM,9113-550 (P5-550)"},
|
||||
{"Just A Server Model":" IBM,9113-550 (P5-550)"},
|
||||
{"Just A Server Model":" IBM,9131-52A (P5-52A)"},
|
||||
{"Just A Server Model":" IBM,9133-55A (P5-55A)"}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "will select and filter a quoted column that has PostgREST reserved characters" $
|
||||
get "/pgrst_reserved_chars?select=%22:arr-%3Eow::cast%22,%22(inside,parens)%22,%22a.dotted.column%22,%22%20%20col%20%20w%20%20space%20%20%22&%22*id*%22=eq.1" `shouldRespondWith`
|
||||
[json|[{":arr->ow::cast":" arrow-1 ","(inside,parens)":" parens-1 ","a.dotted.column":" dotted-1 "," col w space ":" space-1"}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
describe "binary output" $ do
|
||||
context "on GET" $ do
|
||||
it "can query if a single column is selected" $
|
||||
@@ -995,3 +1015,6 @@ spec = do
|
||||
get "/projects_dump?select=id,name&order=id.desc&limit=3" `shouldRespondWith`
|
||||
[json| [{"id":5,"name":"Orphan"}, {"id":4,"name":"OSX"}, {"id":3,"name":"IOS"}] |]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "cannot use ltree(in public schema) extension operators if no extra search path added" $
|
||||
get "/ltree_sample?path=cd.Top.Science.Astronomy" `shouldRespondWith` 400
|
||||
|
||||
+16
-1
@@ -1,6 +1,6 @@
|
||||
module Feature.RpcSpec where
|
||||
|
||||
import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
import Network.HTTP.Types
|
||||
@@ -78,6 +78,16 @@ spec =
|
||||
get "/rpc/sayhello" `shouldRespondWith` 404
|
||||
get "/rpc/sayhello?any_arg=value" `shouldRespondWith` 404
|
||||
|
||||
it "works when having uppercase identifiers" $ do
|
||||
get "/rpc/quotedFunction?user=mscott&fullName=Michael Scott&SSN=401-32-XXXX" `shouldRespondWith`
|
||||
[json|{"user": "mscott", "fullName": "Michael Scott", "SSN": "401-32-XXXX"}|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
post "/rpc/quotedFunction"
|
||||
[json|{"user": "dschrute", "fullName": "Dwight Schrute", "SSN": "030-18-XXXX"}|]
|
||||
`shouldRespondWith`
|
||||
[json|{"user": "dschrute", "fullName": "Dwight Schrute", "SSN": "030-18-XXXX"}|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
context "shaping the response returned by a proc" $ do
|
||||
it "returns a project" $ do
|
||||
post "/rpc/getproject" [json| { "id": 1} |] `shouldRespondWith`
|
||||
@@ -361,3 +371,8 @@ spec =
|
||||
get "/rpc/get_tsearch?text_search_vector=not.fts(english).fun%7Crat" `shouldRespondWith`
|
||||
[json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
it "should work with an argument of custom type in public schema" $
|
||||
get "/rpc/test_arg?my_arg=something" `shouldRespondWith`
|
||||
[json|"foobar"|]
|
||||
{ matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module Feature.StructureSpec where
|
||||
|
||||
import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai
|
||||
import Network.HTTP.Types
|
||||
|
||||
@@ -85,6 +85,27 @@ spec = do
|
||||
|
||||
deleteResponse `shouldBe` Just "No Content"
|
||||
|
||||
it "includes an array type for GET responses" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
|
||||
let childGetSchema = r ^? key "paths"
|
||||
. key "/child_entities"
|
||||
. key "get"
|
||||
. key "responses"
|
||||
. key "200"
|
||||
. key "schema"
|
||||
|
||||
liftIO $
|
||||
childGetSchema `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
"items": {
|
||||
"$ref": "#/definitions/child_entities"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
|]
|
||||
|
||||
it "includes definitions to tables" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
|
||||
@@ -113,7 +134,10 @@ spec = do
|
||||
"format": "integer",
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
|]
|
||||
|
||||
@@ -163,15 +187,54 @@ spec = do
|
||||
]
|
||||
|]
|
||||
|
||||
describe "Materialized view" $
|
||||
|
||||
it "includes materialized view properties" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
|
||||
let method s = key "paths" . key "/materialized_projects" . key s
|
||||
summary = r ^? method "get" . key "summary"
|
||||
description = r ^? method "get" . key "description"
|
||||
parameters = r ^? method "get" . key "parameters"
|
||||
|
||||
liftIO $ do
|
||||
|
||||
summary `shouldBe` Just "A materialized view for projects"
|
||||
|
||||
description `shouldBe` Just "Just a test for materialized views"
|
||||
|
||||
parameters `shouldBe` Just
|
||||
[aesonQQ|
|
||||
[
|
||||
{ "$ref": "#/parameters/rowFilter.materialized_projects.id" },
|
||||
{ "$ref": "#/parameters/rowFilter.materialized_projects.name" },
|
||||
{ "$ref": "#/parameters/rowFilter.materialized_projects.client_id" },
|
||||
{ "$ref": "#/parameters/select" },
|
||||
{ "$ref": "#/parameters/order" },
|
||||
{ "$ref": "#/parameters/range" },
|
||||
{ "$ref": "#/parameters/rangeUnit" },
|
||||
{ "$ref": "#/parameters/offset" },
|
||||
{ "$ref": "#/parameters/limit" },
|
||||
{ "$ref": "#/parameters/preferCount" }
|
||||
]
|
||||
|]
|
||||
|
||||
describe "RPC" $ do
|
||||
|
||||
it "includes body schema for arguments" $ do
|
||||
it "includes function summary/description and body schema for arguments" $ do
|
||||
r <- simpleBody <$> get "/"
|
||||
let args = r ^? key "paths" . key "/rpc/varied_arguments"
|
||||
. key "post" . key "parameters"
|
||||
. nth 0 . key "schema"
|
||||
|
||||
liftIO $
|
||||
let method s = key "paths" . key "/rpc/varied_arguments" . key s
|
||||
args = r ^? method "post" . key "parameters" . nth 0 . key "schema"
|
||||
summary = r ^? method "post" . key "summary"
|
||||
description = r ^? method "post" . key "description"
|
||||
|
||||
liftIO $ do
|
||||
|
||||
summary `shouldBe` Just "An RPC function"
|
||||
|
||||
description `shouldBe` Just "Just a test for RPC function arguments"
|
||||
|
||||
args `shouldBe` Just
|
||||
[aesonQQ|
|
||||
{
|
||||
@@ -213,7 +276,8 @@ spec = do
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
"type": "object",
|
||||
"description": "An RPC function\n\nJust a test for RPC function arguments"
|
||||
}
|
||||
|]
|
||||
|
||||
|
||||
@@ -198,3 +198,30 @@ spec =
|
||||
[("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]
|
||||
[str| [ { "name": "Ruby", "rank": 11 } ]|]
|
||||
`shouldRespondWith` [json|{ "name": "Ruby", "rank": 11 }|] { matchHeaders = [matchContentTypeSingular] }
|
||||
|
||||
context "with a camel case pk column" $ do
|
||||
it "works with POST and merge-duplicates/ignore-duplicates headers" $ do
|
||||
request methodPost "/UnitTest" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
|
||||
[json| [
|
||||
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
|
||||
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
|
||||
]|] `shouldRespondWith` [json|[
|
||||
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
|
||||
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
|
||||
]|]
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
|
||||
}
|
||||
request methodPost "/UnitTest" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
|
||||
[json| [
|
||||
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
|
||||
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
|
||||
]|] `shouldRespondWith` [json|[]|]
|
||||
{ matchStatus = 201
|
||||
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
|
||||
}
|
||||
|
||||
it "works with PUT" $ do
|
||||
put "/UnitTest?idUnitTest=eq.1" [str| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] `shouldRespondWith` 204
|
||||
get "/UnitTest?idUnitTest=eq.1" `shouldRespondWith`
|
||||
[json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] { matchHeaders = [matchContentTypeJson] }
|
||||
|
||||
+31
-15
@@ -4,11 +4,11 @@ import Test.Hspec
|
||||
import SpecHelper
|
||||
|
||||
import qualified Hasql.Pool as P
|
||||
import qualified Hasql.Transaction.Sessions as HT
|
||||
|
||||
import PostgREST.App (postgrest)
|
||||
import PostgREST.Config (pgVersion95, pgVersion96, configSettings)
|
||||
import PostgREST.DbStructure (getDbStructure, getPgVersion, fillSessionWithSettings)
|
||||
import PostgREST.Types (DbStructure(..))
|
||||
import PostgREST.DbStructure (getDbStructure, getPgVersion)
|
||||
import PostgREST.Types (DbStructure(..), pgVersion95, pgVersion96)
|
||||
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, updateAction)
|
||||
import Data.Function (id)
|
||||
import Data.IORef
|
||||
@@ -21,7 +21,9 @@ import qualified Feature.AudienceJwtSecretSpec
|
||||
import qualified Feature.ConcurrentSpec
|
||||
import qualified Feature.CorsSpec
|
||||
import qualified Feature.DeleteSpec
|
||||
import qualified Feature.ExtraSearchPathSpec
|
||||
import qualified Feature.InsertSpec
|
||||
import qualified Feature.JsonOperatorSpec
|
||||
import qualified Feature.NoJwtSpec
|
||||
import qualified Feature.QueryLimitedSpec
|
||||
import qualified Feature.QuerySpec
|
||||
@@ -33,6 +35,7 @@ import qualified Feature.ProxySpec
|
||||
import qualified Feature.AndOrParamsSpec
|
||||
import qualified Feature.RpcSpec
|
||||
import qualified Feature.NonexistentSchemaSpec
|
||||
import qualified Feature.PgVersion95Spec
|
||||
import qualified Feature.PgVersion96Spec
|
||||
import qualified Feature.UpsertSpec
|
||||
|
||||
@@ -45,7 +48,9 @@ main = do
|
||||
|
||||
pool <- P.acquire (3, 10, toS testDbConn)
|
||||
|
||||
result <- P.use pool $ getDbStructure "test" =<< getPgVersion
|
||||
result <- P.use pool $ do
|
||||
ver <- getPgVersion
|
||||
HT.transaction HT.ReadCommitted HT.Read $ getDbStructure "test" ver
|
||||
|
||||
dbStructure <- pure $ either (panic.show) id result
|
||||
|
||||
@@ -53,22 +58,25 @@ main = do
|
||||
|
||||
refDbStructure <- newIORef $ Just dbStructure
|
||||
|
||||
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime $ pure ()
|
||||
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime $ pure ()
|
||||
audJwtApp = return $ postgrest (testCfgAudienceJWT testDbConn) refDbStructure pool getTime $ pure ()
|
||||
asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool getTime $ pure ()
|
||||
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime $ pure ()
|
||||
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime $ pure ()
|
||||
audJwtApp = return $ postgrest (testCfgAudienceJWT testDbConn) refDbStructure pool getTime $ pure ()
|
||||
asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool getTime $ pure ()
|
||||
asymJwkSetApp = return $ postgrest (testCfgAsymJWKSet testDbConn) refDbStructure pool getTime $ pure ()
|
||||
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool getTime $ pure ()
|
||||
extraSearchPathApp = return $ postgrest (testCfgExtraSearchPath testDbConn) refDbStructure pool getTime $ pure ()
|
||||
|
||||
let reset :: IO ()
|
||||
reset = P.use pool (fillSessionWithSettings (configSettings $ testCfg testDbConn)) >> resetDb testDbConn
|
||||
reset = resetDb testDbConn
|
||||
|
||||
actualPgVersion = pgVersion dbStructure
|
||||
extraSpecs =
|
||||
[("Feature.UpsertSpec", Feature.UpsertSpec.spec) | actualPgVersion >= pgVersion95] ++
|
||||
[("Feature.PgVersion95Spec", Feature.PgVersion95Spec.spec) | actualPgVersion >= pgVersion95] ++
|
||||
[("Feature.PgVersion96Spec", Feature.PgVersion96Spec.spec) | actualPgVersion >= pgVersion96]
|
||||
|
||||
specs = uncurry describe <$> [
|
||||
@@ -77,13 +85,13 @@ main = do
|
||||
, ("Feature.CorsSpec" , Feature.CorsSpec.spec)
|
||||
, ("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
|
||||
, ("Feature.InsertSpec" , Feature.InsertSpec.spec)
|
||||
, ("Feature.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec)
|
||||
, ("Feature.QuerySpec" , Feature.QuerySpec.spec)
|
||||
, ("Feature.RpcSpec" , Feature.RpcSpec.spec)
|
||||
, ("Feature.RangeSpec" , Feature.RangeSpec.spec)
|
||||
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
|
||||
, ("Feature.StructureSpec" , Feature.StructureSpec.spec)
|
||||
, ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec)
|
||||
, ("Feature.NonexistentSchemaSpec" , Feature.NonexistentSchemaSpec.spec)
|
||||
] ++ extraSpecs
|
||||
|
||||
hspec $ do
|
||||
@@ -117,6 +125,14 @@ main = do
|
||||
beforeAll_ reset . before asymJwkApp $
|
||||
describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec
|
||||
|
||||
-- this test runs with asymmetric JWKSet
|
||||
beforeAll_ reset . before asymJwkSetApp $
|
||||
describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec
|
||||
|
||||
-- this test runs with a nonexistent db-schema
|
||||
beforeAll_ reset . before nonexistentSchemaApp $
|
||||
describe "Feature.NonexistentSchemaSpec" Feature.NonexistentSchemaSpec.spec
|
||||
|
||||
-- this test runs with an extra search path
|
||||
beforeAll_ reset . before extraSearchPathApp $
|
||||
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec
|
||||
|
||||
+15
-5
@@ -19,7 +19,7 @@ import Text.Heredoc
|
||||
import PostgREST.Config (AppConfig(..))
|
||||
import PostgREST.Types (JSPathExp(..))
|
||||
|
||||
import Test.Hspec hiding (pendingWith)
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai
|
||||
|
||||
import Network.HTTP.Types
|
||||
@@ -61,9 +61,8 @@ validateOpenApiResponse headers = do
|
||||
D4.fetchFilesystemAndValidate schemaContext ((fromJust . decode) respBody) `shouldReturn` Right ()
|
||||
|
||||
getEnvVarWithDefault :: Text -> Text -> IO Text
|
||||
getEnvVarWithDefault var def = do
|
||||
varValue <- getEnv (toS var) `E.catchIOError` const (return $ toS def)
|
||||
return $ toS varValue
|
||||
getEnvVarWithDefault var def = toS <$>
|
||||
getEnv (toS var) `E.catchIOError` const (return $ toS def)
|
||||
|
||||
_baseCfg :: AppConfig
|
||||
_baseCfg = -- Connection Settings
|
||||
@@ -79,6 +78,8 @@ _baseCfg = -- Connection Settings
|
||||
]
|
||||
-- Default role claim key
|
||||
(Right [JSPKey "role"])
|
||||
-- Empty db-extra-search-path
|
||||
[]
|
||||
|
||||
testCfg :: Text -> AppConfig
|
||||
testCfg testDbConn = _baseCfg { configDatabase = testDbConn }
|
||||
@@ -114,9 +115,18 @@ testCfgAsymJWK testDbConn = (testCfg testDbConn) {
|
||||
[str|{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}|]
|
||||
}
|
||||
|
||||
testCfgAsymJWKSet :: Text -> AppConfig
|
||||
testCfgAsymJWKSet testDbConn = (testCfg testDbConn) {
|
||||
configJwtSecret = Just $ encodeUtf8
|
||||
[str|{"keys": [{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}]}|]
|
||||
}
|
||||
|
||||
testNonexistentSchemaCfg :: Text -> AppConfig
|
||||
testNonexistentSchemaCfg testDbConn = (testCfg testDbConn) { configSchema = "nonexistent" }
|
||||
|
||||
testCfgExtraSearchPath :: Text -> AppConfig
|
||||
testCfgExtraSearchPath testDbConn = (testCfg testDbConn) { configExtraSearchPath = ["public", "extensions"] }
|
||||
|
||||
setupDb :: Text -> IO ()
|
||||
setupDb dbConn = do
|
||||
loadFixture dbConn "database"
|
||||
@@ -166,5 +176,5 @@ isErrorFormat s =
|
||||
S.null (S.difference keys validKeys)
|
||||
where
|
||||
obj = decode s :: Maybe (M.Map Text Value)
|
||||
keys = fromMaybe S.empty (M.keysSet <$> obj)
|
||||
keys = maybe S.empty M.keysSet obj
|
||||
validKeys = S.fromList ["message", "details", "hint", "code"]
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
#! /bin/bash
|
||||
#! /usr/bin/env bash
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Please supply the connection uri for the user with create database privileges"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#! /bin/bash
|
||||
#! /usr/bin/env bash
|
||||
if [ -z "$1" ]
|
||||
then
|
||||
echo "Please supply the connection uri for the user with create database privileges"
|
||||
|
||||
Vendored
+88
-10
@@ -250,7 +250,7 @@ INSERT INTO tsearch VALUES (to_tsvector('It''s kind of fun to do the impossible'
|
||||
INSERT INTO tsearch VALUES (to_tsvector('But also fun to do what is possible'));
|
||||
INSERT INTO tsearch VALUES (to_tsvector('Fat cats ate rats'));
|
||||
INSERT INTO tsearch VALUES (to_tsvector('french', 'C''est un peu amusant de faire l''impossible'));
|
||||
INSERT INTO tsearch VALUES (to_tsvector('german', 'Es ist eine Art Spaß, das Unmögliche zu machen'));
|
||||
INSERT INTO tsearch VALUES (to_tsvector('german', 'Es ist eine Art Spaß, das Unmögliche zu machen'));
|
||||
|
||||
--
|
||||
-- Data for Name: users_projects; Type: TABLE DATA; Schema: test; Owner: -
|
||||
@@ -373,16 +373,27 @@ INSERT INTO authors VALUES (7, 'Harper Lee');
|
||||
INSERT INTO authors VALUES (8, 'Kurt Vonnegut');
|
||||
INSERT INTO authors VALUES (9, 'Ken Kesey');
|
||||
|
||||
TRUNCATE TABLE publishers CASCADE;
|
||||
INSERT INTO publishers VALUES (1, 'Secker & Warburg');
|
||||
INSERT INTO publishers VALUES (2, 'Contact Publishing');
|
||||
INSERT INTO publishers VALUES (3, 'Reynal & Hitchcock');
|
||||
INSERT INTO publishers VALUES (4, 'Little, Brown and Company');
|
||||
INSERT INTO publishers VALUES (5, 'Ballantine Books');
|
||||
INSERT INTO publishers VALUES (6, 'Faber and Faber');
|
||||
INSERT INTO publishers VALUES (7, 'J. B. Lippincott & Co.');
|
||||
INSERT INTO publishers VALUES (8, 'Delacorte');
|
||||
INSERT INTO publishers VALUES (9, 'Viking Press & Signet Books');
|
||||
|
||||
TRUNCATE TABLE books CASCADE;
|
||||
INSERT INTO books VALUES (1, '1984', 1949, 1);
|
||||
INSERT INTO books VALUES (2, 'The Diary of a Young Girl', 1947, 2);
|
||||
INSERT INTO books VALUES (3, 'The Little Prince', 1947, 3);
|
||||
INSERT INTO books VALUES (4, 'The Catcher in the Rye', 1951, 4);
|
||||
INSERT INTO books VALUES (5, 'Farenheit 451', 1953, 5);
|
||||
INSERT INTO books VALUES (6, 'Lord of the Flies', 1954, 6);
|
||||
INSERT INTO books VALUES (7, 'To Kill a Mockingbird', 1960, 7);
|
||||
INSERT INTO books VALUES (8, 'Slaughterhouse-Five', 1969, 8);
|
||||
INSERT INTO books VALUES (9, 'One Flew Over the Cuckoo''s Nest', 1962, 9);
|
||||
INSERT INTO books VALUES (1, '1984', 1949, 1, 1);
|
||||
INSERT INTO books VALUES (2, 'The Diary of a Young Girl', 1947, 2, 2);
|
||||
INSERT INTO books VALUES (3, 'The Little Prince', 1947, 3, 3);
|
||||
INSERT INTO books VALUES (4, 'The Catcher in the Rye', 1951, 4, 4);
|
||||
INSERT INTO books VALUES (5, 'Farenheit 451', 1953, 5, 5);
|
||||
INSERT INTO books VALUES (6, 'Lord of the Flies', 1954, 6, 6);
|
||||
INSERT INTO books VALUES (7, 'To Kill a Mockingbird', 1960, 7, 7);
|
||||
INSERT INTO books VALUES (8, 'Slaughterhouse-Five', 1969, 8, 8);
|
||||
INSERT INTO books VALUES (9, 'One Flew Over the Cuckoo''s Nest', 1962, 9, 9);
|
||||
|
||||
SET search_path = test, pg_catalog;
|
||||
|
||||
@@ -411,3 +422,70 @@ INSERT INTO zone VALUES (4, 'store 4', 3, 1);
|
||||
|
||||
-- for foreign table projects_dump
|
||||
copy (select id, name, client_id from projects) to '/tmp/projects_dump.csv' with csv;
|
||||
|
||||
TRUNCATE TABLE "UnitTest" CASCADE;
|
||||
INSERT INTO "UnitTest" VALUES (1, 'unit test 1');
|
||||
|
||||
TRUNCATE TABLE json_arr CASCADE;
|
||||
INSERT INTO json_arr VALUES (1, '[1, 2, 3]');
|
||||
INSERT INTO json_arr VALUES (2, '[4, 5, 6]');
|
||||
INSERT INTO json_arr VALUES (3, '[[9, 8, 7], [11, 12, 13]]');
|
||||
INSERT INTO json_arr VALUES (4, '[[[5, 6], 7, 8]]');
|
||||
INSERT INTO json_arr VALUES (5, '[{"a": "A"}, {"b": "B"}]');
|
||||
INSERT INTO json_arr VALUES (6, '[{"a": [1,2,3]}, {"b": [4,5]}]');
|
||||
INSERT INTO json_arr VALUES (7, '{"c": [1,2,3], "d": [4,5]}');
|
||||
INSERT INTO json_arr VALUES (8, '{"c": [{"d": [4,5,6,7,8]}]}');
|
||||
INSERT INTO json_arr VALUES (9, '[{"0xy1": [1,{"23-xy-45": [2, {"xy-6": [3]}]}]}]');
|
||||
|
||||
TRUNCATE TABLE jsonb_test CASCADE;
|
||||
INSERT INTO jsonb_test VALUES (1, '{ "a": {"b": 2} }');
|
||||
INSERT INTO jsonb_test VALUES (2, '{ "c": [1,2,3] }');
|
||||
INSERT INTO jsonb_test VALUES (3, '[{ "d": "test" }]');
|
||||
INSERT INTO jsonb_test VALUES (4, '{ "e": 1 }');
|
||||
|
||||
TRUNCATE TABLE private.player CASCADE;
|
||||
INSERT into private.player
|
||||
SELECT
|
||||
generate_series,
|
||||
'first_name_' || generate_series,
|
||||
'last_name_' || generate_series,
|
||||
'2018-10-11'
|
||||
FROM generate_series(1, 12);
|
||||
|
||||
TRUNCATE TABLE contract CASCADE;
|
||||
insert into contract
|
||||
select
|
||||
'tournament_' || generate_series,
|
||||
tsrange(now()::timestamp, null),
|
||||
10*generate_series,
|
||||
generate_series,
|
||||
'first_name_' || generate_series,
|
||||
'last_name_' || generate_series,
|
||||
'2018-10-11'
|
||||
from generate_series(1, 6);
|
||||
|
||||
TRUNCATE TABLE ltree_sample CASCADE;
|
||||
INSERT INTO ltree_sample VALUES ('Top');
|
||||
INSERT INTO ltree_sample VALUES ('Top.Science');
|
||||
INSERT INTO ltree_sample VALUES ('Top.Science.Astronomy');
|
||||
INSERT INTO ltree_sample VALUES ('Top.Science.Astronomy.Astrophysics');
|
||||
INSERT INTO ltree_sample VALUES ('Top.Science.Astronomy.Cosmology');
|
||||
|
||||
TRUNCATE TABLE isn_sample CASCADE;
|
||||
INSERT INTO isn_sample VALUES ('978-0-393-04002-9', 'Mathematics: From the Birth of Numbers');
|
||||
|
||||
TRUNCATE TABLE "Server Today" CASCADE;
|
||||
COPY "Server Today" ("cHostname", "Just A Server Model") FROM STDIN CSV DELIMITER '|';
|
||||
argnim1 | IBM,9113-550 (P5-550)
|
||||
argnim2 | IBM,9113-550 (P5-550)
|
||||
daaa2nim71 | IBM,9131-52A (P5-52A)
|
||||
daah3nim71 | IBM,8406-71Y (P7-PS701)
|
||||
hbnim1 | IBM,9133-55A (P5-55A)
|
||||
\.
|
||||
|
||||
TRUNCATE TABLE pgrst_reserved_chars CASCADE;
|
||||
COPY pgrst_reserved_chars ("*id*", ":arr->ow::cast", "(inside,parens)", "a.dotted.column", " col w space ") FROM STDIN CSV DELIMITER '|';
|
||||
1 | arrow-1 | parens-1 | dotted-1 | space-1
|
||||
2 | arrow-2 | parens-2 | dotted-2 | space-2
|
||||
3 | arrow-3 | parens-3 | dotted-3 | space-3
|
||||
\.
|
||||
|
||||
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
||||
set client_min_messages to warning;
|
||||
DROP SCHEMA IF EXISTS test, private, postgrest, jwt, public, تست CASCADE;
|
||||
DROP SCHEMA IF EXISTS test, private, postgrest, jwt, public, تست, extensions CASCADE;
|
||||
DROP TYPE IF EXISTS jwt_token CASCADE;
|
||||
|
||||
Vendored
+19
@@ -5,6 +5,7 @@ GRANT USAGE ON SCHEMA
|
||||
, jwt
|
||||
, public
|
||||
, "تست"
|
||||
, extensions
|
||||
TO postgrest_test_anonymous;
|
||||
|
||||
-- Schema test objects
|
||||
@@ -79,6 +80,24 @@ GRANT ALL ON TABLE
|
||||
, space
|
||||
, zone
|
||||
, projects_dump
|
||||
, "UnitTest"
|
||||
, json_arr
|
||||
, jsonb_test
|
||||
, authors_books_number
|
||||
, authors_have_book_in_decade
|
||||
, forties_and_fifties_books
|
||||
, odd_years_publications
|
||||
, foos
|
||||
, bars
|
||||
, materialized_projects
|
||||
, contract
|
||||
, player_view
|
||||
, contract_view
|
||||
, ltree_sample
|
||||
, isn_sample
|
||||
, projects_count_grouped_by
|
||||
, "Server Today"
|
||||
, pgrst_reserved_chars
|
||||
TO postgrest_test_anonymous;
|
||||
|
||||
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
|
||||
|
||||
Vendored
+213
-15
@@ -17,7 +17,7 @@ CREATE SCHEMA postgrest;
|
||||
CREATE SCHEMA private;
|
||||
CREATE SCHEMA test;
|
||||
CREATE SCHEMA تست;
|
||||
|
||||
CREATE SCHEMA extensions;
|
||||
|
||||
--
|
||||
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -
|
||||
@@ -222,6 +222,10 @@ AS $_$
|
||||
SELECT 'Hi'::text;
|
||||
$_$;
|
||||
|
||||
COMMENT ON FUNCTION varied_arguments(double precision, character varying, boolean, date, money, enum_menagerie_type, integer) IS
|
||||
$_$An RPC function
|
||||
|
||||
Just a test for RPC function arguments$_$;
|
||||
|
||||
--
|
||||
-- Name: jwt_test(); Type: FUNCTION; Schema: test; Owner: -
|
||||
@@ -1120,17 +1124,17 @@ create view images_base64 as (
|
||||
select name, replace(encode(img, 'base64'), E'\n', '') as img from images
|
||||
);
|
||||
|
||||
create function test.ret_enum(val text) returns test.enum_menagerie_type as $$
|
||||
create function test.ret_enum(val text) returns test.enum_menagerie_type as $$
|
||||
select val::test.enum_menagerie_type;
|
||||
$$ language sql;
|
||||
|
||||
create domain one_nine as integer check (value >= 1 and value <= 9);
|
||||
|
||||
create function test.ret_array() returns integer[] as $$
|
||||
create function test.ret_array() returns integer[] as $$
|
||||
select '{1,2,3}'::integer[];
|
||||
$$ language sql;
|
||||
|
||||
create function test.ret_domain(val integer) returns test.one_nine as $$
|
||||
create function test.ret_domain(val integer) returns test.one_nine as $$
|
||||
select val::test.one_nine;
|
||||
$$ language sql;
|
||||
|
||||
@@ -1144,20 +1148,20 @@ $$ language sql;
|
||||
|
||||
create function test.ret_scalars() returns table(
|
||||
a text, b test.enum_menagerie_type, c test.one_nine, d int4range
|
||||
) as $$
|
||||
select row('scalars'::text, enum_first(null::test.enum_menagerie_type),
|
||||
) as $$
|
||||
select row('scalars'::text, enum_first(null::test.enum_menagerie_type),
|
||||
1::test.one_nine, int4range(10, 20));
|
||||
$$ language sql;
|
||||
|
||||
create type test.point_2d as (x integer, y integer);
|
||||
|
||||
create function test.ret_point_2d() returns test.point_2d as $$
|
||||
create function test.ret_point_2d() returns test.point_2d as $$
|
||||
select row(10, 5)::test.point_2d;
|
||||
$$ language sql;
|
||||
|
||||
create type private.point_3d as (x integer, y integer, z integer);
|
||||
|
||||
create function test.ret_point_3d() returns private.point_3d as $$
|
||||
create function test.ret_point_3d() returns private.point_3d as $$
|
||||
select row(7, -3, 4)::private.point_3d;
|
||||
$$ language sql;
|
||||
|
||||
@@ -1171,17 +1175,17 @@ create function test.ret_rows_with_base64_bin() returns setof test.images_base64
|
||||
select i.name, i.img from test.images_base64 i;
|
||||
$$ language sql;
|
||||
|
||||
create function test.single_article(id integer) returns test.articles as $$
|
||||
create function test.single_article(id integer) returns test.articles as $$
|
||||
select a.* from test.articles a where a.id = $1;
|
||||
$$ language sql;
|
||||
|
||||
create function test.get_guc_value(name text) returns text as $$
|
||||
create function test.get_guc_value(name text) returns text as $$
|
||||
select nullif(current_setting(name), '')::text;
|
||||
$$ language sql;
|
||||
|
||||
create table w_or_wo_comma_names ( name text );
|
||||
|
||||
create table items_with_different_col_types (
|
||||
create table items_with_different_col_types (
|
||||
int_data integer,
|
||||
text_data text,
|
||||
bool_data bool,
|
||||
@@ -1194,20 +1198,20 @@ create table items_with_different_col_types (
|
||||
|
||||
-- Tables used for testing complex boolean logic with and/or query params
|
||||
|
||||
create table entities (
|
||||
create table entities (
|
||||
id integer primary key,
|
||||
name text,
|
||||
arr integer[],
|
||||
text_search_vector tsvector
|
||||
);
|
||||
|
||||
create table child_entities (
|
||||
create table child_entities (
|
||||
id integer primary key,
|
||||
name text,
|
||||
parent_id integer references entities(id)
|
||||
);
|
||||
|
||||
create table grandchild_entities (
|
||||
create table grandchild_entities (
|
||||
id integer primary key,
|
||||
name text,
|
||||
parent_id integer references child_entities(id),
|
||||
@@ -1396,11 +1400,17 @@ create table private.authors(
|
||||
name text
|
||||
);
|
||||
|
||||
create table private.publishers(
|
||||
id integer primary key,
|
||||
name text
|
||||
);
|
||||
|
||||
create table private.books(
|
||||
id integer primary key,
|
||||
title text,
|
||||
publication_year smallint,
|
||||
author_id integer references private.authors(id)
|
||||
author_id integer references private.authors(id),
|
||||
first_publisher_id integer references private.publishers(id)
|
||||
);
|
||||
|
||||
create view test.authors as select id, name from private.authors;
|
||||
@@ -1451,3 +1461,191 @@ comment on foreign table projects_dump is
|
||||
$$A temporary projects dump
|
||||
|
||||
Just a test for foreign tables$$;
|
||||
|
||||
create table "UnitTest"(
|
||||
"idUnitTest" integer primary key,
|
||||
"nameUnitTest" text
|
||||
);
|
||||
|
||||
create table json_arr(
|
||||
id integer primary key,
|
||||
data pg_catalog.json
|
||||
);
|
||||
|
||||
create table jsonb_test(
|
||||
id integer primary key,
|
||||
data jsonb
|
||||
);
|
||||
|
||||
create view test.authors_books_number as
|
||||
select
|
||||
id,
|
||||
name,
|
||||
(
|
||||
select
|
||||
count(*)
|
||||
from forties_books where author_id = authors.id
|
||||
) as num_in_forties,
|
||||
(
|
||||
select
|
||||
count(*)
|
||||
from fifties_books where author_id = authors.id
|
||||
) as num_in_fifties,
|
||||
(
|
||||
select
|
||||
count(*)
|
||||
from sixties_books where author_id = authors.id
|
||||
) as num_in_sixties,
|
||||
(
|
||||
select
|
||||
count(*)
|
||||
from (
|
||||
select id
|
||||
from forties_books where author_id = authors.id
|
||||
union
|
||||
select id
|
||||
from fifties_books where author_id = authors.id
|
||||
union
|
||||
select id
|
||||
from sixties_books where author_id = authors.id
|
||||
) _
|
||||
) as num_in_all_decades
|
||||
from private.authors;
|
||||
|
||||
create view test.authors_have_book_in_decade as
|
||||
select
|
||||
id,
|
||||
name,
|
||||
case
|
||||
when (x.id in (select author_id from test.forties_books))
|
||||
then true
|
||||
else false
|
||||
end as has_book_in_forties,
|
||||
case
|
||||
when (x.id in (select author_id from test.fifties_books))
|
||||
then true
|
||||
else false
|
||||
end as has_book_in_fifties,
|
||||
case
|
||||
when (x.id in (select author_id from test.sixties_books))
|
||||
then true
|
||||
else false
|
||||
end as has_book_in_sixties
|
||||
from private.authors x;
|
||||
|
||||
create view test.forties_and_fifties_books as
|
||||
select x.id, x.title, x.publication_year, y.name as first_publisher, x.author_id
|
||||
from (
|
||||
select id, title, publication_year, author_id, first_publisher_id from private.books
|
||||
where publication_year >= 1940 and publication_year < 1960) x
|
||||
join private.publishers y on y.id = x.first_publisher_id;
|
||||
|
||||
create view test.odd_years_publications as
|
||||
with
|
||||
odd_years_books as(
|
||||
select id, title, publication_year, author_id, first_publisher_id
|
||||
from private.books
|
||||
where publication_year % 2 <> 0
|
||||
)
|
||||
select
|
||||
x.id, x.title, x.publication_year,
|
||||
y.name as first_publisher, x.author_id
|
||||
from odd_years_books x
|
||||
join private.publishers y on y.id = x.first_publisher_id;
|
||||
|
||||
create view test.projects_count_grouped_by as
|
||||
select
|
||||
client_id,
|
||||
count(id) as number_of_projects
|
||||
from projects
|
||||
group by client_id;
|
||||
|
||||
CREATE TABLE test."Foo"(
|
||||
id int primary key,
|
||||
name text
|
||||
);
|
||||
|
||||
CREATE TABLE test.bar(
|
||||
id int primary key,
|
||||
name text,
|
||||
"fooId" int references "Foo"(id)
|
||||
);
|
||||
|
||||
CREATE VIEW test.foos as select id,name from "Foo";
|
||||
CREATE VIEW test.bars as select id, "fooId", name from bar;
|
||||
|
||||
create materialized view materialized_projects as
|
||||
select id, name, client_id from projects;
|
||||
|
||||
comment on materialized view materialized_projects is
|
||||
$$A materialized view for projects
|
||||
|
||||
Just a test for materialized views$$;
|
||||
|
||||
create or replace function test."quotedFunction"("user" text, "fullName" text, "SSN" text)
|
||||
returns jsonb AS $$
|
||||
select format('{"user": "%s", "fullName": "%s", "SSN": "%s"}', "user", "fullName", "SSN")::jsonb;
|
||||
$$ language sql;
|
||||
|
||||
create table private.player (
|
||||
id integer not null,
|
||||
first_name text not null,
|
||||
last_name text not null,
|
||||
birth_date date,
|
||||
primary key (last_name, id, first_name, birth_date) -- just for testing a long compound pk
|
||||
);
|
||||
|
||||
create table test.contract (
|
||||
tournament text not null,
|
||||
time tsrange not null,
|
||||
purchase_price int not null,
|
||||
id integer not null,
|
||||
first_name text not null,
|
||||
last_name text not null,
|
||||
birth_date date,
|
||||
foreign key (last_name, id, first_name, birth_date) references private.player
|
||||
);
|
||||
|
||||
create view test.player_view as select * from private.player;
|
||||
|
||||
create view test.contract_view as select * from test.contract;
|
||||
|
||||
create type public.my_type AS enum ('something');
|
||||
|
||||
create function test.test_arg(my_arg public.my_type) returns text as $$
|
||||
select 'foobar'::text;
|
||||
$$ language sql;
|
||||
|
||||
create extension if not exists ltree with schema public;
|
||||
|
||||
create table test.ltree_sample (
|
||||
path public.ltree
|
||||
);
|
||||
|
||||
CREATE FUNCTION test.number_of_labels(test.ltree_sample) RETURNS integer AS $$
|
||||
SELECT nlevel($1.path)
|
||||
$$ language sql;
|
||||
|
||||
create extension if not exists isn with schema extensions;
|
||||
|
||||
create table test.isn_sample (
|
||||
id extensions.isbn,
|
||||
name text
|
||||
);
|
||||
|
||||
create function test.is_valid_isbn(input text) returns boolean as $$
|
||||
select is_valid(input::isbn);
|
||||
$$ language sql;
|
||||
|
||||
create table "Server Today"(
|
||||
"cHostname" text,
|
||||
"Just A Server Model" text
|
||||
);
|
||||
|
||||
create table test.pgrst_reserved_chars (
|
||||
"*id*" integer,
|
||||
":arr->ow::cast" text,
|
||||
"(inside,parens)" text,
|
||||
"a.dotted.column" text,
|
||||
" col w space " text
|
||||
);
|
||||
|
||||
@@ -82,6 +82,24 @@ readSecretFromFile(){
|
||||
pgrStop
|
||||
}
|
||||
|
||||
readDbUriFromFile(){
|
||||
pgrConfig="dburi-from-file.config"
|
||||
pgrStartRead "./configs/$pgrConfig" "./dburis/$1"
|
||||
while pgrStarted && test "$( rootStatus )" -ne 200
|
||||
do
|
||||
# wait for the server to start
|
||||
sleep 0.1 \
|
||||
|| sleep 1 # fallback: subsecond sleep is not standard and may fail
|
||||
done
|
||||
if pgrStarted
|
||||
then
|
||||
ok "connection with $2 dburi read from a file"
|
||||
else
|
||||
ko "failed to read $2 dburi from a file"
|
||||
fi
|
||||
pgrStop
|
||||
}
|
||||
|
||||
reqWithRoleClaimKey(){
|
||||
export ROLE_CLAIM_KEY=$1
|
||||
pgrStart "./configs/role-claim-key.config"
|
||||
@@ -120,6 +138,49 @@ invalidRoleClaimKey(){
|
||||
pgrStop
|
||||
}
|
||||
|
||||
# ensure iat claim is successful in the presence of pgrst time cache, see https://github.com/PostgREST/postgrest/issues/1139
|
||||
ensureIatClaimWorks(){
|
||||
pgrStart "./configs/simple.config"
|
||||
while pgrStarted && test "$( rootStatus )" -ne 200
|
||||
do
|
||||
# wait for the server to start
|
||||
sleep 0.1 \
|
||||
|| sleep 1 # fallback: subsecond sleep is not standard and may fail
|
||||
done
|
||||
for i in {1..10}; do \
|
||||
iatJwt=$(psql -qtAX postgrest_test -c "select jwt.sign(row_to_json(r), 'reallyreallyreallyreallyverysafe') from ( select 'postgrest_test_author' as role, extract(epoch from now()) as iat) r")
|
||||
httpStatus="$( authorsStatus $iatJwt )"
|
||||
if test "$httpStatus" -ne 200
|
||||
then
|
||||
ko "iat claim rejected with $httpStatus"
|
||||
return
|
||||
fi
|
||||
sleep .5;\
|
||||
done
|
||||
ok "accepted iat claim"
|
||||
pgrStop
|
||||
}
|
||||
|
||||
# ensure app settings don't reset on pool timeout of 10 seconds, see https://github.com/PostgREST/postgrest/issues/1141
|
||||
ensureAppSettings(){
|
||||
pgrStart "./configs/app-settings.config"
|
||||
while pgrStarted && test "$( rootStatus )" -ne 200
|
||||
do
|
||||
# wait for the server to start
|
||||
sleep 0.1 \
|
||||
|| sleep 1 # fallback: subsecond sleep is not standard and may fail
|
||||
done
|
||||
sleep 11
|
||||
response=$(curl -s "http://localhost:$pgrPort/rpc/get_guc_value?name=app.settings.external_api_secret")
|
||||
if test "$response" = "\"0123456789abcdef\""
|
||||
then
|
||||
ok "GET /rpc/get_guc_value response is $response"
|
||||
else
|
||||
ko "GET /rpc/get_guc_value response was $response"
|
||||
fi
|
||||
pgrStop
|
||||
}
|
||||
|
||||
# PRE: curl must be available
|
||||
test -n "$(command -v curl)" || bailOut 'curl is not available'
|
||||
|
||||
@@ -144,6 +205,9 @@ readSecretFromFile ascii.b64 'Base64 (ASCII)'
|
||||
readSecretFromFile utf8.b64 'Base64 (UTF-8)'
|
||||
readSecretFromFile binary.b64 'Base64 (binary)'
|
||||
|
||||
readDbUriFromFile uri.noeol "(no EOL)"
|
||||
readDbUriFromFile uri.txt "(EOL)"
|
||||
|
||||
reqWithRoleClaimKey '.postgrest.a_role' '{"postgrest":{"a_role":"postgrest_test_author"}}' 200
|
||||
reqWithRoleClaimKey '.customObject.manyRoles[1]' '{"customObject":{"manyRoles": ["other", "postgrest_test_author"]}}' 200
|
||||
reqWithRoleClaimKey '."https://www.example.com/roles"[0].value' '{"https://www.example.com/roles":[{"value":"postgrest_test_author"}]}' 200
|
||||
@@ -157,6 +221,9 @@ invalidRoleClaimKey '.#$%&$%/'
|
||||
invalidRoleClaimKey ''
|
||||
invalidRoleClaimKey 1234
|
||||
|
||||
ensureIatClaimWorks
|
||||
ensureAppSettings
|
||||
|
||||
cleanUp
|
||||
|
||||
exit $failedTests
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
db-uri = "postgres:///postgrest_test"
|
||||
db-schema = "test"
|
||||
db-anon-role = "postgrest_test_anonymous"
|
||||
db-pool = 1
|
||||
server-host = "127.0.0.1"
|
||||
server-port = 49421
|
||||
|
||||
app.settings.external_api_secret = "0123456789abcdef"
|
||||
@@ -2,7 +2,7 @@ db-uri = "postgres:///postgrest_test"
|
||||
db-schema = "test"
|
||||
db-anon-role = "postgrest_test_anonymous"
|
||||
db-pool = 1
|
||||
server-host = "*4"
|
||||
server-host = "127.0.0.1"
|
||||
server-port = 49421
|
||||
|
||||
# Read secret from a file: /dev/stdin (alias for standard input)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
db-uri = "@/dev/stdin"
|
||||
db-schema = "test"
|
||||
db-anon-role = "postgrest_test_anonymous"
|
||||
db-pool = 1
|
||||
server-host = "127.0.0.1"
|
||||
server-port = 49421
|
||||
jwt-secret = "reallyreallyreallyreallyverysafe"
|
||||
@@ -2,7 +2,7 @@ db-uri = "postgres:///postgrest_test"
|
||||
db-schema = "test"
|
||||
db-anon-role = "postgrest_test_anonymous"
|
||||
db-pool = 1
|
||||
server-host = "*4"
|
||||
server-host = "127.0.0.1"
|
||||
server-port = 49421
|
||||
role-claim-key = "$(ROLE_CLAIM_KEY)"
|
||||
jwt-secret = "reallyreallyreallyreallyverysafe"
|
||||
|
||||
@@ -2,7 +2,7 @@ db-uri = "postgres:///postgrest_test"
|
||||
db-schema = "test"
|
||||
db-anon-role = "postgrest_test_anonymous"
|
||||
db-pool = 1
|
||||
server-host = "*4"
|
||||
server-host = "127.0.0.1"
|
||||
server-port = 49421
|
||||
|
||||
# Read secret from a file: /dev/stdin (alias for standard input)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
db-uri = "postgres:///postgrest_test"
|
||||
db-schema = "test"
|
||||
db-anon-role = "postgrest_test_anonymous"
|
||||
db-pool = 1
|
||||
server-host = "127.0.0.1"
|
||||
server-port = 49421
|
||||
jwt-secret = "reallyreallyreallyreallyverysafe"
|
||||
@@ -0,0 +1 @@
|
||||
postgres:///postgrest_test
|
||||
@@ -0,0 +1 @@
|
||||
postgres:///postgrest_test
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#! /usr/bin/env bash
|
||||
currentTest=1
|
||||
failedTests=0
|
||||
result(){ echo "$1 $currentTest $2"; currentTest=$(( $currentTest + 1 )); }
|
||||
@@ -96,20 +96,20 @@ setUp
|
||||
|
||||
echo "Running memory usage tests.."
|
||||
|
||||
jsonKeyTest "1M" "POST" "/rpc/leak" "15M"
|
||||
jsonKeyTest "1M" "POST" "/leak" "15M"
|
||||
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1" "15M"
|
||||
jsonKeyTest "1M" "POST" "/rpc/leak" "20M"
|
||||
jsonKeyTest "1M" "POST" "/leak" "20M"
|
||||
jsonKeyTest "1M" "PATCH" "/leak?id=eq.1" "20M"
|
||||
|
||||
jsonKeyTest "10M" "POST" "/rpc/leak" "105M"
|
||||
jsonKeyTest "10M" "POST" "/leak" "105M"
|
||||
jsonKeyTest "10M" "PATCH" "/leak?id=eq.1" "105M"
|
||||
|
||||
jsonKeyTest "100M" "POST" "/rpc/leak" "895M"
|
||||
jsonKeyTest "100M" "POST" "/leak" "895M"
|
||||
jsonKeyTest "100M" "PATCH" "/leak?id=eq.1" "895M"
|
||||
jsonKeyTest "50M" "POST" "/rpc/leak" "500M"
|
||||
jsonKeyTest "50M" "POST" "/leak" "500M"
|
||||
jsonKeyTest "50M" "PATCH" "/leak?id=eq.1" "500M"
|
||||
|
||||
postJsonArrayTest "1000" "/perf_articles" "20M"
|
||||
postJsonArrayTest "10000" "/perf_articles" "120M"
|
||||
postJsonArrayTest "10000" "/perf_articles" "150M"
|
||||
postJsonArrayTest "100000" "/perf_articles" "1.15G"
|
||||
|
||||
cleanUp
|
||||
|
||||
@@ -2,7 +2,7 @@ db-uri = "postgres:///postgrest_test"
|
||||
db-schema = "test"
|
||||
db-anon-role = "postgrest_test_anonymous"
|
||||
db-pool = 1
|
||||
server-host = "*4"
|
||||
server-host = "127.0.0.1"
|
||||
server-port = 49421
|
||||
|
||||
jwt-secret = "reallyreallyreallyreallyverysafe"
|
||||
|
||||
Reference in New Issue
Block a user