Compare commits

..
86 Commits
Author SHA1 Message Date
Joe Nelson 593f247abb Expose more heroku config vars 2015-09-01 23:16:30 -07:00
Joe Nelson add63ac25b Keep the dilapidated release script alive 2015-09-01 23:13:47 -07:00
Joe Nelson 1ef8cc5048 Bump patch version 2015-09-01 22:29:13 -07:00
Joe Nelson c5836e0c9e Merge pull request #275 from diogob/fix_all_media_types_in_accept
Fix */* in accept headers
2015-09-01 13:10:02 -07:00
Diogo Biazus 922aa702a2 Adds fix to changelog 2015-09-01 15:58:07 -04:00
Diogo Biazus 89c581816e Adds */* as a valid media type that will return json [fix #274] 2015-09-01 15:57:59 -04:00
Joe Nelson 3d670b9c03 bump minor version 2015-08-28 19:19:37 -07:00
Joe Nelson 5bf644867f Merge pull request #271 from begriffs/surprise-404
Allow continued auth access after db errors
2015-08-26 20:58:55 -07:00
Joe Nelson fcdae73f49 Note fix in changelog 2015-08-26 20:49:23 -07:00
Joe Nelson 1656fb9f57 Let the transaction reset the role and user id for us 2015-08-26 20:49:23 -07:00
Joe Nelson 594327924c Set role locally in a tx to ensure it is reset after error 2015-08-26 20:49:23 -07:00
Joe Nelson 480800edbd Problem after exceptions when authed
Reproduces #264
2015-08-26 20:49:23 -07:00
Joe Nelson 9b01d1b1ab Helpful directions in contributing doc 2015-08-26 20:47:22 -07:00
Joe Nelson ed810bc380 Operator negation 2015-08-21 19:35:48 -07:00
Joe Nelson 789db9a017 Merge pull request #266 from diogob/adds_not_unary_operator
Adds not as a keyword that can optionally be prepended to any operator in a parameter value [fix #173]
2015-08-21 19:25:02 -07:00
Diogo Biazus 52626cc86d Adds test cases for not operator in equality, inequality, like, ilike, tesarch (@@) and is null queries 2015-08-21 15:08:52 -04:00
Diogo Biazus 32b97ef076 Adds not as a keyword that can optionally be prepended to any operator in a parameter value 2015-08-21 10:33:41 -04:00
Joe Nelson 8e319321c9 RPC and Stack 2015-08-20 22:37:19 -07:00
Joe Nelson cdac6d385c Merge pull request #228 from begriffs/rpc
Expose stored procedures
2015-08-20 22:27:30 -07:00
Joe Nelson d65d011e5e Tests for rpc 2015-08-20 22:14:09 -07:00
Joe Nelson 28f771324d Nest response JSON more shallowly 2015-08-20 22:14:08 -07:00
Joe Nelson cf04fbd6ea Call procedures that return setof, not just text
The output is too deeply nested however
2015-08-20 21:52:46 -07:00
Joe Nelson bb511f0df2 WIP: call stored pprocedures that emit plain text
The beginning of #114
2015-08-20 21:52:46 -07:00
Joe Nelson f564fb0977 Rename QualifiedTable to encompass proc names as well 2015-08-20 21:35:12 -07:00
Diogo BiazusandJoe Nelson 3b017dfdf6 Adds 415 response for any non-empty Accept header different from application/json or text/csv. Uses apropriate Content-Type header when sending CSV format. 2015-08-20 20:47:36 -07:00
Diogo BiazusandJoe Nelson cb7d00b839 Adds tags file to gitignore 2015-08-20 20:47:36 -07:00
Joe Nelson 010e18ea0b Merge pull request #269 from diogob/build_with_stack
Build with stack
2015-08-20 15:29:10 -07:00
Diogo Biazus c34f96ce2e Removes body matcher to compile and test against any aeson version >= 0.8 2015-08-20 16:43:57 -04:00
Diogo Biazus f9d50018d9 Rollback to aeson 0.8.0.2 to allow building with stackage, and adds stack.yml 2015-08-20 15:50:28 -04:00
Diogo Biazus 6c1233fcec Adss stack-work directory to gitignore 2015-08-20 15:15:53 -04:00
Joe Nelson acd8e92d24 Note the NOT IN addition 2015-08-17 09:40:49 -07:00
Joe Nelson 559d370a89 Merge pull request #263 from rall/notin
NOT IN queries
2015-08-17 09:38:45 -07:00
Richard Allaway 4cf51b7005 fixes spec for changed error message from updated aeson library 2015-08-17 11:10:15 -04:00
Richard Allaway 4e77492797 adds a spec for NOT IN query case 2015-08-17 10:24:51 -04:00
Richard Allaway f16e2e3ee5 adds 'not in' query 2015-08-17 10:24:51 -04:00
Joe Nelson ebc8c387e0 Thanks @diogob! 2015-08-15 12:14:20 -07:00
Joe Nelson 734484714c CSV responses! 2015-08-15 11:59:12 -07:00
Joe Nelson adac39bd7c Use Content-Type text/csv for CSV responses 2015-08-15 11:57:57 -07:00
Diogo Biazus 9d5011e864 Implements CSV resnponse for the appropriate accept headers 2015-08-14 11:04:26 -04:00
Diogo Biazus ca40ba1fda Refactors app function to DRY header lookups 2015-08-14 11:04:26 -04:00
Joe Nelson 894455f2cd Relax hasql deps for packdeps checker 2015-08-12 00:05:38 -07:00
Joe Nelson 6cb73062a9 Better shields 2015-08-11 23:18:47 -07:00
Joe Nelson 86c68d191c Updated maintenance note in contributing doc 2015-08-09 14:04:57 -07:00
Joe Nelson d20c252cb3 Adjust version of hasql-postgres for packdeps 2015-08-08 12:57:44 -07:00
Joe Nelson cd6b688f7f Merge pull request #244 from diogob/adds_materialized_views_to_root
Adds materialized views to list of relations in GET / [#242]
2015-08-01 16:16:23 -07:00
Joe Nelson 49f41d8edb Merge pull request #243 from diogob/fix_count_column_name_case
Fixes error code 42803 when trying to query a view with a column named count
2015-08-01 16:15:02 -07:00
Diogo Biazus b45953dff8 Mentions fix in CHANGELOG 2015-08-01 01:23:31 -04:00
Diogo Biazus 8075d7e51a Mentions fix in CHANGELOG 2015-08-01 01:21:42 -04:00
Diogo Biazus ab0170ffaf Adds materialized views to list of relations in GET / [#242] 2015-08-01 01:09:12 -04:00
Diogo Biazus 449cacdacf Fixes error code 42803 when trying to query a view with a column named count. 2015-08-01 00:15:17 -04:00
Joe Nelson e724c2df00 Merge pull request #230 from edelans/master
Add link to Jonathan Harrington's nice tutorial
2015-07-26 10:17:39 -07:00
Joe Nelson 12dc180065 Merge pull request #237 from datasaur/master
Enable log capture if stdout is not a terminal (issue #229)
2015-07-24 09:51:37 -07:00
MattK 8eae978eae Enable log capture if stdout is not a terminal 2015-07-24 12:14:18 -04:00
Edouard de Lansalut 019d53bca1 Add link to Jonathan Harrington's nice tutorial 2015-07-22 09:47:14 +02:00
Joe Nelson e1d7dc3dea Note computed columns in changelog 2015-07-21 22:34:45 -07:00
Joe Nelson 28a2826fa8 Merge pull request #221 from diogob/allow_virtual_fields_in_where
Qualifies columns of WHERE clauses so we can use computed columns as filters
2015-07-21 22:31:10 -07:00
Diogo Biazus c76864a653 Qualifies columns used in WHERE clauses so we can use computed columns as filters 2015-07-10 12:35:24 -04:00
Joe Nelson c0d44232a5 Note Debian changes in changelog 2015-07-09 22:52:14 -07:00
Joe Nelson e34e92eb44 Merge pull request #216 from mkhon/master
Debian init script for postgrest.
2015-07-09 22:49:11 -07:00
Joe Nelson 956f73d997 Regression test for situation reported in issue #203 2015-07-03 22:51:04 -07:00
Joe Nelson df04d26c15 Merge pull request #219 from diogob/check_postgresql_version
Verifies PostgreSQL version is supported (+9.2) before spawning server
2015-07-02 13:21:29 -07:00
Diogo Biazus 9c69553373 Verifies PostgreSQL version is supported (+9.2) before spawning server [fixes #157] 2015-07-02 09:26:58 -04:00
Joe Nelson 6604293ac1 Remove regex-tdfa-text to allow building in GHC 7.10
Fixes #212
2015-07-01 21:55:35 -07:00
Max Khon 60a61adbce Use POSTGREST_USER. 2015-06-24 18:30:56 +06:00
Max Khon b56ab47f84 Debian init script for postgrest. 2015-06-24 18:07:30 +06:00
Joe Nelson 25492a089b Merge pull request #214 from diogob/refactor_to_bool
Removes toBool function as we now cast the 'YES/NO' values to boolean in PostgreSQL's queries
2015-06-22 22:29:07 -07:00
Diogo Biazus e87be593c0 Removes toBool function as we now cast the 'YES/NO' values to boolean in PostgreSQL's queries 2015-06-22 18:09:45 -04:00
Joe Nelson 1937363fc8 Note @diogob's contribution 2015-06-21 22:18:51 -07:00
Joe Nelson d68cbec25c Merge pull request #209 from diogob/insertable_views_with_triggers
Changes the insertable to true in views that are insertable through triggers [fixes #206]
2015-06-21 22:16:43 -07:00
Diogo Biazus f4011e5d8c Changes the insertable to true in views that are insertable through triggers [fixes #206] 2015-06-22 00:16:23 -04:00
Joe Nelson e93c96a6f8 Link to Heroku troubleshooting Wiki in readme 2015-06-21 14:01:42 -07:00
Joe Nelson 070f67e9c6 Merge pull request #213 from begriffs/packdeps
Loosen dependency version constraints for packdeps
2015-06-20 12:16:05 -07:00
Joe Nelson 61dac3b02b Loosen dependency version constraints for packdeps 2015-06-20 12:12:36 -07:00
Joe Nelson 169157ec6d Thanks @framp 2015-06-17 20:24:10 -07:00
Joe Nelson bd304c9fc3 Bump version 2015-06-03 21:22:17 -07:00
Joe Nelson 3989aaa144 Note @framp's auth id change 2015-05-26 14:58:59 -07:00
Joe Nelson 1d51a5f543 Merge pull request #201 from framp/master
User_id support (via user_vars)
2015-05-26 14:55:56 -07:00
Joe Nelson e4dafad64d Use github release feature to host binaries 2015-05-25 21:56:47 -07:00
Joe Nelson 3f31c60f1d Mention JWT in readme 2015-05-25 21:46:05 -07:00
Federico Rampazzo 83f48dcd15 User_id support (via user_vars) 2015-05-24 03:26:42 +01:00
Joe Nelson 1cc53245c5 Full text search in changelog 2015-05-23 08:21:02 -07:00
Joe Nelson f24ba048af Merge pull request #199 from diogob/tsearch_operator
Tsearch operator
2015-05-23 08:15:44 -07:00
Diogo Biazus a980db6d2d Adds spec for @@ operator and implements it in PgQuery 2015-05-22 12:28:26 -04:00
Diogo Biazus 4277284a69 Adds table tsearch to test for @@ operator against a tsvector field. Fixes StructureSpec accordingly. 2015-05-22 12:15:14 -04:00
Joe Nelson b070994912 Patch version bump for conditional -Werror flag 2015-05-21 00:18:26 -07:00
Joe Nelson 988df54e53 Use Werror on CI 2015-05-21 00:18:03 -07:00
27 changed files with 821 additions and 202 deletions
+2
View File
@@ -5,3 +5,5 @@ cabal.sandbox.config
hscope.out hscope.out
codex.tags codex.tags
.anvil .anvil
.stack-work
tags
+31
View File
@@ -3,6 +3,37 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/). This project adheres to [Semantic Versioning](http://semver.org/).
## [0.2.11.1] - 2015-09-01
### Fixed
- Accepts `*/*` in Accept header - @diogob
## [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 ## [0.2.9.0] - 2015-05-20
### Added ### Added
- Return range headers in PATCH - Return range headers in PATCH
+8 -3
View File
@@ -39,12 +39,17 @@ your contributions.
committers. Continuous integration will check this as well on every committers. Continuous integration will check this as well on every
pull request. 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 ## Maintenance
### Schedule ### Schedule
Currently I (@begriffs) am the sole maintainer, and while I am Currently I (@begriffs) am the sole maintainer, and while I am
overjoyed to help resolve issues I also have to balance this with overjoyed to help resolve issues I also have to balance this with
my other obligations. I check and respond to github issues **once my other obligations. If you don't get a response right away
per week** (on Mondays). So if you don't get a response right away don't worry, I will definitely get to it. Also you can join the
don't worry, I will definitely get to it. Gitter [chat room](https://gitter.im/begriffs/postgrest) to
discuss issues you are having.
+19 -10
View File
@@ -1,9 +1,10 @@
![Logo](static/logo.png "Logo") ![Logo](static/logo.png "Logo")
[![Build Status](https://circleci.com/gh/begriffs/postgrest.png?circle-token=f723c01686abf0364de1e2eaae5aff1f68bd3ff2)](https://circleci.com/gh/begriffs/postgrest/tree/master) [![Build Status](https://circleci.com/gh/begriffs/postgrest.png?style=shield&circle-token=f723c01686abf0364de1e2eaae5aff1f68bd3ff2)](https://circleci.com/gh/begriffs/postgrest/tree/master)
<a href="https://heroku.com/deploy?template=https://github.com/begriffs/postgrest"> <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> </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)
PostgREST serves a fully RESTful API from any existing PostgreSQL PostgREST serves a fully RESTful API from any existing PostgreSQL
database. It provides a cleaner, more standards-compliant, faster database. It provides a cleaner, more standards-compliant, faster
@@ -20,7 +21,7 @@ your own projects.
### Usage ### 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 ```bash
postgrest --db-host localhost --db-port 5432 \ postgrest --db-host localhost --db-port 5432 \
@@ -75,12 +76,14 @@ Other optimizations are possible, and some are outlined in the
### Security ### Security
PostgREST handles authentication (HTTP Basic over SSL) and delegates PostgREST handles authentication (HTTP Basic over SSL or [JSON Web
authorization to the role information defined in the database. This Tokens](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions#json-web-tokens))
ensures there is a single declarative source of truth for security. and delegates authorization to the role information defined in the
When dealing with the database the server assumes the identity of database. This ensures there is a single declarative source of truth
the currently authenticated user, and for the duration of the for security. When dealing with the database the server assumes
connection cannot do anything the user themselves couldn't. 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 Postgres 9.5 will soon support true [row-level
security](http://michael.otacoo.com/postgresql-2/postgres-9-5-feature-highlight-row-level-security/). 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) * [Performance](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling)
* [Security](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions) * [Security](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions)
* [Tutorial](http://blog.jonharrington.org/postgrest-introduction/) (external) * [Tutorial](http://blog.jonharrington.org/postgrest-introduction/) (external)
* [Heroku](https://github.com/begriffs/postgrest/wiki/Heroku)
### Thanks ### Thanks
* [Adam Baker](https://github.com/adambaker) for code * [Adam Baker](https://github.com/adambaker) for code
contributions and many fundamental design discussions 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 * [Nikita Volkov](https://github.com/nikita-volkov) for writing the
wonderful [Hasql](https://github.com/nikita-volkov/hasql) library wonderful [Hasql](https://github.com/nikita-volkov/hasql) library
and helping me use it and helping me use it
* [Mikey Casalaina](https://github.com/casalaina) for the cool logo * [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
+11 -1
View File
@@ -10,7 +10,7 @@
}, },
"POSTGREST_VER": { "POSTGREST_VER": {
"description": "Version of PostgREST to deploy", "description": "Version of PostgREST to deploy",
"value": "0.2.9.0" "value": "0.2.11.1"
}, },
"DB_NAME": { "DB_NAME": {
"description": "Database name", "description": "Database name",
@@ -41,6 +41,16 @@
"description": "Maximum number of connections in database pool", "description": "Maximum number of connections in database pool",
"required": false, "required": false,
"value": "10" "value": "10"
},
"JWT_SECRET": {
"description": "Secret used to encrypt JSON Web Tokens",
"required": false,
"value": "secret"
},
"V1SCHEMA": {
"description": "DB schema selected whe no version (or version 1) requested",
"required": false,
"value": "1"
} }
} }
} }
+6
View File
@@ -4,6 +4,12 @@ machine:
- createdb -O postgrest_test -U ubuntu postgrest_test - createdb -O postgrest_test -U ubuntu postgrest_test
ghc: ghc:
version: 7.8.3 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: test:
post: post:
- cabal exec hlint -- -X QuasiQuotes src/**/*.hs test/**/*.hs - cabal exec hlint -- -X QuasiQuotes src/**/*.hs test/**/*.hs
Vendored Executable
+8
View File
@@ -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 &
+23
View File
@@ -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=
Vendored Executable
+78
View File
@@ -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
View File
@@ -2,7 +2,7 @@ name: postgrest
description: Reads the schema of a PostgreSQL database and creates RESTful routes description: Reads the schema of a PostgreSQL database and creates RESTful routes
for the tables and views, supporting all HTTP verbs that security for the tables and views, supporting all HTTP verbs that security
permits. permits.
version: 0.2.9.0 version: 0.2.11.1
synopsis: REST API for any Postgres database synopsis: REST API for any Postgres database
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
@@ -12,6 +12,14 @@ maintainer: cred+github@begriffs.com
category: Web category: Web
build-type: Simple build-type: Simple
cabal-version: >=1.10 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 executable postgrest
main-is: PostgREST/Main.hs main-is: PostgREST/Main.hs
@@ -19,21 +27,21 @@ executable postgrest
default-language: Haskell2010 default-language: Haskell2010
build-depends: base >=4.6 && <5 build-depends: base >=4.6 && <5
, postgrest , postgrest
, hasql == 0.7.3.1, hasql-backend == 0.4.1 , hasql >= 0.7.3 && < 0.8
, hasql-postgres == 0.10.3.1 , hasql-backend >= 0.4.1 && < 0.5
, hasql-postgres >= 0.10.4 && < 0.11
, warp >= 3.0.2, wai >= 3.0.1 , warp >= 3.0.2, wai >= 3.0.1
, wai-extra, wai-cors , wai-extra, wai-cors
, wai-middleware-static >= 0.6.0 , wai-middleware-static >= 0.6.0
, HTTP, convertible, http-types , HTTP, convertible, http-types
, case-insensitive , case-insensitive
, scientific, time , scientific, time
, aeson, network >= 2.6 , aeson >= 0.8, network >= 2.6
, bytestring, text, split, string-conversions , bytestring, text, split, string-conversions
, stringsearch , stringsearch
, containers, unordered-containers , containers, unordered-containers
, optparse-applicative == 0.11.* , optparse-applicative == 0.11.*
, regex-base, regex-tdfa , regex-base, regex-tdfa
, regex-tdfa-text
, Ranged-sets , Ranged-sets
, transformers, MissingH , transformers, MissingH
, bcrypt >= 0.0.6, base64-string , bcrypt >= 0.0.6, base64-string
@@ -47,29 +55,32 @@ executable postgrest
hs-source-dirs: src hs-source-dirs: src
library library
ghc-options: -Wall -W -O2 if flag(ci)
ghc-options: -Wall -W -Werror
else
ghc-options: -Wall -W -O2
default-language: Haskell2010 default-language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
build-depends: base >=4.6 && <5 build-depends: base >=4.6 && <5
, hasql == 0.7.3.1, hasql-backend == 0.4.1 , hasql, hasql-backend
, hasql-postgres == 0.10.3.1 , hasql-postgres
, warp >= 3.0.2, wai >= 3.0.1 , warp, wai
, wai-extra, wai-cors , wai-extra, wai-cors
, wai-middleware-static >= 0.6.0 , wai-middleware-static
, HTTP, convertible, http-types , HTTP, convertible, http-types
, case-insensitive , case-insensitive
, scientific, time , scientific, time
, aeson, network >= 2.6 , aeson, network
, bytestring, text, split, string-conversions , bytestring, text, split, string-conversions
, stringsearch , stringsearch
, containers, unordered-containers , containers, unordered-containers
, optparse-applicative == 0.11.* , optparse-applicative
, regex-base, regex-tdfa , regex-base, regex-tdfa
, regex-tdfa-text
, Ranged-sets , Ranged-sets
, transformers, MissingH , transformers, MissingH
, bcrypt >= 0.0.6, base64-string , bcrypt, base64-string
, network-uri >= 2.6 , network-uri
, resource-pool , resource-pool
, blaze-builder , blaze-builder
, vector , vector
@@ -91,7 +102,10 @@ Test-Suite spec
Default-Language: Haskell2010 Default-Language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
Hs-Source-Dirs: test, src 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 Main-Is: Main.hs
Other-Modules: PostgREST.App Other-Modules: PostgREST.App
, PostgREST.Auth , PostgREST.Auth
@@ -103,10 +117,10 @@ Test-Suite spec
, PostgREST.RangeQuery , PostgREST.RangeQuery
, Spec , Spec
, SpecHelper , SpecHelper
Build-Depends: base, hspec >= 2.1.2, QuickCheck Build-Depends: base, hspec == 2.1.*, QuickCheck
, hspec-wai >= 0.5.0, hspec-wai-json , hspec-wai, hspec-wai-json
, hasql == 0.7.3.1, hasql-backend == 0.4.1 , hasql, hasql-backend
, hasql-postgres == 0.10.3.1 , hasql-postgres
, warp, wai , warp, wai
, packdeps, hlint , packdeps, hlint
, HTTP, convertible , HTTP, convertible
@@ -121,7 +135,6 @@ Test-Suite spec
, regex-base , regex-base
, string-conversions , string-conversions
, http-media, regex-tdfa , http-media, regex-tdfa
, regex-tdfa-text
, Ranged-sets , Ranged-sets
, transformers, MissingH, split , transformers, MissingH, split
, bcrypt, base64-string , bcrypt, base64-string
+1 -1
View File
@@ -1,6 +1,6 @@
export POSTGREST_VER=`grep ^version /app/postgrest.cabal | sed -En 's/.*\s+([0-9\.]+)/\1/p'` export POSTGREST_VER=`grep ^version /app/postgrest.cabal | sed -En 's/.*\s+([0-9\.]+)/\1/p'`
curl -L http://softlayer-ams.dl.sourceforge.net/project/s3tools/s3cmd/1.5.0-alpha1/s3cmd-1.5.0-alpha1.tar.gz | tar zx curl -L http://sourceforge.net/projects/s3tools/files/s3cmd/1.5.0-alpha1/s3cmd-1.5.0-alpha1.tar.gz | tar zx
cp /app/dist/build/postgrest/postgrest postgrest-${POSTGREST_VER} cp /app/dist/build/postgrest/postgrest postgrest-${POSTGREST_VER}
tar cJf postgrest-${POSTGREST_VER}.tar.xz postgrest-${POSTGREST_VER} tar cJf postgrest-${POSTGREST_VER}.tar.xz postgrest-${POSTGREST_VER}
+89 -42
View File
@@ -1,22 +1,23 @@
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
module PostgREST.App (app, sqlError, isSqlError) where module PostgREST.App (app, sqlError, isSqlError, contentTypeForAccept) where
import Control.Monad (join) import Control.Monad (join)
import Control.Arrow ((***), second) import Control.Arrow ((***), second)
import Control.Applicative import Control.Applicative
import Data.Text hiding (map) import Data.Text hiding (map, find)
import Data.Maybe (fromMaybe, mapMaybe) import Data.Maybe (fromMaybe, mapMaybe, isJust, isNothing)
import Text.Regex.TDFA ((=~)) import Text.Regex.TDFA ((=~))
import Data.Ord (comparing) import Data.Ord (comparing)
import Data.Ranged.Ranges (emptyRange) import Data.Ranged.Ranges (emptyRange)
import qualified Data.HashMap.Strict as M import qualified Data.HashMap.Strict as M
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.CaseInsensitive (original) import Data.CaseInsensitive (original)
import Data.List (sortBy) import Data.List (sortBy, find)
import Data.Functor.Identity import Data.Functor.Identity
import qualified Data.Set as S import qualified Data.Set as S
import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Char8 as BS
import qualified Blaze.ByteString.Builder as BB import qualified Blaze.ByteString.Builder as BB
import qualified Data.Csv as CSV import qualified Data.Csv as CSV
@@ -25,6 +26,7 @@ import Network.HTTP.Types.Header
import Network.HTTP.Types.URI (parseSimpleQuery) import Network.HTTP.Types.URI (parseSimpleQuery)
import Network.HTTP.Base (urlEncodeVars) import Network.HTTP.Base (urlEncodeVars)
import Network.Wai import Network.Wai
import Network.Wai.Parse (parseHttpAccept)
import Network.Wai.Internal (Response(..)) import Network.Wai.Internal (Response(..))
import Data.Aeson import Data.Aeson
@@ -50,9 +52,9 @@ app conf reqBody req =
return $ responseLBS status200 [jsonH] $ cs body return $ responseLBS status200 [jsonH] $ cs body
([table], "OPTIONS") -> do ([table], "OPTIONS") -> do
let t = QualifiedTable schema (cs table) let qt = qualify table
cols <- columns t cols <- columns qt
pkey <- map cs <$> primaryKeyColumns t pkey <- map cs <$> primaryKeyColumns qt
return $ responseLBS status200 [jsonH, allOrigins] return $ responseLBS status200 [jsonH, allOrigins]
$ encode (TableOptions cols pkey) $ encode (TableOptions cols pkey)
@@ -60,21 +62,21 @@ app conf reqBody req =
if range == Just emptyRange if range == Just emptyRange
then return $ responseLBS status416 [] "HTTP Range error" then return $ responseLBS status416 [] "HTTP Range error"
else do else do
let qt = QualifiedTable schema (cs table) let qt = qualify table
let select = B.Stmt "select " V.empty True <> from = fromMaybe 0 $ rangeOffset <$> range
select = B.Stmt "select " V.empty True <>
parentheticT ( parentheticT (
whereT qq $ countRows qt whereT qt qq $ countRows qt
) <> commaq <> ( ) <> commaq <> (
asJsonWithCount bodyForAccept contentType qt
. limitT range . limitT range
. orderT (orderParse qq) . orderT (orderParse qq)
. whereT qq . whereT qt qq
$ selectStar qt $ selectStar qt
) )
row <- H.maybeEx select row <- H.maybeEx select
let (tableTotal, queryTotal, body) = let (tableTotal, queryTotal, body) =
fromMaybe (0, 0, Just "" :: Maybe Text) row fromMaybe (0, 0, Just "" :: Maybe Text) row
from = fromMaybe 0 $ rangeOffset <$> range
to = from+queryTotal-1 to = from+queryTotal-1
contentRange = contentRangeH from to tableTotal contentRange = contentRangeH from to tableTotal
status = rangeStatus from to tableTotal status = rangeStatus from to tableTotal
@@ -84,7 +86,7 @@ app conf reqBody req =
. parseSimpleQuery . parseSimpleQuery
$ rawQueryString req $ rawQueryString req
return $ responseLBS status return $ responseLBS status
[jsonH, contentRange, [contentTypeH, contentRange,
("Content-Location", ("Content-Location",
"/" <> cs table <> "/" <> cs table <>
if Prelude.null canonical then "" else "?" <> cs canonical if Prelude.null canonical then "" else "?" <> cs canonical
@@ -120,17 +122,17 @@ app conf reqBody req =
login <- signInRole (cs $ userId u) login <- signInRole (cs $ userId u)
(cs $ userPass u) (cs $ userPass u)
case login of case login of
LoginSuccess role -> LoginSuccess role uid ->
return $ responseLBS status201 [ jsonH ] $ 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] $ _ -> return $ responseLBS status401 [jsonH] $
encode . object $ [("message", String "Failed authentication.")] encode . object $ [("message", String "Failed authentication.")]
([table], "POST") -> do ([table], "POST") -> do
let qt = QualifiedTable schema (cs table) let qt = qualify table
echoRequested = lookup "Prefer" hdrs == Just "return=representation" echoRequested = lookupHeader "Prefer" == Just "return=representation"
parsed :: Either String (V.Vector Text, V.Vector (V.Vector Value)) 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 then do
rows <- CSV.decode CSV.NoHeader reqBody rows <- CSV.decode CSV.NoHeader reqBody
if V.null rows then Left "CSV requires header" if V.null rows then Left "CSV requires header"
@@ -161,9 +163,25 @@ app conf reqBody req =
] $ if echoRequested then encode obj else "" ] $ if echoRequested then encode obj else ""
return $ multipart status201 responses 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") -> ([table], "PUT") ->
handleJsonObj reqBody $ \obj -> do handleJsonObj reqBody $ \obj -> do
let qt = QualifiedTable schema (cs table) let qt = qualify table
primaryKeys <- primaryKeyColumns qt primaryKeys <- primaryKeyColumns qt
let specifiedKeys = map (cs . fst) qq let specifiedKeys = map (cs . fst) qq
if S.fromList primaryKeys /= S.fromList specifiedKeys if S.fromList primaryKeys /= S.fromList specifiedKeys
@@ -176,7 +194,7 @@ app conf reqBody req =
then do then do
let vals = M.elems obj let vals = M.elems obj
H.unitEx $ iffNotT H.unitEx $ iffNotT
(whereT qq $ update qt cols vals) (whereT qt qq $ update qt cols vals)
(insertSelect qt cols vals) (insertSelect qt cols vals)
return $ responseLBS status204 [ jsonH ] "" return $ responseLBS status204 [ jsonH ] ""
@@ -187,9 +205,9 @@ app conf reqBody req =
([table], "PATCH") -> ([table], "PATCH") ->
handleJsonObj reqBody $ \obj -> do handleJsonObj reqBody $ \obj -> do
let qt = QualifiedTable schema (cs table) let qt = qualify table
up = returningStarT up = returningStarT
. whereT qq . whereT qt qq
$ update qt (map cs $ M.keys obj) (M.elems obj) $ update qt (map cs $ M.keys obj) (M.elems obj)
patch = withT up "t" $ B.Stmt patch = withT up "t" $ B.Stmt
"select count(t), array_to_json(array_agg(row_to_json(t)))::character varying" "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) = let (queryTotal, body) =
fromMaybe (0 :: Int, Just "" :: Maybe Text) row fromMaybe (0 :: Int, Just "" :: Maybe Text) row
r = contentRangeH 0 (queryTotal-1) queryTotal 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 s = case () of _ | queryTotal == 0 -> status404
| echoRequested -> status200 | echoRequested -> status200
| otherwise -> status204 | otherwise -> status204
return $ responseLBS s [ jsonH, r ] $ if echoRequested then cs $ fromMaybe "[]" body else "" return $ responseLBS s [ jsonH, r ] $ if echoRequested then cs $ fromMaybe "[]" body else ""
([table], "DELETE") -> do ([table], "DELETE") -> do
let qt = QualifiedTable schema (cs table) let qt = qualify table
let del = countT del = countT
. returningStarT . returningStarT
. whereT qq . whereT qt qq
$ deleteFrom qt $ deleteFrom qt
row <- H.maybeEx del row <- H.maybeEx del
let (Identity deletedCount) = fromMaybe (Identity 0 :: Identity Int) row let (Identity deletedCount) = fromMaybe (Identity 0 :: Identity Int) row
@@ -221,15 +239,20 @@ app conf reqBody req =
return $ responseLBS status404 [] "" return $ responseLBS status404 [] ""
where where
path = pathInfo req path = pathInfo req
verb = requestMethod req verb = requestMethod req
qq = queryString req qq = queryString req
hdrs = requestHeaders req qualify = QualifiedIdentifier schema
schema = requestedSchema (cs $ configV1Schema conf) hdrs hdrs = requestHeaders req
lookupHeader = flip lookup hdrs
accept = lookupHeader hAccept
schema = requestedSchema (cs $ configV1Schema conf) accept
authenticator = cs $ configDbUser conf authenticator = cs $ configDbUser conf
jwtSecret = cs $ configJwtSecret conf jwtSecret = cs $ configJwtSecret conf
range = rangeRequested hdrs range = rangeRequested hdrs
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
contentType = fromMaybe "application/json" $ contentTypeForAccept accept
contentTypeH = (hContentType, contentType)
sqlError :: t sqlError :: t
sqlError = undefined sqlError = undefined
@@ -253,18 +276,42 @@ contentRangeH from to total =
<> cs (show total) <> cs (show total)
) )
requestedSchema :: Text -> RequestHeaders -> Text requestedSchema :: Text -> Maybe BS.ByteString -> Text
requestedSchema v1schema hdrs = requestedSchema v1schema accept =
case verStr of case verStr of
Just [[_, ver]] -> if ver == "1" then v1schema else ver Just [[_, ver]] -> if ver == "1" then v1schema else cs ver
_ -> v1schema _ -> v1schema
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: String where verRegex = "version[ ]*=[ ]*([0-9]+)" :: BS.ByteString
accept = cs <$> lookup hAccept hdrs :: Maybe Text verStr = (=~ verRegex) <$> accept :: Maybe [[BS.ByteString]]
verStr = (=~ verRegex) <$> accept :: Maybe [[Text]]
jsonMT :: BS.ByteString
jsonMT = "application/json"
csvMT :: BS.ByteString
csvMT = "text/csv"
allMT :: BS.ByteString
allMT = "*/*"
jsonH :: Header jsonH :: Header
jsonH = (hContentType, "application/json") jsonH = (hContentType, jsonMT)
contentTypeForAccept :: Maybe BS.ByteString -> Maybe BS.ByteString
contentTypeForAccept accept
| isNothing accept || has allMT || has jsonMT = Just jsonMT
| has csvMT = Just csvMT
| otherwise = Nothing
where
Just acceptH = accept
findInAccept = flip find $ parseHttpAccept acceptH
has = isJust . findInAccept . BS.isPrefixOf
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) handleJsonObj :: BL.ByteString -> (Object -> H.Tx P.Postgres s Response)
-> H.Tx P.Postgres s Response -> H.Tx P.Postgres s Response
+21 -11
View File
@@ -40,22 +40,29 @@ instance ToJSON AuthUser where
, "role" .= userRole u ] , "role" .= userRole u ]
type DbRole = Text type DbRole = Text
type UserId = Text
data LoginAttempt = data LoginAttempt =
NoCredentials NoCredentials
| MalformedAuth | MalformedAuth
| LoginFailed | LoginFailed
| LoginSuccess DbRole | LoginSuccess DbRole UserId
deriving (Eq, Show) deriving (Eq, Show)
checkPass :: Text -> Text -> Bool checkPass :: Text -> Text -> Bool
checkPass = (. cs) . validatePassword . cs checkPass = (. cs) . validatePassword . cs
setRole :: Text -> H.Tx P.Postgres s () 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 () setUserId :: Text -> H.Tx P.Postgres s ()
resetRole = H.unitEx [H.stmt|reset role|] 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 :: Text -> Text -> Text -> H.Tx P.Postgres s ()
addUser identity pass role = do addUser identity pass role = do
@@ -66,26 +73,29 @@ addUser identity pass role = do
signInRole :: Text -> Text -> H.Tx P.Postgres s LoginAttempt signInRole :: Text -> Text -> H.Tx P.Postgres s LoginAttempt
signInRole user pass = do 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 -> return $ maybe LoginFailed (\r ->
let (hashed, role) = r in let (uid, hashed, role) = r in
if checkPass hashed pass if checkPass hashed pass
then LoginSuccess role then LoginSuccess role uid
else LoginFailed else LoginFailed
) u ) u
signInWithJWT :: Text -> Text -> LoginAttempt signInWithJWT :: Text -> Text -> LoginAttempt
signInWithJWT secret input = case maybeRole of 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 _ -> LoginFailed
where where
maybeRole = (Data.Map.lookup "role" <$> claims) ::Maybe (Maybe Value) maybeRole = (Data.Map.lookup "role" <$> claims) ::Maybe (Maybe Value)
maybeUserId = (Data.Map.lookup "id" <$> claims) ::Maybe (Maybe Value)
claims = JWT.unregisteredClaims <$> JWT.claims <$> decoded claims = JWT.unregisteredClaims <$> JWT.claims <$> decoded
decoded = JWT.decodeAndVerifySignature (JWT.secret secret) input decoded = JWT.decodeAndVerifySignature (JWT.secret secret) input
tokenJWT :: Text -> Text -> Text -> Text tokenJWT :: Text -> Text -> Text -> Text
tokenJWT secret uid role = JWT.encodeSigned JWT.HS256 (JWT.secret secret) claimsSet tokenJWT secret uid role = JWT.encodeSigned JWT.HS256 (JWT.secret secret) claimsSet
where where
claimsSet = JWT.def { claimsSet = JWT.def {
JWT.unregisteredClaims = Data.Map.fromList [("id", String uid), ("role", String role)] JWT.unregisteredClaims = Data.Map.fromList [("id", String uid), ("role", String role)]
} }
+19 -9
View File
@@ -1,3 +1,4 @@
{-# LANGUAGE QuasiQuotes, ScopedTypeVariables #-}
module Main where module Main where
import Paths_postgrest (version) import Paths_postgrest (version)
@@ -10,21 +11,30 @@ import Control.Monad (unless)
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Network.Wai (strictRequestBody) import Network.Wai (strictRequestBody)
import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Handler.Warp hiding (Connection) 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 Network.Wai.Middleware.RequestLogger (logStdout)
import Data.List (intercalate) import Data.List (intercalate)
import Data.Version (versionBranch) import Data.Version (versionBranch)
import Data.Functor.Identity
import Data.Text(Text)
import qualified Hasql as H import qualified Hasql as H
import qualified Hasql.Postgres as P import qualified Hasql.Postgres as P
import Options.Applicative hiding (columns) 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 :: IO ()
main = do main = do
hSetBuffering stdout LineBuffering
hSetBuffering stdin LineBuffering
hSetBuffering stderr NoBuffering
let opts = info (helper <*> argParser) $ let opts = info (helper <*> argParser) $
fullDesc fullDesc
<> progDesc ( <> progDesc (
@@ -51,19 +61,19 @@ main = do
appSettings = setPort port appSettings = setPort port
. setServerName (cs $ "postgrest/" <> prettyVersion) . setServerName (cs $ "postgrest/" <> prettyVersion)
$ defaultSettings $ defaultSettings
middle = logStdout middle = logStdout . defaultMiddle (configSecure conf)
. (if configSecure conf then redirectInsecure else id)
. gzip def . cors corsPolicy
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
poolSettings <- maybe (fail "Improper session settings") return $ poolSettings <- maybe (fail "Improper session settings") return $
H.poolSettings (fromIntegral $ configPool conf) 30 H.poolSettings (fromIntegral $ configPool conf) 30
pool :: H.Pool P.Postgres pool :: H.Pool P.Postgres
<- H.acquirePool pgSettings poolSettings <- 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 runSettings appSettings $ middle $ \req respond -> do
body <- strictRequestBody req 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 authenticated conf (app conf body) req
either (respond . errResponse) respond resOrError either (respond . errResponse) respond resOrError
+29 -12
View File
@@ -3,7 +3,7 @@
module PostgREST.Middleware where module PostgREST.Middleware where
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe, isNothing)
import Data.Monoid import Data.Monoid
import Data.Text import Data.Text
-- import Data.Pool(withResource, Pool) -- import Data.Pool(withResource, Pool)
@@ -12,15 +12,19 @@ import qualified Hasql as H
import qualified Hasql.Postgres as P import qualified Hasql.Postgres as P
import Data.String.Conversions(cs) 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 (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, import Network.Wai (Application, requestHeaders, responseLBS, rawPathInfo,
rawQueryString, isSecure, Request(..), Response) 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 Network.URI (URI(..), parseURI)
import PostgREST.Config (AppConfig(..)) import PostgREST.Config (AppConfig(..), corsPolicy)
import PostgREST.Auth (LoginAttempt(..), signInRole, signInWithJWT, setRole, resetRole) import PostgREST.Auth (LoginAttempt(..), signInRole, signInWithJWT, setRole, setUserId)
import PostgREST.App (contentTypeForAccept)
import Codec.Binary.Base64.String (decode) import Codec.Binary.Base64.String (decode)
import Prelude import Prelude
@@ -35,8 +39,8 @@ authenticated conf app req = do
return $ responseLBS status400 [] "Malformed basic auth header" return $ responseLBS status400 [] "Malformed basic auth header"
LoginFailed -> LoginFailed ->
return $ responseLBS status401 [] "Invalid username or password" return $ responseLBS status401 [] "Invalid username or password"
LoginSuccess role -> if role /= currentRole then runInRole role 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 NoCredentials -> if anon /= currentRole then runInRole anon "" else app req
where where
jwtSecret = cs $ configJwtSecret conf jwtSecret = cs $ configJwtSecret conf
@@ -54,12 +58,11 @@ authenticated conf app req = do
return $ signInWithJWT jwtSecret jwt return $ signInWithJWT jwtSecret jwt
_ -> return NoCredentials _ -> return NoCredentials
runInRole :: Text -> H.Tx P.Postgres s Response runInRole :: Text -> Text -> H.Tx P.Postgres s Response
runInRole r = do runInRole r uid = do
setUserId uid
setRole r setRole r
res <- app req app req
resetRole
return res
redirectInsecure :: Application -> Application redirectInsecure :: Application -> Application
@@ -82,3 +85,17 @@ redirectInsecure app req respond = do
Nothing -> Nothing ->
respond $ responseLBS status400 [] "SSL is required" respond $ responseLBS status400 [] "SSL is required"
else app req respond 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
View File
@@ -10,8 +10,8 @@ import qualified Hasql.Postgres as P
import qualified Hasql.Backend as B import qualified Hasql.Backend as B
import qualified Data.Text as T import qualified Data.Text as T
import qualified Data.HashMap.Strict as H
import Text.Regex.TDFA ( (=~) ) import Text.Regex.TDFA ( (=~) )
import Text.Regex.TDFA.Text ()
import qualified Network.HTTP.Types.URI as Net import qualified Network.HTTP.Types.URI as Net
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import Data.Monoid import Data.Monoid
@@ -34,9 +34,9 @@ instance Monoid PStmt where
mempty = B.Stmt "" empty True mempty = B.Stmt "" empty True
type StatementT = PStmt -> PStmt type StatementT = PStmt -> PStmt
data QualifiedTable = QualifiedTable { data QualifiedIdentifier = QualifiedIdentifier {
qtSchema :: T.Text qiSchema :: T.Text
, qtName :: T.Text , qiName :: T.Text
} deriving (Show) } deriving (Show)
data OrderTerm = OrderTerm { data OrderTerm = OrderTerm {
@@ -52,14 +52,15 @@ limitT r q =
limit = maybe "ALL" (cs . show) $ join $ rangeLimit <$> r limit = maybe "ALL" (cs . show) $ join $ rangeLimit <$> r
offset = cs . show $ fromMaybe 0 $ rangeOffset <$> r offset = cs . show $ fromMaybe 0 $ rangeOffset <$> r
whereT :: Net.Query -> StatementT whereT :: QualifiedIdentifier -> Net.Query -> StatementT
whereT params q = whereT table params q =
if L.null cols if L.null cols
then q then q
else q <> B.Stmt " where " empty True <> conjunction else q <> B.Stmt " where " empty True <> conjunction
where where
cols = [ col | col <- params, fst col `notElem` ["order"] ] 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 :: PStmt -> T.Text -> StatementT
withT (B.Stmt eq ep epre) v (B.Stmt wq wp wpre) = 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 :: StatementT
countT s = 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 :: QualifiedIdentifier -> PStmt
countRows t = B.Stmt ("select count(1) from " <> fromQt t) empty True 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 :: StatementT
asJsonWithCount s = s { B.stmtTemplate = asJsonWithCount = withCount . asJson
"count(t), array_to_json(array_agg(row_to_json(t)))::character varying from ("
<> B.stmtTemplate s <> ") t" } 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 :: StatementT
asJsonRow s = s { B.stmtTemplate = "row_to_json(t) from (" <> B.stmtTemplate s <> ") t" } asJsonRow s = s { B.stmtTemplate = "row_to_json(t) from (" <> B.stmtTemplate s <> ") t" }
selectStar :: QualifiedTable -> PStmt selectStar :: QualifiedIdentifier -> PStmt
selectStar t = B.Stmt ("select * from " <> fromQt t) empty True selectStar t = B.Stmt ("select * from " <> fromQi t) empty True
returningStarT :: StatementT returningStarT :: StatementT
returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" } returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" }
deleteFrom :: QualifiedTable -> PStmt deleteFrom :: QualifiedIdentifier -> PStmt
deleteFrom t = B.Stmt ("delete from " <> fromQt t) empty True deleteFrom t = B.Stmt ("delete from " <> fromQi t) empty True
insertInto :: QualifiedTable insertInto :: QualifiedIdentifier
-> V.Vector T.Text -> V.Vector T.Text
-> V.Vector (V.Vector JSON.Value) -> V.Vector (V.Vector JSON.Value)
-> PStmt -> PStmt
insertInto t cols vals 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 | otherwise = B.Stmt
("insert into " <> fromQt t <> " (" <> ("insert into " <> fromQi t <> " (" <>
T.intercalate ", " (V.toList $ V.map pgFmtIdent cols) <> T.intercalate ", " (V.toList $ V.map pgFmtIdent cols) <>
") values " ") values "
<> T.intercalate ", " <> T.intercalate ", "
@@ -133,38 +151,48 @@ insertInto t cols vals
<> ")" <> ")"
) vals ) vals
) )
<> " returning row_to_json(" <> fromQt t <> ".*)") <> " returning row_to_json(" <> fromQi t <> ".*)")
empty True empty True
insertSelect :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt insertSelect :: QualifiedIdentifier -> [T.Text] -> [JSON.Value] -> PStmt
insertSelect t [] _ = B.Stmt 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 insertSelect t cols vals = B.Stmt
("insert into " <> fromQt t <> " (" ("insert into " <> fromQi t <> " ("
<> T.intercalate ", " (map pgFmtIdent cols) <> T.intercalate ", " (map pgFmtIdent cols)
<> ") select " <> ") select "
<> T.intercalate ", " (map insertableValue vals)) <> T.intercalate ", " (map insertableValue vals))
empty True empty True
update :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt update :: QualifiedIdentifier -> [T.Text] -> [JSON.Value] -> PStmt
update t cols vals = B.Stmt update t cols vals = B.Stmt
("update " <> fromQt t <> " set (" ("update " <> fromQi t <> " set ("
<> T.intercalate ", " (map pgFmtIdent cols) <> T.intercalate ", " (map pgFmtIdent cols)
<> ") = (" <> ") = ("
<> T.intercalate ", " (map insertableValue vals) <> T.intercalate ", " (map insertableValue vals)
<> ")") <> ")")
empty True empty True
wherePred :: Net.QueryItem -> PStmt callProc :: QualifiedIdentifier -> JSON.Object -> PStmt
wherePred (col, predicate) = callProc qi params = do
B.Stmt (" " <> pgFmtJsonbPath (cs col) <> " " <> op <> " " <> 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 if opCode `elem` ["is","isnot"] then whiteList value
else cs sqlValue) else cs sqlValue)
empty True empty True
where where
opCode:rest = T.split (=='.') $ cs $ fromMaybe "." predicate headPredicate:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
value = T.intercalate "." rest 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 ") whiteList val = fromMaybe (cs (pgFmtLit val) <> "::unknown ")
(L.find ((==) . T.toLower $ val) (L.find ((==) . T.toLower $ val)
["null","true","false"]) ["null","true","false"])
@@ -175,6 +203,8 @@ wherePred (col, predicate) =
"like" -> unknownLiteral $ T.map star value "like" -> unknownLiteral $ T.map star value
"ilike" -> unknownLiteral $ T.map star value "ilike" -> unknownLiteral $ T.map star value
"in" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") " "in" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"notin" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"@@" -> "to_tsquery(" <> unknownLiteral value <> ") "
_ -> unknownLiteral value _ -> unknownLiteral value
op = case opCode of op = case opCode of
@@ -187,8 +217,10 @@ wherePred (col, predicate) =
"like"-> "like" "like"-> "like"
"ilike"-> "ilike" "ilike"-> "ilike"
"in" -> "in" "in" -> "in"
"notin" -> "not in"
"is" -> "is" "is" -> "is"
"isnot" -> "is not" "isnot" -> "is not"
"@@" -> "@@"
_ -> "=" _ -> "="
orderParse :: Net.Query -> [OrderTerm] orderParse :: Net.Query -> [OrderTerm]
@@ -236,11 +268,11 @@ parseJsonbPath p =
(KeyIdentifier b) (KeyIdentifier b)
_ -> Nothing _ -> Nothing
pgFmtJsonbPath :: T.Text -> T.Text pgFmtJsonbPath :: QualifiedIdentifier -> T.Text -> T.Text
pgFmtJsonbPath p = pgFmtJsonbPath table p =
pgFmtJsonbPath' $ fromMaybe (ColIdentifier p) (parseJsonbPath p) pgFmtJsonbPath' $ fromMaybe (ColIdentifier p) (parseJsonbPath p)
where where
pgFmtJsonbPath' (ColIdentifier i) = pgFmtIdent i pgFmtJsonbPath' (ColIdentifier i) = fromQi table <> "." <> pgFmtIdent i
pgFmtJsonbPath' (KeyIdentifier i) = pgFmtLit i pgFmtJsonbPath' (KeyIdentifier i) = pgFmtLit i
pgFmtJsonbPath' (SingleArrow a b) = pgFmtJsonbPath' (SingleArrow a b) =
pgFmtJsonbPath' a <> "->" <> pgFmtJsonbPath' b pgFmtJsonbPath' a <> "->" <> pgFmtJsonbPath' b
@@ -250,26 +282,26 @@ pgFmtJsonbPath p =
pgFmtIdent :: T.Text -> T.Text pgFmtIdent :: T.Text -> T.Text
pgFmtIdent x = pgFmtIdent x =
let escaped = T.replace "\"" "\"\"" (trimNullChars $ cs x) in let escaped = T.replace "\"" "\"\"" (trimNullChars $ cs x) in
if escaped =~ danger if (cs escaped :: BS.ByteString) =~ danger
then "\"" <> escaped <> "\"" then "\"" <> escaped <> "\""
else 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 :: T.Text -> T.Text
pgFmtLit x = pgFmtLit x =
let trimmed = trimNullChars x let trimmed = trimNullChars x
escaped = "'" <> T.replace "'" "''" trimmed <> "'" escaped = "'" <> T.replace "'" "''" trimmed <> "'"
slashed = T.replace "\\" "\\\\" escaped in slashed = T.replace "\\" "\\\\" escaped in
cs $ if escaped =~ ("\\\\" :: T.Text) if T.isInfixOf "\\\\" escaped
then "E" <> slashed then "E" <> slashed
else slashed else slashed
trimNullChars :: T.Text -> T.Text trimNullChars :: T.Text -> T.Text
trimNullChars = T.takeWhile (/= '\x0') trimNullChars = T.takeWhile (/= '\x0')
fromQt :: QualifiedTable -> T.Text fromQi :: QualifiedIdentifier -> T.Text
fromQt t = pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t) fromQi t = pgFmtIdent (qiSchema t) <> "." <> pgFmtIdent (qiName t)
unquoted :: JSON.Value -> T.Text unquoted :: JSON.Value -> T.Text
unquoted (JSON.String t) = t unquoted (JSON.String t) = t
+46 -23
View File
@@ -3,12 +3,12 @@
FlexibleContexts #-} FlexibleContexts #-}
module PostgREST.PgStructure where module PostgREST.PgStructure where
import PostgREST.PgQuery (QualifiedTable(..)) import PostgREST.PgQuery (QualifiedIdentifier(..))
import Data.Text hiding (foldl, map, zipWith, concat) import Data.Text hiding (foldl, map, zipWith, concat)
import Data.Aeson import Data.Aeson
import Data.Functor.Identity import Data.Functor.Identity
import Data.String.Conversions (cs) import Data.String.Conversions (cs)
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe, isJust)
import Control.Applicative import Control.Applicative
import qualified Data.Map as Map import qualified Data.Map as Map
@@ -18,7 +18,7 @@ import qualified Hasql.Postgres as P
import Prelude 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 foreignKeys table = do
r <- H.listEx $ [H.stmt| r <- H.listEx $ [H.stmt|
select kcu.column_name, ccu.table_name AS foreign_table_name, select kcu.column_name, ccu.table_name AS foreign_table_name,
@@ -31,7 +31,7 @@ foreignKeys table = do
where constraint_type = 'FOREIGN KEY' where constraint_type = 'FOREIGN KEY'
and tc.table_name=? and tc.table_schema = ? and tc.table_name=? and tc.table_schema = ?
order by kcu.column_name order by kcu.column_name
|] (qtName table) (qtSchema table) |] (qiName table) (qiSchema table)
return $ foldl addKey Map.empty r return $ foldl addKey Map.empty r
where where
@@ -43,22 +43,37 @@ tables :: Text -> H.Tx P.Postgres s [Table]
tables schema = do tables schema = do
rows <- H.listEx $ rows <- H.listEx $
[H.stmt| [H.stmt|
select table_schema, table_name, select
is_insertable_into n.nspname as table_schema,
from information_schema.tables relname as table_name,
where table_schema = ? c.relkind = 'r' or (c.relkind IN ('v', 'f')) and (pg_relation_is_updatable(c.oid::regclass, false) & 8) = 8
order by table_name 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 |] schema
return $ map tableFromRow rows return $ map tableFromRow rows
columns :: QualifiedTable -> H.Tx P.Postgres s [Column] columns :: QualifiedIdentifier -> H.Tx P.Postgres s [Column]
columns table = do columns table = do
cols <- H.listEx $ [H.stmt| cols <- H.listEx $ [H.stmt|
select info.table_schema as schema, info.table_name as table_name, select info.table_schema as schema, info.table_name as table_name,
info.column_name as name, info.ordinal_position as position, info.column_name as name, info.ordinal_position as position,
info.is_nullable as nullable, info.data_type as col_type, info.is_nullable::boolean as nullable, info.data_type as col_type,
info.is_updatable as updatable, info.is_updatable::boolean as updatable,
info.character_maximum_length as max_len, info.character_maximum_length as max_len,
info.numeric_precision as precision, info.numeric_precision as precision,
info.column_default as default_value, info.column_default as default_value,
@@ -82,7 +97,7 @@ columns table = do
) as enum_info ) as enum_info
on (info.udt_name = enum_info.n) on (info.udt_name = enum_info.n)
order by position |] order by position |]
(qtSchema table) (qtName table) (qiSchema table) (qiName table)
fks <- foreignKeys table fks <- foreignKeys table
return $ map (addFK fks . columnFromRow) cols return $ map (addFK fks . columnFromRow) cols
@@ -91,7 +106,7 @@ columns table = do
addFK fks col = col { colFK = Map.lookup (cs . colName $ col) fks } 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 primaryKeyColumns table = do
r <- H.listEx $ [H.stmt| r <- H.listEx $ [H.stmt|
select kc.column_name 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.table_name = tc.table_name and kc.table_schema = tc.table_schema
and kc.constraint_name = tc.constraint_name and kc.constraint_name = tc.constraint_name
and kc.table_schema = ? and kc.table_schema = ?
and kc.table_name = ? |] (qtSchema table) (qtName table) and kc.table_name = ? |] (qiSchema table) (qiName table)
return $ map runIdentity r return $ map runIdentity r
doesProcExist :: Text -> Text -> H.Tx P.Postgres s Bool
toBool :: Text -> Bool doesProcExist schema proc = do
toBool = (== "YES") 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 { data Table = Table {
tableSchema :: Text tableSchema :: Text
@@ -135,16 +158,16 @@ data Column = Column {
, colFK :: Maybe ForeignKey , colFK :: Maybe ForeignKey
} deriving (Show) } deriving (Show)
tableFromRow :: (Text, Text, Text) -> Table tableFromRow :: (Text, Text, Bool) -> Table
tableFromRow (s, n, i) = Table s n (toBool i) tableFromRow (s, n, i) = Table s n i
columnFromRow :: (Text, Text, Text, columnFromRow :: (Text, Text, Text,
Int, Text, Text, Int, Bool, Text,
Text, Maybe Int, Maybe Int, Bool, Maybe Int, Maybe Int,
Maybe Text, Maybe Text) Maybe Text, Maybe Text)
-> Column -> Column
columnFromRow (s, t, n, pos, nul, typ, u, l, p, d, e) = 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 where
parseEnum :: Maybe Text -> [Text] parseEnum :: Maybe Text -> [Text]
+7
View File
@@ -0,0 +1,7 @@
flags: {}
packages:
- '.'
extra-deps:
- Ranged-sets-0.3.0
- packdeps-0.4.1
resolver: lts-3.1
+12 -5
View File
@@ -28,19 +28,26 @@ spec = beforeAll
let auth = authHeaderBasic "jdoe" "1234" let auth = authHeaderBasic "jdoe" "1234"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200 `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 it "allows users to login (JWT)" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |] _ <- 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 { `shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"} |] matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"} |]
, matchStatus = 201 , matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json"] , matchHeaders = ["Content-Type" <:> "application/json"]
} }
it "indicates login failure (JWT)" $ do it "indicates login failure (JWT)" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |] _ <- 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 { `shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"message":"Failed authentication."} |] matchBody = Just [json| {"message":"Failed authentication."} |]
, matchStatus = 401 , matchStatus = 401
@@ -51,4 +58,4 @@ spec = beforeAll
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |] _ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0" let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] "" request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200 `shouldRespondWith` 200
+1 -1
View File
@@ -22,7 +22,7 @@ spec = around withApp $ describe "CORS" $ do
("Host", "localhost:3000"), ("Host", "localhost:3000"),
("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0"), ("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0"),
("Origin", "http://localhost:8000"), ("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-Language", "en-US,en;q=0.5"),
("Accept-Encoding", "gzip, deflate"), ("Accept-Encoding", "gzip, deflate"),
("Referer", "http://localhost:8000/"), ("Referer", "http://localhost:8000/"),
+27 -6
View File
@@ -87,12 +87,7 @@ spec = afterAll_ resetDb $ around withApp $ do
context "with invalid json payload" $ context "with invalid json payload" $
it "fails with 400 and error" $ it "fails with 400 and error" $
post "/simple_pk" "}{ x = 2" post "/simple_pk" "}{ x = 2" `shouldRespondWith` 400
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"message":"Failed to parse JSON payload. Failed reading: satisfy"} |]
, matchStatus = 400
, matchHeaders = []
}
context "jsonb" . after_ (clearTable "json") $ do context "jsonb" . after_ (clearTable "json") $ do
it "serializes nested object" $ do it "serializes nested object" $ do
@@ -266,6 +261,12 @@ spec = afterAll_ resetDb $ around withApp $ do
liftIO $ simpleHeaders g liftIO $ simpleHeaders g
`shouldSatisfy` matchHeader "Content-Range" "0-9/10" `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 it "can provide a representation" $ do
_ <- post "/items" _ <- post "/items"
[json| { id: 1 } |] [json| { id: 1 } |]
@@ -274,3 +275,23 @@ spec = afterAll_ resetDb $ around withApp $ do
[("Prefer", "return=representation")] [("Prefer", "return=representation")]
[json| { id: 99 } |] [json| { id: 99 } |]
`shouldRespondWith` [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
+115 -2
View File
@@ -3,6 +3,7 @@ module Feature.QuerySpec where
import Test.Hspec import Test.Hspec
import Test.Hspec.Wai import Test.Hspec.Wai
import Test.Hspec.Wai.JSON import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders)) import Network.Wai.Test (SResponse(simpleHeaders))
import SpecHelper import SpecHelper
@@ -10,6 +11,7 @@ import SpecHelper
spec :: Spec spec :: Spec
spec = spec =
beforeAll (clearTable "items" >> createItems 15) beforeAll (clearTable "items" >> createItems 15)
. beforeAll (clearTable "nullable_integer" >> createNullInteger)
. beforeAll ( . beforeAll (
clearTable "no_pk" >> clearTable "no_pk" >>
createNulls 2 >> createNulls 2 >>
@@ -17,6 +19,11 @@ spec =
createJsonData) createJsonData)
. afterAll_ (clearTable "items" >> clearTable "no_pk" >> clearTable "simple_pk") . afterAll_ (clearTable "items" >> clearTable "no_pk" >> clearTable "simple_pk")
. around withApp $ do . 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" $ describe "Querying a nonexistent table" $
it "causes a 404" $ it "causes a 404" $
get "/faketable" `shouldRespondWith` 404 get "/faketable" `shouldRespondWith` 404
@@ -30,6 +37,31 @@ spec =
, matchHeaders = ["Content-Range" <:> "0-0/1"] , 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" $ it "matches items IN" $
get "/items?id=in.1,3,5" get "/items?id=in.1,3,5"
`shouldRespondWith` ResponseMatcher { `shouldRespondWith` ResponseMatcher {
@@ -38,10 +70,32 @@ spec =
, matchHeaders = ["Content-Range" <:> "0-2/3"] , 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` get "/no_pk?a=is.null" `shouldRespondWith`
[json| [{"a": null, "b": null}] |] [json| [{"a": null, "b": null}] |]
get "/nullable_integer?a=is.null" `shouldRespondWith` "[{\"a\":null}]"
it "matches with like" $ do it "matches with like" $ do
get "/simple_pk?k=like.*yx" `shouldRespondWith` get "/simple_pk?k=like.*yx" `shouldRespondWith`
"[{\"k\":\"xyyx\",\"extra\":\"u\"}]" "[{\"k\":\"xyyx\",\"extra\":\"u\"}]"
@@ -50,12 +104,31 @@ spec =
get "/simple_pk?k=like.*YY*" `shouldRespondWith` get "/simple_pk?k=like.*YY*" `shouldRespondWith`
"[{\"k\":\"xYYx\",\"extra\":\"v\"}]" "[{\"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 it "matches with ilike" $ do
get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith` get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith`
"[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]" "[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]"
get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith` get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith`
"[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]" "[{\"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 describe "ordering response" $ do
it "by a column asc" $ it "by a column asc" $
get "/items?id=lte.2&order=id.asc" get "/items?id=lte.2&order=id.asc"
@@ -105,6 +178,31 @@ spec =
it "without other constraints" $ it "without other constraints" $
get "/items?order=asc.id" `shouldRespondWith` 200 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 */* in accept header" $
request methodGet "/simple_pk"
(acceptHdrs "*/*") ""
`shouldRespondWith` 200
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 describe "Canonical location" $ do
it "Sets Content-Location with alphabetized params" $ it "Sets Content-Location with alphabetized params" $
get "/no_pk?b=eq.1&a=eq.1" get "/no_pk?b=eq.1&a=eq.1"
@@ -121,9 +219,24 @@ spec =
respHeaders `shouldSatisfy` matchHeader respHeaders `shouldSatisfy` matchHeader
"Content-Location" "/simple_pk" "Content-Location" "/simple_pk"
describe "jsonb" $ describe "jsonb" $ do
it "can filter by properties inside json column" $ do it "can filter by properties inside json column" $ do
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith` get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
[json| [{"data": {"foo": {"bar": "baz"}}}] |] [json| [{"data": {"foo": {"bar": "baz"}}}] |]
get "/json?data->foo->>bar=eq.fake" `shouldRespondWith` get "/json?data->foo->>bar=eq.fake" `shouldRespondWith`
[json| [] |] [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"}] |]
+5
View File
@@ -16,12 +16,17 @@ spec = around withApp $ do
`shouldRespondWith` [json| [ `shouldRespondWith` [json| [
{"schema":"1","name":"auto_incrementing_pk","insertable":true} {"schema":"1","name":"auto_incrementing_pk","insertable":true}
, {"schema":"1","name":"compound_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":"has_fk","insertable":true}
, {"schema":"1","name":"insertable_view_with_join","insertable":true}
, {"schema":"1","name":"items","insertable":true} , {"schema":"1","name":"items","insertable":true}
, {"schema":"1","name":"json","insertable":true} , {"schema":"1","name":"json","insertable":true}
, {"schema":"1","name":"materialized_view","insertable":false}
, {"schema":"1","name":"menagerie","insertable":true} , {"schema":"1","name":"menagerie","insertable":true}
, {"schema":"1","name":"no_pk","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":"simple_pk","insertable":true}
, {"schema":"1","name":"tsearch","insertable":true}
] |] ] |]
{matchStatus = 200} {matchStatus = 200}
+14 -6
View File
@@ -15,19 +15,18 @@ import qualified Data.Vector as V
import Control.Monad (void) import Control.Monad (void)
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange, import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
hRange, hAuthorization) hRange, hAuthorization, hAccept)
import Codec.Binary.Base64.String (encode) import Codec.Binary.Base64.String (encode)
import Data.CaseInsensitive (CI(..)) import Data.CaseInsensitive (CI(..))
import Data.Maybe (fromMaybe) import Data.Maybe (fromMaybe)
import Text.Regex.TDFA ((=~)) import Text.Regex.TDFA ((=~))
import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Char8 as BS
import Network.Wai.Middleware.Cors (cors)
import System.Process (readProcess) import System.Process (readProcess)
import qualified Data.Aeson.Types as J import qualified Data.Aeson.Types as J
import PostgREST.App (app) import PostgREST.App (app)
import PostgREST.Config (AppConfig(..), corsPolicy) import PostgREST.Config (AppConfig(..))
import PostgREST.Middleware import PostgREST.Middleware
import PostgREST.Error(errResponse) import PostgREST.Error(errResponse)
@@ -55,11 +54,11 @@ withApp perform = do
perform $ middle $ \req resp -> do perform $ middle $ \req resp -> do
body <- strictRequestBody req 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 $ authenticated cfg (app cfg body) req
either (resp . errResponse) resp result either (resp . errResponse) resp result
where middle = cors corsPolicy where middle = defaultMiddle False
resetDb :: IO () resetDb :: IO ()
@@ -84,6 +83,9 @@ loadFixture name =
rangeHdrs :: ByteRange -> [Header] rangeHdrs :: ByteRange -> [Header]
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)] rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
acceptHdrs :: BS.ByteString -> [Header]
acceptHdrs mime = [(hAccept, mime)]
rangeUnit :: Header rangeUnit :: Header
rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items") rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")
@@ -94,7 +96,7 @@ matchHeader name valRegex headers =
authHeaderBasic :: String -> String -> Header authHeaderBasic :: String -> String -> Header
authHeaderBasic u p = authHeaderBasic u p =
(hAuthorization, cs $ "Basic " ++ encode (u ++ ":" ++ p)) (hAuthorization, cs $ "Basic " ++ encode (u ++ ":" ++ p))
authHeaderJWT :: String -> Header authHeaderJWT :: String -> Header
authHeaderJWT token = authHeaderJWT token =
(hAuthorization, cs $ "Bearer " ++ 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)|] 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] 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 :: IO ()
createLikableStrings = do createLikableStrings = do
pool <- testPool pool <- testPool
+1 -1
View File
@@ -79,7 +79,7 @@ spec = around dbWithSchema $ do
addUser user pass role conn addUser user pass role conn
return conn) $ do return conn) $ do
it "accepts correct credentials and return the role" $ \conn -> 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 it "returns nothing with bad creds" $ \conn -> do
signInRole "not-a-user" pass conn `shouldReturn` LoginFailed signInRole "not-a-user" pass conn `shouldReturn` LoginFailed
+1 -1
View File
@@ -14,7 +14,7 @@ spec = around dbWithSchema $ beforeWith setRole $ do
it "shows all the tables" $ \conn -> do it "shows all the tables" $ \conn -> do
ts <- tables "1" conn ts <- tables "1" conn
map tableName ts `shouldBe` ["authors_only","auto_incrementing_pk", 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 describe "columns" $ do
it "responds with each column for the table" $ \conn -> do it "responds with each column for the table" $ \conn -> do
+142 -8
View File
@@ -72,6 +72,28 @@ $$;
ALTER FUNCTION postgrest.update_owner() OWNER TO postgrest_test; 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 search_path = "1", pg_catalog;
SET default_tablespace = ''; SET default_tablespace = '';
@@ -80,6 +102,7 @@ SET default_with_oids = false;
CREATE TABLE authors_only ( CREATE TABLE authors_only (
owner character varying NOT NULL,
secret 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; 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 ( 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 ( CREATE TABLE menagerie (
"integer" integer NOT NULL, "integer" integer NOT NULL,
double double precision NOT NULL, double double precision NOT NULL,
@@ -194,6 +259,14 @@ CREATE TABLE no_pk (
ALTER TABLE "1".no_pk OWNER TO postgrest_test; 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 ( CREATE TABLE simple_pk (
k character varying NOT NULL, k character varying NOT NULL,
extra character varying NOT NULL extra character varying NOT NULL
@@ -212,6 +285,12 @@ CREATE TABLE json
ALTER TABLE "1".json OWNER TO postgrest_test; 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; 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); 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; 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; 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 ALTER TABLE ONLY authors_only
ADD CONSTRAINT authors_only_pkey PRIMARY KEY (secret); 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 ALTER TABLE ONLY auto_incrementing_pk
ADD CONSTRAINT auto_incrementing_pk_pkey PRIMARY KEY (id); 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; 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 PUBLIC;
REVOKE ALL ON SEQUENCE items_id_seq FROM postgrest_test; 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 PUBLIC;
REVOKE ALL ON TABLE simple_pk FROM postgrest_test; REVOKE ALL ON TABLE simple_pk FROM postgrest_test;
GRANT ALL ON TABLE simple_pk TO 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; 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; SET search_path = postgrest, pg_catalog;