Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3d670b9c03 | ||
|
|
5bf644867f | ||
|
|
fcdae73f49 | ||
|
|
1656fb9f57 | ||
|
|
594327924c | ||
|
|
480800edbd | ||
|
|
9b01d1b1ab | ||
|
|
ed810bc380 | ||
|
|
789db9a017 | ||
|
|
52626cc86d | ||
|
|
32b97ef076 | ||
|
|
8e319321c9 | ||
|
|
cdac6d385c | ||
|
|
d65d011e5e | ||
|
|
28f771324d | ||
|
|
cf04fbd6ea | ||
|
|
bb511f0df2 | ||
|
|
f564fb0977 | ||
|
|
3b017dfdf6 | ||
|
|
cb7d00b839 | ||
|
|
010e18ea0b | ||
|
|
c34f96ce2e | ||
|
|
f9d50018d9 | ||
|
|
6c1233fcec | ||
|
|
acd8e92d24 | ||
|
|
559d370a89 | ||
|
|
4cf51b7005 | ||
|
|
4e77492797 | ||
|
|
f16e2e3ee5 | ||
|
|
ebc8c387e0 | ||
|
|
734484714c | ||
|
|
adac39bd7c | ||
|
|
9d5011e864 | ||
|
|
ca40ba1fda | ||
|
|
894455f2cd | ||
|
|
6cb73062a9 | ||
|
|
86c68d191c | ||
|
|
d20c252cb3 | ||
|
|
cd6b688f7f | ||
|
|
49f41d8edb | ||
|
|
b45953dff8 | ||
|
|
8075d7e51a | ||
|
|
ab0170ffaf | ||
|
|
449cacdacf | ||
|
|
e724c2df00 | ||
|
|
12dc180065 | ||
|
|
8eae978eae | ||
|
|
019d53bca1 | ||
|
|
e1d7dc3dea | ||
|
|
28a2826fa8 | ||
|
|
c76864a653 | ||
|
|
c0d44232a5 | ||
|
|
e34e92eb44 | ||
|
|
956f73d997 | ||
|
|
df04d26c15 | ||
|
|
9c69553373 | ||
|
|
6604293ac1 | ||
|
|
60a61adbce | ||
|
|
b56ab47f84 | ||
|
|
25492a089b | ||
|
|
e87be593c0 | ||
|
|
1937363fc8 | ||
|
|
d68cbec25c | ||
|
|
f4011e5d8c | ||
|
|
e93c96a6f8 | ||
|
|
070f67e9c6 | ||
|
|
61dac3b02b | ||
|
|
169157ec6d | ||
|
|
bd304c9fc3 | ||
|
|
3989aaa144 | ||
|
|
1d51a5f543 | ||
|
|
e4dafad64d | ||
|
|
3f31c60f1d | ||
|
|
83f48dcd15 | ||
|
|
1cc53245c5 | ||
|
|
f24ba048af | ||
|
|
a980db6d2d | ||
|
|
4277284a69 | ||
|
|
b070994912 | ||
|
|
988df54e53 |
@@ -5,3 +5,5 @@ cabal.sandbox.config
|
||||
hscope.out
|
||||
codex.tags
|
||||
.anvil
|
||||
.stack-work
|
||||
tags
|
||||
|
||||
@@ -3,6 +3,32 @@
|
||||
All notable changes to this project will be documented in this file.
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [0.2.11.0] - 2015-08-28
|
||||
### Added
|
||||
- Negate any filter in a uniform way, e.g. `?col=not.eq=foo` - @diogob
|
||||
- Call stored procedures
|
||||
- Filter NOT IN values, e.g. `?col=notin.1,2,3` - @rall
|
||||
- CSV responses to GET requests with `Accept: text/csv` - @diogob
|
||||
- Debian init scripts - @mkhon
|
||||
- Allow filters by computed columns - @diogob
|
||||
|
||||
### Fixed
|
||||
- Reset user role on error
|
||||
- Compatible with Stack
|
||||
- Add materialized views to results in GET / - @diogob
|
||||
- Indicate insertable=true for views that are insertable through triggers - @diogob
|
||||
- Builds under GHC 7.10
|
||||
- Allow the use of columns named "count" in relations queried - @diogob
|
||||
|
||||
## [0.2.10.0] - 2015-06-03
|
||||
### Added
|
||||
- Full text search, eg `/foo?text_vector=@@.bar`
|
||||
- Include auth id as well as db role to views (for row-level security)
|
||||
|
||||
## [0.2.9.1] - 2015-05-20
|
||||
### Fixed
|
||||
- Put -Werror behind a cabal flag (for CI) so Hackage accepts package
|
||||
|
||||
## [0.2.9.0] - 2015-05-20
|
||||
### Added
|
||||
- Return range headers in PATCH
|
||||
|
||||
+8
-3
@@ -39,12 +39,17 @@ your contributions.
|
||||
committers. Continuous integration will check this as well on every
|
||||
pull request.
|
||||
|
||||
* For help building the Haskell code on your computer check out the [building from
|
||||
source](https://github.com/begriffs/postgrest/wiki/Building-from-source)
|
||||
wiki page.
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Schedule
|
||||
|
||||
Currently I (@begriffs) am the sole maintainer, and while I am
|
||||
overjoyed to help resolve issues I also have to balance this with
|
||||
my other obligations. I check and respond to github issues **once
|
||||
per week** (on Mondays). So if you don't get a response right away
|
||||
don't worry, I will definitely get to it.
|
||||
my other obligations. If you don't get a response right away
|
||||
don't worry, I will definitely get to it. Also you can join the
|
||||
Gitter [chat room](https://gitter.im/begriffs/postgrest) to
|
||||
discuss issues you are having.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||

|
||||
|
||||
[](https://circleci.com/gh/begriffs/postgrest/tree/master)
|
||||
[](https://circleci.com/gh/begriffs/postgrest/tree/master)
|
||||
<a href="https://heroku.com/deploy?template=https://github.com/begriffs/postgrest">
|
||||
<img src="static/heroku.png" alt="Deploy">
|
||||
<img src="https://img.shields.io/badge/%E2%86%91_Deploy_to-Heroku-7056bf.svg" alt="Deploy">
|
||||
</a>
|
||||
[](https://gitter.im/begriffs/postgrest)
|
||||
|
||||
PostgREST serves a fully RESTful API from any existing PostgreSQL
|
||||
database. It provides a cleaner, more standards-compliant, faster
|
||||
@@ -20,7 +21,7 @@ your own projects.
|
||||
|
||||
### Usage
|
||||
|
||||
Download the binary ([OS X](http://bin.begriffs.com/dbapi/osx/postgrest-0.2.9.0.tar.xz) / [Linux](http://bin.begriffs.com/dbapi/heroku/postgrest-0.2.9.0.tar.xz)) and invoke like so:
|
||||
Download the binary ([latest release](https://github.com/begriffs/postgrest/releases/latest)) and invoke like so:
|
||||
|
||||
```bash
|
||||
postgrest --db-host localhost --db-port 5432 \
|
||||
@@ -75,12 +76,14 @@ Other optimizations are possible, and some are outlined in the
|
||||
|
||||
### Security
|
||||
|
||||
PostgREST handles authentication (HTTP Basic over SSL) and delegates
|
||||
authorization to the role information defined in the database. This
|
||||
ensures there is a single declarative source of truth for security.
|
||||
When dealing with the database the server assumes the identity of
|
||||
the currently authenticated user, and for the duration of the
|
||||
connection cannot do anything the user themselves couldn't.
|
||||
PostgREST handles authentication (HTTP Basic over SSL or [JSON Web
|
||||
Tokens](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions#json-web-tokens))
|
||||
and delegates authorization to the role information defined in the
|
||||
database. This ensures there is a single declarative source of truth
|
||||
for security. When dealing with the database the server assumes
|
||||
the identity of the currently authenticated user, and for the
|
||||
duration of the connection cannot do anything the user themselves
|
||||
couldn't.
|
||||
|
||||
Postgres 9.5 will soon support true [row-level
|
||||
security](http://michael.otacoo.com/postgresql-2/postgres-9-5-feature-highlight-row-level-security/).
|
||||
@@ -154,13 +157,19 @@ and the [guide to routing](https://github.com/begriffs/postgrest/wiki/Routing).
|
||||
* [Performance](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling)
|
||||
* [Security](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions)
|
||||
* [Tutorial](http://blog.jonharrington.org/postgrest-introduction/) (external)
|
||||
* [Heroku](https://github.com/begriffs/postgrest/wiki/Heroku)
|
||||
|
||||
### Thanks
|
||||
|
||||
* [Adam Baker](https://github.com/adambaker) for code
|
||||
contributions and many fundamental design discussions
|
||||
* [Diogo Biazus](https://github.com/diogob) for many improvements
|
||||
and deep postgresql knowledge
|
||||
* [Nikita Volkov](https://github.com/nikita-volkov) for writing the
|
||||
wonderful [Hasql](https://github.com/nikita-volkov/hasql) library
|
||||
and helping me use it
|
||||
* [Mikey Casalaina](https://github.com/casalaina) for the cool logo
|
||||
* [Jonathan Harrington](https://github.com/prio) for writing a nice tutorial
|
||||
* [Jonathan Harrington](https://github.com/prio) for writing a [nice
|
||||
tutorial](http://blog.jonharrington.org/postgrest-introduction/)
|
||||
* [Federico Rampazzo](https://github.com/framp) for suggesting and
|
||||
implementing [JWT](http://jwt.io/) support
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
},
|
||||
"POSTGREST_VER": {
|
||||
"description": "Version of PostgREST to deploy",
|
||||
"value": "0.2.9.0"
|
||||
"value": "0.2.11.0"
|
||||
},
|
||||
"DB_NAME": {
|
||||
"description": "Database name",
|
||||
|
||||
@@ -4,6 +4,12 @@ machine:
|
||||
- createdb -O postgrest_test -U ubuntu postgrest_test
|
||||
ghc:
|
||||
version: 7.8.3
|
||||
dependencies:
|
||||
override:
|
||||
- cabal update
|
||||
- cabal sandbox init
|
||||
- cabal install --upgrade-dependencies --constraint="template-haskell installed" --dependencies-only --enable-tests
|
||||
- cabal configure --enable-tests -f ci
|
||||
test:
|
||||
post:
|
||||
- cabal exec hlint -- -X QuasiQuotes src/**/*.hs test/**/*.hs
|
||||
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/sh
|
||||
d=$(dirname $0)
|
||||
if [ -f /etc/default/postgrest ]; then
|
||||
. /etc/default/postgrest
|
||||
fi
|
||||
POSTGREST_LOG=${POSTGREST_LOG:-/var/log/postgrest/postgrest.log}
|
||||
|
||||
exec $d/postgrest "$@" >>$POSTGREST_LOG 2>&1 &
|
||||
Vendored
+23
@@ -0,0 +1,23 @@
|
||||
# run service as
|
||||
#POSTGREST_USER=postgrest
|
||||
|
||||
# log file
|
||||
#POSTGREST_LOG=/var/log/postgrest/postgrest.log
|
||||
|
||||
# database host
|
||||
#POSTGREST_DBHOST=localhost
|
||||
|
||||
# database to use
|
||||
#POSTGREST_DBNAME=
|
||||
|
||||
# database user
|
||||
#POSTGREST_DBUSER=postgres
|
||||
|
||||
# database password
|
||||
#POSTGREST_DBPASS=
|
||||
|
||||
# database pool
|
||||
#POSTGREST_DBPOOL=10
|
||||
|
||||
# additional options
|
||||
#POSTGREST_OPTS=
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/sh
|
||||
### BEGIN INIT INFO
|
||||
# Provides: postgrest
|
||||
# Required-Start: $local_fs $network postgresql
|
||||
# Required-Stop: $local_fs $network
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Description: PostgreSQL REST API daemon
|
||||
### END INIT INFO
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
if test -f /etc/default/postgrest; then
|
||||
. /etc/default/postgrest
|
||||
fi
|
||||
POSTGREST=/usr/local/bin/postgrest
|
||||
POSTGREST_USER=${POSTGREST_USER:-postgrest}
|
||||
POSTGREST_DBNAME=${POSTGREST_DBNAME:-postgres}
|
||||
POSTGREST_DBUSER=${POSTGREST_DBUSER:-postgres}
|
||||
if [ -n "$POSTGREST_DBHOST" ]; then
|
||||
POSTGREST_OPTS="$POSTGREST_OPTS --db-host $POSTGREST_DBHOST"
|
||||
fi
|
||||
if [ -n "$POSTGREST_DBNAME" ]; then
|
||||
POSTGREST_OPTS="$POSTGREST_OPTS --db-name $POSTGREST_DBNAME"
|
||||
fi
|
||||
if [ -n "$POSTGREST_DBUSER" ]; then
|
||||
POSTGREST_OPTS="$POSTGREST_OPTS --db-user $POSTGREST_DBUSER"
|
||||
POSTGREST_OPTS="$POSTGREST_OPTS --anonymous $POSTGREST_DBUSER"
|
||||
fi
|
||||
if [ -n "$POSTGREST_DBPASS" ]; then
|
||||
POSTGREST_OPTS="$POSTGREST_OPTS --db-pass $POSTGREST_DBPASS"
|
||||
fi
|
||||
if [ -n "$POSTGREST_DBPOOL" ]; then
|
||||
POSTGREST_OPTS="$POSTGREST_OPTS --db-pool $POSTGREST_DBPOOL"
|
||||
fi
|
||||
POSTGREST_OPTS="$POSTGREST_OPTS --v1schema public"
|
||||
|
||||
start()
|
||||
{
|
||||
log_daemon_msg "Starting PostgreSQL REST API daemon" "postgrest" || true
|
||||
if start-stop-daemon --start --quiet --oknodo --chuid ${POSTGREST_USER} --startas /usr/local/bin/postgrest-wrapper --exec $POSTGREST -- $POSTGREST_OPTS; then
|
||||
log_end_msg 0 || true
|
||||
else
|
||||
log_end_msg 1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
stop()
|
||||
{
|
||||
log_daemon_msg "Stopping PostgreSQL REST API daemon" "postgrest" || true
|
||||
if start-stop-daemon --stop --quiet --oknodo --exec $POSTGREST; then
|
||||
log_end_msg 0 || true
|
||||
else
|
||||
log_end_msg 1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
status()
|
||||
{
|
||||
status_of_proc $POSTGREST postgrest && exit 0 || exit $?
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
start)
|
||||
start
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
restart)
|
||||
stop
|
||||
start
|
||||
;;
|
||||
status)
|
||||
status
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {start|stop|restart|status}"
|
||||
esac
|
||||
+34
-21
@@ -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: 0.2.9.0
|
||||
version: 0.2.11.0
|
||||
synopsis: REST API for any Postgres database
|
||||
license: MIT
|
||||
license-file: LICENSE
|
||||
@@ -12,6 +12,14 @@ maintainer: cred+github@begriffs.com
|
||||
category: Web
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
source-repository head
|
||||
type: git
|
||||
location: git://github.com/begriffs/postgrest.git
|
||||
|
||||
Flag CI
|
||||
Description: No warnings allowed in continuous integration
|
||||
Manual: True
|
||||
Default: False
|
||||
|
||||
executable postgrest
|
||||
main-is: PostgREST/Main.hs
|
||||
@@ -19,21 +27,21 @@ executable postgrest
|
||||
default-language: Haskell2010
|
||||
build-depends: base >=4.6 && <5
|
||||
, postgrest
|
||||
, hasql == 0.7.3.1, hasql-backend == 0.4.1
|
||||
, hasql-postgres == 0.10.3.1
|
||||
, hasql >= 0.7.3 && < 0.8
|
||||
, hasql-backend >= 0.4.1 && < 0.5
|
||||
, hasql-postgres >= 0.10.4 && < 0.11
|
||||
, warp >= 3.0.2, wai >= 3.0.1
|
||||
, wai-extra, wai-cors
|
||||
, wai-middleware-static >= 0.6.0
|
||||
, HTTP, convertible, http-types
|
||||
, case-insensitive
|
||||
, scientific, time
|
||||
, aeson, network >= 2.6
|
||||
, aeson >= 0.8, network >= 2.6
|
||||
, bytestring, text, split, string-conversions
|
||||
, stringsearch
|
||||
, containers, unordered-containers
|
||||
, optparse-applicative == 0.11.*
|
||||
, regex-base, regex-tdfa
|
||||
, regex-tdfa-text
|
||||
, Ranged-sets
|
||||
, transformers, MissingH
|
||||
, bcrypt >= 0.0.6, base64-string
|
||||
@@ -47,29 +55,32 @@ executable postgrest
|
||||
hs-source-dirs: src
|
||||
|
||||
library
|
||||
ghc-options: -Wall -W -O2
|
||||
if flag(ci)
|
||||
ghc-options: -Wall -W -Werror
|
||||
else
|
||||
ghc-options: -Wall -W -O2
|
||||
|
||||
default-language: Haskell2010
|
||||
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
|
||||
build-depends: base >=4.6 && <5
|
||||
, hasql == 0.7.3.1, hasql-backend == 0.4.1
|
||||
, hasql-postgres == 0.10.3.1
|
||||
, warp >= 3.0.2, wai >= 3.0.1
|
||||
, hasql, hasql-backend
|
||||
, hasql-postgres
|
||||
, warp, wai
|
||||
, wai-extra, wai-cors
|
||||
, wai-middleware-static >= 0.6.0
|
||||
, wai-middleware-static
|
||||
, HTTP, convertible, http-types
|
||||
, case-insensitive
|
||||
, scientific, time
|
||||
, aeson, network >= 2.6
|
||||
, aeson, network
|
||||
, bytestring, text, split, string-conversions
|
||||
, stringsearch
|
||||
, containers, unordered-containers
|
||||
, optparse-applicative == 0.11.*
|
||||
, optparse-applicative
|
||||
, regex-base, regex-tdfa
|
||||
, regex-tdfa-text
|
||||
, Ranged-sets
|
||||
, transformers, MissingH
|
||||
, bcrypt >= 0.0.6, base64-string
|
||||
, network-uri >= 2.6
|
||||
, bcrypt, base64-string
|
||||
, network-uri
|
||||
, resource-pool
|
||||
, blaze-builder
|
||||
, vector
|
||||
@@ -91,7 +102,10 @@ Test-Suite spec
|
||||
Default-Language: Haskell2010
|
||||
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
|
||||
Hs-Source-Dirs: test, src
|
||||
ghc-options: -Wall -W -Werror
|
||||
if flag(ci)
|
||||
ghc-options: -Wall -W -Werror
|
||||
else
|
||||
ghc-options: -Wall -W -O2
|
||||
Main-Is: Main.hs
|
||||
Other-Modules: PostgREST.App
|
||||
, PostgREST.Auth
|
||||
@@ -103,10 +117,10 @@ Test-Suite spec
|
||||
, PostgREST.RangeQuery
|
||||
, Spec
|
||||
, SpecHelper
|
||||
Build-Depends: base, hspec >= 2.1.2, QuickCheck
|
||||
, hspec-wai >= 0.5.0, hspec-wai-json
|
||||
, hasql == 0.7.3.1, hasql-backend == 0.4.1
|
||||
, hasql-postgres == 0.10.3.1
|
||||
Build-Depends: base, hspec == 2.1.*, QuickCheck
|
||||
, hspec-wai, hspec-wai-json
|
||||
, hasql, hasql-backend
|
||||
, hasql-postgres
|
||||
, warp, wai
|
||||
, packdeps, hlint
|
||||
, HTTP, convertible
|
||||
@@ -121,7 +135,6 @@ Test-Suite spec
|
||||
, regex-base
|
||||
, string-conversions
|
||||
, http-media, regex-tdfa
|
||||
, regex-tdfa-text
|
||||
, Ranged-sets
|
||||
, transformers, MissingH, split
|
||||
, bcrypt, base64-string
|
||||
|
||||
+87
-42
@@ -1,22 +1,23 @@
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
module PostgREST.App (app, sqlError, isSqlError) where
|
||||
module PostgREST.App (app, sqlError, isSqlError, contentTypeForAccept) where
|
||||
|
||||
import Control.Monad (join)
|
||||
import Control.Arrow ((***), second)
|
||||
import Control.Applicative
|
||||
|
||||
import Data.Text hiding (map)
|
||||
import Data.Maybe (fromMaybe, mapMaybe)
|
||||
import Data.Text hiding (map, find)
|
||||
import Data.Maybe (fromMaybe, mapMaybe, isJust, isNothing)
|
||||
import Text.Regex.TDFA ((=~))
|
||||
import Data.Ord (comparing)
|
||||
import Data.Ranged.Ranges (emptyRange)
|
||||
import qualified Data.HashMap.Strict as M
|
||||
import Data.String.Conversions (cs)
|
||||
import Data.CaseInsensitive (original)
|
||||
import Data.List (sortBy)
|
||||
import Data.List (sortBy, find)
|
||||
import Data.Functor.Identity
|
||||
import qualified Data.Set as S
|
||||
import qualified Data.ByteString.Lazy as BL
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import qualified Blaze.ByteString.Builder as BB
|
||||
import qualified Data.Csv as CSV
|
||||
|
||||
@@ -25,6 +26,7 @@ import Network.HTTP.Types.Header
|
||||
import Network.HTTP.Types.URI (parseSimpleQuery)
|
||||
import Network.HTTP.Base (urlEncodeVars)
|
||||
import Network.Wai
|
||||
import Network.Wai.Parse (parseHttpAccept)
|
||||
import Network.Wai.Internal (Response(..))
|
||||
|
||||
import Data.Aeson
|
||||
@@ -50,9 +52,9 @@ app conf reqBody req =
|
||||
return $ responseLBS status200 [jsonH] $ cs body
|
||||
|
||||
([table], "OPTIONS") -> do
|
||||
let t = QualifiedTable schema (cs table)
|
||||
cols <- columns t
|
||||
pkey <- map cs <$> primaryKeyColumns t
|
||||
let qt = qualify table
|
||||
cols <- columns qt
|
||||
pkey <- map cs <$> primaryKeyColumns qt
|
||||
return $ responseLBS status200 [jsonH, allOrigins]
|
||||
$ encode (TableOptions cols pkey)
|
||||
|
||||
@@ -60,21 +62,21 @@ app conf reqBody req =
|
||||
if range == Just emptyRange
|
||||
then return $ responseLBS status416 [] "HTTP Range error"
|
||||
else do
|
||||
let qt = QualifiedTable schema (cs table)
|
||||
let select = B.Stmt "select " V.empty True <>
|
||||
let qt = qualify table
|
||||
from = fromMaybe 0 $ rangeOffset <$> range
|
||||
select = B.Stmt "select " V.empty True <>
|
||||
parentheticT (
|
||||
whereT qq $ countRows qt
|
||||
whereT qt qq $ countRows qt
|
||||
) <> commaq <> (
|
||||
asJsonWithCount
|
||||
bodyForAccept contentType qt
|
||||
. limitT range
|
||||
. orderT (orderParse qq)
|
||||
. whereT qq
|
||||
. whereT qt qq
|
||||
$ selectStar qt
|
||||
)
|
||||
row <- H.maybeEx select
|
||||
let (tableTotal, queryTotal, body) =
|
||||
fromMaybe (0, 0, Just "" :: Maybe Text) row
|
||||
from = fromMaybe 0 $ rangeOffset <$> range
|
||||
to = from+queryTotal-1
|
||||
contentRange = contentRangeH from to tableTotal
|
||||
status = rangeStatus from to tableTotal
|
||||
@@ -84,7 +86,7 @@ app conf reqBody req =
|
||||
. parseSimpleQuery
|
||||
$ rawQueryString req
|
||||
return $ responseLBS status
|
||||
[jsonH, contentRange,
|
||||
[contentTypeH, contentRange,
|
||||
("Content-Location",
|
||||
"/" <> cs table <>
|
||||
if Prelude.null canonical then "" else "?" <> cs canonical
|
||||
@@ -120,17 +122,17 @@ app conf reqBody req =
|
||||
login <- signInRole (cs $ userId u)
|
||||
(cs $ userPass u)
|
||||
case login of
|
||||
LoginSuccess role ->
|
||||
LoginSuccess role uid ->
|
||||
return $ responseLBS status201 [ jsonH ] $
|
||||
encode . object $ [("token", String $ tokenJWT jwtSecret (cs $ userId u) role)]
|
||||
encode . object $ [("token", String $ tokenJWT jwtSecret uid role)]
|
||||
_ -> return $ responseLBS status401 [jsonH] $
|
||||
encode . object $ [("message", String "Failed authentication.")]
|
||||
|
||||
([table], "POST") -> do
|
||||
let qt = QualifiedTable schema (cs table)
|
||||
echoRequested = lookup "Prefer" hdrs == Just "return=representation"
|
||||
let qt = qualify table
|
||||
echoRequested = lookupHeader "Prefer" == Just "return=representation"
|
||||
parsed :: Either String (V.Vector Text, V.Vector (V.Vector Value))
|
||||
parsed = if lookup "Content-Type" hdrs == Just "text/csv"
|
||||
parsed = if lookupHeader "Content-Type" == Just csvMT
|
||||
then do
|
||||
rows <- CSV.decode CSV.NoHeader reqBody
|
||||
if V.null rows then Left "CSV requires header"
|
||||
@@ -161,9 +163,25 @@ app conf reqBody req =
|
||||
] $ if echoRequested then encode obj else ""
|
||||
return $ multipart status201 responses
|
||||
|
||||
(["rpc", proc], "POST") -> do
|
||||
let qi = QualifiedIdentifier schema (cs proc)
|
||||
exists <- doesProcExist schema proc
|
||||
if exists
|
||||
then do
|
||||
let call = B.Stmt "select " V.empty True <>
|
||||
asJson (callProc qi $ fromMaybe M.empty (decode reqBody))
|
||||
body :: Maybe (Identity Text) <- H.maybeEx call
|
||||
return $ responseLBS status200 [jsonH]
|
||||
(cs $ fromMaybe "[]" $ runIdentity <$> body)
|
||||
else return $ responseLBS status404 [] ""
|
||||
|
||||
-- check that proc exists
|
||||
-- check that arg names are all specified
|
||||
-- select * from "1".proc(a := "foo"::undefined) where whereT limit limitT
|
||||
|
||||
([table], "PUT") ->
|
||||
handleJsonObj reqBody $ \obj -> do
|
||||
let qt = QualifiedTable schema (cs table)
|
||||
let qt = qualify table
|
||||
primaryKeys <- primaryKeyColumns qt
|
||||
let specifiedKeys = map (cs . fst) qq
|
||||
if S.fromList primaryKeys /= S.fromList specifiedKeys
|
||||
@@ -176,7 +194,7 @@ app conf reqBody req =
|
||||
then do
|
||||
let vals = M.elems obj
|
||||
H.unitEx $ iffNotT
|
||||
(whereT qq $ update qt cols vals)
|
||||
(whereT qt qq $ update qt cols vals)
|
||||
(insertSelect qt cols vals)
|
||||
return $ responseLBS status204 [ jsonH ] ""
|
||||
|
||||
@@ -187,9 +205,9 @@ app conf reqBody req =
|
||||
|
||||
([table], "PATCH") ->
|
||||
handleJsonObj reqBody $ \obj -> do
|
||||
let qt = QualifiedTable schema (cs table)
|
||||
let qt = qualify table
|
||||
up = returningStarT
|
||||
. whereT qq
|
||||
. whereT qt qq
|
||||
$ update qt (map cs $ M.keys obj) (M.elems obj)
|
||||
patch = withT up "t" $ B.Stmt
|
||||
"select count(t), array_to_json(array_agg(row_to_json(t)))::character varying"
|
||||
@@ -199,17 +217,17 @@ app conf reqBody req =
|
||||
let (queryTotal, body) =
|
||||
fromMaybe (0 :: Int, Just "" :: Maybe Text) row
|
||||
r = contentRangeH 0 (queryTotal-1) queryTotal
|
||||
echoRequested = lookup "Prefer" hdrs == Just "return=representation"
|
||||
echoRequested = lookupHeader "Prefer" == Just "return=representation"
|
||||
s = case () of _ | queryTotal == 0 -> status404
|
||||
| echoRequested -> status200
|
||||
| otherwise -> status204
|
||||
return $ responseLBS s [ jsonH, r ] $ if echoRequested then cs $ fromMaybe "[]" body else ""
|
||||
|
||||
([table], "DELETE") -> do
|
||||
let qt = QualifiedTable schema (cs table)
|
||||
let del = countT
|
||||
let qt = qualify table
|
||||
del = countT
|
||||
. returningStarT
|
||||
. whereT qq
|
||||
. whereT qt qq
|
||||
$ deleteFrom qt
|
||||
row <- H.maybeEx del
|
||||
let (Identity deletedCount) = fromMaybe (Identity 0 :: Identity Int) row
|
||||
@@ -221,15 +239,20 @@ app conf reqBody req =
|
||||
return $ responseLBS status404 [] ""
|
||||
|
||||
where
|
||||
path = pathInfo req
|
||||
verb = requestMethod req
|
||||
qq = queryString req
|
||||
hdrs = requestHeaders req
|
||||
schema = requestedSchema (cs $ configV1Schema conf) hdrs
|
||||
path = pathInfo req
|
||||
verb = requestMethod req
|
||||
qq = queryString req
|
||||
qualify = QualifiedIdentifier schema
|
||||
hdrs = requestHeaders req
|
||||
lookupHeader = flip lookup hdrs
|
||||
accept = lookupHeader hAccept
|
||||
schema = requestedSchema (cs $ configV1Schema conf) accept
|
||||
authenticator = cs $ configDbUser conf
|
||||
jwtSecret = cs $ configJwtSecret conf
|
||||
range = rangeRequested hdrs
|
||||
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
||||
jwtSecret = cs $ configJwtSecret conf
|
||||
range = rangeRequested hdrs
|
||||
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
||||
contentType = fromMaybe "application/json" $ contentTypeForAccept accept
|
||||
contentTypeH = (hContentType, contentType)
|
||||
|
||||
sqlError :: t
|
||||
sqlError = undefined
|
||||
@@ -253,18 +276,40 @@ contentRangeH from to total =
|
||||
<> cs (show total)
|
||||
)
|
||||
|
||||
requestedSchema :: Text -> RequestHeaders -> Text
|
||||
requestedSchema v1schema hdrs =
|
||||
requestedSchema :: Text -> Maybe BS.ByteString -> Text
|
||||
requestedSchema v1schema accept =
|
||||
case verStr of
|
||||
Just [[_, ver]] -> if ver == "1" then v1schema else ver
|
||||
Just [[_, ver]] -> if ver == "1" then v1schema else cs ver
|
||||
_ -> v1schema
|
||||
|
||||
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: String
|
||||
accept = cs <$> lookup hAccept hdrs :: Maybe Text
|
||||
verStr = (=~ verRegex) <$> accept :: Maybe [[Text]]
|
||||
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: BS.ByteString
|
||||
verStr = (=~ verRegex) <$> accept :: Maybe [[BS.ByteString]]
|
||||
|
||||
|
||||
jsonMT :: BS.ByteString
|
||||
jsonMT = "application/json"
|
||||
|
||||
csvMT :: BS.ByteString
|
||||
csvMT = "text/csv"
|
||||
|
||||
jsonH :: Header
|
||||
jsonH = (hContentType, "application/json")
|
||||
jsonH = (hContentType, jsonMT)
|
||||
|
||||
contentTypeForAccept :: Maybe BS.ByteString -> Maybe BS.ByteString
|
||||
contentTypeForAccept accept
|
||||
| isNothing accept || hasJson = Just jsonMT
|
||||
| hasCsv = Just csvMT
|
||||
| otherwise = Nothing
|
||||
where
|
||||
Just acceptH = accept
|
||||
findInAccept = flip find $ parseHttpAccept acceptH
|
||||
hasJson = isJust $ findInAccept $ BS.isPrefixOf jsonMT
|
||||
hasCsv = isJust $ findInAccept $ BS.isPrefixOf csvMT
|
||||
|
||||
bodyForAccept :: BS.ByteString -> QualifiedIdentifier -> StatementT
|
||||
bodyForAccept contentType table
|
||||
| contentType == csvMT = asCsvWithCount table
|
||||
| otherwise = asJsonWithCount -- defaults to JSON
|
||||
|
||||
handleJsonObj :: BL.ByteString -> (Object -> H.Tx P.Postgres s Response)
|
||||
-> H.Tx P.Postgres s Response
|
||||
|
||||
+21
-11
@@ -40,22 +40,29 @@ instance ToJSON AuthUser where
|
||||
, "role" .= userRole u ]
|
||||
|
||||
type DbRole = Text
|
||||
type UserId = Text
|
||||
|
||||
data LoginAttempt =
|
||||
NoCredentials
|
||||
| MalformedAuth
|
||||
| LoginFailed
|
||||
| LoginSuccess DbRole
|
||||
| LoginSuccess DbRole UserId
|
||||
deriving (Eq, Show)
|
||||
|
||||
checkPass :: Text -> Text -> Bool
|
||||
checkPass = (. cs) . validatePassword . cs
|
||||
|
||||
setRole :: Text -> H.Tx P.Postgres s ()
|
||||
setRole role = H.unitEx $ B.Stmt ("set role " <> cs (pgFmtLit role)) V.empty True
|
||||
setRole role = H.unitEx $ B.Stmt ("set local role " <> cs (pgFmtLit role)) V.empty True
|
||||
|
||||
resetRole :: H.Tx P.Postgres s ()
|
||||
resetRole = H.unitEx [H.stmt|reset role|]
|
||||
setUserId :: Text -> H.Tx P.Postgres s ()
|
||||
setUserId uid = if uid /= "" then
|
||||
H.unitEx $ B.Stmt ("set local user_vars.user_id = " <> cs (pgFmtLit uid)) V.empty True
|
||||
else
|
||||
resetUserId
|
||||
|
||||
resetUserId :: H.Tx P.Postgres s ()
|
||||
resetUserId = H.unitEx [H.stmt|reset user_vars.user_id|]
|
||||
|
||||
addUser :: Text -> Text -> Text -> H.Tx P.Postgres s ()
|
||||
addUser identity pass role = do
|
||||
@@ -66,26 +73,29 @@ addUser identity pass role = do
|
||||
|
||||
signInRole :: Text -> Text -> H.Tx P.Postgres s LoginAttempt
|
||||
signInRole user pass = do
|
||||
u <- H.maybeEx $ [H.stmt|select pass, rolname from postgrest.auth where id = ?|] user
|
||||
u <- H.maybeEx $ [H.stmt|select id, pass, rolname from postgrest.auth where id = ?|] user
|
||||
return $ maybe LoginFailed (\r ->
|
||||
let (hashed, role) = r in
|
||||
let (uid, hashed, role) = r in
|
||||
if checkPass hashed pass
|
||||
then LoginSuccess role
|
||||
then LoginSuccess role uid
|
||||
else LoginFailed
|
||||
) u
|
||||
|
||||
signInWithJWT :: Text -> Text -> LoginAttempt
|
||||
signInWithJWT secret input = case maybeRole of
|
||||
Just (Just (String role)) -> LoginSuccess $ cs role
|
||||
Just (Just (String role)) -> case maybeUserId of
|
||||
Just (Just (String uid)) -> LoginSuccess (cs role) (cs uid)
|
||||
_ -> LoginFailed
|
||||
_ -> LoginFailed
|
||||
where
|
||||
where
|
||||
maybeRole = (Data.Map.lookup "role" <$> claims) ::Maybe (Maybe Value)
|
||||
maybeUserId = (Data.Map.lookup "id" <$> claims) ::Maybe (Maybe Value)
|
||||
claims = JWT.unregisteredClaims <$> JWT.claims <$> decoded
|
||||
decoded = JWT.decodeAndVerifySignature (JWT.secret secret) input
|
||||
|
||||
|
||||
tokenJWT :: Text -> Text -> Text -> Text
|
||||
tokenJWT secret uid role = JWT.encodeSigned JWT.HS256 (JWT.secret secret) claimsSet
|
||||
where
|
||||
claimsSet = JWT.def {
|
||||
JWT.unregisteredClaims = Data.Map.fromList [("id", String uid), ("role", String role)]
|
||||
}
|
||||
}
|
||||
|
||||
+19
-9
@@ -1,3 +1,4 @@
|
||||
{-# LANGUAGE QuasiQuotes, ScopedTypeVariables #-}
|
||||
module Main where
|
||||
|
||||
import Paths_postgrest (version)
|
||||
@@ -10,21 +11,30 @@ import Control.Monad (unless)
|
||||
import Control.Monad.IO.Class (liftIO)
|
||||
import Data.String.Conversions (cs)
|
||||
import Network.Wai (strictRequestBody)
|
||||
import Network.Wai.Middleware.Cors (cors)
|
||||
import Network.Wai.Handler.Warp hiding (Connection)
|
||||
import Network.Wai.Middleware.Gzip (gzip, def)
|
||||
import Network.Wai.Middleware.Static (staticPolicy, only)
|
||||
import Network.Wai.Middleware.RequestLogger (logStdout)
|
||||
import Data.List (intercalate)
|
||||
import Data.Version (versionBranch)
|
||||
import Data.Functor.Identity
|
||||
import Data.Text(Text)
|
||||
import qualified Hasql as H
|
||||
import qualified Hasql.Postgres as P
|
||||
import Options.Applicative hiding (columns)
|
||||
|
||||
import PostgREST.Config (AppConfig(..), argParser, corsPolicy)
|
||||
import System.IO (stderr, stdin, stdout, hSetBuffering, BufferMode(..))
|
||||
|
||||
import PostgREST.Config (AppConfig(..), argParser)
|
||||
|
||||
isServerVersionSupported = do
|
||||
Identity (row :: Text) <- H.tx Nothing $ H.singleEx $ [H.stmt|SHOW server_version_num|]
|
||||
return $ read (cs row) >= 90200
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
hSetBuffering stdout LineBuffering
|
||||
hSetBuffering stdin LineBuffering
|
||||
hSetBuffering stderr NoBuffering
|
||||
|
||||
let opts = info (helper <*> argParser) $
|
||||
fullDesc
|
||||
<> progDesc (
|
||||
@@ -51,19 +61,19 @@ main = do
|
||||
appSettings = setPort port
|
||||
. setServerName (cs $ "postgrest/" <> prettyVersion)
|
||||
$ defaultSettings
|
||||
middle = logStdout
|
||||
. (if configSecure conf then redirectInsecure else id)
|
||||
. gzip def . cors corsPolicy
|
||||
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
|
||||
middle = logStdout . defaultMiddle (configSecure conf)
|
||||
|
||||
poolSettings <- maybe (fail "Improper session settings") return $
|
||||
H.poolSettings (fromIntegral $ configPool conf) 30
|
||||
pool :: H.Pool P.Postgres
|
||||
<- H.acquirePool pgSettings poolSettings
|
||||
|
||||
resOrError <- H.session pool isServerVersionSupported
|
||||
either (fail . show) (\supported -> unless supported $ fail "Cannot run in this PostgreSQL version, PostgREST needs at least 9.2.0") resOrError
|
||||
|
||||
runSettings appSettings $ middle $ \req respond -> do
|
||||
body <- strictRequestBody req
|
||||
resOrError <- liftIO $ H.session pool $ H.tx Nothing $
|
||||
resOrError <- liftIO $ H.session pool $ H.tx (Just (H.ReadCommitted, Just True)) $
|
||||
authenticated conf (app conf body) req
|
||||
either (respond . errResponse) respond resOrError
|
||||
|
||||
|
||||
+29
-12
@@ -3,7 +3,7 @@
|
||||
|
||||
module PostgREST.Middleware where
|
||||
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Maybe (fromMaybe, isNothing)
|
||||
import Data.Monoid
|
||||
import Data.Text
|
||||
-- import Data.Pool(withResource, Pool)
|
||||
@@ -12,15 +12,19 @@ import qualified Hasql as H
|
||||
import qualified Hasql.Postgres as P
|
||||
import Data.String.Conversions(cs)
|
||||
|
||||
import Network.HTTP.Types.Header (hLocation, hAuthorization)
|
||||
import Network.HTTP.Types.Header (hLocation, hAuthorization, hAccept)
|
||||
import Network.HTTP.Types (RequestHeaders)
|
||||
import Network.HTTP.Types.Status (status400, status401, status301)
|
||||
import Network.HTTP.Types.Status (status400, status401, status301, status415)
|
||||
import Network.Wai (Application, requestHeaders, responseLBS, rawPathInfo,
|
||||
rawQueryString, isSecure, Request(..), Response)
|
||||
import Network.Wai.Middleware.Gzip (gzip, def)
|
||||
import Network.Wai.Middleware.Cors (cors)
|
||||
import Network.Wai.Middleware.Static (staticPolicy, only)
|
||||
import Network.URI (URI(..), parseURI)
|
||||
|
||||
import PostgREST.Config (AppConfig(..))
|
||||
import PostgREST.Auth (LoginAttempt(..), signInRole, signInWithJWT, setRole, resetRole)
|
||||
import PostgREST.Config (AppConfig(..), corsPolicy)
|
||||
import PostgREST.Auth (LoginAttempt(..), signInRole, signInWithJWT, setRole, setUserId)
|
||||
import PostgREST.App (contentTypeForAccept)
|
||||
import Codec.Binary.Base64.String (decode)
|
||||
|
||||
import Prelude
|
||||
@@ -35,8 +39,8 @@ authenticated conf app req = do
|
||||
return $ responseLBS status400 [] "Malformed basic auth header"
|
||||
LoginFailed ->
|
||||
return $ responseLBS status401 [] "Invalid username or password"
|
||||
LoginSuccess role -> if role /= currentRole then runInRole role else app req
|
||||
NoCredentials -> if anon /= currentRole then runInRole anon else app req
|
||||
LoginSuccess role uid -> if role /= currentRole then runInRole role uid else app req
|
||||
NoCredentials -> if anon /= currentRole then runInRole anon "" else app req
|
||||
|
||||
where
|
||||
jwtSecret = cs $ configJwtSecret conf
|
||||
@@ -54,12 +58,11 @@ authenticated conf app req = do
|
||||
return $ signInWithJWT jwtSecret jwt
|
||||
_ -> return NoCredentials
|
||||
|
||||
runInRole :: Text -> H.Tx P.Postgres s Response
|
||||
runInRole r = do
|
||||
runInRole :: Text -> Text -> H.Tx P.Postgres s Response
|
||||
runInRole r uid = do
|
||||
setUserId uid
|
||||
setRole r
|
||||
res <- app req
|
||||
resetRole
|
||||
return res
|
||||
app req
|
||||
|
||||
|
||||
redirectInsecure :: Application -> Application
|
||||
@@ -82,3 +85,17 @@ redirectInsecure app req respond = do
|
||||
Nothing ->
|
||||
respond $ responseLBS status400 [] "SSL is required"
|
||||
else app req respond
|
||||
|
||||
unsupportedAccept :: Application -> Application
|
||||
unsupportedAccept app req respond = do
|
||||
let
|
||||
accept = lookup hAccept $ requestHeaders req
|
||||
if isNothing $ contentTypeForAccept accept
|
||||
then respond $ responseLBS status415 [] "Unsupported Accept header, try: application/json"
|
||||
else app req respond
|
||||
|
||||
defaultMiddle :: Bool -> Application -> Application
|
||||
defaultMiddle secure = (if secure then redirectInsecure else id)
|
||||
. gzip def . cors corsPolicy
|
||||
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
|
||||
. unsupportedAccept
|
||||
|
||||
+71
-39
@@ -10,8 +10,8 @@ import qualified Hasql.Postgres as P
|
||||
import qualified Hasql.Backend as B
|
||||
|
||||
import qualified Data.Text as T
|
||||
import qualified Data.HashMap.Strict as H
|
||||
import Text.Regex.TDFA ( (=~) )
|
||||
import Text.Regex.TDFA.Text ()
|
||||
import qualified Network.HTTP.Types.URI as Net
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import Data.Monoid
|
||||
@@ -34,9 +34,9 @@ instance Monoid PStmt where
|
||||
mempty = B.Stmt "" empty True
|
||||
type StatementT = PStmt -> PStmt
|
||||
|
||||
data QualifiedTable = QualifiedTable {
|
||||
qtSchema :: T.Text
|
||||
, qtName :: T.Text
|
||||
data QualifiedIdentifier = QualifiedIdentifier {
|
||||
qiSchema :: T.Text
|
||||
, qiName :: T.Text
|
||||
} deriving (Show)
|
||||
|
||||
data OrderTerm = OrderTerm {
|
||||
@@ -52,14 +52,15 @@ limitT r q =
|
||||
limit = maybe "ALL" (cs . show) $ join $ rangeLimit <$> r
|
||||
offset = cs . show $ fromMaybe 0 $ rangeOffset <$> r
|
||||
|
||||
whereT :: Net.Query -> StatementT
|
||||
whereT params q =
|
||||
whereT :: QualifiedIdentifier -> Net.Query -> StatementT
|
||||
whereT table params q =
|
||||
if L.null cols
|
||||
then q
|
||||
else q <> B.Stmt " where " empty True <> conjunction
|
||||
where
|
||||
cols = [ col | col <- params, fst col `notElem` ["order"] ]
|
||||
conjunction = mconcat $ L.intersperse andq (map wherePred cols)
|
||||
wherePredTable = wherePred table
|
||||
conjunction = mconcat $ L.intersperse andq (map wherePredTable cols)
|
||||
|
||||
withT :: PStmt -> T.Text -> StatementT
|
||||
withT (B.Stmt eq ep epre) v (B.Stmt wq wp wpre) =
|
||||
@@ -95,36 +96,53 @@ iffNotT (B.Stmt aq ap apre) (B.Stmt bq bp bpre) =
|
||||
|
||||
countT :: StatementT
|
||||
countT s =
|
||||
s { B.stmtTemplate = "WITH qqq AS (" <> B.stmtTemplate s <> ") SELECT count(1) FROM qqq" }
|
||||
s { B.stmtTemplate = "WITH qqq AS (" <> B.stmtTemplate s <> ") SELECT pg_catalog.count(1) FROM qqq" }
|
||||
|
||||
countRows :: QualifiedTable -> PStmt
|
||||
countRows t = B.Stmt ("select count(1) from " <> fromQt t) empty True
|
||||
countRows :: QualifiedIdentifier -> PStmt
|
||||
countRows t = B.Stmt ("select pg_catalog.count(1) from " <> fromQi t) empty True
|
||||
|
||||
asCsvWithCount :: QualifiedIdentifier -> StatementT
|
||||
asCsvWithCount table = withCount . asCsv table
|
||||
|
||||
asCsv :: QualifiedIdentifier -> StatementT
|
||||
asCsv table s = s { B.stmtTemplate =
|
||||
"(select string_agg(quote_ident(column_name::text), ',') from "
|
||||
<> "(select column_name from information_schema.columns where quote_ident(table_schema) || '.' || table_name = '"
|
||||
<> fromQi table <> "' order by ordinal_position) h) || '\r' || "
|
||||
<> "coalesce(string_agg(substring(t::text, 2, length(t::text) - 2), '\r'), '') from ("
|
||||
<> B.stmtTemplate s <> ") t" }
|
||||
|
||||
asJsonWithCount :: StatementT
|
||||
asJsonWithCount s = s { B.stmtTemplate =
|
||||
"count(t), array_to_json(array_agg(row_to_json(t)))::character varying from ("
|
||||
<> B.stmtTemplate s <> ") t" }
|
||||
asJsonWithCount = withCount . asJson
|
||||
|
||||
asJson :: StatementT
|
||||
asJson s = s { B.stmtTemplate =
|
||||
"array_to_json(array_agg(row_to_json(t)))::character varying from ("
|
||||
<> B.stmtTemplate s <> ") t" }
|
||||
|
||||
withCount :: StatementT
|
||||
withCount s = s { B.stmtTemplate = "pg_catalog.count(t), " <> B.stmtTemplate s }
|
||||
|
||||
asJsonRow :: StatementT
|
||||
asJsonRow s = s { B.stmtTemplate = "row_to_json(t) from (" <> B.stmtTemplate s <> ") t" }
|
||||
|
||||
selectStar :: QualifiedTable -> PStmt
|
||||
selectStar t = B.Stmt ("select * from " <> fromQt t) empty True
|
||||
selectStar :: QualifiedIdentifier -> PStmt
|
||||
selectStar t = B.Stmt ("select * from " <> fromQi t) empty True
|
||||
|
||||
returningStarT :: StatementT
|
||||
returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" }
|
||||
|
||||
deleteFrom :: QualifiedTable -> PStmt
|
||||
deleteFrom t = B.Stmt ("delete from " <> fromQt t) empty True
|
||||
deleteFrom :: QualifiedIdentifier -> PStmt
|
||||
deleteFrom t = B.Stmt ("delete from " <> fromQi t) empty True
|
||||
|
||||
insertInto :: QualifiedTable
|
||||
insertInto :: QualifiedIdentifier
|
||||
-> V.Vector T.Text
|
||||
-> V.Vector (V.Vector JSON.Value)
|
||||
-> PStmt
|
||||
insertInto t cols vals
|
||||
| V.null cols = B.Stmt ("insert into " <> fromQt t <> " default values returning *") empty True
|
||||
| V.null cols = B.Stmt ("insert into " <> fromQi t <> " default values returning *") empty True
|
||||
| otherwise = B.Stmt
|
||||
("insert into " <> fromQt t <> " (" <>
|
||||
("insert into " <> fromQi t <> " (" <>
|
||||
T.intercalate ", " (V.toList $ V.map pgFmtIdent cols) <>
|
||||
") values "
|
||||
<> T.intercalate ", "
|
||||
@@ -133,38 +151,48 @@ insertInto t cols vals
|
||||
<> ")"
|
||||
) vals
|
||||
)
|
||||
<> " returning row_to_json(" <> fromQt t <> ".*)")
|
||||
<> " returning row_to_json(" <> fromQi t <> ".*)")
|
||||
empty True
|
||||
|
||||
insertSelect :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
|
||||
insertSelect :: QualifiedIdentifier -> [T.Text] -> [JSON.Value] -> PStmt
|
||||
insertSelect t [] _ = B.Stmt
|
||||
("insert into " <> fromQt t <> " default values returning *") empty True
|
||||
("insert into " <> fromQi t <> " default values returning *") empty True
|
||||
insertSelect t cols vals = B.Stmt
|
||||
("insert into " <> fromQt t <> " ("
|
||||
("insert into " <> fromQi t <> " ("
|
||||
<> T.intercalate ", " (map pgFmtIdent cols)
|
||||
<> ") select "
|
||||
<> T.intercalate ", " (map insertableValue vals))
|
||||
empty True
|
||||
|
||||
update :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
|
||||
update :: QualifiedIdentifier -> [T.Text] -> [JSON.Value] -> PStmt
|
||||
update t cols vals = B.Stmt
|
||||
("update " <> fromQt t <> " set ("
|
||||
("update " <> fromQi t <> " set ("
|
||||
<> T.intercalate ", " (map pgFmtIdent cols)
|
||||
<> ") = ("
|
||||
<> T.intercalate ", " (map insertableValue vals)
|
||||
<> ")")
|
||||
empty True
|
||||
|
||||
wherePred :: Net.QueryItem -> PStmt
|
||||
wherePred (col, predicate) =
|
||||
B.Stmt (" " <> pgFmtJsonbPath (cs col) <> " " <> op <> " " <>
|
||||
callProc :: QualifiedIdentifier -> JSON.Object -> PStmt
|
||||
callProc qi params = do
|
||||
let args = T.intercalate "," $ map assignment (H.toList params)
|
||||
B.Stmt ("select * from " <> fromQi qi <> "(" <> args <> ")") empty True
|
||||
where
|
||||
assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v
|
||||
|
||||
wherePred :: QualifiedIdentifier -> Net.QueryItem -> PStmt
|
||||
wherePred table (col, predicate) =
|
||||
B.Stmt (notOp <> " " <> pgFmtJsonbPath table (cs col) <> " " <> op <> " " <>
|
||||
if opCode `elem` ["is","isnot"] then whiteList value
|
||||
else cs sqlValue)
|
||||
empty True
|
||||
|
||||
where
|
||||
opCode:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
|
||||
value = T.intercalate "." rest
|
||||
headPredicate:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
|
||||
hasNot caseTrue caseFalse = if headPredicate == "not" then caseTrue else caseFalse
|
||||
opCode = hasNot (head rest) headPredicate
|
||||
notOp = hasNot headPredicate ""
|
||||
value = hasNot (T.intercalate "." $ tail rest) (T.intercalate "." rest)
|
||||
whiteList val = fromMaybe (cs (pgFmtLit val) <> "::unknown ")
|
||||
(L.find ((==) . T.toLower $ val)
|
||||
["null","true","false"])
|
||||
@@ -175,6 +203,8 @@ wherePred (col, predicate) =
|
||||
"like" -> unknownLiteral $ T.map star value
|
||||
"ilike" -> unknownLiteral $ T.map star value
|
||||
"in" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
|
||||
"notin" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
|
||||
"@@" -> "to_tsquery(" <> unknownLiteral value <> ") "
|
||||
_ -> unknownLiteral value
|
||||
|
||||
op = case opCode of
|
||||
@@ -187,8 +217,10 @@ wherePred (col, predicate) =
|
||||
"like"-> "like"
|
||||
"ilike"-> "ilike"
|
||||
"in" -> "in"
|
||||
"notin" -> "not in"
|
||||
"is" -> "is"
|
||||
"isnot" -> "is not"
|
||||
"@@" -> "@@"
|
||||
_ -> "="
|
||||
|
||||
orderParse :: Net.Query -> [OrderTerm]
|
||||
@@ -236,11 +268,11 @@ parseJsonbPath p =
|
||||
(KeyIdentifier b)
|
||||
_ -> Nothing
|
||||
|
||||
pgFmtJsonbPath :: T.Text -> T.Text
|
||||
pgFmtJsonbPath p =
|
||||
pgFmtJsonbPath :: QualifiedIdentifier -> T.Text -> T.Text
|
||||
pgFmtJsonbPath table p =
|
||||
pgFmtJsonbPath' $ fromMaybe (ColIdentifier p) (parseJsonbPath p)
|
||||
where
|
||||
pgFmtJsonbPath' (ColIdentifier i) = pgFmtIdent i
|
||||
pgFmtJsonbPath' (ColIdentifier i) = fromQi table <> "." <> pgFmtIdent i
|
||||
pgFmtJsonbPath' (KeyIdentifier i) = pgFmtLit i
|
||||
pgFmtJsonbPath' (SingleArrow a b) =
|
||||
pgFmtJsonbPath' a <> "->" <> pgFmtJsonbPath' b
|
||||
@@ -250,26 +282,26 @@ pgFmtJsonbPath p =
|
||||
pgFmtIdent :: T.Text -> T.Text
|
||||
pgFmtIdent x =
|
||||
let escaped = T.replace "\"" "\"\"" (trimNullChars $ cs x) in
|
||||
if escaped =~ danger
|
||||
if (cs escaped :: BS.ByteString) =~ danger
|
||||
then "\"" <> escaped <> "\""
|
||||
else escaped
|
||||
|
||||
where danger = "^$|^[^a-z_]|[^a-z_0-9]" :: T.Text
|
||||
where danger = "^$|^[^a-z_]|[^a-z_0-9]" :: BS.ByteString
|
||||
|
||||
pgFmtLit :: T.Text -> T.Text
|
||||
pgFmtLit x =
|
||||
let trimmed = trimNullChars x
|
||||
escaped = "'" <> T.replace "'" "''" trimmed <> "'"
|
||||
slashed = T.replace "\\" "\\\\" escaped in
|
||||
cs $ if escaped =~ ("\\\\" :: T.Text)
|
||||
if T.isInfixOf "\\\\" escaped
|
||||
then "E" <> slashed
|
||||
else slashed
|
||||
|
||||
trimNullChars :: T.Text -> T.Text
|
||||
trimNullChars = T.takeWhile (/= '\x0')
|
||||
|
||||
fromQt :: QualifiedTable -> T.Text
|
||||
fromQt t = pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t)
|
||||
fromQi :: QualifiedIdentifier -> T.Text
|
||||
fromQi t = pgFmtIdent (qiSchema t) <> "." <> pgFmtIdent (qiName t)
|
||||
|
||||
unquoted :: JSON.Value -> T.Text
|
||||
unquoted (JSON.String t) = t
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
FlexibleContexts #-}
|
||||
module PostgREST.PgStructure where
|
||||
|
||||
import PostgREST.PgQuery (QualifiedTable(..))
|
||||
import PostgREST.PgQuery (QualifiedIdentifier(..))
|
||||
import Data.Text hiding (foldl, map, zipWith, concat)
|
||||
import Data.Aeson
|
||||
import Data.Functor.Identity
|
||||
import Data.String.Conversions (cs)
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Data.Maybe (fromMaybe, isJust)
|
||||
import Control.Applicative
|
||||
|
||||
import qualified Data.Map as Map
|
||||
@@ -18,7 +18,7 @@ import qualified Hasql.Postgres as P
|
||||
|
||||
import Prelude
|
||||
|
||||
foreignKeys :: QualifiedTable -> H.Tx P.Postgres s (Map.Map Text ForeignKey)
|
||||
foreignKeys :: QualifiedIdentifier -> H.Tx P.Postgres s (Map.Map Text ForeignKey)
|
||||
foreignKeys table = do
|
||||
r <- H.listEx $ [H.stmt|
|
||||
select kcu.column_name, ccu.table_name AS foreign_table_name,
|
||||
@@ -31,7 +31,7 @@ foreignKeys table = do
|
||||
where constraint_type = 'FOREIGN KEY'
|
||||
and tc.table_name=? and tc.table_schema = ?
|
||||
order by kcu.column_name
|
||||
|] (qtName table) (qtSchema table)
|
||||
|] (qiName table) (qiSchema table)
|
||||
|
||||
return $ foldl addKey Map.empty r
|
||||
where
|
||||
@@ -43,22 +43,37 @@ tables :: Text -> H.Tx P.Postgres s [Table]
|
||||
tables schema = do
|
||||
rows <- H.listEx $
|
||||
[H.stmt|
|
||||
select table_schema, table_name,
|
||||
is_insertable_into
|
||||
from information_schema.tables
|
||||
where table_schema = ?
|
||||
order by table_name
|
||||
select
|
||||
n.nspname as table_schema,
|
||||
relname as table_name,
|
||||
c.relkind = 'r' or (c.relkind IN ('v', 'f')) and (pg_relation_is_updatable(c.oid::regclass, false) & 8) = 8
|
||||
or (exists (
|
||||
select 1
|
||||
from pg_trigger
|
||||
where pg_trigger.tgrelid = c.oid and (pg_trigger.tgtype::integer & 69) = 69)
|
||||
) as insertable
|
||||
from
|
||||
pg_class c
|
||||
join pg_namespace n on n.oid = c.relnamespace
|
||||
where
|
||||
c.relkind in ('v', 'r', 'm')
|
||||
and n.nspname = ?
|
||||
and (
|
||||
pg_has_role(c.relowner, 'USAGE'::text)
|
||||
or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text)
|
||||
)
|
||||
order by relname
|
||||
|] schema
|
||||
return $ map tableFromRow rows
|
||||
|
||||
|
||||
columns :: QualifiedTable -> H.Tx P.Postgres s [Column]
|
||||
columns :: QualifiedIdentifier -> H.Tx P.Postgres s [Column]
|
||||
columns table = do
|
||||
cols <- H.listEx $ [H.stmt|
|
||||
select info.table_schema as schema, info.table_name as table_name,
|
||||
info.column_name as name, info.ordinal_position as position,
|
||||
info.is_nullable as nullable, info.data_type as col_type,
|
||||
info.is_updatable as updatable,
|
||||
info.is_nullable::boolean as nullable, info.data_type as col_type,
|
||||
info.is_updatable::boolean as updatable,
|
||||
info.character_maximum_length as max_len,
|
||||
info.numeric_precision as precision,
|
||||
info.column_default as default_value,
|
||||
@@ -82,7 +97,7 @@ columns table = do
|
||||
) as enum_info
|
||||
on (info.udt_name = enum_info.n)
|
||||
order by position |]
|
||||
(qtSchema table) (qtName table)
|
||||
(qiSchema table) (qiName table)
|
||||
|
||||
fks <- foreignKeys table
|
||||
return $ map (addFK fks . columnFromRow) cols
|
||||
@@ -91,7 +106,7 @@ columns table = do
|
||||
addFK fks col = col { colFK = Map.lookup (cs . colName $ col) fks }
|
||||
|
||||
|
||||
primaryKeyColumns :: QualifiedTable -> H.Tx P.Postgres s [Text]
|
||||
primaryKeyColumns :: QualifiedIdentifier -> H.Tx P.Postgres s [Text]
|
||||
primaryKeyColumns table = do
|
||||
r <- H.listEx $ [H.stmt|
|
||||
select kc.column_name
|
||||
@@ -103,12 +118,20 @@ primaryKeyColumns table = do
|
||||
and kc.table_name = tc.table_name and kc.table_schema = tc.table_schema
|
||||
and kc.constraint_name = tc.constraint_name
|
||||
and kc.table_schema = ?
|
||||
and kc.table_name = ? |] (qtSchema table) (qtName table)
|
||||
and kc.table_name = ? |] (qiSchema table) (qiName table)
|
||||
return $ map runIdentity r
|
||||
|
||||
|
||||
toBool :: Text -> Bool
|
||||
toBool = (== "YES")
|
||||
doesProcExist :: Text -> Text -> H.Tx P.Postgres s Bool
|
||||
doesProcExist schema proc = do
|
||||
row :: Maybe (Identity Int) <- H.maybeEx $ [H.stmt|
|
||||
SELECT 1
|
||||
FROM pg_catalog.pg_namespace n
|
||||
JOIN pg_catalog.pg_proc p
|
||||
ON pronamespace = n.oid
|
||||
WHERE nspname = ?
|
||||
AND proname = ?
|
||||
|] schema proc
|
||||
return $ isJust row
|
||||
|
||||
data Table = Table {
|
||||
tableSchema :: Text
|
||||
@@ -135,16 +158,16 @@ data Column = Column {
|
||||
, colFK :: Maybe ForeignKey
|
||||
} deriving (Show)
|
||||
|
||||
tableFromRow :: (Text, Text, Text) -> Table
|
||||
tableFromRow (s, n, i) = Table s n (toBool i)
|
||||
tableFromRow :: (Text, Text, Bool) -> Table
|
||||
tableFromRow (s, n, i) = Table s n i
|
||||
|
||||
columnFromRow :: (Text, Text, Text,
|
||||
Int, Text, Text,
|
||||
Text, Maybe Int, Maybe Int,
|
||||
Int, Bool, Text,
|
||||
Bool, Maybe Int, Maybe Int,
|
||||
Maybe Text, Maybe Text)
|
||||
-> Column
|
||||
columnFromRow (s, t, n, pos, nul, typ, u, l, p, d, e) =
|
||||
Column s t n pos (toBool nul) typ (toBool u) l p d (parseEnum e) Nothing
|
||||
Column s t n pos nul typ u l p d (parseEnum e) Nothing
|
||||
|
||||
where
|
||||
parseEnum :: Maybe Text -> [Text]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
flags: {}
|
||||
packages:
|
||||
- '.'
|
||||
extra-deps:
|
||||
- Ranged-sets-0.3.0
|
||||
- packdeps-0.4.1
|
||||
resolver: lts-3.1
|
||||
@@ -28,19 +28,26 @@ spec = beforeAll
|
||||
let auth = authHeaderBasic "jdoe" "1234"
|
||||
request methodGet "/authors_only" [auth] ""
|
||||
`shouldRespondWith` 200
|
||||
|
||||
|
||||
it "recovers after 400 error with logged in user" $ do
|
||||
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
|
||||
let auth = authHeaderBasic "jdoe" "1234"
|
||||
_ <- request methodPost "/rpc/problem" [auth] ""
|
||||
request methodGet "/authors_only" [auth] ""
|
||||
`shouldRespondWith` 200
|
||||
|
||||
it "allows users to login (JWT)" $ do
|
||||
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
|
||||
post "/postgrest/tokens" [json| { "id":"jdoe", "pass": "1234" } |]
|
||||
post "/postgrest/tokens" [json| { "id":"jdoe", "pass": "1234" } |]
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"} |]
|
||||
, matchStatus = 201
|
||||
, matchHeaders = ["Content-Type" <:> "application/json"]
|
||||
}
|
||||
|
||||
|
||||
it "indicates login failure (JWT)" $ do
|
||||
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
|
||||
post "/postgrest/tokens" [json| { "id":"jdoe", "pass": "NOPE" } |]
|
||||
post "/postgrest/tokens" [json| { "id":"jdoe", "pass": "NOPE" } |]
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| {"message":"Failed authentication."} |]
|
||||
, matchStatus = 401
|
||||
@@ -51,4 +58,4 @@ spec = beforeAll
|
||||
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
|
||||
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
|
||||
request methodGet "/authors_only" [auth] ""
|
||||
`shouldRespondWith` 200
|
||||
`shouldRespondWith` 200
|
||||
|
||||
@@ -22,7 +22,7 @@ spec = around withApp $ describe "CORS" $ do
|
||||
("Host", "localhost:3000"),
|
||||
("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0"),
|
||||
("Origin", "http://localhost:8000"),
|
||||
("Accept", "text/plain, */*; q=0.01"),
|
||||
("Accept", "text/csv, */*; q=0.01"),
|
||||
("Accept-Language", "en-US,en;q=0.5"),
|
||||
("Accept-Encoding", "gzip, deflate"),
|
||||
("Referer", "http://localhost:8000/"),
|
||||
|
||||
@@ -87,12 +87,7 @@ spec = afterAll_ resetDb $ around withApp $ do
|
||||
|
||||
context "with invalid json payload" $
|
||||
it "fails with 400 and error" $
|
||||
post "/simple_pk" "}{ x = 2"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| {"message":"Failed to parse JSON payload. Failed reading: satisfy"} |]
|
||||
, matchStatus = 400
|
||||
, matchHeaders = []
|
||||
}
|
||||
post "/simple_pk" "}{ x = 2" `shouldRespondWith` 400
|
||||
|
||||
context "jsonb" . after_ (clearTable "json") $ do
|
||||
it "serializes nested object" $ do
|
||||
@@ -266,6 +261,12 @@ spec = afterAll_ resetDb $ around withApp $ do
|
||||
liftIO $ simpleHeaders g
|
||||
`shouldSatisfy` matchHeader "Content-Range" "0-9/10"
|
||||
|
||||
it "can update based on a computed column" $
|
||||
request methodPatch
|
||||
"/items?always_true=eq.false"
|
||||
[("Prefer", "return=representation")]
|
||||
[json| { id: 100 } |]
|
||||
`shouldRespondWith` 404
|
||||
it "can provide a representation" $ do
|
||||
_ <- post "/items"
|
||||
[json| { id: 1 } |]
|
||||
@@ -274,3 +275,23 @@ spec = afterAll_ resetDb $ around withApp $ do
|
||||
[("Prefer", "return=representation")]
|
||||
[json| { id: 99 } |]
|
||||
`shouldRespondWith` [json| [{id:99}] |]
|
||||
|
||||
describe "Row level permission" $
|
||||
it "set user_id when inserting rows" $ do
|
||||
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
|
||||
_ <- post "/postgrest/users" [json| { "id":"jroe", "pass": "1234", "role": "postgrest_test_author" } |]
|
||||
|
||||
p1 <- request methodPost "/authors_only"
|
||||
[ authHeaderBasic "jdoe" "1234", ("Prefer", "return=representation") ]
|
||||
[json| { "secret": "nyancat" } |]
|
||||
liftIO $ do
|
||||
simpleBody p1 `shouldBe` [json| { "owner":"jdoe", "secret":"nyancat" } |]
|
||||
simpleStatus p1 `shouldBe` created201
|
||||
|
||||
p2 <- request methodPost "/authors_only"
|
||||
-- jwt token for jroe
|
||||
[ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.YuF_VfmyIxWyuceT7crnNKEprIYXsJAyXid3rjPjIow", ("Prefer", "return=representation") ]
|
||||
[json| { "secret": "lolcat", "owner": "hacker" } |]
|
||||
liftIO $ do
|
||||
simpleBody p2 `shouldBe` [json| { "owner":"jroe", "secret":"lolcat" } |]
|
||||
simpleStatus p2 `shouldBe` created201
|
||||
|
||||
+110
-2
@@ -3,6 +3,7 @@ module Feature.QuerySpec where
|
||||
import Test.Hspec
|
||||
import Test.Hspec.Wai
|
||||
import Test.Hspec.Wai.JSON
|
||||
import Network.HTTP.Types
|
||||
import Network.Wai.Test (SResponse(simpleHeaders))
|
||||
|
||||
import SpecHelper
|
||||
@@ -10,6 +11,7 @@ import SpecHelper
|
||||
spec :: Spec
|
||||
spec =
|
||||
beforeAll (clearTable "items" >> createItems 15)
|
||||
. beforeAll (clearTable "nullable_integer" >> createNullInteger)
|
||||
. beforeAll (
|
||||
clearTable "no_pk" >>
|
||||
createNulls 2 >>
|
||||
@@ -17,6 +19,11 @@ spec =
|
||||
createJsonData)
|
||||
. afterAll_ (clearTable "items" >> clearTable "no_pk" >> clearTable "simple_pk")
|
||||
. around withApp $ do
|
||||
|
||||
describe "Querying a table with a column called count" $
|
||||
it "should not confuse count column with pg_catalog.count aggregate" $
|
||||
get "/has_count_column" `shouldRespondWith` 200
|
||||
|
||||
describe "Querying a nonexistent table" $
|
||||
it "causes a 404" $
|
||||
get "/faketable" `shouldRespondWith` 404
|
||||
@@ -30,6 +37,31 @@ spec =
|
||||
, matchHeaders = ["Content-Range" <:> "0-0/1"]
|
||||
}
|
||||
|
||||
it "matches with equality using not operator" $
|
||||
get "/items?id=not.eq.5"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-13/14"]
|
||||
}
|
||||
|
||||
it "matches with more than one condition using not operator" $
|
||||
get "/simple_pk?k=like.*yx&extra=not.eq.u" `shouldRespondWith` "[]"
|
||||
|
||||
it "matches with inequality using not operator" $ do
|
||||
get "/items?id=not.lt.14&order=id.asc"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":14},{"id":15}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||
}
|
||||
get "/items?id=not.gt.2&order=id.asc"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":1},{"id":2}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||
}
|
||||
|
||||
it "matches items IN" $
|
||||
get "/items?id=in.1,3,5"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
@@ -38,10 +70,32 @@ spec =
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||
}
|
||||
|
||||
it "matches nulls" $
|
||||
it "matches items NOT IN" $
|
||||
get "/items?id=notin.2,4,6,7,8,9,10,11,12,13,14,15"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||
}
|
||||
|
||||
it "matches items NOT IN using not operator" $
|
||||
get "/items?id=not.in.2,4,6,7,8,9,10,11,12,13,14,15"
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||
}
|
||||
|
||||
it "matches nulls using not operator" $
|
||||
get "/no_pk?a=not.is.null" `shouldRespondWith`
|
||||
[json| [{"a":"1","b":"0"},{"a":"2","b":"0"}] |]
|
||||
|
||||
it "matches nulls in varchar and numeric fields alike" $ do
|
||||
get "/no_pk?a=is.null" `shouldRespondWith`
|
||||
[json| [{"a": null, "b": null}] |]
|
||||
|
||||
get "/nullable_integer?a=is.null" `shouldRespondWith` "[{\"a\":null}]"
|
||||
|
||||
it "matches with like" $ do
|
||||
get "/simple_pk?k=like.*yx" `shouldRespondWith`
|
||||
"[{\"k\":\"xyyx\",\"extra\":\"u\"}]"
|
||||
@@ -50,12 +104,31 @@ spec =
|
||||
get "/simple_pk?k=like.*YY*" `shouldRespondWith`
|
||||
"[{\"k\":\"xYYx\",\"extra\":\"v\"}]"
|
||||
|
||||
it "matches with like using not operator" $
|
||||
get "/simple_pk?k=not.like.*yx" `shouldRespondWith`
|
||||
"[{\"k\":\"xYYx\",\"extra\":\"v\"}]"
|
||||
|
||||
it "matches with ilike" $ do
|
||||
get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith`
|
||||
"[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]"
|
||||
get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith`
|
||||
"[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]"
|
||||
|
||||
it "matches with ilike using not operator" $
|
||||
get "/simple_pk?k=not.ilike.xy*&order=extra.asc" `shouldRespondWith` "[]"
|
||||
|
||||
it "matches with tsearch @@" $
|
||||
get "/tsearch?text_search_vector=@@.foo" `shouldRespondWith`
|
||||
[json| [{"text_search_vector":"'bar':2 'foo':1"}] |]
|
||||
|
||||
it "matches with tsearch @@ using not operator" $
|
||||
get "/tsearch?text_search_vector=not.@@.foo" `shouldRespondWith`
|
||||
[json| [{"text_search_vector":"'baz':1 'qux':2"}] |]
|
||||
|
||||
it "matches with computed column" $
|
||||
get "/items?always_true=eq.true" `shouldRespondWith`
|
||||
[json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
|
||||
|
||||
describe "ordering response" $ do
|
||||
it "by a column asc" $
|
||||
get "/items?id=lte.2&order=id.asc"
|
||||
@@ -105,6 +178,26 @@ spec =
|
||||
it "without other constraints" $
|
||||
get "/items?order=asc.id" `shouldRespondWith` 200
|
||||
|
||||
describe "Accept headers" $ do
|
||||
it "should respond an unknown accept type with 415" $
|
||||
request methodGet "/simple_pk"
|
||||
(acceptHdrs "text/unknowntype") ""
|
||||
`shouldRespondWith` 415
|
||||
|
||||
it "should respond correctly to multiple types in accept header" $
|
||||
request methodGet "/simple_pk"
|
||||
(acceptHdrs "text/unknowntype, text/csv") ""
|
||||
`shouldRespondWith` 200
|
||||
|
||||
it "should respond with CSV to 'text/csv' request" $
|
||||
request methodGet "/simple_pk"
|
||||
(acceptHdrs "text/csv; version=1") ""
|
||||
`shouldRespondWith` ResponseMatcher {
|
||||
matchBody = Just "k,extra\rxyyx,u\rxYYx,v"
|
||||
, matchStatus = 200
|
||||
, matchHeaders = ["Content-Type" <:> "text/csv"]
|
||||
}
|
||||
|
||||
describe "Canonical location" $ do
|
||||
it "Sets Content-Location with alphabetized params" $
|
||||
get "/no_pk?b=eq.1&a=eq.1"
|
||||
@@ -121,9 +214,24 @@ spec =
|
||||
respHeaders `shouldSatisfy` matchHeader
|
||||
"Content-Location" "/simple_pk"
|
||||
|
||||
describe "jsonb" $
|
||||
describe "jsonb" $ do
|
||||
it "can filter by properties inside json column" $ do
|
||||
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
|
||||
[json| [{"data": {"foo": {"bar": "baz"}}}] |]
|
||||
get "/json?data->foo->>bar=eq.fake" `shouldRespondWith`
|
||||
[json| [] |]
|
||||
it "can filter by properties inside json column using not" $
|
||||
get "/json?data->foo->>bar=not.eq.baz" `shouldRespondWith`
|
||||
[json| [] |]
|
||||
|
||||
describe "remote procedure call" $ do
|
||||
context "a proc that returns a set" . before_ (clearTable "items" >> createItems 10) .
|
||||
after_ (clearTable "items") $
|
||||
it "returns proper json" $
|
||||
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
|
||||
[json| [ {"id": 3}, {"id":4} ] |]
|
||||
|
||||
context "a proc that returns plain text" $
|
||||
it "returns proper json" $
|
||||
post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`
|
||||
[json| [{"sayhello":"Hello, world"}] |]
|
||||
|
||||
@@ -16,12 +16,17 @@ spec = around withApp $ do
|
||||
`shouldRespondWith` [json| [
|
||||
{"schema":"1","name":"auto_incrementing_pk","insertable":true}
|
||||
, {"schema":"1","name":"compound_pk","insertable":true}
|
||||
, {"schema":"1","name":"has_count_column","insertable":false}
|
||||
, {"schema":"1","name":"has_fk","insertable":true}
|
||||
, {"schema":"1","name":"insertable_view_with_join","insertable":true}
|
||||
, {"schema":"1","name":"items","insertable":true}
|
||||
, {"schema":"1","name":"json","insertable":true}
|
||||
, {"schema":"1","name":"materialized_view","insertable":false}
|
||||
, {"schema":"1","name":"menagerie","insertable":true}
|
||||
, {"schema":"1","name":"no_pk","insertable":true}
|
||||
, {"schema":"1","name":"nullable_integer","insertable":true}
|
||||
, {"schema":"1","name":"simple_pk","insertable":true}
|
||||
, {"schema":"1","name":"tsearch","insertable":true}
|
||||
] |]
|
||||
{matchStatus = 200}
|
||||
|
||||
|
||||
+14
-6
@@ -15,19 +15,18 @@ import qualified Data.Vector as V
|
||||
import Control.Monad (void)
|
||||
|
||||
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
|
||||
hRange, hAuthorization)
|
||||
hRange, hAuthorization, hAccept)
|
||||
import Codec.Binary.Base64.String (encode)
|
||||
import Data.CaseInsensitive (CI(..))
|
||||
import Data.Maybe (fromMaybe)
|
||||
import Text.Regex.TDFA ((=~))
|
||||
import qualified Data.ByteString.Char8 as BS
|
||||
import Network.Wai.Middleware.Cors (cors)
|
||||
import System.Process (readProcess)
|
||||
|
||||
import qualified Data.Aeson.Types as J
|
||||
|
||||
import PostgREST.App (app)
|
||||
import PostgREST.Config (AppConfig(..), corsPolicy)
|
||||
import PostgREST.Config (AppConfig(..))
|
||||
import PostgREST.Middleware
|
||||
import PostgREST.Error(errResponse)
|
||||
|
||||
@@ -55,11 +54,11 @@ withApp perform = do
|
||||
|
||||
perform $ middle $ \req resp -> do
|
||||
body <- strictRequestBody req
|
||||
result <- liftIO $ H.session pool $ H.tx Nothing
|
||||
result <- liftIO $ H.session pool $ H.tx (Just (H.ReadCommitted, Just True))
|
||||
$ authenticated cfg (app cfg body) req
|
||||
either (resp . errResponse) resp result
|
||||
|
||||
where middle = cors corsPolicy
|
||||
where middle = defaultMiddle False
|
||||
|
||||
|
||||
resetDb :: IO ()
|
||||
@@ -84,6 +83,9 @@ loadFixture name =
|
||||
rangeHdrs :: ByteRange -> [Header]
|
||||
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
|
||||
|
||||
acceptHdrs :: BS.ByteString -> [Header]
|
||||
acceptHdrs mime = [(hAccept, mime)]
|
||||
|
||||
rangeUnit :: Header
|
||||
rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")
|
||||
|
||||
@@ -94,7 +96,7 @@ matchHeader name valRegex headers =
|
||||
authHeaderBasic :: String -> String -> Header
|
||||
authHeaderBasic u p =
|
||||
(hAuthorization, cs $ "Basic " ++ encode (u ++ ":" ++ p))
|
||||
|
||||
|
||||
authHeaderJWT :: String -> Header
|
||||
authHeaderJWT token =
|
||||
(hAuthorization, cs $ "Bearer " ++ token)
|
||||
@@ -125,6 +127,12 @@ createNulls n = do
|
||||
stmt' = [H.stmt|insert into "1".no_pk (a,b) values (null,null)|]
|
||||
stmts = map [H.stmt|insert into "1".no_pk (a,b) values (?,0)|] [1..n]
|
||||
|
||||
createNullInteger :: IO ()
|
||||
createNullInteger = do
|
||||
pool <- testPool
|
||||
void . liftIO $ H.session pool $ H.tx Nothing $
|
||||
H.unitEx $ [H.stmt| insert into "1".nullable_integer (a) values (null) |]
|
||||
|
||||
createLikableStrings :: IO ()
|
||||
createLikableStrings = do
|
||||
pool <- testPool
|
||||
|
||||
@@ -79,7 +79,7 @@ spec = around dbWithSchema $ do
|
||||
addUser user pass role conn
|
||||
return conn) $ do
|
||||
it "accepts correct credentials and return the role" $ \conn ->
|
||||
signInRole user pass conn `shouldReturn` LoginSuccess role
|
||||
signInRole user pass conn `shouldReturn` LoginSuccess role user
|
||||
|
||||
it "returns nothing with bad creds" $ \conn -> do
|
||||
signInRole "not-a-user" pass conn `shouldReturn` LoginFailed
|
||||
|
||||
@@ -14,7 +14,7 @@ spec = around dbWithSchema $ beforeWith setRole $ do
|
||||
it "shows all the tables" $ \conn -> do
|
||||
ts <- tables "1" conn
|
||||
map tableName ts `shouldBe` ["authors_only","auto_incrementing_pk",
|
||||
"compound_pk","has_fk","items","menagerie","no_pk", "simple_pk"]
|
||||
"compound_pk","has_fk","insertable_view_with_join","items","menagerie","no_pk", "simple_pk"]
|
||||
|
||||
describe "columns" $ do
|
||||
it "responds with each column for the table" $ \conn -> do
|
||||
|
||||
Vendored
+142
-8
@@ -72,6 +72,28 @@ $$;
|
||||
|
||||
ALTER FUNCTION postgrest.update_owner() OWNER TO postgrest_test;
|
||||
|
||||
CREATE FUNCTION set_authors_only_owner() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
begin
|
||||
NEW.owner = current_setting('user_vars.user_id');
|
||||
RETURN NEW;
|
||||
end
|
||||
$$;
|
||||
|
||||
ALTER FUNCTION postgrest.set_authors_only_owner() OWNER TO postgrest_test;
|
||||
|
||||
CREATE FUNCTION "1".insert_insertable_view_with_join() RETURNS trigger
|
||||
LANGUAGE plpgsql
|
||||
AS $$
|
||||
begin
|
||||
INSERT INTO "1".auto_incrementing_pk (nullable_string, non_nullable_string) VALUES (NEW.nullable_string, NEW.non_nullable_string);
|
||||
RETURN NEW;
|
||||
end;
|
||||
$$;
|
||||
|
||||
ALTER FUNCTION "1".insert_insertable_view_with_join() OWNER TO postgrest_test;
|
||||
|
||||
SET search_path = "1", pg_catalog;
|
||||
|
||||
SET default_tablespace = '';
|
||||
@@ -80,6 +102,7 @@ SET default_with_oids = false;
|
||||
|
||||
|
||||
CREATE TABLE authors_only (
|
||||
owner character varying NOT NULL,
|
||||
secret character varying NOT NULL
|
||||
);
|
||||
|
||||
@@ -146,6 +169,29 @@ ALTER TABLE "1".has_fk_id_seq OWNER TO postgrest_test;
|
||||
|
||||
ALTER SEQUENCE has_fk_id_seq OWNED BY has_fk.id;
|
||||
|
||||
CREATE MATERIALIZED VIEW "1".materialized_view AS
|
||||
SELECT
|
||||
version();
|
||||
|
||||
ALTER TABLE "1".materialized_view OWNER TO postgrest_test;
|
||||
|
||||
CREATE VIEW "1".insertable_view_with_join AS
|
||||
SELECT has_fk.id,
|
||||
has_fk.auto_inc_fk,
|
||||
has_fk.simple_fk,
|
||||
auto_incrementing_pk.nullable_string,
|
||||
auto_incrementing_pk.non_nullable_string,
|
||||
auto_incrementing_pk.inserted_at
|
||||
FROM (has_fk
|
||||
JOIN auto_incrementing_pk USING (id));
|
||||
|
||||
|
||||
ALTER TABLE "1".insertable_view_with_join OWNER TO postgrest_test;
|
||||
|
||||
CREATE VIEW "1".has_count_column AS
|
||||
SELECT 1 AS count;
|
||||
|
||||
ALTER TABLE "1".insertable_view_with_join OWNER TO postgrest_test;
|
||||
|
||||
|
||||
CREATE TABLE items (
|
||||
@@ -171,6 +217,25 @@ ALTER SEQUENCE items_id_seq OWNED BY items.id;
|
||||
|
||||
|
||||
|
||||
CREATE FUNCTION "1".getitemrange(min bigint, max bigint) RETURNS SETOF "1".items AS $$
|
||||
SELECT * FROM "1".items WHERE id > $1 AND id <= $2;
|
||||
$$ LANGUAGE SQL;
|
||||
|
||||
|
||||
|
||||
CREATE FUNCTION "1".sayhello(name text) RETURNS text AS $$
|
||||
SELECT 'Hello, ' || $1;
|
||||
$$ LANGUAGE SQL;
|
||||
|
||||
|
||||
CREATE FUNCTION "1".problem() RETURNS void LANGUAGE plpgsql AS
|
||||
$$
|
||||
BEGIN
|
||||
RAISE 'bad thing';
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
CREATE TABLE menagerie (
|
||||
"integer" integer NOT NULL,
|
||||
double double precision NOT NULL,
|
||||
@@ -194,6 +259,14 @@ CREATE TABLE no_pk (
|
||||
ALTER TABLE "1".no_pk OWNER TO postgrest_test;
|
||||
|
||||
|
||||
CREATE TABLE nullable_integer (
|
||||
a integer
|
||||
);
|
||||
|
||||
|
||||
ALTER TABLE "1".nullable_integer OWNER TO postgrest_test;
|
||||
|
||||
|
||||
CREATE TABLE simple_pk (
|
||||
k character varying NOT NULL,
|
||||
extra character varying NOT NULL
|
||||
@@ -212,6 +285,12 @@ CREATE TABLE json
|
||||
ALTER TABLE "1".json OWNER TO postgrest_test;
|
||||
|
||||
|
||||
CREATE TABLE tsearch (
|
||||
text_search_vector tsvector
|
||||
);
|
||||
|
||||
ALTER TABLE "1".tsearch OWNER TO postgrest_test;
|
||||
|
||||
SET search_path = postgrest, pg_catalog;
|
||||
|
||||
|
||||
@@ -299,14 +378,8 @@ INSERT INTO items (id) VALUES (1);
|
||||
SELECT pg_catalog.setval('items_id_seq', 1, true);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
INSERT INTO tsearch (text_search_vector) VALUES ('''bar'':2 ''foo'':1');
|
||||
INSERT INTO tsearch (text_search_vector) VALUES ('''baz'':1 ''qux'':2');
|
||||
|
||||
SET search_path = postgrest, pg_catalog;
|
||||
|
||||
@@ -324,11 +397,22 @@ SELECT pg_catalog.setval('articles_id_seq', 1, false);
|
||||
|
||||
SET search_path = "1", pg_catalog;
|
||||
|
||||
CREATE FUNCTION public.always_true("1".items) RETURNS boolean
|
||||
LANGUAGE sql STABLE
|
||||
AS $$ SELECT true $$;
|
||||
|
||||
ALTER FUNCTION public.always_true("1".items) OWNER TO postgrest_test;
|
||||
|
||||
|
||||
|
||||
ALTER TABLE ONLY authors_only
|
||||
ADD CONSTRAINT authors_only_pkey PRIMARY KEY (secret);
|
||||
|
||||
CREATE TRIGGER insert_insertable_view_with_join INSTEAD OF INSERT ON "1".insertable_view_with_join FOR EACH ROW EXECUTE PROCEDURE "1".insert_insertable_view_with_join();
|
||||
|
||||
|
||||
CREATE TRIGGER secrets_owner_track BEFORE INSERT OR UPDATE ON authors_only FOR EACH ROW EXECUTE PROCEDURE postgrest.set_authors_only_owner();
|
||||
|
||||
|
||||
ALTER TABLE ONLY auto_incrementing_pk
|
||||
ADD CONSTRAINT auto_incrementing_pk_pkey PRIMARY KEY (id);
|
||||
@@ -456,6 +540,22 @@ GRANT ALL ON TABLE items TO postgrest_test;
|
||||
GRANT ALL ON TABLE items TO postgrest_anonymous;
|
||||
|
||||
|
||||
REVOKE ALL ON FUNCTION getitemrange(bigint, bigint) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION getitemrange(bigint, bigint) FROM postgrest_test;
|
||||
GRANT EXECUTE ON FUNCTION getitemrange(bigint, bigint) TO postgrest_test;
|
||||
GRANT EXECUTE ON FUNCTION getitemrange(bigint, bigint) TO postgrest_anonymous;
|
||||
|
||||
|
||||
REVOKE ALL ON FUNCTION sayhello(text) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION sayhello(text) FROM postgrest_test;
|
||||
GRANT EXECUTE ON FUNCTION sayhello(text) TO postgrest_test;
|
||||
GRANT EXECUTE ON FUNCTION sayhello(text) TO postgrest_anonymous;
|
||||
|
||||
|
||||
REVOKE ALL ON FUNCTION problem() FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION problem() FROM postgrest_test_author;
|
||||
GRANT EXECUTE ON FUNCTION problem() TO postgrest_test_author;
|
||||
|
||||
|
||||
REVOKE ALL ON SEQUENCE items_id_seq FROM PUBLIC;
|
||||
REVOKE ALL ON SEQUENCE items_id_seq FROM postgrest_test;
|
||||
@@ -478,6 +578,13 @@ GRANT ALL ON TABLE no_pk TO postgrest_anonymous;
|
||||
|
||||
|
||||
|
||||
REVOKE ALL ON TABLE nullable_integer FROM PUBLIC;
|
||||
REVOKE ALL ON TABLE nullable_integer FROM postgrest_test;
|
||||
GRANT ALL ON TABLE nullable_integer TO postgrest_test;
|
||||
GRANT ALL ON TABLE nullable_integer TO postgrest_anonymous;
|
||||
|
||||
|
||||
|
||||
REVOKE ALL ON TABLE simple_pk FROM PUBLIC;
|
||||
REVOKE ALL ON TABLE simple_pk FROM postgrest_test;
|
||||
GRANT ALL ON TABLE simple_pk TO postgrest_test;
|
||||
@@ -491,6 +598,33 @@ GRANT ALL ON TABLE json TO postgrest_test;
|
||||
GRANT ALL ON TABLE json TO postgrest_anonymous;
|
||||
|
||||
|
||||
|
||||
REVOKE ALL ON TABLE tsearch FROM PUBLIC;
|
||||
REVOKE ALL ON TABLE tsearch FROM postgrest_test;
|
||||
GRANT ALL ON TABLE tsearch TO postgrest_test;
|
||||
GRANT ALL ON TABLE tsearch TO postgrest_anonymous;
|
||||
|
||||
REVOKE ALL ON TABLE materialized_view FROM PUBLIC;
|
||||
REVOKE ALL ON TABLE materialized_view FROM postgrest_test;
|
||||
GRANT ALL ON TABLE materialized_view TO postgrest_test;
|
||||
GRANT ALL ON TABLE materialized_view TO postgrest_anonymous;
|
||||
|
||||
REVOKE ALL ON TABLE insertable_view_with_join FROM PUBLIC;
|
||||
REVOKE ALL ON TABLE insertable_view_with_join FROM postgrest_test;
|
||||
GRANT ALL ON TABLE insertable_view_with_join TO postgrest_test;
|
||||
GRANT ALL ON TABLE insertable_view_with_join TO postgrest_anonymous;
|
||||
|
||||
REVOKE ALL ON TABLE has_count_column FROM PUBLIC;
|
||||
REVOKE ALL ON TABLE has_count_column FROM postgrest_test;
|
||||
GRANT ALL ON TABLE has_count_column TO postgrest_test;
|
||||
GRANT ALL ON TABLE has_count_column TO postgrest_anonymous;
|
||||
|
||||
REVOKE ALL ON FUNCTION public.always_true("1".items) FROM PUBLIC;
|
||||
REVOKE ALL ON FUNCTION public.always_true("1".items) FROM postgrest_test;
|
||||
GRANT ALL ON FUNCTION public.always_true("1".items) TO postgrest_test;
|
||||
GRANT ALL ON FUNCTION public.always_true("1".items) TO postgrest_anonymous;
|
||||
|
||||
|
||||
SET search_path = postgrest, pg_catalog;
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user