Compare commits

...
18 Commits
Author SHA1 Message Date
steve-chavez 1e732ac94a Bump to v5.2.0 2018-12-12 18:59:10 -05:00
steve-chavez 6b2778749f Add ability to quote column names on filters 2018-12-12 12:07:30 -05:00
steve-chavez 36f86827ee Fix #1008, Handle columns that contain spaces 2018-12-12 12:07:30 -05:00
steve-chavez d78410473e Add test for embedding on a view with group by 2018-12-12 12:07:30 -05:00
Steve ChávezandGitHub 501edc718d Add db-extra-search-path config (#1218)
For adding schemas to the search_path, solves issues related to extensions created in the public schema.
2018-12-08 11:39:31 -05:00
周丰andSteve Chávez 0d6d112b38 Read db-uri configuration from a separate file. (#1215)
* Read dburi configuration from a separate file.

* Add changelog entry.
2018-12-04 13:58:29 -05:00
Russell DaviesandSteve Chávez 473ac70789 Add support for parsing JSON Web Key Sets 2018-11-13 13:58:17 -05:00
steve-chavez dadfe965b9 Do hlint 2.1 hints 2018-11-06 11:54:10 -05:00
steve-chavez 63ead89470 Fix #1181, correct qualify of function argument type 2018-11-06 11:54:10 -05:00
steve-chavez ab23ed7999 Add CHANGELOG entry for allowing CORS on PUT 2018-10-21 13:38:34 -05:00
Kyle RussellandSteve Chávez 2da6bd6d1c Config: apply CorsResourcePolicy to PUT requests
PUT requests are part of the public upsert API, so if postgrest
runs at a different origin, these requests will fail because they
don't pass CORS.
2018-10-21 13:30:52 -05:00
steve-chavez 5bfb68b982 Fix #1180, embedding on views with composite pks
Also add CHANGELOG entries for previous fixes.
2018-10-12 09:24:22 -05:00
steve-chavez dc834572d6 Fix #1182, fix subselect view embedding for pg10 2018-10-12 09:24:22 -05:00
steve-chavez b48824bddd Fix #1065, correct test for pg10
Also add pg10 testing job on CircleCI
2018-10-12 09:24:22 -05:00
steve-chavez 6d326fe341 Update README.md 2018-10-12 09:22:58 -05:00
Roger LAMandSteve Chávez d94cf2ed72 tiny typo? 2018-10-11 09:00:27 -05:00
steve-chavez 27ca6b4e90 Pin hasql versions in cabal and stack 2018-09-28 08:57:26 -05:00
Ben GamariandSteve Chávez e0cc4d1571 Update hasql
Move to hasql 1.3.
2018-09-28 08:57:26 -05:00
44 changed files with 742 additions and 341 deletions
+46 -6
View File
@@ -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:
+17
View File
@@ -9,6 +9,23 @@ 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
+21 -10
View File
@@ -1,20 +1,21 @@
![Logo](static/logo.png "Logo")
[![Build Status](https://circleci.com/gh/PostgREST/postgrest/tree/master.svg?style=shield)](https://circleci.com/gh/PostgREST/postgrest/tree/master)
[![Donate](https://img.shields.io/badge/Donate-Patreon-orange.svg?colorB=F96854)](https://www.patreon.com/postgrest)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](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>
[![Join the chat at https://gitter.im/begriffs/postgrest](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/begriffs/postgrest)
[![Docs](https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat)](http://postgrest.org)
[![Hackage docs](https://img.shields.io/hackage/v/postgrest.svg?label=hackage)](http://hackage.haskell.org/package/postgrest)
[![Docker Stars](https://img.shields.io/docker/pulls/postgrest/postgrest.svg)](https://hub.docker.com/r/postgrest/postgrest/)
[![Donate](https://img.shields.io/badge/patreon-donate-orange.svg?colorB=F96854)](https://www.patreon.com/postgrest)
[![Build Status](https://circleci.com/gh/PostgREST/postgrest/tree/master.svg?style=shield)](https://circleci.com/gh/PostgREST/postgrest/tree/master)
[![Hackage docs](https://img.shields.io/hackage/v/postgrest.svg?label=hackage)](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.
@@ -24,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
@@ -52,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
@@ -73,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
@@ -81,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
@@ -93,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
@@ -107,7 +108,17 @@ 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
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:
+1 -1
View File
@@ -10,7 +10,7 @@
},
"POSTGREST_VER": {
"description": "Version of PostgREST to deploy",
"value": "5.1.0"
"value": "5.2.0"
},
"DB_URI": {
"description": "Database connection string, e.g. postgres://user:pass@xxxxxxx.rds.amazonaws.com/mydb",
+47 -32
View File
@@ -3,38 +3,38 @@
module Main where
import PostgREST.App (postgrest)
import PostgREST.Config (AppConfig (..),
minimumPgVersion,
prettyVersion, readOptions)
import PostgREST.DbStructure (getDbStructure, getPgVersion)
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)
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
@@ -83,7 +83,7 @@ connectionWorker mainTid pool schema refDbStructure refIsWorkerOn = do
("Cannot run in this PostgreSQL version, PostgREST needs at least "
<> pgvName minimumPgVersion)
killThread mainTid
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
@@ -139,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
@@ -278,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}
+11 -8
View File
@@ -2,7 +2,7 @@ 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: 5.1.0
version: 5.2.0
synopsis: REST API for any Postgres database
license: MIT
license-file: LICENSE
@@ -33,8 +33,9 @@ executable postgrest
default-language: Haskell2010
build-depends: auto-update
, base >= 4.8 && < 4.10
, hasql == 1.1
, hasql-pool == 0.4.3
, hasql >= 1.3 && < 1.4
, hasql-pool >= 0.5 && < 0.6
, hasql-transaction >= 0.7 && < 0.8
, postgrest
, protolude == 0.2.2
, text
@@ -64,9 +65,9 @@ library
, contravariant-extras
, either
, gitrev
, hasql == 1.1
, hasql-pool == 0.4.3
, hasql-transaction == 0.5.2
, hasql >= 1.3 && < 1.4
, hasql-pool >= 0.5 && < 0.6
, hasql-transaction >= 0.7 && < 0.8
, heredoc
, HTTP
, http-types
@@ -123,6 +124,7 @@ Test-Suite spec
, Feature.ConcurrentSpec
, Feature.CorsSpec
, Feature.DeleteSpec
, Feature.ExtraSearchPathSpec
, Feature.InsertSpec
, Feature.JsonOperatorSpec
, Feature.NoJwtSpec
@@ -152,8 +154,9 @@ Test-Suite spec
, cassava
, containers
, contravariant
, hasql == 1.1
, hasql-pool == 0.4.3
, hasql >= 1.3 && < 1.4
, hasql-pool >= 0.5 && < 0.6
, hasql-transaction >= 0.7 && < 0.8
, heredoc
, hjsonschema == 1.5.0.1
, hspec
+4 -5
View File
@@ -110,8 +110,7 @@ userApiRequest schema req reqBody
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 -14
View File
@@ -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
@@ -65,7 +65,7 @@ import Protolude hiding (intercalate, Proxy)
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
@@ -102,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
@@ -131,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
@@ -162,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
@@ -191,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
@@ -224,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
@@ -248,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
@@ -286,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)
@@ -316,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
@@ -397,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
+22 -12
View File
@@ -16,7 +16,7 @@ module PostgREST.Auth (
containsRole
, jwtClaims
, JWTAttempt(..)
, parseJWK
, parseSecret
) where
import Control.Lens.Operators
@@ -43,7 +43,7 @@ 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
@@ -83,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))
+14 -18
View File
@@ -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
@@ -143,6 +141,7 @@ readOptions = do
<*> pure False
<*> (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
@@ -179,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 (
@@ -202,7 +205,7 @@ 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
|
@@ -212,7 +215,7 @@ readOptions = do
|## 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
@@ -226,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
@@ -233,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"
+6 -6
View File
@@ -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,12 +225,12 @@ 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)
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
+149 -140
View File
@@ -14,7 +14,7 @@ module PostgREST.DbStructure (
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
@@ -25,23 +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)
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 schema $ 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
@@ -56,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)
@@ -155,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')"
@@ -182,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
@@ -195,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
@@ -243,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} ->
@@ -279,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]
_ -> [])
@@ -324,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
@@ -347,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
@@ -532,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,
@@ -575,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|
/*
@@ -685,68 +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 Schema [Synonym]
allSynonyms cols =
H.statement sql (HE.value HE.text) (decodeSynonyms cols) True
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 sql = [q|
with
views as (
where
subselectRegex :: Text
subselectRegex | pgVer < pgVersion100 = ":subselect {.*?:constraintDeps <>} :location"
| otherwise = ":subselect {.*?:stmt_len 0} :location"
sql = [qc|
with
views as (
select
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
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
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
view_schema, view_name,
regexp_replace(view_definition, ':subselect {.*?:constraintDeps <>} :location', '', '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;
|]
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
@@ -756,7 +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
versionRow = HD.singleRow $ PgVersion <$> HD.column HD.int4 <*> HD.column HD.text
+7 -4
View File
@@ -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
+6 -6
View File
@@ -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, pgFmtSetLocal)
import PostgREST.QueryBuilder (unquoted, pgFmtSetLocal, pgFmtSetLocalSearchPath)
import Protolude hiding (concat, null)
import Protolude
runWithClaims :: AppConfig -> JWTAttempt ->
(ApiRequest -> H.Transaction Response) ->
@@ -32,7 +32,7 @@ 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 ++ appSettingsSql
H.sql $ toS . mconcat $ setSearchPathSql : setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql ++ appSettingsSql
mapM_ H.sql customReqCheck
app req
where
@@ -40,9 +40,9 @@ runWithClaims conf eClaims app 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 $
(\r -> "set local role " <> r <> ";") . toS . pgFmtLit . unquoted <$> M.lookup "role" claimsWithRole
setSchemaSql = ["set schema " <> pgFmtLit (configSchema conf) <> ";"] :: [Text]
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
+1 -1
View File
@@ -60,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 =
+14 -14
View File
@@ -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)
@@ -68,17 +69,18 @@ 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 $> '-'
pJsonPath :: Parser JsonPath
pJsonPath = many pJsonOperation
@@ -151,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 (.)"
@@ -184,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
@@ -234,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]"
+24 -20
View File
@@ -24,15 +24,15 @@ module PostgREST.QueryBuilder (
, unquoted
, ResultsWithCount
, 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
@@ -48,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
@@ -58,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[]"
@@ -76,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
@@ -102,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
@@ -139,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|
@@ -182,9 +182,9 @@ callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle para
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"
@@ -230,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
@@ -381,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
@@ -414,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
@@ -471,7 +471,11 @@ pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias
pgFmtSetLocal :: Text -> (Text, Text) -> SqlFragment
pgFmtSetLocal prefix (k, v) =
"set local " <> pgFmtIdent (prefix <> k) <> " = " <> pgFmtLit 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')
+3 -4
View File
@@ -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
+20 -1
View File
@@ -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
@@ -317,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"
+4 -3
View File
@@ -6,9 +6,10 @@ extra-deps:
- hjsonschema-1.5.0.1
- Ranged-sets-0.3.0
- protolude-0.2.2
- hasql-1.1
- hasql-pool-0.4.3
- hasql-transaction-0.5.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:
+1 -1
View File
@@ -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},
+1 -1
View File
@@ -8,7 +8,7 @@ import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
import Protolude
-- }}}
spec :: SpecWith Application
+1 -1
View File
@@ -8,7 +8,7 @@ import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
import Protolude
-- }}}
spec :: SpecWith Application
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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"
+37
View File
@@ -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] }
+1 -1
View File
@@ -8,7 +8,7 @@ import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
import Protolude
-- }}}
spec :: SpecWith Application
+1 -1
View File
@@ -1,6 +1,6 @@
module Feature.PgVersion95Spec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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 -1
View File
@@ -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
+56 -1
View File
@@ -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
@@ -395,6 +395,44 @@ spec = do
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" $
get "/clients?id=eq.1&select=id,projects:projects.client_id(id,tasks(id))" `shouldRespondWith`
@@ -781,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" $
@@ -963,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
+6 -1
View File
@@ -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
@@ -371,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 -1
View File
@@ -1,6 +1,6 @@
module Feature.StructureSpec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec
import Test.Hspec.Wai
import Network.HTTP.Types
+25 -13
View File
@@ -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)
import PostgREST.DbStructure (getDbStructure, getPgVersion)
import PostgREST.Types (DbStructure(..))
import PostgREST.Types (DbStructure(..), pgVersion95, pgVersion96)
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, updateAction)
import Data.Function (id)
import Data.IORef
@@ -21,6 +21,7 @@ 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
@@ -47,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
@@ -55,15 +58,17 @@ 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 = resetDb testDbConn
@@ -87,7 +92,6 @@ main = do
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
, ("Feature.StructureSpec" , Feature.StructureSpec.spec)
, ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec)
, ("Feature.NonexistentSchemaSpec" , Feature.NonexistentSchemaSpec.spec)
] ++ extraSpecs
hspec $ do
@@ -121,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
View File
@@ -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"]
+48 -1
View File
@@ -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: -
@@ -442,3 +442,50 @@ 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
\.
+1 -1
View File
@@ -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;
+9
View File
@@ -5,6 +5,7 @@ GRANT USAGE ON SCHEMA
, jwt
, public
, "تست"
, extensions
TO postgrest_test_anonymous;
-- Schema test objects
@@ -89,6 +90,14 @@ GRANT ALL ON TABLE
, 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;
+71 -1
View File
@@ -17,7 +17,7 @@ CREATE SCHEMA postgrest;
CREATE SCHEMA private;
CREATE SCHEMA test;
CREATE SCHEMA تست;
CREATE SCHEMA extensions;
--
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -
@@ -1553,6 +1553,13 @@ select
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
@@ -1579,3 +1586,66 @@ create or replace function test."quotedFunction"("user" text, "fullName" 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
);
+21
View File
@@ -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"
@@ -187,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
@@ -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"
+1
View File
@@ -0,0 +1 @@
postgres:///postgrest_test
+1
View File
@@ -0,0 +1 @@
postgres:///postgrest_test