Compare commits

...
379 Commits
Author SHA1 Message Date
Joe Nelson bd1826670e Version 0.2.12.0 2015-10-25 11:46:16 -07:00
Joe Nelson aab1500879 Merge pull request #330 from ruslantalpa/master
Fix for #321
2015-10-24 12:57:31 -07:00
Ruslan Talpa 0ed8ec9868 Fix for #321 2015-10-24 22:50:03 +03:00
Joe Nelson df00d728ff Docs about methods for updating records 2015-10-21 21:12:27 -07:00
Joe Nelson 9fad028074 Document API read requests 2015-10-20 19:25:31 -07:00
Joe Nelson e0fe610d7b Merge pull request #324 from begriffs/mkdocs
Documentation outline
2015-10-19 16:51:53 -07:00
Joe Nelson 20198367da Documentation outline 2015-10-19 16:45:52 -07:00
Joe Nelson d3eca26393 Merge pull request #319 from ruslantalpa/master
avoid ByteString -> Text -> ByteString converstion of the response body
2015-10-15 10:22:30 -07:00
Ruslan Talpa 864c865e52 avoid ByteString -> Text -> ByteString converstion of the response body 2015-10-15 16:20:57 +03:00
Joe Nelson 3e9f9f300c Merge pull request #309 from ruslantalpa/master
Fix for #302 (supporting compound foreign keys in relations)
2015-10-08 12:21:00 -07:00
Ruslan Talpa 6ba4dc4617 Fix for #302 2015-10-08 11:33:42 +03:00
Joe Nelson 2d0c4fecd8 Merge pull request #307 from calebmer/tolerate-missing-role
Tolerate missing role in user creation
2015-10-07 18:09:04 -07:00
calebmer 8e107e5b24 Tolerate missing role in user creation 2015-10-07 20:01:46 -04:00
Joe Nelson 2f551fea97 Merge pull request #305 from diogob/moves_minimum_pg_version_to_config
Moves minimum pg version to config
2015-10-06 17:58:18 -07:00
Diogo Biazus adfd980a60 Moves minimum pg version to config, eliminates magic constant from main and improves docs. 2015-10-06 20:27:54 -04:00
Joe Nelson f186d6bb33 Merge pull request #299 from fike/master
Added files in debian subdirectory.
2015-10-06 09:25:14 -07:00
Joe Nelson 180d647c70 Merge pull request #303 from diogob/refactor_config
Moves all config related code to PostgREST.Config module and sets default db-pass
2015-10-05 19:13:23 -07:00
Diogo Biazus 3020812f71 Adds basic haddock comments on Config module 2015-10-05 21:14:18 -04:00
Diogo Biazus ed9eea3e8b Imports (<>) from Data.Monoid 2015-10-05 18:53:44 -04:00
Diogo Biazus 346170220e Uses empty password by default 2015-10-05 14:58:53 -04:00
Diogo Biazus 3a1f7938e8 Moves all config related code to PostgREST.Config module and
tweak the code to better encapsulate functionality.
2015-10-05 14:50:35 -04:00
Joe Nelson 9b222fb93d Thank you @ruslantalpa 2015-10-04 10:10:17 -07:00
Joe Nelson 0f1b313d96 Merge pull request #295 from ruslantalpa/master
Extend the capabilities of PostgREST #280
2015-10-04 09:42:32 -07:00
Ruslan Talpa b675276f31 merge new test from @diogob 2015-10-04 07:26:05 +03:00
Ruslan Talpa c014d09072 Merge pull request #1 from diogob/fix_composite_fk_children
Adds a failing spec for child relation (comments) using a composite FK
2015-10-04 07:17:04 +03:00
Diogo Biazus 4dd508a628 Adds a failing spec for child relation (comments) using a composite foreign key (references to users_tasks) 2015-10-03 15:02:02 -04:00
Ruslan Talpa e23a49395b error formatting for parsers and relation 2015-10-02 11:36:56 +03:00
Fernando Ike 80f9685bb9 Fixed year dat in d/copyright 2015-10-01 18:15:08 -03:00
Ruslan Talpa 7754f96af8 Merge remote-tracking branch 'begriffs/master' 2015-10-01 22:22:14 +03:00
Joe Nelson 7d8523a786 Merge pull request #298 from diogob/fix_count_none
Fix count=none when we have filters
2015-10-01 10:26:25 -07:00
Ruslan Talpa e6baafdb8f remove space 2015-10-01 15:28:02 +03:00
Ruslan Talpa c7666c0a67 tests for table relations & bugfix for not detecting child relations of view 2015-10-01 14:20:40 +03:00
Ruslan Talpa 8ff4b4be66 lint fix 2015-10-01 10:36:33 +03:00
Ruslan Talpa ffeb8b1d5f Merge remote-tracking branch 'begriffs/master' 2015-10-01 10:32:30 +03:00
Ruslan Talpa 812135d1e5 cleanup suggested by @begriffs 2015-10-01 10:30:41 +03:00
Joe Nelson 3eb58c511b Merge pull request #300 from diogob/update_stack_37
Updates stackage resolver to 3.7
2015-09-30 22:30:40 -07:00
Diogo Biazus 0df6ea57ae Updates stackage resolver to 3.7 2015-09-30 23:35:44 -04:00
Diogo Biazus f0ec46fd11 Fix count=none when we have filters 2015-09-30 16:50:52 -04:00
Fernando Ike add25ae68e Added files in debian subdirectory.
These files are base to build postgrest Debian package. Additional, it
has TODO-deps.md file with whole task to build postgrest as Debian
official package.
2015-09-30 17:49:11 -03:00
Ruslan Talpa 7f2c39ef94 small cleanup suggested by @begriffs 2015-09-29 19:16:25 +03:00
Ruslan Talpa 94b1d2815c small changes suggested by @diogob 2015-09-29 11:40:33 +03:00
Ruslan Talpa 1da26cac98 trying to make it work with ghc 7.8 (3) 2015-09-28 16:54:34 +03:00
Ruslan Talpa a4a2c8b886 trying to make it work with ghc 7.8 (2) 2015-09-28 16:46:33 +03:00
Ruslan Talpa 68c7f45be1 trying to make it work with ghc 7.8 2015-09-28 16:40:22 +03:00
Ruslan Talpa dbf3d5809b add bifunctors to cabal config 2015-09-28 16:23:57 +03:00
Ruslan Talpa bcf3bf5586 handle many to many relations 2015-09-28 15:53:03 +03:00
Ruslan Talpa edff915f9e delete unused functions 2015-09-28 13:41:45 +03:00
Ruslan Talpa 9342cc8c8a removide duplication from data types (all tests passing) 2015-09-28 12:06:49 +03:00
Ruslan Talpa db13724131 moved string packing to parsers 2015-09-28 10:11:59 +03:00
Ruslan Talpa 380cab6ca3 Merge branch 'master' into skin 2015-09-25 22:45:49 +03:00
Ruslan Talpa bd4bf489dc Merge remote-tracking branch 'begriffs/master' 2015-09-25 22:14:58 +03:00
Joe Nelson 29ae1bdefd Merge pull request #293 from diogob/prefer_count_none
Prefer count none
2015-09-25 10:35:53 -07:00
Diogo Biazus efd6580b26 Adds Prefer count=none to changelog 2015-09-25 13:22:19 -04:00
Diogo Biazus f08512eaa9 Updates optparse-applicative package version constraints 2015-09-25 12:19:24 -04:00
Diogo Biazus e28b2dd00e Implements Prefer count=none header
Uses Maybe for total parameter in contentRangeH
2015-09-25 10:52:44 -04:00
Ruslan Talpa 3d1736e0d3 fix for generating count query 2015-09-25 16:33:25 +03:00
Ruslan Talpa 2fb5c5187a deleted some commented code 2015-09-25 15:28:48 +03:00
Ruslan Talpa ff2c0b63e2 changed identation to 2 spaces to match the rest of the project 2015-09-25 12:04:44 +03:00
Ruslan Talpa c42832f1c5 code cleanup 2015-09-25 11:51:37 +03:00
Ruslan Talpa 770e04c04a remove unused functions in pgstructure 2015-09-25 10:36:19 +03:00
Ruslan Talpa 54d1e4112a detect primary keys for views & use cached info in PUT/PATCH requests 2015-09-25 10:30:19 +03:00
Ruslan Talpa 89fffd1518 Merge branch 'master' into skin 2015-09-25 10:00:18 +03:00
Diogo Biazus 1fbf276474 Adds spec for Prefer header set to count=none 2015-09-24 18:31:36 -04:00
Ruslan Talpa 8783615ebc detect primary keys for views (fix #217) 2015-09-24 23:30:47 +03:00
Ruslan Talpa f546dc3ac8 small note about a bug 2015-09-24 22:06:44 +03:00
Ruslan Talpa f2d6c59bab fixes for failing tests (only 2 failing, yay! :)) 2015-09-24 16:29:20 +03:00
Ruslan Talpa 448a81dff8 first fully functioning build, some tests are failing (limit,order,csv not implemented yet) 2015-09-24 11:45:34 +03:00
Ruslan Talpa fb92b76a1a integrated skin code gor generating Sql Query (only left to execute it) 2015-09-23 14:31:24 +03:00
Ruslan Talpa c0e17c44ba all tests passing after moving table structure detection at load time 2015-09-23 09:38:40 +03:00
Ruslan Talpa 6f55e1d389 fix for one of the failing tests (acl for tables added) 2015-09-22 18:17:24 +03:00
Ruslan Talpa a7b883c922 moved db structure detection at the beginning (2 tests failing) 2015-09-22 16:56:19 +03:00
Ruslan Talpa 6bd6108619 Merge remote-tracking branch 'begriffs/master' 2015-09-07 09:28:31 +03:00
Joe Nelson aee0c2c731 Test that patch requests can set a field to null
Exploring situation mentioned in #233
2015-09-06 17:07:00 -07:00
Joe Nelson b5e76f42a5 Note @ruslantalpa's contribution in changelog 2015-09-06 16:14:53 -07:00
Joe Nelson 35f05f6fe1 Standardize indentation 2015-09-06 16:13:11 -07:00
Joe Nelson cf2f576ec0 Merge pull request #276 from ruslantalpa/master
Support for &select=col1,col2,col3 as suggested in issue #227
2015-09-06 15:24:47 -07:00
Ruslan Talpa 92a1d8c7e3 constrain cast parameters to letters only 2015-09-05 16:54:09 +03:00
Ruslan Talpa cc418d3519 aditional tests for bad casting parameters 2015-09-05 15:31:36 +03:00
Ruslan Talpa 9c8ac2a489 tests for &select= feature and support for casting columns (usefull when extracting subfields from json columns) 2015-09-05 15:05:53 +03:00
Ruslan Talpa 6c16395dfb change fn name from selectT to select 2015-09-04 19:04:39 +03:00
Ruslan Talpa 40d4fb0d75 Support for extracting fields from json columns 2015-09-02 12:41:02 +03:00
Ruslan Talpa b9fec2ce41 Support for &select=col1,col2,col3 as suggested in issue #227 2015-09-02 11:03:34 +03:00
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
Joe Nelson 4bbf053896 Bump version 2015-05-20 09:42:51 -07:00
Joe Nelson da79f1da3e Merge pull request #193 from srid/makelibrary
Make postgrest a library
2015-05-19 23:59:16 -07:00
Sridhar Ratnakumar c3ad87ffaf Make postgrest usable as a library 2015-05-19 23:40:39 -07:00
Joe Nelson 035acebf59 Update CHANGELOG.md 2015-05-19 22:23:49 -07:00
Joe Nelson 0b665676d7 Update CHANGELOG.md 2015-05-19 22:21:28 -07:00
Joe Nelson dcf62b020f Merge pull request #194 from framp/jwt
JWT support
2015-05-19 22:17:34 -07:00
Federico Rampazzo 77aecc9e86 JWT support 2015-05-20 06:01:50 +01:00
Joe Nelson e35ad0f340 Update changelog, remove debugging 2015-05-12 10:55:51 -07:00
Joe Nelson 709e70561f Provide more information in PATCH response
* 404 if no records updated
* Range header for number updated
* Full results depending on Prefer header

Fixes #187
Fixes #182
2015-05-12 10:47:55 -07:00
Joe Nelson ef0dc26de5 Merge pull request #186 from jcristovao/patch-2
GHC 7.10.1 support
2015-04-24 09:37:27 -07:00
João Cristóvão 35d36d95c1 GHC 7.10.1 support
Without it, the following error occurs:

```
src/PgStructure.hs:88:5:
    Non type-variable argument
      in the constraint: Data.String.Conversions.ConvertibleStrings
                           Text k
    (Use FlexibleContexts to permit this)
    When checking that ‘addFK’ has the inferred type
      addFK :: forall k.
               (Ord k, Data.String.Conversions.ConvertibleStrings Text k) =>
               Map.Map k ForeignKey -> Column -> Column
    In an equation for ‘columns’:
        columns table
          = do { cols <- H.listEx
                         $ (\ _1 _2
                              -> Hasql.Backend.Stmt
                                   "select info.table_schema as schema, info.table_name as table_name, info.column_name as name, info.ordinal_position as position, info.is_nullable as nullable, info.data_type as col_type, info.is_updatable as updatable, info.character_maximum_length as max_len, info.numeric_precision as precision, info.column_default as default_value, array_to_string(enum_info.vals, ',') as enum from ( select table_schema, table_name, column_name, ordinal_position, is_nullable, data_type, is_updatable, character_maximum_length, numeric_precision, column_default, udt_name from information_schema.columns where table_schema = ? and table_name = ? ) as info left outer join ( select n.nspname as s, t.typname as n, array_agg(e.enumlabel ORDER BY e.enumsortorder) as vals from pg_type t join pg_enum e on t.oid = e.enumtypid join pg_catalog.pg_namespace n ON n.oid = t.typnamespace group by s, n ) as enum_info on (info.udt_name = enum_info.n) order by position"
                                   (GHC.ST.runST (do { ... }))
                                   True)
                             (qtSchema table) (qtName table);
                 fks <- foreignKeys table;
                 return $ map (addFK fks . columnFromRow) cols }
          where
              addFK fks col = col {colFK = Map.lookup (cs . colName $ col) fks}
```

Also, the regex-tdfa-text library needs a similar patch, but I didn't have time to contact the author yet.

Cheers
2015-04-24 16:22:43 +01:00
Joe Nelson 13cda09c7e Contributing 2015-04-22 17:22:39 -07:00
Joe Nelson 5688030104 Allow posting JSON array and object
Fixes #168

Fixes #156
2015-04-19 17:46:11 -07:00
Joe Nelson 02228b76cc Bump version 2015-04-17 16:26:27 -07:00
Joe Nelson 4d7cc3d67e Merge branch 'bulk-insert' 2015-04-17 15:55:42 -07:00
Joe Nelson ad8700e996 bulk insert in changelog 2015-04-17 15:52:57 -07:00
Joe Nelson fbc90bdb84 Tests for csv bulk import
Fixes #17
2015-04-17 15:42:58 -07:00
Joe Nelson f4c49f03f4 Allow NULL in csv field and unquote the multipart headers 2015-04-16 12:43:22 -07:00
Joe Nelson a87f13f9bb Fix original tests 2015-04-16 10:45:24 -07:00
Joe Nelson 7d03a71fed All tests but one are passing 2015-04-12 19:56:33 -07:00
Joe Nelson 70d33445db WIP: nice but doomed approach to making multipart response 2015-04-12 13:03:16 -07:00
Joe Nelson 4dbcf45555 WIP: typechecking but not yet sending back links
Rather amazing how well this actually works given
it only type checked
2015-04-11 21:41:32 -07:00
Joe Nelson 87acee924e WIP: parsing csv 2015-04-05 17:13:16 -07:00
Joe Nelson a22cf82688 Build sql for inserting multiple rows 2015-04-04 20:20:13 -07:00
Joe Nelson 532cfdff95 Merge pull request #183 from begriffs/jsonb
Filter results using properties in a jsonb column
2015-04-04 18:35:40 -07:00
Joe Nelson 5807b41997 Add jsonb to changelog 2015-04-04 18:33:08 -07:00
Joe Nelson c78d323989 Specify hasql versions exactly to help ci 2015-04-04 17:43:14 -07:00
Joe Nelson 9792b9b46a Allow filtering by values inside json columns 2015-04-04 17:38:54 -07:00
Joe Nelson ca1c524ede WIP: working on querying and ordering with jsonb paths
Affects #118
2015-03-30 00:51:52 -07:00
Joe Nelson a1822a8e08 Update whitespace 2015-03-29 12:32:14 -07:00
Joe Nelson 066cdbc697 Merge pull request #176 from brikou/patch-2
Add link to GUI demo
2015-03-23 18:35:02 -07:00
Brikou CARRE b9c3902bd8 Add link to GUI demo
You can read more about using ng-admin with postgREST here http://marmelab.com/blog/2015/03/23/using-ng-admin-with-postgrest.html
2015-03-23 15:54:34 +01:00
Joe Nelson 03613e2f8e Add link to tutorial
Thanks @prio
2015-03-22 20:27:54 -07:00
Joe Nelson de7eecb166 Document fix from pull request 2015-03-16 21:11:57 -07:00
Joe Nelson 417d98d7fa Merge pull request #172 from GaloisInc/master
Show long help output with defaults on usage errors
2015-03-16 21:09:57 -07:00
Jonathan Daugherty 6dfbe854a0 Make postgrest show long help output with defaults on usage errors
This makes optparse-applicative show the long version of help output.
Its default behavior is not to do this since the showHelpOnError
setting defaults to False.  This turns it on.
2015-03-16 13:27:22 -07:00
Joe Nelson a9bc119b82 Merge branch 'insert-nulls'
Fixes #166
2015-03-15 18:30:47 -07:00
Joe Nelson 42d3d0de6c Fix location header for inserted objects with nulls 2015-03-15 18:26:32 -07:00
Joe Nelson ddb5ba8b64 Can now post nulls, but header link is wrong
affects #166
2015-03-15 16:30:42 -07:00
Joe Nelson 185d5b1c62 Merge branch 'jcristovao-isnull' 2015-03-15 15:22:28 -07:00
Joe Nelson f7ff08edf7 Test is.null matcher 2015-03-15 15:20:31 -07:00
João CristóvãoandJoe Nelson f4e0c12cba IS/IS NOT null, true, false 2015-03-15 13:37:45 -07:00
Joe Nelson 8ed599a769 Merge branch 'jcristovao-nullsFirst' 2015-03-15 12:30:34 -07:00
Joe Nelson aeb62e75bd Add nullsfirst to changelog 2015-03-15 12:30:17 -07:00
Joe Nelson 5c7ee1effc Fix problems caused by my rebase 2015-03-15 12:24:30 -07:00
João CristóvãoandJoe Nelson 01c67ab793 Added tests for nullsfirst / nullslast 2015-03-15 11:37:06 -07:00
João CristóvãoandJoe Nelson 97c9bfe93f Added support for nulls first / nulls last 2015-03-15 10:28:21 -07:00
Joe Nelson 6e6769ea16 Bump version 2015-03-03 23:29:22 -08:00
Joe Nelson 1e417aef90 Note schema override in changelog 2015-03-03 23:20:13 -08:00
Joe Nelson 60eb826cb0 Merge branch 'v1-schema-override'
Affects #155
Affects #158
Fixes #117
2015-03-03 23:18:59 -08:00
Joe Nelson 3d3c7277c7 Allow user to override schema used for v1 of api 2015-03-03 22:29:37 -08:00
Joe Nelson 5be0b5d5ae Return inserted object from POST when Prefer: return=representation
Fixes #27
Fixes #159
2015-03-01 21:42:05 -08:00
Joe Nelson 11e5918690 Merge branch 'select-in'
Fixes #98
Affects #158
2015-03-01 19:18:21 -08:00
Joe Nelson c207e7ee30 Add IN filter to changelog 2015-03-01 18:38:36 -08:00
Joe Nelson 6474a83221 Support IN query param operator 2015-03-01 18:30:08 -08:00
Joe Nelson 9b15071f23 Log requests
Fixes #141
2015-02-28 09:31:00 -08:00
Joe Nelson 51b379d78b Clarify the --secure command line option 2015-02-19 21:28:07 -08:00
Joe Nelson cac5daca01 Merge pull request #154 from brikou/patch-1
Fix URL to video
2015-02-19 10:30:20 -08:00
Brikou CARRE 4bc66c4ca3 Fix URL to video 2015-02-19 10:04:45 +01:00
Joe Nelson eafb1a848f Bump minor version, add changelog 2015-02-18 11:39:13 -08:00
Joe Nelson 328f27e7bd Merge pull request #152 from brikou/expose_location_header
Add 'Location' to exposed headers
2015-02-18 08:41:00 -08:00
Brikou Carré a93e6c050d Add 'Location' to exposed headers 2015-02-18 10:22:24 +01:00
Joe Nelson 337b49f386 Expose Content-Range response header (and others) in CORS
Fixes #148
2015-02-16 19:35:25 -08:00
Joe Nelson 92daa7d11a Merge branch 'like'
Fixes #132
2015-02-15 17:37:40 -08:00
Joe Nelson fa48c86195 Logically simplify (i)like test cases 2015-02-15 17:34:26 -08:00
Joe Nelson b88192f95a Remove lint 2015-02-15 16:31:53 -08:00
Joe Nelson 5a1ae934b9 Put array open bracket nearer to the json 2015-02-15 16:14:25 -08:00
Joe Nelson 58f18181c2 Update order by params to new style introduced from master 2015-02-15 16:13:06 -08:00
Joe Nelson eea1bc0cac Quasiquote json to fix vim syntax highlighting 2015-02-15 16:03:31 -08:00
Joe Nelson e37d5d9b25 Style tweak 2015-02-15 15:59:49 -08:00
Adam C. BakerandJoe Nelson 5cc77709ba Add like/ilike 2015-02-15 15:23:18 -08:00
Joe Nelson 5280b9fd6d Merge pull request #138 from jcristovao/patch-1
order does not work
2015-02-12 08:41:40 -08:00
João Cristóvão 9cb1ced010 Change order test to match docs 2015-02-12 09:44:49 +00:00
João Cristóvão 0a3057e92d Update PgQuery.hs
I'm a bit baffled nobody noticed this before :P

Anyhow, without it order does not work.

Thanks,
Cheers
2015-02-11 19:49:31 +00:00
Joe Nelson 699d6d78dc Bump patch version 2015-02-07 15:27:56 -08:00
Joe Nelson 942ba52f56 Merge branch 'update-deps' 2015-02-07 14:48:26 -08:00
Joe Nelson d77b6df851 Use new optparse-applicative and bcrypt
Fixes #131 and affects #129
2015-02-07 14:21:40 -08:00
Joe Nelson 3662c1d428 Lint and check for restrictive package locks on ci 2015-02-07 13:48:04 -08:00
Joe Nelson e909ef3e62 Mention all modules in .cabal file
per #129
2015-02-04 14:50:33 -08:00
Joe Nelson 9a393c8603 Bump patch version 2015-01-31 12:48:02 -08:00
Joe Nelson ea429e077a Merge pull request #128 from begriffs/hasql-7
Upgrade to Hasql 0.7
2015-01-31 12:45:45 -08:00
Joe Nelson 90f9b00e5e Include JSON content type for errors 2015-01-31 12:33:44 -08:00
Joe Nelson f06f3e3394 Isolate tests
Credentials from AuthSpec were interfering with StructureSpec
2015-01-31 11:32:08 -08:00
Joe Nelson 88af46fbe3 Reduce test settings duplication
and better variable name
2015-01-31 11:31:11 -08:00
Joe Nelson 27450fae66 Fix NULL input problem
Relates to #127

Still auth problems though
2015-01-28 23:58:17 -08:00
Joe Nelson da6318f5a2 Fix warnings, lint, and ambiguous hasql imports 2015-01-28 20:37:19 -08:00
Adam C. Baker 3151aa2ebc fix table parsing 2015-01-26 18:42:46 -08:00
Adam C. Baker e1ad5cb1ae handle errors in the app too 2015-01-26 18:07:38 -08:00
Adam C. Baker ead346816b reporting errors, but not passing tests. 2015-01-26 17:31:59 -08:00
Joe Nelson b504473790 WIP: Upgrade to hasql 7
Still fails handling query errors
2015-01-25 18:33:50 -08:00
Joe Nelson 8073c84809 More informative cabal file for hackage release 2015-01-17 11:08:46 -08:00
Joe Nelson d28dee41a1 Heroku deployment button on README 2015-01-17 10:46:58 -08:00
Joe Nelson 003685ff46 Add support for DELETE verb
Fixes #125
2015-01-17 00:48:49 -08:00
Joe Nelson 61f8f41a36 Temporarily remove heroku button while I get it working properly 2015-01-07 23:45:58 -08:00
Joe Nelson 55f6318dcd Deploy button and thanks 2015-01-07 23:09:10 -08:00
Joe Nelson 96d108d724 Merge pull request #121 from begriffs/fix-order-by
Do not add WHERE clause if only param is order
2015-01-05 23:07:29 -08:00
Joe Nelson d77924a9d1 Update links to binaries 2015-01-05 23:03:51 -08:00
Joe Nelson 4c74e54ae1 Do not add WHERE clause if only param is order
Fixes #119
2015-01-05 22:37:49 -08:00
Joe Nelson 591f0eb78d Video link 2014-12-30 14:52:58 -08:00
Adam C. Baker c9b960f955 only create schema once 2014-12-29 17:57:31 -08:00
Adam C. Baker 23a75aefcc bump hspec version.
use new version for before_
2014-12-29 17:57:31 -08:00
Adam C. Baker 3756309b22 create items in test, not in the schema. 2014-12-29 17:57:31 -08:00
Adam C. Baker 2ae99daa12 resetDb once for each set of tests. 2014-12-29 17:57:31 -08:00
Joe Nelson 796de39762 More prominent demo server link 2014-12-29 16:12:17 -08:00
Joe Nelson c541f83cef Link to demo server and its schema 2014-12-29 15:27:29 -08:00
Joe Nelson a142128915 Fix logo typo 2014-12-29 15:07:09 -08:00
Joe Nelson ee8f754d7a Consolidate guides to make them easier to spot 2014-12-29 11:45:43 -08:00
Joe Nelson 3d02abc844 Link to binaries 2014-12-29 11:18:09 -08:00
Joe Nelson a049a8d5cd Update performance stats 2014-12-29 10:31:19 -08:00
Joe Nelson 2c635cfde8 Optimization when auth role coincides with anon role
No need to set/reset role because it is already correct
2014-12-29 09:39:13 -08:00
Joe Nelson 24f70f5bdf Add @casalaina's awesome logo 2014-12-29 08:58:35 -08:00
Joe Nelson d7d5473664 Link to load test graph 2014-12-28 21:13:58 -08:00
Joe Nelson d7e0fb948e Tweak future features 2014-12-28 11:11:33 -08:00
Joe Nelson 8200f54ae4 Bump version 2014-12-25 17:47:23 -08:00
Joe Nelson 5eca8668ef Better docs 2014-12-25 14:28:06 -08:00
Joe Nelson 30e9733719 Hide db details on connection failure 2014-12-22 15:40:35 -08:00
Joe Nelson 0830772384 Provide better diagnostics for exception parse failure 2014-12-22 15:40:20 -08:00
Joe Nelson 2bf45d4f02 Bump patch version 2014-12-21 19:58:08 -08:00
Joe Nelson f689ce684a Provide proper JSON details for errors
Refines HTTP codes for server vs client problems

Fixes #92
Fixes #40
2014-12-21 19:34:21 -08:00
Joe Nelson 07b039370d Postgres error message parser 2014-12-17 14:15:12 -08:00
Joe Nelson dbe1f59066 Update project name in strings
Fixes #105
2014-12-17 10:24:24 -08:00
Joe Nelson 5f4212e2fd Link to CI report from badge 2014-12-16 20:48:17 -08:00
Joe Nelson 9712549d64 Use "::unknown" type casts to overcome binary protocol restrictions
Fixes #112
2014-12-16 20:09:01 -08:00
Joe Nelson 19b62c00ee Enclose entire app response in single Tx 2014-12-12 13:55:09 -08:00
Joe Nelson d763e27bb2 More readable conditional 2014-12-07 21:32:36 -08:00
Joe Nelson 4f9132af43 Adjust tests to use new-style app config 2014-12-07 18:54:27 -08:00
Adam C. Baker 638cd3d49d update run example for new CLI 2014-12-07 18:51:51 -08:00
Joe Nelson b302843e10 Parse args more sensibly, and use correct port for postgres connection 2014-12-07 18:42:18 -08:00
Joe Nelson 1fd757c2ad Remove coerce dependency for ghc 7.6 2014-12-07 14:46:33 -08:00
Joe Nelson 782d6520a2 Upgrade hasql to 0.4.0 2014-12-07 00:11:14 -08:00
Joe Nelson 4d8c508a17 Parse db uri again 2014-12-06 23:21:49 -08:00
Joe Nelson beb61d8a05 Pend a test until complication is resolved with #107 2014-12-06 18:46:34 -08:00
Joe Nelson c0e4d64423 Merge branch 'logisch' 2014-12-06 17:43:04 -08:00
Joe Nelson 605372f829 Set permissions correctly for structure spec 2014-12-06 17:42:42 -08:00
Joe Nelson 7d58097449 Be sure to reset db before ALL tests 2014-12-06 17:42:42 -08:00
Joe Nelson a45718e992 Re-enable authentication 2014-12-06 17:42:22 -08:00
Joe Nelson 13bc9a4510 Lock hasql version for now 2014-12-06 17:42:22 -08:00
Joe Nelson fb6e297e88 Do not include .0 on integers in generated links 2014-12-06 17:42:22 -08:00
Joe Nelson 5900475460 Reset db between tests rather than using transactions to accomodate Hasql limitation 2014-12-06 17:42:21 -08:00
Joe Nelson 4eb4d8da1e Removed test for RFC compliance
Who would send Content-Range anyway, that is weird in this context
2014-12-06 17:42:21 -08:00
Joe Nelson f1ecbec543 Do not quote params in Location header
Also treat the enum field in OPTIONS as an array uniformly
2014-12-06 17:42:21 -08:00
Joe Nelson 42faaa97c9 Include all fields in Location header when no primary keys defined 2014-12-06 17:42:21 -08:00
Joe Nelson e3d3a07d14 Fun, fun, offbyone 2014-12-06 17:42:21 -08:00
Joe Nelson ca4138df2a Send back Location header on insert 2014-12-06 17:42:21 -08:00
Joe Nelson 0ebb83de3a Include cors middleware in test 2014-12-06 17:42:21 -08:00
Joe Nelson dc43bcbca9 Silence the database fixture output from stdout 2014-12-06 17:42:21 -08:00
Joe Nelson 8e9bbe6dcb Fix insert 2014-12-06 17:42:20 -08:00
Joe Nelson eb7e9586ab Handle missing table sql errors with 404 2014-12-06 17:42:20 -08:00
Joe Nelson 21b0b02c03 Temporarily use psql to load schemas 2014-12-06 17:42:20 -08:00
Joe Nelson dba17b895e Bye Travis, hi Circle 2014-12-06 17:42:20 -08:00
Joe Nelson d6e3526bff Use Text for textual data
Also upgrade Hasql
2014-12-06 17:42:20 -08:00
Joe Nelson f6b7b42d75 Feature specs compile 2014-12-06 17:42:20 -08:00
Joe Nelson da79f6cea1 Share connection pool with all http clients 2014-12-06 17:42:20 -08:00
Joe Nelson d2888c62f1 hasql wants to rowParse text not bytestring 2014-12-06 17:42:19 -08:00
Joe Nelson dcbecf085f Program compiles but without auth, uri parsing, or json error reporting 2014-12-06 17:42:19 -08:00
Joe Nelson ae9fe506be App.hs typechecks 2014-12-06 17:42:19 -08:00
Joe Nelson 70d9641e35 WIP: More files converted to Hasql 2014-12-06 17:42:19 -08:00
Joe Nelson 8ebfbccd08 WIP: converting to Hasql 2014-12-06 17:42:19 -08:00
Joe Nelson b74b91b1c0 Interpolate json string values in queries without extra quoting 2014-12-06 17:42:19 -08:00
Joe Nelson dd2adf4daa Tests compile, run, and fail
Temporarily disabled unit tests
2014-12-06 17:42:19 -08:00
Joe Nelson d791f3939e The Dbapi module is now App 2014-12-06 17:42:18 -08:00
Joe Nelson d9377fa50a Fix select star and json stuff 2014-12-06 17:42:18 -08:00
Joe Nelson f7e0386d1c Fix empty results 2014-12-06 17:42:18 -08:00
Joe Nelson 1b8f0f2829 Main compiles. Still bugs though 2014-12-06 17:42:18 -08:00
Joe Nelson 0f849e9bf1 Move cors things to Main 2014-12-06 17:42:18 -08:00
Joe Nelson f21559ea81 Clean up App.hs 2014-12-06 17:42:18 -08:00
Joe Nelson 61174cfa17 Add user creation route 2014-12-06 17:42:18 -08:00
Joe Nelson 8c073cedc6 Add PATCH handler 2014-12-06 17:42:17 -08:00
Joe Nelson 361cffc283 Add PUT handler 2014-12-06 17:42:17 -08:00
Joe Nelson 8894400f5c Add POST handler 2014-12-06 17:42:17 -08:00
Joe Nelson b5f054d976 WIP: GET route 2014-12-06 17:42:17 -08:00
Joe Nelson cb80dba234 WIP: converting app request handlers 2014-12-06 17:42:17 -08:00
Joe Nelson 9b7af0296e Fix auth functions 2014-12-06 17:42:17 -08:00
Joe Nelson 9d363da8f9 Consolidate auth functions 2014-12-06 17:42:17 -08:00
Adam C. BakerandJoe Nelson 41a982fb31 Remove HDBC imports 2014-12-06 17:42:17 -08:00
Joe Nelson 7196fbc001 WIP: use postgresql-simple in pgstructure 2014-12-06 17:42:16 -08:00
Joe Nelson 2f91fdd8b5 WIP: more clean query functions 2014-12-06 17:42:16 -08:00
Joe Nelson cb467b62c1 WIP: cleaner PgQuery functions 2014-12-06 17:42:16 -08:00
Joe Nelson 94ab57941d Thank you! 2014-12-05 16:27:00 -08:00
Joe Nelson bed759333d Name changes in readme 2014-12-05 16:05:02 -08:00
Adam C. Baker 2b76c87d1d trivial test change to illustrate alternative to pick 2014-11-11 21:29:40 -08:00
Joe Nelson 7bbeeb7c61 Merge branch 'patch' 2014-11-06 23:53:50 -08:00
Joe Nelson 58adb5e828 Allow patch requests
Fixes #87
2014-11-06 23:49:29 -08:00
Joe Nelson 76ea92bfce Derp, actually put the data on a put request 2014-11-06 18:31:13 -08:00
Joe Nelson ffa2870061 Merge branch 'enforce-new-warp' 2014-11-06 15:46:40 -08:00
Joe Nelson 742ff027ba Require warp >= 3.0.2 for setServerName
Fixes #94
2014-11-06 15:35:17 -08:00
Joe Nelson a29f2dd107 Postgres on travis does not recognize lock_timeout 2014-11-06 14:48:15 -08:00
Joe Nelson 648c770cb4 Merge branch 'selective-transactions' 2014-11-06 14:22:23 -08:00
Joe Nelson f1c374403a Remove noisy notifications 2014-11-06 14:19:19 -08:00
Joe Nelson 5cbbe12b65 Use transactions for only patch and put 2014-11-06 14:19:19 -08:00
Joe Nelson f58e3db924 Disable transactions for read-only requests 2014-11-06 14:19:19 -08:00
Joe Nelson 0e57bee90d Merge pull request #93 from begriffs/repackage
declare required extensions
2014-11-06 13:56:47 -08:00
Adam C. Baker 74855db87b declare required extensions
and remove default extension pragmas (OverloadedStrings)
2014-11-05 00:32:10 -08:00
Joe Nelson 8c6b34aaf2 Merge branch 'native-sql-format' 2014-11-03 23:49:21 -08:00
Joe Nelson cc2c65a9ab Build escaped queries in the app itself
universally replace String with Text
2014-11-03 23:30:04 -08:00
Joe Nelson c27f178754 Create functions that mimic Postgres' format() command 2014-11-03 19:53:54 -08:00
Joe Nelson 36f7ac3ff4 Merge branch 'speed' 2014-11-02 00:44:57 -07:00
Joe Nelson 822c9aad22 Cannot use query param for set role. Concat should be safe though 2014-11-02 00:23:24 -07:00
Joe Nelson 1a83f2d344 Stop using strings, they are dog slow 2014-11-02 00:10:37 -07:00
Joe Nelson 28da88829e Merge pull request #91 from begriffs/s3
Easy builds for Heroku
2014-10-28 20:09:32 -07:00
Joe Nelson 5453e1988c Document heroku build 2014-10-28 20:05:15 -07:00
Joe Nelson b09ca76333 Add optimization and s3 release script 2014-10-28 19:44:50 -07:00
Joe Nelson 12c1f0fa23 Merge pull request #90 from begriffs/server-header
Report server and version in headers
2014-10-28 17:30:57 -07:00
Joe Nelson 6a5b756cac Report server and version in headers 2014-10-28 17:17:37 -07:00
Joe Nelson aaf8a7fbfa Merge pull request #89 from begriffs/release
cleanup connections on close.
2014-10-28 15:26:27 -07:00
Adam C. Baker 2b829f9152 cleanup connections on close. 2014-10-28 14:41:32 -07:00
Joe Nelson f84ca1d615 Merge branch 'pool-opts'
Fixes #85
2014-10-21 16:21:01 -07:00
Joe Nelson 32e3396caf Provide command-line flag for db pool size 2014-10-21 16:15:11 -07:00
Joe Nelson 563187f3c0 Merge pull request #86 from begriffs/user-management
Route for adding a new user
2014-10-21 15:37:44 -07:00
Joe Nelson 05bf6b9b06 Standarize db schema dump format 2014-10-21 15:16:03 -07:00
Joe Nelson 572c235b79 Add /dbapi/users route for creating new user 2014-10-21 15:01:04 -07:00
76 changed files with 5324 additions and 1587 deletions
+4
View File
@@ -1,3 +1,4 @@
.DS_Store
db
dist
.cabal-sandbox
@@ -5,3 +6,6 @@ cabal.sandbox.config
hscope.out
codex.tags
.anvil
.stack-work
tags
site
-22
View File
@@ -1,22 +0,0 @@
language: haskell
ghc: 7.8
addons:
postgresql: "9.3"
notifications:
slack: looprecur:z2nPS2Cfx2D33LE2obyU2FyQ
before_install:
- createuser --superuser --no-password dbapi_test
- createdb -O dbapi_test -U postgres dbapi_test
- travis_retry sudo add-apt-repository -y ppa:hvr/ghc
- travis_retry sudo apt-get update
- travis_retry sudo apt-get install --force-yes happy-1.19.3 alex-3.1.3
- export PATH=/opt/alex/3.1.3/bin:/opt/happy/1.19.3/bin:$PATH
install:
- travis_retry curl http://bin.begriffs.com/dbapi/cabal-sandbox.tar.xz | tar xJ
- chmod a+x .cabal-sandbox/bin/*
- cabal sandbox init
- cabal install --enable-test --dependencies-only
- cabal install --enable-test
script:
- cabal test --show-details=always --test-options="--color"
- .cabal-sandbox/bin/hlint src/*.hs test/**/*.hs
+86
View File
@@ -0,0 +1,86 @@
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [0.2.12.0] - 2015-10-25
### Added
- Embed associations, e.g. `/film?select=*,director(*)` - @ruslantalpa
- Filter columns, e.g. `?select=col1,col2` - @ruslantalpa
- Does not execute the count total if header "Prefer: count=none" - @diogob
### Fixed
- Tolerate a missing role in user creation - @calebmer
- Avoid unnecessary text re-encoding - @ruslantalpa
## [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
### Added
- Return range headers in PATCH
- Return PATCHed resources if header "Prefer: return=representation"
- Allow nested objects and arrays in JSON post for jsonb columns
- JSON Web Tokens - [Federico Rampazzo](https://github.com/framp)
- Expose PostgREST as a Haskell package
### Fixed
- Return 404 if no records updated by PATCH
## [0.2.8.0] - 2015-04-17
### Added
- Option to specify nulls first or last, eg `/people?order=age.desc.nullsfirst`
- Filter nulls, `?col=is.null` and `?col=isnot.null`
- Filter within jsonb, `?col->a->>b=eq.c`
- Accept CSV in post body for bulk inserts
### Fixed
- Allow NULL values in posts
- Show full command line usage on param errors
## [0.2.7.0] - 2015-03-03
### Added
- Server response logging
- Filter IN values, e.g. `?col=in.1,2,3`
- Return POSTed resource if header "Prefer: return=representation"
- Allow override of default (v1) schema
## [0.2.6.0] - 2015-02-18
### Added
- A changelog
- Filter by substring match, e.g. `?col=like.*hello*` (or ilike for
case insensitivity).
- Access-Control-Expose-Headers for CORS
### Fixed
- Make filter position match docs, e.g. `?order=col.asc` rather
than `?order=asc.col`.
+55
View File
@@ -0,0 +1,55 @@
# Contributing to PostgREST
**First:** if you're unsure or afraid of _anything_, just ask or
submit the issue or pull request anyways. You won't be yelled at
for giving your best effort. The worst that can happen is that
you'll be politely asked to change something. We appreciate any
sort of contributions, and don't want a wall of rules to get in the
way of that.
However, for those individuals who want a bit more guidance on the
best way to contribute to the project, read on. This document will
cover what we're looking for. By addressing all the points we're
looking for, it raises the chances we can quickly merge or address
your contributions.
## Issues
### Reporting an Issue
* Make sure you test against the latest released version. It is possible
we already fixed the bug you're experiencing.
* Also check the `CHANGELOG.md` to see if any unreleased changes affect
the issue. The very newest changes can take a little while to be released
as a new official version.
* Provide steps to reproduce the issue, including your OS version and
the specific database schema that you are using.
## Code
### Haskell Conventions
* All contributions must pass the tests before being merged. When
you create a pull request your code will automatically be tested.
* All code must also pass [hlint](http://community.haskell.org/~ndm/hlint/)
with no warnings. This helps enforce a uniform style for all
committers. Continuous integration will check this as well on every
pull request.
* For help building the Haskell code on your computer check out the [building from
source](https://github.com/begriffs/postgrest/wiki/Building-from-source)
wiki page.
## Maintenance
### Schedule
Currently I (@begriffs) am the sole maintainer, and while I am
overjoyed to help resolve issues I also have to balance this with
my other obligations. If you don't get a response right away
don't worry, I will definitely get to it. Also you can join the
Gitter [chat room](https://gitter.im/begriffs/postgrest) to
discuss issues you are having.
+164 -30
View File
@@ -1,43 +1,177 @@
## Serve a RESTful API from any Postgres database
![Logo](static/logo.png "Logo")
[![Build Status](https://travis-ci.org/begriffs/dbapi.svg?branch=master)](https://travis-ci.org/begriffs/dbapi)
[![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">
<img src="https://img.shields.io/badge/%E2%86%91_Deploy_to-Heroku-7056bf.svg" alt="Deploy">
</a>
[![Join the chat at https://gitter.im/begriffs/postgrest](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/begriffs/postgrest)
### Installation
PostgREST serves a fully RESTful API from any existing PostgreSQL
database. It provides a cleaner, more standards-compliant, faster
API than you are likely to write from scratch.
```sh
brew install postgres
cabal install -j --enable-tests
### Demo [postgrest.herokuapp.com](https://postgrest.herokuapp.com) | Watch [Video](http://begriffs.com/posts/2014-12-30-intro-to-postgrest.html) | [GUI Demo](http://marmelab.com/ng-admin-postgrest)
Try making requests to the live demo server with an HTTP client
such as [postman](http://www.getpostman.com/). The structure of the
demo database is defined by
[begriffs/postgrest-example](https://github.com/begriffs/postgrest-example).
You can use it as inspiration for test-driven server migrations in
your own projects.
### Usage
Download the binary ([latest release](https://github.com/begriffs/postgrest/releases/latest)) and invoke like so:
```bash
postgrest --db-host localhost --db-port 5432 \
--db-name my_db --db-user postgres \
--db-pass foobar --db-pool 200 \
--anonymous postgres --port 3000 \
--v1schema public
```
Example usage:
In production include the `--secure` option which redirects all
requests to HTTPS. Note that PostgREST does not handle the SSL
internally and must be put behind another server that does (such
as nginx or the Heroku load balancer).
```sh
cabal run -p 3000 -d postgres://[auth-role]:@localhost:5432/[database] -a [anonymous-role]
```
### Performance
You will need to provide two database roles (which are allowed to
be the same). One is called the authenticator role (`auth-role`
above) which should have enough privileges to read the `auth` table
in the `dbapi` schema if you intend to support multi-user applications.
TLDR; subsecond response times for up to 2000 requests/sec on Heroku free tier. ([see the load test](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling))
The other role is for anonymous access (`anonymous-role` above).
Immediately upon acceping any unauthenticated HTTP connection dbapi
assumes this role in its queries to postgres. Give this role as
much or little permissions as you would like.
If you're used to servers written in interpreted languages (or named
after precious gems), prepare to be pleasantly surprised by PostgREST
performance.
### Running tests
Three factors contribute to the speed. First the server is written
in [Haskell](https://new-www.haskell.org/) using the
[Warp](http://www.yesodweb.com/blog/2011/03/preliminary-warp-cross-language-benchmarks)
HTTP server (aka a compiled language with lightweight threads).
Next it delegates as much calculation as possible to the database
including
```sh
createuser --superuser --no-password dbapi_test
createdb -O dbapi_test -U postgres dbapi_test
* Serializing JSON responses directly in SQL
* Data validation
* Authorization
* Combined row counting and retrieval
* Data post in single command (`returning *`)
cabal test --show-details=always --test-options="--color"
```
Finally it uses the database efficiently with the
[Hasql](https://nikita-volkov.github.io/hasql-benchmarks/) library
by
### Accessing server on localhost
* Reusing prepared statements
* Keeping a pool of db connections
* Using the Postgres binary protocol
* Being stateless to allow horizontal scaling
Dbapi permits only secure https access. By default it uses a
self-signed key which will cause Chrome and other rest clients to
complain. You will need to configure your system to trust this key.
On mac follow [these
instructions](http://www.robpeck.com/2010/10/google-chrome-mac-os-x-and-self-signed-ssl-certificates/).
Ultimately the server (when load balanced) is constrained by database
performance. This may make it inappropriate for very large traffic
load. To learn more about scaling with Heroku and Amazon RDS see
the [performance guide](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling).
Other optimizations are possible, and some are outlined in the
[Future Features](#future-features).
### Security
PostgREST handles authentication (HTTP Basic over SSL or [JSON Web
Tokens](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions#json-web-tokens))
and delegates authorization to the role information defined in the
database. This ensures there is a single declarative source of truth
for security. When dealing with the database the server assumes
the identity of the currently authenticated user, and for the
duration of the connection cannot do anything the user themselves
couldn't.
Postgres 9.5 will soon support true [row-level
security](http://michael.otacoo.com/postgresql-2/postgres-9-5-feature-highlight-row-level-security/).
In the meantime what isn't yet implemented can be simulated with
triggers and security-barrier views. Because the possible queries
to the database are limited to certain templates using
[leakproof](http://blog.2ndquadrant.com/how-do-postgresql-security_barrier-views-work/)
functions, the trigger workaround does not compromise row-level
security.
For example security patterns see the [security
guide](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions).
### Versioning
A robust long-lived API needs the freedom to exist in multiple
versions. PostgREST supports versioning through HTTP content
negotiation. Requests for a certain version translate into switching
which database schema to search for tables. PostgreSQL schema search
paths allow tables from earlier versions to be reused verbatim in
later versions.
To learn more, see the [guide to versioning](https://github.com/begriffs/postgrest/wiki/API-Versioning).
### Self-documention
Rather than writing and maintaining separate docs yourself let the
API explain its own affordances using HTTP. All PostgREST endpoints
respond to the OPTIONS verb and explain what they support as well
as the data format of their JSON payload.
The number of rows returned by an endpoint is reported by - and
limited with - range headers. More about
[that](http://begriffs.com/posts/2014-03-06-beyond-http-header-links.html).
There are more opportunities for self-documentation listed in [Future
Features](#future-features).
### Data Integrity
Rather than relying on an Object Relational Mapper and custom
imperative coding, this system requires you put declarative constraints
directly into your database. Hence no application can corrupt your
data (including your API server).
The PostgREST exposes HTTP interface with safeguards to prevent
surprises, such as enforcing idempotent PUT requests, and
See examples of [Postgres
constraints](http://www.tutorialspoint.com/postgresql/postgresql_constraints.htm)
and the [guide to routing](https://github.com/begriffs/postgrest/wiki/Routing).
### Future Features
* Watching endpoint changes with sockets and Postgres pubsub
* Specifying per-view HTTP caching
* Inferring good default caching policies from the Postgres stats collector
* Generating mock data for test clients
* Maintaining separate connection pools per role to avoid "set/reset
role" performance penalty
* Describe more relationships with Link headers
* Depending on accept headers, render OPTIONS as [RAML](http://raml.org/) or a
relational diagram
* Add two-legged auth with OAuth 1.0a(?)
* ... the other [issues](https://github.com/begriffs/postgrest/issues)
### Guides
* [Routing](https://github.com/begriffs/postgrest/wiki/Routing)
* [Versioning](https://github.com/begriffs/postgrest/wiki/API-Versioning)
* [Performance](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling)
* [Security](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions)
* [Tutorial](http://blog.jonharrington.org/postgrest-introduction/) (external)
* [Heroku](https://github.com/begriffs/postgrest/wiki/Heroku)
### Thanks
* [Ruslan Talpa](https://github.com/ruslantalpa) for rewriting the
route parsing and query generation code to support resource embedding
* [Adam Baker](https://github.com/adambaker) for code
contributions and many fundamental design discussions
* [Diogo Biazus](https://github.com/diogob) for many improvements
and deep postgresql knowledge
* [Nikita Volkov](https://github.com/nikita-volkov) for writing the
wonderful [Hasql](https://github.com/nikita-volkov/hasql) library
and helping me use it
* [Mikey Casalaina](https://github.com/casalaina) for the cool logo
* [Jonathan Harrington](https://github.com/prio) for writing a [nice
tutorial](http://blog.jonharrington.org/postgrest-introduction/)
* [Federico Rampazzo](https://github.com/framp) for suggesting and
implementing [JWT](http://jwt.io/) support
+56
View File
@@ -0,0 +1,56 @@
{
"name": "PostgREST",
"description": "RESTful API for any PostgreSQL database.",
"logo": "https://halcyon.sh/logo.svg",
"repository": "https://github.com/begriffs/postgrest",
"env": {
"BUILDPACK_URL": {
"description": "Heroku buildpack for deploying Haskell applications",
"value": "https://github.com/begriffs/postgrest-heroku"
},
"POSTGREST_VER": {
"description": "Version of PostgREST to deploy",
"value": "0.2.12.0"
},
"DB_NAME": {
"description": "Database name",
"required": true
},
"AUTH_ROLE": {
"description": "Database role to use checking client authentication",
"required": true
},
"AUTH_PASS": {
"description": "Authentication password",
"required": false
},
"ANONYMOUS_ROLE": {
"description": "Database role for non-authenticated requests",
"required": true
},
"DB_HOST": {
"description": "Database server hostname",
"required": true
},
"DB_PORT": {
"description": "Database server port",
"required": false,
"value": "5432"
},
"DB_POOL": {
"description": "Maximum number of connections in database pool",
"required": false,
"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"
}
}
}
+16
View File
@@ -0,0 +1,16 @@
machine:
pre:
- createuser --superuser --no-password postgrest_test
- createdb -O postgrest_test -U ubuntu postgrest_test
ghc:
version: 7.8.3
dependencies:
override:
- cabal update
- cabal sandbox init
- cabal install --upgrade-dependencies --constraint="template-haskell installed" --dependencies-only --enable-tests
- cabal configure --enable-tests -f ci
test:
post:
- cabal exec hlint -- -X QuasiQuotes src/**/*.hs test/**/*.hs
- cabal exec packdeps postgrest.cabal
-69
View File
@@ -1,69 +0,0 @@
name: dbapi
version: 0.2.2.1
synopsis: The database is your api
license: MIT
license-file: LICENSE
author: Joe Nelson, Adam Baker
maintainer: cred+github@begriffs.com
category: Web
build-type: Simple
cabal-version: >=1.10
executable dbapi
main-is: Main.hs
ghc-options: -Wall -W -Werror
default-language: Haskell2010
build-depends: base >=4.6 && <5
, HDBC, HDBC-postgresql
, warp, wai >= 3.0.1 && < 3.0.2
, wai-extra, wai-cors
, wai-middleware-static >= 0.6.0
, HTTP, convertible, http-types
, case-insensitive
, scientific, time
, aeson, network >= 2.6
, bytestring, text, split, string-conversions
, containers, unordered-containers
, optparse-applicative >= 0.9.1 && < 0.10
, regex-base, regex-tdfa
, Ranged-sets
, transformers
, bcrypt, base64-string
, network-uri >= 2.6
, resource-pool, process
Other-Modules: Dbapi
, PgStructure
, PgQuery
, RangeQuery
, Middleware
hs-source-dirs: src
Test-Suite spec
Type: exitcode-stdio-1.0
Default-Language: Haskell2010
Hs-Source-Dirs: test, src
ghc-options: -Wall -W -Werror
Main-Is: Main.hs
Other-Modules: Dbapi, Spec, SpecHelper
Build-Depends: base, hspec2
, hspec-wai >= 0.5.0, hspec-wai-json
, HDBC, HDBC-postgresql
, warp, wai >= 3.0.1 && < 3.0.2
, HTTP, convertible
, case-insensitive
, wai-extra, wai-cors, containers
, wai-middleware-static >= 0.6.0
, http-types, scientific, time
, bytestring, aeson, network >= 2.6
, text, optparse-applicative
, unordered-containers
, regex-base
, string-conversions
, http-media, regex-tdfa
, Ranged-sets
, transformers
, bcrypt
, base64-string
, split
, network-uri >= 2.6
, resource-pool
+52
View File
@@ -0,0 +1,52 @@
# TODO list to build debian "official" package
It feels for free to modify, fix or take some task or all.
## debian/control
* Fill description field
* Add Vcs-Browser
* Add Vcs-Git
* Add Uploaders field
## debian/copyright
* Add more contributers
## Dependencies packages
Some libraries dependencies aren't Debian package. Below is the list was built by [cabal-debian](https://wiki.debian.org/Haskell/CollabMaint/GettingStarted). These libraries are necessary to build Postgrest the right way.
* libghc-base64-string-dev
* libghc-base64-string-prof
* libghc-bcrypt-dev
* libghc-bcrypt-prof
* libghc-hasql-dev
* libghc-hasql-prof
* libghc-hasql-backend-dev
* libghc-hasql-backend-prof
* libghc-hasql-postgres-dev
* libghc-hasql-postgres-prof
* libghc-string-conversions-dev
* libghc-string-conversions-prof
* libghc-wai-cors-dev
* libghc-wai-cors-prof
* libghc-wai-middleware-static-dev
* libghc-wai-middleware-static-prof
* libghc-hasql-dev
* libghc-hasql-backend-dev
* libghc-hasql-postgres-dev
* libghc-heredoc-dev
* libghc-hspec-wai-dev
* libghc-hspec-wai-json-dev
* libghc-http-media-dev
* libghc-packdeps-dev
* libghc-base64-string-doc
* libghc-bcrypt-doc
* libghc-hasql-doc
* libghc-hasql-backend-doc
* libghc-hasql-postgres-doc
* libghc-string-conversions-doc
* libghc-wai-cors-doc
* libghc-wai-middleware-static-doc
+5
View File
@@ -0,0 +1,5 @@
haskell-postgrest (0.2.11.1-1) UNRELEASED; urgency=low
* Initial release
-- Debian Haskell Group <pkg-haskell-maintainers@lists.alioth.debian.org> Wed, 30 Sep 2015 18:52:46 +0000
+1
View File
@@ -0,0 +1 @@
9
+196
View File
@@ -0,0 +1,196 @@
Source: haskell-postgrest
Maintainer: Debian Haskell Group <pkg-haskell-maintainers@lists.alioth.debian.org>
Priority: extra
Section: haskell
Build-Depends: debhelper (>= 9),
haskell-devscripts (>= 0.8),
cdbs,
ghc,
ghc-prof,
libghc-http-dev,
libghc-http-prof,
libghc-missingh-dev,
libghc-missingh-prof,
libghc-ranged-sets-dev,
libghc-ranged-sets-prof,
libghc-aeson-dev,
libghc-aeson-prof,
libghc-base64-string-dev,
libghc-base64-string-prof,
libghc-bcrypt-dev,
libghc-bcrypt-prof,
libghc-blaze-builder-dev,
libghc-blaze-builder-prof,
libghc-case-insensitive-dev,
libghc-case-insensitive-prof,
libghc-cassava-dev,
libghc-cassava-prof,
libghc-convertible-dev,
libghc-convertible-prof,
libghc-hasql-dev,
libghc-hasql-prof,
libghc-hasql-backend-dev,
libghc-hasql-backend-prof,
libghc-hasql-postgres-dev,
libghc-hasql-postgres-prof,
libghc-http-types-dev,
libghc-http-types-prof,
libghc-jwt-dev,
libghc-jwt-prof,
libghc-mtl-dev,
libghc-mtl-prof,
libghc-network-dev,
libghc-network-prof,
libghc-network-uri-dev,
libghc-network-uri-prof,
libghc-optparse-applicative-dev,
libghc-optparse-applicative-prof,
libghc-regex-base-dev,
libghc-regex-base-prof,
libghc-regex-tdfa-dev,
libghc-regex-tdfa-prof,
libghc-resource-pool-dev,
libghc-resource-pool-prof,
libghc-scientific-dev,
libghc-scientific-prof,
libghc-split-dev,
libghc-split-prof,
libghc-string-conversions-dev,
libghc-string-conversions-prof,
libghc-stringsearch-dev,
libghc-stringsearch-prof,
libghc-text-dev,
libghc-text-prof,
libghc-unordered-containers-dev,
libghc-unordered-containers-prof,
libghc-vector-dev,
libghc-vector-prof,
libghc-wai-dev,
libghc-wai-prof,
libghc-wai-cors-dev,
libghc-wai-cors-prof,
libghc-wai-extra-dev,
libghc-wai-extra-prof,
libghc-wai-middleware-static-dev,
libghc-wai-middleware-static-prof,
libghc-warp-dev,
libghc-warp-prof,
libghc-aeson-dev (>= 0.8),
libghc-bcrypt-dev (>= 0.0.6),
libghc-hasql-dev (>= 0.7.3),
libghc-hasql-dev (<< 0.8),
libghc-hasql-backend-dev (>= 0.4.1),
libghc-hasql-backend-dev (<< 0.5),
libghc-hasql-postgres-dev (>= 0.10.4),
libghc-hasql-postgres-dev (<< 0.11),
libghc-network-dev (>= 2.6),
libghc-network-uri-dev (>= 2.6),
libghc-optparse-applicative-dev (>= 0.11),
libghc-optparse-applicative-dev (<< 0.12),
libghc-wai-dev (>= 3.0.1),
libghc-wai-middleware-static-dev (>= 0.6.0),
libghc-warp-dev (>= 3.0.2),
libghc-quickcheck2-dev,
libghc-heredoc-dev,
libghc-hlint-dev,
libghc-hspec-dev (>= 2.1),
libghc-hspec-dev (<< 2.2),
libghc-hspec-wai-dev,
libghc-hspec-wai-json-dev,
libghc-http-media-dev,
libghc-packdeps-dev,
Build-Depends-Indep: ghc-doc,
libghc-http-doc,
libghc-missingh-doc,
libghc-ranged-sets-doc,
libghc-aeson-doc,
libghc-base64-string-doc,
libghc-bcrypt-doc,
libghc-blaze-builder-doc,
libghc-case-insensitive-doc,
libghc-cassava-doc,
libghc-convertible-doc,
libghc-hasql-doc,
libghc-hasql-backend-doc,
libghc-hasql-postgres-doc,
libghc-http-types-doc,
libghc-jwt-doc,
libghc-mtl-doc,
libghc-network-doc,
libghc-network-uri-doc,
libghc-optparse-applicative-doc,
libghc-regex-base-doc,
libghc-regex-tdfa-doc,
libghc-resource-pool-doc,
libghc-scientific-doc,
libghc-split-doc,
libghc-string-conversions-doc,
libghc-stringsearch-doc,
libghc-text-doc,
libghc-unordered-containers-doc,
libghc-vector-doc,
libghc-wai-doc,
libghc-wai-cors-doc,
libghc-wai-extra-doc,
libghc-wai-middleware-static-doc,
libghc-warp-doc,
Standards-Version: 3.9.6
Homepage: https://github.com/begriffs/postgrest
Description: REST API for any Postgres database
Reads the schema of a PostgreSQL database and creates RESTful routes
for the tables and views, supporting all HTTP verbs that security
permits.
Package: libghc-postgrest-dev
Architecture: any
Depends: ${haskell:Depends},
${misc:Depends},
${shlibs:Depends},
Recommends: ${haskell:Recommends},
Suggests: ${haskell:Suggests},
Conflicts: ${haskell:Conflicts},
Provides: ${haskell:Provides},
Description: ${haskell:ShortDescription}${haskell:ShortBlurb}
${haskell:LongDescription}
.
${haskell:Blurb}
Package: libghc-postgrest-prof
Architecture: any
Depends: ${haskell:Depends},
${misc:Depends},
Recommends: ${haskell:Recommends},
Suggests: ${haskell:Suggests},
Conflicts: ${haskell:Conflicts},
Provides: ${haskell:Provides},
Description: ${haskell:ShortDescription}${haskell:ShortBlurb}
${haskell:LongDescription}
.
${haskell:Blurb}
Package: libghc-postgrest-doc
Architecture: all
Section: doc
Depends: ${haskell:Depends},
${misc:Depends},
Recommends: ${haskell:Recommends},
Suggests: ${haskell:Suggests},
Conflicts: ${haskell:Conflicts},
Description: ${haskell:ShortDescription}${haskell:ShortBlurb}
${haskell:LongDescription}
.
${haskell:Blurb}
Package: haskell-postgrest-utils
Architecture: any
Section: misc
Depends: ${haskell:Depends},
${misc:Depends},
Recommends: ${haskell:Recommends},
Suggests: ${haskell:Suggests},
Conflicts: ${haskell:Conflicts},
Provides: ${haskell:Provides},
Description: ${haskell:ShortDescription}${haskell:ShortBlurb}
${haskell:LongDescription}
.
${haskell:Blurb}
+32
View File
@@ -0,0 +1,32 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: postgrest
Upstream-Contact: Joe Nelson <joe@begriffs.com>
Source: https://hackage.haskell.org/package/postgrest
Files: *
Copyright: 2014-2015 Joe Nelson <joe@begriffs.com>
License: Expat
Files: debian/*
Copyright: 2015 Fernando Ike <fike@midstorm.org>
License: Expat
License: Expat
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
.
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+1
View File
@@ -0,0 +1 @@
dist-ghc/build/postgrest/postgrest usr/bin
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
Vendored Executable
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/make -f
DEB_ENABLE_TESTS = yes
DEB_CABAL_PACKAGE = postgrest
DEB_DEFAULT_COMPILER = ghc
include /usr/share/cdbs/1/rules/debhelper.mk
include /usr/share/cdbs/1/class/hlibrary.mk
build/haskell-postgrest-utils:: build-ghc-stamp
+1
View File
@@ -0,0 +1 @@
3.0 (quilt)
+2
View File
@@ -0,0 +1,2 @@
version=3
http://hackage.haskell.org/package/postgrest/distro-monitor .*-([0-9\.]+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
+1
View File
@@ -0,0 +1 @@
postgrest.com
+9
View File
@@ -0,0 +1,9 @@
## Deployment
### Heroku
#### Getting Started
#### Using Amazon RDS
### Debian
+9
View File
@@ -0,0 +1,9 @@
## Data Migration
### Sqitch
### Test-Driven Migrations
#### Structural Tests
#### Value Tests with pgTAP
+9
View File
@@ -0,0 +1,9 @@
## Performance
### Benchmarks
### Caching
### Quality of Service
### Tips
+21
View File
@@ -0,0 +1,21 @@
## Security
### SSL
### Database Roles
### JSON Web Tokens
#### Issuing via sql procedures
### Row-Level Security
#### Simulated - PostgreSQL <9.5
#### Real - PostgreSQL >=9.5
### Building Auth on top of JWT
#### Basic Auth
#### Github Sign-in
+9
View File
@@ -0,0 +1,9 @@
## API Versioning
### Schema Search Path
### Changing a Resource
### Removing a Resource
### Avoiding DB and Client Coupling
+296
View File
@@ -0,0 +1,296 @@
## Requesting Information
### Tables and Views
* ✅ Cacheable, prefetchable
* ✅ Idempotent
The list of accessible tables and views is provided at
```HTTP
GET /
```
Every view and table accessible by the active db role is exposed
in a one-level deep route. For instance the full contents of a table
`people` is returned at
```HTTP
GET /people
```
There are no `deeply/nested/routes`. Each route provides `OPTIONS`,
`GET`, `POST`, `PUT`, `PATCH`, and `DELETE` verbs depending entirely
on database permissions.
<div class="admonition note">
<p class="admonition-title">Design Consideration</p>
<p>Why not provide nested routes? Many APIs allow nesting to
retrieve related information, such as <code>/films/1/director</code>.
We offer a more flexible mechanism instead to embed related
information, including many-to-many relationships. This is covered
in the section about Embedding.</p>
</div>
### Stored Procedures
* ❌ Cannot necessarily be cached or prefetched
* ❌ Not necessarily idempotent
Every stored procedure is accessible under the `/rpc` prefix. The
API endpoint supports only POST which executes the function.
```HTTP
POST /rpc/proc_name
```
PostgREST supports calling procedures with [named
arguments](http://www.postgresql.org/docs/9.4/static/sql-syntax-calling-funcs.html#SQL-SYNTAX-CALLING-FUNCS-NAMED).
To do so include a JSON object in the request payload and each
key/value of the object will become an argument.
<div class="admonition note">
<p class="admonition-title">Design Consideration</p>
<p>Why the /rpc prefix? One reason is to avoid name collisions
between views and procedures. It also helps emphasize to API
consumers that these functions are not normal restful things.
The functions can have arbitrary and surprising behavior, not
the standard "post creates a resource" thing that users expect
from the other routes.</p>
<p>We considered allowing GET requests for functions that are
marked non-volatile but could not reconcile how to pass in
parameters. Query string arguments are reserved for shaping/filtering
the output, not providing input.</p>
</div>
### Filtering
#### Filtering Rows
You can filter result rows by adding conditions on columns, each
condition a query string parameter. For instance, to return people
aged under 13 years old:
```HTTP
GET /people?age=lt.13
```
Adding multiple parameters conjoins the conditions:
```HTTP
GET /people?age=gte.18&student=is.true
```
These operators are available:
abbreviation | meaning
------------ | -------
eq | equals
gt | greater than
lt | less than
gte | greater than or equal
lte | less than or equal
like | LIKE operator (use * in place of %)
ilike | ILIKE operator (use * in place of %)
@@ | full-text search using to_tsquery
is | checking for exact equality (null,true,false)
in | one of a list of values e.g. `?a=in.1,2,3`
not | negates another operator, see below
To negate any operator, prefix it with `not` like `?a=not.eq.2`.
For more complicated filters (such as those involving condition 1
*OR* condition 2) you will have to create a new view in the database.
Filters may be applied to [computed
columns](http://www.postgresql.org/docs/current/interactive/xfunc-sql.html#XFUNC-SQL-COMPOSITE-FUNCTIONS)
as well as actual table/view columns, even though the computed
columns will not appear in the output.
#### Filtering Columns
You can customize which columns are returned by using the `select`
parameter:
```HTTP
GET /people?select=age,height,weight
```
To cast the column types, add a double colon
```HTTP
GET /people?select=age::text,height,weight
```
Not all type coercions are possible, and you will get an error
describing any problems from selection or type casting.
The `select` keyword is reserved. You thus cannot filter rows based
on a column named select. Then again it is a reserved SQL keyword
too, hence an unlikely column name.
#### Inside JSONB
PostgreSQL >=9.4.2 supports native JSON columns and can even index
them by internal keys using the `jsonb` column type. PostgREST
allows you to filter results by internal JSON object values. Use
the single- and double-arrows to path into and obtain values, e.g.
```HTTP
GET /stuff?json_col->a->>b=eq.2
```
This query finds rows in `stuff` where `json_col->'a'->>'b'` is
equal to 2 (or "2" -- it coerces as needed). The final arrow must
be the double kind, `->>`, or else PostgREST will not attempt to
look inside the JSON.
### Ordering
The reserved word `order` reorders the response rows. It uses a
comma-separated list of columns and directions:
```HTTP
GET /people?order=age.desc,height.asc
```
If no direction is specified it defaults to descending order:
```HTTP
GET /people?order=age
```
If you care where nulls are sorted, add `nullsfirst` or `nullslast`:
```HTTP
GET /people?order=age.nullsfirst
```
### Limiting and Pagination
#### Pagination by Limit-Offset
PostgREST uses HTTP range headers for limiting and describing the
size of results. Every response contains the current range and total
results:
```
Range-Unit: items
Content-Range → 0-14/15
```
This means items zero through fourteen are returned out of a total
of fifteen -- i.e. all of them. This information is available in
every response and can help you render pagination controls on the
client. This is a RFC7233-compliant solution that keeps the response
JSON cleaner.
The client can set the limit and offset of a request by setting the
`Range` header. Translate the limit and offset into a range. To
request the first five elements, include these request headers:
```
Range-Unit: items
Range: 0-4
```
You can also use open-ended ranges for an offset with no limit:
`Range: 10-`.
#### Suppressing Counts
Sometimes knowing the total row count of a query is unnecessary and
only adds extra cost to the database query. So you can skip the
count total using a ```Prefer``` header as:
```
Prefer: count=none
```
So the PostgREST response will be something like:
```
Range-Unit: items
Content-Range → 0-14/*
```
### Embedding Foreign Entities
Suppose you have a `projects` table which references `clients` through
a foreign key called `client_id`. When listing projects through the
API you can have it embed the client within each project response.
For example,
```HTTP
GET /projects?id=eq.1&select=id, name, clients(*)
```
Notice this is the same `select` keyword which is used to choose
which columns to include. When a column name is followed by parentheses
that means to fetch the entire record and nest it. You include a
list of columns inside the parens, or asterisk to request all
columns.
The embedding works for 1-N, N-1, and N-N relationships. That means
you could also ask for a client and all their projects:
```HTTP
GET /clients?id=eq.42&select=id, name, projects(*)
```
### Response Format
Query responses default to JSON but you can get them in CSV as well. Just make your request with the header
```HTTP
Accept: text/csv
```
### Singular vs Plural
Many APIs distinguish plural and singular resources, e.g.`/stories`
vs `/stories/1`. Why do we use `/stories?id=eq.1`? It is because a
single resource is for us a row determined by a primary key, and
primary keys can be *compound* (meaning defined across more than
one column). The common urls come from a degenerate case of simple
(and overwhelmingly numeric) primary keys often introduced automatically
be Object Relational Mapping.
For consistency's sake all these endpoints return a JSON array,
`/stories`, `/stories?genre=eq.mystery`, `/stories?id=eq.1`. They
are all filtering a bigger array. However you might want the
last one to return a single JSON object, not an array with one
element. There is currently an open issue to enable this.
### Data Schema
As well as issuing a `GET /` to obtain a list of the tables, views,
and stored procedures available, you can get more information about
any particular endpoint.
```HTTP
OPTIONS /my_view
```
This will include the row names, their types, primary key
information, and foreign keys for the given table or view.
<div class="admonition danger">
<p class="admonition-title">Deprecation Warning</p>
<p>Although we currently use the OPTIONS verb for this, some
people <a
href="https://www.mnot.net/blog/2012/10/29/NO_OPTIONS">argue</a> that
this is inappropriate. We are considering a <code>describedby</code>
header link instead.</p>
</div>
### CORS
PostgREST sets highly permissive cross origin resource sharing. It
accepts Ajax requests from any domain.
+131
View File
@@ -0,0 +1,131 @@
## Updating Data
### Record Creation
* ❌ Cannot be cached or prefetched
* ❌ Not idempotent
To create a row in a database table post a JSON object whose keys
are the names of the columns you would like to create. Missing keys
will be set to default values when applicable.
```HTTP
POST /table_name
{ "col1": "value1", "col2": "value2" }
```
The response will include a `Location` header describing where to
find the new object. If you would like to get the full object back
in the response to your request, include the header `Prefer:
return=representation`. That way you won't have to make another
HTTP call to discover properties that may have been filled in on
the server side.
### Bulk Insertion
* ❌ Cannot be cached or prefetched
* ❌ Not idempotent
While regular insertion uses JSON to encode the value, bulk insertion
uses CSV. Simply post to a table route with `Content-Type: text/csv`
and include the names of the columns as the first row. For instance
```HTTP
POST /people
name,age,height
J Doe,62,70
Jonas,10,55
```
An empty field (`,,`) is coerced to an empty string and the reserved
word `NULL` is mapped to the SQL null value. Note that there should
be no spaces between the column names and commas.
The server sends a multipart response for bulk insertions. Each part
contains a Location header with URL of each created resource.
```HTTP
Content-Type: application/json
Location: /festival?name=eq.Venice%20Film%20Festival
--postgrest_boundary
Content-Type: application/json
Location: /festival?name=eq.Cannes%20Film%20Festival
```
### Upsertion
* ❌ Cannot be cached or prefetched
* ✅ Idempotent
To insert or update a single row use the `PUT` verb on a properly
filtered table url:
```HTTP
PUT /table_name?primary_key=eq.foo
{ "col1": "value1", "col2": "value2" }
```
The request must satisfy two things. First all columns must be
specified (because a default value might be a changing value which
would violate idempotence). Second the URL must match the URL you
would use to get the value of the resource. This means that all
primary key columns must be included in the filter (there are more
than one when the primary key is compound).
If you would like to get the full object back in the response to
your request, include the header `Prefer: return=representation`.
It will of match exactly the object you sent though.
### Bulk Updates
* ❌ Cannot be cached or prefetched
* ❌ Not idempotent
To change parts of a resource or resources use the `PATCH` verb.
For instance, here is how to mark all young people as children.
```HTTP
PATCH /people?age=lt.13
{
"person_type": "child"
}
```
This affects any rows matched by the url param filters, overwrites
any fields specified in in the payload JSON and leaves the other
fields unaffected. Note that although the payload is not in the
JSON patch format specified by
[RFC6902](https://tools.ietf.org/html/rfc6902), HTTP does not specify
which patch format to use. Our format is more pleasant, meant for
basic field replacements, and not at all "incorrect."
### Deletion
* ❌ Cannot be cached or prefetched
* ✅ Idempotent
Simply use the `DELETE` verb. All recors that match your filter
will be removed. For instance deleting inactive users:
```HTTP
DELETE /user?active=eq.false
```
### Protecting Dangerous Actions
Notice that it is very easy to delete or update many records at
once. In fact forgetting a filter will affect an entire table!
<div class="admonition warning">
<p class="admonition-title">Invitation to Contribute</p>
<p>We would like to investigate nginx rules to guard dangerous
actions, perhaps requiring a confirmation header or query param
to perform the action.</p>
<p>You're invited to research this option and contribute to
this documentation.</p>
</div>
+346
View File
@@ -0,0 +1,346 @@
## Getting Started
### Your First (simple) API
Let's start with the simplest thing possible. We will expose some tables directly for reading and writing by anyone.
Start by making a database
```sh
createdb demo1
```
We'll set it up with a film example (courtesy of [Jonathan Harrington](http://blog.jonharrington.org/postgrest-introduction/)). Copy the following into your clipboard:
```sql
BEGIN;
CREATE TABLE director
(
name text NOT NULL PRIMARY KEY
);
CREATE TABLE film
(
id serial PRIMARY KEY,
title text NOT NULL,
year date NOT NULL,
director text,
rating real NOT NULL DEFAULT 0,
language text NOT NULL,
CONSTRAINT film_director_fkey FOREIGN KEY (director)
REFERENCES director (name) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE TABLE festival
(
name text NOT NULL PRIMARY KEY
);
CREATE TABLE competition
(
id serial PRIMARY KEY,
name text NOT NULL,
festival text NOT NULL,
year date NOT NULL,
CONSTRAINT comp_festival_fkey FOREIGN KEY (festival)
REFERENCES festival (name) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE
);
CREATE TABLE film_nomination
(
id serial PRIMARY KEY,
competition integer NOT NULL,
film integer NOT NULL,
won boolean NOT NULL DEFAULT true,
CONSTRAINT nomination_competition_fkey FOREIGN KEY (competition)
REFERENCES competition (id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT nomination_film_fkey FOREIGN KEY (film)
REFERENCES film (id) MATCH SIMPLE
ON UPDATE CASCADE ON DELETE CASCADE
);
COMMIT;
```
Apply it to your new database by running
```sh
# On OS X
pbpaste | psql demo1
# Or Linux
# xclip -selection clipboard -o | psql demo1
```
Start the PostgREST server and point it at the new database.
```sh
postgrest -d demo1 -U postgres -a postgres --v1schema public
```
<div class="admonition note">
<p class="admonition-title">Note about database users</p>
<p>If you installed PostgreSQL with Homebrew on Mac then the
database username may be your own login rather than
<code>postgres</code>.</p>
</div>
Let's use PostgREST to populate the database. Install a REST client such as [Postman](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en). Now let's insert some data as a bulk post in CSV format:
```HTTP
POST http://localhost:3000/festival
Content-Type: text/csv
name
Venice Film Festival
Cannes Film Festival
```
In Postman it will look like this
![Festival bulk insert in postman](/img/post-festivals.png)
Notice that the post type is `raw` and that `Content-Type: text/csv` set in the Headers tab.
Note that the server returns a multipart response with URL of each created resource.
```HTTP
Content-Type: application/json
Location: /festival?name=eq.Venice%20Film%20Festival
--postgrest_boundary
Content-Type: application/json
Location: /festival?name=eq.Cannes%20Film%20Festival
```
If you send a GET request to `/festival` it should return
```json
[
{
"name": "Venice Film Festival"
},
{
"name": "Cannes Film Festival"
}
]
```
Now that you've seen how to do a bulk insert, let's do some more and fully populate the database.
Post the following to `/competition`:
```csv
name,festival,year
Golden Lion,Venice Film Festival,2014-01-01
Palme d'Or,Cannes Film Festival,2014-01-01
```
Now `/director`:
```csv
name
Bertrand Bonello
Atom Egoyan
David Gordon Green
Andrey Konchalovskiy
Mario Martone
Mike Leigh
Roy Andersson
Saverio Costanzo
Alix Delaporte
Jean-Pierre Dardenne
Xiaoshuai Wang
Kaan Müjdeci
Tommy Lee Jones
Nuri Bilge Ceylan
Michel Hazanavicius
Xavier Dolan
Ramin Bahrani
Alice Rohrwacher
Andrew Niccol
Rakhshan Bani-Etemad
David Oelhoffen
Bennett Miller
David Cronenberg
Shin'ya Tsukamoto
Joshua Oppenheimer
Olivier Assayas
Jean-Luc Godard
Alejandro González Iñárritu
Benoît Jacquot
Fatih Akin
Francesco Munzi
Ken Loach
Abel Ferrara
Xavier Beauvois
Naomi Kawase
```
And `/film`:
```csv
title,year,director,rating,language
Chuang ru zhe,2014-01-01,Xiaoshuai Wang,6.19999981,english
The Look of Silence,2014-01-01,Joshua Oppenheimer,8.30000019,Indonesian
Fires on the Plain,2014-01-01,Shin'ya Tsukamoto,5.80000019,Japanese
Far from Men,2014-01-01,David Oelhoffen,7.5,english
Good Kill,2014-01-01,Andrew Niccol,6.0999999,english
Leopardi,2014-01-01,Mario Martone,6.9000001,english
Sivas,2014-01-01,Kaan Müjdeci,7.69999981,english
Black Souls,2014-01-01,Francesco Munzi,7.0999999,english
Three Hearts,2014-01-01,Benoît Jacquot,5.80000019,French
Pasolini,2014-01-01,Abel Ferrara,5.80000019,english
Le dernier coup de marteau,2014-01-01,Alix Delaporte,6.5,english
Manglehorn,2014-01-01,David Gordon Green,7.0999999,english
Hungry Hearts,2014-01-01,Saverio Costanzo,6.4000001,English
Belye nochi pochtalona Alekseya Tryapitsyna,2014-01-01,Andrey Konchalovskiy,6.9000001,Russian
99 Homes,2014-01-01,Ramin Bahrani,7.30000019,english
The Cut,2014-01-01,Fatih Akin,6,Armenian
Birdman: Or (The Unexpected Virtue of Ignorance),2014-01-01,Alejandro González Iñárritu,8,English
La rançon de la gloire,2014-01-01,Xavier Beauvois,5.69999981,French
A Pigeon Sat on a Branch Reflecting on Existence,2014-01-01,Roy Andersson,7.19999981,english
Tales,2014-01-01,Rakhshan Bani-Etemad,6.80000019,english
The Wonders,2014-01-01,Alice Rohrwacher,6.80000019,Italian
Foxcatcher,2014-01-01,Bennett Miller,7.19999981,English
Mr. Turner,2014-01-01,Mike Leigh,7,English
Jimmy's Hall,2014-01-01,Ken Loach,6.69999981,English
The Homesman,2014-01-01,Tommy Lee Jones,6.5999999,English
The Captive,2014-01-01,Atom Egoyan,5.9000001,english
Goodbye to Language,2014-01-01,Jean-Luc Godard,6.19999981,French
The Search,2014-01-01,Michel Hazanavicius,6.9000001,French
Still the Water,2014-01-01,Naomi Kawase,6.9000001,Japanese
Mommy,2014-01-01,Xavier Dolan,8.30000019,French
"Two Days, One Night",2014-01-01,Jean-Pierre Dardenne,7.4000001,French
Maps to the Stars,2014-01-01,David Cronenberg,6.4000001,English
Saint Laurent,2014-01-01,Bertrand Bonello,6.5,French
Clouds of Sils Maria,2014-01-01,Olivier Assayas,6.9000001,english
Winter Sleep,2014-01-01,Nuri Bilge Ceylan,8.5,Turkish
```
Finally `/film_nomination`:
```csv
competition,film,won
1,1,f
1,2,f
1,3,f
1,4,f
1,5,f
1,6,f
1,7,f
1,8,f
1,9,f
1,10,f
1,11,f
1,12,f
1,13,f
1,14,f
1,15,f
1,16,f
1,17,f
1,18,f
1,19,f
1,20,f
2,21,f
2,22,f
2,23,f
2,24,f
2,25,f
2,26,f
2,27,f
2,28,f
2,29,f
2,30,f
2,31,f
2,32,f
2,33,f
2,34,f
2,35,f
```
At this point nominations are fully specified but it's not a convenient interface for a rest client. Let's make a view they can use. Paste this into `psql demo1`.
```sql
create or replace view nomination as
select comp.festival,
comp.name as competition,
comp.year,
film.title,
film.director,
film.rating
from film_nomination as nom
left join film on nom.film = film.id
left join competition as comp on nom.competition = comp.id
order by comp.year desc, comp.festival, competition;
```
Time to try it out. Let's get the contents of the new view, ordered by film rating
```
GET http://localhost:3000/nomination?order=rating.desc
```
If you find it more human readable, add an `Accept: text/csv` header.
### Releasing a New Version
Suppose we want this endpoint to cater to those moviegoers with attention deficit disorder. In today's busy world we don't have time to read an extra couple words or compare nuanced reviews. In API version two we will truncate the names and round the ratings!
Each version lives in a numbered schema, so let's make a schema for version two.
```sql
CREATE SCHEMA "2";
GRANT USAGE ON SCHEMA "2" TO PUBLIC;
ALTER DATABASE demo1 SET search_path = "2", "public";
```
To override the `films` endpoint create a view in the "2" schema with that name:
```sql
create or replace view "2".film as
select id, substring(f.title from 1 for 10) as title,
year, director, round(f.rating) as rating, language
from "public".film as f;
```
We select the desired version as part of content negotiation. Try this get request:
```HTTP
GET http://localhost:3000/film
Accept: text/csv; version=2
```
Then try toggling the version string in the Accept header and watch the results change. Pretty good, now how about writing values? PostgreSQL's nice feature called auto-updatable views allows writes to pass through views. Sadly this view is not eligible because truncation and rounding cannot be uniquely reversed. If we attempt to post a new result it complains:
```json
{
"hint": null,
"details": "View columns that are not columns of their base relation are not updatable.",
"code": "0A000",
"message": "cannot insert into column \"title\" of view \"film\""
}
```
This is a case where we need explicit triggers
```sql
-- TODO - FIX THIS
-- CREATE OR REPLACE RULE insert_v2_films AS
-- ON INSERT TO "2".film
-- DO INSTEAD
-- INSERT INTO public.film (id, title, year, director, rating, language)
-- VALUES (NEW.id, NEW.title,
-- NEW.year, NEW.director,
-- NEW.rating, NEW.language)
-- RETURNING public.film.*;
```
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

+68
View File
@@ -0,0 +1,68 @@
![PostgREST logo](img/logo.png)
## Introduction
PostgREST is a standalone web server that turns your database directly into a RESTful API. The structural constraints and permissions in the database determine the API endpoints and operations.
This guide explains how to install the software and provides practical examples of its use. You'll learn how to build a fast, versioned, secure API and how to deploy it to production.
The project has a friendly and growing community. Here are some ways to get help or get involved:
* The project [chat room](https://gitter.im/begriffs/postgrest)
* Report or search [issues](https://github.com/begriffs/postgrest/issues)
### Motivation
Using PostgREST is an alternative to manual CRUD programming. Custom API servers suffer problems. Writing business logic often duplicates, ignores or hobbles database structure. Object-relational mapping is a leaky abstraction leading to slow imperative code. The PostgREST philosophy establishes a single declarative source of truth: the data itself.
#### Declarative Programming
It's easier to ask Postgres to join data for you and let its query planner figure out the details than to loop through rows yourself. It's easier to assign permissions to db objects than to add guards in controllers. (This is especially true for cascading permissions in data dependencies.) It's easier set constraints than to litter code with sanity checks.
#### Leakproof Abstraction
There is no ORM involved. Creating new views happens in SQL with known performance implications. A database administrator can now create an API from scratch with no custom programming.
#### Embracing the Relational Model
In 1970 E. F. Codd criticized the then-dominant hierarchical model of databases in his article <a href="https://www.seas.upenn.edu/~zives/03f/cis550/codd.pdf">A Relational Model of Data for Large Shared Data Banks</a>. Reading the article reveals a striking similarity between hierarchical databases and nested http routes. With PostgREST we attempt to use flexible filtering and embedding rather than nested routes.
#### One Thing Well
PostgREST has a focused scope. It works well with other tools like Nginx. This forces you to cleanly separate the data-centric CRUD operations from other concerns. Use a collection of sharp tools rather than building a big ball of mud.
#### Shared Improvements
As with any open source project, we all gain from features and fixes in the tool. It's more beneficial than improvements locked inextricably within custom codebases.
### Myths
#### You have to make tons of stored procs and triggers
Modern PostgreSQL features like auto-updatable views and computed columns make this mostly unnecessary. Triggers do play a part, but generally not for irksome boilerplate. When they are required triggers are preferable to ad-hoc app code anyway, since the former work reliably for any codepath.
#### Exposing the database destroys encapsulation
PostgREST does versioning through database schemas. This allows you to expose tables and views without making the app brittle. Underlying tables can be superseded and hidden behind public facing views. The chapter about versioning shows how to do this.
### Conventions
This guide contains highlighted notes and tangential information interspersed with the text.
<div class="admonition note">
<p class="admonition-title">Design Consideration</p>
<p>Contains history which informed the current design. Sometimes it discusses unavoidable tradeoffs or a point of theory.</p>
</div>
<div class="admonition warning">
<p class="admonition-title">Invitation to Contribute</p>
<p>Points out things we know we want to add or improve. They might give you ideas for ways to contribute to the project.</p>
</div>
<div class="admonition danger">
<p class="admonition-title">Deprecation Warning</p>
<p>Alerts you to features which will be removed in the next major (breaking) release.</p>
</div>
+23
View File
@@ -0,0 +1,23 @@
## Ecosystem
### Client-Side Libraries
* [mithril.postgrest](https://github.com/catarse/mithril.postgrest) - Mithril plugin to create and authenticate requests
* [lewisjared/postgrest-request](https://github.com/lewisjared/postgrest-request) - node interface to postgrest instances
* [JarvusInnovations/jarvus-postgrest-apikit](https://github.com/JarvusInnovations/jarvus-postgrest-apikit) - Sencha framework package for binding models/stores/proxies to PostgREST tables
### Extensions
* [srid/spas](https://github.com/srid/spas) - allow file uploads and basic auth
### Example Apps
* [timwis/ext-postgrest-crud](https://github.com/timwis/ext-postgrest-crud) - browser-based spreadsheet
* [srid/chronicle](https://github.com/srid/chronicle#deploying-to-heroku) - tracking a tree of personal memories
* [begriffs/postgrest-example](https://github.com/begriffs/postgrest-example) - how to configure a db for use as an API
* [marmelab/ng-admin-postgrest](https://github.com/marmelab/ng-admin-postgrest) - automatic database admin panel
* [tyrchen/goodfilm](https://github.com/tyrchen/goodfilm) - example film api
### In Production
* [Catarse](https://www.catarse.me/)
+62
View File
@@ -0,0 +1,62 @@
## Installation
### Installing from Pre-Built Release
The [release page](https://github.com/begriffs/postgrest/releases/latest) has precompiled binaries for Mac OS X and 64-bit Ubuntu. Next extract the tarball and run the binary inside with no arguments to see usage instructions:
```sh
# Untar the release (available at https://github.com/begriffs/postgrest/releases/latest)
$ tar zxf postgrest-0.2.12.0-osx.tar.xz
# Try running it
$ ./postgrest
# You should see a usage help message
```
<div class="admonition warning">
<p class="admonition-title">Invitation to Contribute</p>
<p>I currently build the binaries manually for each version. We need to set up an automated build matrix for various architectures. It should support 32- and 64-bit versions of
<ul><li>Scientific Linux 6</li><li>CentOS</li><li>RHEL 6</li></ul>
Also it would be good to create packages for Homebrew, and apt.</p>
</div>
We'll learn the meaning of the command line flags later, but here is a minimal example of running the app. It does all operations as user `postgres`, including for unauthenticated requests.
```sh
$ ./postgrest -d dbname -U postgres --a postgres --v1schema public
```
### Building from Source
When a prebuilt binary does not exist for your system you can build the project from source. You'll also need to do this if you want to help with development. [Stack](https://github.com/commercialhaskell/stack) makes it easy. It will install any necessary Haskell dependencies on your system.
* [Install Stack](https://github.com/commercialhaskell/stack#how-to-install) for your platform
* Build the project
```bash
git clone https://github.com/begriffs/postgrest.git
cd postgrest
stack build
```
* Run the server
```bash
stack exec postgrest -- arg1 arg2
# ... your arguments after the double dashes
```
If you want to run the test suite, stack can do that too: `stack test`.
### Installing PostgreSQL
To use PostgREST you will need an underlying database. You can use something like Amazon [RDS](https://aws.amazon.com/rds/) but installing your own locally is cheaper and more convenient for development.
* [Instructions for OS X](http://exponential.io/blog/2015/02/21/install-postgresql-on-mac-os-x-via-brew/)
* [Instructions for Ubuntu 14.04](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-14-04)
+24
View File
@@ -0,0 +1,24 @@
site_name: PostgREST
site_url: http://postgrest.com
site_description: Building declarative APIs
site_author: Joe Nelson
site_favicon: favicon.ico
repo_url: https://github.com/begriffs/postgrest
pages:
- Home: index.md
- Install:
- The Server: install/server.md
- Ecosystem: install/ecosystem.md
- API:
- Reading: api/reading.md
- Writing: api/writing.md
- Admin:
- Security: admin/security.md
- Versioning: admin/versioning.md
- Migration: admin/migration.md
- Deployment: admin/deployment.md
- Performance: admin/performance.md
- Examples:
- Getting Started: examples/start.md
+167
View File
@@ -0,0 +1,167 @@
name: postgrest
description: Reads the schema of a PostgreSQL database and creates RESTful routes
for the tables and views, supporting all HTTP verbs that security
permits.
version: 0.2.12.0
synopsis: REST API for any Postgres database
license: MIT
license-file: LICENSE
author: Joe Nelson, Adam Baker
homepage: https://github.com/begriffs/postgrest
maintainer: cred+github@begriffs.com
category: Web
build-type: Simple
cabal-version: >=1.10
source-repository head
type: git
location: git://github.com/begriffs/postgrest.git
Flag CI
Description: No warnings allowed in continuous integration
Manual: True
Default: False
executable postgrest
main-is: PostgREST/Main.hs
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
default-language: Haskell2010
build-depends: base >=4.6 && <5
, postgrest
, hasql >= 0.7.3 && < 0.8
, hasql-backend >= 0.4.1 && < 0.5
, hasql-postgres >= 0.10.4 && < 0.11
, warp >= 3.0.2, wai >= 3.0.1
, wai-extra, wai-cors
, wai-middleware-static >= 0.6.0
, HTTP, convertible, http-types
, case-insensitive
, scientific, time
, aeson >= 0.8, network >= 2.6
, bytestring, text, split, string-conversions
, stringsearch
, containers, unordered-containers
, optparse-applicative >= 0.11 && < 0.13
, regex-base, regex-tdfa
, Ranged-sets
, transformers, MissingH
, bcrypt >= 0.0.6, base64-string
, network-uri >= 2.6
, resource-pool
, blaze-builder
, vector
, mtl
, cassava
, jwt
, parsec
, errors
, bifunctors
hs-source-dirs: src
library
if flag(ci)
ghc-options: -Wall -W -Werror
else
ghc-options: -Wall -W -O2
default-language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
build-depends: base >=4.6 && <5
, hasql, hasql-backend
, hasql-postgres
, warp, wai
, wai-extra, wai-cors
, wai-middleware-static
, HTTP, convertible, http-types
, case-insensitive
, scientific, time
, aeson, network
, bytestring, text, split, string-conversions
, stringsearch
, containers, unordered-containers
, optparse-applicative
, regex-base, regex-tdfa
, Ranged-sets
, transformers, MissingH
, bcrypt, base64-string
, network-uri
, resource-pool
, blaze-builder
, vector
, mtl
, cassava
, jwt
, parsec
, errors
, bifunctors
Other-Modules: Paths_postgrest
Exposed-Modules: PostgREST.App
, PostgREST.Types
, PostgREST.Parsers
, PostgREST.QueryBuilder
, PostgREST.Auth
, PostgREST.Config
, PostgREST.Error
, PostgREST.Middleware
, PostgREST.PgQuery
, PostgREST.PgStructure
, PostgREST.RangeQuery
hs-source-dirs: src
Test-Suite spec
Type: exitcode-stdio-1.0
Default-Language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
Hs-Source-Dirs: test, src
if flag(ci)
ghc-options: -Wall -W -Werror
else
ghc-options: -Wall -W -O2
Main-Is: Main.hs
Other-Modules: PostgREST.App
, PostgREST.Types
, PostgREST.Parsers
, PostgREST.QueryBuilder
, PostgREST.Auth
, PostgREST.Config
, PostgREST.Error
, PostgREST.Middleware
, PostgREST.PgQuery
, PostgREST.PgStructure
, PostgREST.RangeQuery
, Spec
, SpecHelper
, Paths_postgrest
Build-Depends: base, hspec == 2.1.*, QuickCheck
, hspec-wai, hspec-wai-json
, hasql, hasql-backend
, hasql-postgres
, warp, wai
, packdeps, hlint
, HTTP, convertible
, case-insensitive
, wai-extra, wai-cors, containers
, wai-middleware-static
, http-types, scientific, time
, bytestring, aeson, network
, text, optparse-applicative
, stringsearch
, unordered-containers
, regex-base
, string-conversions
, http-media, regex-tdfa
, Ranged-sets
, transformers, MissingH, split
, bcrypt, base64-string
, network-uri
, resource-pool
, blaze-builder
, vector
, mtl
, cassava
, process
, heredoc
, jwt
, parsec
, errors
, bifunctors
+10
View File
@@ -0,0 +1,10 @@
export POSTGREST_VER=`grep ^version /app/postgrest.cabal | sed -En 's/.*\s+([0-9\.]+)/\1/p'`
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}
tar cJf postgrest-${POSTGREST_VER}.tar.xz postgrest-${POSTGREST_VER}
touch ~/.s3cfg
s3cmd-1.5.0-alpha1/s3cmd put --access_key=${S3_ACCESS_KEY} --secret_key=${S3_SECRET_KEY} -P -f postgrest-${POSTGREST_VER}.tar.xz $S3_BUCKET/postgrest-${POSTGREST_VER}.tar.xz
-204
View File
@@ -1,204 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
-- {{{ Imports
module Dbapi where
import Types (SqlRow, getRow)
import Control.Monad (join)
import Control.Arrow ((***))
import Control.Applicative
import Options.Applicative hiding (columns)
import Data.Maybe (fromMaybe, isJust)
import Text.Regex.TDFA ((=~))
import Data.Map (intersection, fromList, toList, Map)
import Data.List (sort)
import qualified Data.Set as S
import Data.Convertible.Base (convert)
import Data.Text (strip)
import Network.HTTP.Types.Status
import Network.HTTP.Types.Header
import Network.HTTP.Types.URI
import Network.HTTP.Base (urlEncodeVars)
import Network.Wai
import Network.Wai.Internal
import Network.Wai.Middleware.Cors (CorsResourcePolicy(..))
import qualified Data.ByteString.Char8 as BS
import Data.String.Conversions (cs)
import qualified Data.CaseInsensitive as CI
import Database.HDBC.PostgreSQL (Connection)
import PgStructure (printTables, printColumns, primaryKeyColumns,
columns, Column(colName))
import qualified Data.Aeson as JSON
import PgQuery
import RangeQuery
import Data.Ranged.Ranges (emptyRange)
-- }}}
data AppConfig = AppConfig {
configDbUri :: String
, configPort :: Int
, configAnonRole :: String
, configSecure :: Bool
}
jsonContentType :: (HeaderName, BS.ByteString)
jsonContentType = (hContentType, "application/json")
jsonBodyAction :: Request -> (SqlRow -> IO Response) -> IO Response
jsonBodyAction req handler = do
parse <- jsonBody req
case parse of
Left err -> return $ responseLBS status400 [jsonContentType] json
where json = JSON.encode . JSON.object $ [("error", JSON.String $ "Failed to parse JSON payload. " <> cs err) ]
Right body -> handler body
jsonBody :: Request -> IO (Either String SqlRow)
jsonBody = fmap JSON.eitherDecode . strictRequestBody
filterByKeys :: Ord a => Map a b -> [a] -> Map a b
filterByKeys m keys =
if null keys then m else
m `intersection` fromList (zip keys $ repeat undefined)
app :: Connection -> Application
app conn req respond =
respond =<< case (path, verb) of
([], _) ->
responseLBS status200 [jsonContentType] <$> printTables ver conn
([table], "OPTIONS") ->
responseLBS status200 [jsonContentType, allOrigins] <$>
printColumns ver (cs table) conn
([table], "GET") ->
if range == Just emptyRange
then return $ responseLBS status416 [] "HTTP Range error"
else do
r <- respondWithRangedResult <$> getRows ver (cs table) qq range conn
let canonical = urlEncodeVars $ sort $
map (join (***) cs) $
parseSimpleQuery $
rawQueryString req
return $ addHeaders [
("Content-Location",
"/" <> cs table <> if null canonical then "" else "?" <> cs canonical
)] r
([table], "POST") ->
jsonBodyAction req (\row -> do
allvals <- insert ver table row conn
keys <- primaryKeyColumns ver (cs table) conn
let params = urlEncodeVars $ map (\t -> (fst t, "eq." <> convert (snd t) :: String)) $ toList $ filterByKeys allvals keys
return $ responseLBS status201
[ jsonContentType
, (hLocation, "/" <> cs table <> "?" <> cs params)
] ""
)
([table], "PUT") ->
jsonBodyAction req (\row -> do
keys <- primaryKeyColumns ver (cs table) conn
let specifiedKeys = map (cs . fst) qq
if S.fromList keys /= S.fromList specifiedKeys
then return $ responseLBS status405 []
"You must speficy all and only primary keys as params"
else
if isJust cRange
then return $ responseLBS status400 []
"Content-Range is not allowed in PUT request"
else do
cols <- columns ver (cs table) conn
let colNames = S.fromList $ map (cs . colName) cols
let specifiedCols = S.fromList $ map fst $ getRow row
return $ if colNames == specifiedCols then
responseLBS status200 [ jsonContentType ] ""
else if S.null colNames then responseLBS status404 [] ""
else responseLBS status400 []
"You must specify all columns in PUT request"
)
(_, _) ->
return $ responseLBS status404 [] ""
where
path = pathInfo req
verb = requestMethod req
qq = queryString req
hdrs = requestHeaders req
ver = fromMaybe "1" $ requestedVersion hdrs
range = requestedRange hdrs
cRange = requestedContentRange hdrs
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
defaultCorsPolicy :: CorsResourcePolicy
defaultCorsPolicy = CorsResourcePolicy Nothing
["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] ["Authorization"] Nothing
(Just $ 60*60*24) False False True
corsPolicy :: Request -> Maybe CorsResourcePolicy
corsPolicy req = case lookup "origin" headers of
Just origin -> Just defaultCorsPolicy {
corsOrigins = Just ([origin], True)
, corsRequestHeaders = "Authentication":accHeaders
}
Nothing -> Nothing
where
headers = requestHeaders req
accHeaders = case lookup "access-control-request-headers" headers of
Just hdrs -> map (CI.mk . cs . strip . cs) $ BS.split ',' hdrs
Nothing -> []
respondWithRangedResult :: RangedResult -> Response
respondWithRangedResult rr =
responseLBS status [
jsonContentType,
("Content-Range",
if total == 0 || from > total
then "*/" <> cs (show total)
else cs (show from) <> "-"
<> cs (show to) <> "/"
<> cs (show total)
)
] (rrBody rr)
where
from = rrFrom rr
to = rrTo rr
total = rrTotal rr
status
| from > total = status416
| (1 + to - from) < total = status206
| otherwise = status200
requestedVersion :: RequestHeaders -> Maybe String
requestedVersion hdrs =
case verStr of
Just [[_, ver]] -> Just ver
_ -> Nothing
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: String
accept = cs <$> lookup hAccept hdrs :: Maybe String
verStr = (=~ verRegex) <$> accept :: Maybe [[String]]
addHeaders :: ResponseHeaders -> Response -> Response
addHeaders hdrs (ResponseFile s headers fp m) =
ResponseFile s (headers ++ hdrs) fp m
addHeaders hdrs (ResponseBuilder s headers b) =
ResponseBuilder s (headers ++ hdrs) b
addHeaders hdrs (ResponseStream s headers b) =
ResponseStream s (headers ++ hdrs) b
addHeaders hdrs (ResponseRaw s resp) =
ResponseRaw s (addHeaders hdrs resp)
-47
View File
@@ -1,47 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module Main where
import Dbapi
import Middleware (inTransaction, authenticated, withSavepoint, clientErrors,
redirectInsecure, withDBConnection)
import Network.Wai.Handler.Warp hiding (Connection)
import Data.String.Conversions (cs)
import Control.Monad (unless)
import Control.Applicative
import Options.Applicative hiding (columns)
import Network.Wai.Middleware.Gzip (gzip, def)
import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Middleware.Static (staticPolicy, only)
import Database.HDBC (disconnect)
import Database.HDBC.PostgreSQL(connectPostgreSQL')
import Data.Pool(createPool)
argParser :: Parser AppConfig
argParser = AppConfig
<$> strOption (long "db" <> short 'd' <> metavar "URI"
<> help "database uri to expose, e.g. postgres://user:pass@host:port/database")
<*> option (long "port" <> short 'p' <> metavar "NUMBER" <> value 3000
<> help "port number on which to run HTTP server")
<*> strOption (long "anonymous" <> short 'a' <> metavar "ROLE"
<> help "postgres role to use for non-authenticated requests")
<*> switch (long "secure" <> short 's'
<> help "Redirect all requests to HTTPS" )
main :: IO ()
main = do
conf <- execParser (info (helper <*> argParser) describe)
pool <- createPool (connectPostgreSQL' (configDbUri conf)) disconnect 1 600 10
let port = configPort conf
unless (configSecure conf) $
putStrLn "WARNING, running in insecure mode, auth will be in plaintext"
Prelude.putStrLn $ "Listening on port " ++ (show $ configPort conf :: String)
run port $ (if configSecure conf then redirectInsecure else id)
. gzip def . cors corsPolicy . clientErrors
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
. withDBConnection pool . inTransaction
. authenticated (cs $ configAnonRole conf) . withSavepoint $ app
where
describe = progDesc "create a REST API to an existing Postgres database"
-110
View File
@@ -1,110 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Middleware where
import Data.Aeson ((.=), toJSON, ToJSON, object, encode)
import Data.Maybe (fromMaybe)
import Data.Monoid (mconcat)
import Data.Pool(withResource, Pool)
import Database.HDBC (runRaw)
import Database.HDBC.PostgreSQL (Connection)
import Database.HDBC.Types (SqlError(..))
import Data.String.Conversions(cs)
import qualified Data.ByteString.Char8 as BS
import Control.Exception (finally, throw, catchJust, catch, SomeException,
bracket_)
import Network.HTTP.Types.Header (RequestHeaders, hContentType, hAuthorization,
hLocation)
import Network.HTTP.Types.Status (status400, status401, status404, status301)
import Network.Wai (Application, requestHeaders, responseLBS, rawPathInfo,
rawQueryString, isSecure)
import Network.URI (URI(..), parseURI)
import PgQuery(LoginAttempt(..), signInRole, setRole, resetRole)
import Codec.Binary.Base64.String (decode)
withDBConnection :: Pool Connection -> (Connection -> Application) -> Application
withDBConnection pool app req respond =
withResource pool (\c -> app c req respond)
inTransaction :: (Connection -> Application) -> Connection -> Application
inTransaction app conn req respond =
finally (runRaw conn "begin" >> app conn req respond) (runRaw conn "commit")
withSavepoint :: (Connection -> Application) -> Connection -> Application
withSavepoint app conn req respond = do
runRaw conn "savepoint req_sp"
catch (app conn req respond) (\e -> let _ = (e::SomeException) in
runRaw conn "rollback to savepoint req_sp" >> throw e)
authenticated :: BS.ByteString -> (Connection -> Application) ->
Connection -> Application
authenticated anon app conn req respond = do
attempt <- httpRequesterRole (requestHeaders req)
case attempt of
MalformedAuth ->
respond $ responseLBS status400 [] "Malformed basic auth header"
LoginFailed ->
respond $ responseLBS status401 [] "Invalid username or password"
LoginSuccess role ->
bracket_ (setRole conn role) (resetRole conn) $ app conn req respond
NoCredentials ->
bracket_ (setRole conn anon) (resetRole conn) $ app conn req respond
where
httpRequesterRole :: RequestHeaders -> IO LoginAttempt
httpRequesterRole hdrs = do
let auth = fromMaybe "" $ lookup hAuthorization hdrs
case BS.split ' ' (cs auth) of
("Basic" : b64 : _) ->
case BS.split ':' $ cs (decode $ cs b64) of
(u:p:_) -> signInRole u p conn
_ -> return MalformedAuth
_ -> return NoCredentials
instance ToJSON SqlError where
toJSON t = object [
"error" .= object [
"code" .= seNativeError t
, "message" .= seErrorMsg t
, "state" .= seState t
]
]
clientErrors :: Application -> Application
clientErrors app req respond =
catchJust isPgException (app req respond) $ \err ->
respond $ if seState err == "42P01"
then responseLBS status404 [] ""
else responseLBS status400 [(hContentType, "application/json")] (encode err)
where
isPgException :: SqlError -> Maybe SqlError
isPgException = Just
redirectInsecure :: Application -> Application
redirectInsecure app req respond = do
let hdrs = requestHeaders req
host = lookup "host" hdrs
uriM = parseURI . cs =<< mconcat [
Just "https://",
host,
Just $ rawPathInfo req,
Just $ rawQueryString req]
isHerokuSecure = lookup "x-forwarded-proto" hdrs == Just "https"
if not (isSecure req || isHerokuSecure)
then case uriM of
Just uri ->
respond $ responseLBS status301 [
(hLocation, cs . show $ uri { uriScheme = "https:" })
] ""
Nothing ->
respond $ responseLBS status400 [] "SSL is required"
else app req respond
-248
View File
@@ -1,248 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
-- {{{ Imports
module PgQuery (
getRows
, insert
, upsert
, addUser
, signInRole
, setRole
, resetRole
, checkPass
, RangedResult(..)
, LoginAttempt(..)
, DbRole
) where
import Data.Text (Text)
import Data.String.Conversions (cs)
import Data.Functor ( (<$>) )
import Data.Maybe (fromMaybe, mapMaybe)
import Data.List (intersperse, intercalate)
import Data.List.Split (splitOn)
import Data.Monoid ((<>), mconcat)
import qualified Data.Map as M
import Control.Monad (join, void)
import qualified RangeQuery as R
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BL
import Database.HDBC hiding (colType, colNullable)
import Database.HDBC.PostgreSQL
import qualified Network.HTTP.Types.URI as Net
import Types (SqlRow(..), getRow, sqlRowColumns, sqlRowValues)
import Crypto.BCrypt (hashPasswordUsingPolicy, fastBcryptHashingPolicy, validatePassword)
-- }}}
data RangedResult = RangedResult {
rrFrom :: Int
, rrTo :: Int
, rrTotal :: Int
, rrBody :: BL.ByteString
} deriving (Show)
type QuotedSql = (String, [SqlValue])
type Schema = String
type DbRole = BS.ByteString
data LoginAttempt =
NoCredentials
| MalformedAuth
| LoginFailed
| LoginSuccess DbRole
deriving (Eq, Show)
getRows :: Schema -> String -> Net.Query -> Maybe R.NonnegRange -> Connection -> IO RangedResult
getRows schema table qq range conn = do
query <- populateSql conn
$ globalAndLimitedCounts schema table qq <>
jsonArrayRows
(selectStarClause schema table
<> whereClause qq
<> orderClause qq
<> limitClause range)
r <- quickQuery conn query []
return $ case r of
[[total, _, SqlNull]] -> RangedResult offset 0 (fromSql total) "[]"
[[total, limited_total, json]] ->
RangedResult offset (offset + fromSql limited_total - 1)
(fromSql total) (fromSql json)
_ -> RangedResult 0 0 0 "[]"
where
offset = fromMaybe 0 $ R.offset <$> range
whereClause :: Net.Query -> QuotedSql
whereClause qs =
if null qs then ("", []) else (" where ", []) <> conjunction
where
cols = [ col | col <- qs, fst col `notElem` ["order"] ]
conjunction = mconcat $ intersperse (" and ", []) (map wherePred cols)
orderClause :: Net.Query -> QuotedSql
orderClause qs = do
let order = fromMaybe "" $ join $ lookup "order" qs
terms = mapMaybe parseOrderTerm $ splitOn "," $ cs order
termPred = mconcat $ intersperse (", ", []) (map orderTermSql terms)
if null terms
then ("", [])
else (" order by ", []) <> termPred
where
parseOrderTerm :: String -> Maybe OrderTerm
parseOrderTerm s =
case splitOn "." s of
[d,c] ->
if d `elem` ["asc", "desc"]
then Just $ OrderTerm d c
else Nothing
_ -> Nothing
orderTermSql :: OrderTerm -> QuotedSql
orderTermSql t =
("%I " <> otDirection t, [toSql $ otColumn t])
data OrderTerm = OrderTerm {
otDirection :: String
, otColumn :: String
}
wherePred :: Net.QueryItem -> QuotedSql
wherePred (column, predicate) =
("%I " <> op <> "%L", map toSql [column, value])
where
opCode:rest = BS.split '.' $ fromMaybe "." predicate
value = BS.intercalate "." rest
op = case opCode of
"eq" -> "="
"gt" -> ">"
"lt" -> "<"
"gte" -> ">="
"lte" -> "<="
"neq" -> "<>"
_ -> "="
limitClause :: Maybe R.NonnegRange -> QuotedSql
limitClause range =
(" LIMIT %s OFFSET %s ", [toSql limit, toSql offset])
where
limit = fromMaybe "ALL" $ show <$> (R.limit =<< range)
offset = fromMaybe 0 $ R.offset <$> range
globalAndLimitedCounts :: Schema -> String -> Net.Query -> QuotedSql
globalAndLimitedCounts schema table qq =
(" select ", [])
<> ("(select count(1) from %I.%I ", map toSql [schema, table])
<> whereClause qq
<> ("), count(t), ", [])
selectStarClause :: Schema -> String -> QuotedSql
selectStarClause schema table =
(" select * from %I.%I ", map toSql [schema, table])
jsonArrayRows :: QuotedSql -> QuotedSql
jsonArrayRows q =
("array_to_json(array_agg(row_to_json(t))) from (", []) <> q <> (") t", [])
insert :: Schema -> Text -> SqlRow -> Connection -> IO (M.Map String SqlValue)
insert schema table row conn = do
sql <- populateSql conn $ insertClause schema table row
stmt <- prepare conn sql
_ <- execute stmt $ sqlRowValues row
Just m <- fetchRowMap stmt
return m
addUser :: BS.ByteString -> BS.ByteString -> BS.ByteString -> Connection -> IO ()
addUser identity pass role conn = do
hashed <- hashPasswordUsingPolicy fastBcryptHashingPolicy $ cs pass
_ <- insert "dbapi" "auth" (SqlRow [
("id", toSql identity), ("pass", toSql hashed), ("rolname", toSql role)
]) conn
return ()
signInRole :: BS.ByteString -> BS.ByteString -> Connection -> IO LoginAttempt
signInRole user pass conn = do
u <- quickQuery conn "select pass, rolname from dbapi.auth where id = ?" [toSql user]
return $ case u of
[[hashed, role]] ->
if checkPass (fromSql hashed) (cs pass)
then LoginSuccess $ fromSql role
else LoginFailed
_ -> LoginFailed
checkPass :: BS.ByteString -> BS.ByteString -> Bool
checkPass = validatePassword
upsert :: Schema -> Text -> SqlRow -> Net.Query -> Connection -> IO (M.Map String SqlValue)
upsert schema table row qq conn = do
sql <- populateSql conn $ upsertClause schema table row qq
stmt <- prepare conn sql
_ <- execute stmt $ join $ replicate 2 $ sqlRowValues row
Just m <- fetchRowMap stmt
return m
placeholders :: String -> SqlRow -> String
placeholders symbol = intercalate ", " . map (const symbol) . getRow
insertClause :: Schema -> Text -> SqlRow -> QuotedSql
insertClause schema table (SqlRow []) =
("insert into %I.%I default values returning *", [toSql schema, toSql table])
insertClause schema table row =
("insert into %I.%I (" ++ placeholders "%I" row ++ ")",
map toSql $ cs schema : table : sqlRowColumns row)
<> (" values (" ++ placeholders "?" row ++ ") returning *", sqlRowValues row)
insertClauseViaSelect :: Schema -> Text -> SqlRow -> QuotedSql
insertClauseViaSelect schema table row =
("insert into %I.%I (" ++ placeholders "%I" row ++ ")",
map toSql $ cs schema : table : sqlRowColumns row)
<> (" select " ++ placeholders "?" row, sqlRowValues row)
updateClause :: Schema -> Text -> SqlRow -> QuotedSql
updateClause schema table row =
("update %I.%I set (" ++ placeholders "%I" row ++ ")",
map toSql $ cs schema : table : sqlRowColumns row)
<> (" = (" ++ placeholders "?" row ++ ")", [])
upsertClause :: Schema -> Text -> SqlRow -> Net.Query -> QuotedSql
upsertClause schema table row qq =
("with upsert as (", []) <> updateClause schema table row
<> whereClause qq
<> (" returning *) ", []) <> insertClauseViaSelect schema table row
<> (" where not exists (select * from upsert) returning *", [])
populateSql :: Connection -> QuotedSql -> IO String
populateSql conn sql = do
[[escaped]] <- quickQuery conn q (snd sql)
return $ fromSql escaped
where
q = concat [ "select format('", fst sql, "', ", ph (snd sql), ")" ]
ph :: [a] -> String
ph = intercalate ", " . map (const "?::varchar")
setRole :: Connection -> DbRole -> IO ()
setRole conn role = do
query <- populateSql conn ("set role %I", [toSql role])
void $ run conn query []
resetRole :: Connection -> IO ()
resetRole conn = void $ run conn "reset role" []
-193
View File
@@ -1,193 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module PgStructure where
import Data.Functor ( (<$>) )
import Data.Maybe (mapMaybe)
import Control.Applicative ( (<*>) )
import qualified Data.ByteString.Lazy as BL
import Data.List.Split (splitOn)
import qualified Data.Aeson as JSON
import qualified Data.Map as Map
import Database.HDBC hiding (colType, colNullable)
import Database.HDBC.PostgreSQL
import Data.Aeson ((.=))
data Table = Table {
tableSchema :: String
, tableName :: String
, tableInsertable :: Bool
} deriving (Show)
instance JSON.ToJSON Table where
toJSON v = JSON.object [
"schema" .= tableSchema v
, "name" .= tableName v
, "insertable" .= tableInsertable v ]
toBool :: String -> Bool
toBool = (== "YES")
data ForeignKey = ForeignKey {
fkTable::String, fkCol::String
} deriving (Eq, Show)
instance JSON.ToJSON ForeignKey where
toJSON fk = JSON.object ["table".=fkTable fk, "column".=fkCol fk]
foreignKeys :: String -> String -> Connection -> IO (Map.Map String ForeignKey)
foreignKeys schema table conn = do
r <- quickQuery conn
"select kcu.column_name, ccu.table_name AS foreign_table_name,\
\ ccu.column_name AS foreign_column_name \
\from information_schema.table_constraints AS tc \
\ join information_schema.key_column_usage AS kcu \
\ on tc.constraint_name = kcu.constraint_name \
\ join information_schema.constraint_column_usage AS ccu \
\ on ccu.constraint_name = tc.constraint_name \
\where constraint_type = 'FOREIGN KEY' \
\ and tc.table_name=? and tc.table_schema = ? \
\order by kcu.column_name" (map toSql [table, schema])
return $ foldl addKey Map.empty $ map (map fromSql) r
where
addKey m [col, ftab, fcol] = Map.insert col (ForeignKey ftab fcol) m
addKey m _ = m --should never happen
data Column = Column {
colSchema :: String
, colTable :: String
, colName :: String
, colPosition :: Int
, colNullable :: Bool
, colType :: String
, colUpdatable :: Bool
, colMaxLen :: Maybe Int
, colPrecision :: Maybe Int
, colDefault :: Maybe String
, colEnum :: Maybe [String]
, colFK :: Maybe ForeignKey
} deriving (Show)
instance JSON.ToJSON Column where
toJSON c = JSON.object [
"schema" .= colSchema c
, "name" .= colName c
, "position" .= colPosition c
, "nullable" .= colNullable c
, "type" .= colType c
, "updatable" .= colUpdatable c
, "maxLen" .= colMaxLen c
, "precision" .= colPrecision c
, "references".= colFK c
, "default" .= colDefault c
, "enum" .= colEnum c ]
data TableOptions = TableOptions {
tblOptcolumns :: [Column]
, tblOptpkey :: [String]
}
instance JSON.ToJSON TableOptions where
toJSON t = JSON.object [
"columns" .= tblOptcolumns t
, "pkey" .= tblOptpkey t ]
tables :: String -> Connection -> IO [Table]
tables s conn = do
r <- quickQuery conn
"select table_schema, table_name,\
\ is_insertable_into\
\ from information_schema.tables\
\ where table_schema = ?\
\ order by table_name" [toSql s]
return $ mapMaybe mkTable r
where
mkTable [schema, name, insertable] =
Just $ Table (fromSql schema)
(fromSql name)
(toBool (fromSql insertable))
mkTable _ = Nothing
columns :: String -> String -> Connection -> IO [Column]
columns s t conn = do
r <- quickQuery conn
"select info.table_schema as schema, info.table_name as table_name, \
\ info.column_name as name, info.ordinal_position as position, \
\ info.is_nullable as nullable, info.data_type as col_type, \
\ info.is_updatable as updatable, \
\ info.character_maximum_length as max_len, \
\ info.numeric_precision as precision, \
\ info.column_default as default_value, \
\ array_to_string(enum_info.vals, ',') as enum \
\ from ( \
\ select table_schema, table_name, column_name, ordinal_position, \
\ is_nullable, data_type, is_updatable, \
\ character_maximum_length, numeric_precision, \
\ column_default, udt_name \
\ from information_schema.columns \
\ where table_schema = ? and table_name = ? \
\ ) as info \
\ left outer join ( \
\ select n.nspname as s, \
\ t.typname as n, \
\ array_agg(e.enumlabel ORDER BY e.enumsortorder) as vals \
\ from pg_type t \
\ join pg_enum e on t.oid = e.enumtypid \
\ join pg_catalog.pg_namespace n ON n.oid = t.typnamespace \
\ group by s, n \
\ ) as enum_info \
\ on (info.udt_name = enum_info.n) \
\order by position" [toSql s, toSql t]
fks <- foreignKeys s t conn
let lookupFK (_:_:name:_) = Map.lookup (fromSql name) fks
lookupFK _ = Nothing
let cols = zipWith ($) (map mkColumn r) (map lookupFK r)
return cols
where
mkColumn [schema, table, name, pos, nullable, colT, updatable, maxlen, precision, defVal, enum] = Column (fromSql schema)
(fromSql table)
(fromSql name)
(fromSql pos)
(toBool (fromSql nullable))
(fromSql colT)
(toBool (fromSql updatable))
(fromSql maxlen)
(fromSql precision)
(fromSql defVal)
(splitOn "," <$> fromSql enum)
mkColumn _ = error $ "Incomplete column data received for table " ++
t ++ " in schema " ++ s ++ "."
printTables :: String -> Connection -> IO BL.ByteString
printTables schema conn = JSON.encode <$> tables schema conn
printColumns :: String -> String -> Connection -> IO BL.ByteString
printColumns schema table conn =
JSON.encode <$> (TableOptions <$> cols <*> pkey)
where
cols :: IO [Column]
cols = columns schema table conn
pkey :: IO [String]
pkey = primaryKeyColumns schema table conn
primaryKeyColumns :: String -> String -> Connection -> IO [String]
primaryKeyColumns s t conn = do
r <- quickQuery conn
"select kc.column_name \
\ from \
\ information_schema.table_constraints tc, \
\ information_schema.key_column_usage kc \
\where \
\ tc.constraint_type = 'PRIMARY KEY' \
\ and kc.table_name = tc.table_name and kc.table_schema = tc.table_schema \
\ and kc.constraint_name = tc.constraint_name \
\ and kc.table_schema = ? \
\ and kc.table_name = ?" [toSql s, toSql t]
return $ map fromSql (concat r)
+416
View File
@@ -0,0 +1,416 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
module PostgREST.App (
app
, sqlError
, isSqlError
, contentTypeForAccept
, jsonH
, requestedSchema
, TableOptions(..)
) where
import qualified Blaze.ByteString.Builder as BB
import Control.Applicative
import Control.Arrow (second, (***))
import Control.Monad (join)
import Data.Bifunctor (first)
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BL
import Data.CaseInsensitive (original)
import qualified Data.Csv as CSV
import Data.Functor.Identity
import qualified Data.HashMap.Strict as M
import Data.List (find, sortBy)
import Data.Maybe (fromMaybe, isJust, isNothing,
mapMaybe)
import Data.Ord (comparing)
import Data.Ranged.Ranges (emptyRange)
import qualified Data.Set as S
import Data.String.Conversions (cs)
import Data.Text (Text, replace, strip)
import Text.Regex.TDFA ((=~))
import Text.Parsec.Error
import Network.HTTP.Base (urlEncodeVars)
import Network.HTTP.Types.Header
import Network.HTTP.Types.Status
import Network.HTTP.Types.URI (parseSimpleQuery)
import Network.Wai
import Network.Wai.Internal (Response (..))
import Network.Wai.Parse (parseHttpAccept)
import Data.Aeson
import Data.Monoid
import qualified Data.Vector as V
import qualified Hasql as H
import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P
import PostgREST.Auth
import PostgREST.Config (AppConfig (..))
import PostgREST.Parsers
import PostgREST.PgQuery
import PostgREST.PgStructure
import PostgREST.QueryBuilder
import PostgREST.RangeQuery
import PostgREST.Types
import Prelude
app :: DbStructure -> AppConfig -> BL.ByteString -> DbRole -> Request -> H.Tx P.Postgres s Response
app dbstructure conf reqBody dbrole req =
case (path, verb) of
([], _) -> do
let body = encode $ filter (filterTableAcl dbrole) $ filter ((cs schema==).tableSchema) allTabs
return $ responseLBS status200 [jsonH] $ cs body
([table], "OPTIONS") -> do
let cols = filter (filterCol schema table) allCols
pkeys = map pkName $ filter (filterPk schema table) allPrKeys
body = encode (TableOptions cols pkeys)
return $ responseLBS status200 [jsonH, allOrigins] $ cs body
([table], "GET") ->
if range == Just emptyRange
then return $ responseLBS status416 [] "HTTP Range error"
else
case queries of
Left e -> return $ responseLBS status400 [("Content-Type", "application/json")] $ cs e
Right (qs, cqs) -> do
let qt = qualify table
count = if hasPrefer "count=none"
then countNone
else cqs
q = B.Stmt "select " V.empty True <>
parentheticT count
<> commaq <> (
bodyForAccept contentType qt -- TODO! when in csv mode, the first row (columns) is not correct when requesting sub tables
. limitT range
$ qs
)
row <- H.maybeEx q
let (tableTotal, queryTotal, body) = fromMaybe (Just (0::Int), 0::Int, Just "" :: Maybe BL.ByteString) row
to = from+queryTotal-1
contentRange = contentRangeH from to tableTotal
status = rangeStatus from to tableTotal
canonical = urlEncodeVars
. sortBy (comparing fst)
. map (join (***) cs)
. parseSimpleQuery
$ rawQueryString req
return $ responseLBS status
[contentTypeH, contentRange,
("Content-Location",
"/" <> cs table <>
if Prelude.null canonical then "" else "?" <> cs canonical
)
] (fromMaybe "[]" body)
where
from = fromMaybe 0 $ rangeOffset <$> range
apiRequest = first formatParserError (parseGetRequest req)
>>= first formatRelationError . addRelations schema allRels Nothing
>>= addJoinConditions schema allCols
where
formatRelationError :: Text -> Text
formatRelationError e = cs $ encode $ object [
"mesage" .= ("could not find foreign keys between these entities"::String),
"details" .= e]
formatParserError :: ParseError -> Text
formatParserError e = cs $ encode $ object [
"message" .= message,
"details" .= details]
where
message = show (errorPos e)
details = strip $ replace "\n" " " $ cs
$ showErrorMessages "or" "unknown parse error" "expecting" "unexpected" "end of input" (errorMessages e)
query = requestToQuery schema <$> apiRequest
countQuery = requestToCountQuery schema <$> apiRequest
queries = (,) <$> query <*> countQuery
(["postgrest", "users"], "POST") -> do
let user = decode reqBody :: Maybe AuthUser
case user of
Nothing -> return $ responseLBS status400 [jsonH] $
encode . object $ [("message", String "Failed to parse user.")]
Just u -> do
_ <- addUser (cs $ userId u)
(cs $ userPass u) (cs <$> userRole u)
return $ responseLBS status201
[ jsonH
, (hLocation, "/postgrest/users?id=eq." <> cs (userId u))
] ""
(["postgrest", "tokens"], "POST") ->
case jwtSecret of
"secret" -> return $ responseLBS status500 [jsonH] $
encode . object $ [("message", String "JWT Secret is set as \"secret\" which is an unsafe default.")]
_ -> do
let user = decode reqBody :: Maybe AuthUser
case user of
Nothing -> return $ responseLBS status400 [jsonH] $
encode . object $ [("message", String "Failed to parse user.")]
Just u -> do
setRole authenticator
login <- signInRole (cs $ userId u) (cs $ userPass u)
case login of
LoginSuccess role uid ->
return $ responseLBS status201 [ jsonH ] $
encode . object $ [("token", String $ tokenJWT jwtSecret uid role)]
_ -> return $ responseLBS status401 [jsonH] $
encode . object $ [("message", String "Failed authentication.")]
([table], "POST") -> do
let qt = qualify table
echoRequested = hasPrefer "return=representation"
parsed :: Either String (V.Vector Text, V.Vector (V.Vector Value))
parsed = if lookupHeader "Content-Type" == Just csvMT
then do
rows <- CSV.decode CSV.NoHeader reqBody
if V.null rows then Left "CSV requires header"
else Right (V.head rows, (V.map $ V.map $ parseCsvCell . cs) (V.tail rows))
else eitherDecode reqBody >>= \val ->
case val of
Object obj -> Right . second V.singleton . V.unzip . V.fromList $
M.toList obj
_ -> Left "Expecting single JSON object or CSV rows"
case parsed of
Left err -> return $ responseLBS status400 [] $
encode . object $ [("message", String $ "Failed to parse JSON payload. " <> cs err)]
Right toBeInserted -> do
rows :: [Identity Text] <- H.listEx $ uncurry (insertInto qt) toBeInserted
let inserted :: [Object] = mapMaybe (decode . cs . runIdentity) rows
pKeys = map pkName $ filter (filterPk schema table) allPrKeys
responses = flip map inserted $ \obj -> do
let primaries =
if Prelude.null pKeys
then obj
else M.filterWithKey (const . (`elem` pKeys)) obj
let params = urlEncodeVars
$ map (\t -> (cs $ fst t, cs (paramFilter $ snd t)))
$ sortBy (comparing fst) $ M.toList primaries
responseLBS status201
[ jsonH
, (hLocation, "/" <> cs table <> "?" <> cs params)
] $ if echoRequested then encode obj else ""
return $ multipart status201 responses
(["rpc", proc], "POST") -> do
let qi = QualifiedIdentifier schema (cs proc)
exists <- doesProcExist schema proc
if exists
then do
let call = B.Stmt "select " V.empty True <>
asJson (callProc qi $ fromMaybe M.empty (decode reqBody))
body :: Maybe (Identity Text) <- H.maybeEx call
return $ responseLBS status200 [jsonH]
(cs $ fromMaybe "[]" $ runIdentity <$> body)
else return $ responseLBS status404 [] ""
-- check that proc exists
-- check that arg names are all specified
-- select * from "1".proc(a := "foo"::undefined) where whereT limit limitT
([table], "PUT") ->
handleJsonObj reqBody $ \obj -> do
let qt = qualify table
pKeys = map pkName $ filter (filterPk schema table) allPrKeys
specifiedKeys = map (cs . fst) qq
if S.fromList pKeys /= S.fromList specifiedKeys
then return $ responseLBS status405 []
"You must speficy all and only primary keys as params"
else do
let tableCols = map (cs . colName) $ filter (filterCol schema table) allCols
cols = map cs $ M.keys obj
if S.fromList tableCols == S.fromList cols
then do
let vals = M.elems obj
H.unitEx $ iffNotT
(whereT qt qq $ update qt cols vals)
(insertSelect qt cols vals)
return $ responseLBS status204 [ jsonH ] ""
else return $ if Prelude.null tableCols
then responseLBS status404 [] ""
else responseLBS status400 []
"You must specify all columns in PUT request"
([table], "PATCH") ->
handleJsonObj reqBody $ \obj -> do
let qt = qualify table
up = returningStarT
. whereT qt qq
$ update qt (map cs $ M.keys obj) (M.elems obj)
patch = withT up "t" $ B.Stmt
"select count(t), array_to_json(array_agg(row_to_json(t)))::character varying"
V.empty True
row <- H.maybeEx patch
let (queryTotal, body) =
fromMaybe (0 :: Int, Just "" :: Maybe Text) row
r = contentRangeH 0 (queryTotal-1) (Just queryTotal)
echoRequested = hasPrefer "return=representation"
s = case () of _ | queryTotal == 0 -> status404
| echoRequested -> status200
| otherwise -> status204
return $ responseLBS s [ jsonH, r ] $ if echoRequested then cs $ fromMaybe "[]" body else ""
([table], "DELETE") -> do
let qt = qualify table
del = countT
. returningStarT
. whereT qt qq
$ deleteFrom qt
row <- H.maybeEx del
let (Identity deletedCount) = fromMaybe (Identity 0 :: Identity Int) row
return $ if deletedCount == 0
then responseLBS status404 [] ""
else responseLBS status204 [("Content-Range", "*/"<> cs (show deletedCount))] ""
(_, _) ->
return $ responseLBS status404 [] ""
where
allTabs = tables dbstructure
allRels = relations dbstructure
allCols = columns dbstructure
allPrKeys = primaryKeys dbstructure
filterCol sc table (Column{colSchema=s, colTable=t}) = s==sc && table==t
filterCol _ _ _ = False
filterPk sc table pk = sc == pkSchema pk && table == pkTable pk
filterTableAcl :: Text -> Table -> Bool
filterTableAcl r (Table{tableAcl=a}) = r `elem` a
path = pathInfo req
verb = requestMethod req
qq = queryString req
qualify = QualifiedIdentifier schema
hdrs = requestHeaders req
lookupHeader = flip lookup hdrs
hasPrefer val = any (\(h,v) -> h == "Prefer" && v == val) hdrs
accept = lookupHeader hAccept
schema = requestedSchema (cs $ configV1Schema conf) accept
authenticator = cs $ configDbUser conf
jwtSecret = cs $ configJwtSecret conf
range = rangeRequested hdrs
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
contentType = fromMaybe "application/json" $ contentTypeForAccept accept
contentTypeH = (hContentType, contentType)
sqlError :: t
sqlError = undefined
isSqlError :: t
isSqlError = undefined
rangeStatus :: Int -> Int -> Maybe Int -> Status
rangeStatus _ _ Nothing = status200
rangeStatus from to (Just total)
| from > total = status416
| (1 + to - from) < total = status206
| otherwise = status200
contentRangeH :: Int -> Int -> Maybe Int -> Header
contentRangeH from to total =
("Content-Range", cs headerValue)
where
headerValue = rangeString <> "/" <> totalString
rangeString
| totalNotZero && fromInRange = show from <> "-" <> cs (show to)
| otherwise = "*"
totalString = fromMaybe "*" (show <$> total)
totalNotZero = fromMaybe True ((/=) 0 <$> total)
fromInRange = from <= to
requestedSchema :: Text -> Maybe BS.ByteString -> Text
requestedSchema v1schema accept =
case verStr of
Just [[_, ver]] -> if ver == "1" then v1schema else cs ver
_ -> v1schema
where
verRegex = "version[ ]*=[ ]*([0-9]+)" :: BS.ByteString
verStr = (=~ verRegex) <$> accept :: Maybe [[BS.ByteString]]
jsonMT :: BS.ByteString
jsonMT = "application/json"
csvMT :: BS.ByteString
csvMT = "text/csv"
allMT :: BS.ByteString
allMT = "*/*"
jsonH :: Header
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)
-> H.Tx P.Postgres s Response
handleJsonObj reqBody handler = do
let p = eitherDecode reqBody
case p of
Left err ->
return $ responseLBS status400 [jsonH] jErr
where
jErr = encode . object $
[("message", String $ "Failed to parse JSON payload. " <> cs err)]
Right (Object o) -> handler o
Right _ ->
return $ responseLBS status400 [jsonH] jErr
where
jErr = encode . object $
[("message", String "Expecting a JSON object")]
parseCsvCell :: BL.ByteString -> Value
parseCsvCell s = if s == "NULL" then Null else String $ cs s
multipart :: Status -> [Response] -> Response
multipart _ [] = responseLBS status204 [] ""
multipart _ [r] = r
multipart s rs =
responseLBS s [(hContentType, "multipart/mixed; boundary=\"postgrest_boundary\"")] $
BL.intercalate "\n--postgrest_boundary\n" (map renderResponseBody rs)
where
renderHeader :: Header -> BL.ByteString
renderHeader (k, v) = cs (original k) <> ": " <> cs v
renderResponseBody :: Response -> BL.ByteString
renderResponseBody (ResponseBuilder _ headers b) =
BL.intercalate "\n" (map renderHeader headers)
<> "\n\n" <> BB.toLazyByteString b
renderResponseBody _ = error
"Unable to create multipart response from non-ResponseBuilder"
data TableOptions = TableOptions {
tblOptcolumns :: [Column]
, tblOptpkey :: [Text]
}
instance ToJSON TableOptions where
toJSON t = object [
"columns" .= tblOptcolumns t
, "pkey" .= tblOptpkey t ]
+104
View File
@@ -0,0 +1,104 @@
module PostgREST.Auth where
import Control.Applicative
import Control.Monad (mzero)
import Crypto.BCrypt
import Data.Aeson
import Data.Map
import Data.Monoid
import Data.String.Conversions (cs)
import Data.Text
import Data.Maybe (isNothing)
import qualified Data.Vector as V
import qualified Hasql as H
import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P
import PostgREST.PgQuery (pgFmtLit)
import Prelude
import qualified Web.JWT as JWT
import System.IO.Unsafe
data AuthUser = AuthUser {
userId :: String
, userPass :: String
, userRole :: Maybe String
} deriving (Show)
instance FromJSON AuthUser where
parseJSON (Object v) = AuthUser <$>
v .: "id" <*>
v .: "pass" <*>
v .:? "role"
parseJSON _ = mzero
instance ToJSON AuthUser where
toJSON u = object [
"id" .= userId u
, "pass" .= userPass u
, "role" .= userRole u ]
type DbRole = Text
type UserId = Text
data LoginAttempt =
NoCredentials
| MalformedAuth
| LoginFailed
| LoginSuccess DbRole UserId
deriving (Eq, Show)
checkPass :: Text -> Text -> Bool
checkPass = (. cs) . validatePassword . cs
setRole :: Text -> H.Tx P.Postgres s ()
setRole role = H.unitEx $ B.Stmt ("set local role " <> cs (pgFmtLit role)) V.empty True
setUserId :: Text -> H.Tx P.Postgres s ()
setUserId uid =
if uid /= ""
then H.unitEx $ B.Stmt ("set local user_vars.user_id = " <> cs (pgFmtLit uid)) V.empty True
else resetUserId
resetUserId :: H.Tx P.Postgres s ()
resetUserId = H.unitEx [H.stmt|reset user_vars.user_id|]
addUser :: Text -> Text -> Maybe Text -> H.Tx P.Postgres s ()
addUser identity pass role =
H.unitEx $
if isNothing role
then [H.stmt|insert into postgrest.auth (id, pass) values (?, ?)|]
identity hashedText
else [H.stmt|insert into postgrest.auth (id, pass, rolname) values (?, ?, ?)|]
identity hashedText role
where Just hashed = unsafePerformIO $ hashPasswordUsingPolicy fastBcryptHashingPolicy (cs pass)
hashedText = cs hashed :: Text
signInRole :: Text -> Text -> H.Tx P.Postgres s LoginAttempt
signInRole user pass = do
u <- H.maybeEx $ [H.stmt|select id, pass, rolname from postgrest.auth where id = ?|] user
return $ maybe LoginFailed (\r ->
let (uid, hashed, role) = r in
if checkPass hashed pass
then LoginSuccess role uid
else LoginFailed
) u
signInWithJWT :: Text -> Text -> LoginAttempt
signInWithJWT secret input = case maybeRole of
Just (Just (String role)) -> case maybeUserId of
Just (Just (String uid)) -> LoginSuccess (cs role) (cs uid)
_ -> LoginFailed
_ -> LoginFailed
where
maybeRole = (Data.Map.lookup "role" <$> claims) ::Maybe (Maybe Value)
maybeUserId = (Data.Map.lookup "id" <$> claims) ::Maybe (Maybe Value)
claims = JWT.unregisteredClaims <$> JWT.claims <$> decoded
decoded = JWT.decodeAndVerifySignature (JWT.secret secret) input
tokenJWT :: Text -> Text -> Text -> Text
tokenJWT secret uid role = JWT.encodeSigned JWT.HS256 (JWT.secret secret) claimsSet
where
claimsSet = JWT.def {
JWT.unregisteredClaims = Data.Map.fromList [("id", String uid), ("role", String role)]
}
+108
View File
@@ -0,0 +1,108 @@
{-|
Module : PostgREST.Config
Description : Manages PostgREST configuration options.
This module provides a helper function to read the command line arguments using the optparse-applicative
and the AppConfig type to store them.
It also can be used to define other middleware configuration that may be delegated to some sort of
external configuration.
It currently includes a hardcoded CORS policy but this could easly be turned in configurable behaviour if needed.
Other hardcoded options such as the minimum version number also belong here.
-}
module PostgREST.Config ( prettyVersion
, readOptions
, corsPolicy
, minimumPgVersion
, AppConfig (..)
)
where
import Control.Applicative
import qualified Data.ByteString.Char8 as BS
import qualified Data.CaseInsensitive as CI
import Data.List (intercalate)
import Data.String.Conversions (cs)
import Data.Text (strip)
import Data.Version (versionBranch)
import Network.Wai
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
import Options.Applicative hiding (columns)
import Paths_postgrest (version)
import Prelude
-- | Data type to store all command line options
data AppConfig = AppConfig {
configDbName :: String
, configDbPort :: Int
, configDbUser :: String
, configDbPass :: String
, configDbHost :: String
, configPort :: Int
, configAnonRole :: String
, configSecure :: Bool
, configPool :: Int
, configV1Schema :: String
, configJwtSecret :: String
}
argParser :: Parser AppConfig
argParser = AppConfig
<$> strOption (long "db-name" <> short 'd' <> metavar "NAME" <> help "name of database")
<*> option auto (long "db-port" <> short 'P' <> metavar "PORT" <> value 5432 <> help "postgres server port" <> showDefault)
<*> strOption (long "db-user" <> short 'U' <> metavar "ROLE" <> help "postgres authenticator role")
<*> strOption (long "db-pass" <> metavar "PASS" <> value "" <> help "password for authenticator role")
<*> strOption (long "db-host" <> metavar "HOST" <> value "localhost" <> help "postgres server hostname" <> showDefault)
<*> option auto (long "port" <> short 'p' <> metavar "PORT" <> value 3000 <> help "port number on which to run HTTP server" <> showDefault)
<*> strOption (long "anonymous" <> short 'a' <> metavar "ROLE" <> help "postgres role to use for non-authenticated requests")
<*> switch (long "secure" <> short 's' <> help "Redirect all requests to HTTPS")
<*> option auto (long "db-pool" <> metavar "COUNT" <> value 10 <> help "Max connections in database pool" <> showDefault)
<*> strOption (long "v1schema" <> metavar "NAME" <> value "1" <> help "Schema to use for nonspecified version (or explicit v1)" <> showDefault)
<*> strOption (long "jwt-secret" <> metavar "SECRET" <> value "secret" <> help "Secret used to encrypt and decrypt JWT tokens)" <> showDefault)
defaultCorsPolicy :: CorsResourcePolicy
defaultCorsPolicy = CorsResourcePolicy Nothing
["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] ["Authorization"] Nothing
(Just $ 60*60*24) False False True
-- | CORS policy to be used in by Wai Cors middleware
corsPolicy :: Request -> Maybe CorsResourcePolicy
corsPolicy req = case lookup "origin" headers of
Just origin -> Just defaultCorsPolicy {
corsOrigins = Just ([origin], True)
, corsRequestHeaders = "Authentication":accHeaders
, corsExposedHeaders = Just [
"Content-Encoding", "Content-Location", "Content-Range", "Content-Type"
, "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit"
]
}
Nothing -> Nothing
where
headers = requestHeaders req
accHeaders = case lookup "access-control-request-headers" headers of
Just hdrs -> map (CI.mk . cs . strip . cs) $ BS.split ',' hdrs
Nothing -> []
-- | User friendly version number
prettyVersion :: String
prettyVersion = intercalate "." $ map show $ versionBranch version
-- | Function to read and parse options from the command line
readOptions :: IO AppConfig
readOptions = customExecParser parserPrefs opts
where
opts = info (helper <*> argParser) $
fullDesc
<> progDesc (
"PostgREST "
<> prettyVersion
<> " / create a REST API to an existing Postgres database"
)
parserPrefs = prefs showHelpOnError
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
minimumPgVersion :: Integer
minimumPgVersion = 90300
+73
View File
@@ -0,0 +1,73 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
module PostgREST.Error (PgError, errResponse) where
import Data.Aeson ((.=))
import qualified Data.Aeson as JSON
import Data.String.Conversions (cs)
import Data.String.Utils (replace)
import qualified Data.Text as T
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Network.HTTP.Types.Header
import qualified Network.HTTP.Types.Status as HT
import Network.Wai (Response, responseLBS)
type PgError = H.SessionError P.Postgres
errResponse :: PgError -> Response
errResponse e = responseLBS (httpStatus e)
[(hContentType, "application/json")] (JSON.encode e)
instance JSON.ToJSON PgError where
toJSON (H.TxError (P.ErroneousResult c m d h)) = JSON.object [
"code" .= (cs c::T.Text),
"message" .= (cs m::T.Text),
"details" .= (fmap cs d::Maybe T.Text),
"hint" .= (fmap cs h::Maybe T.Text)]
toJSON (H.TxError (P.NoResult d)) = JSON.object [
"message" .= ("No response from server"::T.Text),
"details" .= (fmap cs d::Maybe T.Text)]
toJSON (H.TxError (P.UnexpectedResult m)) = JSON.object ["message" .= m]
toJSON (H.TxError P.NotInTransaction) = JSON.object [
"message" .= ("Not in transaction"::T.Text)]
toJSON (H.CxError (P.CantConnect d)) = JSON.object [
"message" .= ("Can't connect to the database"::T.Text),
"details" .= (fmap cs d::Maybe T.Text)]
toJSON (H.CxError (P.UnsupportedVersion v)) = JSON.object [
"message" .= ("Postgres version "++version++" is not supported") ]
where version = replace "0" "." (show v)
toJSON (H.ResultError m) = JSON.object ["message" .= m]
httpStatus :: PgError -> HT.Status
httpStatus (H.TxError (P.ErroneousResult codeBS _ _ _)) =
let code = cs codeBS in
case code of
'0':'8':_ -> HT.status503 -- pg connection err
'0':'9':_ -> HT.status500 -- triggered action exception
'0':'L':_ -> HT.status403 -- invalid grantor
'0':'P':_ -> HT.status403 -- invalid role specification
'2':'5':_ -> HT.status500 -- invalid tx state
'2':'8':_ -> HT.status403 -- invalid auth specification
'2':'D':_ -> HT.status500 -- invalid tx termination
'3':'8':_ -> HT.status500 -- external routine exception
'3':'9':_ -> HT.status500 -- external routine invocation
'3':'B':_ -> HT.status500 -- savepoint exception
'4':'0':_ -> HT.status500 -- tx rollback
'5':'3':_ -> HT.status503 -- insufficient resources
'5':'4':_ -> HT.status413 -- too complex
'5':'5':_ -> HT.status500 -- obj not on prereq state
'5':'7':_ -> HT.status500 -- operator intervention
'5':'8':_ -> HT.status500 -- system error
'F':'0':_ -> HT.status500 -- conf file error
'H':'V':_ -> HT.status500 -- foreign data wrapper error
'P':'0':_ -> HT.status500 -- PL/pgSQL Error
'X':'X':_ -> HT.status500 -- internal Error
"42P01" -> HT.status404 -- undefined table
"42501" -> HT.status404 -- insufficient privilege
_ -> HT.status400
httpStatus (H.TxError (P.NoResult _)) = HT.status503
httpStatus _ = HT.status500
+97
View File
@@ -0,0 +1,97 @@
module Main where
import PostgREST.PgStructure
import PostgREST.Types
import Network.Wai
import PostgREST.App
import PostgREST.Error (errResponse)
import PostgREST.Middleware
import Control.Monad (unless)
import Control.Monad.IO.Class (liftIO)
import Data.Functor.Identity
import Data.Monoid ((<>))
import Data.String.Conversions (cs)
import Data.Text (Text)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Network.Wai.Handler.Warp hiding (Connection)
import Network.Wai.Middleware.RequestLogger (logStdout)
import System.IO (BufferMode (..),
hSetBuffering, stderr,
stdin, stdout)
import PostgREST.Config (AppConfig (..),
prettyVersion,
readOptions,
minimumPgVersion)
isServerVersionSupported :: H.Session P.Postgres IO Bool
isServerVersionSupported = do
Identity (row :: Text) <- H.tx Nothing $ H.singleEx $ [H.stmt|SHOW server_version_num|]
return $ read (cs row) >= minimumPgVersion
main :: IO ()
main = do
hSetBuffering stdout LineBuffering
hSetBuffering stdin LineBuffering
hSetBuffering stderr NoBuffering
conf <- readOptions
let port = configPort conf
unless (configSecure conf) $
putStrLn "WARNING, running in insecure mode, auth will be in plaintext"
unless ("secret" /= configJwtSecret conf) $
putStrLn "WARNING, running in insecure mode, JWT secret is the default value"
Prelude.putStrLn $ "Listening on port " ++
(show $ configPort conf :: String)
let pgSettings = P.ParamSettings (cs $ configDbHost conf)
(fromIntegral $ configDbPort conf)
(cs $ configDbUser conf)
(cs $ configDbPass conf)
(cs $ configDbName conf)
appSettings = setPort port
. setServerName (cs $ "postgrest/" <> prettyVersion)
$ defaultSettings
middle = logStdout . defaultMiddle (configSecure conf)
poolSettings <- maybe (fail "Improper session settings") return $
H.poolSettings (fromIntegral $ configPool conf) 30
pool :: H.Pool P.Postgres <- H.acquirePool pgSettings poolSettings
supportedOrError <- H.session pool isServerVersionSupported
either (fail . show)
(\supported ->
unless supported $
fail "Cannot run in this PostgreSQL version, PostgREST needs at least 9.2.0"
) supportedOrError
let txSettings = Just (H.ReadCommitted, Just True)
metadata <- H.session pool $ H.tx txSettings $ do
tabs <- allTables
rels <- allRelations
cols <- allColumns rels
keys <- allPrimaryKeys
return (tabs, rels, cols, keys)
dbstructure <- case metadata of
Left e -> fail $ show e
Right (tabs, rels, cols, keys) ->
return DbStructure {
tables=tabs
, columns=cols
, relations=rels
, primaryKeys=keys
}
runSettings appSettings $ middle $ \ req respond -> do
body <- strictRequestBody req
resOrError <- liftIO $ H.session pool $ H.tx txSettings $
authenticated conf (app dbstructure conf body) req
either (respond . errResponse) respond resOrError
+105
View File
@@ -0,0 +1,105 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE ScopedTypeVariables #-}
module PostgREST.Middleware where
import Data.Maybe (fromMaybe, isNothing)
import Data.Monoid
import Data.Text
import Data.String.Conversions (cs)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Network.HTTP.Types (RequestHeaders)
import Network.HTTP.Types.Header (hAccept, hAuthorization,
hLocation)
import Network.HTTP.Types.Status (status301, status400, status401,
status415)
import Network.URI (URI (..), parseURI)
import Network.Wai (Application, Request (..),
Response, isSecure, rawPathInfo,
rawQueryString, requestHeaders,
responseLBS)
import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Middleware.Gzip (def, gzip)
import Network.Wai.Middleware.Static (only, staticPolicy)
import Codec.Binary.Base64.String (decode)
import PostgREST.App (contentTypeForAccept)
import PostgREST.Auth (DbRole, LoginAttempt (..),
setRole, setUserId, signInRole,
signInWithJWT)
import PostgREST.Config (AppConfig (..), corsPolicy)
import Prelude
authenticated :: forall s. AppConfig ->
(DbRole -> Request -> H.Tx P.Postgres s Response) ->
Request -> H.Tx P.Postgres s Response
authenticated conf app req = do
attempt <- httpRequesterRole (requestHeaders req)
case attempt of
MalformedAuth ->
return $ responseLBS status400 [] "Malformed basic auth header"
LoginFailed ->
return $ responseLBS status401 [] "Invalid username or password"
LoginSuccess role uid -> if role /= currentRole then runInRole role uid else app currentRole req
NoCredentials -> if anon /= currentRole then runInRole anon "" else app currentRole req
where
jwtSecret = cs $ configJwtSecret conf
currentRole = cs $ configDbUser conf
anon = cs $ configAnonRole conf
httpRequesterRole :: RequestHeaders -> H.Tx P.Postgres s LoginAttempt
httpRequesterRole hdrs = do
let auth = fromMaybe "" $ lookup hAuthorization hdrs
case split (==' ') (cs auth) of
("Basic" : b64 : _) ->
case split (==':') (cs . decode . cs $ b64) of
(u:p:_) -> signInRole u p
_ -> return MalformedAuth
("Bearer" : jwt : _) ->
return $ signInWithJWT jwtSecret jwt
_ -> return NoCredentials
runInRole :: Text -> Text -> H.Tx P.Postgres s Response
runInRole r uid = do
setUserId uid
setRole r
app r req
redirectInsecure :: Application -> Application
redirectInsecure app req respond = do
let hdrs = requestHeaders req
host = lookup "host" hdrs
uriM = parseURI . cs =<< mconcat [
Just "https://",
host,
Just $ rawPathInfo req,
Just $ rawQueryString req]
isHerokuSecure = lookup "x-forwarded-proto" hdrs == Just "https"
if not (isSecure req || isHerokuSecure)
then case uriM of
Just uri ->
respond $ responseLBS status301 [
(hLocation, cs . show $ uri { uriScheme = "https:" })
] ""
Nothing ->
respond $ responseLBS status400 [] "SSL is required"
else app req respond
unsupportedAccept :: Application -> Application
unsupportedAccept app req respond = do
let
accept = lookup hAccept $ requestHeaders req
if isNothing $ contentTypeForAccept accept
then respond $ responseLBS status415 [] "Unsupported Accept header, try: application/json"
else app req respond
defaultMiddle :: Bool -> Application -> Application
defaultMiddle secure = (if secure then redirectInsecure else id)
. gzip def . cors corsPolicy
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
. unsupportedAccept
+159
View File
@@ -0,0 +1,159 @@
module PostgREST.Parsers
( parseGetRequest
)
where
import Control.Applicative hiding ((<$>))
--lines needed for ghc 7.8
import Data.Functor ((<$>))
import Data.Traversable (traverse)
import Control.Monad (join)
import Data.List (delete, find)
import Data.Maybe
import Data.Monoid
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Tree
import Network.Wai (Request, pathInfo, queryString)
import PostgREST.Types
import Text.ParserCombinators.Parsec hiding (many, (<|>))
parseGetRequest :: Request -> Either ParseError ApiRequest
parseGetRequest httpRequest =
foldr addFilter <$> (addOrder <$> apiRequest <*> ord) <*> flts
where
apiRequest = parse (pRequestSelect rootTableName) ("failed to parse select parameter <<"++selectStr++">>") $ cs selectStr
addOrder (Node r f) o = Node r{order=o} f
flts = mapM pRequestFilter whereFilters
rootTableName = cs $ head $ pathInfo httpRequest -- TODO unsafe head
qString = [(cs k, cs <$> v)|(k,v) <- queryString httpRequest]
orderStr = join $ lookup "order" qString
ord = traverse (parse pOrder ("failed to parse order parameter <<"++fromMaybe "" orderStr++">>")) orderStr
selectStr = fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qString --in case the parametre is missing or empty we default to *
whereFilters = [ (k, fromJust v) | (k,v) <- qString, k `notElem` ["select", "order"], isJust v ]
pRequestSelect :: Text -> Parser ApiRequest
pRequestSelect rootNodeName = do
fieldTree <- pFieldForest
return $ foldr treeEntry (Node (Select rootNodeName [] [] [] Nothing Nothing) []) fieldTree
where
treeEntry :: Tree SelectItem -> ApiRequest -> ApiRequest
treeEntry (Node fld@((fn, _),_) fldForest) (Node rNode rForest) =
case fldForest of
[] -> Node (rNode {fields=fld:fields rNode}) rForest
_ -> Node rNode (foldr treeEntry (Node (Select fn [] [] [] Nothing Nothing) []) fldForest:rForest)
pRequestFilter :: (String, String) -> Either ParseError (Path, Filter)
pRequestFilter (k, v) = (,) <$> path <*> (Filter <$> fld <*> op <*> val)
where
treePath = parse pTreePath ("failed to parser tree path (" ++ k ++ ")") k
opVal = parse pOpValueExp ("failed to parse filter (" ++ v ++ ")") v
path = fst <$> treePath
fld = snd <$> treePath
op = fst <$> opVal
val = snd <$> opVal
addFilter :: (Path, Filter) -> ApiRequest -> ApiRequest
addFilter ([], flt) (Node rn@(Select {filters=flts}) forest) = Node (rn {filters=flt:flts}) forest
addFilter (path, flt) (Node rn forest) =
case targetNode of
Nothing -> Node rn forest -- the filter is silenty dropped in the Request does not contain the required path
Just tn -> Node rn (addFilter (remainingPath, flt) tn:restForest)
where
targetNodeName:remainingPath = path
(targetNode,restForest) = splitForest targetNodeName forest
splitForest name forst =
case maybeNode of
Nothing -> (Nothing,forest)
Just node -> (Just node, delete node forest)
where maybeNode = find ((name==).mainTable.rootLabel) forst
ws :: Parser Text
ws = cs <$> many (oneOf " \t")
lexeme :: Parser a -> Parser a
lexeme p = ws *> p <* ws
pTreePath :: Parser (Path,Field)
pTreePath = do
p <- pFieldName `sepBy1` pDelimiter
jp <- optionMaybe ( string "->" >> pJsonPath)
let pp = map cs p
jpp = map cs <$> jp
return (init pp, (last pp, jpp))
where
pFieldForest :: Parser [Tree SelectItem]
pFieldForest = pFieldTree `sepBy1` lexeme (char ',')
pFieldTree :: Parser (Tree SelectItem)
pFieldTree = try (Node <$> pSelect <*> ( char '(' *> pFieldForest <* char ')'))
<|> Node <$> pSelect <*> pure []
pStar :: Parser Text
pStar = cs <$> (string "*" *> pure ("*"::String))
pFieldName :: Parser Text
pFieldName = cs <$> (many1 (letter <|> digit <|> oneOf "_")
<?> "field name (* or [a..z0..9_])")
pJsonPathDelimiter :: Parser Text
pJsonPathDelimiter = cs <$> (try (string "->>") <|> string "->")
pJsonPath :: Parser [Text]
pJsonPath = pFieldName `sepBy1` pJsonPathDelimiter
pField :: Parser Field
pField = lexeme $ (,) <$> pFieldName <*> optionMaybe ( pJsonPathDelimiter *> pJsonPath)
pSelect :: Parser SelectItem
pSelect = lexeme $
try ((,) <$> pField <*>((cs <$>) <$> optionMaybe (string "::" *> many letter)) )
<|> do
s <- pStar
return ((s, Nothing), Nothing)
pOperator :: Parser Operator
pOperator = cs <$> ( try (string "lte") -- has to be before lt
<|> try (string "lt")
<|> try (string "eq")
<|> try (string "gte") -- has to be before gh
<|> try (string "gt")
<|> try (string "lt")
<|> try (string "neq")
<|> try (string "like")
<|> try (string "ilike")
<|> try (string "in")
<|> try (string "notin")
<|> try (string "is" )
<|> try (string "isnot")
<|> try (string "@@")
<?> "operator (eq, gt, ...)"
)
pValue :: Parser FValue
pValue = VText <$> (cs <$> many anyChar)
pDelimiter :: Parser Char
pDelimiter = char '.' <?> "delimiter (.)"
pOperatiorWithNegation :: Parser Operator
pOperatiorWithNegation = try ( (<>) <$> ( cs <$> string "not." ) <*> pOperator) <|> pOperator
pOpValueExp :: Parser (Operator, FValue)
pOpValueExp = (,) <$> pOperatiorWithNegation <*> (pDelimiter *> pValue)
pOrder :: Parser [OrderTerm]
pOrder = lexeme pOrderTerm `sepBy` char ','
pOrderTerm :: Parser OrderTerm
pOrderTerm =
try ( do
c <- pFieldName
_ <- pDelimiter
d <- string "asc" <|> string "desc"
nls <- optionMaybe (pDelimiter *> ( try(string "nullslast" *> pure ("nulls last"::String)) <|> try(string "nullsfirst" *> pure ("nulls first"::String))))
return $ OrderTerm (cs c) (cs d) (cs <$> nls)
)
<|> OrderTerm <$> (cs <$> pFieldName) <*> pure "asc" <*> pure Nothing
+306
View File
@@ -0,0 +1,306 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module PostgREST.PgQuery where
import qualified Hasql as H
import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P
import PostgREST.RangeQuery
import PostgREST.Types (OrderTerm (..), QualifiedIdentifier(..))
import Control.Monad (join)
import qualified Data.Aeson as JSON
import qualified Data.ByteString.Char8 as BS
import Data.Functor
import qualified Data.HashMap.Strict as H
import qualified Data.List as L
import Data.Maybe (fromMaybe)
import Data.Monoid
import Data.Scientific (FPFormat (..), formatScientific,
isInteger)
import Data.String.Conversions (cs)
import qualified Data.Text as T
import Data.Vector (empty)
import qualified Data.Vector as V
import qualified Network.HTTP.Types.URI as Net
import Text.Regex.TDFA ((=~))
import Prelude
type PStmt = H.Stmt P.Postgres
instance Monoid PStmt where
mappend (B.Stmt query params prep) (B.Stmt query' params' prep') =
B.Stmt (query <> query') (params <> params') (prep && prep')
mempty = B.Stmt "" empty True
type StatementT = PStmt -> PStmt
limitT :: Maybe NonnegRange -> StatementT
limitT r q =
q <> B.Stmt (" LIMIT " <> limit <> " OFFSET " <> offset <> " ") empty True
where
limit = maybe "ALL" (cs . show) $ join $ rangeLimit <$> r
offset = cs . show $ fromMaybe 0 $ rangeOffset <$> r
whereT :: QualifiedIdentifier -> Net.Query -> StatementT
whereT table params q =
if L.null cols
then q
else q <> B.Stmt " where " empty True <> conjunction
where
cols = [ col | col <- params, fst col `notElem` ["order","select"] ]
wherePredTable = wherePred table
conjunction = mconcat $ L.intersperse andq (map wherePredTable cols)
withT :: PStmt -> T.Text -> StatementT
withT (B.Stmt eq ep epre) v (B.Stmt wq wp wpre) =
B.Stmt ("WITH " <> v <> " AS (" <> eq <> ") " <> wq <> " from " <> v)
(ep <> wp)
(epre && wpre)
orderT :: [OrderTerm] -> StatementT
orderT ts q =
if L.null ts
then q
else q <> B.Stmt " order by " empty True <> clause
where
clause = mconcat $ L.intersperse commaq (map queryTerm ts)
queryTerm :: OrderTerm -> PStmt
queryTerm t = B.Stmt
(" " <> cs (pgFmtIdent $ otTerm t) <> " "
<> cs (otDirection t) <> " "
<> maybe "" cs (otNullOrder t) <> " ")
empty True
parentheticT :: StatementT
parentheticT s =
s { B.stmtTemplate = " (" <> B.stmtTemplate s <> ") " }
iffNotT :: PStmt -> StatementT
iffNotT (B.Stmt aq ap apre) (B.Stmt bq bp bpre) =
B.Stmt
("WITH aaa AS (" <> aq <> " returning *) " <>
bq <> " WHERE NOT EXISTS (SELECT * FROM aaa)")
(ap <> bp)
(apre && bpre)
countT :: StatementT
countT s =
s { B.stmtTemplate = "WITH qqq AS (" <> B.stmtTemplate s <> ") SELECT pg_catalog.count(1) FROM qqq" }
countRows :: QualifiedIdentifier -> PStmt
countRows t = B.Stmt ("select pg_catalog.count(1) from " <> fromQi t) empty True
countNone :: PStmt
countNone = B.Stmt "select null" 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 = withCount . asJson
asJson :: StatementT
asJson s = s {
B.stmtTemplate =
"array_to_json(array_agg(row_to_json(t)))::character varying from ("
<> B.stmtTemplate s <> ") t" }
withCount :: StatementT
withCount s = s { B.stmtTemplate = "pg_catalog.count(t), " <> B.stmtTemplate s }
asJsonRow :: StatementT
asJsonRow s = s { B.stmtTemplate = "row_to_json(t) from (" <> B.stmtTemplate s <> ") t" }
returningStarT :: StatementT
returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" }
deleteFrom :: QualifiedIdentifier -> PStmt
deleteFrom t = B.Stmt ("delete from " <> fromQi t) empty True
insertInto :: QualifiedIdentifier
-> V.Vector T.Text
-> V.Vector (V.Vector JSON.Value)
-> PStmt
insertInto t cols vals
| V.null cols = B.Stmt ("insert into " <> fromQi t <> " default values returning *") empty True
| otherwise = B.Stmt
("insert into " <> fromQi t <> " (" <>
T.intercalate ", " (V.toList $ V.map pgFmtIdent cols) <>
") values "
<> T.intercalate ", "
(V.toList $ V.map (\v -> "("
<> T.intercalate ", " (V.toList $ V.map insertableValue v)
<> ")"
) vals
)
<> " returning row_to_json(" <> fromQi t <> ".*)")
empty True
insertSelect :: QualifiedIdentifier -> [T.Text] -> [JSON.Value] -> PStmt
insertSelect t [] _ = B.Stmt
("insert into " <> fromQi t <> " default values returning *") empty True
insertSelect t cols vals = B.Stmt
("insert into " <> fromQi t <> " ("
<> T.intercalate ", " (map pgFmtIdent cols)
<> ") select "
<> T.intercalate ", " (map insertableValue vals))
empty True
update :: QualifiedIdentifier -> [T.Text] -> [JSON.Value] -> PStmt
update t cols vals = B.Stmt
("update " <> fromQi t <> " set ("
<> T.intercalate ", " (map pgFmtIdent cols)
<> ") = ("
<> T.intercalate ", " (map insertableValue vals)
<> ")")
empty True
callProc :: QualifiedIdentifier -> JSON.Object -> PStmt
callProc qi params = do
let args = T.intercalate "," $ map assignment (H.toList params)
B.Stmt ("select * from " <> fromQi qi <> "(" <> args <> ")") empty True
where
assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v
wherePred :: QualifiedIdentifier -> Net.QueryItem -> PStmt
wherePred table (col, predicate) =
B.Stmt (notOp <> " " <> pgFmtJsonbPath table (cs col) <> " " <> op <> " " <>
if opCode `elem` ["is","isnot"] then whiteList value
else cs sqlValue)
empty True
where
headPredicate:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
hasNot caseTrue caseFalse = if headPredicate == "not" then caseTrue else caseFalse
opCode = hasNot (head rest) headPredicate
notOp = hasNot headPredicate ""
value = hasNot (T.intercalate "." $ tail rest) (T.intercalate "." rest)
sqlValue = pgFmtValue opCode value
op = pgFmtOperator opCode
whiteList :: T.Text -> T.Text
whiteList val = fromMaybe
(cs (pgFmtLit val) <> "::unknown ")
(L.find ((==) . T.toLower $ val) ["null","true","false"])
pgFmtValue :: T.Text -> T.Text -> T.Text
pgFmtValue opCode value =
case opCode of
"like" -> unknownLiteral $ T.map star value
"ilike" -> unknownLiteral $ T.map star value
"in" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"notin" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"@@" -> "to_tsquery(" <> unknownLiteral value <> ") "
_ -> unknownLiteral value
where
star c = if c == '*' then '%' else c
unknownLiteral = (<> "::unknown ") . pgFmtLit
pgFmtOperator :: T.Text -> T.Text
pgFmtOperator opCode =
case opCode of
"eq" -> "="
"gt" -> ">"
"lt" -> "<"
"gte" -> ">="
"lte" -> "<="
"neq" -> "<>"
"like"-> "like"
"ilike"-> "ilike"
"in" -> "in"
"notin" -> "not in"
"is" -> "is"
"isnot" -> "is not"
"@@" -> "@@"
_ -> "="
commaq :: PStmt
commaq = B.Stmt ", " empty True
andq :: PStmt
andq = B.Stmt " and " empty True
data JsonbPath =
ColIdentifier T.Text
| KeyIdentifier T.Text
| SingleArrow JsonbPath JsonbPath
| DoubleArrow JsonbPath JsonbPath
deriving (Show)
parseJsonbPath :: T.Text -> Maybe JsonbPath
parseJsonbPath p =
case T.splitOn "->>" p of
[a,b] ->
let i:is = T.splitOn "->" a in
Just $ DoubleArrow
(foldl SingleArrow (ColIdentifier i) (map KeyIdentifier is))
(KeyIdentifier b)
_ -> Nothing
pgFmtJsonbPath :: QualifiedIdentifier -> T.Text -> T.Text
pgFmtJsonbPath table p =
pgFmtJsonbPath' $ fromMaybe (ColIdentifier p) (parseJsonbPath p)
where
pgFmtJsonbPath' (ColIdentifier i) = fromQi table <> "." <> pgFmtIdent i
pgFmtJsonbPath' (KeyIdentifier i) = pgFmtLit i
pgFmtJsonbPath' (SingleArrow a b) =
pgFmtJsonbPath' a <> "->" <> pgFmtJsonbPath' b
pgFmtJsonbPath' (DoubleArrow a b) =
pgFmtJsonbPath' a <> "->>" <> pgFmtJsonbPath' b
pgFmtIdent :: T.Text -> T.Text
pgFmtIdent x =
let escaped = T.replace "\"" "\"\"" (trimNullChars $ cs x) in
if (cs escaped :: BS.ByteString) =~ danger
then "\"" <> escaped <> "\""
else escaped
where danger = "^$|^[^a-z_]|[^a-z_0-9]" :: BS.ByteString
pgFmtLit :: T.Text -> T.Text
pgFmtLit x =
let trimmed = trimNullChars x
escaped = "'" <> T.replace "'" "''" trimmed <> "'"
slashed = T.replace "\\" "\\\\" escaped in
if T.isInfixOf "\\\\" escaped
then "E" <> slashed
else slashed
trimNullChars :: T.Text -> T.Text
trimNullChars = T.takeWhile (/= '\x0')
fromQi :: QualifiedIdentifier -> T.Text
fromQi t = pgFmtIdent (qiSchema t) <> "." <> pgFmtIdent (qiName t)
unquoted :: JSON.Value -> T.Text
unquoted (JSON.String t) = t
unquoted (JSON.Number n) =
cs $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
unquoted (JSON.Bool b) = cs . show $ b
unquoted v = cs $ JSON.encode v
insertableText :: T.Text -> T.Text
insertableText = (<> "::unknown") . pgFmtLit
insertableValue :: JSON.Value -> T.Text
insertableValue JSON.Null = "null"
insertableValue v = insertableText $ unquoted v
paramFilter :: JSON.Value -> T.Text
paramFilter JSON.Null = "is.null"
paramFilter v = "eq." <> unquoted v
+264
View File
@@ -0,0 +1,264 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeSynonymInstances #-}
module PostgREST.PgStructure where
import Control.Applicative
import Control.Monad (join)
import Data.Functor.Identity
import Data.List (elemIndex, find)
import Data.Maybe (fromMaybe, isJust, mapMaybe)
import Data.Monoid
import Data.Text (Text, split)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import PostgREST.PgQuery ()
import PostgREST.Types
import GHC.Exts (groupWith)
import Prelude
doesProcExist :: Text -> Text -> H.Tx P.Postgres s Bool
doesProcExist schema proc = do
row :: Maybe (Identity Int) <- H.maybeEx $ [H.stmt|
SELECT 1
FROM pg_catalog.pg_namespace n
JOIN pg_catalog.pg_proc p
ON pronamespace = n.oid
WHERE nspname = ?
AND proname = ?
|] schema proc
return $ isJust row
tableFromRow :: (Text, Text, Bool, Maybe Text) -> Table
tableFromRow (s, n, i, a) = Table s n i (parseAcl a)
where
parseAcl :: Maybe Text -> [Text]
parseAcl str = fromMaybe [] $ split (==',') <$> str
columnFromRow :: (Text, Text, Text,
Int, Bool, Text,
Bool, Maybe Int, Maybe Int,
Maybe Text, Maybe Text)
-> Column
columnFromRow (s, t, n, pos, nul, typ, u, l, p, d, e) =
Column s t n pos nul typ u l p d (parseEnum e) Nothing
where
parseEnum :: Maybe Text -> [Text]
parseEnum str = fromMaybe [] $ split (==',') <$> str
relationFromRow :: (Text, Text, [Text], Text, [Text]) -> Relation
relationFromRow (s, t, cs, ft, fcs) = Relation s t cs ft fcs Child Nothing Nothing Nothing
pkFromRow :: (Text, Text, Text) -> PrimaryKey
pkFromRow (s, t, n) = PrimaryKey s t n
addParentRelation :: Relation -> [Relation] -> [Relation]
addParentRelation rel@(Relation s t c ft fc _ _ _ _) rels = Relation s ft fc t c Parent Nothing Nothing Nothing:rel:rels
allTables :: H.Tx P.Postgres s [Table]
allTables = do
rows <- H.listEx $ [H.stmt|
SELECT
n.nspname AS table_schema,
c.relname AS table_name,
c.relkind = 'r' OR (c.relkind IN ('v','f'))
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8
OR (EXISTS
( SELECT 1
FROM pg_trigger
WHERE pg_trigger.tgrelid = c.oid
AND (pg_trigger.tgtype::integer & 69) = 69) ) AS insertable,
array_to_string(array_agg(r.rolname), ',') AS acl
FROM pg_class c
CROSS JOIN pg_roles r
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('v','r','m')
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
AND (
pg_has_role(r.rolname, c.relowner, 'USAGE'::text) OR
has_table_privilege(r.rolname, c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR
has_any_column_privilege(r.rolname, c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text) )
GROUP BY table_schema, table_name, insertable
ORDER BY table_schema, table_name
|]
return $ map tableFromRow rows
allRelations :: H.Tx P.Postgres s [Relation]
allRelations = do
rels <- H.listEx $ [H.stmt|
WITH table_fk AS (
SELECT ns.nspname AS table_schema,
tab.relname AS table_name,
column_info.cols AS columns,
other.relname AS foreign_table_name,
column_info.refs AS foreign_columns
FROM pg_constraint,
LATERAL (SELECT array_agg(cols.attname) AS cols,
array_agg(cols.attnum) AS nums,
array_agg(refs.attname) AS refs
FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k,
LATERAL (SELECT * FROM pg_attribute
WHERE attrelid = conrelid AND attnum = col)
AS cols,
LATERAL (SELECT * FROM pg_attribute
WHERE attrelid = confrelid AND attnum = ref)
AS refs)
AS column_info,
LATERAL (SELECT * FROM pg_namespace
WHERE pg_namespace.oid = connamespace) AS ns,
LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = conrelid) AS tab,
LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other
WHERE confrelid != 0
ORDER BY (conrelid, column_info.nums)
)
SELECT * FROM table_fk
UNION
(
SELECT
vcu.table_schema,
vcu.view_name AS table_name,
array_agg(vcu.column_name::text) AS columns,
table_fk.foreign_table_name,
table_fk.foreign_columns
FROM information_schema.view_column_usage as vcu
JOIN table_fk ON
table_fk.table_schema = vcu.view_schema AND
table_fk.table_name = vcu.table_name AND
vcu.column_name = ANY (table_fk.columns)
WHERE vcu.view_schema NOT IN ('pg_catalog', 'information_schema')
AND columns = table_fk.columns
GROUP BY vcu.table_schema, vcu.view_name, table_fk.foreign_table_name, table_fk.foreign_columns
)
UNION
(
SELECT
vcu.view_schema as table_schema,
table_fk.table_name,
table_fk.columns,
vcu.view_name as foreign_table_name,
array_agg(vcu.column_name::text) as foreign_columns
FROM information_schema.view_column_usage as vcu
JOIN table_fk ON
table_fk.table_schema = vcu.view_schema AND
table_fk.foreign_table_name = vcu.table_name AND
vcu.column_name = ANY (table_fk.foreign_columns)
WHERE vcu.view_schema NOT IN ('pg_catalog', 'information_schema')
AND foreign_columns = table_fk.foreign_columns
GROUP BY vcu.view_schema, table_fk.table_name, vcu.view_name, table_fk.columns
)
|]
let simpleRelations = foldr (addParentRelation.relationFromRow) [] rels
let links = filter ((==2).length) $ groupWith groupFn $ filter ( (==Child). relType) simpleRelations
return $ simpleRelations ++ mapMaybe link2Relation links
where
groupFn :: Relation -> Text
groupFn (Relation{relSchema=s, relTable=t}) = s<>"_"<>t
link2Relation [
Relation{relSchema=sc, relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c},
Relation{ relColumns=lc2, relFTable=ft, relFColumns=fc}
] = Just $ Relation sc t c ft fc Many (Just lt) (Just lc1) (Just lc2)
link2Relation _ = Nothing
allColumns :: [Relation] -> H.Tx P.Postgres s [Column]
allColumns rels = do
cols <- H.listEx $ [H.stmt|
SELECT
info.table_schema AS schema,
info.table_name AS table_name,
info.column_name AS name,
info.ordinal_position AS position,
info.is_nullable::boolean AS nullable,
info.data_type AS col_type,
info.is_updatable::boolean AS updatable,
info.character_maximum_length AS max_len,
info.numeric_precision AS precision,
info.column_default AS default_value,
array_to_string(enum_info.vals, ',') AS enum
FROM (
SELECT
table_schema,
table_name,
column_name,
ordinal_position,
is_nullable,
data_type,
is_updatable,
character_maximum_length,
numeric_precision,
column_default,
udt_name
FROM information_schema.columns
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
) AS info
LEFT OUTER JOIN (
SELECT
n.nspname AS s,
t.typname AS n,
array_agg(e.enumlabel ORDER BY e.enumsortorder) AS vals
FROM pg_type t
JOIN pg_enum e ON t.oid = e.enumtypid
JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
GROUP BY s,n
) AS enum_info ON (info.udt_name = enum_info.n)
ORDER BY schema, position
|]
return $ map (addFK . columnFromRow) cols
where
addFK col = col { colFK = fk col }
fk col = join $ relToFk (colName col) <$> find (lookupFn col) rels
lookupFn :: Column -> Relation -> Bool
lookupFn (Column{colSchema=cs, colTable=ct, colName=cn}) (Relation{relSchema=rs, relTable=rt, relColumns=rc, relType=rty}) =
cs==rs && ct==rt && cn `elem` rc && rty==Child
lookupFn _ _ = False
relToFk cName (Relation{relFTable=t, relColumns=cs, relFColumns=fcs}) = ForeignKey t <$> c
where
pos = elemIndex cName cs
c = (fcs !!) <$> pos
allPrimaryKeys :: H.Tx P.Postgres s [PrimaryKey]
allPrimaryKeys = do
pks <- H.listEx $ [H.stmt|
WITH table_pk AS (
SELECT
kc.table_schema,
kc.table_name,
kc.column_name
FROM
information_schema.table_constraints tc,
information_schema.key_column_usage kc
WHERE
tc.constraint_type = 'PRIMARY KEY' AND
kc.table_name = tc.table_name AND
kc.table_schema = tc.table_schema AND
kc.constraint_name = tc.constraint_name AND
kc.table_schema NOT IN ('pg_catalog', 'information_schema')
)
SELECT table_schema,
table_name,
column_name
FROM table_pk
UNION (
SELECT
vcu.view_schema,
vcu.view_name,
vcu.column_name
FROM information_schema.view_column_usage AS vcu
JOIN
table_pk ON table_pk.table_schema = vcu.view_schema AND
table_pk.table_name = vcu.table_name AND
table_pk.column_name = vcu.column_name
WHERE vcu.view_schema NOT IN ('pg_catalog','information_schema')
)
|]
return $ map pkFromRow pks
+165
View File
@@ -0,0 +1,165 @@
module PostgREST.QueryBuilder
where
import Control.Error
import Data.List (find)
import Data.Monoid
import Data.Text hiding (filter, find, foldr, head, last, map,
null, zipWith)
import Control.Applicative
import Data.Tree
import PostgREST.PgQuery (PStmt, fromQi,
orderT, pgFmtIdent, pgFmtLit, pgFmtOperator,
pgFmtValue, whiteList)
import PostgREST.Types
import qualified Data.Vector as V (empty)
import qualified Hasql.Backend as B
findRelation :: [Relation] -> Text -> Text -> Text -> Maybe Relation
findRelation allRelations s t1 t2 =
find (\r -> s == relSchema r && t1 == relTable r && t2 == relFTable r) allRelations
addRelations :: Text -> [Relation] -> Maybe ApiRequest -> ApiRequest -> Either Text ApiRequest
addRelations schema allRelations parentNode node@(Node query@(Select {mainTable=table}) forest) =
case parentNode of
Nothing -> Node query{relation=Nothing} <$> updatedForest
(Just (Node (Select{mainTable=parentTable}) _)) -> Node <$> (addRel query <$> rel) <*> updatedForest
where
rel = note ("no relation between " <> table <> " and " <> parentTable)
$ findRelation allRelations schema table parentTable
<|> findRelation allRelations schema parentTable table
addRel :: Query -> Relation -> Query
addRel q r = q{relation = Just r}
where
updatedForest = mapM (addRelations schema allRelations (Just node)) forest
getJoinConditions :: Relation -> [Filter]
getJoinConditions (Relation s t cs ft fcs typ lt lc1 lc2) =
case typ of
Child -> zipWith (toFilter t ft) cs fcs
Parent -> zipWith (toFilter t ft) cs fcs
Many -> zipWith (toFilter t (fromMaybe "" lt)) cs (fromMaybe [] lc1) ++ zipWith (toFilter ft (fromMaybe "" lt)) fcs (fromMaybe [] lc2)
where
toFilter :: Text -> Text -> FieldName -> FieldName -> Filter
toFilter tb ftb c fc = Filter (c, Nothing) "=" (VForeignKey (QualifiedIdentifier s tb) (ForeignKey ftb fc))
addJoinConditions :: Text -> [Column] -> ApiRequest -> Either Text ApiRequest
addJoinConditions schema allColumns (Node query@(Select{relation=r}) forest) =
case r of
Nothing -> Node updatedQuery <$> updatedForest -- this is the root node
Just rel@(Relation{relType=Child}) -> Node (addCond updatedQuery (getJoinConditions rel)) <$> updatedForest
Just (Relation{relType=Parent}) -> Node updatedQuery <$> updatedForest
Just rel@(Relation{relType=Many, relLTable=(Just linkTable)}) ->
Node <$> pure qq <*> updatedForest
where
q = addCond updatedQuery (getJoinConditions rel)
qq = q{joinTables=linkTable:joinTables q}
_ -> Left "unknow relation"
where
-- add parentTable and parentJoinConditions to the query
updatedQuery = foldr (flip addCond) (query{joinTables = parentTables ++ joinTables query}) parentJoinConditions
where
parentJoinConditions = map (getJoinConditions.snd) parents
parentTables = map fst parents
parents = mapMaybe (getParents.rootLabel) forest
getParents qq@(Select{relation=(Just rel@(Relation{relType=Parent}))}) = Just (mainTable qq, rel)
getParents _ = Nothing
updatedForest = mapM (addJoinConditions schema allColumns) forest
addCond q con = q{filters=con ++ filters q}
requestToCountQuery :: Text -> ApiRequest -> PStmt
requestToCountQuery schema (Node (Select mainTbl _ _ conditions _ _) _) =
B.Stmt query V.empty True
where
query = Data.Text.unwords [
"SELECT pg_catalog.count(1)",
"FROM ", fromQi $ QualifiedIdentifier schema mainTbl,
("WHERE " <> intercalate " AND " ( map (pgFmtCondition (QualifiedIdentifier schema mainTbl)) localConditions )) `emptyOnNull` localConditions
]
emptyOnNull val x = if null x then "" else val
localConditions = filter fn conditions
where
fn (Filter{value=VText _}) = True
fn (Filter{value=VForeignKey _ _}) = False
requestToQuery :: Text -> ApiRequest -> PStmt
requestToQuery schema (Node (Select mainTbl colSelects tbls conditions ord _) forest) =
orderT (fromMaybe [] ord) query
where
query = B.Stmt qStr V.empty True
qStr = Data.Text.unwords [
("WITH " <> intercalate ", " withs) `emptyOnNull` withs,
"SELECT ", intercalate ", " (map (pgFmtSelectItem (QualifiedIdentifier schema mainTbl)) colSelects ++ selects),
"FROM ", intercalate ", " (map (fromQi . QualifiedIdentifier schema) (mainTbl:tbls)),
("WHERE " <> intercalate " AND " ( map (pgFmtCondition (QualifiedIdentifier schema mainTbl) ) conditions )) `emptyOnNull` conditions
]
emptyOnNull val x = if null x then "" else val
(withs, selects) = foldr getQueryParts ([],[]) forest
getQueryParts :: Tree Query -> ([Text], [Text]) -> ([Text], [Text])
getQueryParts (Node q@(Select{mainTable=table, relation=(Just (Relation {relType=Child}))}) forst) (w,s) = (w,sel:s)
where
sel = "("
<> "SELECT array_to_json(array_agg(row_to_json("<>table<>"))) "
<> "FROM (" <> subquery <> ") " <> table
<> ") AS " <> table
where (B.Stmt subquery _ _) = requestToQuery schema (Node q forst)
getQueryParts (Node q@(Select{mainTable=table, relation=(Just (Relation{relType=Parent}))}) forst) (w,s) = (wit:w,sel:s)
where
sel = "row_to_json(" <> table <> ".*) AS "<>table --TODO must be singular
wit = table <> " AS ( " <> subquery <> " )"
where (B.Stmt subquery _ _) = requestToQuery schema (Node q forst)
getQueryParts (Node q@(Select{mainTable=table, relation=(Just (Relation {relType=Many}))}) forst) (w,s) = (w,sel:s)
where
sel = "("
<> "SELECT array_to_json(array_agg(row_to_json("<>table<>"))) "
<> "FROM (" <> subquery <> ") " <> table
<> ") AS " <> table
where (B.Stmt subquery _ _) = requestToQuery schema (Node q forst)
-- the following is just to remove the warning
--getQueryParts is not total but requestToQuery is called only after addJoinConditions which ensures the only
--posible relations are Child Parent Many
getQueryParts (Node (Select{relation=Nothing}) _) _ = undefined
pgFmtCondition :: QualifiedIdentifier -> Filter -> Text
pgFmtCondition table (Filter (col,jp) ops val) =
notOp <> " " <> sqlCol <> " " <> pgFmtOperator opCode <> " " <>
if opCode `elem` ["is","isnot"] then whiteList (getInner val) else sqlValue
where
headPredicate:rest = split (=='.') ops
hasNot caseTrue caseFalse = if headPredicate == "not" then caseTrue else caseFalse
opCode = hasNot (head rest) headPredicate
notOp = hasNot headPredicate ""
sqlCol = case val of
VText _ -> pgFmtColumn table col <> pgFmtJsonPath jp
VForeignKey qi _ -> pgFmtColumn qi col
sqlValue = valToStr val
getInner v = case v of
VText s -> s
_ -> ""
valToStr v = case v of
VText s -> pgFmtValue opCode s
VForeignKey (QualifiedIdentifier s _) (ForeignKey ft fc) -> pgFmtColumn (QualifiedIdentifier s ft) fc
pgFmtColumn :: QualifiedIdentifier -> Text -> Text
pgFmtColumn table "*" = fromQi table <> ".*"
pgFmtColumn table c = fromQi table <> "." <> pgFmtIdent c
pgFmtJsonPath :: Maybe JsonPath -> Text
pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x
pgFmtJsonPath (Just (x:xs)) = "->" <> pgFmtLit x <> pgFmtJsonPath ( Just xs )
pgFmtJsonPath _ = ""
pgFmtTable :: Table -> Text
pgFmtTable Table{tableSchema=s, tableName=n} = fromQi $ QualifiedIdentifier s n
pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> Text
pgFmtSelectItem table ((c, jp), Nothing) = pgFmtColumn table c <> pgFmtJsonPath jp <> asJsonPath jp
pgFmtSelectItem table ((c, jp), Just cast ) = "CAST (" <> pgFmtColumn table c <> pgFmtJsonPath jp <> " AS " <> cast <> " )" <> asJsonPath jp
asJsonPath :: Maybe JsonPath -> Text
asJsonPath Nothing = ""
asJsonPath (Just xx) = " AS " <> last xx
+61
View File
@@ -0,0 +1,61 @@
module PostgREST.RangeQuery (
rangeParse
, rangeRequested
, rangeLimit
, rangeOffset
, NonnegRange
) where
import Control.Applicative
import Network.HTTP.Types.Header
import PostgREST.Types ()
import qualified Data.ByteString.Char8 as BS
import Data.Ranged.Boundaries
import Data.Ranged.Ranges
import Data.String.Conversions (cs)
import Text.Read (readMaybe)
import Text.Regex.TDFA ((=~))
import Data.Maybe (fromMaybe, listToMaybe)
import Prelude
type NonnegRange = Range Int
rangeParse :: BS.ByteString -> Maybe NonnegRange
rangeParse range = do
let rangeRegex = "^([0-9]+)-([0-9]*)$" :: BS.ByteString
parsedRange <- listToMaybe (range =~ rangeRegex :: [[BS.ByteString]])
let [_, from, to] = readMaybe . cs <$> parsedRange
let lower = fromMaybe emptyRange (rangeGeq <$> from)
let upper = fromMaybe (rangeGeq 0) (rangeLeq <$> to)
return $ rangeIntersection lower upper
rangeRequested :: RequestHeaders -> Maybe NonnegRange
rangeRequested = (rangeParse =<<) . lookup hRange
rangeLimit :: NonnegRange -> Maybe Int
rangeLimit range =
case [rangeLower range, rangeUpper range] of
[BoundaryBelow from, BoundaryAbove to] -> Just (1 + to - from)
_ -> Nothing
rangeOffset :: NonnegRange -> Int
rangeOffset range =
case rangeLower range of
BoundaryBelow from -> from
_ -> error "range without lower bound" -- should never happen
rangeGeq :: Int -> NonnegRange
rangeGeq n =
Range (BoundaryBelow n) BoundaryAboveAll
rangeLeq :: Int -> NonnegRange
rangeLeq n =
Range BoundaryBelowAll (BoundaryAbove n)
+113
View File
@@ -0,0 +1,113 @@
module PostgREST.Types where
import Data.Text
import Data.Tree
import qualified Data.ByteString.Char8 as BS
import Data.Aeson
data DbStructure = DbStructure {
tables :: [Table]
, columns :: [Column]
, relations :: [Relation]
, primaryKeys :: [PrimaryKey]
}
data Table = Table {
tableSchema :: Text
, tableName :: Text
, tableInsertable :: Bool
, tableAcl :: [Text]
} deriving (Show)
data ForeignKey = ForeignKey {
fkTable::Text, fkCol::Text
} deriving (Show, Eq)
data Column = Column {
colSchema :: Text
, colTable :: Text
, colName :: Text
, colPosition :: Int
, colNullable :: Bool
, colType :: Text
, colUpdatable :: Bool
, colMaxLen :: Maybe Int
, colPrecision :: Maybe Int
, colDefault :: Maybe Text
, colEnum :: [Text]
, colFK :: Maybe ForeignKey
} | Star {colSchema :: Text, colTable :: Text } deriving (Show)
data PrimaryKey = PrimaryKey {
pkSchema::Text, pkTable::Text, pkName::Text
}
data OrderTerm = OrderTerm {
otTerm :: Text
, otDirection :: BS.ByteString
, otNullOrder :: Maybe BS.ByteString
} deriving (Show, Eq)
data QualifiedIdentifier = QualifiedIdentifier {
qiSchema :: Text
, qiName :: Text
} deriving (Show, Eq)
data RelationType = Child | Parent | Many deriving (Show, Eq)
data Relation = Relation {
relSchema :: Text
, relTable :: Text
, relColumns :: [Text]
, relFTable :: Text
, relFColumns :: [Text]
, relType :: RelationType
, relLTable :: Maybe Text
, relLCols1 :: Maybe [Text]
, relLCols2 :: Maybe [Text]
} deriving (Show, Eq)
type Operator = Text
data FValue = VText Text | VForeignKey QualifiedIdentifier ForeignKey deriving (Show, Eq)
type FieldName = Text
type JsonPath = [Text]
type Field = (FieldName, Maybe JsonPath)
type Cast = Text
type SelectItem = (Field, Maybe Cast)
type Path = [Text]
data Query = Select {
mainTable::Text
, fields::[SelectItem]
, joinTables::[Text]
, filters::[Filter]
, order::Maybe [OrderTerm]
, relation::Maybe Relation
} deriving (Show, Eq)
data Filter = Filter {field::Field, operator::Operator, value::FValue} deriving (Show, Eq)
type ApiRequest = Tree Query
instance ToJSON Column where
toJSON c = object [
"schema" .= colSchema c
, "name" .= colName c
, "position" .= colPosition c
, "nullable" .= colNullable c
, "type" .= colType c
, "updatable" .= colUpdatable c
, "maxLen" .= colMaxLen c
, "precision" .= colPrecision c
, "references".= colFK c
, "default" .= colDefault c
, "enum" .= colEnum c ]
instance ToJSON ForeignKey where
toJSON fk = object ["table".=fkTable fk, "column".=fkCol fk]
instance ToJSON Table where
toJSON v = object [
"schema" .= tableSchema v
, "name" .= tableName v
, "insertable" .= tableInsertable v ]
-55
View File
@@ -1,55 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module RangeQuery where
import Control.Applicative
import Network.HTTP.Types.Header
import Data.Ranged.Boundaries
import Data.Ranged.Ranges
import Data.String.Conversions (cs)
import Text.Regex.TDFA ((=~))
import Text.Read (readMaybe)
import Data.Maybe (fromMaybe, listToMaybe)
type NonnegRange = Range Int
rangeGeq :: Int -> NonnegRange
rangeGeq n =
Range (BoundaryBelow n) BoundaryAboveAll
rangeLeq :: Int -> NonnegRange
rangeLeq n =
Range BoundaryBelowAll (BoundaryAbove n)
parseRange :: String -> Maybe NonnegRange
parseRange range = do
let rangeRegex = "^([0-9]+)-([0-9]*)$" :: String
parsedRange <- listToMaybe (range =~ rangeRegex :: [[String]])
let [_, from, to] = readMaybe <$> parsedRange
let lower = fromMaybe emptyRange (rangeGeq <$> from)
let upper = fromMaybe (rangeGeq 0) (rangeLeq <$> to)
return $ rangeIntersection lower upper
requestedRange :: RequestHeaders -> Maybe NonnegRange
requestedRange hdrs = parseRange =<< cs <$> lookup hRange hdrs
requestedContentRange :: RequestHeaders -> Maybe NonnegRange
requestedContentRange hdrs = parseRange =<< cs <$> lookup "Content-Range" hdrs
limit :: NonnegRange -> Maybe Int
limit range =
case [rangeLower range, rangeUpper range]
of [BoundaryBelow from, BoundaryAbove to] -> Just (1 + to - from)
_ -> Nothing
offset :: NonnegRange -> Int
offset range =
case rangeLower range
of BoundaryBelow from -> from
_ -> error "range without lower bound" -- should never happen
-61
View File
@@ -1,61 +0,0 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Types where
import Database.HDBC (toSql, iToSql, SqlValue(..))
import qualified Data.Aeson as JSON
import Data.Aeson.Types (Parser)
import Data.Scientific (floatingOrInteger)
import Data.HashMap.Strict (foldlWithKey')
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8)
import Data.Time.Calendar (showGregorian)
import Control.Monad (mzero)
instance JSON.FromJSON SqlValue where
parseJSON (JSON.Number n) = return $ either toSql iToSql (floatingOrInteger n :: Either Double Int)
parseJSON (JSON.String s) = return $ toSql s
parseJSON (JSON.Bool b) = return $ toSql b
parseJSON JSON.Null = return SqlNull
parseJSON (JSON.Object o) = return . toSql $ JSON.encode o
parseJSON (JSON.Array a) = return . toSql $ JSON.encode a
instance JSON.ToJSON SqlValue where
toJSON (SqlString s) = JSON.toJSON s
toJSON (SqlByteString s) = JSON.toJSON $ decodeUtf8 s
toJSON (SqlWord32 w) = JSON.toJSON w
toJSON (SqlWord64 w) = JSON.toJSON w
toJSON (SqlInt32 i) = JSON.toJSON i
toJSON (SqlInt64 i) = JSON.toJSON i
toJSON (SqlInteger i) = JSON.toJSON i
toJSON (SqlChar c) = JSON.toJSON c
toJSON (SqlBool b) = JSON.toJSON b
toJSON (SqlDouble n) = JSON.toJSON n
toJSON (SqlRational n) = JSON.toJSON n
toJSON (SqlLocalDate d) = JSON.toJSON $ showGregorian d
toJSON (SqlLocalTimeOfDay t) = JSON.toJSON $ show t
toJSON (SqlLocalTime t) = JSON.toJSON $ show t
toJSON SqlNull = JSON.Null
toJSON x = JSON.toJSON $ show x
newtype SqlRow = SqlRow {getRow :: [(Text, SqlValue)] } deriving (Show)
sqlRowColumns :: SqlRow -> [Text]
sqlRowColumns = map fst . getRow
sqlRowValues :: SqlRow -> [SqlValue]
sqlRowValues = map snd . getRow
instance JSON.FromJSON SqlRow where
parseJSON (JSON.Object m) = foldlWithKey' add (return $ SqlRow []) m
where
add :: Parser SqlRow -> Text -> JSON.Value -> Parser SqlRow
add parser k v = do
SqlRow l <- parser
sqlV <- JSON.parseJSON v
return . SqlRow $ (k, sqlV) : l
parseJSON _ = mzero
+7
View File
@@ -0,0 +1,7 @@
flags: {}
packages:
- '.'
extra-deps:
- Ranged-sets-0.3.0
- packdeps-0.4.1
resolver: lts-3.10
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

+61 -13
View File
@@ -1,24 +1,72 @@
{-# LANGUAGE OverloadedStrings #-}
module Feature.AuthSpec where
-- {{{ Imports
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import SpecHelper
-- }}}
spec :: Spec
spec = around appWithFixture $
describe "authorization" $ do
it "hides tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` 400 -- TODO: should be 404
it "indicates login failure" $ do
let auth = authHeader "dbapi_test_author_a" "fakefake"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 401
-- it "allows users with permissions to see their tables" $ do
-- let auth = authHeader "dbapi_test_author_a" ""
-- request methodGet "/authors_only" [auth] ""
-- `shouldRespondWith` 200
spec = beforeAll
(clearTable "postgrest.auth") . afterAll_ (clearTable "postgrest.auth")
$ around withApp
$ describe "authorization" $ do
it "hides tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` 404
it "indicates login failure (BasicAuth)" $ do
let auth = authHeaderBasic "postgrest_test_author" "fakefake"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 401
it "allows users with permissions to see their tables (BasicAuth)" $ do
_ <- post "/postgrest/users" [json| { "id": "jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeaderBasic "jdoe" "1234"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "respects database constraints for role" $
post "/postgrest/users" [json| { "id": "ssmith", "pass": "1234", "role": "SUPER_ADMIN_TRUNCATE_POWERS" } |]
`shouldRespondWith` 400
it "does not send a value when no role is provided" $ do
post "/postgrest/users" [json| { "id": "bdeey", "pass": "1234" } |]
`shouldRespondWith` 201
let auth = authHeaderBasic "jdoe" "1234"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "recovers after 400 error with logged in user" $ do
_ <- post "/postgrest/users" [json| { "id": "jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeaderBasic "jdoe" "1234"
_ <- request methodPost "/rpc/problem" [auth] ""
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "allows users to login (JWT)" $ do
_ <- post "/postgrest/users" [json| { "id": "jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
post "/postgrest/tokens" [json| { "id": "jdoe", "pass": "1234" } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"} |]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json"]
}
it "indicates login failure (JWT)" $ do
_ <- post "/postgrest/users" [json| { "id": "jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
post "/postgrest/tokens" [json| { "id": "jdoe", "pass": "NOPE" } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"message":"Failed authentication."} |]
, matchStatus = 401
, matchHeaders = ["Content-Type" <:> "application/json"]
}
it "allows users with permissions to see their tables (JWT)" $ do
_ <- post "/postgrest/users" [json| { "id": "jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
+10 -5
View File
@@ -1,5 +1,3 @@
{-# LANGUAGE OverloadedStrings #-}
module Feature.CorsSpec where
-- {{{ Imports
@@ -14,8 +12,7 @@ import Network.HTTP.Types
-- }}}
spec :: Spec
spec = around appWithFixture $
describe "CORS" $ do
spec = around withApp $ describe "CORS" $ do
let preflightHeaders = [
("Accept", "*/*"),
("Origin", "http://example.com"),
@@ -25,7 +22,7 @@ spec = around appWithFixture $
("Host", "localhost:3000"),
("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0"),
("Origin", "http://localhost:8000"),
("Accept", "text/plain, */*; q=0.01"),
("Accept", "text/csv, */*; q=0.01"),
("Accept-Language", "en-US,en;q=0.5"),
("Accept-Encoding", "gzip, deflate"),
("Referer", "http://localhost:8000/"),
@@ -56,6 +53,14 @@ spec = around appWithFixture $
r <- request methodOptions "/" preflightHeaders ""
liftIO $ simpleBody r `shouldBe` ""
describe "regular request" $
it "exposes necesssary response headers" $ do
r <- request methodGet "/items" [("Origin", "http://example.com")] ""
liftIO $ simpleHeaders r `shouldSatisfy` matchHeader
"Access-Control-Expose-Headers"
"Content-Encoding, Content-Location, Content-Range, Content-Type, \
\Date, Location, Server, Transfer-Encoding, Range-Unit"
describe "postflight request" $
it "allows INFO body through even with CORS request headers present" $ do
r <- request methodOptions "/items" normalCors ""
+37
View File
@@ -0,0 +1,37 @@
module Feature.DeleteSpec where
import Test.Hspec
import Test.Hspec.Wai
import SpecHelper
import Network.HTTP.Types
spec :: Spec
spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items")
. around withApp $
describe "Deleting" $ do
context "existing record" $ do
it "succeeds with 204 and deletion count" $
request methodDelete "/items?id=eq.1" [] ""
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing
, matchStatus = 204
, matchHeaders = ["Content-Range" <:> "*/1"]
}
it "actually clears items ouf the db" $ do
_ <- request methodDelete "/items?id=lt.15" [] ""
get "/items"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":15}]"
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/1"]
}
context "known route, unknown record" $
it "fails with 404" $
request methodDelete "/items?id=eq.101" [] "" `shouldRespondWith` 404
context "totally unknown route" $
it "fails with 404" $
request methodDelete "/foozle?id=eq.101" [] "" `shouldRespondWith` 404
+203 -32
View File
@@ -1,8 +1,5 @@
{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
module Feature.InsertSpec where
-- {{{ Imports
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
@@ -12,27 +9,29 @@ import SpecHelper
import qualified Data.Aeson as JSON
import Data.Maybe (fromJust)
import Text.Heredoc
import Network.HTTP.Types.Header
import Network.HTTP.Types
import Control.Monad (replicateM_)
import TestTypes(IncPK, incStr, incNullableStr)
-- }}}
import TestTypes(IncPK(..), CompoundPK(..))
spec :: Spec
spec = around appWithFixture $ do
spec = afterAll_ resetDb $ around withApp $ do
describe "Posting new record" $ do
it "accepts disparate json types" $
post "/menagerie"
after_ (clearTable "menagerie") . it "accepts disparate json types" $ do
p <- post "/menagerie"
[json| {
"integer": 13, "double": 3.14159, "varchar": "testing!"
, "boolean": false, "date": "01/01/1900", "money": "$3.99"
, "boolean": false, "date": "1900-01-01", "money": "$3.99"
, "enum": "foo"
} |]
`shouldRespondWith` 201
liftIO $ do
simpleBody p `shouldBe` ""
simpleStatus p `shouldBe` created201
context "with no pk supplied" $ do
context "into a table with auto-incrementing pk" $
context "into a table with auto-incrementing pk" . after_ (clearTable "auto_incrementing_pk") $
it "succeeds with 201 and link" $ do
p <- post "/auto_incrementing_pk" [json| { "non_nullable_string":"not null"} |]
liftIO $ do
@@ -51,7 +50,7 @@ spec = around appWithFixture $ do
post "/simple_pk" [json| { "extra":"foo"} |]
`shouldRespondWith` 400
context "into a table with no pk" $
context "into a table with no pk" . after_ (clearTable "no_pk") $ do
it "succeeds with 201 and a link including all fields" $ do
p <- post "/no_pk" [json| { "a":"foo", "b":"bar" } |]
liftIO $ do
@@ -59,7 +58,25 @@ spec = around appWithFixture $ do
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.foo&b=eq.bar"
simpleStatus p `shouldBe` created201
context "with compound pk supplied" $
it "returns full details of inserted record if asked" $ do
p <- request methodPost "/no_pk"
[("Prefer", "return=representation")]
[json| { "a":"bar", "b":"baz" } |]
liftIO $ do
simpleBody p `shouldBe` [json| { "a":"bar", "b":"baz" } |]
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.bar&b=eq.baz"
simpleStatus p `shouldBe` created201
it "can post nulls" $ do
p <- request methodPost "/no_pk"
[("Prefer", "return=representation")]
[json| { "a":null, "b":"foo" } |]
liftIO $ do
simpleBody p `shouldBe` [json| { "a":null, "b":"foo" } |]
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=is.null&b=eq.foo"
simpleStatus p `shouldBe` created201
context "with compound pk supplied" . after_ (clearTable "compound_pk") $
it "builds response location header appropriately" $
post "/compound_pk" [json| { "k1":12, "k2":42 } |]
`shouldRespondWith` ResponseMatcher {
@@ -70,13 +87,68 @@ spec = around appWithFixture $ do
context "with invalid json payload" $
it "fails with 400 and error" $
post "/simple_pk" "}{ x = 2"
post "/simple_pk" "}{ x = 2" `shouldRespondWith` 400
context "jsonb" . after_ (clearTable "json") $ do
it "serializes nested object" $ do
let inserted = [json| { "data": { "foo":"bar" } } |]
p <- request methodPost "json" [("Prefer", "return=representation")] inserted
liftIO $ do
simpleBody p `shouldBe` inserted
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/json\\?data=eq\\.%7B%22foo%22%3A%22bar%22%7D"
simpleStatus p `shouldBe` created201
it "serializes nested array" $ do
let inserted = [json| { "data": [1,2,3] } |]
p <- request methodPost "json" [("Prefer", "return=representation")] inserted
liftIO $ do
simpleBody p `shouldBe` inserted
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/json\\?data=eq\\.%5B1%2C2%2C3%5D"
simpleStatus p `shouldBe` created201
describe "CSV insert" $ do
after_ (clearTable "menagerie") . context "disparate csv types" $
it "succeeds with multipart response" $ do
p <- request methodPost "/menagerie" [("Content-Type", "text/csv")]
[str|integer,double,varchar,boolean,date,money,enum
|13,3.14159,testing!,false,1900-01-01,$3.99,foo
|12,0.1,a string,true,1929-10-01,12,bar
|]
liftIO $ do
simpleBody p `shouldBe` "Content-Type: application/json\nLocation: /menagerie?integer=eq.13\n\n\n--postgrest_boundary\nContent-Type: application/json\nLocation: /menagerie?integer=eq.12\n\n"
simpleStatus p `shouldBe` created201
after_ (clearTable "no_pk") . context "requesting full representation" $ do
it "returns full details of inserted record" $
request methodPost "/no_pk"
[("Content-Type", "text/csv"), ("Prefer", "return=representation")]
"a,b\nbar,baz"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"error":"Failed to parse JSON payload. Failed reading: satisfy"} |]
, matchStatus = 400
, matchHeaders = []
matchBody = Just [json| { "a":"bar", "b":"baz" } |]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json",
"Location" <:> "/no_pk?a=eq.bar&b=eq.baz"]
}
it "can post nulls" $
request methodPost "/no_pk"
[("Content-Type", "text/csv"), ("Prefer", "return=representation")]
"a,b\nNULL,foo"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| { "a":null, "b":"foo" } |]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json",
"Location" <:> "/no_pk?a=is.null&b=eq.foo"]
}
after_ (clearTable "no_pk") . context "with wrong number of columns" $ do
it "fails for too few" $ do
p <- request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz"
liftIO $ simpleStatus p `shouldBe` badRequest400
it "fails for too many" $ do
p <- request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz,bat,bad"
liftIO $ simpleStatus p `shouldBe` badRequest400
describe "Putting record" $ do
context "to unkonwn uri" $
@@ -94,39 +166,138 @@ spec = around appWithFixture $ do
context "with a fully-specified primary key" $ do
context "with Content-Range header" $
it "fails as per RFC7231" $
request methodPut "/compound_pk?k1=eq.1&k2=eq.2"
[("Content-Range", "0-0")]
[json| { "k1":1, "k2":2, "extra":3 } |]
`shouldRespondWith` 400
context "not specifying every column in the table" $
it "is rejected for lack of idempotence" $
request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42 } |]
`shouldRespondWith` 400
context "specifying every column in the table" $
it "succeeds with 201 and link" $ do
context "specifying every column in the table" . after_ (clearTable "compound_pk") $ do
it "can create a new record" $ do
p <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":3 } |]
liftIO $ do
simpleBody p `shouldBe` ""
simpleStatus p `shouldBe` status200
simpleStatus p `shouldBe` status204
context "with an auto-incrementing primary key" $
r <- get "/compound_pk?k1=eq.12&k2=eq.42"
let rows = fromJust (JSON.decode $ simpleBody r :: Maybe [CompoundPK])
liftIO $ do
length rows `shouldBe` 1
let record = head rows
compoundK1 record `shouldBe` 12
compoundK2 record `shouldBe` 42
compoundExtra record `shouldBe` Just 3
it "succeeds with 201 and link" $
it "can update an existing record" $ do
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":4 } |]
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":5 } |]
r <- get "/compound_pk?k1=eq.12&k2=eq.42"
let rows = fromJust (JSON.decode $ simpleBody r :: Maybe [CompoundPK])
liftIO $ do
length rows `shouldBe` 1
let record = head rows
compoundExtra record `shouldBe` Just 5
context "with an auto-incrementing primary key" . after_ (clearTable "auto_incrementing_pk") $
it "succeeds with 204" $
request methodPut "/auto_incrementing_pk?id=eq.1" []
[json| {
"id":1,
"nullable_string":"hi",
"non_nullable_string":"bye",
"inserted_at": "now()"
"inserted_at": "2020-11-11"
} |]
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing,
matchStatus = 200,
matchStatus = 204,
matchHeaders = []
}
describe "Patching record" $ do
context "to unkonwn uri" $
it "gives a 404" $
request methodPatch "/fake" []
[json| { "real": false } |]
`shouldRespondWith` 404
context "on an empty table" $
it "indicates no records found to update" $
request methodPatch "/simple_pk" []
[json| { "extra":20 } |]
`shouldRespondWith` 404
context "in a nonempty table" . before_ (clearTable "items" >> createItems 15) .
after_ (clearTable "items") $ do
it "can update a single item" $ do
g <- get "/items?id=eq.42"
liftIO $ simpleHeaders g
`shouldSatisfy` matchHeader "Content-Range" "\\*/0"
request methodPatch "/items?id=eq.1" []
[json| { "id":42 } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing,
matchStatus = 204,
matchHeaders = ["Content-Range" <:> "0-0/1"]
}
g' <- get "/items?id=eq.42"
liftIO $ simpleHeaders g'
`shouldSatisfy` matchHeader "Content-Range" "0-0/1"
it "can update multiple items" $ do
replicateM_ 10 $ post "/auto_incrementing_pk"
[json| { non_nullable_string: "a" } |]
replicateM_ 10 $ post "/auto_incrementing_pk"
[json| { non_nullable_string: "b" } |]
_ <- request methodPatch
"/auto_incrementing_pk?non_nullable_string=eq.a" []
[json| { non_nullable_string: "c" } |]
g <- get "/auto_incrementing_pk?non_nullable_string=eq.c"
liftIO $ simpleHeaders g
`shouldSatisfy` matchHeader "Content-Range" "0-9/10"
it "can set a column to NULL" $ do
_ <- post "/no_pk" [json| { a: "keepme", b: "nullme" } |]
_ <- request methodPatch "/no_pk?b=eq.nullme" [] [json| { b: null } |]
get "/no_pk?a=eq.keepme" `shouldRespondWith`
[json| [{ a: "keepme", b: null }] |]
it "can update based on a computed column" $
request methodPatch
"/items?always_true=eq.false"
[("Prefer", "return=representation")]
[json| { id: 100 } |]
`shouldRespondWith` 404
it "can provide a representation" $ do
_ <- post "/items"
[json| { id: 1 } |]
request methodPatch
"/items?id=eq.1"
[("Prefer", "return=representation")]
[json| { id: 99 } |]
`shouldRespondWith` [json| [{id:99}] |]
describe "Row level permission" $
it "set user_id when inserting rows" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
_ <- post "/postgrest/users" [json| { "id":"jroe", "pass": "1234", "role": "postgrest_test_author" } |]
p1 <- request methodPost "/authors_only"
[ authHeaderBasic "jdoe" "1234", ("Prefer", "return=representation") ]
[json| { "secret": "nyancat" } |]
liftIO $ do
simpleBody p1 `shouldBe` [json| { "owner":"jdoe", "secret":"nyancat" } |]
simpleStatus p1 `shouldBe` created201
p2 <- request methodPost "/authors_only"
-- jwt token for jroe
[ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.YuF_VfmyIxWyuceT7crnNKEprIYXsJAyXid3rjPjIow", ("Prefer", "return=representation") ]
[json| { "secret": "lolcat", "owner": "hacker" } |]
liftIO $ do
simpleBody p2 `shouldBe` [json| { "owner":"jroe", "secret":"lolcat" } |]
simpleStatus p2 `shouldBe` created201
+282 -24
View File
@@ -1,45 +1,282 @@
{-# LANGUAGE OverloadedStrings #-}
module Feature.QuerySpec where
import Test.Hspec
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders))
import SpecHelper
spec :: Spec
spec = around appWithFixture $ do
spec =
beforeAll (clearTable "items" >> createItems 15)
. beforeAll (clearTable "complex_items" >> createComplexItems)
. beforeAll (clearTable "nullable_integer" >> createNullInteger)
. beforeAll (
clearTable "no_pk" >>
createNulls 2 >>
createLikableStrings >>
createJsonData)
. afterAll_ (clearTable "items" >> clearTable "complex_items" >> clearTable "no_pk" >> clearTable "simple_pk")
. around withApp $ do
describe "Querying a table with a column called count" $
it "should not confuse count column with pg_catalog.count aggregate" $
get "/has_count_column" `shouldRespondWith` 200
describe "Querying a nonexistent table" $
it "causes a 404" $
get "/faketable" `shouldRespondWith` 404
describe "Filtering response" $
context "column equality" $
describe "Filtering response" $ do
it "matches with equality" $
get "/items?id=eq.5"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":5}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/1"]
}
it "matches with equality using not operator" $
get "/items?id=not.eq.5"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-13/14"]
}
it "matches with more than one condition using not operator" $
get "/simple_pk?k=like.*yx&extra=not.eq.u" `shouldRespondWith` "[]"
it "matches with inequality using not operator" $ do
get "/items?id=not.lt.14&order=id.asc"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":14},{"id":15}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/2"]
}
get "/items?id=not.gt.2&order=id.asc"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":2}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/2"]
}
it "matches items IN" $
get "/items?id=in.1,3,5"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "matches items NOT IN" $
get "/items?id=notin.2,4,6,7,8,9,10,11,12,13,14,15"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "matches items NOT IN using not operator" $
get "/items?id=not.in.2,4,6,7,8,9,10,11,12,13,14,15"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "matches nulls using not operator" $
get "/no_pk?a=not.is.null" `shouldRespondWith`
[json| [{"a":"1","b":"0"},{"a":"2","b":"0"}] |]
it "matches nulls in varchar and numeric fields alike" $ do
get "/no_pk?a=is.null" `shouldRespondWith`
[json| [{"a": null, "b": null}] |]
get "/nullable_integer?a=is.null" `shouldRespondWith` "[{\"a\":null}]"
it "matches with like" $ do
get "/simple_pk?k=like.*yx" `shouldRespondWith`
"[{\"k\":\"xyyx\",\"extra\":\"u\"}]"
get "/simple_pk?k=like.xy*" `shouldRespondWith`
"[{\"k\":\"xyyx\",\"extra\":\"u\"}]"
get "/simple_pk?k=like.*YY*" `shouldRespondWith`
"[{\"k\":\"xYYx\",\"extra\":\"v\"}]"
it "matches with like using not operator" $
get "/simple_pk?k=not.like.*yx" `shouldRespondWith`
"[{\"k\":\"xYYx\",\"extra\":\"v\"}]"
it "matches with ilike" $ do
get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith`
"[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]"
get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith`
"[{\"k\":\"xyyx\",\"extra\":\"u\"},{\"k\":\"xYYx\",\"extra\":\"v\"}]"
it "matches with ilike using not operator" $
get "/simple_pk?k=not.ilike.xy*&order=extra.asc" `shouldRespondWith` "[]"
it "matches with tsearch @@" $
get "/tsearch?text_search_vector=@@.foo" `shouldRespondWith`
[json| [{"text_search_vector":"'bar':2 'foo':1"}] |]
it "matches with tsearch @@ using not operator" $
get "/tsearch?text_search_vector=not.@@.foo" `shouldRespondWith`
[json| [{"text_search_vector":"'baz':1 'qux':2"}] |]
it "matches with computed column" $
get "/items?always_true=eq.true" `shouldRespondWith`
[json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
it "matches filtering nested items" $
get "/clients?select=id,projects(id,tasks(id,name))&projects.tasks.name=like.Design*" `shouldRespondWith`
"[{\"id\":1,\"projects\":[{\"id\":1,\"tasks\":[{\"id\":1,\"name\":\"Design w7\"}]},{\"id\":2,\"tasks\":[{\"id\":3,\"name\":\"Design w10\"}]}]},{\"id\":2,\"projects\":[{\"id\":3,\"tasks\":[{\"id\":5,\"name\":\"Design IOS\"}]},{\"id\":4,\"tasks\":[{\"id\":7,\"name\":\"Design OSX\"}]}]}]"
describe "Shaping response with select parameter" $ do
it "selectStar works in absense of parameter" $
get "/complex_items?id=eq.3" `shouldRespondWith`
"[{\"id\":3,\"name\":\"Three\",\"settings\":{\"foo\":{\"int\":1,\"bar\":\"baz\"}}}]"
it "one simple column" $
get "/complex_items?select=id" `shouldRespondWith`
[json| [{"id":1},{"id":2},{"id":3}] |]
it "one simple column with casting (text)" $
get "/complex_items?select=id::text" `shouldRespondWith`
[json| [{"id":"1"},{"id":"2"},{"id":"3"}] |]
it "json column" $
get "/complex_items?id=eq.1&select=settings" `shouldRespondWith`
[json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |]
it "json subfield one level with casting (json)" $
get "/complex_items?id=eq.1&select=settings->>foo::json" `shouldRespondWith`
[json| [{"foo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
it "fails on bad casting (data of the wrong format)" $
get "/complex_items?select=settings->foo->>bar::integer"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |]
, matchStatus = 400
, matchHeaders = []
}
it "fails on bad casting (wrong cast type)" $
get "/complex_items?select=id::fakecolumntype"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |]
, matchStatus = 400
, matchHeaders = []
}
it "json subfield two levels (string)" $
get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith`
[json| [{"bar":"baz"}] |]
it "json subfield two levels with casting (int)" $
get "/complex_items?id=eq.1&select=settings->foo->>int::integer" `shouldRespondWith`
[json| [{"int":1}] |] -- the value in the db is an int, but here we expect a string for now
it "requesting parents and children" $
get "/projects?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith`
"[{\"id\":1,\"name\":\"Windows 7\",\"clients\":{\"id\":1,\"name\":\"Microsoft\"},\"tasks\":[{\"id\":1,\"name\":\"Design w7\"},{\"id\":2,\"name\":\"Code w7\"}]}]"
it "requesting children 2 levels" $
get "/clients?id=eq.1&select=id,projects(id,tasks(id))" `shouldRespondWith`
"[{\"id\":1,\"projects\":[{\"id\":1,\"tasks\":[{\"id\":1},{\"id\":2}]},{\"id\":2,\"tasks\":[{\"id\":3},{\"id\":4}]}]}]"
it "requesting many<->many relation" $
get "/tasks?select=id,users(id)" `shouldRespondWith`
"[{\"id\":1,\"users\":[{\"id\":1},{\"id\":3}]},{\"id\":2,\"users\":[{\"id\":1}]},{\"id\":3,\"users\":[{\"id\":1}]},{\"id\":4,\"users\":[{\"id\":1}]},{\"id\":5,\"users\":[{\"id\":2},{\"id\":3}]},{\"id\":6,\"users\":[{\"id\":2}]},{\"id\":7,\"users\":[{\"id\":2}]},{\"id\":8,\"users\":null}]"
it "requesting parents and children on views" $
get "/projects_view?id=eq.1&select=id, name, clients(*), tasks(id, name)" `shouldRespondWith`
"[{\"id\":1,\"name\":\"Windows 7\",\"clients\":{\"id\":1,\"name\":\"Microsoft\"},\"tasks\":[{\"id\":1,\"name\":\"Design w7\"},{\"id\":2,\"name\":\"Code w7\"}]}]"
it "requesting children with composite key" $
get "/users_tasks?user_id=eq.2&task_id=eq.6&select=*, comments(content)" `shouldRespondWith`
"[{\"user_id\":2,\"task_id\":6,\"comments\":[{\"content\":\"Needs to be delivered ASAP\"}]}]"
it "matches the predicate" $
get "/items?id=eq.5"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":5}]"
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/1"]
}
describe "ordering response" $ do
it "by a column asc" $
get "/items?id=lte.2&order=asc.id"
get "/items?id=lte.2&order=id.asc"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":1},{\"id\":2}]"
matchBody = Just [json| [{"id":1},{"id":2}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/2"]
}
it "by a column desc" $
get "/items?id=lte.2&order=desc.id"
get "/items?id=lte.2&order=id.desc"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":2},{\"id\":1}]"
matchBody = Just [json| [{"id":2},{"id":1}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/2"]
}
it "by a column asc with nulls last" $
get "/no_pk?order=a.asc.nullslast"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"a":"1","b":"0"},
{"a":"2","b":"0"},
{"a":null,"b":null}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "by a column desc with nulls first" $
get "/no_pk?order=a.desc.nullsfirst"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"a":null,"b":null},
{"a":"2","b":"0"},
{"a":"1","b":"0"}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "by a column desc with nulls last" $
get "/no_pk?order=a.desc.nullslast"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"a":"2","b":"0"},
{"a":"1","b":"0"},
{"a":null,"b":null}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "without other constraints" $
get "/items?order=id.asc" `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
it "Sets Content-Location with alphabetized params" $
get "/no_pk?b=eq.1&a=eq.1"
@@ -49,10 +286,31 @@ spec = around appWithFixture $ do
, matchHeaders = ["Content-Location" <:> "/no_pk?a=eq.1&b=eq.1"]
}
it "Omits question mark when there are no params" $
get "/no_pk"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[]"
, matchStatus = 200
, matchHeaders = ["Content-Location" <:> "/no_pk"]
}
it "Omits question mark when there are no params" $ do
r <- get "/simple_pk"
liftIO $ do
let respHeaders = simpleHeaders r
respHeaders `shouldSatisfy` matchHeader
"Content-Location" "/simple_pk"
describe "jsonb" $ do
it "can filter by properties inside json column" $ do
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
[json| [{"data": {"foo": {"bar": "baz"}}}] |]
get "/json?data->foo->>bar=eq.fake" `shouldRespondWith`
[json| [] |]
it "can filter by properties inside json column using not" $
get "/json?data->foo->>bar=not.eq.baz" `shouldRespondWith`
[json| [] |]
describe "remote procedure call" $ do
context "a proc that returns a set" . before_ (clearTable "items" >> createItems 10) .
after_ (clearTable "items") $
it "returns proper json" $
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
[json| [ {"id": 3}, {"id":4} ] |]
context "a proc that returns plain text" $
it "returns proper json" $
post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`
[json| [{"sayhello":"Hello, world"}] |]
+32 -3
View File
@@ -1,22 +1,51 @@
{-# LANGUAGE OverloadedStrings #-}
module Feature.RangeSpec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders,simpleStatus))
import SpecHelper
spec :: Spec
spec = around appWithFixture $
spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items")
. around withApp $
describe "GET /items" $ do
context "without range headers" $
context "without range headers" $ do
context "with response under server size limit" $
it "returns whole range with status 200" $
get "/items" `shouldRespondWith` 200
context "when I don't want the count" $ do
it "returns range Content-Range with /*" $
request methodGet "/menagerie"
[("Prefer", "count=none")] ""
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[]"
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"]
}
it "returns range Content-Range with range/*" $
request methodGet "/items?order=id"
[("Prefer", "count=none")] ""
`shouldRespondWith` ResponseMatcher {
matchBody = Just [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}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-14/*"]
}
it "returns range Content-Range with range/* even using other filters" $
request methodGet "/items?id=eq.1&order=id"
[("Prefer", "count=none")] ""
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"]
}
context "with range headers" $ do
context "of acceptable range" $ do
+140 -26
View File
@@ -1,38 +1,55 @@
{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
module Feature.StructureSpec where
import Test.Hspec
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import SpecHelper
import Network.HTTP.Types
import Codec.Binary.Base64.String (encode)
import Data.Monoid ((<>))
import Data.String.Conversions (cs)
spec :: Spec
spec = let {uName = "a user"; uPass = "nobody can ever know";
uRole = "dbapi_test"} in
around withDatabaseConnection $
aroundWith (withUser uName uPass uRole) $ aroundWith withApp $ do
describe "GET /" $
spec = around withApp $ do
describe "GET /" $ do
it "lists views in schema" $
request methodGet "/"
[("Authorization", "Basic "<>(cs.encode $ cs uName<>":"<>cs uPass))] ""
request methodGet "/" [] ""
`shouldRespondWith` [json| [
{"schema":"1","name":"authors_only","insertable":true}
, {"schema":"1","name":"auto_incrementing_pk","insertable":true}
{"schema":"1","name":"auto_incrementing_pk","insertable":true}
, {"schema":"1","name":"clients","insertable":true}
, {"schema":"1","name":"comments","insertable":true}
, {"schema":"1","name":"complex_items","insertable":true}
, {"schema":"1","name":"compound_pk","insertable":true}
, {"schema":"1","name":"has_count_column","insertable":false}
, {"schema":"1","name":"has_fk","insertable":true}
, {"schema":"1","name":"insertable_view_with_join","insertable":true}
, {"schema":"1","name":"items","insertable":true}
, {"schema":"1","name":"json","insertable":true}
, {"schema":"1","name":"materialized_view","insertable":false}
, {"schema":"1","name":"menagerie","insertable":true}
, {"schema":"1","name":"no_pk","insertable":true}
, {"schema":"1","name":"nullable_integer","insertable":true}
, {"schema":"1","name":"projects","insertable":true}
, {"schema":"1","name":"projects_view","insertable":true}
, {"schema":"1","name":"simple_pk","insertable":true}
, {"schema":"1","name":"tasks","insertable":true}
, {"schema":"1","name":"tsearch","insertable":true}
, {"schema":"1","name":"users","insertable":true}
, {"schema":"1","name":"users_projects","insertable":true}
, {"schema":"1","name":"users_tasks","insertable":true}
] |]
{matchStatus = 200}
it "lists only views user has permission to see" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeaderBasic "jdoe" "1234"
request methodGet "/" [auth] ""
`shouldRespondWith` [json| [
{"schema":"1","name":"authors_only","insertable":true}
] |]
{matchStatus = 200}
describe "Table info" $ do
it "is available with OPTIONS verb" $
request methodOptions "/menagerie" [] "" `shouldRespondWith`
@@ -48,7 +65,7 @@ uRole = "dbapi_test"} in
"name": "integer",
"type": "integer",
"maxLen": null,
"enum": null,
"enum": [],
"nullable": false,
"position": 1,
"references": null,
@@ -61,7 +78,7 @@ uRole = "dbapi_test"} in
"name": "double",
"type": "double precision",
"maxLen": null,
"enum": null,
"enum": [],
"nullable": false,
"references": null,
"position": 2
@@ -73,7 +90,7 @@ uRole = "dbapi_test"} in
"name": "varchar",
"type": "character varying",
"maxLen": null,
"enum": null,
"enum": [],
"nullable": false,
"position": 3,
"references": null,
@@ -86,7 +103,7 @@ uRole = "dbapi_test"} in
"name": "boolean",
"type": "boolean",
"maxLen": null,
"enum": null,
"enum": [],
"nullable": false,
"references": null,
"position": 4
@@ -98,7 +115,7 @@ uRole = "dbapi_test"} in
"name": "date",
"type": "date",
"maxLen": null,
"enum": null,
"enum": [],
"nullable": false,
"references": null,
"position": 5
@@ -110,7 +127,7 @@ uRole = "dbapi_test"} in
"name": "money",
"type": "money",
"maxLen": null,
"enum": null,
"enum": [],
"nullable": false,
"position": 6,
"references": null,
@@ -136,9 +153,106 @@ uRole = "dbapi_test"} in
}
|]
it "includes foreign key data" $
request methodOptions "/has_fk"
[("Authorization", "Basic "<>(cs.encode $ cs uName<>":"<>cs uPass))] ""
it "it includes primary and foreign keys for views" $
request methodOptions "/insertable_view_with_join" [] "" `shouldRespondWith`
[json|
{
"pkey":[
"id"
],
"columns":[
{
"references":null,
"default":null,
"precision":64,
"updatable":false,
"schema":"1",
"name":"id",
"type":"bigint",
"maxLen":null,
"enum":[],
"nullable":true,
"position":1
},
{
"references":{
"column":"id",
"table":"auto_incrementing_pk"
},
"default":null,
"precision":32,
"updatable":false,
"schema":"1",
"name":"auto_inc_fk",
"type":"integer",
"maxLen":null,
"enum":[],
"nullable":true,
"position":2
},
{
"references":{
"column":"k",
"table":"simple_pk"
},
"default":null,
"precision":null,
"updatable":false,
"schema":"1",
"name":"simple_fk",
"type":"character varying",
"maxLen":255,
"enum":[],
"nullable":true,
"position":3
},
{
"references":null,
"default":null,
"precision":null,
"updatable":false,
"schema":"1",
"name":"nullable_string",
"type":"character varying",
"maxLen":null,
"enum":[],
"nullable":true,
"position":4
},
{
"references":null,
"default":null,
"precision":null,
"updatable":false,
"schema":"1",
"name":"non_nullable_string",
"type":"character varying",
"maxLen":null,
"enum":[],
"nullable":true,
"position":5
},
{
"references":null,
"default":null,
"precision":null,
"updatable":false,
"schema":"1",
"name":"inserted_at",
"type":"timestamp with time zone",
"maxLen":null,
"enum":[],
"nullable":true,
"position":6
}
]
}
|]
it "includes foreign key data" $ do
pendingWith "have to resolve issue #107"
request methodOptions "/has_fk" [] ""
`shouldRespondWith` [json|
{
"pkey": ["id"],
@@ -153,7 +267,7 @@ uRole = "dbapi_test"} in
"maxLen": null,
"nullable": false,
"position": 1,
"enum": null,
"enum": [],
"references": null
}, {
"default": null,
@@ -165,7 +279,7 @@ uRole = "dbapi_test"} in
"maxLen": null,
"nullable": true,
"position": 2,
"enum": null,
"enum": [],
"references": {"table": "auto_incrementing_pk", "column": "id"}
}, {
"default": null,
@@ -177,7 +291,7 @@ uRole = "dbapi_test"} in
"maxLen": 255,
"nullable": true,
"position": 3,
"enum": null,
"enum": [],
"references": {"table": "simple_pk", "column": "k"}
}
]
+2 -11
View File
@@ -1,17 +1,8 @@
module Main where
import Database.HDBC (runRaw, disconnect)
import Test.Hspec
import SpecHelper
import Spec
import SpecHelper (openConnection, loadFixture)
main :: IO ()
main = do
c <-openConnection
runRaw c "drop schema if exists \"1\" cascade"
runRaw c "drop schema if exists private cascade"
runRaw c "drop schema if exists dbapi cascade"
loadFixture "roles" c
loadFixture "schema" c
disconnect c
hspec spec
main = resetDb >> hspec spec
+145 -50
View File
@@ -1,77 +1,112 @@
{-# LANGUAGE OverloadedStrings #-}
module SpecHelper where
import Network.Wai
import Test.Hspec
import Test.Hspec.Wai
import Database.HDBC
import Database.HDBC.PostgreSQL
import Hasql as H
import Hasql.Backend as B
import Hasql.Postgres as P
import Data.String.Conversions (cs)
import Control.Exception.Base (bracket, finally)
import Data.Monoid
import Data.Text hiding (map)
import qualified Data.Vector as V
import Control.Monad (void)
import Control.Applicative
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
hRange, hAuthorization)
hRange, hAuthorization, hAccept)
import Codec.Binary.Base64.String (encode)
import Data.CaseInsensitive (CI(..))
import Data.Maybe (fromMaybe)
import Text.Regex.TDFA ((=~))
import qualified Data.ByteString.Char8 as BS
import Network.Wai.Middleware.Cors (cors)
import System.Process (readProcess)
import Middleware(clientErrors, withSavepoint, authenticated)
import qualified Data.Aeson.Types as J
import Dbapi (app, corsPolicy, AppConfig(..))
import PgQuery(addUser)
import PostgREST.App (app)
import PostgREST.Config (AppConfig(..))
import PostgREST.Middleware
import PostgREST.Error(errResponse)
import PostgREST.PgStructure
import PostgREST.Types
isLeft :: Either a b -> Bool
isLeft (Left _ ) = True
isLeft _ = False
cfg :: AppConfig
cfg = AppConfig "postgres://dbapi_test:@localhost:5432/dbapi_test" 9000 "dbapi_anonymous" False
cfg = AppConfig "postgrest_test" 5432 "postgrest_test" "" "localhost" 3000 "postgrest_anonymous" False 10 "1" "safe"
openConnection :: IO Connection
openConnection = connectPostgreSQL' $ configDbUri cfg
testPoolOpts :: PoolSettings
testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30
withDatabaseConnection :: (Connection -> IO ()) -> IO ()
withDatabaseConnection = bracket openConnection disconnect
pgSettings :: P.Settings
pgSettings = P.ParamSettings (cs $ configDbHost cfg)
(fromIntegral $ configDbPort cfg)
(cs $ configDbUser cfg)
(cs $ configDbPass cfg)
(cs $ configDbName cfg)
loadFixture :: String -> Connection -> IO ()
loadFixture name conn = do
sql <- readFile $ "test/fixtures/" ++ name ++ ".sql"
runRaw conn sql
withApp :: ActionWith Application -> IO ()
withApp perform = do
pool :: H.Pool P.Postgres
<- H.acquirePool pgSettings testPoolOpts
dbWithSchema :: ActionWith Connection -> IO ()
dbWithSchema action = withDatabaseConnection $ \c -> do
runRaw c "begin;"
action c
rollback c
let txSettings = Just (H.ReadCommitted, Just True)
metadata <- H.session pool $ H.tx txSettings $ do
tabs <- allTables
rels <- allRelations
cols <- allColumns rels
keys <- allPrimaryKeys
return (tabs, rels, cols, keys)
withUser :: BS.ByteString -> BS.ByteString -> BS.ByteString ->
ActionWith Connection -> ActionWith Connection
withUser name pass role action conn = do
addUser name pass role conn
finally (action conn) $ do
_ <- run conn "delete from dbapi.auth where id=?" [toSql name]
runRaw conn "commit"
dbstructure <- case metadata of
Left e -> fail $ show e
Right (tabs, rels, cols, keys) ->
return $ DbStructure {
tables=tabs
, columns=cols
, relations=rels
, primaryKeys=keys
}
withApp :: ActionWith Application -> ActionWith Connection
withApp action conn = do
runRaw conn "begin;"
action $ cors corsPolicy $ authenticated "dbapi_anonymous" app conn
rollback conn
perform $ middle $ \req resp -> do
body <- strictRequestBody req
result <- liftIO $ H.session pool $ H.tx txSettings
$ authenticated cfg (app dbstructure cfg body) req
either (resp . errResponse) resp result
where middle = defaultMiddle False
resetDb :: IO ()
resetDb = do
pool :: H.Pool P.Postgres
<- H.acquirePool pgSettings testPoolOpts
void . liftIO $ H.session pool $
H.tx Nothing $ do
H.unitEx [H.stmt| drop schema if exists "1" cascade |]
H.unitEx [H.stmt| drop schema if exists private cascade |]
H.unitEx [H.stmt| drop schema if exists postgrest cascade |]
loadFixture "roles"
loadFixture "schema"
loadFixture :: FilePath -> IO()
loadFixture name =
void $ readProcess "psql" ["-U", "postgrest_test", "-d", "postgrest_test", "-a", "-f", "test/fixtures/" ++ name ++ ".sql"] []
appWithFixture :: ActionWith Application -> IO ()
appWithFixture action = withDatabaseConnection $ \c -> do
runRaw c "begin;"
action $ cors corsPolicy . clientErrors $
(authenticated "dbapi_anonymous" . withSavepoint) app c
rollback c
rangeHdrs :: ByteRange -> [Header]
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
acceptHdrs :: BS.ByteString -> [Header]
acceptHdrs mime = [(hAccept, mime)]
rangeUnit :: Header
rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")
@@ -79,14 +114,74 @@ matchHeader :: CI BS.ByteString -> String -> [Header] -> Bool
matchHeader name valRegex headers =
maybe False (=~ valRegex) $ lookup name headers
authHeader :: String -> String -> Header
authHeader user pass =
(hAuthorization, cs $ "Basic " ++ encode (user ++ ":" ++ pass))
authHeaderBasic :: String -> String -> Header
authHeaderBasic u p =
(hAuthorization, cs $ "Basic " ++ encode (u ++ ":" ++ p))
-- for hspec-wai
pending_ :: WaiSession ()
pending_ = liftIO Test.Hspec.pending
authHeaderJWT :: String -> Header
authHeaderJWT token =
(hAuthorization, cs $ "Bearer " ++ token)
-- for hspec-wai
pendingWith_ :: String -> WaiSession ()
pendingWith_ = liftIO . Test.Hspec.pendingWith
testPool :: IO(H.Pool P.Postgres)
testPool = H.acquirePool pgSettings testPoolOpts
clearTable :: Text -> IO ()
clearTable table = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $ B.Stmt ("delete from \"1\"."<>table) V.empty True
createItems :: Int -> IO ()
createItems n = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing txn
where
txn = mapM_ H.unitEx stmts
stmts = map [H.stmt|insert into "1".items (id) values (?)|] [1..n]
createComplexItems :: IO ()
createComplexItems = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing txn
where
txn = mapM_ H.unitEx stmts
stmts = getZipList $ [H.stmt|insert into "1".complex_items (id, name, settings) values (?,?,?)|]
<$> ZipList ([1..3]::[Int])
<*> ZipList (["One", "Two", "Three"]::[Text])
<*> ZipList ([jobj,jobj,jobj])
jobj = (J.object [("foo", J.object [("int", J.Number 1),("bar", J.String "baz")])])
createNulls :: Int -> IO ()
createNulls n = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing txn
where
txn = mapM_ H.unitEx (stmt':stmts)
stmt' = [H.stmt|insert into "1".no_pk (a,b) values (null,null)|]
stmts = map [H.stmt|insert into "1".no_pk (a,b) values (?,0)|] [1..n]
createNullInteger :: IO ()
createNullInteger = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $ [H.stmt| insert into "1".nullable_integer (a) values (null) |]
createLikableStrings :: IO ()
createLikableStrings = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $ do
H.unitEx $ insertSimplePk "xyyx" "u"
H.unitEx $ insertSimplePk "xYYx" "v"
where
insertSimplePk :: Text -> Text -> H.Stmt P.Postgres
insertSimplePk = [H.stmt|insert into "1".simple_pk (k, extra) values (?,?)|]
createJsonData :: IO ()
createJsonData = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $
[H.stmt|
insert into "1".json (data) values (?)
|]
(J.object [("foo", J.object [("bar", J.String "baz")])])
+32 -12
View File
@@ -1,16 +1,17 @@
{-# LANGUAGE OverloadedStrings #-}
module TestTypes (
IncPK(..),
fromList
IncPK(..)
, CompoundPK(..)
-- , incFromList
-- , compoundFromList
) where
import qualified Data.Aeson as JSON
import Data.Aeson ((.:))
import Data.Maybe (fromJust)
import Control.Applicative ((<$>), (<*>))
-- import Data.Maybe (fromJust)
import Control.Applicative
import Control.Monad (mzero)
import Database.HDBC (SqlValue, fromSql)
import Prelude
data IncPK = IncPK {
incId :: Int
@@ -27,9 +28,28 @@ instance JSON.FromJSON IncPK where
r .: "inserted_at"
parseJSON _ = mzero
fromList :: [(String, SqlValue)] -> IncPK
fromList row = IncPK
(fromSql . fromJust $ lookup "id" row)
(fromSql . fromJust $ lookup "nullable_string" row)
(fromSql . fromJust $ lookup "non_nullable_string" row)
(fromSql . fromJust $ lookup "inserted_at" row)
-- incFromList :: [(String, SqlValue)] -> IncPK
-- incFromList row = IncPK
-- (fromSql . fromJust $ lookup "id" row)
-- (fromSql . fromJust $ lookup "nullable_string" row)
-- (fromSql . fromJust $ lookup "non_nullable_string" row)
-- (fromSql . fromJust $ lookup "inserted_at" row)
data CompoundPK = CompoundPK {
compoundK1 :: Int
, compoundK2 :: Int
, compoundExtra :: Maybe Int
}
instance JSON.FromJSON CompoundPK where
parseJSON (JSON.Object r) = CompoundPK <$>
r .: "k1" <*>
r .: "k2" <*>
r .: "extra"
parseJSON _ = mzero
-- compoundFromList :: [(String, SqlValue)] -> CompoundPK
-- compoundFromList row = CompoundPK
-- (fromSql . fromJust $ lookup "k1" row)
-- (fromSql . fromJust $ lookup "k2" row)
-- (fromSql . fromJust $ lookup "extra" row)
-36
View File
@@ -1,36 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module Unit.ErrorsSpec where
import Test.Hspec
import Database.HDBC (runRaw, quickQuery, fromSql, SqlError)
import SpecHelper (dbWithSchema)
import Middleware (withSavepoint)
import PgQuery (insert)
import Types(SqlRow(..))
import Control.Exception(catch)
import Control.Monad(void)
import Network.Wai (defaultRequest, responseLBS)
import Network.HTTP.Types.Status (ok200)
spec :: Spec
spec = let
dbErrApp conn _ res = do
putStrLn "In fake app"
_ <- insert "1" "items" (SqlRow []) conn
runRaw conn "select 1/0"
_ <- insert "1" "items" (SqlRow []) conn
res $ responseLBS ok200 [("Content-Type", "application/json")] "{}"
in around dbWithSchema $
describe "withSavepoint" $
it "allows partial rollback of request" $ \c -> do
let app = withSavepoint dbErrApp c
[[beforeCount]] <- quickQuery c "select count(*) from \"1\".items" []
runRaw c "set role dbapi_anonymous"
_ <- insert "1" "items" (SqlRow []) c
catch (void $ app defaultRequest (const undefined) ) $
\e -> let _ = (e::SqlError) in do
_ <- insert "1" "items" (SqlRow []) c
[[afterCount]] <- quickQuery c "select count(*) from \"1\".items" []
fromSql afterCount `shouldBe` (fromSql beforeCount::Int) + 2
@@ -1,15 +1,16 @@
{-# LANGUAGE OverloadedStrings #-}
module Unit.PgQuerySpec where
import Test.Hspec
import Test.QuickCheck
import Test.QuickCheck.Monadic
import Database.HDBC (IConnection, SqlValue, toSql, prepare,
quickQuery, fromSql, execute, seState, fetchAllRowsAL)
import PgQuery (LoginAttempt(..), insert, addUser, signInRole, checkPass)
import PgQuery (LoginAttempt(..), insert, addUser, signInRole, checkPass
, pgFmtIdent, pgFmtLit)
import Types (SqlRow(SqlRow))
import TestTypes (fromList, incStr, incNullableStr, incInsert, incId)
import TestTypes (incFromList, incStr, incNullableStr, incInsert, incId)
import Data.Map (toList)
import Data.String.Conversions (cs)
import Data.Monoid ((<>))
@@ -33,13 +34,13 @@ spec = around dbWithSchema $ do
it "inserts and responds with a full object description" $ \conn -> do
r <- insert "1" "auto_incrementing_pk" (SqlRow [
("non_nullable_string", toSql ("a string"::String))]) conn
let returnRow = fromList . toList $ r
let returnRow = incFromList . toList $ r
incStr returnRow `shouldBe` "a string"
incNullableStr returnRow `shouldBe` Nothing
incInsert returnRow `shouldSatisfy` not . null
incId returnRow `shouldSatisfy` (>= 0)
tRows <- quickALQuery conn "select * from \"1\".auto_incrementing_pk" []
[returnRow] `shouldBe` map fromList tRows
[returnRow] `shouldBe` map incFromList tRows
it "throws an exception if the PK is not unique" $ \conn -> do
r <- insert "1" "auto_incrementing_pk" (SqlRow [
@@ -63,7 +64,7 @@ spec = around dbWithSchema $ do
describe "addUser" $ do
it "adds a correct user to the right table" $ \conn -> do
addUser user pass role conn
[r] <- quickQuery conn "select * from dbapi.auth" []
[r] <- quickQuery conn "select * from postgrest.auth" []
let [newUser, newRole, encryptedPass] = map fromSql r :: [String]
cs newUser `shouldBe` user
cs newRole `shouldBe` role
@@ -78,8 +79,21 @@ spec = around dbWithSchema $ do
addUser user pass role conn
return conn) $ do
it "accepts correct credentials and return the role" $ \conn ->
signInRole user pass conn `shouldReturn` LoginSuccess role
signInRole user pass conn `shouldReturn` LoginSuccess role user
it "returns nothing with bad creds" $ \conn -> do
signInRole "not-a-user" pass conn `shouldReturn` LoginFailed
signInRole user (pass <> "crap") conn `shouldReturn` LoginFailed
describe "pgFmtIdent" $
it "Does what format %I would do" $ \conn -> property $ \fuzz ->
monadicIO $ do
[[row]] <- run $ quickALQuery conn "select format('%I', ? :: varchar)" [toSql (fuzz :: String)]
assert $ fromSql (snd row) == pgFmtIdent (cs fuzz)
describe "pgFmtLit" $
it "Does what format %L would do" $ \conn ->
property $ monadicIO $ do
fuzz <- pick arbitrary
[[row]] <- run $ quickALQuery conn "select format('%L', ? :: varchar)" [toSql (fuzz :: String)]
assert $ fromSql (snd row) == pgFmtLit (cs fuzz)
@@ -14,7 +14,7 @@ spec = around dbWithSchema $ beforeWith setRole $ do
it "shows all the tables" $ \conn -> do
ts <- tables "1" conn
map tableName ts `shouldBe` ["authors_only","auto_incrementing_pk",
"compound_pk","has_fk","items","menagerie","no_pk", "simple_pk"]
"compound_pk","has_fk","insertable_view_with_join","items","menagerie","no_pk", "simple_pk"]
describe "columns" $ do
it "responds with each column for the table" $ \conn -> do
@@ -34,4 +34,4 @@ spec = around dbWithSchema $ beforeWith setRole $ do
("auto_inc_fk", ForeignKey {fkTable="auto_incrementing_pk", fkCol="id"}),
("simple_fk", ForeignKey { fkTable="simple_pk", fkCol="k"})]
where setRole conn = quickQuery conn "set role dbapi_test" [] >> return conn
where setRole conn = quickQuery conn "set role postgrest_test" [] >> return conn
+1 -1
View File
@@ -1 +1 @@
pg_dump --host localhost --port 5432 --username "postgres" --no-password --format plain --column-inserts --verbose --file "./test/fixtures/schema.sql" "dbapi_test"
pg_dump --host localhost --port 5432 --username "postgres" --no-password --format plain --column-inserts --verbose --file "./test/fixtures/schema.sql" "postgrest_test"
+3 -6
View File
@@ -11,9 +11,6 @@ BEGIN
END;
$$;
select pg_temp.create_role_if_not_exists('dbapi_anonymous', 'with nologin');
select pg_temp.create_role_if_not_exists('test_default_role', 'with nologin');
select pg_temp.create_role_if_not_exists('dbapi_test_author', 'with nologin');
select pg_temp.create_role_if_not_exists('dbapi_test_author_a', 'with nologin in role dbapi_test_author');
select pg_temp.create_role_if_not_exists('dbapi_test_author_b', 'with nologin in role dbapi_test_author');
select pg_temp.create_role_if_not_exists('postgrest_anonymous', 'with nologin') as a
, pg_temp.create_role_if_not_exists('test_default_role', 'with nologin') as b
, pg_temp.create_role_if_not_exists('postgrest_test_author', 'with nologin') into temp shh;
+399 -319
View File
File diff suppressed because it is too large Load Diff