Compare commits

..
553 Commits
Author SHA1 Message Date
Joe Nelson b46b3c7b9a Conditional compilation for signal handling
Will not compile on windows
2015-12-16 18:05:48 -08:00
Joe Nelson 079cf0aa54 v0.3.0.2 2015-12-16 17:19:19 -08:00
Joe Nelson 0589ddcd90 Merge pull request #424 from calebmer/feature/select-column
Enable selection by column
2015-12-16 16:09:25 -08:00
calebmer f85975f5ad Add docs for column embedding 2015-12-16 18:37:35 -05:00
calebmer d1de6615f2 Add back in missing dependency after rebase 2015-12-16 18:37:25 -05:00
calebmer 8f0ba7c41e Unify JSON definition in tests 2015-12-16 18:24:04 -05:00
calebmer 7392d204bf More inclusive foreign key ducktyping 2015-12-16 18:24:04 -05:00
calebmer ee82ad1864 Better documentation adding relations 2015-12-16 18:24:03 -05:00
calebmer 6446fc962d Add changelog note 2015-12-16 18:24:03 -05:00
calebmer 88d4798d5e Add column select sans id support 2015-12-16 18:24:03 -05:00
calebmer abe87f2f16 Add support for selecting by column 2015-12-16 18:24:02 -05:00
Joe Nelson b08a402df8 Merge pull request #417 from ruslantalpa/fix_399_remove_returning
Fix #399 insert records in tables with no SELECT privileges
2015-12-16 11:35:50 -08:00
Joe Nelson f95b501232 Update stack installation instructions 2015-12-16 09:34:28 -08:00
Ruslan Talpa 4544ce3255 Fix #399 insert records in tables with no SELECT privileges 2015-12-16 10:16:14 +02:00
Joe Nelson 4bc4a68051 Merge pull request #425 from ruslantalpa/fix_414_revert_count_query_no_cte
#414 revert to separate count query
2015-12-15 12:06:19 -08:00
Ruslan Talpa 1acd07cb61 fix #414 revert to separate count query 2015-12-15 12:04:04 +02:00
Joe Nelson 2cf903cebe Merged feature/default-schema 2015-12-14 08:49:52 -08:00
Joe Nelson 9ee2b74a5d Merge pull request #422 from calebmer/feature/always-quote
`pgFmtIdent` always quotes #388
2015-12-13 19:19:38 -08:00
calebmer 6ef9000b31 pgFmtIdent always quotes #388 2015-12-13 16:58:56 -05:00
calebmer bacc899fb4 Change default schema #389 2015-12-13 16:36:32 -05:00
Joe Nelson 7f0dc82d7a Beginning of security docs 2015-12-11 22:28:00 -08:00
Joe Nelson 155e2d1c0c Merge pull request #406 from begriffs/redocument-brew
Add brew install instructions back
2015-12-11 09:45:37 -08:00
Joe Nelson ccadcec6d5 Merge branch 'spec-cache-dbstructure' 2015-12-11 09:42:26 -08:00
Joe Nelson 02f707cd5c Merge pull request #421 from ruslantalpa/spec-cache-dbstructure
Fix failing test after test structure optimization
2015-12-11 09:37:51 -08:00
Joe Nelson 848c7da4d9 Merge pull request #420 from NikolayS/patch-1
ordering with DESC & NULLS LAST together
2015-12-11 09:24:54 -08:00
Ruslan Talpa 3cd01f9859 Fix failing test after test structure optimization 2015-12-11 10:23:11 +02:00
NikolayS aed97d97f8 ordering with DESC & NULLS LAST together
One more sample query which is probably one of most useful ones in real life (by default, DESC ordering leads to NULLS FIRST option, this is not so clear for beginners), showing how to combine ordering direction and NULLS *** for the same column.
2015-12-11 09:22:45 +03:00
Joe Nelson b3144aee15 Cache schema structure between tests for speed
Causes one test to fail
2015-12-10 13:36:28 -08:00
Joe Nelson 87ceac7b00 Document basic_auth.current_email() 2015-12-10 11:23:10 -08:00
Ruslan Talpa a6512a2a69 Test and fixes for #407 #403 #107 2015-12-09 20:59:42 +02:00
Joe Nelson 6f7bf30a2d Merge pull request #416 from ruslantalpa/fix_396_nullable_fk
Fix #396 include records with missing parents and return [] instead of  null for missing children
2015-12-09 10:29:45 -08:00
Ruslan Talpa dad41cd3ed Fix #396 include records with missing parents and remove CTE related to parent relation 2015-12-09 14:21:27 +02:00
Joe Nelson 52849065cc Merge pull request #410 from diogob/refactor_tests
Refactor tests
2015-12-08 13:43:02 -08:00
Joe Nelson 728decb0b8 Change brew install time estimate 2015-12-08 10:31:36 -08:00
Diogo Biazus 4cc0189ac9 Prefixes all roles used in test suite with postgrest_test 2015-12-08 12:03:46 -05:00
Diogo Biazus 8b4cb4be6e Replaces postgrest_authenticator for postgrest_test_authenticator 2015-12-08 11:53:18 -05:00
Diogo Biazus d95aeb15f9 Reduces privileges fixture to a smaller file so it's manageable without the use of pg_dump 2015-12-08 11:50:13 -05:00
Diogo Biazus 6cc8ca707f Separates privileges in another fixture 2015-12-08 11:33:22 -05:00
Diogo Biazus 63805d05a1 Removes GRANT ALL and let Structure spec pending 2015-12-08 11:11:48 -05:00
Diogo Biazus f66f9e7c92 Adds -d postgrest to setup database step in SpecHelper 2015-12-08 11:06:50 -05:00
Joe Nelson 213d86f3e3 Merge pull request #409 from begriffs/ctrl-c
Release db connections on sigint
2015-12-07 23:46:37 -08:00
Joe Nelson 969a95b29b Remove shutdown message which spoils log format 2015-12-07 23:34:34 -08:00
Diogo Biazus ca3ab7babf Cleanup code in SpecHelper 2015-12-08 01:01:38 -05:00
Diogo Biazus 0961524e70 Refactors tests to use new fixtures and remove some haskell functions that wont be necessary anymore 2015-12-08 00:44:49 -05:00
Diogo Biazus 027bbc6074 Adds separate fixture file with commands to create test database 2015-12-08 00:00:00 -05:00
Diogo Biazus c1c44aae05 Adds separate fixture file with data used in specs 2015-12-07 23:59:33 -05:00
Joe Nelson 3bc1ad0133 Changelog 2015-12-07 16:55:45 -08:00
Joe Nelson ca5078b4f8 Alphabetize other imports 2015-12-07 16:54:59 -08:00
Joe Nelson 67a3194903 Release all db connections on ctrl-c
Fixes #268
2015-12-07 16:54:59 -08:00
Joe Nelson 24b7a7d3e9 Merge pull request #403 from ruslantalpa/master
Detect relations event when authenticator does not have rights to intermediate tables
2015-12-07 15:06:21 -08:00
Joe Nelson 0e56bff5d6 Revert "Removed brew install instructions"
I know how to release a new brew formula so it won't fall out of date
2015-12-07 13:46:06 -08:00
Joe Nelson b8b5fac03c Merge pull request #404 from begriffs/max-rows
Add --max-rows option
2015-12-06 15:57:37 -08:00
Joe Nelson e88fa7db31 Docs and changelog 2015-12-05 19:59:30 -08:00
Joe Nelson 598b2bfbda Remove lint 2015-12-05 19:50:07 -08:00
Joe Nelson f4c73c7666 Test for --max-rows behavior 2015-12-05 19:31:53 -08:00
Joe Nelson cbbb1871bb Add --max-rows option (no automated tests yet)
Fixes #288
2015-12-05 19:03:18 -08:00
Ruslan Talpa 093874469e Detect relations event when authenticator does not have rights to intermediate tables 2015-12-04 17:45:50 +02:00
Joe Nelson d21120962d Blog example 2015-12-03 21:58:55 -08:00
Joe Nelson 27ad4641d0 Merge pull request #400 from diogob/order_by_computed_column
Order by computed column
2015-12-02 19:19:02 -08:00
Diogo Biazus 65e7744858 Uses anti_id computed column to make test case clearer 2015-12-02 17:12:37 -05:00
Diogo Biazus c2e3dd716c Updates CHANGELOG 2015-12-02 15:28:27 -05:00
Diogo Biazus 1266bad2f8 Adds another note about computed coulmns to the docs 2015-12-02 15:27:27 -05:00
Diogo Biazus 71dab115c9 Uses qualified column name in order by clause to allow computed columns 2015-12-02 15:23:10 -05:00
Joe Nelson 205aec20fe Note min postgres version in docs 2015-11-30 21:10:15 -08:00
Joe Nelson 83fa3070fb Add intro video to docs 2015-11-30 14:30:51 -08:00
Joe Nelson a2870494c9 Merge pull request #397 from begriffs/bye-bcrypt
bcrypt is no longer a required dependency
2015-11-29 23:38:37 -08:00
Joe Nelson 614b4dfbab Remove deps deemed unnecessary by packunused
Also sort build-depends
2015-11-29 23:32:13 -08:00
Joe Nelson cbc6685725 bcrypt is no longer a required dependency
Fixes #395
2015-11-29 23:02:34 -08:00
Joe Nelson f17d47790a Better docs for command line usage
Mentions #277 and how to prevent it
2015-11-29 22:46:34 -08:00
Joe Nelson 334f900e15 Ask for sql logs in bug reports 2015-11-29 19:44:57 -08:00
Joe Nelson 6804d91f8d Merge pull request #393 from begriffs/user-mgmt-docs
Document how to build username/pw auth on top of jwt
2015-11-29 12:48:15 -08:00
Joe Nelson 54bf0b460f Finish auth example docs 2015-11-29 12:43:44 -08:00
Joe Nelson fa281fe59c WIP: documenting how to build username/pw auth on top of jwt 2015-11-28 16:07:51 -08:00
Joe Nelson 048a8531f2 Merge pull request #392 from calebmer/hotfix/jwt-time
Fix time fetch on initialization
2015-11-28 15:06:41 -08:00
Caleb Meredith ed51502387 Add unreleased line to top of changelog 2015-11-28 18:03:27 -05:00
Caleb Meredith e7a47215f0 Update CHANGELOG.md 2015-11-28 17:56:37 -05:00
calebmer b04e2ec663 Change updated package error to warning 2015-11-28 17:41:54 -05:00
calebmer b651a45734 Fix time fetch on initialization 2015-11-28 16:49:12 -05:00
Joe Nelson 1a54135f0d Add authenticator role to blog schema 2015-11-27 23:23:23 -08:00
Joe Nelson c6f69956f6 Inline fk constraints in example 2015-11-27 23:07:29 -08:00
Joe Nelson c681ff2d9d Update reading/writing docs 2015-11-27 22:59:11 -08:00
Joe Nelson 5e22538684 Link to Ruslan's blog demo 2015-11-27 22:59:11 -08:00
Joe Nelson d6d0ba524f Removed brew install instructions
The brew formula gets outdated
2015-11-27 22:59:11 -08:00
Joe Nelson cd53402ace Update the getting started example for v0.3 2015-11-27 22:59:07 -08:00
Joe Nelson 19b856db3b v0.3.0.1 2015-11-27 13:04:47 -08:00
Joe Nelson 4b911263e3 Merge pull request #386 from ruslantalpa/master
bugfix + Disambiguate range selection when plurality=singular
2015-11-27 00:49:59 -08:00
Ruslan Talpa c0fa5c3d4a changelog entry 2015-11-25 10:13:34 +02:00
Ruslan Talpa b3055888c5 Merge remote-tracking branch 'begriffs/master' 2015-11-25 10:10:11 +02:00
Ruslan Talpa 48ee77a64d bugfix: filter columns on embeded parent objects 2015-11-24 12:53:50 +02:00
Joe Nelson 6f9fc7adac Mark release in changelog 2015-11-24 00:08:31 -08:00
Ruslan Talpa f678dff735 disambiguate range selection when plurality=singular 2015-11-24 09:39:44 +02:00
Joe Nelson e623cf8198 Coming in for landing on v0.3 2015-11-23 23:09:59 -08:00
Joe Nelson 0fd02ddc1d Merge pull request #385 from begriffs/03readme
Update readme for v3
2015-11-23 23:03:53 -08:00
Joe Nelson 8250008130 Update readme for v3
Link to the new docs, not wikis
2015-11-23 22:50:43 -08:00
Joe Nelson 377aeefde9 Test tricky situation for plurality=singular 2015-11-23 17:15:09 -08:00
Joe Nelson 1170137e7f Merge pull request #377 from begriffs/simplify
Disentangle code
2015-11-23 12:49:54 -08:00
Joe Nelson a5e7d9aff0 Update changelog 2015-11-23 11:15:30 -08:00
Joe Nelson ae80d08484 Merge pull request #384 from diogob/reduces_querybuilder_interface
Reduces querybuilder interface
2015-11-23 11:04:50 -08:00
Joe Nelson 029276bd62 Use brackets to specify subqueries 2015-11-23 10:58:04 -08:00
Diogo Biazus addd47c09f Adds haddock string to QueryBuilder module 2015-11-23 13:56:47 -05:00
Diogo Biazus be9cce0043 Moves createReadStatement and createWriteStatement to QueryBuilder since they build queries. Also reduces QueryBuilder module interface. 2015-11-23 13:45:10 -05:00
Joe Nelson 64da08b220 Merge pull request #383 from ruslantalpa/simplify
Type refactoring (ApiRequest/DbRequest)
2015-11-23 09:31:01 -08:00
Ruslan Talpa 50745ad48b Type refactoring (ApiRequest/DbRequest) 2015-11-23 10:12:29 +02:00
Joe Nelson 7f58600dbb All green
But still unsightly
2015-11-22 22:38:54 -08:00
Joe Nelson bc552848ba Prevent inserting CSV with varying row length 2015-11-22 21:51:42 -08:00
Joe Nelson 936a368be7 Update only the columns specified in the json payload 2015-11-22 17:46:00 -08:00
Joe Nelson d03d68c25f Simplify checking object keys for equality 2015-11-22 15:00:28 -08:00
Joe Nelson 1f6fc5cbd8 Ensure JSON payload objects all have same keys 2015-11-22 13:51:46 -08:00
Joe Nelson 60007b5f10 WIP: Insertion memory leak fixed
But a whole lot of other things broken, including updates
2015-11-20 13:08:23 -08:00
Joe Nelson f54742186b Use show instances for a shortcut 2015-11-20 10:29:08 -08:00
Joe Nelson d2d86b35c4 Merge pull request #380 from ruslantalpa/simplify
Get the refactored code to compile and pass tests
2015-11-20 09:07:29 -08:00
Ruslan Talpa 78fd766de3 small refactor 2015-11-20 18:19:13 +02:00
Ruslan Talpa cf2e45d47f Fix lint issue 2015-11-20 16:11:21 +02:00
Ruslan Talpa 0cce22f8c1 check request payload for structure & remove bad test for csv 2015-11-20 16:07:20 +02:00
Ruslan Talpa aa2f0287b1 Fix RPC failing tests 2015-11-20 15:50:53 +02:00
Ruslan Talpa f18cfbd7f4 Stricter pattern matching & case branches rearangement + remove a few small functions 2015-11-20 15:36:04 +02:00
Ruslan Talpa f5bb898992 delete commented code 2015-11-20 14:10:28 +02:00
Ruslan Talpa 7f52430e0e Get the refactored code to compile (5 test failing) 2015-11-20 14:04:00 +02:00
Joe Nelson c80ab6be0f WIP: converting App 2015-11-19 20:45:24 -08:00
Joe Nelson b6a58935f3 Greater type safety for OrderTerm 2015-11-19 11:21:27 -08:00
Joe Nelson f3d5759c1a JWT is handled by middleware, do not need it in Intent 2015-11-19 11:20:30 -08:00
Joe Nelson 87c946ef52 Treat JWT as a Secret, not String 2015-11-19 09:58:41 -08:00
Joe Nelson 2c3a52fc35 Cool person style for long comments 2015-11-19 00:03:48 -08:00
Joe Nelson 496cd77510 More docs for pickContentType 2015-11-18 23:56:59 -08:00
Joe Nelson 8f04967103 Provide more context about unknown routes 2015-11-18 23:51:59 -08:00
Joe Nelson b260cff6fd Provide more context for unknown actions 2015-11-18 23:51:38 -08:00
Joe Nelson ffeb02e72e Include request payload in userIntent 2015-11-18 23:43:47 -08:00
Joe Nelson ed18f2b1e8 Function to convert parsed CSV to array of JSON objects 2015-11-18 20:21:21 -08:00
Joe Nelson a0af096ff4 WIP: translate HTTP request into user intent
The app code will base its logic off the intent data
2015-11-18 19:18:13 -08:00
Joe Nelson c21e5a9fc8 Merge pull request #375 from calebmer/feature/plurality-singular
Implement single route selection
2015-11-18 14:39:33 -08:00
calebmer 79caba278e Add changelog entry 2015-11-18 17:30:37 -05:00
Joe Nelson 80da27237d Merge pull request #376 from ruslantalpa/master
Refactor parseRequest buildSelectApiRequest, add buildMutateApiRequest function
2015-11-17 12:44:29 -08:00
Ruslan Talpa f24dd90e51 refactor buildSelectApiRequest 2015-11-17 14:19:23 +02:00
Ruslan Talpa 17920b85c7 Refactor parseRequest, add buildMutateApiRequest function 2015-11-17 10:26:06 +02:00
Joe Nelson ffd45ef0b5 Merge pull request #374 from calebmer/feature/error-refactor
Small error refactor
2015-11-16 14:50:54 -08:00
calebmer ad555986d4 Error refactors 2015-11-16 17:28:39 -05:00
calebmer 54eb0d3ec4 Implement single route selection 2015-11-16 17:21:07 -05:00
Joe Nelson aa87853e71 Merge pull request #373 from begriffs/schema-templates
Schema templates
2015-11-16 13:27:32 -08:00
Joe Nelson 65c5054c93 Merge pull request #372 from ruslantalpa/app_refactor
App.hs refactor (3)
2015-11-16 13:21:22 -08:00
Joe Nelson 8711362cf6 Merge pull request #370 from ruslantalpa/add_type_syn
Add type synonyms to to beter explain things that used to be Text (2)
2015-11-16 08:52:06 -08:00
Joe Nelson 57aad6baa6 Merge pull request #369 from ruslantalpa/master
Clean up things like T.Text T.intercalate in QueryBuilder (1)
2015-11-16 08:49:22 -08:00
Ruslan Talpa 0c3545fa09 App.hs refactor 2015-11-16 12:26:55 +02:00
Ruslan Talpa 75646247f2 Add type synonims to to beter explain things that used to be Text 2015-11-16 10:40:46 +02:00
Ruslan Talpa 3152b24d3f Clean up things like T.Text T.intercalate in QueryBuilder 2015-11-16 09:55:10 +02:00
Joe Nelson 8171a9959d Merge pull request #363 from diogob/refactor_query_builder
Merge PgQuery and QueryBuilder...
2015-11-15 10:30:29 -08:00
Joe Nelson c4018ad882 Merge pull request #366 from calebmer/patch-1
Summarize column synonym change
2015-11-15 09:21:52 -08:00
Caleb Meredith 62951f29ac Update CHANGELOG.md 2015-11-15 11:05:52 -05:00
Diogo Biazus 9dc3399d4a Adds Schema type alias back to QueryBuilder 2015-11-15 10:18:34 -05:00
Joe Nelson e0c99f52e0 Merge pull request #364 from ruslantalpa/master
Shape mutation response feture mention in changelog
2015-11-14 23:04:29 -08:00
Ruslan Talpa 6c0966f656 shape mutation response feture mention in changelog 2015-11-15 08:27:34 +02:00
Diogo Biazus 9da7db9e09 Moves some top level bindings to 'where' when it makes sense 2015-11-14 23:08:35 -05:00
Diogo Biazus f47d5e52f4 Moves all PgQuery module to QueryBuilder 2015-11-14 22:54:55 -05:00
Diogo Biazus fbe6600ff1 Makes exports explicit and move some functions around 2015-11-14 22:43:03 -05:00
Joe Nelson a6cfecbc29 Merge v3 2015-11-14 17:52:05 -08:00
Joe Nelson 51e1d8d796 Merge pull request #353 from calebmer/feature/view-relations
Better view relations
2015-11-14 10:00:46 -08:00
calebmer 0bf3dd9b1c Remove extraneous files 2015-11-14 08:02:09 -05:00
Joe Nelson 98caf9e091 Light validation on email column 2015-11-13 13:16:40 -08:00
Joe Nelson 71e6d0414d Inline constraints 2015-11-13 12:59:45 -08:00
calebmer 3794d358b4 Prettify monads 2015-11-12 15:09:28 -05:00
calebmer 2f6254f44c Rename DbStructure names 2015-11-12 15:01:23 -05:00
Joe Nelson 6cc493da6b Version 0.2.12.1 2015-11-12 11:06:11 -08:00
Joe Nelson e8bac0c739 Merge pull request #355 from ruslantalpa/v3
Small bug fix introduced by the last JWT PR
2015-11-12 08:40:21 -08:00
calebmer 0f5bc34c04 Remove fromJust assumptions 2015-11-12 08:22:27 -05:00
Ruslan Talpa 6e9a28ba3c fix the missing status400 include 2015-11-12 15:19:11 +02:00
calebmer acb8f8a154 Fix failing insert test 2015-11-12 08:14:14 -05:00
Ruslan Talpa eb94c507f9 remove secure flag from debian config 2015-11-12 15:07:40 +02:00
calebmer 1061854f35 Resolve SQL errors 2015-11-12 07:38:11 -05:00
calebmer b8b073810f Rename functions 2015-11-11 15:53:52 -05:00
Joe Nelson 22c50392e5 Merge pull request #344 from calebmer/feature/jwt-expire
Ensure JWT expires
2015-11-11 08:31:20 -08:00
Joe Nelson 80928535b0 Active flag is more accurately called verified 2015-11-11 08:15:06 -08:00
calebmer 0374d4e651 Add tables back to DbStructure 2015-11-11 10:32:19 -05:00
calebmer 16af8fc61e Better relations for views
- Better column synonyms detection
- Raise relations to accessible schema when possible
2015-11-11 09:11:57 -05:00
calebmer e1e4fe6d5c Types reference each other 2015-11-11 09:09:53 -05:00
calebmer 3a682360f3 Remove view relations from sql statement 2015-11-11 09:03:06 -05:00
calebmer 7560fafbab Refactor DbStructure
- Rename `dbstructure` to `db` in App.hs
- Rename PgStructure* to DbStructure*
- Move `DbStructure` creation to DbStructure.hs
2015-11-11 09:03:06 -05:00
calebmer 62cb8e0453 Cleanup JWT expires 2015-11-11 08:34:50 -05:00
Joe Nelson 3f1d2d8ed9 Merge pull request #342 from ruslantalpa/v3
Refactoring & 	Revert to the old way of displaying the list of tables
2015-11-11 01:03:07 -08:00
Joe Nelson ad316841f2 Restrict possible values of token_type 2015-11-10 21:09:56 -08:00
Joe Nelson 807e4b7787 Turn signup() into a plain sql function 2015-11-10 21:07:14 -08:00
Joe Nelson 91f15aa0e3 Use uuid type for token and name for role 2015-11-10 21:00:50 -08:00
Joe Nelson bda5f0a176 Match trigger names with their functions 2015-11-10 21:00:50 -08:00
Joe Nelson 86d992c21f Roles 2015-11-10 21:00:50 -08:00
Joe Nelson a508f8df37 Remove usernames, just use email 2015-11-10 21:00:50 -08:00
Joe Nelson 1b28f77854 Allow authors to see only themselves in /users 2015-11-10 21:00:50 -08:00
Joe Nelson c8479e792f Actual blog and row level security! 2015-11-10 21:00:50 -08:00
Joe Nelson 22fb13b30a Shorten login function to login 2015-11-10 21:00:50 -08:00
Joe Nelson 9daaf6ba70 s/logins/users 2015-11-10 21:00:50 -08:00
Joe Nelson 64172873e4 Explanation about split schemas 2015-11-10 21:00:50 -08:00
Joe Nelson 3a659843d2 Limit field lengths in logins
It is open to the public so people could abuse the storage

Also switch to text type everywhere
2015-11-10 21:00:49 -08:00
Joe Nelson 9fa16e2053 Handle missing "active" key in logins update
The default value of underlying column gets clobbered by triggers
2015-11-10 21:00:49 -08:00
Joe Nelson c25cd1b97e Use uuid-ossp extension rather than custom function 2015-11-10 21:00:49 -08:00
Joe Nelson 1e017b86d3 Drop trigger before (re)creating it 2015-11-10 21:00:49 -08:00
Joe Nelson 1524a4fe74 User management view and trigger 2015-11-10 21:00:49 -08:00
Joe Nelson d4ef343b8d Leave public functions in current schema
Put supporting things into basic_auth schema

Also tested from a clean db
2015-11-10 21:00:49 -08:00
Joe Nelson 2687ad63dd Basic tables/procs for basic auth 2015-11-10 21:00:49 -08:00
Joe Nelson 08fc83709c Merge pull request #352 from ruslantalpa/master
A simpler way for non haskell devs to install postgrest
2015-11-10 08:28:33 -08:00
Ruslan Talpa 92174dc0d2 modify build from source steps to also copy the bin to the right location (remove small comment) 2015-11-10 10:31:22 +02:00
Ruslan Talpa 2d72847d39 modify build from source steps to also copy the bin to the right location 2015-11-10 10:30:07 +02:00
Ruslan Talpa 1b402abaf2 Merge remote-tracking branch 'begriffs/v3' into v3 2015-11-10 10:22:06 +02:00
Joe Nelson 368bf34842 Merge pull request #345 from calebmer/feature/insecure-forbidden
Do not redirect insecure requests
2015-11-08 10:24:01 -08:00
calebmer c024687629 Remove secure flag entirely 2015-11-08 13:03:50 -05:00
Joe Nelson 4f7dd12133 Merge pull request #348 from xDAGRONx/homebrew
Add documentation for installing via Homebrew
2015-11-06 10:12:19 -08:00
JC Wilcox 8b16a1ee33 Add documentation for installing via Homebrew
Modify the server installation guide to include steps for installation
using Hombrew on OS X.
2015-11-06 10:23:33 -06:00
Joe Nelson e210283a70 Merge pull request #346 from begriffs/jwt-proc-return
Encode JWT when proc return types end in jwt_claims
2015-11-05 17:58:45 -08:00
Joe Nelson 3d2a78e962 Encode JWT when proc return types end in jwt_claims
Fixes it when the jwt_claims type is defined in a non-default schema
2015-11-05 17:46:33 -08:00
calebmer 34c153086c Do not redirect insecure requests 2015-11-05 17:42:48 -05:00
calebmer aab2f0d1f1 Ensure JWT expires 2015-11-05 17:04:54 -05:00
Ruslan Talpa cfad68f5cb Fix test 2015-11-04 16:28:22 +02:00
Ruslan Talpa 37b1d7d692 fix debian scripts to match current parameters 2015-11-04 16:23:02 +02:00
Ruslan Talpa 28b7b80bbb remove PUT from cors 2015-11-04 16:22:13 +02:00
Ruslan Talpa 14e806759d Revert to the old way of displaying the list of tables 2015-11-03 09:31:56 +02:00
Joe Nelson 5e9d29e07b Merge pull request #340 from ruslantalpa/master
Fix for #334
2015-11-02 17:27:33 -08:00
Ruslan Talpa 5113187bb1 changelog update and lint fix 2015-11-02 23:35:59 +02:00
Ruslan Talpa b3e88a37d3 Refacttoring 2015-11-02 23:31:35 +02:00
Joe Nelson 4803d7c828 Merge pull request #339 from ruslantalpa/v3
Refactor to add operators in just one place, @> and <@ operators for …
2015-11-02 09:44:06 -08:00
Ruslan Talpa ef08af359c Fix for #334 2015-11-02 14:44:45 +02:00
Ruslan Talpa 2e4c862d25 Refactor to add operators in just one place, @> and <@ operators for #338 and #181 2015-11-02 10:49:22 +02:00
Joe Nelson 6b118819d7 Merge pull request #337 from diogob/changes_stack_resolver
Changes stack resolver to nightly.
2015-11-01 15:15:11 -08:00
Diogo Biazus 2c1236652e Fixes hlint suggestions 2015-11-01 16:49:21 -05:00
Diogo Biazus 066d120c0e Changes stack resolver to nightly. 2015-10-31 12:01:26 -04:00
Joe Nelson 6b7e833023 Merge pull request #331 from ruslantalpa/v3
Code refactoring POST/PUT/PATCH to be more like GET and use internal data type
2015-10-31 00:02:39 -07:00
Joe Nelson 31c3edc183 Merge pull request #335 from PierreR/jsonb
Add a test case for issue #334
2015-10-30 13:10:51 -07:00
Pierre Radermecker 2cfe3651b4 Add a test case for issue #334 2015-10-30 19:16:59 +01:00
Ruslan Talpa 246c47dba4 cleanup 2015-10-30 12:19:35 +02:00
Ruslan Talpa 6fd0d5648f PUT path commented, DELETE rewritten, all statementT functions commented 2015-10-30 12:08:25 +02:00
Ruslan Talpa 738989c375 Cleanup 2 2015-10-29 16:22:31 +02:00
Ruslan Talpa 9458ee3292 Cleanup / Refactoring 2015-10-29 16:04:16 +02:00
Joe Nelson 1c22b8d429 Merge pull request #333 from diogob/fix_empty_set_returning_function
Fixes 500 when set returning function returns an empty row set [fix #332]
2015-10-28 10:57:05 -07:00
Diogo Biazus c6956d0ff6 Adds fix to CHANGELOG 2015-10-28 09:17:25 -04:00
Ruslan Talpa 915ce0fa9d Fix for detecting many2many relations when the link table for more then 2 tables 2015-10-28 10:41:16 +02:00
Diogo Biazus 4369a5617e Fixes 500 when set returning function returns an empty row set [fix #332] 2015-10-27 21:19:55 -04:00
Ruslan Talpa 482a43d722 PATCH path rewriten in new style 2015-10-27 16:27:44 +02:00
Ruslan Talpa f7e6005087 cleanup 2015-10-27 14:14:04 +02:00
Ruslan Talpa f02b8381ea shape the response after inserting 2015-10-27 13:16:14 +02:00
Ruslan Talpa 58d009b388 Merge branch 'v3' of https://github.com/ruslantalpa/postgrest into v3 2015-10-26 16:21:09 +02:00
Ruslan Talpa d43bac6e8f Code cleanup (lint suggestions) 2015-10-26 16:19:54 +02:00
Ruslan Talpa d8b7332acc Code cleanup (lint suggestions) 2015-10-26 15:27:28 +02:00
Ruslan Talpa 1fdb700bc8 Fix a few tests 2015-10-26 13:50:56 +02:00
Joe Nelson bd1826670e Version 0.2.12.0 2015-10-25 11:46:16 -07:00
Ruslan Talpa c265cf829b Merge remote-tracking branch 'begriffs/v3' into v3 2015-10-24 23:07:08 +03:00
Ruslan Talpa 5390fb702d Fix for #321 2015-10-24 23:06:19 +03: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
Ruslan Talpa 00c8303fed Merge branch 'v3' of https://github.com/ruslantalpa/postgrest into v3 2015-10-24 21:26:19 +03:00
Ruslan Talpa cd3a149aa4 Merge remote-tracking branch 'begriffs/v3' into v3 2015-10-24 21:25:01 +03:00
Joe Nelson 97d612a60d Merge pull request #327 from diogob/update_stack
Update stack resolver
2015-10-23 13:43:29 -07:00
Ruslan Talpa c6d40fff8c Merge remote-tracking branch 'begriffs/v3' into v3 2015-10-23 23:39:37 +03:00
Diogo Biazus 5de9db0ca1 Updates stack resolver to 3.10 and adds new hspec to custom build plan 2015-10-23 15:44:18 -04:00
Joe Nelson a624eb08ce Merge pull request #326 from begriffs/710-only
GHC 7.10 only
2015-10-23 11:22:45 -07:00
Joe Nelson af15046d3f Use newer hspec 2015-10-23 11:08:59 -07:00
Joe Nelson 7692693aae Enforce GHC >= 7.10 and fix Stack warnings 2015-10-23 10:33:27 -07:00
Ruslan Talpa e40dcb1324 simplify operator formatting function 2015-10-23 12:53:31 +03:00
Ruslan Talpa 826de74a5d rearange paths to put the most used ones at the top in the case expression 2015-10-23 12:31:46 +03:00
Ruslan Talpa f5fb78ec99 version changed to 3, circle ci to use ghc 7.10.1, stricter import/export in PgQuery and remove of dead code 2015-10-23 12:23:46 +03:00
Ruslan Talpa c606149c43 code cleanup 2 2015-10-23 10:45:35 +03:00
Ruslan Talpa d4a8716a0e code cleanup 2015-10-23 10:13:51 +03:00
Ruslan Talpa 21bd921ee7 Merge remote-tracking branch 'begriffs/v3' into v3 2015-10-23 09:15:00 +03:00
Joe Nelson 8b28b33da3 Merge pull request #320 from diogob/jwt_generic_variables
Jwt generic variables
2015-10-22 21:53:53 -07:00
Diogo Biazus 91dfd47f1d Adds another test case for jwt with empty claims 2015-10-22 23:20:16 -04:00
Diogo Biazus aad19b53c7 Includes one test case for recovering from from 400 error and another for invalid JWT tokens 2015-10-22 23:12:14 -04:00
Ruslan Talpa cea4cc5860 a bit of warning cleanup 2015-10-22 18:01:47 +03:00
Ruslan Talpa ca4014f751 a few more tests fixed (2) 2015-10-22 17:03:06 +03:00
Ruslan Talpa 2662e24991 a few more tests fixed 2015-10-22 16:17:23 +03:00
Ruslan Talpa 2b8f5f791a using query fragments instead of query transformers o generate queries 2015-10-22 13:47:34 +03:00
Joe Nelson df00d728ff Docs about methods for updating records 2015-10-21 21:12:27 -07:00
Diogo Biazus d000a6c61a Eliminates SET role duplication and changes Auth module interface 2015-10-22 00:08:02 -04:00
Ruslan Talpa 71ef03070e POST path modified with internal data type but tests failing (no Location and data returned as array) 2015-10-21 12:46:01 +03:00
Joe Nelson 9fad028074 Document API read requests 2015-10-20 19:25:31 -07:00
Diogo Biazus 81ee7cbd5e Removes redundant do 2015-10-20 19:54:29 -04:00
Diogo Biazus 250a4dcfb2 Adds back import to make GHC 7.8 happy 2015-10-20 19:51:25 -04:00
Diogo Biazus 6e55017f96 Cleans and adds haddock comments 2015-10-20 19:48:54 -04:00
Diogo Biazus a192cadced All green :D 2015-10-20 19:23:12 -04:00
Diogo Biazus 044e3865ac Removes 'Prefer: return=jwt' header and chooses jwt return based on function type 2015-10-20 01:03:11 -04:00
Diogo Biazus 2ea7bc29c6 Moves function from top level to where 2015-10-20 00:43:21 -04: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
Diogo Biazus a3aba84ba8 Fixes linter suggestions 2015-10-18 20:10:26 -04:00
Diogo Biazus 4e9afc8096 Adds import needed by ghc 7.8 2015-10-18 20:05:34 -04:00
Diogo Biazus f56efeb039 Reduces conde duplication assimbling response vody for RPC 2015-10-18 20:02:33 -04:00
Diogo Biazus 8b5f4e8556 Removes lenses and uses simpler approach to generate JWT claims. Also fixes the setVar to avoid the ::unknown type cast from insertableValue 2015-10-18 17:14:11 -04:00
Diogo Biazus 7ef5b7b43a Simplifies pattern matching using insertableValue and quote variable as identifier. 2015-10-18 14:03:43 -04:00
Diogo Biazus 241a38e958 Makes JWT generation possible in RPC endpoints
Fixes SET execution to execute in separate statements as Hasql uses
prepared statements we need to send 1 commend per statement.
2015-10-18 02:30:32 -04:00
Diogo Biazus aae55e0282 Make complete match agains ClaimsMap in setVar 2015-10-17 20:41:42 -04:00
Diogo Biazus 31f1a30d6f Cleans imports in Middleware and define exports in Auth 2015-10-16 15:56:07 -04:00
Diogo Biazus 275002e25d Adds dbrole filter back to root path querying the database 2015-10-16 15:55:56 -04:00
Diogo Biazus 3add3f5b6c First draft of big auth simplification 2015-10-16 15:55:47 -04:00
Ruslan Talpa 1f80b806bd data types refactoring 2015-10-16 15:21:31 +03:00
Ruslan Talpa 609f1aabca Merge remote-tracking branch 'begriffs/v3' into v3 2015-10-16 11:34:10 +03: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 2c67b8d7ba Merge pull request #316 from begriffs/clearer-errors
Clearer errors
2015-10-13 15:36:35 -07:00
Joe Nelson 6dbb69a828 Ensure that version error message tracks changing requirements 2015-10-12 17:10:57 -07:00
Joe Nelson 3733a84a38 Use type alias for clarity 2015-10-12 17:10:57 -07:00
Joe Nelson e37b2d8c59 Provide detailed logging for any db errors caused internally by postgrest 2015-10-12 17:10:52 -07:00
Joe Nelson 2373a41699 Merge pull request #315 from calebmer/feature/pg-string-settings
Use postgres connection string instead of 5+ options
2015-10-12 16:22:27 -07:00
calebmer 2cbf2af6c7 Fix readme bash syntax 2015-10-11 16:51:52 -04:00
calebmer fdcf074dfd Update readme 2015-10-11 16:50:47 -04:00
calebmer de43ac52c4 Rename connection string metavar 2015-10-11 16:47:09 -04:00
calebmer f6aa93f094 Add details to changelog 2015-10-11 16:46:51 -04:00
calebmer 5feb334191 Use postgres connection string 2015-10-11 16:26:29 -04:00
Joe Nelson 3bd10004a7 Merge pull request #314 from calebmer/remove-versioning
Remove versioning feature
2015-10-11 10:35:34 -07:00
calebmer 02c405ad80 Remove versioning feature 2015-10-11 13:16:59 -04: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
82 changed files with 7603 additions and 2028 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
+139
View File
@@ -0,0 +1,139 @@
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [0.3.0.2] - 2015-12-16
### Fixed
- Miscalculation of time used for expiring tokens - @calebmer
- Remove bcrypt dependency to fix Windows build - @begriffs
- Detect relations event when authenticator does not have rights to intermediate tables - @ruslantalpa
- Ensure db connections released on sigint - @begriffs
- Fix #396 include records with missing parents - @ruslantalpa
- `pgFmtIdent` always quotes #388 - @calebmer
- Default schema, changed from `"1"` to `public` - @calebmer
- #414 revert to separate count query
- Fix #399, allow inserting in tables with no select privileges using "Prefer: representation=minimal" - @ruslantalpa
### Added
- Allow order by computed columns - @diogob
- Set max rows in response with --max-rows - @begriffs
- Selection by column name (can detect if `_id` is not included) - @calebmer
## [0.3.0.1] - 2015-11-27
### Fixed
- Filter columns on embedded parent items - @ruslantalpa
## [0.3.0.0] - 2015-11-24
### Fixed
- Use reasonable amount of memory during bulk inserts - @begriffs
### Added
- Ensure JWT expires - @calebmer
- Postgres connection string argument - @calebmer
- Encode JWT for procs that return type `jwt_claims` - @diogob
- Full text operators `@>`,`<@` - @ruslantalpa
- Shaping of the response body (filter columns, embed relations) with &select parameter for POST/PATCH - @ruslantalpa
- Detect relationships between public views and private tables - @calebmer
- `Prefer: plurality=singular` for selecting single objects - @calebmer
### Removed
- API versioning feature - @calebmer
- `--db-x` command line arguments - @calebmer
- Secure flag - @calebmer
- PUT request handling - @ruslantalpa
### Changed
- Embed foreign keys with {} rather than () - @begriffs
- Remove version number from binary filename in release - @begriffs
## [0.2.12.1] - 2015-11-12
### Fixed
- Correct order for -> and ->> in a json path - @ruslantalpa
- Return empty array instead of 500 when a set returning function returns an empty result set - @diogob
## [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`.
+59
View File
@@ -0,0 +1,59 @@
# 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.
* Please include SQL logs for issues involving runtime problems. To obtain logs first
[enable logging all statements](http://www.microhowto.info/howto/log_all_queries_to_a_postgresql_server.html),
then [find your logs](http://blog.endpoint.com/2014/11/dear-postgresql-where-are-my-logs.html).
## 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.
+67 -52
View File
@@ -1,12 +1,17 @@
![Logo](static/logo.png "Logo")
[![Build Status](https://circleci.com/gh/begriffs/postgrest.png?circle-token=f723c01686abf0364de1e2eaae5aff1f68bd3ff2)](https://circleci.com/gh/begriffs/postgrest/tree/master)
[![Build Status](https://circleci.com/gh/begriffs/postgrest.png?style=shield&circle-token=f723c01686abf0364de1e2eaae5aff1f68bd3ff2)](https://circleci.com/gh/begriffs/postgrest/tree/master)
<a href="https://heroku.com/deploy?template=https://github.com/begriffs/postgrest">
<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)
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.
### Demo [postgrest.herokuapp.com](https://postgrest.herokuapp.com) | Watch [Video](https://begriffs.com/posts/2014-12-30-intro-to-postgrest.html)
### Demo [postgrest.herokuapp.com](https://postgrest.herokuapp.com) | Read [Docs](http://postgrest.com/) | Watch [Video](http://begriffs.com/posts/2014-12-30-intro-to-postgrest.html)
Try making requests to the live demo server with an HTTP client
such as [postman](http://www.getpostman.com/). The structure of the
@@ -15,28 +20,39 @@ demo database is defined by
You can use it as inspiration for test-driven server migrations in
your own projects.
Also try other tools in the PostgREST
[ecosystem](http://postgrest.com/install/ecosystem/) like the
[ng-admin demo](http://marmelab.com/ng-admin-postgrest).
### Usage
Download the binary ([OS X](http://bin.begriffs.com/dbapi/osx/postgrest-0.2.5.0.tar.xz) / [Ubuntu](http://bin.begriffs.com/dbapi/heroku/postgrest-0.2.5.0.tar.xz)) and invoke like so:
1. Download the binary ([latest release](https://github.com/begriffs/postgrest/releases/latest))
for your platform.
2. 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 --secure \
--port 3000
```
```bash
postgrest postgres://postgres:foobar@localhost:5432/my_db \
--port 3000 \
--schema public \
--anonymous postgres \
--pool 200
```
For more information on valid connection strings see the
[PostgreSQL docs](http://www.postgresql.org/docs/9.4/static/libpq-connect.html#LIBPQ-CONNSTRING).
### Performance
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))
TLDR; subsecond response times for up to 2000 requests/sec on Heroku
free tier. ([see the load
test](http://postgrest.com/admin/performance/#benchmarks))
If you're used to servers written in interpreted languages (or named
after precious gems), prepare to be pleasantly surprised by PostgREST
performance.
Three factors contribute to the speed. First the server is written
in [Haskell](https://new-www.haskell.org/) using the
in [Haskell](https://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
@@ -54,58 +70,64 @@ by
* Reusing prepared statements
* Keeping a pool of db connections
* Using the Postgres binary protocol
* Using the PostgreSQL binary protocol
* Being stateless to allow horizontal scaling
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).
the [performance guide](http://postgrest.com/admin/performance/).
Alternatively [CitusDB](https://www.citusdata.com/products/what-is-citusdb)
supports Postgres clustering for higher performance.
Other optimizations are possible, and some are outlined in the
[Future Features](#future-features).
### Security
PostgREST handles authentication (HTTP Basic over SSL) and delegates
authorization to the role information defined in the database. This
ensures there is a single declarative source of truth for security.
When dealing with the database the server assumes the identity of
the currently authenticated user, and for the duration of the
connection cannot do anything the user themselves couldn't.
PostgREST handles authentication (via [JSON Web
Tokens](http://postgrest.com/admin/security/#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. Other forms of authentication can be built on top
of the JWT primitive. See the docs for more information.
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
PostgreSQL 9.5 supports true [row-level
security](http://www.postgresql.org/docs/9.5/static/ddl-rowsecurity.html).
In previous versions it 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).
guide](http://postgrest.com/admin/security/).
### 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).
versions. 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. You run an instance of PostgREST per schema and route requests
among them with a reverse proxy such as [nginx](http://nginx.org).
Learn more [here](http://postgrest.com/admin/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.
as the data format of their JSON payload. RAML support is an upcoming
feature.
The number of rows returned by an endpoint is reported by - and
limited with - range headers. More about
The project uses HTTP itself to commicate other metadata. For
instance the number of rows returned by an endpoint is reported by -
and limited with - range headers. More about
[that](http://begriffs.com/posts/2014-03-06-beyond-http-header-links.html).
There are more opportunities for self-documentation listed in [Future
@@ -121,9 +143,9 @@ 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
See examples of [PostgreSQL
constraints](http://www.tutorialspoint.com/postgresql/postgresql_constraints.htm)
and the [guide to routing](https://github.com/begriffs/postgrest/wiki/Routing).
and the [guide to routing](http://postgrest.com/api/reading/).
### Future Features
@@ -136,21 +158,14 @@ and the [guide to routing](https://github.com/begriffs/postgrest/wiki/Routing).
* 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)
### Thanks
* [Adam Baker](https://github.com/adambaker) for code
contributions and many fundamental design discussions
* [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
I'm grateful to the generous project
[contributors](https://github.com/begriffs/postgrest/graphs/contributors)
who have improved PostgREST immensely with their code and good
judgement. See more details in the
[changelog](https://github.com/begriffs/postgrest/blob/master/CHANGELOG.md).
The cool logo came from [Mikey Casalaina](https://github.com/casalaina).
+23 -12
View File
@@ -6,38 +6,49 @@
"env": {
"BUILDPACK_URL": {
"description": "Heroku buildpack for deploying Haskell applications",
"value": "https://github.com/mietek/haskell-on-heroku"
"value": "https://github.com/begriffs/postgrest-heroku"
},
"POSTGREST_VER": {
"description": "Version of PostgREST to deploy",
"value": "0.3.0.2"
},
"DB_NAME": {
"description": "Database name"
"description": "Database name",
"required": true
},
"DB_AUTH_ROLE": {
"description": "Database role to use checking client authentication"
"AUTH_ROLE": {
"description": "Database role to use checking client authentication",
"required": true
},
"DB_AUTH_PASS": {
"AUTH_PASS": {
"description": "Authentication password",
"required": false
},
"DB_ANON_ROLE": {
"description": "Database role for non-authenticated requests"
"ANONYMOUS_ROLE": {
"description": "Database role for non-authenticated requests",
"required": true
},
"DB_HOST": {
"description": "Database server hostname",
"required": false,
"value": "localhost"
"required": true
},
"DB_PORT": {
"description": "Database server port",
"required": false,
"value": "5432"
},
"DB_POOL_SIZE": {
"DB_POOL": {
"description": "Maximum number of connections in database pool",
"required": false,
"value": "10"
},
"DB_SECURE": {
"description": "Redirect all requests to HTTPS",
"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"
}
+11 -1
View File
@@ -3,4 +3,14 @@ machine:
- createuser --superuser --no-password postgrest_test
- createdb -O postgrest_test -U ubuntu postgrest_test
ghc:
version: 7.8.3
version: 7.10.1
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 || true
+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 &
+29
View File
@@ -0,0 +1,29 @@
# run service as
#POSTGREST_USER=postgrest
# log file
#POSTGREST_LOG=/var/log/postgrest/postgrest.log
# database host
#POSTGREST_DBHOST=localhost
# database host
#POSTGREST_DBPORT=5432
# database to use
#POSTGREST_DBNAME=app
# database user
#POSTGREST_DBUSER=authenticator
# database password
#POSTGREST_DBPASS=
# database pool
#POSTGREST_POOL=10
# jwt secret
#POSTGREST_JWT_SECRET=secret
# default schema
#POSTGREST_SCHEMA=public
Vendored Executable
+99
View File
@@ -0,0 +1,99 @@
#!/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
CONNECTION_STRING="postgres://"
POSTGREST_OPTS=""
POSTGREST_USER=${POSTGREST_USER:-postgrest}
POSTGREST_PORT=${POSTGREST_PORT:-3000}
POSTGREST_DBUSER=${POSTGREST_DBUSER:-authenticator}
#POSTGREST_DBPASS=${POSTGREST_DBPASS:-authenticator}
POSTGREST_DBHOST=${POSTGREST_DBHOST:-localhost}
POSTGREST_DBPORT=${POSTGREST_DBPORT:-5432}
POSTGREST_DBNAME=${POSTGREST_DBNAME:-app}
POSTGREST_DBPOOL=${POSTGREST_DBPOOL:-10}
POSTGREST_ANON=${POSTGREST_ANON:-anonymous}
POSTGREST_JWT_SECRET=${POSTGREST_JWT_SECRET:-secret}
POSTGREST_SCHEMA=${POSTGREST_SCHEMA:-public}
CONNECTION_STRING="$CONNECTION_STRING$POSTGREST_DBUSER"
if [ -n "$POSTGREST_DBPASS" ]; then
CONNECTION_STRING="$CONNECTION_STRING:$POSTGREST_DBPASS"
fi
CONNECTION_STRING="$CONNECTION_STRING@$POSTGREST_DBHOST:$POSTGREST_DBPORT/$POSTGREST_DBNAME"
if [ -n "$POSTGREST_PORT" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --port $POSTGREST_PORT"
fi
if [ -n "$POSTGREST_POOL" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --pool $POSTGREST_POOL"
fi
if [ -n "$POSTGREST_JWT_SECRET" ]; then
#export POSTGREST_JWT_SECRET="$POSTGREST_JWT_SECRET"
POSTGREST_OPTS="$POSTGREST_OPTS --jwt-secret $POSTGREST_JWT_SECRET"
fi
if [ -n "$POSTGREST_SCHEMA" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --schema $POSTGREST_SCHEMA"
fi
if [ -n "$POSTGREST_ANON" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --anonymous $POSTGREST_ANON"
fi
#export CONNECTION_STRING="$CONNECTION_STRING"
START_PARAMS="$CONNECTION_STRING $POSTGREST_OPTS"
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 -- $START_PARAMS; 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
+82
View File
@@ -0,0 +1,82 @@
## Security
PostgREST is designed to keep the database at the center of API
security. All authorization happens through database roles and
permissions. It is PostgREST's job to *authenticate* requests --
i.e. verify that a client is who they say they are -- and then let
the database *authorize* client actions.
We use [JSON Web Tokens](http://jwt.io/) to authenticate API requests.
As you'll recall a JWT contains a list of cryptographically signed
claims. PostgREST cares specifically about a claim called `role`.
When request contains a valid JWT with a role claim PostgREST will
switch to the database role with that name for the duration of the
HTTP request. If the client included no (or an invalid) JWT then
PostgREST selects the "anonymous role" which is specified by a
command line arguments to the server on startup.
```js
{
"role": "jdoe123"
}
// Encoded as JWT with a secret of "secret" this becomes
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZTEyMyJ9.X_ZeWSS9qsKDCDczv8C-GE2fccrPQjOh_ALMZJa5jsU
```
Using JWT allows us to authenticate with external services. A login
service needs merely to share a JWT encryption secret with the
PostgREST server. The secret is also a server command line option.
It is even possible to generate JWT from inside a stored procedure
in your database. Any SQL stored procedure that returns a type whose
name ends in `jwt_claims` will have its return value encoded into
JWT. See the [User Management](http://postgrest.com/examples/users/)
example for details.
### Database Roles
Suppose you start the server like this:
```bash
postgrest postgres://foo@localhost:5432/mydb --anonymous anon
```
This means that `foo` is the so-called *authenticator role* and
`anon` is the anonymous role. When a new HTTP request arrives at the
server the latter is connected to the database as user `foo`. If
no JWT is present, or if it is invalid, or if it does not contain
the role claim then the server changes to the anonymous role with
the query
```sql
SET LOCAL ROLE anon;
```
Otherwise it sets the role to that specified by JWT. For security
your authenticator role should have access to nothing except the
ability to become other users. Supposing you have three roles, one
for anonymous users, one for authors, and another for the authenticator,
you would set it up like this
```sql
CREATE ROLE authenticator NOINHERIT;
CREATE ROLE anon;
CREATE ROLE author;
GRANT anon, author TO authenticator;
```
### Row-Level Security
#### Simulated - PostgreSQL <9.5
#### Real - PostgreSQL >=9.5
### Building Auth on top of JWT
#### Basic Auth
#### Github Sign-in
### SSL
+9
View File
@@ -0,0 +1,9 @@
## API Versioning
### Schema Search Path
### Changing a Resource
### Removing a Resource
### Avoiding DB and Client Coupling
+339
View File
@@ -0,0 +1,339 @@
## 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`, `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 (inspired by GraphQL) to embed
related information. It can handle one-to-many and 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).
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
GET /people?order=age.desc.nullslast
```
You can also use [computed
columns](http://www.postgresql.org/docs/current/interactive/xfunc-sql.html#XFUNC-SQL-COMPOSITE-FUNCTIONS)
to order the results, even though the computed
columns will not appear in the output.
### 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
```
With count suppressed the PostgREST response will look like:
```
Range-Unit: items
Content-Range → 0-14/*
```
### Embedding Foreign Entities
To help you make fewer requests, PostgREST allows the embedding of
traditional SQL relationships into a response. 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{*}
```
In the examples above we asked for all columns in the embedded resource
but the the select query is recursive. You could for instance specify
```HTTP
GET /foo?select=x, y, bar{z, w, baz{*}}
```
You can select not only using table names, but also column names!
To embed the same foreign key row from our client example earlier
you could do the following:
```HTTP
GET /projects?id=eq.1&select=id, name, client_id{*}
```
In the response there will be a `client_id` object containing all
the data for that row.
However, a `client_id` object doesn't make a lot of sense, so you
could do one of two things. Create a view which renames `client_id`
to just `client` (this is the hard way), or just try `client{*}`
in the select parameter! PostgREST supports smart ducktype checking
for common foreign key names, so if your column name ends with
`_id`, `_fk`, or any variation of the two (including camelcase)
you can embed a row with just the name's beginning.
So for a complete example:
```HTTP
GET /projects?id=eq.1&select=id, name, client{*}
```
Would embed in the `client` key the row referenced with `client_id`.
### 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. To request a singular response send the header
`Prefer: plurality=singular`.
### 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.
+123
View File
@@ -0,0 +1,123 @@
## 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
You can POST a JSON array or CSV to insert multiple rows in a single
HTTP request. Note that using CSV requires less parsing on the server
and is **much faster**.
Example of CSV bulk insert. 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.
Example of JSON bulk insert. Send an array:
```HTTP
POST /people
[
{ "name": "J Doe", "age": 62, "height": 70 },
{ "name": "Janus", "age": 10, "height": 55 }
]
```
If you would like to get the full object back in the response to
your request, include the header `Prefer: return=representation`.
Chances are you only want certain information back, though, like
created ids. You can pass a `select` parameter to affect the shape
of the response (further documented in the [reading](/api/reading/)
page). For instance
```HTTP
POST /people?select=id
[...]
```
returns something like
```json
[ { "id": 1 }, { "id": 2 } ]
```
### 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=is.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>
+163
View File
@@ -0,0 +1,163 @@
## Multi-Tenant Blog
In our blog app there will be anonymous users and authors. Each
author can create and edit their own posts, and read (but not edit)
the posts of other authors. Anonymous users cannot edit anything
but can sign up for author accounts. Authors can also post comments
on articles.
This example builds off the previous one. We had previously created
a signup and login system on top of JWT. We'll use this auth system
for the blog. **Run the SQL in the previous example** first, before
continuing with this example.
For your convenience, the complete sql for the blog demo is
[here](https://github.com/begriffs/postgrest/blob/master/schema-templates/blog.sql).
You can try it out in this [vagrant
image](https://github.com/ruslantalpa/blogdemo) as well.
### Adding Blog-Specific Tables
Storing the posts and comments is this simple. The comments do not
form a tree, they are linear under a post.
```sql
create table if not exists
posts (
id bigserial primary key,
title text not null,
body text not null,
author text not null references basic_auth.users (email)
on delete restrict on update cascade
default basic_auth.current_email(),
created_at timestamptz not null default current_date
);
create table if not exists
comments (
id bigserial primary key,
body text not null,
author text not null references basic_auth.users (email)
on delete restrict on update cascade
default basic_auth.current_email(),
post bigint not null references posts (id)
on delete cascade on update cascade,
created_at timestamptz not null default current_date
);
```
### Permissions
Basic table-level permissions. We'll add an the `authenticator`
role which can't do anything itself other than switch into other
roles as directed by JWT.
```sql
create role anon;
create role author;
create role authenticator noinherit;
grant anon, author to authenticator;
grant usage on schema public, basic_auth to anon, author;
-- anon can create new logins and can read comments/posts
grant insert on table basic_auth.users, basic_auth.tokens to anon;
grant select on table pg_authid, basic_auth.users, posts, comments to anon;
grant execute on function
login(text,text),
request_password_reset(text),
reset_password(text,uuid,text),
signup(text, text)
to anon;
-- authors can edit comments/posts
grant select, insert, update, delete
on basic_auth.tokens, basic_auth.users to anon, author;
grant select, insert, update, delete
on table users, posts, comments to author;
grant usage, select on sequence posts_id_seq, comments_id_seq to author;
```
To ensure that authors cannot edit each others' posts and comments
we'll use [row-level
security](http://www.postgresql.org/docs/9.5/static/ddl-rowsecurity.html).
Note that it requires PostgreSQL 9.5 or later.
```sql
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
drop policy if exists authors_eigenedit on posts;
create policy authors_eigenedit on posts
using (true)
with check (
author = basic_auth.current_email()
);
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
drop policy if exists authors_eigenedit on comments;
create policy authors_eigenedit on comments
using (true)
with check (
author = basic_auth.current_email()
);
```
Finally we need to modify the `users` view from the previous example.
This is because all authors share a single db role. We could have
chosen to assign a new role for every author (all inheriting from
`author`) but we choose to tell them apart by their email addresses.
The addition below prevents authors from seeing each others' info
in the `users` view.
```diff
create or replace view users as
select actual.role as role,
'***'::text as pass,
actual.email as email,
actual.verified as verified
from basic_auth.users as actual,
(select rolname
from pg_authid
where pg_has_role(current_user, oid, 'member')
) as member_of
where actual.role = member_of.rolname
+ and (
+ actual.role <> 'author'
+ or email = basic_auth.current_email()
+ );
```
### Example client queries
* Top ten most recent posts
```HTTP
GET /posts?order=created_at.desc
Range: 0-9
```
* Single post (randomly chose id=1) with its comments
```HTTP
GET /posts?id=eq.1&select=*,comments{*}
```
* Add a new post
```HTTP
POST /posts
Authorization: Bearer [JWT TOKEN]
{
"title": "My first post",
"body": "Meh, forgot what I wanted to say."
}
```
### Conclusion
Voilà, a blog API. Most of the code ended up being for defining
security. Once you have set up an authentication system, the code
to do application specific things like blog posts and comments is
short. All the front-end routes and verbs are created automatically
for you.
+509
View File
@@ -0,0 +1,509 @@
## 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 REFERENCES director (name)
ON UPDATE CASCADE ON DELETE CASCADE,
rating real NOT NULL DEFAULT 0,
language text NOT NULL
);
CREATE TABLE festival
(
name text NOT NULL PRIMARY KEY
);
CREATE TABLE competition
(
id serial PRIMARY KEY,
name text NOT NULL,
festival text NOT NULL REFERENCES festival (name)
ON UPDATE CASCADE ON DELETE CASCADE,
year date NOT NULL
);
CREATE TABLE film_nomination
(
id serial PRIMARY KEY,
competition integer NOT NULL REFERENCES competition (id)
ON UPDATE NO ACTION ON DELETE NO ACTION,
film integer NOT NULL REFERENCES film (id)
ON UPDATE CASCADE ON DELETE CASCADE,
won boolean NOT NULL DEFAULT true
);
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. (See the [installation instructions](/install/server/).)
```sh
postgrest postgres://postgres:@localhost:5432/demo1 -a postgres --schema 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>
### Populating Data
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.
The server returns HTTP 201 Created. Because we inserted more than one item at once there is no `Location` header in the response. However sometimes you want to learn more about items which you just inserted. To have the server include the full restuls include the header `Prefer: return=representation`.
At this point 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
```
### Getting and Embedding Data
First let's review which films are stored in the database:
```http
GET http://localhost:3000/film
```
It gives us back a list of JSON objects. What if we care only about the film titles? Use `select` to shape the output:
```http
GET http://localhost:3000/film?select=title
```
```json
[
{
"title": "Chuang ru zhe"
},
{
"title": "The Look of Silence"
},
{
"title": "Fires on the Plain"
},
...
]
```
Here is where it gets cool. PostgREST can embed objects in its response through foreign key relationships. Earlier we created a join table called `film_nomination`. It joins films and competitions. We can ask the server about the structure of this table:
```
OPTIONS http://localhost:3000/film_nomination
```
```json
{
"pkey": [
"id"
],
"columns": [
{
"references": null,
"default": "nextval('film_nomination_id_seq'::regclass)",
"precision": 32,
"updatable": true,
"schema": "public",
"name": "id",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 1
},
{
"references": {
"schema": "public",
"column": "id",
"table": "competition"
},
"default": null,
"precision": 32,
"updatable": true,
"schema": "public",
"name": "competition",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 2
},
{
"references": {
"schema": "public",
"column": "id",
"table": "film"
},
"default": null,
"precision": 32,
"updatable": true,
"schema": "public",
"name": "film",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 3
},
{
"references": null,
"default": "true",
"precision": null,
"updatable": true,
"schema": "public",
"name": "won",
"type": "boolean",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 4
}
]
}
```
From this you can see that the columns `film` and `competition` reference their eponymous tables. Let's ask the server for each film along with names of the competitions it entered. You don't have to do any custom coding. Send this query:
```http
GET http://localhost:3000/film?select=title,competition{name}
```
```json
[
{
"title": "Chuang ru zhe",
"competition": [
{
"name": "Golden Lion"
}
]
},
{
"title": "The Look of Silence",
"competition": [
{
"name": "Golden Lion"
}
]
},
...
]
```
The relation flows both ways. Here is how to get the name of each competition's name and the movies shown at it.
```http
GET http://localhost:3000/competition?select=name,film{title}
```
```json
[
{
"name": "Golden Lion",
"film": [
{
"title": "Chuang ru zhe"
},
{
"title": "The Look of Silence"
},
...
]
},
{
"name": "Palme d'Or",
"film": [
{
"title": "The Wonders"
},
{
"title": "Foxcatcher"
},
...
]
}
]
```
Why not learn about the directors too? There is a many-to-one relation directly between films and directors. We can alter our previous query to include directors in its results.
```http
GET http://localhost:3000/competition?select=name,film{title,director{*}}
```
```json
[
{
"name": "Golden Lion",
"film": [
{
"title": "Manglehorn",
"director": {
"name": "David Gordon Green"
}
},
{
"title": "Belye nochi pochtalona Alekseya Tryapitsyna",
"director": {
"name": "Andrey Konchalovskiy"
}
},
...
]
},
...
]
```
### Singular Responses
How do we ask for a single film, for instance the second one we inserted?
```http
GET http://localhost:3000/film?id=eq.2
```
It returns
```json
[
{
"id": 2,
"title": "The Look of Silence",
"year": "2014-01-01",
"director": "Joshua Oppenheimer",
"rating": 8.3,
"language": "Indonesian"
}
]
```
Like any query, it gives us a result *set*, in this case an array with one element. However you and I know that `id` is a primary key, it will never return more than one result. We might want it returned as a JSON object, not an array. To express this preference include the header `Prefer: plurality=singular`. It will respond with
```json
{
"id": 2,
"title": "The Look of Silence",
"year": "2014-01-01",
"director": "Joshua Oppenheimer",
"rating": 8.3,
"language": "Indonesian"
}
```
<div class="admonition note">
<p class="admonition-title">Why this approach to singular responses?</p>
<p>
PostgREST knows which columns comprise a primary key for a
table, so why not automatically choose plurality=singular when
these column filters are present? The fact is it could come as a
shock to a client that by adding one more filter condition it can
change the entire response format.
</p>
<p>
Then why not expose another kind of route such as /film/2 to indicate
one particular film? Because this does not accommodate compound keys.
The convention complects a plurality preference with table key
assumptions. We should separate concerns.
</p>
<p>
It turns out you can still have routes like /film/2. Use a
proxy such as Nginx. It can rewrite routes such as /films/2
into /films?id=eq.2 and add the Prefer header to make the results
singular.
</p>
</div>
### Conclusion
This tutorial showed how to create a database with a basic schema, run PostgREST, and interact with the API. The next tutorial will show how to enable security for a multi-tenant blogging API.
+482
View File
@@ -0,0 +1,482 @@
## User Management
API clients authenticate with [JSON Web Tokens](http://jwt.io).
PostgREST does not support any other authentication mechanism
directly, but they can be built on top. In this demo we will build
a username and password system on top of JWT using only plpgsql.
Future examples such as the multi-tenant blogging platform will use
the results from this example for their auth. We will build a system
for users to sign up, log in, manage their accounts, and for admins
to manange other people's accounts. We will also see how to trigger
outside events like sending password reset emails.
Before jumping into the code, a little more about how the tokens
work. Every JWT contains cryptographically signed *claims*. PostgREST
cares specificaly about a claim called `role`. When a client includes
a `role` claim PostgREST executes their request using that database
role.
How would a client include a role claim, or claims in general?
Without knowing the server JWT secret a client cannot create a
claim. The only place to get a JWT is from the PostgREST server or
from another service sharing the secret and acting on its behalf.
We'll use a stored procedure returning type `jwt_claims` which is
a special type causing the server to encrypt and sign the return
value.
### Storing Users and Passwords
We create a database schema especially for auth information. We'll
also need the postgres extensions
[pgcrypto](http://www.postgresql.org/docs/current/static/pgcrypto.html) and
[uuid-ossp](http://www.postgresql.org/docs/current/static/uuid-ossp.html).
```sql
create extension if not exists pgcrypto;
create extension if not exists "uuid-ossp";
-- We put things inside the basic_auth schema to hide
-- them from public view. Certain public procs/views will
-- refer to helpers and tables inside.
create schema if not exists basic_auth;
```
Next a table to store the mapping from usernames and passwords to
database roles. The code below includes triggers and functions to
encrypt the password and ensure the role exists.
```sql
create table if not exists
basic_auth.users (
email text primary key check ( email ~* '^.+@.+\..+$' ),
pass text not null check (length(pass) < 512),
role name not null check (length(role) < 512),
verified boolean not null default false
-- If you like add more columns, or a json column
);
create or replace function
basic_auth.check_role_exists() returns trigger
language plpgsql
as $$
begin
if not exists (select 1 from pg_roles as r where r.rolname = new.role) then
raise foreign_key_violation using message =
'unknown database role: ' || new.role;
return null;
end if;
return new;
end
$$;
drop trigger if exists ensure_user_role_exists on basic_auth.users;
create constraint trigger ensure_user_role_exists
after insert or update on basic_auth.users
for each row
execute procedure basic_auth.check_role_exists();
create or replace function
basic_auth.encrypt_pass() returns trigger
language plpgsql
as $$
begin
if tg_op = 'INSERT' or new.pass <> old.pass then
new.pass = crypt(new.pass, gen_salt('bf'));
end if;
return new;
end
$$;
drop trigger if exists encrypt_pass on basic_auth.users;
create trigger encrypt_pass
before insert or update on basic_auth.users
for each row
execute procedure basic_auth.encrypt_pass();
```
With the table in place we can make a helper to check passwords.
It returns the database role for a user if the email and password
are correct.
```sql
create or replace function
basic_auth.user_role(email text, pass text) returns name
language plpgsql
as $$
begin
return (
select role from basic_auth.users
where users.email = user_role.email
and users.pass = crypt(user_role.pass, users.pass)
);
end;
$$;
```
### Password Reset
When a user requests a password reset or signs up we create a token
they will use later to prove their identity. The tokens go in this
table.
```sql
drop type if exists token_type_enum cascade;
create type token_type_enum as enum ('validation', 'reset');
create table if not exists
basic_auth.tokens (
token uuid primary key,
token_type token_type_enum not null,
email text not null references basic_auth.users (email)
on delete cascade on update cascade,
created_at timestamptz not null default current_date
);
```
In the main schema (as opposed to the `basic_auth` schema) we expose
a password reset request function. HTTP clients will call it. The
function takes the email address of the user.
```sql
create or replace function
request_password_reset(email text) returns void
language plpgsql
as $$
declare
tok uuid;
begin
delete from basic_auth.tokens
where token_type = 'reset'
and tokens.email = request_password_reset.email;
select uuid_generate_v4() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'reset', request_password_reset.email);
perform pg_notify('reset',
json_build_object(
'email', request_password_reset.email,
'token', tok,
'token_type', 'reset'
)::text
);
end;
$$;
```
This function does not send any emails. It sends a postgres
[NOTIFY](http://www.postgresql.org/docs/current/static/sql-notify.html)
command. External programs such as a mailer listen for this event
and do the work. The most robust way to process these signals is
by pushing them onto work queues. Here are two programs to do that:
1. [aweber/pgsql-listen-exchange](https://github.com/aweber/pgsql-listen-exchange) for RabbitMQ
2. [SpiderOak/skeeter](https://github.com/SpiderOak/skeeter) for ZeroMQ
For experimentation you don't need that though. Here's a sample
Node program that listens for the events and logs them to stdout.
```js
var PS = require('pg-pubsub');
if(process.argv.length !== 3) {
console.log("USAGE: DB_URL");
process.exit(2);
}
var url = process.argv[2],
ps = new PS(url);
// password reset request events
ps.addChannel('reset', console.log);
// email validation required event
ps.addChannel('validate', console.log);
// modify me to send emails
```
Once the user has a reset token they can use it as an argument to
the password reset function, calling it through the PostgREST RPC
interface.
```sql
create or replace function
reset_password(email text, token uuid, pass text)
returns void
language plpgsql
as $$
declare
tok uuid;
begin
if exists(select 1 from basic_auth.tokens
where tokens.email = reset_password.email
and tokens.token = reset_password.token
and token_type = 'reset') then
update basic_auth.users set pass=reset_password.pass
where users.email = reset_password.email;
delete from basic_auth.tokens
where tokens.email = reset_password.email
and tokens.token = reset_password.token
and token_type = 'reset';
else
raise invalid_password using message =
'invalid user or token';
end if;
delete from basic_auth.tokens
where token_type = 'reset'
and tokens.email = reset_password.email;
select uuid_generate_v4() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'reset', reset_password.email);
perform pg_notify('reset',
json_build_object(
'email', reset_password.email,
'token', tok
)::text
);
end;
$$;
```
### Email Validation
This is similar to password resets. Once again we generate a token.
It differs in that there is a trigger to send validations when a
new login is added to the users table.
```sql
create or replace function
basic_auth.send_validation() returns trigger
language plpgsql
as $$
declare
tok uuid;
begin
select uuid_generate_v4() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'validation', new.email);
perform pg_notify('validate',
json_build_object(
'email', new.email,
'token', tok,
'token_type', 'validation'
)::text
);
return new;
end
$$;
drop trigger if exists send_validation on basic_auth.users;
create trigger send_validation
after insert on basic_auth.users
for each row
execute procedure basic_auth.send_validation();
```
### Editing Own User
We'll construct a redacted view for users. It hides passwords and
shows only those users whose roles the currently logged in user has
db permission to access.
```sql
create or replace view users as
select actual.role as role,
'***'::text as pass,
actual.email as email,
actual.verified as verified
from basic_auth.users as actual,
(select rolname
from pg_authid
where pg_has_role(current_user, oid, 'member')
) as member_of
where actual.role = member_of.rolname;
-- can also add restriction that current_setting('postgrest.claims.email')
-- is equal to email so that user can only see themselves
```
Using this view clients can see themeslves and any other users with
the right db roles. This view does not yet support inserts or updates
because not all the columns refer directly to underlying columns.
Nor do we want it to be auto-updatable because it would allow an escalation
of privileges. Someone could update their own row and change their
role to become more powerful.
We'll handle updates with a trigger, but we'll need a helper function
to prevent an escalation of privileges.
```sql
create or replace function
basic_auth.clearance_for_role(u name) returns void as
$$
declare
ok boolean;
begin
select exists (
select rolname
from pg_authid
where pg_has_role(current_user, oid, 'member')
and rolname = u
) into ok;
if not ok then
raise invalid_password using message =
'current user not member of role ' || u;
end if;
end
$$ LANGUAGE plpgsql;
```
With the above function we can now make a safe trigger to allow
user updates.
```sql
create or replace function
update_users() returns trigger
language plpgsql
AS $$
begin
if tg_op = 'INSERT' then
perform basic_auth.clearance_for_role(new.role);
insert into basic_auth.users
(role, pass, email, verified)
values
(new.role, new.pass, new.email,
coalesce(new.verified, false));
return new;
elsif tg_op = 'UPDATE' then
-- no need to check clearance for old.role because
-- an ineligible row would not have been available to update (http 404)
perform basic_auth.clearance_for_role(new.role);
update basic_auth.users set
email = new.email,
role = new.role,
pass = new.pass,
verified = coalesce(new.verified, old.verified, false)
where email = old.email;
return new;
elsif tg_op = 'DELETE' then
-- no need to check clearance for old.role (see previous case)
delete from basic_auth.users
where basic_auth.email = old.email;
return null;
end if;
end
$$;
drop trigger if exists update_users on users;
create trigger update_users
instead of insert or update or delete on
users for each row execute procedure update_users();
```
Finally add a public function people can use to sign up. You can
hard code a default db role in it. It alters the underlying
`basic_auth.users` so you can set whatever role you want without
restriction.
```sql
create or replace function
signup(email text, pass text) returns void
as $$
insert into basic_auth.users (email, pass, role) values
(signup.email, signup.pass, 'hardcoded-role-here');
$$ language sql;
```
### Generating JWT
As mentioned at the start, clients authenticate with JWT. PostgREST
has a special convention to allow your sql functions to return JWT.
Any function that returns a type whose name ends in `jwt_claims` will
have its return value encoded. For instance, let's make a login function
which consults our users table.
First create a return type:
```sql
drop type if exists basic_auth.jwt_claims cascade;
create type basic_auth.jwt_claims AS (role text, email text);
```
And now the function:
```sql
create or replace function
login(email text, pass text) returns basic_auth.jwt_claims
language plpgsql
as $$
declare
_role name;
result basic_auth.jwt_claims;
begin
select basic_auth.user_role(email, pass) into _role;
if _role is null then
raise invalid_password using message = 'invalid user or password';
end if;
-- TODO; check verified flag if you care whether users
-- have validated their emails
select _role as role, login.email as email into result;
return result;
end;
$$;
```
An API request to login would look like this.
```HTTP
POST /rpc/login
{ "email": "foo@bar.com", "pass": "foobar" }
```
Response
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicm9sZSI6ImF1dGhvciJ9.KHwYdK9dAMAg-MGCQXuDiFuvbmW-y8FjfYIcMrETnto"
}
```
Try decoding the token at [jwt.io](http://jwt.io/). (It was encoded
with a secret of `secret` which is the default.) To use this token
in a future API request include it in an `Authorization` request
header.
```HTTP
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicm9sZSI6ImF1dGhvciJ9.KHwYdK9dAMAg-MGCQXuDiFuvbmW-y8FjfYIcMrETnto
```
### Same-Role Users
You may not want a separate db role for every user. You can distinguish
one user from another in SQL by examining the JWT claims which
PostgREST makes available in the SQL variable `postgrest.claims`.
Here's a function to get the email of the currently authenticated
user.
```sql
create or replace function
basic_auth.current_email() returns text
language plpgsql
as $$
begin
return current_setting('postgrest.claims.email');
exception
-- handle unrecognized configuration parameter error
when undefined_object then return '';
end;
$$;
```
Remember that the `login` function set the claims `email` and `role`.
You can modify `login` to set other claims as well if they are
useful for your other SQL functions to reference later.
### Conclusion
This section explained the implementation details for building a
password based authentication system in pure sql. The next example
will put it to work in a multi-tenant blogging API.
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

+91
View File
@@ -0,0 +1,91 @@
<style>
.videoWrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
padding-top: 25px;
height: 0;
}
.videoWrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
![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.
### Intro Video
Some things have changed since this video was created but the basics are the same. Learn the big vision behind automating APIs.
<div class="videoWrapper">
<iframe src="https://player.vimeo.com/video/115668217" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
</div>
### 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>
+24
View File
@@ -0,0 +1,24 @@
## 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
* [ruslantalpa/blogdemo](https://github.com/ruslantalpa/blogdemo) - blog api demo in a vagrant image
* [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/)
+145
View File
@@ -0,0 +1,145 @@
## Installation
### Installing from Pre-Built Release
The [release page](https://github.com/begriffs/postgrest/releases/latest)
has precompiled binaries for Mac OS X, Windows, and several Linux
distros. 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-[version]-[platform].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 architecture.
It would be nice to set up an automated build matrix for various
architectures. It should support Mac, Windows and 32- and 64-bit
versions of
<ul><li>Scientific Linux 6</li><li>CentOS</li><li>RHEL 6</li></ul></p>
</div>
### 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](http://docs.haskellstack.org/en/stable/README.html#how-to-install) for your platform
```bash
#ubuntu example
#See the link above for other operating systems
wget -q -O- https://s3.amazonaws.com/download.fpcomplete.com/ubuntu/fpco.key | sudo apt-key add -
echo 'deb http://download.fpcomplete.com/ubuntu/trusty stable main'|sudo tee /etc/apt/sources.list.d/fpco.list
sudo apt-get update && sudo apt-get install stack -y
```
* Build & install in one step
```bash
git clone https://github.com/begriffs/postgrest.git
cd postgrest
sudo stack install --install-ghc --local-bin-path /usr/local/bin
```
* Run the server
If you want to run the test suite, stack can do that too: `stack test`.
### Running the Server
```bash
postgrest postgres://user:pass@host:port/db [flags]
```
The user in the connection string is the "authenticator role," i.e.
a role which is used temporarily to switch into other roles depending
on the authentication request JWT. For simple API's you can use the
same role for authenticator and anonymous.
The possible flags are:
<dl>
<dt>-p, --port</dt>
<dd>The port on which the server will listen for HTTP requests.
Defaults to 3000.</dd>
<dt>-a, --anonymous</dt>
<dd>The database role used to execute commands for those requests
which provide no JWT authorization.</dd>
<dt>-s, --schema</dt>
<dd>The db schema which you want to expose as an API. For historical
reasons it defaults to <code>1</code>, but you're more likely
to want to choose a value of <code>public</code>.</dd>
<dt>-j, --jwt-secret</dt>
<dd>The secret passphrase used to encrypt JWT tokens. Defaults to
<code>secret</code> but do not use the default in production!
Load-balanced PostgREST servers should share the same secret.</dd>
<dt>-p, --pool</dt>
<dd>Max connections to use in db pool. Defaults to to 10, but you
should find an optimal value for your db by running the SQL
command <code>show max_connections;</code></dd>
<dt>-m, --max-rows</dt>
<dd>Max number of rows to return in a read request. The default is
no limit.</dd>
</dl>
<div class="admonition note">
<p class="admonition-title">Hiding Password from Process List</p>
<p>Passing the database password and JWT secret as naked
parameters might not be a good idea because the parameters are
visible in a <code>ps</code> listing. One solution is to set
environment variables such as PASS and use <code>$PASS</code>
in the connection string. Another is to use a user-specific
<a
href="http://www.postgresql.org/docs/current/static/libpq-pgpass.html">.pgpass</a>
file.</p>
</div>
### Install via Homebrew (Mac OS X)
You can use the Homebrew package manager to install PostgREST on Mac
```bash
# Ensure brew is up to date
brew update
# Check for any problems with brew's setup
brew doctor
# Install the postgrest package
brew install postgrest
```
This will automatically install PostgreSQL as a dependency (see the [Installing PostgreSQL](#installing-postgresql) section for setup instructions). The process tends to take up to 15 minutes to install the package and its dependencies.
After installation completes, the tool is added to your $PATH and can be used from anywhere with:
```bash
postgrest --help
```
### Installing PostgreSQL
To use PostgREST you will need an underlying database (PostgreSQL version 9.3 or greater is required). 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)
* [Installer for Windows](http://www.enterprisedb.com/products-services-training/pgdownload#windows)
+26
View File
@@ -0,0 +1,26 @@
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
- User Management: examples/users.md
- Multi-Tenant Blog: examples/blog.md
+177 -66
View File
@@ -1,89 +1,200 @@
name: postgrest
version: 0.2.5.0
synopsis: The database is your api
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.3.0.2
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: Main.hs
ghc-options: -Wall -W -Werror -O2
if flag(ci)
ghc-options: -Wall -W -Werror
else
ghc-options: -Wall -W -O2
main-is: PostgREST/Main.hs
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
default-language: Haskell2010
default-extensions: OverloadedStrings
other-extensions: QuasiQuotes
build-depends: base >=4.6 && <5
, hasql == 0.4.*, hasql-backend
, hasql-postgres == 0.8.*
, warp >= 3.0.2, wai >= 3.0.1
, wai-extra, wai-cors
, wai-middleware-static >= 0.6.0
, HTTP, convertible, http-types
build-depends: aeson >= 0.8
, base >= 4.8 && < 5
, bytestring
, case-insensitive
, scientific, time
, aeson, network >= 2.6
, bytestring, text, split, string-conversions
, stringsearch, parsec
, containers, unordered-containers
, optparse-applicative >= 0.9.1 && < 0.10
, regex-base, regex-tdfa
, regex-tdfa-text
, Ranged-sets
, cassava
, containers
, errors
, hasql >= 0.7.3 && < 0.8
, hasql-backend >= 0.4.1 && < 0.5
, hasql-postgres >= 0.10.4 && < 0.11
, jwt
, optparse-applicative >= 0.11 && < 0.13
, parsec
, postgrest
, regex-tdfa
, safe >= 0.3 && < 0.4
, scientific
, string-conversions
, text
, time
, transformers
, bcrypt, base64-string
, network-uri >= 2.6
, resource-pool
, blaze-builder
, unordered-containers
, vector
, mtl
Other-Modules: App
, Auth
, Config
, PgStructure
, PgQuery
, PgError
, RangeQuery
, Middleware
, wai >= 3.0.1
, wai-cors
, wai-extra
, wai-middleware-static >= 0.6.0
, warp >= 3.0.2
, HTTP, http-types
, MissingH
, Ranged-sets
if !os(windows)
build-depends: unix >= 2.7 && < 3
hs-source-dirs: src
other-modules: Paths_postgrest
, PostgREST.App
, PostgREST.Auth
, PostgREST.Config
, PostgREST.Error
, PostgREST.Middleware
, PostgREST.Parsers
, PostgREST.DbStructure
, PostgREST.QueryBuilder
, PostgREST.RangeQuery
, PostgREST.ApiRequest
, PostgREST.Types
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: aeson
, base >=4.6 && <5
, bytestring
, case-insensitive
, cassava
, containers
, errors
, hasql
, hasql-backend
, hasql-postgres
, http-types
, jwt
, optparse-applicative
, parsec
, regex-tdfa
, safe
, scientific
, string-conversions
, text
, time
, unordered-containers
, vector
, wai
, wai-cors
, wai-extra
, wai-middleware-static
, HTTP
, MissingH
, Ranged-sets
Other-Modules: Paths_postgrest
Exposed-Modules: PostgREST.App
, PostgREST.Auth
, PostgREST.Config
, PostgREST.Error
, PostgREST.Middleware
, PostgREST.Parsers
, PostgREST.DbStructure
, PostgREST.QueryBuilder
, PostgREST.RangeQuery
, PostgREST.ApiRequest
, PostgREST.Types
hs-source-dirs: src
Test-Suite spec
Type: exitcode-stdio-1.0
Default-Language: Haskell2010
default-extensions: OverloadedStrings
other-extensions: QuasiQuotes
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
Hs-Source-Dirs: test, src
ghc-options: -Wall -W -Werror
if flag(ci)
ghc-options: -Wall -W -Werror
else
ghc-options: -Wall -W -O2
Main-Is: Main.hs
Other-Modules: App, Auth, Config, Spec, SpecHelper
Build-Depends: base, hspec >= 2.1.2, QuickCheck
, hspec-wai >= 0.5.0, hspec-wai-json
, hasql == 0.4.*, hasql-backend
, hasql-postgres == 0.8.*
, warp >= 3.0.2, wai >= 3.0.1
, 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
, stringsearch, parsec
, unordered-containers
, regex-base
, string-conversions
, http-media, regex-tdfa
, regex-tdfa-text
, Ranged-sets
, transformers
, bcrypt
Other-Modules: Feature.AuthSpec
, Feature.CorsSpec
, Feature.DeleteSpec
, Feature.InsertSpec
, Feature.QuerySpec
, Feature.RangeSpec
, Feature.StructureSpec
, Paths_postgrest
, PostgREST.App
, PostgREST.Auth
, PostgREST.Config
, PostgREST.Error
, PostgREST.Middleware
, PostgREST.Parsers
, PostgREST.DbStructure
, PostgREST.QueryBuilder
, PostgREST.RangeQuery
, PostgREST.ApiRequest
, PostgREST.Types
, SpecHelper
, TestTypes
Build-Depends: aeson
, base
, base64-string
, split
, network-uri >= 2.6
, resource-pool
, blaze-builder
, vector
, mtl
, bytestring
, case-insensitive
, cassava
, containers
, errors
, hasql
, hasql-backend
, hasql-postgres
, heredoc
, hlint
, hspec == 2.2.*
, hspec-wai
, hspec-wai-json
, http-types
, jwt
, optparse-applicative
, packdeps
, parsec
, process
, regex-tdfa
, safe
, scientific
, string-conversions
, text
, time
, unordered-containers
, vector
, wai
, wai-cors
, wai-extra
, wai-middleware-static
, HTTP
, MissingH
, Ranged-sets
+381
View File
@@ -0,0 +1,381 @@
-------------------------------------------------------------------------------
-- Adapted from https://github.com/robconery/pg-auth
begin;
-- comment out the role creation statements if
-- you want to run this script more than once
create role anon;
create role author;
create role authenticator noinherit;
grant anon, author to authenticator;
create extension if not exists pgcrypto;
create extension if not exists "uuid-ossp";
-- We put things inside the basic_auth schema to hide
-- them from public view. Certain public procs/views will
-- refer to helpers and tables inside.
create schema if not exists basic_auth;
-------------------------------------------------------------------------------
-- Utility functions
create or replace function
basic_auth.clearance_for_role(u name) returns void as
$$
declare
ok boolean;
begin
select exists (
select rolname
from pg_authid
where pg_has_role(current_user, oid, 'member')
and rolname = u
) into ok;
if not ok then
raise invalid_password using message =
'current user not member of role ' || u;
end if;
end
$$ LANGUAGE plpgsql;
-------------------------------------------------------------------------------
-- Users storage and constraints
create table if not exists
basic_auth.users (
email text primary key check ( email ~* '^.+@.+\..+$' ),
pass text not null check (length(pass) < 512),
role name not null check (length(role) < 512),
verified boolean not null default false
-- If you like add more columns, or a json column
);
create or replace function
basic_auth.check_role_exists() returns trigger
language plpgsql
as $$
begin
if not exists (select 1 from pg_roles as r where r.rolname = new.role) then
raise foreign_key_violation using message =
'unknown database role: ' || new.role;
return null;
end if;
return new;
end
$$;
drop trigger if exists ensure_user_role_exists on basic_auth.users;
create constraint trigger ensure_user_role_exists
after insert or update on basic_auth.users
for each row
execute procedure basic_auth.check_role_exists();
create or replace function
basic_auth.encrypt_pass() returns trigger
language plpgsql
as $$
begin
if tg_op = 'INSERT' or new.pass <> old.pass then
new.pass = crypt(new.pass, gen_salt('bf'));
end if;
return new;
end
$$;
drop trigger if exists encrypt_pass on basic_auth.users;
create trigger encrypt_pass
before insert or update on basic_auth.users
for each row
execute procedure basic_auth.encrypt_pass();
create or replace function
basic_auth.send_validation() returns trigger
language plpgsql
as $$
declare
tok uuid;
begin
select uuid_generate_v4() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'validation', new.email);
perform pg_notify('validate',
json_build_object(
'email', new.email,
'token', tok,
'token_type', 'validation'
)::text
);
return new;
end
$$;
drop trigger if exists send_validation on basic_auth.users;
create trigger send_validation
after insert on basic_auth.users
for each row
execute procedure basic_auth.send_validation();
-------------------------------------------------------------------------------
-- Email Validation and Password Reset
drop type if exists token_type_enum cascade;
create type token_type_enum as enum ('validation', 'reset');
create table if not exists
basic_auth.tokens (
token uuid primary key,
token_type token_type_enum not null,
email text not null references basic_auth.users (email)
on delete cascade on update cascade,
created_at timestamptz not null default current_date
);
-------------------------------------------------------------------------------
-- Login helper
create or replace function
basic_auth.user_role(email text, pass text) returns name
language plpgsql
as $$
begin
return (
select role from basic_auth.users
where users.email = user_role.email
and users.pass = crypt(user_role.pass, users.pass)
);
end;
$$;
create or replace function
basic_auth.current_email() returns text
language plpgsql
as $$
begin
return current_setting('postgrest.claims.email');
exception
-- handle unrecognized configuration parameter error
when undefined_object then return '';
end;
$$;
-------------------------------------------------------------------------------
-- Public functions (in current schema, not basic_auth)
create or replace function
request_password_reset(email text) returns void
language plpgsql
as $$
declare
tok uuid;
begin
delete from basic_auth.tokens
where token_type = 'reset'
and tokens.email = request_password_reset.email;
select uuid_generate_v4() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'reset', request_password_reset.email);
perform pg_notify('reset',
json_build_object(
'email', request_password_reset.email,
'token', tok,
'token_type', 'reset'
)::text
);
end;
$$;
create or replace function
reset_password(email text, token uuid, pass text)
returns void
language plpgsql
as $$
declare
tok uuid;
begin
if exists(select 1 from basic_auth.tokens
where tokens.email = reset_password.email
and tokens.token = reset_password.token
and token_type = 'reset') then
update basic_auth.users set pass=reset_password.pass
where users.email = reset_password.email;
delete from basic_auth.tokens
where tokens.email = reset_password.email
and tokens.token = reset_password.token
and token_type = 'reset';
else
raise invalid_password using message =
'invalid user or token';
end if;
delete from basic_auth.tokens
where token_type = 'reset'
and tokens.email = reset_password.email;
select uuid_generate_v4() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'reset', reset_password.email);
perform pg_notify('reset',
json_build_object(
'email', reset_password.email,
'token', tok
)::text
);
end;
$$;
drop type if exists basic_auth.jwt_claims cascade;
create type
basic_auth.jwt_claims AS (role text, email text);
create or replace function
login(email text, pass text) returns basic_auth.jwt_claims
language plpgsql
as $$
declare
_role name;
result basic_auth.jwt_claims;
begin
select basic_auth.user_role(email, pass) into _role;
if _role is null then
raise invalid_password using message = 'invalid user or password';
end if;
-- TODO; check verified flag if you care whether users
-- have validated their emails
select _role as role, login.email as email into result;
return result;
end;
$$;
create or replace function
signup(email text, pass text) returns void
as $$
insert into basic_auth.users (email, pass, role) values
(signup.email, signup.pass, 'author');
$$ language sql;
-------------------------------------------------------------------------------
-- User management
create or replace view users as
select actual.role as role,
'***'::text as pass,
actual.email as email,
actual.verified as verified
from basic_auth.users as actual,
(select rolname
from pg_authid
where pg_has_role(current_user, oid, 'member')
) as member_of
where actual.role = member_of.rolname
and (
actual.role <> 'author'
or email = basic_auth.current_email()
);
create or replace function
update_users() returns trigger
language plpgsql
AS $$
begin
if tg_op = 'INSERT' then
perform basic_auth.clearance_for_role(new.role);
insert into basic_auth.users
(role, pass, email, verified) values
(coalesce(new.role, 'author'), new.pass,
new.email, coalesce(new.verified, false));
return new;
elsif tg_op = 'UPDATE' then
-- no need to check clearance for old.role because
-- an ineligible row would not even available to update (http 404)
perform basic_auth.clearance_for_role(new.role);
update basic_auth.users set
email = new.email,
role = new.role,
pass = new.pass,
verified = coalesce(new.verified, old.verified, false)
where email = old.email;
return new;
elsif tg_op = 'DELETE' then
-- no need to check clearance for old.role (see previous case)
delete from basic_auth.users
where basic_auth.email = old.email;
return null;
end if;
end
$$;
drop trigger if exists update_users on users;
create trigger update_users
instead of insert or update or delete on
users for each row execute procedure update_users();
-------------------------------------------------------------------------------
-- Blogging stuff!
create table if not exists
posts (
id bigserial primary key,
title text not null,
body text not null,
author text not null references basic_auth.users (email)
on delete restrict on update cascade
default basic_auth.current_email(),
created_at timestamptz not null default current_date
);
create table if not exists
comments (
id bigserial primary key,
body text not null,
author text not null references basic_auth.users (email)
on delete restrict on update cascade
default basic_auth.current_email(),
post bigint not null references posts (id)
on delete cascade on update cascade,
created_at timestamptz not null default current_date
);
-------------------------------------------------------------------------------
-- Permissions
grant insert on table basic_auth.users, basic_auth.tokens to anon;
grant select on table pg_authid, basic_auth.users, posts, comments to anon;
grant execute on function
login(text,text),
request_password_reset(text),
reset_password(text,uuid,text),
signup(text, text)
to anon;
grant select, insert, update, delete
on basic_auth.tokens, basic_auth.users to anon, author;
grant select, insert, update, delete
on table users, posts, comments to author;
grant usage, select on sequence posts_id_seq, comments_id_seq to author;
grant usage on schema public, basic_auth to anon, author;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
drop policy if exists authors_eigenedit on posts;
create policy authors_eigenedit on posts
using (true)
with check (
author = basic_auth.current_email()
);
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
drop policy if exists authors_eigenedit on comments;
create policy authors_eigenedit on comments
using (true)
with check (
author = basic_auth.current_email()
);
commit;
+1 -1
View File
@@ -1,6 +1,6 @@
export POSTGREST_VER=`grep ^version /app/postgrest.cabal | sed -En 's/.*\s+([0-9\.]+)/\1/p'`
curl -L http://softlayer-ams.dl.sourceforge.net/project/s3tools/s3cmd/1.5.0-alpha1/s3cmd-1.5.0-alpha1.tar.gz | tar zx
curl -L http://sourceforge.net/projects/s3tools/files/s3cmd/1.5.0-alpha1/s3cmd-1.5.0-alpha1.tar.gz | tar zx
cp /app/dist/build/postgrest/postgrest postgrest-${POSTGREST_VER}
tar cJf postgrest-${POSTGREST_VER}.tar.xz postgrest-${POSTGREST_VER}
-266
View File
@@ -1,266 +0,0 @@
{-# LANGUAGE FlexibleContexts #-}
module App (app, sqlError, isSqlError) where
import Control.Monad (join)
import Control.Arrow ((***))
import Control.Applicative
import Data.Text hiding (map)
import Data.Maybe (fromMaybe)
import Text.Regex.TDFA ((=~))
import Data.Ord (comparing)
import Data.Ranged.Ranges (emptyRange)
import Data.HashMap.Strict (keys, elems, filterWithKey, toList)
import Data.String.Conversions (cs)
import Data.List (sortBy)
import Data.Functor.Identity
import qualified Data.Set as S
import qualified Data.ByteString.Lazy as BL
import Network.HTTP.Types.Status
import Network.HTTP.Types.Header
import Network.HTTP.Types.URI (parseSimpleQuery)
import Network.HTTP.Base (urlEncodeVars)
import Network.Wai
import Data.Aeson
import Data.Monoid
import qualified Hasql as H
import qualified Hasql.Postgres as H
import Auth
import PgQuery
import RangeQuery
import PgStructure
import PgError
import Text.Parsec hiding (Column)
app :: BL.ByteString -> Request -> H.Tx H.Postgres s Response
app reqBody req =
case (path, verb) of
([], _) -> do
body <- encode <$> tables (cs schema)
return $ responseLBS status200 [jsonH] $ cs body
([table], "OPTIONS") -> do
let t = QualifiedTable schema (cs table)
cols <- columns t
pkey <- map cs <$> primaryKeyColumns t
return $ responseLBS status200 [jsonH, allOrigins]
$ encode (TableOptions cols pkey)
([table], "GET") ->
if range == Just emptyRange
then return $ responseLBS status416 [] "HTTP Range error"
else do
let qt = QualifiedTable schema (cs table)
let select = coerce $
("select ",[],mempty) <>
parentheticT (
whereT qq $ countRows qt
) <> commaq <> (
asJsonWithCount
. limitT range
. orderT (orderParse qq)
. whereT qq
$ selectStar qt
)
row <- H.single select
let (tableTotal, queryTotal, body) =
fromMaybe (0, 0, Just "" :: Maybe Text) row
from = fromMaybe 0 $ rangeOffset <$> range
to = from+queryTotal-1
contentRange = contentRangeH from to tableTotal
status = rangeStatus from to tableTotal
canonical = urlEncodeVars
. sortBy (comparing fst)
. map (join (***) cs)
. parseSimpleQuery
$ rawQueryString req
return $ responseLBS status
[jsonH, contentRange,
("Content-Location",
"/" <> cs table <>
if Prelude.null canonical then "" else "?" <> cs canonical
)
] (cs $ fromMaybe "[]" body)
(["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))
] ""
([table], "POST") ->
handleJsonObj reqBody $ \obj -> do
let qt = QualifiedTable schema (cs table)
query = coerce $
insertInto qt (map cs $ keys obj) (elems obj)
row <- H.single query
let (Identity insertedJson) = fromMaybe (Identity "{}" :: Identity Text) row
Just inserted = decode (cs insertedJson) :: Maybe Object
primaryKeys <- map cs <$> primaryKeyColumns qt
let primaries = if Prelude.null primaryKeys
then inserted
else filterWithKey (const . (`elem` primaryKeys)) inserted
let params = urlEncodeVars
$ map (\t -> (cs $ fst t, "eq." <> cs (unquoted $ snd t)))
$ sortBy (comparing fst) $ toList primaries
return $ responseLBS status201
[ jsonH
, (hLocation, "/" <> cs table <> "?" <> cs params)
] ""
([table], "PUT") ->
handleJsonObj reqBody $ \obj -> do
let qt = QualifiedTable schema (cs table)
primaryKeys <- primaryKeyColumns qt
let specifiedKeys = map (cs . fst) qq
if S.fromList primaryKeys /= S.fromList specifiedKeys
then return $ responseLBS status405 []
"You must speficy all and only primary keys as params"
else do
tableCols <- map (cs . colName) <$> columns qt
let cols = map cs $ keys obj
if S.fromList tableCols == S.fromList cols
then do
let vals = elems obj
H.unit . coerce $ iffNotT
(whereT 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 = QualifiedTable schema (cs table)
H.unit
$ coerce
$ whereT qq
$ update qt (map cs $ keys obj) (elems obj)
return $ responseLBS status204 [ jsonH ] ""
([table], "DELETE") -> do
let qt = QualifiedTable schema (cs table)
let del = coerce $ countT
. returningStarT
. whereT qq
$ deleteFrom qt
row <- H.single 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
path = pathInfo req
verb = requestMethod req
qq = queryString req
hdrs = requestHeaders req
schema = requestedSchema hdrs
range = rangeRequested hdrs
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
coerce (q, args, All b) = (q, args, b)
isSqlError :: H.Error -> Maybe H.Error
isSqlError = Just
sqlError :: H.Error -> Response
sqlError err =
let inside = case err of
H.CantConnect _ ->
"Message: \"Cannot connect to postgres server\""
H.ConnectionLost t -> t
H.ErroneousResult t -> t
H.UnexpectedResult t -> t
H.UnparsableTemplate t -> t
H.UnparsableRow t -> t
H.NotInTransaction -> "An operation which requires a"
<> "database transaction was executed without one" in
either
(\hint ->
responseLBS status500
[(hContentType, "application/json")]
(cs . encode . object $ [
("message", String $
"Failed to parse exception:" <> inside)
, ("hint", String . cs . show $ hint)]))
(\msg ->
responseLBS (httpStatus msg)
[(hContentType, "application/json")]
(encode msg))
(parse message "" inside)
rangeStatus :: Int -> Int -> Int -> Status
rangeStatus from to total
| from > total = status416
| (1 + to - from) < total = status206
| otherwise = status200
contentRangeH :: Int -> Int -> Int -> Header
contentRangeH from to total =
("Content-Range",
if total == 0 || from > total
then "*/" <> cs (show total)
else cs (show from) <> "-"
<> cs (show to) <> "/"
<> cs (show total)
)
requestedSchema :: RequestHeaders -> Text
requestedSchema hdrs =
case verStr of
Just [[_, ver]] -> ver
_ -> "1"
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: String
accept = cs <$> lookup hAccept hdrs :: Maybe Text
verStr = (=~ verRegex) <$> accept :: Maybe [[Text]]
jsonH :: Header
jsonH = (hContentType, "application/json")
handleJsonObj :: BL.ByteString -> (Object -> H.Tx H.Postgres s Response)
-> H.Tx H.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")]
data TableOptions = TableOptions {
tblOptcolumns :: [Column]
, tblOptpkey :: [Text]
}
instance ToJSON TableOptions where
toJSON t = object [
"columns" .= tblOptcolumns t
, "pkey" .= tblOptpkey t ]
-69
View File
@@ -1,69 +0,0 @@
{-# LANGUAGE QuasiQuotes, ScopedTypeVariables, OverloadedStrings #-}
module Auth where
import Data.Aeson
import Control.Monad (mzero)
import Control.Applicative ( (<*>), (<$>) )
import Crypto.BCrypt
import Data.Text
import Data.Monoid
import qualified Hasql as H
import qualified Hasql.Postgres as H
import Data.String.Conversions (cs)
import PgQuery (pgFmtLit)
import System.IO.Unsafe
data AuthUser = AuthUser {
userId :: String
, userPass :: String
, userRole :: 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
data LoginAttempt =
NoCredentials
| MalformedAuth
| LoginFailed
| LoginSuccess DbRole
deriving (Eq, Show)
checkPass :: Text -> Text -> Bool
checkPass = (. cs) . validatePassword . cs
setRole :: Text -> H.Tx H.Postgres s ()
setRole role = H.unit ("set role " <> cs (pgFmtLit role), [], True)
resetRole :: H.Tx H.Postgres s ()
resetRole = H.unit [H.q|reset role|]
addUser :: Text -> Text -> Text -> H.Tx H.Postgres s ()
addUser identity pass role = do
let Just hashed = unsafePerformIO $ hashPasswordUsingPolicy fastBcryptHashingPolicy (cs pass)
H.unit $
[H.q|insert into postgrest.auth (id, pass, rolname) values (?, ?, ?)|]
identity (cs hashed :: Text) role
signInRole :: Text -> Text -> H.Tx H.Postgres s LoginAttempt
signInRole user pass = do
u <- H.single $ [H.q|select pass, rolname from postgrest.auth where id = ?|] user
return $ maybe LoginFailed (\r ->
let (hashed, role) = r in
if checkPass hashed pass
then LoginSuccess role
else LoginFailed
) u
-54
View File
@@ -1,54 +0,0 @@
module Config where
import Network.Wai
import Control.Applicative
import Data.Text (strip)
import qualified Data.CaseInsensitive as CI
import qualified Data.ByteString.Char8 as BS
import Data.String.Conversions (cs)
import Options.Applicative hiding (columns)
import Network.Wai.Middleware.Cors (CorsResourcePolicy(..))
data AppConfig = AppConfig {
configDbName :: String
, configDbPort :: Int
, configDbUser :: String
, configDbPass :: String
, configDbHost :: String
, configPort :: Int
, configAnonRole :: String
, configSecure :: Bool
, configPool :: Int
}
argParser :: Parser AppConfig
argParser = AppConfig
<$> strOption (long "db-name" <> short 'd' <> help "name of database")
<*> option (long "db-port" <> short 'P' <> value 5432 <> help "postgres server port")
<*> strOption (long "db-user" <> short 'U' <> help "postgres authenticator role")
<*> strOption (long "db-pass" <> value "" <> help "password for authenticator role")
<*> strOption (long "db-host" <> short 'h' <> value "localhost" <> help "postgres server hostname")
<*> option (long "port" <> short 'p' <> value 3000 <> help "port number on which to run HTTP server")
<*> strOption (long "anonymous" <> short 'a' <> help "postgres role to use for non-authenticated requests")
<*> switch (long "secure" <> short 's' <> help "Redirect all requests to HTTPS")
<*> option (long "db-pool" <> value 10 <> help "Max connections in database pool")
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 -> []
-64
View File
@@ -1,64 +0,0 @@
module Main where
import Paths_postgrest (version)
import App
import Middleware
import Control.Monad (unless)
import Control.Monad.IO.Class (liftIO)
import Control.Exception
import Data.String.Conversions (cs)
import Network.Wai (strictRequestBody)
import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Handler.Warp hiding (Connection)
import Network.Wai.Middleware.Gzip (gzip, def)
import Network.Wai.Middleware.Static (staticPolicy, only)
import Data.List (intercalate)
import Data.Version (versionBranch)
import qualified Hasql as H
import qualified Hasql.Postgres as H
import Options.Applicative hiding (columns)
import Config (AppConfig(..), argParser, corsPolicy)
main :: IO ()
main = do
conf <- execParser (info (helper <*> argParser) describe)
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)
let pgSettings = H.ParamSettings (cs $ configDbHost conf)
(fromIntegral $ configDbPort conf)
(cs $ configDbUser conf)
(cs $ configDbPass conf)
(cs $ configDbName conf)
sessSettings <- maybe (fail "Improper session settings") return $
H.sessionSettings (fromIntegral $ configPool conf) 30
let appSettings = setPort port
. setServerName (cs $ "postgrest/" <> prettyVersion)
$ defaultSettings
middle =
(if configSecure conf then redirectInsecure else id)
. gzip def . cors corsPolicy
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
anonRole = cs $ configAnonRole conf
currRole = cs $ configDbUser conf
H.session pgSettings sessSettings $ H.sessionUnlifter >>= \unlift ->
liftIO $ runSettings appSettings $ middle $ \req respond -> do
body <- strictRequestBody req
respond =<< catchJust isSqlError
(unlift $ H.tx Nothing
$ authenticated currRole anonRole (app body) req)
(return . sqlError)
where
describe = progDesc "create a REST API to an existing Postgres database"
prettyVersion = intercalate "." $ map show $ versionBranch version
-76
View File
@@ -1,76 +0,0 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Middleware where
import Data.Maybe (fromMaybe)
import Data.Monoid (mconcat)
import Data.Text
-- import Data.Pool(withResource, Pool)
import qualified Hasql as H
import qualified Hasql.Postgres as H
import Data.String.Conversions(cs)
import Network.HTTP.Types.Header (hLocation, hAuthorization)
import Network.HTTP.Types (RequestHeaders)
import Network.HTTP.Types.Status (status400, status401, status301)
import Network.Wai (Application, requestHeaders, responseLBS, rawPathInfo,
rawQueryString, isSecure, Request(..), Response)
import Network.URI (URI(..), parseURI)
import Auth (LoginAttempt(..), signInRole, setRole, resetRole)
import Codec.Binary.Base64.String (decode)
authenticated :: forall s. Text -> Text ->
(Request -> H.Tx H.Postgres s Response) ->
Request -> H.Tx H.Postgres s Response
authenticated currentRole anon 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 -> if role /= currentRole then runInRole role else app req
NoCredentials -> if anon /= currentRole then runInRole anon else app req
where
httpRequesterRole :: RequestHeaders -> H.Tx H.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
_ -> return NoCredentials
runInRole :: Text -> H.Tx H.Postgres s Response
runInRole r = do
setRole r
res <- app req
resetRole
return res
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
-86
View File
@@ -1,86 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module PgError (Message(..), message, httpStatus) where
import Text.Parsec
import Text.Parsec.Text
import qualified Data.Map as M
import Text.Regex.TDFA.Text ()
import Data.Text hiding (drop, concat, head)
import Data.Aeson
import Data.Maybe
import Control.Monad (void)
import Data.String.Conversions (cs)
import Data.CaseInsensitive (CI, mk)
import Network.HTTP.Types.Status
data Message = Message {
msgStatus :: Maybe Text
, msgCode :: Text
, msgText :: Maybe Text
, msgHint :: Maybe Text
} deriving (Show, Eq)
message :: Parser Message
message = do
ps <- sepBy valPair (char ';')
let m = M.fromList ps
return $ Message
(M.lookup "status" m)
(fromMaybe "" $ M.lookup "code" m)
(M.lookup "message" m)
(M.lookup "hint" m)
valPair :: Parser (CI Text, Text)
valPair = do
_ <- spaces
name <- many1 letter
_ <- char ':'
spaces
_ <- many $ char '"'
val <- manyTill anyChar $
try
(void $ many (char '"') >> (
(void . lookAhead $ (char ';'))
<|> ((optional $ char '.') >> eof)
))
return (mk (cs name), cs val)
instance ToJSON Message where
toJSON t = object [
"message" .= msgText t
, "code" .= msgCode t
, "status" .= msgStatus t
, "hint" .= msgHint t
]
httpStatus :: Message -> Status
httpStatus m =
let code = cs $ msgCode m :: String in
case code of
'0' : '8' : _ -> status503 -- pg connection err
'0' : '9' : _ -> status500 -- triggered action exception
'0' : 'L' : _ -> status403 -- invalid grantor
'0' : 'P' : _ -> status403 -- invalid role specification
'2' : '5' : _ -> status500 -- invalid tx state
'2' : '8' : _ -> status403 -- invalid auth specification
'2' : 'D' : _ -> status500 -- invalid tx termination
'3' : '8' : _ -> status500 -- external routine exception
'3' : '9' : _ -> status500 -- external routine invocation
'3' : 'B' : _ -> status500 -- savepoint exception
'4' : '0' : _ -> status500 -- tx rollback
'5' : '3' : _ -> status503 -- insufficient resources
'5' : '4' : _ -> status413 -- too complex
'5' : '5' : _ -> status500 -- obj not on prereq state
'5' : '7' : _ -> status500 -- operator intervention
'5' : '8' : _ -> status500 -- system error
'F' : '0' : _ -> status500 -- conf file error
'H' : 'V' : _ -> status500 -- foreign data wrapper error
'P' : '0' : _ -> status500 -- PL/pgSQL Error
'X' : 'X' : _ -> status500 -- internal Error
"42P01" -> status404 -- undefined table
"42501" -> status404 -- insufficient privilege
_ -> status400
-234
View File
@@ -1,234 +0,0 @@
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
module PgQuery where
import RangeQuery
import qualified Hasql.Postgres as H
import qualified Hasql.Backend as H
import Data.Text hiding (map)
import Text.Regex.TDFA ( (=~) )
import Text.Regex.TDFA.Text ()
import qualified Network.HTTP.Types.URI as Net
import qualified Data.ByteString.Char8 as BS
import Data.Monoid
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Functor ( (<$>) )
import Control.Monad (join)
import Data.String.Conversions (cs)
import qualified Data.Aeson as JSON
import qualified Data.List as L
import Data.Scientific (isInteger, formatScientific, FPFormat(..))
type DynamicSQL = (BS.ByteString, [H.StatementArgument H.Postgres], All)
type StatementT = DynamicSQL -> DynamicSQL
data QualifiedTable = QualifiedTable {
qtSchema :: Text
, qtName :: Text
} deriving (Show)
data OrderTerm = OrderTerm {
otTerm :: Text
, otDirection :: BS.ByteString
}
limitT :: Maybe NonnegRange -> StatementT
limitT r q =
q <> (" LIMIT " <> limit <> " OFFSET " <> offset <> " ", [], mempty)
where
limit = maybe "ALL" (cs . show) $ join $ rangeLimit <$> r
offset = cs . show $ fromMaybe 0 $ rangeOffset <$> r
whereT :: Net.Query -> StatementT
whereT params q =
if L.null cols
then q
else q <> (" where ",[],mempty) <> conjunction
where
cols = [ col | col <- params, fst col `notElem` ["order"] ]
conjunction = mconcat $ L.intersperse andq (map wherePred cols)
orderT :: [OrderTerm] -> StatementT
orderT ts q =
if L.null ts
then q
else q <> (" order by ",[],mempty) <> clause
where
clause = mconcat $ L.intersperse commaq (map queryTerm ts)
queryTerm :: OrderTerm -> DynamicSQL
queryTerm t = (" " <> cs (pgFmtIdent $ otTerm t) <> " "
<> otDirection t <> " "
, [], mempty)
parentheticT :: StatementT
parentheticT (sql, params, pre) =
(" (" <> sql <> ") ", params, pre)
iffNotT :: DynamicSQL -> StatementT
iffNotT (aq, ap, apre) (bq, bp, bpre) =
("WITH aaa AS (" <> aq <> " returning *) " <>
bq <> " WHERE NOT EXISTS (SELECT * FROM aaa)"
, ap ++ bp
, All $ getAll apre && getAll bpre
)
countT :: StatementT
countT (sql, params, pre) =
("WITH qqq AS (" <> sql <> ") SELECT count(1) FROM qqq"
, params
, pre)
countRows :: QualifiedTable -> DynamicSQL
countRows t =
("select count(1) from " <> fromQt t, [], mempty)
asJsonWithCount :: StatementT
asJsonWithCount (sql, params, pre) = (
"count(t), array_to_json(array_agg(row_to_json(t)))::character varying from (" <> sql <> ") t"
, params, pre
)
asJsonRow :: StatementT
asJsonRow (sql, params, pre) = (
"row_to_json(t) from (" <> sql <> ") t", params, pre
)
selectStar :: QualifiedTable -> DynamicSQL
selectStar t =
("select * from " <> fromQt t, [], mempty)
returningStarT :: StatementT
returningStarT (sql, params, pre) =
(sql <> " RETURNING *", params, pre)
deleteFrom :: QualifiedTable -> DynamicSQL
deleteFrom t =
("delete from " <> fromQt t, [], mempty)
insertInto :: QualifiedTable -> [Text] -> [JSON.Value] -> DynamicSQL
insertInto t [] _ =
("insert into " <> fromQt t <> " default values returning *", [], mempty)
insertInto t cols vals =
("insert into " <> fromQt t <> " (" <>
cs (intercalate ", " (map pgFmtIdent cols)) <>
") values (" <>
cs (
intercalate ", " (map
((<> "::unknown") . pgFmtLit . unquoted)
vals)
) <> ") returning row_to_json(" <> fromQt t <> ".*)"
, []
, mempty
)
insertSelect :: QualifiedTable -> [Text] -> [JSON.Value] -> DynamicSQL
insertSelect t [] _ =
("insert into " <> fromQt t <> " default values returning *", [], mempty)
insertSelect t cols vals =
("insert into " <> fromQt t <> " (" <>
cs (intercalate ", " (map pgFmtIdent cols)) <>
") select " <>
cs (
intercalate ", " (map
((<> "::unknown") . pgFmtLit . unquoted)
vals)
)
, []
, mempty
)
update :: QualifiedTable -> [Text] -> [JSON.Value] -> DynamicSQL
update t cols vals =
("update " <> fromQt t <> " set (" <>
cs (intercalate ", " (map pgFmtIdent cols)) <>
") = (" <>
cs (
intercalate ", " (map
((<> "::unknown") . pgFmtLit . unquoted)
vals)
) <> ")"
, []
, mempty
)
wherePred :: Net.QueryItem -> DynamicSQL
wherePred (col, predicate) =
(" " <> cs (pgFmtIdent $ cs col) <> " " <> op <> " " <> cs (pgFmtLit value) <> "::unknown ", [], mempty)
where
opCode:rest = split (=='.') $ cs $ fromMaybe "." predicate
value = intercalate "." rest
op = case opCode of
"eq" -> "="
"gt" -> ">"
"lt" -> "<"
"gte" -> ">="
"lte" -> "<="
"neq" -> "<>"
_ -> "="
orderParse :: Net.Query -> [OrderTerm]
orderParse q =
mapMaybe orderParseTerm . split (==',') $ cs order
where
order = fromMaybe "" $ join (lookup "order" q)
orderParseTerm :: Text -> Maybe OrderTerm
orderParseTerm s =
case split (=='.') s of
[d,c] ->
if d `elem` ["asc", "desc"]
then Just $ OrderTerm c $
if d == "asc" then "asc" else "desc"
else Nothing
_ -> Nothing
commaq :: DynamicSQL
commaq = (", ", [], mempty)
andq :: DynamicSQL
andq = (" and ", [], mempty)
pgFmtIdent :: Text -> Text
pgFmtIdent x =
let escaped = replace "\"" "\"\"" (trimNullChars $ cs x) in
if escaped =~ danger
then "\"" <> escaped <> "\""
else escaped
where danger = "^$|^[^a-z_]|[^a-z_0-9]" :: Text
pgFmtLit :: Text -> Text
pgFmtLit x =
let trimmed = trimNullChars x
escaped = "'" <> replace "'" "''" trimmed <> "'"
slashed = replace "\\" "\\\\" escaped in
cs $ if escaped =~ ("\\\\" :: Text)
then "E" <> slashed
else slashed
trimNullChars :: Text -> Text
trimNullChars = Data.Text.takeWhile (/= '\x0')
fromQt :: QualifiedTable -> BS.ByteString
fromQt t = cs $ pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t)
unquoted :: JSON.Value -> 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 _ = ""
pgParam :: JSON.Value -> H.StatementArgument H.Postgres
pgParam (JSON.Number n) = H.renderValue
(cs $ formatScientific Fixed
(if isInteger n then Just 0 else Nothing) n :: Text)
pgParam (JSON.String s) = H.renderValue s
pgParam (JSON.Bool b) = H.renderValue $
if b then "t" else "f" :: Text
pgParam JSON.Null = H.renderValue (Nothing :: Maybe Text)
pgParam (JSON.Object o) = H.renderValue $ JSON.encode o
pgParam (JSON.Array a) = H.renderValue $ JSON.encode a
-188
View File
@@ -1,188 +0,0 @@
{-# LANGUAGE QuasiQuotes, OverloadedStrings, TypeSynonymInstances,
MultiParamTypeClasses, ScopedTypeVariables #-}
module PgStructure where
import PgQuery (QualifiedTable(..))
import Data.Functor ( (<$>) )
import Data.Text hiding (foldl, map, zipWith, concat)
import Data.Aeson
import Data.Functor.Identity
import qualified Data.Vector as V
import Data.String.Conversions (cs)
import Control.Applicative ( (<*>) )
import qualified Data.List as L
import qualified Data.Map as Map
import qualified Hasql as H
import qualified Hasql.Backend as H
import qualified Hasql.Postgres as H
foreignKeys :: QualifiedTable -> H.Tx H.Postgres s (Map.Map Text ForeignKey)
foreignKeys table = do
r :: [(Text, Text, Text)] <- H.list $ [H.q|
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
|] (qtName table) (qtSchema table)
return $ foldl addKey Map.empty r
where
addKey m (col, ftab, fcol) = Map.insert col (ForeignKey (cs ftab) (cs fcol)) m
tables :: Text -> H.Tx H.Postgres s [Table]
tables schema =
H.list $ [H.q|
select table_schema, table_name,
is_insertable_into
from information_schema.tables
where table_schema = ?
order by table_name
|] schema
columns :: QualifiedTable -> H.Tx H.Postgres s [Column]
columns table = do
cols <- H.list $ [H.q|
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 |] (qtSchema table) (qtName table)
fks <- foreignKeys table
return $ map (\col -> col { colFK = Map.lookup (cs . colName $ col) fks }) cols
primaryKeyColumns :: QualifiedTable -> H.Tx H.Postgres s [Text]
primaryKeyColumns table = do
r :: [Identity Text] <- H.list $ [H.q|
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 = ? |] (qtSchema table) (qtName table)
return $ map runIdentity r
vanishNull :: [a] -> Maybe [a]
vanishNull xs = if L.null xs then Nothing else Just xs
toBool :: Text -> Bool
toBool = (== "YES")
data Table = Table {
tableSchema :: Text
, tableName :: Text
, tableInsertable :: Bool
} deriving (Show)
data ForeignKey = ForeignKey {
fkTable::Text, fkCol::Text
} deriving (Eq, Show)
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
} deriving (Show)
instance H.RowParser H.Postgres Column where
parseRow r =
let schema = H.parseResult $ r V.! 0
table = H.parseResult $ r V.! 1
name = H.parseResult $ r V.! 2
position = H.parseResult $ r V.! 3
nullable = toBool <$> (H.parseResult $ r V.! 4 :: Either Text Text)
typ = H.parseResult $ r V.! 5
updatable = toBool <$> (H.parseResult $ r V.! 6 :: Either Text Text)
maxLen = H.parseResult $ r V.! 7
precision = H.parseResult $ r V.! 8
defValue = H.parseResult $ r V.! 9
enum = either (const $ Right []) (Right . split (==','))
(H.parseResult $ r V.! 10 :: Either Text Text)
in
if V.length r /= 11
then Left "Wrong number of fields in Column"
else Column <$> schema <*> table <*> name <*> position <*> nullable
<*> typ <*> updatable <*> maxLen <*> precision
<*> defValue <*> enum
<*> return Nothing
instance H.RowParser H.Postgres Table where
parseRow r =
let schema = H.parseResult $ r V.! 0
name = H.parseResult $ r V.! 1
insertable = toBool <$> (H.parseResult $ r V.! 2 :: Either Text Text) in
if V.length r /= 3
then Left "Wrong number of fields in Table"
else Table <$> schema <*> name <*> insertable
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 ]
+221
View File
@@ -0,0 +1,221 @@
module PostgREST.ApiRequest where
import qualified Data.Aeson as JSON
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import qualified Data.Csv as CSV
import Data.List (find)
import qualified Data.HashMap.Strict as M
import qualified Data.Set as S
import Data.Maybe (fromMaybe, isJust, isNothing,
listToMaybe, fromJust)
import Control.Monad (join)
import Data.Monoid ((<>))
import Data.String.Conversions (cs)
import qualified Data.Text as T
import qualified Data.Vector as V
import Network.Wai (Request (..))
import Network.Wai.Parse (parseHttpAccept)
import PostgREST.RangeQuery (NonnegRange, rangeRequested)
import PostgREST.Types (QualifiedIdentifier (..),
Schema, Payload(..),
UniformObjects(..))
import Data.Ranged.Ranges (singletonRange)
type RequestBody = BL.ByteString
-- | Types of things a user wants to do to tables/views/procs
data Action = ActionCreate | ActionRead
| ActionUpdate | ActionDelete
| ActionInfo | ActionInvoke
| ActionUnknown BS.ByteString deriving Eq
-- | The target db object of a user action
data Target = TargetIdent QualifiedIdentifier
| TargetRoot
| TargetUnknown [T.Text]
-- | How to return the inserted data
data PreferRepresentation = Full | HeadersOnly | None deriving Eq
-- | Enumeration of currently supported content types for
-- route responses and upload payloads
data ContentType = ApplicationJSON | TextCSV deriving Eq
instance Show ContentType where
show ApplicationJSON = "application/json"
show TextCSV = "text/csv"
{-|
Describes what the user wants to do. This data type is a
translation of the raw elements of an HTTP request into domain
specific language. There is no guarantee that the intent is
sensible, it is up to a later stage of processing to determine
if it is an action we are able to perform.
-}
data ApiRequest = ApiRequest {
-- | Set to Nothing for unknown HTTP verbs
iAction :: Action
-- | Set to Nothing for malformed range
, iRange :: NonnegRange
-- | Set to Nothing for strangely nested urls
, iTarget :: Target
-- | The content type the client most desires (or JSON if undecided)
, iAccepts :: Either BS.ByteString ContentType
-- | Data sent by client and used for mutation actions
, iPayload :: Maybe Payload
-- | If client wants created items echoed back
, iPreferRepresentation :: PreferRepresentation
-- | If client wants first row as raw object
, iPreferSingular :: Bool
-- | Whether the client wants a result count (slower)
, iPreferCount :: Bool
-- | Filters on the result ("id", "eq.10")
, iFilters :: [(String, String)]
-- | &select parameter used to shape the response
, iSelect :: String
-- | &order parameter
, iOrder :: Maybe String
}
-- | Examines HTTP request and translates it into user intent.
userApiRequest :: Schema -> Request -> RequestBody -> ApiRequest
userApiRequest schema req reqBody =
let action = case method of
"GET" -> ActionRead
"POST" -> if isTargetingProc
then ActionInvoke
else ActionCreate
"PATCH" -> ActionUpdate
"DELETE" -> ActionDelete
"OPTIONS" -> ActionInfo
other -> ActionUnknown other
target = case path of
[] -> TargetRoot
[table] -> TargetIdent
$ QualifiedIdentifier schema table
["rpc", proc] -> TargetIdent
$ QualifiedIdentifier schema proc
other -> TargetUnknown other
payload = case pickContentType (lookupHeader "content-type") of
Right ApplicationJSON ->
either (PayloadParseError . cs)
(\val -> case ensureUniform (pluralize val) of
Nothing -> PayloadParseError "All object keys must match"
Just json -> PayloadJSON json)
(JSON.eitherDecode reqBody)
Right TextCSV ->
either (PayloadParseError . cs)
(\val -> case ensureUniform (csvToJson val) of
Nothing -> PayloadParseError "All lines must have same number of fields"
Just json -> PayloadJSON json)
(CSV.decodeByName reqBody)
Left accept ->
PayloadParseError $
"Content-type not acceptable: " <> accept
relevantPayload = case action of
ActionCreate -> Just payload
ActionUpdate -> Just payload
ActionInvoke -> Just payload
_ -> Nothing in
ApiRequest {
iAction = action
, iRange = if singular then singletonRange 0 else rangeRequested hdrs
, iTarget = target
, iAccepts = pickContentType $ lookupHeader "accept"
, iPayload = relevantPayload
, iPreferRepresentation = representation
, iPreferSingular = singular
, iPreferCount = not $ hasPrefer "count=none"
, iFilters = [ (k, fromJust v) | (k,v) <- qParams, k `notElem` ["select", "order"], isJust v ]
, iSelect = if method == "DELETE"
then "*"
else fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
, iOrder = join $ lookup "order" qParams
}
where
path = pathInfo req
method = requestMethod req
isTargetingProc = fromMaybe False $ (== "rpc") <$> listToMaybe path
hdrs = requestHeaders req
qParams = [(cs k, cs <$> v)|(k,v) <- queryString req]
lookupHeader = flip lookup hdrs
hasPrefer val = any (\(h,v) -> h == "Prefer" && v == val) hdrs
singular = hasPrefer "plurality=singular"
representation
| hasPrefer "return=representation" = Full
| hasPrefer "return=minimal" = None
| otherwise = HeadersOnly
-- PRIVATE ---------------------------------------------------------------
{-|
Picks a preferred content type from an Accept header (or from
Content-Type as a degenerate case).
For example
text/csv -> TextCSV
*/* -> ApplicationJSON
text/csv, application/json -> TextCSV
application/json, text/csv -> ApplicationJSON
-}
pickContentType :: Maybe BS.ByteString -> Either BS.ByteString ContentType
pickContentType accept
| isNothing accept || has ctAll || has ctJson = Right ApplicationJSON
| has ctCsv = Right TextCSV
| otherwise = Left accept'
where
ctAll = "*/*"
ctCsv = "text/csv"
ctJson = "application/json"
Just accept' = accept
findInAccept = flip find $ parseHttpAccept accept'
has = isJust . findInAccept . BS.isPrefixOf
type CsvData = V.Vector (M.HashMap T.Text BL.ByteString)
{-|
Converts CSV like
a,b
1,hi
2,bye
into a JSON array like
[ {"a": "1", "b": "hi"}, {"a": 2, "b": "bye"} ]
The reason for its odd signature is so that it can compose
directly with CSV.decodeByName
-}
csvToJson :: (CSV.Header, CsvData) -> JSON.Array
csvToJson (_, vals) =
V.map rowToJsonObj vals
where
rowToJsonObj = JSON.Object .
M.map (\str ->
if str == "NULL"
then JSON.Null
else JSON.String $ cs str
)
-- | Convert {foo} to [{foo}], leave arrays unchanged
-- and truncate everything else to an empty array.
pluralize :: JSON.Value -> JSON.Array
pluralize obj@(JSON.Object _) = V.singleton obj
pluralize (JSON.Array arr) = arr
pluralize _ = V.empty
-- | Test that Array contains only Objects having the same keys
-- and if so mark it as UniformObjects
ensureUniform :: JSON.Array -> Maybe UniformObjects
ensureUniform arr =
let objs :: V.Vector JSON.Object
objs = foldr -- filter non-objects, map to raw objects
(\val result -> case val of
JSON.Object o -> V.cons o result
_ -> result)
V.empty arr
keysPerObj = V.map (S.fromList . M.keys) objs
canonicalKeys = fromMaybe S.empty $ keysPerObj V.!? 0
areKeysUniform = all (==canonicalKeys) keysPerObj in
if (V.length objs == V.length arr) && areKeysUniform
then Just (UniformObjects objs)
else Nothing
+340
View File
@@ -0,0 +1,340 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
--module PostgREST.App where
module PostgREST.App (
app
) where
import Control.Applicative
import Control.Arrow ((***))
import Control.Monad (join)
import Data.Bifunctor (first)
import qualified Data.ByteString.Lazy as BL
import Data.Functor.Identity
import Data.List (find, sortBy, delete)
import Data.Maybe (fromMaybe, fromJust, mapMaybe)
import Data.Ord (comparing)
import Data.Ranged.Ranges (emptyRange)
import Data.String.Conversions (cs)
import Data.Text (Text, replace, strip)
import Data.Tree
import Text.Parsec.Error
import Text.ParserCombinators.Parsec (parse)
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 Data.Aeson
import Data.Aeson.Types (emptyArray)
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.Config (AppConfig (..))
import PostgREST.Parsers
import PostgREST.DbStructure
import PostgREST.RangeQuery
import PostgREST.ApiRequest (ApiRequest(..), ContentType(..)
, Action(..), Target(..)
, PreferRepresentation (..)
, userApiRequest)
import PostgREST.Types
import PostgREST.Auth (tokenJWT)
import PostgREST.Error (errResponse)
import PostgREST.QueryBuilder ( asJson
, callProc
, addJoinConditions
, sourceCTEName
, requestToQuery
, requestToCountQuery
, addRelations
, createReadStatement
, createWriteStatement
)
import Prelude
app :: DbStructure -> AppConfig -> RequestBody -> Request -> H.Tx P.Postgres s Response
app dbStructure conf reqBody req =
let
-- TODO: blow up for Left values (there is a middleware that checks the headers)
contentType = either (const ApplicationJSON) id (iAccepts apiRequest)
contentTypeH = (hContentType, cs $ show contentType) in
case (iAction apiRequest, iTarget apiRequest, iPayload apiRequest) of
(ActionRead, TargetIdent qi, Nothing) ->
case readSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (q, cq) -> do
let range = restrictRange (configMaxRows conf) $ iRange apiRequest
singular = iPreferSingular apiRequest
stm = createReadStatement q cq range singular
(iPreferCount apiRequest) (contentType == TextCSV)
if range == emptyRange
then return $ errResponse status416 "HTTP Range error"
else do
row <- H.maybeEx stm
let (tableTotal, queryTotal, _ , body) = extractQueryResult row
if singular
then return $ if queryTotal <= 0
then responseLBS status404 [] ""
else responseLBS status200 [contentTypeH] (fromMaybe "{}" body)
else do
let frm = rangeOffset range
to = frm+queryTotal-1
contentRange = contentRangeH frm to tableTotal
status = rangeStatus frm to tableTotal
canonical = urlEncodeVars -- should this be moved to the dbStructure (location)?
. sortBy (comparing fst)
. map (join (***) cs)
. parseSimpleQuery
$ rawQueryString req
return $ responseLBS status
[contentTypeH, contentRange,
("Content-Location",
"/" <> cs (qiName qi) <>
if Prelude.null canonical then "" else "?" <> cs canonical
)
] (fromMaybe "[]" body)
(ActionCreate, TargetIdent qi@(QualifiedIdentifier _ table),
Just payload@(PayloadJSON (UniformObjects rows))) ->
case mutateSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (sq,mq) -> do
let isSingle = (==1) $ V.length rows
let pKeys = map pkName $ filter (filterPk schema table) allPrKeys -- would it be ok to move primary key detection in the query itself?
let stm = createWriteStatement qi sq mq isSingle (iPreferRepresentation apiRequest) pKeys (contentType == TextCSV) payload
row <- H.maybeEx stm
let (_, _, location, body) = extractQueryResult row
return $ responseLBS status201
[
contentTypeH,
(hLocation, "/" <> cs table <> "?" <> cs (fromMaybe "" location))
]
$ if iPreferRepresentation apiRequest == Full then fromMaybe "[]" body else ""
(ActionUpdate, TargetIdent qi, Just payload@(PayloadJSON _)) ->
case mutateSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (sq,mq) -> do
let stm = createWriteStatement qi sq mq False (iPreferRepresentation apiRequest) [] (contentType == TextCSV) payload
row <- H.maybeEx stm
let (_, queryTotal, _, body) = extractQueryResult row
r = contentRangeH 0 (queryTotal-1) (Just queryTotal)
s = case () of _ | queryTotal == 0 -> status404
| iPreferRepresentation apiRequest == Full -> status200
| otherwise -> status204
return $ responseLBS s [contentTypeH, r]
$ if iPreferRepresentation apiRequest == Full then fromMaybe "[]" body else ""
(ActionDelete, TargetIdent qi, Nothing) ->
case mutateSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (sq,mq) -> do
let fakeload = PayloadJSON $ UniformObjects V.empty
let stm = createWriteStatement qi sq mq False (iPreferRepresentation apiRequest) [] (contentType == TextCSV) fakeload
row <- H.maybeEx stm
let (_, queryTotal, _, _) = extractQueryResult row
return $ if queryTotal == 0
then notFound
else responseLBS status204 [("Content-Range", "*/"<> cs (show queryTotal))] ""
(ActionInfo, TargetIdent (QualifiedIdentifier tSchema tTable), Nothing) -> do
let cols = filter (filterCol tSchema tTable) $ dbColumns dbStructure
pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys
body = encode (TableOptions cols pkeys)
filterCol :: Schema -> TableName -> Column -> Bool
filterCol sc tb (Column{colTable=Table{tableSchema=s, tableName=t}}) = s==sc && t==tb
filterCol _ _ _ = False
return $ responseLBS status200 [jsonH, allOrigins] $ cs body
(ActionInvoke, TargetIdent qi,
Just (PayloadJSON (UniformObjects payload))) -> do
exists <- doesProcExist qi
if exists
then do
let p = V.head payload
call = B.Stmt "select " V.empty True <>
asJson (callProc qi p)
jwtSecret = configJwtSecret conf
bodyJson :: Maybe (Identity Value) <- H.maybeEx call
returnJWT <- doesProcReturnJWT qi
return $ responseLBS status200 [jsonH]
(let body = fromMaybe emptyArray $ runIdentity <$> bodyJson in
if returnJWT
then "{\"token\":\"" <> cs (tokenJWT jwtSecret body) <> "\"}"
else cs $ encode body)
else return notFound
(ActionRead, TargetRoot, Nothing) -> do
body <- encode <$> accessibleTables (filter ((== cs schema) . tableSchema) (dbTables dbStructure))
return $ responseLBS status200 [jsonH] $ cs body
(ActionUnknown _, _, _) -> return notFound
(_, TargetUnknown _, _) -> return notFound
(_, _, Just (PayloadParseError e)) ->
return $ responseLBS status400 [jsonH] $
cs (formatGeneralError "Cannot parse request payload" (cs e))
(_, _, _) -> return notFound
where
notFound = responseLBS status404 [] ""
filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk
allPrKeys = dbPrimaryKeys dbStructure
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
schema = cs $ configSchema conf
apiRequest = userApiRequest schema req reqBody
readDbRequest = DbRead <$> buildReadRequest (dbRelations dbStructure) apiRequest
mutateDbRequest = DbMutate <$> buildMutateRequest apiRequest
selectQuery = requestToQuery schema <$> readDbRequest
countQuery = requestToCountQuery schema <$> readDbRequest
mutateQuery = requestToQuery schema <$> mutateDbRequest
readSqlParts = (,) <$> selectQuery <*> countQuery
mutateSqlParts = (,) <$> selectQuery <*> mutateQuery
rangeStatus :: Int -> Int -> Maybe Int -> Status
rangeStatus _ _ Nothing = status200
rangeStatus frm to (Just total)
| frm > total = status416
| (1 + to - frm) < total = status206
| otherwise = status200
contentRangeH :: Int -> Int -> Maybe Int -> Header
contentRangeH frm to total =
("Content-Range", cs headerValue)
where
headerValue = rangeString <> "/" <> totalString
rangeString
| totalNotZero && fromInRange = show frm <> "-" <> cs (show to)
| otherwise = "*"
totalString = fromMaybe "*" (show <$> total)
totalNotZero = fromMaybe True ((/=) 0 <$> total)
fromInRange = frm <= to
jsonH :: Header
jsonH = (hContentType, "application/json")
formatRelationError :: Text -> Text
formatRelationError = formatGeneralError
"could not find foreign keys between these entities"
formatParserError :: ParseError -> Text
formatParserError e = formatGeneralError message details
where
message = cs $ show (errorPos e)
details = strip $ replace "\n" " " $ cs
$ showErrorMessages "or" "unknown parse error" "expecting" "unexpected" "end of input" (errorMessages e)
formatGeneralError :: Text -> Text -> Text
formatGeneralError message details = cs $ encode $ object [
"message" .= message,
"details" .= details]
augumentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either Text ReadRequest
augumentRequestWithJoin schema allRels request =
(first formatRelationError . addRelations schema allRels Nothing) request
>>= addJoinConditions schema
buildReadRequest :: [Relation] -> ApiRequest -> Either Text ReadRequest
buildReadRequest allRels apiRequest =
augumentRequestWithJoin schema rels =<< first formatParserError (foldr addFilter <$> (addOrder <$> readRequest <*> ord) <*> flts)
where
selStr = iSelect apiRequest
orderS = iOrder apiRequest
action = iAction apiRequest
target = iTarget apiRequest
(schema, rootTableName) = fromJust $ -- Make it safe
case target of
(TargetIdent (QualifiedIdentifier s t) ) -> Just (s, t)
_ -> Nothing
rootName = if action == ActionRead
then rootTableName
else sourceCTEName
filters = if action == ActionRead
then iFilters apiRequest
else filter (( '.' `elem` ) . fst) $ iFilters apiRequest -- there can be no filters on the root table whre we are doing insert/update
rels = case action of
ActionCreate -> fakeSourceRelations ++ allRels
ActionUpdate -> fakeSourceRelations ++ allRels
_ -> allRels
where fakeSourceRelations = mapMaybe (toSourceRelation rootTableName) allRels -- see comment in toSourceRelation
readRequest = parse (pRequestSelect rootName) ("failed to parse select parameter <<"++selStr++">>") selStr
addOrder (Node (q,i) f) o = Node (q{order=o}, i) f
flts = mapM pRequestFilter filters
ord = traverse (parse pOrder ("failed to parse order parameter <<"++fromMaybe "" orderS++">>")) orderS
buildMutateRequest :: ApiRequest -> Either Text MutateRequest
buildMutateRequest apiRequest =
mutateApiRequest
where
action = iAction apiRequest
target = iTarget apiRequest
payload = fromJust $ iPayload apiRequest
rootTableName = -- TODO: Make it safe
case target of
(TargetIdent (QualifiedIdentifier _ t) ) -> t
_ -> undefined
mutateApiRequest = case action of
ActionCreate -> Insert rootTableName <$> pure payload
ActionUpdate -> Update rootTableName <$> pure payload <*> cond
ActionDelete -> Delete rootTableName <$> cond
_ -> Left "Unsupported HTTP verb"
mutateFilters = filter (not . ( '.' `elem` ) . fst) $ iFilters apiRequest -- update/delete filters can be only on the root table
cond = first formatParserError $ map snd <$> mapM pRequestFilter mutateFilters
addFilter :: (Path, Filter) -> ReadRequest -> ReadRequest
addFilter ([], flt) (Node (q@(Select {flt_=flts}), i) forest) = Node (q {flt_=flt:flts}, i) 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==).fst.snd.rootLabel) forst
-- in a relation where one of the tables mathces "TableName"
-- replace the name to that table with pg_source
-- this "fake" relations is needed so that in a mutate query
-- we can look a the "returning *" part which is wrapped with a "with"
-- as just another table that has relations with other tables
toSourceRelation :: TableName -> Relation -> Maybe Relation
toSourceRelation mt r@(Relation t _ ft _ _ rt _ _)
| mt == tableName t = Just $ r {relTable=t {tableName=sourceCTEName}}
| mt == tableName ft = Just $ r {relFTable=t {tableName=sourceCTEName}}
| Just mt == (tableName <$> rt) = Just $ r {relLTable=(\tbl -> tbl {tableName=sourceCTEName}) <$> rt}
| otherwise = Nothing
data TableOptions = TableOptions {
tblOptcolumns :: [Column]
, tblOptpkey :: [Text]
}
instance ToJSON TableOptions where
toJSON t = object [
"columns" .= tblOptcolumns t
, "pkey" .= tblOptpkey t ]
extractQueryResult :: Maybe (Maybe Int, Int, Maybe BL.ByteString, Maybe BL.ByteString)
-> (Maybe Int, Int, Maybe BL.ByteString, Maybe BL.ByteString)
extractQueryResult = fromMaybe (Just 0, 0, Just "", Just "")
+84
View File
@@ -0,0 +1,84 @@
{-# LANGUAGE FlexibleContexts #-}
{-|
Module : PostgREST.Auth
Description : PostgREST authorization functions.
This module provides functions to deal with the JWT authorization (http://jwt.io).
It also can be used to define other authorization functions,
in the future Oauth, LDAP and similar integrations can be coded here.
Authentication should always be implemented in an external service.
In the test suite there is an example of simple login function that can be used for a
very simple authentication system inside the PostgreSQL database.
-}
module PostgREST.Auth (
setRole
, claimsToSQL
, jwtClaims
, tokenJWT
) where
import Control.Monad (join)
import Data.Aeson (Value (..), Object)
import Data.Aeson.Types (emptyObject, emptyArray)
import Data.Vector as V (null, head)
import Data.Map as M (fromList, toList)
import Data.Monoid ((<>))
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Time.Clock (NominalDiffTime)
import PostgREST.QueryBuilder (pgFmtLit, pgFmtIdent, unquoted)
import qualified Web.JWT as JWT
import qualified Data.HashMap.Lazy as H
{-|
Receives a map of JWT claims and returns a list
of PostgreSQL statements to set the claims as user defined GUCs.
Except if we have a claim called role,
this one is mapped to a SET ROLE statement.
In case there is any problem decoding the JWT it returns Nothing.
-}
claimsToSQL :: JWT.ClaimsMap -> [Text]
claimsToSQL = map setVar . toList
where
setVar ("role", String val) = setRole val
setVar (k, val) = "set local postgrest.claims." <> pgFmtIdent k <>
" = " <> valueToVariable val <> ";"
valueToVariable = pgFmtLit . unquoted
{-|
Receives the JWT secret (from config) and a JWT and
returns a map of JWT claims
In case there is any problem decoding the JWT it returns Nothing.
-}
jwtClaims :: JWT.Secret -> Text -> NominalDiffTime -> Maybe JWT.ClaimsMap
jwtClaims secret input time =
case join $ claim JWT.exp of
Just expires ->
if JWT.secondsSinceEpoch expires > time
then customClaims
else Nothing
_ -> customClaims
where
decoded = JWT.decodeAndVerifySignature secret input
claim :: (JWT.JWTClaimsSet -> a) -> Maybe a
claim prop = prop . JWT.claims <$> decoded
customClaims = claim JWT.unregisteredClaims
-- | Receives the name of a role and returns a SET ROLE statement
setRole :: Text -> Text
setRole role = "set local role " <> cs (pgFmtLit role) <> ";"
{-|
Receives the JWT secret (from config) and a JWT and a JSON value
and returns a signed JWT.
-}
tokenJWT :: JWT.Secret -> Value -> Text
tokenJWT secret (Array a) = JWT.encodeSigned JWT.HS256 secret
JWT.def { JWT.unregisteredClaims = fromHashMap o }
where
Object o = if V.null a then emptyObject else V.head a
fromHashMap :: Object -> JWT.ClaimsMap
fromHashMap = M.fromList . H.toList
tokenJWT secret _ = tokenJWT secret emptyArray
+102
View File
@@ -0,0 +1,102 @@
{-|
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
import Paths_postgrest (version)
import Safe (readMay)
import Web.JWT (Secret, secret)
import Prelude
-- | Data type to store all command line options
data AppConfig = AppConfig {
configDatabase :: String
, configPort :: Int
, configAnonRole :: String
, configSchema :: String
, configJwtSecret :: Secret
, configPool :: Int
, configMaxRows :: Maybe Int
}
argParser :: Parser AppConfig
argParser = AppConfig
<$> argument str (help "database connection string" <> metavar "STRING")
<*> option auto (long "port" <> short 'p' <> help "port number on which to run HTTP server" <> metavar "PORT" <> value 3000 <> showDefault)
<*> strOption (long "anonymous" <> short 'a' <> help "postgres role to use for non-authenticated requests" <> metavar "ROLE")
<*> strOption (long "schema" <> short 's' <> help "schema to use for API routes" <> metavar "NAME" <> value "public" <> showDefault)
<*> (secret . cs <$>
strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault))
<*> option auto (long "pool" <> short 'o' <> help "max connections in database pool" <> metavar "COUNT" <> value 10 <> showDefault)
<*> (readMay <$> strOption (long "max-rows" <> short 'm' <> help "max rows in response" <> metavar "COUNT" <> value "infinity" <> showDefault))
defaultCorsPolicy :: CorsResourcePolicy
defaultCorsPolicy = CorsResourcePolicy Nothing
["GET", "POST", "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
+564
View File
@@ -0,0 +1,564 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeSynonymInstances #-}
module PostgREST.DbStructure (
getDbStructure
, accessibleTables
, doesProcExist
, doesProcReturnJWT
) where
import Control.Applicative
import Control.Monad (join)
import Data.Functor.Identity
import Data.List (elemIndex, find, subsequences, sort, transpose)
import Data.Maybe (fromMaybe, fromJust, isJust, mapMaybe, listToMaybe)
import Data.Monoid
import Data.Text (Text, split)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import qualified Hasql.Backend as B
import PostgREST.Types
import GHC.Exts (groupWith)
import Prelude
getDbStructure :: Schema -> H.Tx P.Postgres s DbStructure
getDbStructure schema = do
tabs <- allTables
cols <- allColumns tabs
syns <- allSynonyms cols
rels <- allRelations tabs cols
keys <- allPrimaryKeys tabs
let rels' = (addManyToManyRelations . raiseRelations schema syns . addParentRelations . addSynonymousRelations syns) rels
cols' = addForeignKeys rels' cols
keys' = synonymousPrimaryKeys syns keys
return DbStructure {
dbTables = tabs
, dbColumns = cols'
, dbRelations = rels'
, dbPrimaryKeys = keys'
}
doesProc :: forall c s. B.CxValue c Int =>
(Text -> Text -> B.Stmt c) -> QualifiedIdentifier -> H.Tx c s Bool
doesProc stmt qi = do
row :: Maybe (Identity Int) <- H.maybeEx $ stmt (qiSchema qi) (qiName qi)
return $ isJust row
doesProcExist :: QualifiedIdentifier -> H.Tx P.Postgres s Bool
doesProcExist = doesProc [H.stmt|
SELECT 1
FROM pg_catalog.pg_namespace n
JOIN pg_catalog.pg_proc p
ON pronamespace = n.oid
WHERE nspname = ?
AND proname = ?
|]
doesProcReturnJWT :: QualifiedIdentifier -> H.Tx P.Postgres s Bool
doesProcReturnJWT = doesProc [H.stmt|
SELECT 1
FROM pg_catalog.pg_namespace n
JOIN pg_catalog.pg_proc p
ON pronamespace = n.oid
WHERE nspname = ?
AND proname = ?
AND pg_catalog.pg_get_function_result(p.oid) like '%jwt_claims'
|]
accessibleTables :: [Table] -> H.Tx P.Postgres s [Table]
accessibleTables allTabs = do
accessible <- H.listEx $ [H.stmt|
SELECT
n.nspname AS table_schema,
c.relname AS table_name
FROM pg_class c
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(c.relowner, 'USAGE'::text) OR
has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR
has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text)
)
ORDER BY table_schema, table_name
|]
let isAccessible table = isJust $ find (\(s,n) -> tableSchema table == s && tableName table == n) accessible
return $ filter isAccessible allTabs
synonymousColumns :: [(Column,Column)] -> [Column] -> [[Column]]
synonymousColumns allSyns cols = synCols'
where
syns = sort $ filter ((== colTable (head cols)) . colTable . fst) allSyns
synCols  = transpose $ map (\c -> map snd $ filter ((== c) . fst) syns) cols
synCols' = (filter sameTable . filter matchLength) synCols
matchLength cs = length cols == length cs
sameTable (c:cs) = all (\cc -> colTable c == colTable cc) (c:cs)
sameTable [] = False
addForeignKeys :: [Relation] -> [Column] -> [Column]
addForeignKeys rels = map addFk
where
addFk col = col { colFK = fk col }
fk col = join $ relToFk col <$> find (lookupFn col) rels
lookupFn :: Column -> Relation -> Bool
lookupFn c (Relation{relColumns=cs, relType=rty}) = c `elem` cs && rty==Child
-- lookupFn _ _ = False
relToFk col (Relation{relColumns=cols, relFColumns=colsF}) = ForeignKey <$> colF
where
pos = elemIndex col cols
colF = (colsF !!) <$> pos
addSynonymousRelations :: [(Column,Column)] -> [Relation] -> [Relation]
addSynonymousRelations _ [] = []
addSynonymousRelations syns (rel:rels) = rel : synRelsP ++ synRelsF ++ addSynonymousRelations syns rels
where
synRelsP = synRels (relColumns rel) (\t cs -> rel{relTable=t,relColumns=cs})
synRelsF = synRels (relFColumns rel) (\t cs -> rel{relFTable=t,relFColumns=cs})
synRels cols mapFn = map (\cs -> mapFn (colTable $ head cs) cs) $ synonymousColumns syns cols
addParentRelations :: [Relation] -> [Relation]
addParentRelations [] = []
addParentRelations (rel@(Relation t c ft fc _ _ _ _):rels) = Relation ft fc t c Parent Nothing Nothing Nothing : rel : addParentRelations rels
addManyToManyRelations :: [Relation] -> [Relation]
addManyToManyRelations rels = rels ++ mapMaybe link2Relation links
where
links = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==Child). relType) rels
groupFn :: Relation -> Text
groupFn (Relation{relTable=Table{tableSchema=s, tableName=t}}) = s<>"_"<>t
combinations k ns = filter ((k==).length) (subsequences ns)
link2Relation [
Relation{relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c},
Relation{ relColumns=lc2, relFTable=ft, relFColumns=fc}
]
| lc1 /= lc2 && length lc1 == 1 && length lc2 == 1 = Just $ Relation t c ft fc Many (Just lt) (Just lc1) (Just lc2)
| otherwise = Nothing
link2Relation _ = Nothing
raiseRelations :: Schema -> [(Column,Column)] -> [Relation] -> [Relation]
raiseRelations schema syns = map raiseRel
where
raiseRel rel
| tableSchema table == schema = rel
| isJust newCols = rel{relFTable=fromJust newTable,relFColumns=fromJust newCols}
| otherwise = rel
where
cols = relFColumns rel
table = relFTable rel
newCols = listToMaybe $ filter ((== schema) . tableSchema . colTable . head) (synonymousColumns syns cols)
newTable = (colTable . head) <$> newCols
synonymousPrimaryKeys :: [(Column,Column)] -> [PrimaryKey] -> [PrimaryKey]
synonymousPrimaryKeys _ [] = []
synonymousPrimaryKeys syns (key:keys) = key : newKeys ++ synonymousPrimaryKeys syns keys
where
keySyns = filter ((\c -> colTable c == pkTable key && colName c == pkName key) . fst) syns
newKeys = map ((\c -> PrimaryKey{pkTable=colTable c,pkName=colName c}) . snd) keySyns
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
FROM pg_class c
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')
GROUP BY table_schema, table_name, insertable
ORDER BY table_schema, table_name
|]
return $ map tableFromRow rows
tableFromRow :: (Text, Text, Bool) -> Table
tableFromRow (s, n, i) = Table s n i
allColumns :: [Table] -> H.Tx P.Postgres s [Column]
allColumns tabs = do
cols <- H.listEx $ [H.stmt|
SELECT DISTINCT
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 (
/*
-- CTE based on information_schema.columns to remove the owner filter
*/
WITH columns AS (
SELECT current_database()::information_schema.sql_identifier AS table_catalog,
nc.nspname::information_schema.sql_identifier AS table_schema,
c.relname::information_schema.sql_identifier AS table_name,
a.attname::information_schema.sql_identifier AS column_name,
a.attnum::information_schema.cardinal_number AS ordinal_position,
pg_get_expr(ad.adbin, ad.adrelid)::information_schema.character_data AS column_default,
CASE
WHEN a.attnotnull OR t.typtype = 'd'::"char" AND t.typnotnull THEN 'NO'::text
ELSE 'YES'::text
END::information_schema.yes_or_no AS is_nullable,
CASE
WHEN t.typtype = 'd'::"char" THEN
CASE
WHEN bt.typelem <> 0::oid AND bt.typlen = (-1) THEN 'ARRAY'::text
WHEN nbt.nspname = 'pg_catalog'::name THEN format_type(t.typbasetype, NULL::integer)
ELSE 'USER-DEFINED'::text
END
ELSE
CASE
WHEN t.typelem <> 0::oid AND t.typlen = (-1) THEN 'ARRAY'::text
WHEN nt.nspname = 'pg_catalog'::name THEN format_type(a.atttypid, NULL::integer)
ELSE 'USER-DEFINED'::text
END
END::information_schema.character_data AS data_type,
information_schema._pg_char_max_length(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS character_maximum_length,
information_schema._pg_char_octet_length(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS character_octet_length,
information_schema._pg_numeric_precision(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS numeric_precision,
information_schema._pg_numeric_precision_radix(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS numeric_precision_radix,
information_schema._pg_numeric_scale(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS numeric_scale,
information_schema._pg_datetime_precision(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS datetime_precision,
information_schema._pg_interval_type(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.character_data AS interval_type,
NULL::integer::information_schema.cardinal_number AS interval_precision,
NULL::character varying::information_schema.sql_identifier AS character_set_catalog,
NULL::character varying::information_schema.sql_identifier AS character_set_schema,
NULL::character varying::information_schema.sql_identifier AS character_set_name,
CASE
WHEN nco.nspname IS NOT NULL THEN current_database()
ELSE NULL::name
END::information_schema.sql_identifier AS collation_catalog,
nco.nspname::information_schema.sql_identifier AS collation_schema,
co.collname::information_schema.sql_identifier AS collation_name,
CASE
WHEN t.typtype = 'd'::"char" THEN current_database()
ELSE NULL::name
END::information_schema.sql_identifier AS domain_catalog,
CASE
WHEN t.typtype = 'd'::"char" THEN nt.nspname
ELSE NULL::name
END::information_schema.sql_identifier AS domain_schema,
CASE
WHEN t.typtype = 'd'::"char" THEN t.typname
ELSE NULL::name
END::information_schema.sql_identifier AS domain_name,
current_database()::information_schema.sql_identifier AS udt_catalog,
COALESCE(nbt.nspname, nt.nspname)::information_schema.sql_identifier AS udt_schema,
COALESCE(bt.typname, t.typname)::information_schema.sql_identifier AS udt_name,
NULL::character varying::information_schema.sql_identifier AS scope_catalog,
NULL::character varying::information_schema.sql_identifier AS scope_schema,
NULL::character varying::information_schema.sql_identifier AS scope_name,
NULL::integer::information_schema.cardinal_number AS maximum_cardinality,
a.attnum::information_schema.sql_identifier AS dtd_identifier,
'NO'::character varying::information_schema.yes_or_no AS is_self_referencing,
'NO'::character varying::information_schema.yes_or_no AS is_identity,
NULL::character varying::information_schema.character_data AS identity_generation,
NULL::character varying::information_schema.character_data AS identity_start,
NULL::character varying::information_schema.character_data AS identity_increment,
NULL::character varying::information_schema.character_data AS identity_maximum,
NULL::character varying::information_schema.character_data AS identity_minimum,
NULL::character varying::information_schema.yes_or_no AS identity_cycle,
'NEVER'::character varying::information_schema.character_data AS is_generated,
NULL::character varying::information_schema.character_data AS generation_expression,
CASE
WHEN c.relkind = 'r'::"char" OR (c.relkind = ANY (ARRAY['v'::"char", 'f'::"char"])) AND pg_column_is_updatable(c.oid::regclass, a.attnum, false) THEN 'YES'::text
ELSE 'NO'::text
END::information_schema.yes_or_no AS is_updatable
FROM pg_attribute a
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
JOIN (pg_class c
JOIN pg_namespace nc ON c.relnamespace = nc.oid) ON a.attrelid = c.oid
JOIN (pg_type t
JOIN pg_namespace nt ON t.typnamespace = nt.oid) ON a.atttypid = t.oid
LEFT JOIN (pg_type bt
JOIN pg_namespace nbt ON bt.typnamespace = nbt.oid) ON t.typtype = 'd'::"char" AND t.typbasetype = bt.oid
LEFT JOIN (pg_collation co
JOIN pg_namespace nco ON co.collnamespace = nco.oid) ON a.attcollation = co.oid AND (nco.nspname <> 'pg_catalog'::name OR co.collname <> 'default'::name)
WHERE NOT pg_is_other_temp_schema(nc.oid) AND a.attnum > 0 AND NOT a.attisdropped AND (c.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char"]))
/*--AND (pg_has_role(c.relowner, 'USAGE'::text) OR has_column_privilege(c.oid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text))*/
)
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*/
FROM 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 $ mapMaybe (columnFromRow tabs) cols
columnFromRow :: [Table] ->
(Text, Text, Text,
Int, Bool, Text,
Bool, Maybe Int, Maybe Int,
Maybe Text, Maybe Text)
-> Maybe Column
columnFromRow tabs (s, t, n, pos, nul, typ, u, l, p, d, e) = buildColumn <$> table
where
buildColumn tbl = Column tbl n pos nul typ u l p d (parseEnum e) Nothing
table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs
parseEnum :: Maybe Text -> [Text]
parseEnum str = fromMaybe [] $ split (==',') <$> str
allRelations :: [Table] -> [Column] -> H.Tx P.Postgres s [Relation]
allRelations tabs cols = do
rels <- H.listEx $ [H.stmt|
SELECT ns1.nspname AS table_schema,
tab.relname AS table_name,
column_info.cols AS columns,
ns2.nspname AS foreign_table_schema,
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 ns1,
LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = conrelid) AS tab,
LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other,
LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = other.relnamespace) AS ns2
WHERE confrelid != 0
ORDER BY (conrelid, column_info.nums)
|]
return $ mapMaybe (relationFromRow tabs cols) rels
relationFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation
relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) =
Relation <$> table <*> cols <*> tableF <*> colsF <*> pure Child <*> pure Nothing <*> pure Nothing <*> pure Nothing
where
findTable s t = find (\tbl -> tableSchema tbl == s && tableName tbl == t) allTabs
findCol s t c = find (\col -> tableSchema (colTable col) == s && tableName (colTable col) == t && colName col == c) allCols
table = findTable rs rt
tableF = findTable frs frt
cols = mapM (findCol rs rt) rcs
colsF = mapM (findCol frs frt) frcs
allPrimaryKeys :: [Table] -> H.Tx P.Postgres s [PrimaryKey]
allPrimaryKeys tabs = do
pks <- H.listEx $ [H.stmt|
/*
-- CTE to replace information_schema.table_constraints to remove owner limit
*/
WITH tc AS (
SELECT current_database()::information_schema.sql_identifier AS constraint_catalog,
nc.nspname::information_schema.sql_identifier AS constraint_schema,
c.conname::information_schema.sql_identifier AS constraint_name,
current_database()::information_schema.sql_identifier AS table_catalog,
nr.nspname::information_schema.sql_identifier AS table_schema,
r.relname::information_schema.sql_identifier AS table_name,
CASE c.contype
WHEN 'c'::"char" THEN 'CHECK'::text
WHEN 'f'::"char" THEN 'FOREIGN KEY'::text
WHEN 'p'::"char" THEN 'PRIMARY KEY'::text
WHEN 'u'::"char" THEN 'UNIQUE'::text
ELSE NULL::text
END::information_schema.character_data AS constraint_type,
CASE
WHEN c.condeferrable THEN 'YES'::text
ELSE 'NO'::text
END::information_schema.yes_or_no AS is_deferrable,
CASE
WHEN c.condeferred THEN 'YES'::text
ELSE 'NO'::text
END::information_schema.yes_or_no AS initially_deferred
FROM pg_namespace nc,
pg_namespace nr,
pg_constraint c,
pg_class r
WHERE nc.oid = c.connamespace AND nr.oid = r.relnamespace AND c.conrelid = r.oid AND (c.contype <> ALL (ARRAY['t'::"char", 'x'::"char"])) AND r.relkind = 'r'::"char" AND NOT pg_is_other_temp_schema(nr.oid)
/*--AND (pg_has_role(r.relowner, 'USAGE'::text) OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR has_any_column_privilege(r.oid, 'INSERT, UPDATE, REFERENCES'::text))*/
UNION ALL
SELECT current_database()::information_schema.sql_identifier AS constraint_catalog,
nr.nspname::information_schema.sql_identifier AS constraint_schema,
(((((nr.oid::text || '_'::text) || r.oid::text) || '_'::text) || a.attnum::text) || '_not_null'::text)::information_schema.sql_identifier AS constraint_name,
current_database()::information_schema.sql_identifier AS table_catalog,
nr.nspname::information_schema.sql_identifier AS table_schema,
r.relname::information_schema.sql_identifier AS table_name,
'CHECK'::character varying::information_schema.character_data AS constraint_type,
'NO'::character varying::information_schema.yes_or_no AS is_deferrable,
'NO'::character varying::information_schema.yes_or_no AS initially_deferred
FROM pg_namespace nr,
pg_class r,
pg_attribute a
WHERE nr.oid = r.relnamespace AND r.oid = a.attrelid AND a.attnotnull AND a.attnum > 0 AND NOT a.attisdropped AND r.relkind = 'r'::"char" AND NOT pg_is_other_temp_schema(nr.oid)
/*--AND (pg_has_role(r.relowner, 'USAGE'::text) OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR has_any_column_privilege(r.oid, 'INSERT, UPDATE, REFERENCES'::text))*/
),
/*
-- CTE to replace information_schema.key_column_usage to remove owner limit
*/
kc AS (
SELECT current_database()::information_schema.sql_identifier AS constraint_catalog,
ss.nc_nspname::information_schema.sql_identifier AS constraint_schema,
ss.conname::information_schema.sql_identifier AS constraint_name,
current_database()::information_schema.sql_identifier AS table_catalog,
ss.nr_nspname::information_schema.sql_identifier AS table_schema,
ss.relname::information_schema.sql_identifier AS table_name,
a.attname::information_schema.sql_identifier AS column_name,
(ss.x).n::information_schema.cardinal_number AS ordinal_position,
CASE
WHEN ss.contype = 'f'::"char" THEN information_schema._pg_index_position(ss.conindid, ss.confkey[(ss.x).n])
ELSE NULL::integer
END::information_schema.cardinal_number AS position_in_unique_constraint
FROM pg_attribute a,
( SELECT r.oid AS roid,
r.relname,
r.relowner,
nc.nspname AS nc_nspname,
nr.nspname AS nr_nspname,
c.oid AS coid,
c.conname,
c.contype,
c.conindid,
c.confkey,
c.confrelid,
information_schema._pg_expandarray(c.conkey) AS x
FROM pg_namespace nr,
pg_class r,
pg_namespace nc,
pg_constraint c
WHERE nr.oid = r.relnamespace AND r.oid = c.conrelid AND nc.oid = c.connamespace AND (c.contype = ANY (ARRAY['p'::"char", 'u'::"char", 'f'::"char"])) AND r.relkind = 'r'::"char" AND NOT pg_is_other_temp_schema(nr.oid)) ss
WHERE ss.roid = a.attrelid AND a.attnum = (ss.x).x AND NOT a.attisdropped
/*--AND (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text))*/
)
SELECT
kc.table_schema,
kc.table_name,
kc.column_name
FROM
/*
--information_schema.table_constraints tc,
--information_schema.key_column_usage kc
*/
tc, 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')
|]
return $ mapMaybe (pkFromRow tabs) pks
pkFromRow :: [Table] -> (Schema, Text, Text) -> Maybe PrimaryKey
pkFromRow tabs (s, t, n) = PrimaryKey <$> table <*> pure n
where table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs
allSynonyms :: [Column] -> H.Tx P.Postgres s [(Column,Column)]
allSynonyms allCols = do
syns <- H.listEx $ [H.stmt|
WITH synonyms AS (
/*
-- CTE to replace the view from information_schema because the information in it depended on the logged in role
-- notice the commented line
*/
WITH view_column_usage AS (
SELECT DISTINCT
CAST(current_database() AS character varying) AS view_catalog,
CAST(nv.nspname AS character varying) AS view_schema,
CAST(v.relname AS character varying) AS view_name,
CAST(current_database() AS character varying) AS table_catalog,
CAST(nt.nspname AS character varying) AS table_schema,
CAST(t.relname AS character varying) AS table_name,
CAST(a.attname AS character varying) AS column_name
FROM pg_namespace nv, pg_class v, pg_depend dv,
pg_depend dt, pg_class t, pg_namespace nt,
pg_attribute a
WHERE nv.oid = v.relnamespace
AND v.relkind = 'v'
AND v.oid = dv.refobjid
AND dv.refclassid = 'pg_catalog.pg_class'::regclass
AND dv.classid = 'pg_catalog.pg_rewrite'::regclass
AND dv.deptype = 'i'
AND dv.objid = dt.objid
AND dv.refobjid <> dt.refobjid
AND dt.classid = 'pg_catalog.pg_rewrite'::regclass
AND dt.refclassid = 'pg_catalog.pg_class'::regclass
AND dt.refobjid = t.oid
AND t.relnamespace = nt.oid
AND t.relkind IN ('r', 'v', 'f')
AND t.oid = a.attrelid
AND dt.refobjsubid = a.attnum
/*--AND pg_has_role(t.relowner, 'USAGE')*/
)
SELECT
vcu.table_schema AS src_table_schema,
vcu.table_name AS src_table_name,
vcu.column_name AS src_column_name,
view.schemaname AS syn_table_schema,
view.viewname AS syn_table_name,
view.definition AS view_definition
FROM
pg_catalog.pg_views AS view,
view_column_usage AS vcu
WHERE
view.schemaname = vcu.view_schema AND
view.viewname = vcu.view_name AND
view.schemaname NOT IN ('pg_catalog', 'information_schema')
/*--AND (SELECT COUNT(*) FROM information_schema.view_table_usage WHERE view_schema = view.schemaname AND view_name = view.viewname) = 1*/
)
SELECT
src_table_schema, src_table_name, src_column_name,
syn_table_schema, syn_table_name,
(regexp_matches(view_definition, CONCAT('\.(', src_column_name, ')(?=,|$)'), 'gn'))[1] AS syn_column_name
FROM synonyms
UNION (
SELECT
src_table_schema, src_table_name, src_column_name,
syn_table_schema, syn_table_name,
(regexp_matches(view_definition, CONCAT('\.', src_column_name, '\sAS\s("?)(.+?)\1(,|$)'), 'gn'))[2] AS syn_column_name /* " <- for syntax highlighting */
FROM synonyms
)
|]
return $ mapMaybe (synonymFromRow allCols) syns
synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe (Column,Column)
synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
where
col1 = findCol s1 t1 c1
col2 = findCol s2 t2 c2
findCol s t c = find (\col -> (tableSchema . colTable) col == s && (tableName . colTable) col == t && colName col == c) allCols
+77
View File
@@ -0,0 +1,77 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
module PostgREST.Error (PgError, pgErrResponse, errResponse) where
import Data.Aeson ((.=))
import qualified Data.Aeson as JSON
import Data.String.Conversions (cs)
import Data.String.Utils (replace)
import Data.Text (Text)
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 :: HT.Status -> Text -> Response
errResponse status message = responseLBS status [(hContentType, "application/json")] (cs $ T.concat ["{\"message\":\"",message,"\"}"])
pgErrResponse :: PgError -> Response
pgErrResponse 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 @@
{-# LANGUAGE CPP #-}
module Main where
import PostgREST.App
import PostgREST.Config (AppConfig (..),
minimumPgVersion,
prettyVersion,
readOptions)
import PostgREST.DbStructure
import PostgREST.Error (PgError, pgErrResponse)
import PostgREST.Middleware
import Control.Monad (unless, void)
import Control.Monad.IO.Class (liftIO)
import Data.Aeson (encode)
import Data.Functor.Identity
import Data.Monoid ((<>))
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Time.Clock.POSIX (getPOSIXTime)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Network.Wai
import Network.Wai.Handler.Warp hiding (Connection)
import Network.Wai.Middleware.RequestLogger (logStdout)
import System.IO (BufferMode (..),
hSetBuffering, stderr,
stdin, stdout)
import Web.JWT (secret)
#ifndef mingw32_HOST_OS
import System.Posix.Signals
import Control.Concurrent (myThreadId)
import Control.Exception.Base (throwTo, AsyncException(..))
#endif
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
hasqlError :: PgError -> IO a
hasqlError = error . cs . encode
main :: IO ()
main = do
hSetBuffering stdout LineBuffering
hSetBuffering stdin LineBuffering
hSetBuffering stderr NoBuffering
conf <- readOptions
let port = configPort conf
unless (secret "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.StringSettings $ cs (configDatabase conf)
appSettings = setPort port
. setServerName (cs $ "postgrest/" <> prettyVersion)
$ defaultSettings
middle = logStdout . defaultMiddle
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 hasqlError
(\supported ->
unless supported $
error (
"Cannot run in this PostgreSQL version, PostgREST needs at least "
<> show minimumPgVersion)
) supportedOrError
#ifndef mingw32_HOST_OS
tid <- myThreadId
void $ installHandler keyboardSignal (Catch $ do
H.releasePool pool
throwTo tid UserInterrupt
) Nothing
#endif
let txSettings = Just (H.ReadCommitted, Just True)
dbOrError <- H.session pool $ H.tx txSettings $ getDbStructure (cs $ configSchema conf)
dbStructure <- either hasqlError return dbOrError
runSettings appSettings $ middle $ \ req respond -> do
time <- getPOSIXTime
body <- strictRequestBody req
resOrError <- liftIO $ H.session pool $ H.tx txSettings $
runWithClaims conf time (app dbStructure conf body) req
either (respond . pgErrResponse) respond resOrError
+69
View File
@@ -0,0 +1,69 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE ScopedTypeVariables #-}
module PostgREST.Middleware where
import Data.Maybe (fromMaybe)
import Data.Text
import Data.String.Conversions (cs)
import Data.Time.Clock (NominalDiffTime)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Network.HTTP.Types.Header (hAccept, hAuthorization)
import Network.HTTP.Types.Status (status415, status400)
import Network.Wai (Application, Request (..), Response,
requestHeaders)
import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Middleware.Gzip (def, gzip)
import Network.Wai.Middleware.Static (only, staticPolicy)
import PostgREST.ApiRequest (pickContentType)
import PostgREST.Auth (setRole, jwtClaims, claimsToSQL)
import PostgREST.Config (AppConfig (..), corsPolicy)
import PostgREST.Error (errResponse)
import Prelude hiding(concat)
import qualified Data.Vector as V
import qualified Hasql.Backend as B
import qualified Data.Map.Lazy as M
runWithClaims :: forall s. AppConfig -> NominalDiffTime ->
(Request -> H.Tx P.Postgres s Response) ->
Request -> H.Tx P.Postgres s Response
runWithClaims conf time app req = do
_ <- H.unitEx $ stmt setAnon
case split (== ' ') (cs auth) of
("Bearer" : tokenStr : _) ->
case jwtClaims jwtSecret tokenStr time of
Just claims ->
if M.member "role" claims
then do
mapM_ H.unitEx $ stmt <$> claimsToSQL claims
app req
else invalidJWT
_ -> invalidJWT
_ -> app req
where
stmt c = B.Stmt c V.empty True
hdrs = requestHeaders req
jwtSecret = configJwtSecret conf
auth = fromMaybe "" $ lookup hAuthorization hdrs
anon = cs $ configAnonRole conf
setAnon = setRole anon
invalidJWT = return $ errResponse status400 "Invalid JWT"
unsupportedAccept :: Application -> Application
unsupportedAccept app req respond =
case accept of
Left _ -> respond $ errResponse status415 "Unsupported Accept header, try: application/json"
Right _ -> app req respond
where accept = pickContentType $ lookup hAccept $ requestHeaders req
defaultMiddle :: Application -> Application
defaultMiddle =
gzip def
. cors corsPolicy
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
. unsupportedAccept
+112
View File
@@ -0,0 +1,112 @@
module PostgREST.Parsers
-- ( parseGetRequest
-- )
where
import Control.Applicative hiding ((<$>))
import Data.Monoid
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Tree
import PostgREST.Types
import Text.ParserCombinators.Parsec hiding (many, (<|>))
import PostgREST.QueryBuilder (operators)
pRequestSelect :: Text -> Parser ReadRequest
pRequestSelect rootNodeName = do
fieldTree <- pFieldForest
return $ foldr treeEntry (Node (Select [] [rootNodeName] [] Nothing, (rootNodeName, Nothing)) []) fieldTree
where
treeEntry :: Tree SelectItem -> ReadRequest -> ReadRequest
treeEntry (Node fld@((fn, _),_) fldForest) (Node (q, i) rForest) =
case fldForest of
[] -> Node (q {select=fld:select q}, i) rForest
_ -> Node (q, i) (foldr treeEntry (Node (Select [] [fn] [] Nothing, (fn, 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
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 pJsonPath
let pp = map cs p
jpp = map cs <$> jp
return (init pp, (last pp, jpp))
pFieldForest :: Parser [Tree SelectItem]
pFieldForest = pFieldTree `sepBy1` lexeme (char ',')
pFieldTree :: Parser (Tree SelectItem)
pFieldTree = try (Node <$> pSelect <*> between (char '{') (char '}') pFieldForest)
<|> 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_])")
pJsonPathStep :: Parser Text
pJsonPathStep = cs <$> try (string "->" *> pFieldName)
pJsonPath :: Parser [Text]
pJsonPath = (++) <$> many pJsonPathStep <*> ( (:[]) <$> (string "->>" *> pFieldName) )
pField :: Parser Field
pField = lexeme $ (,) <$> pFieldName <*> optionMaybe 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 <$> (pOp <?> "operator (eq, gt, ...)")
where pOp = foldl (<|>) empty $ map (try . string . cs . fst) operators
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" *> pure OrderAsc)
<|> (string "desc" *> pure OrderDesc)
nls <- optionMaybe (pDelimiter *> (
try(string "nullslast" *> pure OrderNullsLast)
<|> try(string "nullsfirst" *> pure OrderNullsFirst)
))
return $ OrderTerm c d nls
)
<|> OrderTerm <$> (cs <$> pFieldName) <*> pure OrderAsc <*> pure Nothing
+471
View File
@@ -0,0 +1,471 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TupleSections #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-|
Module : PostgREST.QueryBuilder
Description : PostgREST SQL generating functions.
This module provides functions to consume data types that
represent database objects (e.g. Relation, Schema, SqlQuery)
and produces SQL Statements.
Any function that outputs a SQL fragment should be in this module.
-}
module PostgREST.QueryBuilder (
addRelations
, addJoinConditions
, asJson
, callProc
, createReadStatement
, createWriteStatement
, operators
, pgFmtIdent
, pgFmtLit
, requestToQuery
, requestToCountQuery
, sourceCTEName
, unquoted
) where
import qualified Hasql as H
import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P
import qualified Data.Aeson as JSON
import PostgREST.RangeQuery (NonnegRange, rangeLimit, rangeOffset)
import Control.Error (note, fromMaybe, mapMaybe)
import qualified Data.HashMap.Strict as HM
import Data.List (find, (\\))
import Data.Monoid ((<>))
import Data.Text (Text, intercalate, unwords, replace, isInfixOf, toLower, split)
import qualified Data.Text as T (map, takeWhile)
import Data.String.Conversions (cs)
import Control.Applicative (empty, (<|>))
import Control.Monad (join)
import Data.Tree (Tree(..))
import qualified Data.Vector as V
import PostgREST.Types
import qualified Data.Map as M
import Text.Regex.TDFA ((=~))
import qualified Data.ByteString.Char8 as BS
import Data.Scientific ( FPFormat (..)
, formatScientific
, isInteger
)
import Prelude hiding (unwords)
import PostgREST.ApiRequest (PreferRepresentation (..))
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
createReadStatement :: SqlQuery -> SqlQuery -> NonnegRange -> Bool -> Bool -> Bool -> B.Stmt P.Postgres
createReadStatement selectQuery countQuery range isSingle countTotal asCsv =
B.Stmt (
"WITH " <> sourceCTEName <> " AS (" <> selectQuery <> ") " <>
"SELECT " <> intercalate ", " [
countResultF <> " AS total_result_set",
"pg_catalog.count(t) AS page_total",
"null AS header",
bodyF <> " AS body"
] <>
" FROM ( SELECT * FROM " <> sourceCTEName <> " " <> limitF range <> ") t"
) V.empty True
where
countResultF = if countTotal then "("<>countQuery<>")" else "null"
bodyF
| asCsv = asCsvF
| isSingle = asJsonSingleF
| otherwise = asJsonF
createWriteStatement :: QualifiedIdentifier -> SqlQuery -> SqlQuery -> Bool -> PreferRepresentation ->
[Text] -> Bool -> Payload -> B.Stmt P.Postgres
createWriteStatement _ _ _ _ _ _ _ (PayloadParseError _) = undefined
createWriteStatement _ _ mutateQuery _ None
_ _ (PayloadJSON (UniformObjects rows)) =
B.Stmt (
"WITH " <> sourceCTEName <> " AS (" <> mutateQuery <> ") " <>
"SELECT null, 0, null, null"
) (V.singleton . B.encodeValue . JSON.Array . V.map JSON.Object $ rows) True
createWriteStatement qi _ mutateQuery isSingle HeadersOnly
pKeys _ (PayloadJSON (UniformObjects rows)) =
B.Stmt (
"WITH " <> sourceCTEName <> " AS (" <> mutateQuery <> " RETURNING " <> fromQi qi <> ".*" <> ") " <>
"SELECT " <> intercalate ", " [
"null AS total_result_set",
"pg_catalog.count(t) AS page_total",
if isSingle then locationF pKeys else "null",
"null"
] <>
" FROM (SELECT 1 FROM " <> sourceCTEName <> ") t"
) (V.singleton . B.encodeValue . JSON.Array . V.map JSON.Object $ rows) True
createWriteStatement qi selectQuery mutateQuery isSingle Full
pKeys asCsv (PayloadJSON (UniformObjects rows)) =
B.Stmt (
"WITH " <> sourceCTEName <> " AS (" <> mutateQuery <> " RETURNING " <> fromQi qi <> ".*" <> ") " <>
"SELECT " <> intercalate ", " [
"null AS total_result_set", -- when updateing it does not make sense
"pg_catalog.count(t) AS page_total",
if isSingle then locationF pKeys else "null" <> " AS header",
bodyF <> " AS body"
] <>
" FROM ( "<>selectQuery<>") t"
) (V.singleton . B.encodeValue . JSON.Array . V.map JSON.Object $ rows) True
where
bodyF
| asCsv = asCsvF
| isSingle = asJsonSingleF
| otherwise = asJsonF
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either Text ReadRequest
addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _)) forest) =
case parentNode of
(Just (Node (Select{from=[parentTable]}, (_, _)) _)) -> Node <$> (addRel readNode <$> rel) <*> updatedForest
where
rel = note ("no relation between " <> parentTable <> " and " <> name)
$ findRelationByTable schema name parentTable
<|> findRelationByTable schema parentTable name
<|> findRelationByColumn schema parentTable name
addRel :: (ReadQuery, (NodeName, Maybe Relation)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation))
addRel (q, (n, _)) r = (q {from=fromRelation}, (n, Just r))
where fromRelation = map (\t -> if t == n then tableName (relTable r) else t) (from q)
_ -> Node (query, (name, Nothing)) <$> updatedForest
where
updatedForest = mapM (addRelations schema allRelations (Just node)) forest
-- Searches through all the relations and returns a match given the parameter conditions.
-- Will only find a relation where both schemas are in the PostgREST schema.
-- `findRelationByColumn` also does a ducktype check to see if the column name has any variation of `id` or `fk`. If so then the relation is returned as a match.
findRelationByTable s t1 t2 =
find (\r -> s == tableSchema (relTable r) && s == tableSchema (relFTable r) && t1 == tableName (relTable r) && t2 == tableName (relFTable r)) allRelations
findRelationByColumn s t c =
find (\r -> s == tableSchema (relTable r) && s == tableSchema (relFTable r) && t == tableName (relFTable r) && length (relFColumns r) == 1 && c `colMatches` (colName . head . relFColumns) r) allRelations
where n `colMatches` rc = (cs ("^" <> rc <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~ (cs n :: BS.ByteString)
addJoinConditions :: Schema -> ReadRequest -> Either Text ReadRequest
addJoinConditions schema (Node (query, (n, r)) forest) =
case r of
Nothing -> Node (updatedQuery, (n,r)) <$> updatedForest -- this is the root node
Just rel@(Relation{relType=Child}) -> Node (addCond updatedQuery (getJoinConditions rel),(n,r)) <$> updatedForest
Just (Relation{relType=Parent}) -> Node (updatedQuery, (n,r)) <$> updatedForest
Just rel@(Relation{relType=Many, relLTable=(Just linkTable)}) ->
Node (qq, (n, r)) <$> updatedForest
where
q = addCond updatedQuery (getJoinConditions rel)
qq = q{from=tableName linkTable : from q}
_ -> Left "unknown relation"
where
-- add parentTable and parentJoinConditions to the query
updatedQuery = foldr (flip addCond) query parentJoinConditions
where
parentJoinConditions = map (getJoinConditions . snd) parents
parents = mapMaybe (getParents . rootLabel) forest
getParents (_, (tbl, Just rel@(Relation{relType=Parent}))) = Just (tbl, rel)
getParents _ = Nothing
updatedForest = mapM (addJoinConditions schema) forest
addCond q con = q{flt_=con ++ flt_ q}
asJson :: StatementT
asJson s = s {
B.stmtTemplate =
"array_to_json(coalesce(array_agg(row_to_json(t)), '{}'))::character varying from ("
<> B.stmtTemplate s <> ") t" }
callProc :: QualifiedIdentifier -> JSON.Object -> PStmt
callProc qi params = do
let args = intercalate "," $ map assignment (HM.toList params)
B.Stmt ("select * from " <> fromQi qi <> "(" <> args <> ")") empty True
where
assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v
operators :: [(Text, SqlFragment)]
operators = [
("eq", "="),
("gte", ">="), -- has to be before gt (parsers)
("gt", ">"),
("lte", "<="), -- has to be before lt (parsers)
("lt", "<"),
("neq", "<>"),
("like", "like"),
("ilike", "ilike"),
("in", "in"),
("notin", "not in"),
("isnot", "is not"), -- has to be before is (parsers)
("is", "is"),
("@@", "@@"),
("@>", "@>"),
("<@", "<@")
]
pgFmtIdent :: SqlFragment -> SqlFragment
pgFmtIdent x = "\"" <> replace "\"" "\"\"" (trimNullChars $ cs x) <> "\""
pgFmtLit :: SqlFragment -> SqlFragment
pgFmtLit x =
let trimmed = trimNullChars x
escaped = "'" <> replace "'" "''" trimmed <> "'"
slashed = replace "\\" "\\\\" escaped in
if "\\\\" `isInfixOf` escaped
then "E" <> slashed
else slashed
requestToCountQuery :: Schema -> DbRequest -> SqlQuery
requestToCountQuery _ (DbMutate _) = undefined
requestToCountQuery schema (DbRead (Node (Select _ _ conditions _, (mainTbl, _)) _)) =
unwords [
"SELECT pg_catalog.count(1)",
"FROM ", fromQi $ QualifiedIdentifier schema mainTbl,
("WHERE " <> intercalate " AND " ( map (pgFmtCondition (QualifiedIdentifier schema mainTbl)) localConditions )) `emptyOnNull` localConditions
]
where
fn (Filter{value=VText _}) = True
fn (Filter{value=VForeignKey _ _}) = False
localConditions = filter fn conditions
requestToQuery :: Schema -> DbRequest -> SqlQuery
requestToQuery _ (DbMutate (Insert _ (PayloadParseError _))) = undefined
requestToQuery _ (DbMutate (Update _ (PayloadParseError _) _)) = undefined
requestToQuery schema (DbRead (Node (Select colSelects tbls conditions ord, (nodeName, maybeRelation)) forest)) =
query
where
-- TODO! the folloing helper functions are just to remove the "schema" part when the table is "source" which is the name
-- of our WITH query part
mainTbl = fromMaybe nodeName (tableName . relTable <$> maybeRelation)
tblSchema tbl = if tbl == sourceCTEName then "" else schema
qi = QualifiedIdentifier (tblSchema mainTbl) mainTbl
toQi t = QualifiedIdentifier (tblSchema t) t
query = unwords [
"SELECT ", intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects),
"FROM ", intercalate ", " (map (fromQi . toQi) tbls),
unwords (map joinStr joins),
("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi ) localConditions )) `emptyOnNull` localConditions,
orderF (fromMaybe [] ord)
]
orderF ts =
if null ts
then ""
else "ORDER BY " <> clause
where
clause = intercalate "," (map queryTerm ts)
queryTerm :: OrderTerm -> Text
queryTerm t = " "
<> cs (pgFmtColumn qi $ otTerm t) <> " "
<> (cs.show) (otDirection t) <> " "
<> maybe "" (cs.show) (otNullOrder t) <> " "
(joins, selects) = foldr getQueryParts ([],[]) forest
parentTables = map snd joins
parentConditions = join $ map (( `filter` conditions ) . filterParentConditions) parentTables
localConditions = conditions \\ parentConditions
joinStr :: (SqlFragment, TableName) -> SqlFragment
joinStr (sql, t) = "LEFT OUTER JOIN " <> sql <> " ON " <>
intercalate " AND " ( map (pgFmtCondition qi ) joinConditions )
where
joinConditions = filter (filterParentConditions t) conditions
filterParentConditions parentTable (Filter _ _ (VForeignKey (QualifiedIdentifier "" t) _)) = parentTable == t
filterParentConditions _ _ = False
getQueryParts :: Tree ReadNode -> ([(SqlFragment, TableName)], [SqlFragment]) -> ([(SqlFragment,TableName)], [SqlFragment])
getQueryParts (Node n@(_, (name, Just (Relation {relType=Child,relTable=Table{tableName=table}}))) forst) (j,s) = (j,sel:s)
where
sel = "COALESCE(("
<> "SELECT array_to_json(array_agg(row_to_json("<>table<>"))) "
<> "FROM (" <> subquery <> ") " <> table
<> "), '[]') AS " <> pgFmtIdent name
where subquery = requestToQuery schema (DbRead (Node n forst))
getQueryParts (Node n@(_, (name, Just (Relation {relType=Parent,relTable=Table{tableName=table}}))) forst) (j,s) = (joi:j,sel:s)
where
sel = "row_to_json(" <> table <> ".*) AS "<>pgFmtIdent name --TODO must be singular
joi = ("( " <> subquery <> " ) AS " <> table, table)
where subquery = requestToQuery schema (DbRead (Node n forst))
getQueryParts (Node n@(_, (name, Just (Relation {relType=Many,relTable=Table{tableName=table}}))) forst) (j,s) = (j,sel:s)
where
sel = "COALESCE (("
<> "SELECT array_to_json(array_agg(row_to_json("<>table<>"))) "
<> "FROM (" <> subquery <> ") " <> table
<> "), '[]') AS " <> pgFmtIdent name
where subquery = requestToQuery schema (DbRead (Node n 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 (_,(_,Nothing)) _) _ = undefined
requestToQuery schema (DbMutate (Insert mainTbl (PayloadJSON (UniformObjects rows)))) =
let qi = QualifiedIdentifier schema mainTbl
cols = map pgFmtIdent $ fromMaybe [] (HM.keys <$> (rows V.!? 0))
colsString = intercalate ", " cols in
unwords [
"INSERT INTO ", fromQi qi,
" (" <> colsString <> ")" <>
" SELECT " <> colsString <>
" FROM json_populate_recordset(null::" , fromQi qi, ", ?)"
]
requestToQuery schema (DbMutate (Update mainTbl (PayloadJSON (UniformObjects rows)) conditions)) =
case rows V.!? 0 of
Just obj ->
let assignments = map
(\(k,v) -> pgFmtIdent k <> "=" <> insertableValue v) $ HM.toList obj in
unwords [
"UPDATE ", fromQi qi,
" SET " <> intercalate "," assignments <> " ",
("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi ) conditions )) `emptyOnNull` conditions
]
Nothing -> undefined
where
qi = QualifiedIdentifier schema mainTbl
requestToQuery schema (DbMutate (Delete mainTbl conditions)) =
query
where
qi = QualifiedIdentifier schema mainTbl
query = unwords [
"DELETE FROM ", fromQi qi,
("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi ) conditions )) `emptyOnNull` conditions
]
sourceCTEName :: SqlFragment
sourceCTEName = "pg_source"
unquoted :: JSON.Value -> 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
-- private functions
asCsvF :: SqlFragment
asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
where
asCsvHeaderF =
"(SELECT string_agg(a.k, ',')" <>
" FROM (" <>
" SELECT json_object_keys(r)::TEXT as k" <>
" FROM ( " <>
" SELECT row_to_json(hh) as r from " <> sourceCTEName <> " as hh limit 1" <>
" ) s" <>
" ) a" <>
")"
asCsvBodyF = "coalesce(string_agg(substring(t::text, 2, length(t::text) - 2), '\n'), '')"
asJsonF :: SqlFragment
asJsonF = "array_to_json(array_agg(row_to_json(t)))::character varying"
asJsonSingleF :: SqlFragment --TODO! unsafe when the query actually returns multiple rows, used only on inserting and returning single element
asJsonSingleF = "string_agg(row_to_json(t)::text, ',')::character varying "
locationF :: [Text] -> SqlFragment
locationF pKeys =
"(" <>
" WITH s AS (SELECT row_to_json(ss) as r from " <> sourceCTEName <> " as ss limit 1)" <>
" SELECT string_agg(json_data.key || '=' || coalesce( 'eq.' || json_data.value, 'is.null'), '&')" <>
" FROM s, json_each_text(s.r) AS json_data" <>
(
if null pKeys
then ""
else " WHERE json_data.key IN ('" <> intercalate "','" pKeys <> "')"
) <>
")"
limitF :: NonnegRange -> SqlFragment
limitF r = "LIMIT " <> limit <> " OFFSET " <> offset
where
limit = maybe "ALL" (cs . show) $ rangeLimit r
offset = cs . show $ rangeOffset r
fromQi :: QualifiedIdentifier -> SqlFragment
fromQi t = (if s == "" then "" else pgFmtIdent s <> ".") <> pgFmtIdent n
where
n = qiName t
s = qiSchema t
getJoinConditions :: Relation -> [Filter]
getJoinConditions (Relation t cols ft fcs typ lt lc1 lc2) =
case typ of
Child -> zipWith (toFilter tN ftN) cols fcs
Parent -> zipWith (toFilter tN ftN) cols fcs
Many -> zipWith (toFilter tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toFilter ftN ltN) fcs (fromMaybe [] lc2)
where
s = if typ == Parent then "" else tableSchema t
tN = tableName t
ftN = tableName ft
ltN = fromMaybe "" (tableName <$> lt)
toFilter :: Text -> Text -> Column -> Column -> Filter
toFilter tb ftb c fc = Filter (colName c, Nothing) "=" (VForeignKey (QualifiedIdentifier s tb) (ForeignKey fc{colTable=(colTable fc){tableName=ftb}}))
emptyOnNull :: Text -> [a] -> Text
emptyOnNull val x = if null x then "" else val
insertableValue :: JSON.Value -> SqlFragment
insertableValue JSON.Null = "null"
insertableValue v = (<> "::unknown") . pgFmtLit $ unquoted v
whiteList :: Text -> SqlFragment
whiteList val = fromMaybe
(cs (pgFmtLit val) <> "::unknown ")
(find ((==) . toLower $ val) ["null","true","false"])
pgFmtColumn :: QualifiedIdentifier -> Text -> SqlFragment
pgFmtColumn table "*" = fromQi table <> ".*"
pgFmtColumn table c = fromQi table <> "." <> pgFmtIdent c
pgFmtField :: QualifiedIdentifier -> Field -> SqlFragment
pgFmtField table (c, jp) = pgFmtColumn table c <> pgFmtJsonPath jp
pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> SqlFragment
pgFmtSelectItem table (f@(_, jp), Nothing) = pgFmtField table f <> pgFmtAsJsonPath jp
pgFmtSelectItem table (f@(_, jp), Just cast ) = "CAST (" <> pgFmtField table f <> " AS " <> cast <> " )" <> pgFmtAsJsonPath jp
pgFmtCondition :: QualifiedIdentifier -> Filter -> SqlFragment
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 Column{colTable=Table{tableName=ft}, colName=fc}) -> pgFmtColumn qi fc
where qi = QualifiedIdentifier (if ft == sourceCTEName then "" else s) ft
_ -> ""
pgFmtValue :: Text -> Text -> SqlFragment
pgFmtValue opCode val =
case opCode of
"like" -> unknownLiteral $ T.map star val
"ilike" -> unknownLiteral $ T.map star val
"in" -> "(" <> intercalate ", " (map unknownLiteral $ split (==',') val) <> ") "
"notin" -> "(" <> intercalate ", " (map unknownLiteral $ split (==',') val) <> ") "
"@@" -> "to_tsquery(" <> unknownLiteral val <> ") "
_ -> unknownLiteral val
where
star c = if c == '*' then '%' else c
unknownLiteral = (<> "::unknown ") . pgFmtLit
pgFmtOperator :: Text -> SqlFragment
pgFmtOperator opCode = fromMaybe "=" $ M.lookup opCode operatorsMap
where
operatorsMap = M.fromList operators
pgFmtJsonPath :: Maybe JsonPath -> SqlFragment
pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x
pgFmtJsonPath (Just (x:xs)) = "->" <> pgFmtLit x <> pgFmtJsonPath ( Just xs )
pgFmtJsonPath _ = ""
pgFmtAsJsonPath :: Maybe JsonPath -> SqlFragment
pgFmtAsJsonPath Nothing = ""
pgFmtAsJsonPath (Just xx) = " AS " <> last xx
trimNullChars :: Text -> Text
trimNullChars = T.takeWhile (/= '\x0')
+68
View File
@@ -0,0 +1,68 @@
module PostgREST.RangeQuery (
rangeParse
, rangeRequested
, rangeLimit
, rangeOffset
, restrictRange
, 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 -> NonnegRange
rangeParse range = do
let rangeRegex = "^([0-9]+)-([0-9]*)$" :: BS.ByteString
case listToMaybe (range =~ rangeRegex :: [[BS.ByteString]]) of
Just parsedRange ->
let [_, from, to] = readMaybe . cs <$> parsedRange
lower = fromMaybe emptyRange (rangeGeq <$> from)
upper = fromMaybe (rangeGeq 0) (rangeLeq <$> to) in
rangeIntersection lower upper
Nothing -> rangeGeq 0
rangeRequested :: RequestHeaders -> NonnegRange
rangeRequested = rangeParse . fromMaybe "" . lookup hRange
restrictRange :: Maybe Int -> NonnegRange -> NonnegRange
restrictRange Nothing r = r
restrictRange (Just limit) r =
rangeIntersection r $
Range BoundaryBelowAll (BoundaryAbove $ rangeOffset r + limit - 1)
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)
+156
View File
@@ -0,0 +1,156 @@
module PostgREST.Types where
import Data.Text
import Data.Tree
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString as BS
import qualified Data.Vector as V
import Data.Aeson
data DbStructure = DbStructure {
dbTables :: [Table]
, dbColumns :: [Column]
, dbRelations :: [Relation]
, dbPrimaryKeys :: [PrimaryKey]
} deriving (Show, Eq)
type Schema = Text
type TableName = Text
type SqlQuery = Text
type SqlFragment = Text
type RequestBody = BL.ByteString
data Table = Table {
tableSchema :: Schema
, tableName :: TableName
, tableInsertable :: Bool
} deriving (Show, Ord)
data ForeignKey = ForeignKey { fkCol :: Column } deriving (Show, Eq, Ord)
data Column =
Column {
colTable :: Table
, 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 { colTable :: Table }
deriving (Show, Ord)
type Synonym = (Column,Column)
data PrimaryKey = PrimaryKey {
pkTable :: Table
, pkName :: Text
} deriving (Show, Eq)
data OrderDirection = OrderAsc | OrderDesc deriving (Eq)
instance Show OrderDirection where
show OrderAsc = "asc"
show OrderDesc = "desc"
data OrderNulls = OrderNullsFirst | OrderNullsLast deriving (Eq)
instance Show OrderNulls where
show OrderNullsFirst = "nulls first"
show OrderNullsLast = "nulls last"
data OrderTerm = OrderTerm {
otTerm :: Text
, otDirection :: OrderDirection
, otNullOrder :: Maybe OrderNulls
} deriving (Show, Eq)
data QualifiedIdentifier = QualifiedIdentifier {
qiSchema :: Schema
, qiName :: TableName
} deriving (Show, Eq)
data RelationType = Child | Parent | Many deriving (Show, Eq)
data Relation = Relation {
relTable :: Table
, relColumns :: [Column]
, relFTable :: Table
, relFColumns :: [Column]
, relType :: RelationType
, relLTable :: Maybe Table
, relLCols1 :: Maybe [Column]
, relLCols2 :: Maybe [Column]
} deriving (Show, Eq)
-- | An array of JSON objects that has been verified to have
-- the same keys in every object
newtype UniformObjects = UniformObjects (V.Vector Object)
deriving (Show, Eq)
-- | When Hasql supports the COPY command then we can
-- have a special payload just for CSV, but until
-- then CSV is converted to a JSON array.
data Payload = PayloadJSON UniformObjects
| PayloadParseError BS.ByteString
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 NodeName = Text
type SelectItem = (Field, Maybe Cast)
type Path = [Text]
data ReadQuery = Select { select::[SelectItem], from::[TableName], flt_::[Filter], order::Maybe [OrderTerm] } deriving (Show, Eq)
data MutateQuery = Insert { in_::TableName, qPayload::Payload }
| Delete { in_::TableName, where_::[Filter] }
| Update { in_::TableName, qPayload::Payload, where_::[Filter] } deriving (Show, Eq)
data Filter = Filter {field::Field, operator::Operator, value::FValue} deriving (Show, Eq)
type ReadNode = (ReadQuery, (NodeName, Maybe Relation))
type ReadRequest = Tree ReadNode
type MutateRequest = MutateQuery
data DbRequest = DbRead ReadRequest | DbMutate MutateRequest
instance ToJSON Column where
toJSON c = object [
"schema" .= tableSchema t
, "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 ]
where
t = colTable c
instance ToJSON ForeignKey where
toJSON fk = object [
"schema" .= tableSchema t
, "table" .= tableName t
, "column" .= colName c ]
where
c = fkCol fk
t = colTable c
instance ToJSON Table where
toJSON v = object [
"schema" .= tableSchema v
, "name" .= tableName v
, "insertable" .= tableInsertable v ]
instance Eq Table where
Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2
instance Eq Column where
Column{colTable=t1,colName=n1} == Column{colTable=t2,colName=n2} = t1 == t2 && n1 == n2
_ == _ = False
-58
View File
@@ -1,58 +0,0 @@
module RangeQuery (
rangeParse
, rangeRequested
, rangeLimit
, rangeOffset
, NonnegRange
) where
import Control.Applicative
import Network.HTTP.Types.Header
import qualified Data.ByteString.Char8 as BS
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
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)
-57
View File
@@ -1,57 +0,0 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Types where
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: nightly-2015-10-27
+61 -14
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE QuasiQuotes #-}
module Feature.AuthSpec where
-- {{{ Imports
@@ -7,19 +6,67 @@ import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
-- }}}
spec :: Spec
spec = around withApp $ describe "authorization" $ do
it "hides tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` 404
it "indicates login failure" $ do
let auth = authHeader "postgrest_test_author" "fakefake"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 401
it "allows users with permissions to see their tables" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeader "jdoe" "1234"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = around (withApp cfgDefault struct pool)
$ describe "authorization" $ do
it "hides tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` 404
it "returns jwt functions as jwt tokens" $
post "/rpc/login" [json| { "id": "jdoe", "pass": "1234" } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"} |]
, matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/json"]
}
it "allows users with permissions to see their tables" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "works with tokens which have extra fields" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIiwia2V5MSI6InZhbHVlMSIsImtleTIiOiJ2YWx1ZTIiLCJrZXkzIjoidmFsdWUzIiwiYSI6MSwiYiI6MiwiYyI6M30.GfydCh-F4wnM379xs0n1zUgalwJIsb6YoBapCo8HlFk"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
-- this test will stop working 9999999999s after the UNIX EPOCH
it "succeeds with an unexpired token" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.QaPPLWTuyydMu_q7H4noMT7Lk6P4muet1OpJXF6ofhc"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "fails with an expired token" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.enk_qZ_u6gZsXY4R8bREKB_HNExRpM0lIWSLktk9JJQ"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400
it "hides tables from users with invalid JWT" $ do
let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400
it "should fail when jwt contains no claims" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.MKYc_lOECtB0LJOiykilAdlHodB-I0_id2qHKq35dmc"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400
it "hides tables from users with JWT that contain no claims about role" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Impkb2UifQ.zyohGMnrDy4_8eJTl6I2AUXO3MeCCiwR24aGWRkTE9o"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400
it "recovers after 400 error with logged in user" $ do
_ <- post "/authors_only" [json| { "owner": "jdoe", "secret": "test content" } |]
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
_ <- request methodPost "/rpc/problem" [auth] ""
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
+16 -4
View File
@@ -6,13 +6,17 @@ import Test.Hspec.Wai
import Network.Wai.Test (SResponse(simpleHeaders, simpleBody))
import qualified Data.ByteString.Lazy as BL
import Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
import Network.HTTP.Types
-- }}}
spec :: Spec
spec = around withApp $ describe "CORS" $ do
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = around (withApp cfgDefault struct pool) $ describe "CORS" $ do
let preflightHeaders = [
("Accept", "*/*"),
("Origin", "http://example.com"),
@@ -22,7 +26,7 @@ spec = around withApp $ describe "CORS" $ do
("Host", "localhost:3000"),
("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0"),
("Origin", "http://localhost:8000"),
("Accept", "text/plain, */*; q=0.01"),
("Accept", "text/csv, */*; q=0.01"),
("Accept-Language", "en-US,en;q=0.5"),
("Accept-Encoding", "gzip, deflate"),
("Referer", "http://localhost:8000/"),
@@ -41,7 +45,7 @@ spec = around withApp $ describe "CORS" $ do
"true"
respHeaders `shouldSatisfy` matchHeader
"Access-Control-Allow-Methods"
"GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD"
"GET, POST, PATCH, DELETE, OPTIONS, HEAD"
respHeaders `shouldSatisfy` matchHeader
"Access-Control-Allow-Headers"
"Authentication, Foo, Bar, Accept, Accept-Language, Content-Language"
@@ -53,6 +57,14 @@ spec = around withApp $ describe "CORS" $ do
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 ""
+10 -4
View File
@@ -2,13 +2,19 @@ module Feature.DeleteSpec where
import Test.Hspec
import Test.Hspec.Wai
import Text.Heredoc
import Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
import Network.HTTP.Types
spec :: Spec
spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items")
. around withApp $
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = beforeAll resetDb
. around (withApp cfgDefault struct pool) $
describe "Deleting" $ do
context "existing record" $ do
it "succeeds with 204 and deletion count" $
@@ -23,7 +29,7 @@ spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable
_ <- request methodDelete "/items?id=lt.15" [] ""
get "/items"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":15}]"
matchBody = Just [str|[{"id":15}]|]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/1"]
}
+234 -32
View File
@@ -1,36 +1,60 @@
{-# LANGUAGE QuasiQuotes #-}
module Feature.InsertSpec where
import Test.Hspec
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.Wai.Test (SResponse(simpleBody,simpleHeaders,simpleStatus))
import Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
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(..), CompoundPK(..))
--import Debug.Trace
spec :: Spec
spec = around withApp $ do
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = beforeAll_ resetDb $ around (withApp cfgDefault struct pool) $ do
describe "Posting new record" $ do
after_ (clearTable "menagerie") . it "accepts disparate json types" $ do
p <- post "/menagerie"
[json| {
"integer": 13, "double": 3.14159, "varchar": "testing!"
, "boolean": false, "date": "1900-01-01", "money": "$3.99"
, "enum": "foo"
} |]
liftIO $ do
simpleBody p `shouldBe` ""
simpleStatus p `shouldBe` created201
after_ (clearTable "menagerie") . context "disparate csv types" $ do
it "accepts disparate json types" $ do
p <- post "/menagerie"
[json| {
"integer": 13, "double": 3.14159, "varchar": "testing!"
, "boolean": false, "date": "1900-01-01", "money": "$3.99"
, "enum": "foo"
} |]
liftIO $ do
simpleBody p `shouldBe` ""
simpleStatus p `shouldBe` created201
it "filters columns in result using &select" $
request methodPost "/menagerie?select=integer,varchar" [("Prefer", "return=representation")]
[json| {
"integer": 14, "double": 3.14159, "varchar": "testing!"
, "boolean": false, "date": "1900-01-01", "money": "$3.99"
, "enum": "foo"
} |] `shouldRespondWith` ResponseMatcher {
matchBody = Just [str|{"integer":14,"varchar":"testing!"}|]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json"]
}
it "includes related data after insert" $
request methodPost "/projects?select=id,name,clients{id,name}" [("Prefer", "return=representation")]
[str|{"id":6,"name":"New Project","client_id":2}|] `shouldRespondWith` ResponseMatcher {
matchBody = Just [str|{"id":6,"name":"New Project","clients":{"id":2,"name":"Apple"}}|]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json", "Location" <:> "/projects?id=eq.6"]
}
context "with no pk supplied" $ do
context "into a table with auto-incrementing pk" . after_ (clearTable "auto_incrementing_pk") $
@@ -52,7 +76,7 @@ spec = around withApp $ do
post "/simple_pk" [json| { "extra":"foo"} |]
`shouldRespondWith` 400
context "into a table with no pk" . after_ (clearTable "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
@@ -60,6 +84,33 @@ spec = around withApp $ do
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.foo&b=eq.bar"
simpleStatus p `shouldBe` created201
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 insert in tables with no select privileges" $ do
p <- request methodPost "/insertonly"
[("Prefer", "return=minimal")]
[json| { "v":"some value" } |]
liftIO $ do
simpleBody p `shouldBe` ""
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 } |]
@@ -71,24 +122,126 @@ spec = around withApp $ 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" } } |]
request methodPost "/json"
[("Prefer", "return=representation")]
inserted
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"message":"Failed to parse JSON payload. Failed reading: satisfy"} |]
, matchStatus = 400
, matchHeaders = []
matchBody = Just inserted
, matchStatus = 201
, matchHeaders = ["Location" <:> [str|/json?data=eq.{"foo":"bar"}|]]
}
-- TODO! the test above seems right, why was the one below working before and not now
-- 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] } |]
request methodPost "/json"
[("Prefer", "return=representation")]
inserted
`shouldRespondWith` ResponseMatcher {
matchBody = Just inserted
, matchStatus = 201
, matchHeaders = ["Location" <:> [str|/json?data=eq.[1,2,3]|]]
}
-- TODO! the test above seems right, why was the one below working before and not now
-- 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
pendingWith "Decide on what to do with CSV insert"
let inserted = [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
|]
request methodPost "/menagerie" [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")] inserted
`shouldRespondWith` ResponseMatcher {
matchBody = Just inserted
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv"]
}
-- 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"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
"a,b\nbar,baz"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "a,b\nbar,baz"
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv",
"Location" <:> "/no_pk?a=eq.bar&b=eq.baz"]
}
-- it "can post nulls (old way)" $ do
-- pendingWith "changed the response when in csv mode"
-- 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"]
-- }
it "can post nulls" $
request methodPost "/no_pk"
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
"a,b\nNULL,foo"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "a,b\n,foo"
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv",
"Location" <:> "/no_pk?a=is.null&b=eq.foo"]
}
after_ (clearTable "no_pk") . context "with wrong number of columns" $
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 does not fail because the extra columns are ignored
-- 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" $
it "gives a 404" $
it "gives a 404" $ do
pendingWith "Decide on PUT usefullness"
request methodPut "/fake" []
[json| { "real": false } |]
`shouldRespondWith` 404
context "to a known uri" $ do
context "without a fully-specified primary key" $
it "is not an allowed operation" $
it "is not an allowed operation" $ do
pendingWith "Decide on PUT usefullness"
request methodPut "/compound_pk?k1=eq.12" []
[json| { "k1":12, "k2":42 } |]
`shouldRespondWith` 405
@@ -96,13 +249,15 @@ spec = around withApp $ do
context "with a fully-specified primary key" $ do
context "not specifying every column in the table" $
it "is rejected for lack of idempotence" $
it "is rejected for lack of idempotence" $ do
pendingWith "Decide on PUT usefullness"
request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42 } |]
`shouldRespondWith` 400
context "specifying every column in the table" . after_ (clearTable "compound_pk") $ do
it "can create a new record" $ do
pendingWith "Decide on PUT usefullness"
p <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":3 } |]
liftIO $ do
@@ -119,6 +274,7 @@ spec = around withApp $ do
compoundExtra record `shouldBe` Just 3
it "can update an existing record" $ do
pendingWith "Decide on PUT usefullness"
_ <- 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" []
@@ -133,7 +289,8 @@ spec = around withApp $ do
context "with an auto-incrementing primary key" . after_ (clearTable "auto_incrementing_pk") $
it "succeeds with 204" $
it "succeeds with 204" $ do
pendingWith "Decide on PUT usefullness"
request methodPut "/auto_incrementing_pk?id=eq.1" []
[json| {
"id":1,
@@ -156,20 +313,23 @@ spec = around withApp $ do
`shouldRespondWith` 404
context "on an empty table" $
it "succeeds with no effect" $
request methodPatch "/simple_pk" []
it "indicates no records found to update" $
request methodPatch "/empty_table" []
[json| { "extra":20 } |]
`shouldRespondWith` 204
`shouldRespondWith` 404
context "in a nonempty table" . before_ (clearTable "items" >> createItems 15) .
after_ (clearTable "items") $ do
context "in a nonempty table" $ 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" []
request methodPatch "/items?id=eq.2" []
[json| { "id":42 } |]
`shouldRespondWith` 204
`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"
@@ -185,3 +345,45 @@ spec = around withApp $ do
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
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
_ <- 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"
[ auth, ("Prefer", "return=representation") ]
[json| { "secret": "nyancat" } |]
liftIO $ do
simpleBody p1 `shouldBe` [str|{"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` [str|{"owner":"jroe","secret":"lolcat"}|]
simpleStatus p2 `shouldBe` created201
+34
View File
@@ -0,0 +1,34 @@
module Feature.QueryLimitedSpec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders, simpleStatus))
import Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool =
beforeAll resetDb
. around (withApp (cfgLimitRows 3) struct pool) $
describe "Requesting many items with server limits enabled" $ do
it "restricts results" $
get "/items"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":2},{"id":3}] |]
, matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-2/15"]
}
it "respects additional client limiting" $ do
r <- request methodGet "/items"
(rangeHdrs $ ByteRangeFromTo 0 1) ""
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "0-1/15"
simpleStatus r `shouldBe` partialContent206
+349 -25
View File
@@ -1,46 +1,342 @@
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 Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
import Text.Heredoc
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = around (withApp cfgDefault struct pool) $ 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
spec :: Spec
spec = beforeAll (clearTable "items" >> createItems 15)
. afterAll_ (clearTable "items") . around withApp $ do
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` [str|[{"a":null}]|]
it "matches with like" $ do
get "/simple_pk?k=like.*yx" `shouldRespondWith`
[str|[{"k":"xyyx","extra":"u"}]|]
get "/simple_pk?k=like.xy*" `shouldRespondWith`
[str|[{"k":"xyyx","extra":"u"}]|]
get "/simple_pk?k=like.*YY*" `shouldRespondWith`
[str|[{"k":"xYYx","extra":"v"}]|]
it "matches with like using not operator" $
get "/simple_pk?k=not.like.*yx" `shouldRespondWith`
[str|[{"k":"xYYx","extra":"v"}]|]
it "matches with ilike" $ do
get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith`
[str|[{"k":"xyyx","extra":"u"},{"k":"xYYx","extra":"v"}]|]
get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith`
[str|[{"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&order=id.asc" `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 "order by computed column" $
get "/items?order=anti_id.desc" `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`
[str|[{"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"}]}]}]|]
it "matches with @> operator" $
get "/complex_items?select=id&arr_data=@>.{2}" `shouldRespondWith`
[str|[{"id":2},{"id":3}]|]
it "matches with <@ operator" $
get "/complex_items?select=id&arr_data=<@.{1,2,4}" `shouldRespondWith`
[str|[{"id":1},{"id":2}]|]
describe "Shaping response with select parameter" $ do
it "selectStar works in absense of parameter" $
get "/complex_items?id=eq.3" `shouldRespondWith`
[str|[{"id":3,"name":"Three","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3]}]|]
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`
[str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
it "requesting parents and filtering parent columns" $
get "/projects?id=eq.1&select=id, name, clients{id}" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","clients":{"id":1}}]|]
it "rows with missing parents are included" $
get "/projects?id=in.1,5&select=id,clients{id}" `shouldRespondWith`
[str|[{"id":1,"clients":{"id":1}},{"id":5,"clients":null}]|]
it "rows with no children return [] instead of null" $
get "/projects?id=in.5&select=id,tasks{id}" `shouldRespondWith`
[str|[{"id":5,"tasks":[]}]|]
it "requesting children 2 levels" $
get "/clients?id=eq.1&select=id,projects{id,tasks{id}}" `shouldRespondWith`
[str|[{"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`
[str|[{"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":[]}]|]
it "requesting parents and children on views" $
get "/projects_view?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith`
[str|[{"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`
[str|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|]
it "detect relations in views from exposed schema that are based on tables in private schema and have columns renames" $
get "/articles?id=eq.1&select=id,articleStars{users{*}}" `shouldRespondWith`
[str|[{"id":1,"articleStars":[{"users":{"id":1,"name":"Angela Martin"}},{"users":{"id":2,"name":"Michael Scott"}},{"users":{"id":3,"name":"Dwight Schrute"}}]}]|]
it "can select by column name" $
get "/projects?id=in.1,3&select=id,name,client_id,client_id{id,name}" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","client_id":1,"client_id":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client_id":{"id":2,"name":"Apple"}}]|]
it "can select by column name sans id" $
get "/projects?id=in.1,3&select=id,name,client_id,client{id,name}" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]
describe "Plurality singular" $ do
it "will select an existing object" $
request methodGet "/items?id=eq.5" [("Prefer","plurality=singular")] ""
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"id":5} |]
, matchStatus = 200
, matchHeaders = []
}
it "works in the presence of a range header" $
let headers = ("Prefer","plurality=singular") :
rangeHdrs (ByteRangeFromTo 0 9) in
request methodGet "/items" headers ""
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"id":1} |]
, matchStatus = 200
, matchHeaders = []
}
it "will respond with 404 when not found" $
request methodGet "/items?id=eq.9999" [("Prefer","plurality=singular")] ""
`shouldRespondWith` 404
it "can shape plurality singular object routes" $
request methodGet "/projects_view?id=eq.1&select=id,name,clients{*},tasks{id,name}" [("Prefer","plurality=singular")] ""
`shouldRespondWith`
[str|{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}|]
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=asc.id" `shouldRespondWith` 200
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\nxyyx,u\nxYYx,v"
, matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/csv"]
}
describe "Canonical location" $ do
it "Sets Content-Location with alphabetized params" $
@@ -51,10 +347,38 @@ spec = beforeAll (clearTable "items" >> createItems 15)
, 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": {"id": 1, "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| [] |]
it "can filter by properties inside json column using ->>" $
get "/json?data->>id=eq.1" `shouldRespondWith`
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
describe "remote procedure call" $ do
context "a proc that returns a set" $
it "returns proper json" $
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
[json| [ {"id": 3}, {"id":4} ] |]
context "a proc that returns an empty rowset" $
it "returns empty json array" $
post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`
[json| [] |]
context "a proc that returns plain text" $
it "returns proper json" $
post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`
[json| [{"sayhello":"Hello, world"}] |]
+38 -5
View File
@@ -2,21 +2,54 @@ 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
import Hasql as H
import Hasql.Postgres as P
spec :: Spec
spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items")
. around withApp $
import SpecHelper
import PostgREST.Types (DbStructure(..))
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = beforeAll resetDb
. around (withApp cfgDefault struct pool) $
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
+161 -29
View File
@@ -1,41 +1,60 @@
{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
module Feature.StructureSpec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
import Network.HTTP.Types
spec :: Spec
spec = around withApp $ do
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = around (withApp cfgDefault struct pool) $ do
describe "GET /" $ do
it "lists views in schema" $
request methodGet "/" [] ""
`shouldRespondWith` [json| [
{"schema":"1","name":"auto_incrementing_pk","insertable":true}
, {"schema":"1","name":"compound_pk","insertable":true}
, {"schema":"1","name":"has_fk","insertable":true}
, {"schema":"1","name":"items","insertable":true}
, {"schema":"1","name":"menagerie","insertable":true}
, {"schema":"1","name":"no_pk","insertable":true}
, {"schema":"1","name":"simple_pk","insertable":true}
{"schema":"test","name":"articleStars","insertable":true}
, {"schema":"test","name":"articles","insertable":true}
, {"schema":"test","name":"auto_incrementing_pk","insertable":true}
, {"schema":"test","name":"clients","insertable":true}
, {"schema":"test","name":"comments","insertable":true}
, {"schema":"test","name":"complex_items","insertable":true}
, {"schema":"test","name":"compound_pk","insertable":true}
, {"schema":"test","name":"has_count_column","insertable":false}
, {"schema":"test","name":"has_fk","insertable":true}
, {"schema":"test","name":"insertable_view_with_join","insertable":true}
, {"schema":"test","name":"insertonly","insertable":true}
, {"schema":"test","name":"items","insertable":true}
, {"schema":"test","name":"json","insertable":true}
, {"schema":"test","name":"materialized_view","insertable":false}
, {"schema":"test","name":"menagerie","insertable":true}
, {"schema":"test","name":"no_pk","insertable":true}
, {"schema":"test","name":"nullable_integer","insertable":true}
, {"schema":"test","name":"projects","insertable":true}
, {"schema":"test","name":"projects_view","insertable":true}
, {"schema":"test","name":"simple_pk","insertable":true}
, {"schema":"test","name":"tasks","insertable":true}
, {"schema":"test","name":"tsearch","insertable":true}
, {"schema":"test","name":"users","insertable":true}
, {"schema":"test","name":"users_projects","insertable":true}
, {"schema":"test","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 = authHeader "jdoe" "1234"
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/" [auth] ""
`shouldRespondWith` [json| [
{"schema":"1","name":"authors_only","insertable":true}
{"schema":"test","name":"authors_only","insertable":true}
] |]
{matchStatus = 200}
describe "Table info" $ do
it "is available with OPTIONS verb" $
request methodOptions "/menagerie" [] "" `shouldRespondWith`
@@ -47,7 +66,7 @@ spec = around withApp $ do
"default": null,
"precision": 32,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "integer",
"type": "integer",
"maxLen": null,
@@ -60,7 +79,7 @@ spec = around withApp $ do
"default": null,
"precision": 53,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "double",
"type": "double precision",
"maxLen": null,
@@ -72,7 +91,7 @@ spec = around withApp $ do
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "varchar",
"type": "character varying",
"maxLen": null,
@@ -85,7 +104,7 @@ spec = around withApp $ do
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "boolean",
"type": "boolean",
"maxLen": null,
@@ -97,7 +116,7 @@ spec = around withApp $ do
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "date",
"type": "date",
"maxLen": null,
@@ -109,7 +128,7 @@ spec = around withApp $ do
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "money",
"type": "money",
"maxLen": null,
@@ -122,7 +141,7 @@ spec = around withApp $ do
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "enum",
"type": "USER-DEFINED",
"maxLen": null,
@@ -139,19 +158,72 @@ spec = around withApp $ do
}
|]
it "includes foreign key data" $ do
pendingWith "have to resolve issue #107"
it "it includes primary and foreign keys for views" $
request methodOptions "/projects_view" [] "" `shouldRespondWith`
[json|
{
"pkey":[
"id"
],
"columns":[
{
"references":null,
"default":null,
"precision":32,
"updatable":true,
"schema":"test",
"name":"id",
"type":"integer",
"maxLen":null,
"enum":[],
"nullable":true,
"position":1
},
{
"references":null,
"default":null,
"precision":null,
"updatable":true,
"schema":"test",
"name":"name",
"type":"text",
"maxLen":null,
"enum":[],
"nullable":true,
"position":2
},
{
"references": {
"schema":"test",
"column":"id",
"table":"clients"
},
"default":null,
"precision":32,
"updatable":true,
"schema":"test",
"name":"client_id",
"type":"integer",
"maxLen":null,
"enum":[],
"nullable":true,
"position":3
}
]
}
|]
it "includes foreign key data" $
request methodOptions "/has_fk" [] ""
`shouldRespondWith` [json|
{
"pkey": ["id"],
"columns":[
{
"default": "nextval('\"1\".has_fk_id_seq'::regclass)",
"default": "nextval('test.has_fk_id_seq'::regclass)",
"precision": 64,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "id",
"type": "bigint",
"maxLen": null,
@@ -163,27 +235,87 @@ spec = around withApp $ do
"default": null,
"precision": 32,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "auto_inc_fk",
"type": "integer",
"maxLen": null,
"nullable": true,
"position": 2,
"enum": [],
"references": {"table": "auto_incrementing_pk", "column": "id"}
"references": {"schema":"test", "table": "auto_incrementing_pk", "column": "id"}
}, {
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "simple_fk",
"type": "character varying",
"maxLen": 255,
"nullable": true,
"position": 3,
"enum": [],
"references": {"table": "simple_pk", "column": "k"}
"references": {"schema":"test", "table": "simple_pk", "column": "k"}
}
]
}
|]
it "includes all information on views for renamed columns, and raises relations to correct schema" $
request methodOptions "/articleStars" [] ""
`shouldRespondWith` [json|
{
"pkey": [
"articleId",
"userId"
],
"columns": [
{
"references": {
"schema": "test",
"column": "id",
"table": "articles"
},
"default": null,
"precision": 32,
"updatable": true,
"schema": "test",
"name": "articleId",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": true,
"position": 1
},
{
"references": {
"schema": "test",
"column": "id",
"table": "users"
},
"default": null,
"precision": 32,
"updatable": true,
"schema": "test",
"name": "userId",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": true,
"position": 2
},
{
"references": null,
"default": null,
"precision": null,
"updatable": true,
"schema": "test",
"name": "createdAt",
"type": "timestamp without time zone",
"maxLen": null,
"enum": [],
"nullable": true,
"position": 3
}
]
}
|]
+31 -3
View File
@@ -1,9 +1,37 @@
{-# LANGUAGE QuasiQuotes #-}
module Main where
import Test.Hspec
import SpecHelper
import Spec
--import PostgREST.Types (DbStructure(..))
import qualified Feature.AuthSpec
import qualified Feature.CorsSpec
import qualified Feature.DeleteSpec
import qualified Feature.InsertSpec
import qualified Feature.QueryLimitedSpec
import qualified Feature.QuerySpec
import qualified Feature.RangeSpec
import qualified Feature.StructureSpec
main :: IO ()
main = resetDb >> hspec spec
main = do
setupDb
pool <- specDbPool
dbStructure <- specDbStructure pool
-- Not using hspec-discover because we want to precompute
-- the db structure and pass it to specs for speed
hspec $ specs dbStructure pool
where
specs dbStructure pool = do
describe "Feature.AuthSpec" $ Feature.AuthSpec.spec dbStructure pool
describe "Feature.CorsSpec" $ Feature.CorsSpec.spec dbStructure pool
describe "Feature.DeleteSpec" $ Feature.DeleteSpec.spec dbStructure pool
describe "Feature.InsertSpec" $ Feature.InsertSpec.spec dbStructure pool
describe "Feature.QueryLimitedSpec" $ Feature.QueryLimitedSpec.spec dbStructure pool
describe "Feature.QuerySpec" $ Feature.QuerySpec.spec dbStructure pool
describe "Feature.RangeSpec" $ Feature.RangeSpec.spec dbStructure pool
describe "Feature.StructureSpec" $ Feature.StructureSpec.spec dbStructure pool
-1
View File
@@ -1 +0,0 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --no-main #-}
+68 -57
View File
@@ -1,5 +1,3 @@
{-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
module SpecHelper where
import Network.Wai
@@ -7,78 +5,93 @@ import Test.Hspec
import Test.Hspec.Wai
import Hasql as H
import Hasql.Postgres as H
import Hasql.Backend as B
import Hasql.Postgres as P
import Data.String.Conversions (cs)
import Data.Monoid
-- import Control.Exception.Base (bracket, finally)
import Data.Text hiding (map)
import qualified Data.Vector as V
import Data.Time.Clock.POSIX (getPOSIXTime)
import Control.Monad (void)
import Control.Exception
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 Web.JWT (secret)
import App (app, sqlError, isSqlError)
import Config (AppConfig(..), corsPolicy)
import Middleware
-- import Auth (addUser)
import PostgREST.App (app)
import PostgREST.Config (AppConfig(..))
import PostgREST.Middleware
import PostgREST.Error(pgErrResponse)
import PostgREST.DbStructure
import PostgREST.Types
isLeft :: Either a b -> Bool
isLeft (Left _ ) = True
isLeft _ = False
dbString :: String
dbString = "postgres://postgrest_test_authenticator@localhost:5432/postgrest_test"
cfg :: AppConfig
cfg = AppConfig "postgrest_test" 5432 "postgrest_test" "" "localhost" 3000 "postgrest_anonymous" False 10
cfg :: String -> Maybe Int -> AppConfig
cfg conStr = AppConfig conStr 3000 "postgrest_test_anonymous" "test" (secret "safe") 10
testSettings :: SessionSettings
testSettings = fromMaybe (error "bad settings") $ H.sessionSettings 1 30
cfgDefault :: AppConfig
cfgDefault = cfg dbString Nothing
pgSettings :: Postgres
pgSettings = H.ParamSettings "localhost" 5432 "postgrest_test" "" "postgrest_test"
cfgLimitRows :: Int -> AppConfig
cfgLimitRows = cfg dbString . Just
withApp :: ActionWith Application -> IO ()
withApp perform =
let anonRole = cs $ configAnonRole cfg
currRole = cs $ configDbUser cfg in
perform $ middle $ \req resp ->
H.session pgSettings testSettings $ H.sessionUnlifter >>= \unlift ->
liftIO $ do
body <- strictRequestBody req
resp =<< catchJust isSqlError
(unlift $ H.tx Nothing
$ authenticated currRole anonRole (app body) req)
(return . sqlError)
testPoolOpts :: PoolSettings
testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30
where middle = cors corsPolicy
pgSettings :: P.Settings
pgSettings = P.StringSettings $ cs dbString
specDbPool :: IO (H.Pool P.Postgres)
specDbPool = H.acquirePool pgSettings testPoolOpts
resetDb :: IO ()
resetDb = do
H.session pgSettings testSettings $
H.tx Nothing $ do
H.unit [H.q| drop schema if exists "1" cascade |]
H.unit [H.q| drop schema if exists private cascade |]
H.unit [H.q| drop schema if exists postgrest cascade |]
specDbStructure :: H.Pool P.Postgres -> IO DbStructure
specDbStructure pool = do
dbOrError <- H.session pool $ H.tx specTxSettings
$ getDbStructure "test"
either (fail . show) return dbOrError
withApp :: AppConfig -> DbStructure -> H.Pool P.Postgres
-> ActionWith Application -> IO ()
withApp config dbStructure pool perform = do
perform $ middle $ \req resp -> do
time <- getPOSIXTime
body <- strictRequestBody req
result <- liftIO $ H.session pool $ H.tx specTxSettings
$ runWithClaims config time (app dbStructure config body) req
either (resp . pgErrResponse) resp result
where middle = defaultMiddle
setupDb :: IO ()
setupDb = do
void $ readProcess "psql" ["-d", "postgres", "-a", "-f", "test/fixtures/database.sql"] []
loadFixture "roles"
loadFixture "schema"
loadFixture "privileges"
resetDb
resetDb :: IO ()
resetDb = loadFixture "data"
loadFixture :: FilePath -> IO()
loadFixture name =
void $ readProcess "psql" ["-U", "postgrest_test", "-d", "postgrest_test", "-a", "-f", "test/fixtures/" ++ name ++ ".sql"] []
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")
@@ -86,24 +99,22 @@ matchHeader :: CI BS.ByteString -> String -> [Header] -> Bool
matchHeader name valRegex headers =
maybe False (=~ valRegex) $ lookup name headers
authHeader :: String -> String -> Header
authHeader u p =
authHeaderBasic :: String -> String -> Header
authHeaderBasic u p =
(hAuthorization, cs $ "Basic " ++ encode (u ++ ":" ++ p))
clearTable :: BS.ByteString -> IO ()
clearTable table = H.session pgSettings testSettings $ H.tx Nothing $
H.unit ("delete from \"1\"."<>table, [], True)
authHeaderJWT :: String -> Header
authHeaderJWT token =
(hAuthorization, cs $ "Bearer " ++ token)
createItems :: Int -> IO ()
createItems n = H.session pgSettings testSettings $ H.tx Nothing txn
where
txn = sequence_ $ map H.unit stmts
stmts = map [H.q|insert into "1".items (id) values (?)|] [1..n]
testPool :: IO(H.Pool P.Postgres)
testPool = H.acquirePool pgSettings testPoolOpts
-- for hspec-wai
pending_ :: WaiSession ()
pending_ = liftIO Test.Hspec.pending
clearTable :: Text -> IO ()
clearTable table = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $ B.Stmt ("truncate table test." <> table <> " cascade") V.empty True
-- for hspec-wai
pendingWith_ :: String -> WaiSession ()
pendingWith_ = liftIO . Test.Hspec.pendingWith
specTxSettings :: Maybe (TxIsolationLevel, Maybe Bool)
specTxSettings = Just (H.ReadCommitted, Just True)
+3 -1
View File
@@ -8,9 +8,11 @@ module TestTypes (
import qualified Data.Aeson as JSON
import Data.Aeson ((.:))
-- import Data.Maybe (fromJust)
import Control.Applicative ((<$>), (<*>))
import Control.Applicative
import Control.Monad (mzero)
import Prelude
data IncPK = IncPK {
incId :: Int
, incNullableStr :: Maybe String
@@ -1,7 +1,7 @@
module Unit.PgStructureSpec where
module Unit.DbStructureSpec where
import Test.Hspec
import PgStructure (Table(..), tables, Column(..), columns, ForeignKey(..),
import DbStructure (Table(..), tables, Column(..), columns, ForeignKey(..),
foreignKeys)
import Database.HDBC (quickQuery)
@@ -12,25 +12,25 @@ spec :: Spec
spec = around dbWithSchema $ beforeWith setRole $ do
describe "tables" $
it "shows all the tables" $ \conn -> do
ts <- tables "1" conn
ts <- tables "test" 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
cs <- columns "1" "auto_incrementing_pk" conn
cs <- columns "test" "auto_incrementing_pk" conn
map colName cs `shouldBe` ["id","nullable_string","non_nullable_string",
"inserted_at"]
it "includes foreign key data" $ \conn -> do
cs <- columns "1" "has_fk" conn
cs <- columns "test" "has_fk" conn
map colFK cs `shouldBe` [Nothing,
Just $ ForeignKey "auto_incrementing_pk" "id",
Just $ ForeignKey "simple_pk" "k"]
describe "foreignKeys" $
it "has a description of the foreign key columns" $ \conn ->
foreignKeys "1" "has_fk" conn `shouldReturn` M.fromList [
foreignKeys "test" "has_fk" conn `shouldReturn` M.fromList [
("auto_inc_fk", ForeignKey {fkTable="auto_incrementing_pk", fkCol="id"}),
("simple_fk", ForeignKey { fkTable="simple_pk", fkCol="k"})]
-41
View File
@@ -1,41 +0,0 @@
module Unit.ErrorsSpec where
import Test.Hspec
import Text.Parsec
import PgError
import Data.Either (rights)
spec :: Spec
spec =
describe "Parsing Hasql errors" $ do
it "can handle status and code" $
let p = parse message "" "Status: \"foo\"; Code: \"abc\"." in
rights [p] `shouldBe` [
Message (Just "foo") "abc" Nothing Nothing
]
it "can handle weird redundant quotes in status" $
let p = parse message "" "Status: \"\"foo\"\"; Code: \"abc\"." in
rights [p] `shouldBe` [
Message (Just "foo") "abc" Nothing Nothing
]
it "can handle text and code" $
let p = parse message "" "Message: \"foo\"; Code: \"abc\"." in
rights [p] `shouldBe` [
Message Nothing "abc" (Just "foo") Nothing
]
it "can handle status, text and code" $
let p = parse message "" "Status: \"hi\"; Message: \"foo\"; Code: \"abc\"." in
rights [p] `shouldBe` [
Message (Just "hi") "abc" (Just "foo") Nothing
]
it "can handle unescaped quotes in message" $
let p = parse message "" "Status: \"hi\"; Message: \"unknown \"foo\"!\"; Code: \"abc\"." in
rights [p] `shouldBe` [
Message (Just "hi") "abc" (Just "unknown \"foo\"!") Nothing
]
it "can handle periods in message" $
let p = parse message "" "Message: \"unknown \"foo\".bar\"; Code: \"42P01\"." in
rights [p] `shouldBe` [
Message Nothing "42P01" (Just "unknown \"foo\".bar") Nothing
]
+7 -7
View File
@@ -32,7 +32,7 @@ spec = around dbWithSchema $ do
describe "insert" $
describe "with an auto-increment key" $ do
it "inserts and responds with a full object description" $ \conn -> do
r <- insert "1" "auto_incrementing_pk" (SqlRow [
r <- insert "test" "auto_incrementing_pk" (SqlRow [
("non_nullable_string", toSql ("a string"::String))]) conn
let returnRow = incFromList . toList $ r
incStr returnRow `shouldBe` "a string"
@@ -43,24 +43,24 @@ spec = around dbWithSchema $ do
[returnRow] `shouldBe` map incFromList tRows
it "throws an exception if the PK is not unique" $ \conn -> do
r <- insert "1" "auto_incrementing_pk" (SqlRow [
r <- insert "test" "auto_incrementing_pk" (SqlRow [
("non_nullable_string", toSql ("a string"::String))]) conn
let row = SqlRow . map (Control.Arrow.first cs) . toList $ r
insert "1" "auto_incrementing_pk" row conn `shouldThrow` \e ->
insert "test" "auto_incrementing_pk" row conn `shouldThrow` \e ->
seState e == "23505" -- uniqueness violation code
it "throws an exception if a required value is missing" $ \conn ->
insert "1" "auto_incrementing_pk" (SqlRow [
insert "test" "auto_incrementing_pk" (SqlRow [
("nullable_string", toSql ("a string"::String))]) conn
`shouldThrow` \e -> seState e == "23502"
it "generates a default values query if no data is provided" $ \c -> do
r <- insert "1" "items" (SqlRow []) c
r <- insert "test" "items" (SqlRow []) c
let [row] = toList r
quickALQuery c "select * from \"1\".items where id = ?" [snd row]
`shouldReturn` [[row]]
let {user = "jdoe"; pass = "secret"; role = "test_default_role"}
let {user = "jdoe"; pass = "secret"; role = "postgrest_test_default_role"}
describe "addUser" $ do
it "adds a correct user to the right table" $ \conn -> do
addUser user pass role conn
@@ -79,7 +79,7 @@ spec = around dbWithSchema $ do
addUser user pass role conn
return conn) $ do
it "accepts correct credentials and return the role" $ \conn ->
signInRole user pass conn `shouldReturn` LoginSuccess role
signInRole user pass conn `shouldReturn` LoginSuccess role user
it "returns nothing with bad creds" $ \conn -> do
signInRole "not-a-user" pass conn `shouldReturn` LoginFailed
+266
View File
@@ -0,0 +1,266 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 9.5beta1
-- Dumped by pg_dump version 9.5beta1
SET statement_timeout = 0;
SET lock_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;
SET search_path = postgrest, pg_catalog;
--
-- Data for Name: auth; Type: TABLE DATA; Schema: postgrest; Owner: -
--
TRUNCATE TABLE auth CASCADE;
INSERT INTO auth VALUES ('jdoe', 'postgrest_test_author', '1234 ');
SET search_path = private, pg_catalog;
--
-- Data for Name: articles; Type: TABLE DATA; Schema: private; Owner: -
--
TRUNCATE TABLE articles CASCADE;
INSERT INTO articles VALUES (1, 'No… It''s a thing; it''s like a plan, but with more greatness.', 'diogo');
INSERT INTO articles VALUES (2, 'Stop talking, brain thinking. Hush.', 'diogo');
INSERT INTO articles VALUES (3, 'It''s a fez. I wear a fez now. Fezes are cool.', 'diogo');
SET search_path = test, pg_catalog;
--
-- Data for Name: users; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE users CASCADE;
INSERT INTO users VALUES (1, 'Angela Martin');
INSERT INTO users VALUES (2, 'Michael Scott');
INSERT INTO users VALUES (3, 'Dwight Schrute');
SET search_path = private, pg_catalog;
--
-- Data for Name: article_stars; Type: TABLE DATA; Schema: private; Owner: -
--
TRUNCATE TABLE article_stars CASCADE;
INSERT INTO article_stars VALUES (1, 1, '2015-12-08 04:22:57.472738');
INSERT INTO article_stars VALUES (1, 2, '2015-12-08 04:22:57.472738');
INSERT INTO article_stars VALUES (2, 3, '2015-12-08 04:22:57.472738');
INSERT INTO article_stars VALUES (3, 2, '2015-12-08 04:22:57.472738');
INSERT INTO article_stars VALUES (1, 3, '2015-12-08 04:22:57.472738');
SET search_path = test, pg_catalog;
--
-- Data for Name: authors_only; Type: TABLE DATA; Schema: test; Owner: -
--
--
-- Data for Name: auto_incrementing_pk; Type: TABLE DATA; Schema: test; Owner: -
--
--
-- Name: auto_incrementing_pk_id_seq; Type: SEQUENCE SET; Schema: test; Owner: -
--
SELECT pg_catalog.setval('auto_incrementing_pk_id_seq', 1, true);
--
-- Data for Name: clients; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE clients CASCADE;
INSERT INTO clients VALUES (1, 'Microsoft');
INSERT INTO clients VALUES (2, 'Apple');
--
-- Data for Name: projects; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE projects CASCADE;
INSERT INTO projects VALUES (1, 'Windows 7', 1);
INSERT INTO projects VALUES (2, 'Windows 10', 1);
INSERT INTO projects VALUES (3, 'IOS', 2);
INSERT INTO projects VALUES (4, 'OSX', 2);
INSERT INTO projects VALUES (5, 'Orphan', NULL);
--
-- Data for Name: tasks; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE tasks CASCADE;
INSERT INTO tasks VALUES (1, 'Design w7', 1);
INSERT INTO tasks VALUES (2, 'Code w7', 1);
INSERT INTO tasks VALUES (3, 'Design w10', 2);
INSERT INTO tasks VALUES (4, 'Code w10', 2);
INSERT INTO tasks VALUES (5, 'Design IOS', 3);
INSERT INTO tasks VALUES (6, 'Code IOS', 3);
INSERT INTO tasks VALUES (7, 'Design OSX', 4);
INSERT INTO tasks VALUES (8, 'Code OSX', 4);
--
-- Data for Name: users_tasks; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE users_tasks CASCADE;
INSERT INTO users_tasks VALUES (1, 1);
INSERT INTO users_tasks VALUES (1, 2);
INSERT INTO users_tasks VALUES (1, 3);
INSERT INTO users_tasks VALUES (1, 4);
INSERT INTO users_tasks VALUES (2, 5);
INSERT INTO users_tasks VALUES (2, 6);
INSERT INTO users_tasks VALUES (2, 7);
INSERT INTO users_tasks VALUES (3, 1);
INSERT INTO users_tasks VALUES (3, 5);
--
-- Data for Name: comments; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE comments CASCADE;
INSERT INTO comments VALUES (1, 1, 2, 6, 'Needs to be delivered ASAP');
--
-- Data for Name: complex_items; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE complex_items CASCADE;
INSERT INTO complex_items VALUES (1, 'One', '{"foo":{"int":1,"bar":"baz"}}', '{1}');
INSERT INTO complex_items VALUES (2, 'Two', '{"foo":{"int":1,"bar":"baz"}}', '{1,2}');
INSERT INTO complex_items VALUES (3, 'Three', '{"foo":{"int":1,"bar":"baz"}}', '{1,2,3}');
--
-- Data for Name: compound_pk; Type: TABLE DATA; Schema: test; Owner: -
--
--
-- Data for Name: simple_pk; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE simple_pk CASCADE;
INSERT INTO simple_pk VALUES ('xyyx', 'u');
INSERT INTO simple_pk VALUES ('xYYx', 'v');
--
-- Data for Name: has_fk; Type: TABLE DATA; Schema: test; Owner: -
--
--
-- Name: has_fk_id_seq; Type: SEQUENCE SET; Schema: test; Owner: -
--
SELECT pg_catalog.setval('has_fk_id_seq', 1, false);
--
-- Data for Name: items; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE items CASCADE;
INSERT INTO items VALUES (1);
INSERT INTO items VALUES (2);
INSERT INTO items VALUES (3);
INSERT INTO items VALUES (4);
INSERT INTO items VALUES (5);
INSERT INTO items VALUES (6);
INSERT INTO items VALUES (7);
INSERT INTO items VALUES (8);
INSERT INTO items VALUES (9);
INSERT INTO items VALUES (10);
INSERT INTO items VALUES (11);
INSERT INTO items VALUES (12);
INSERT INTO items VALUES (13);
INSERT INTO items VALUES (14);
INSERT INTO items VALUES (15);
--
-- Name: items_id_seq; Type: SEQUENCE SET; Schema: test; Owner: -
--
SELECT pg_catalog.setval('items_id_seq', 1, true);
--
-- Data for Name: json; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE json CASCADE;
INSERT INTO json VALUES ('{"foo":{"bar":"baz"},"id":1}');
--
-- Data for Name: menagerie; Type: TABLE DATA; Schema: test; Owner: -
--
--
-- Data for Name: no_pk; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE no_pk CASCADE;
INSERT INTO no_pk VALUES (NULL, NULL);
INSERT INTO no_pk VALUES ('1', '0');
INSERT INTO no_pk VALUES ('2', '0');
--
-- Data for Name: nullable_integer; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE nullable_integer CASCADE;
INSERT INTO nullable_integer VALUES (NULL);
--
-- Data for Name: tsearch; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE tsearch CASCADE;
INSERT INTO tsearch VALUES ('''bar'':2 ''foo'':1');
INSERT INTO tsearch VALUES ('''baz'':1 ''qux'':2');
--
-- Data for Name: users_projects; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE users_projects CASCADE;
INSERT INTO users_projects VALUES (1, 1);
INSERT INTO users_projects VALUES (1, 2);
INSERT INTO users_projects VALUES (2, 3);
INSERT INTO users_projects VALUES (2, 4);
INSERT INTO users_projects VALUES (3, 1);
INSERT INTO users_projects VALUES (3, 3);
--
-- PostgreSQL database dump complete
--
+4
View File
@@ -0,0 +1,4 @@
DROP DATABASE IF EXISTS postgrest_test;
DROP ROLE IF EXISTS postgrest_test;
CREATE USER postgrest_test createdb createrole;
CREATE DATABASE postgrest_test OWNER postgrest_test;
+46
View File
@@ -0,0 +1,46 @@
-- Privileges for anonymous
GRANT USAGE ON SCHEMA
postgrest
, test
TO postgrest_test_anonymous;
-- Schema test objects
SET search_path = test, pg_catalog;
GRANT ALL ON TABLE
items
, "articleStars"
, articles
, auto_incrementing_pk
, clients
, comments
, complex_items
, compound_pk
, has_count_column
, has_fk
, insertable_view_with_join
, json
, materialized_view
, menagerie
, no_pk
, nullable_integer
, projects
, projects_view
, simple_pk
, tasks
, tsearch
, users
, users_projects
, users_tasks
TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
GRANT USAGE ON SEQUENCE
auto_incrementing_pk_id_seq
, items_id_seq
TO postgrest_test_anonymous;
-- Privileges for non anonymous users
GRANT USAGE ON SCHEMA test TO postgrest_test_author;
GRANT ALL ON TABLE authors_only TO postgrest_test_author;
+6 -15
View File
@@ -1,16 +1,7 @@
create function pg_temp.create_role_if_not_exists(rolename name, opts character varying) RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
IF NOT EXISTS (SELECT * FROM pg_roles WHERE rolname = rolename) THEN
EXECUTE format('CREATE ROLE %I %s', rolename, opts);
RETURN 'CREATE ROLE';
ELSE
RETURN format('ROLE ''%I'' ALREADY EXISTS', rolename);
END IF;
END;
$$;
DROP ROLE IF EXISTS postgrest_test_authenticator, postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author;
CREATE ROLE postgrest_test_authenticator WITH login;
CREATE ROLE postgrest_test_anonymous;
CREATE ROLE postgrest_test_default_role;
CREATE ROLE postgrest_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;
GRANT postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author TO postgrest_test_authenticator;
+575 -499
View File
File diff suppressed because it is too large Load Diff